mirror of
https://github.com/melihaksoy/Android-Kotlin-Modulerized-CleanArchitecture.git
synced 2026-03-17 23:14:07 +01:00
Initial commit
This commit is contained in:
5
core/src/main/AndroidManifest.xml
Normal file
5
core/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.melih.core">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
</manifest>
|
||||
16
core/src/main/kotlin/com/melih/core/actions/Actions.kt
Normal file
16
core/src/main/kotlin/com/melih/core/actions/Actions.kt
Normal file
@@ -0,0 +1,16 @@
|
||||
package com.melih.core.actions
|
||||
|
||||
import android.content.Intent
|
||||
|
||||
const val EXTRA_LAUNCH_ID = "extras:detail:launchid"
|
||||
|
||||
/**
|
||||
* Navigation actions for navigation between feature activities
|
||||
*/
|
||||
object Actions {
|
||||
|
||||
fun openDetailFor(id: Long) =
|
||||
Intent("action.dashboard.open")
|
||||
.putExtra(EXTRA_LAUNCH_ID, id)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.melih.core.base.lifecycle
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.navigation.ui.NavigationUI
|
||||
import dagger.android.support.DaggerAppCompatActivity
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
||||
const val NAV_HOST_FRAGMENT_TAG = "nav_host_fragment_tag"
|
||||
|
||||
/**
|
||||
* Base class of all Activity classes
|
||||
*/
|
||||
abstract class BaseActivity<T : ViewDataBinding> : DaggerAppCompatActivity() {
|
||||
|
||||
protected lateinit var binding: T
|
||||
protected lateinit var navHostFragment: NavHostFragment
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = DataBindingUtil.setContentView(this, getLayoutId())
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
navHostFragment = createNavHostFragment()
|
||||
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.add(addNavHostTo(), navHostFragment, NAV_HOST_FRAGMENT_TAG)
|
||||
.commitNow()
|
||||
} else {
|
||||
navHostFragment = supportFragmentManager
|
||||
.findFragmentByTag(NAV_HOST_FRAGMENT_TAG) as NavHostFragment
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
if (!NavigationUI.navigateUp(navHostFragment.navController, null)) {
|
||||
onBackPressed()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@LayoutRes
|
||||
abstract fun getLayoutId(): Int
|
||||
|
||||
abstract fun createNavHostFragment(): NavHostFragment
|
||||
|
||||
@IdRes
|
||||
abstract fun addNavHostTo(): Int
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.melih.core.base.lifecycle
|
||||
|
||||
import android.content.Context
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.melih.core.di.ViewModelFactory
|
||||
import dagger.android.AndroidInjector
|
||||
import dagger.android.DispatchingAndroidInjector
|
||||
import dagger.android.support.AndroidSupportInjection
|
||||
import dagger.android.support.HasSupportFragmentInjector
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
/**
|
||||
* Parent of fragments which has injections. Aim is to seperate [BaseFragment] functionality for fragments which
|
||||
* won't need any injection.
|
||||
*
|
||||
* Note that fragments that extends from [BaseDaggerFragment] should contribute android injector.
|
||||
*
|
||||
* This class provides [viewModelFactory] which serves as factory for view models
|
||||
* in the project. It's injected by map of view models that this app is serving. Check [ViewModelFactory]
|
||||
* to see how it works.
|
||||
*/
|
||||
abstract class BaseDaggerFragment<T : ViewDataBinding> : BaseFragment<T>(), HasSupportFragmentInjector {
|
||||
|
||||
// region Properties
|
||||
|
||||
@get:Inject
|
||||
internal var childFragmentInjector: DispatchingAndroidInjector<Fragment>? = null
|
||||
|
||||
@Inject
|
||||
lateinit var viewModelFactory: ViewModelFactory
|
||||
// endregion
|
||||
|
||||
// region Functions
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
AndroidSupportInjection.inject(this)
|
||||
super.onAttach(context)
|
||||
}
|
||||
|
||||
override fun supportFragmentInjector(): AndroidInjector<Fragment>? = childFragmentInjector
|
||||
// endregion
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.melih.core.base.lifecycle
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
|
||||
/**
|
||||
* Parent of all fragments.
|
||||
*
|
||||
* Purpose of [BaseFragment] is to simplify view creation and provide easy access to fragment's
|
||||
* [navController] and [binding].
|
||||
*/
|
||||
abstract class BaseFragment<T : ViewDataBinding> : Fragment() {
|
||||
|
||||
// region Properties
|
||||
|
||||
protected lateinit var navController: NavController
|
||||
protected lateinit var binding: T
|
||||
// endregion
|
||||
|
||||
// region Functions
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
navController = NavHostFragment.findNavController(this)
|
||||
binding = DataBindingUtil.inflate(inflater, getLayoutId(), container, false)
|
||||
binding.lifecycleOwner = this
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@LayoutRes
|
||||
abstract fun getLayoutId(): Int
|
||||
// endregion
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.melih.core.base.recycler
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
/**
|
||||
* Base adapter to reduce boilerplate on creating / binding view holders.
|
||||
*
|
||||
*
|
||||
*/
|
||||
abstract class BaseListAdapter<T>(
|
||||
callback: DiffUtil.ItemCallback<T>,
|
||||
private val clickListener: (T) -> Unit
|
||||
) : ListAdapter<T, BaseViewHolder<T>>(callback) {
|
||||
|
||||
private var itemClickListener: ((T) -> Unit)? = null
|
||||
|
||||
/**
|
||||
* This method will be called to create view holder to obfuscate layout inflation creation / process
|
||||
*
|
||||
* @param inflater layout inflator
|
||||
* @param parent parent view group
|
||||
* @param viewType viewType of holder
|
||||
*/
|
||||
abstract fun createViewHolder(
|
||||
inflater: LayoutInflater,
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): BaseViewHolder<T>
|
||||
|
||||
/**
|
||||
* [createViewHolder] will provide holders, no need to override this
|
||||
*/
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<T> =
|
||||
createViewHolder(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
viewType
|
||||
)
|
||||
|
||||
/**
|
||||
* Calls [bind][BaseViewHolder.bind] on view holders
|
||||
*/
|
||||
override fun onBindViewHolder(holder: BaseViewHolder<T>, position: Int) {
|
||||
val item = getItem(position)
|
||||
|
||||
holder.itemView.setOnClickListener {
|
||||
clickListener(item)
|
||||
}
|
||||
|
||||
holder.bind(item)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base view holder takes view data binding
|
||||
*/
|
||||
abstract class BaseViewHolder<T>(binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
/**
|
||||
* Items are delivered to [bind] via [BaseListAdapter.onBindViewHolder]
|
||||
*
|
||||
* @param item entity
|
||||
* @param position position from adapter
|
||||
*/
|
||||
abstract fun bind(item: T)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.melih.core.base.viewmodel
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.melih.repository.interactors.base.Reason
|
||||
import com.melih.repository.interactors.base.Result
|
||||
|
||||
/**
|
||||
* Base [ViewModel] for view models that will process data.
|
||||
*
|
||||
* This view model provides state & error with [stateData] & [errorData] respectively.
|
||||
*/
|
||||
abstract class BaseViewModel<T> : ViewModel() {
|
||||
|
||||
// region Abstractions
|
||||
|
||||
abstract fun loadData()
|
||||
// endregion
|
||||
|
||||
// region Properties
|
||||
|
||||
private val _successData = MutableLiveData<T>()
|
||||
private val _stateData = MutableLiveData<Result.State>()
|
||||
private val _errorData = MutableLiveData<Reason>()
|
||||
|
||||
/**
|
||||
* Observe [successData] to get notified of data if it's successfuly fetched
|
||||
*/
|
||||
val successData: LiveData<T>
|
||||
get() = _successData
|
||||
|
||||
/**
|
||||
* Observe [stateData] to get notified of state of data
|
||||
*/
|
||||
val stateData: LiveData<Result.State>
|
||||
get() = _stateData
|
||||
|
||||
/**
|
||||
* Observe [errorData] to get notified if an error occurs
|
||||
*/
|
||||
val errorData: LiveData<Reason>
|
||||
get() = _errorData
|
||||
// endregion
|
||||
|
||||
// region Functions
|
||||
|
||||
/**
|
||||
* Default success handler which assigns given [data] to [successData]
|
||||
*
|
||||
* @param data success data
|
||||
*/
|
||||
protected fun handleSuccess(data: T) {
|
||||
_successData.value = data
|
||||
}
|
||||
|
||||
/**
|
||||
* Default state handler which assigns given [state] to [stateData]
|
||||
*
|
||||
* @param state state of operation
|
||||
*/
|
||||
protected fun handleState(state: Result.State) {
|
||||
_stateData.value = state
|
||||
}
|
||||
|
||||
/**
|
||||
* Default error handler which assign received [error] to [errorData]
|
||||
*
|
||||
* @param error check [Error] class for possible error types
|
||||
*/
|
||||
protected fun handleFailure(reason: Reason) {
|
||||
_errorData.value = reason
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload data
|
||||
*/
|
||||
fun refresh() {
|
||||
loadData()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry loading data, incase there's difference between refresh and retry, should go here
|
||||
*/
|
||||
fun retry() {
|
||||
loadData()
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
22
core/src/main/kotlin/com/melih/core/di/CoreComponent.kt
Normal file
22
core/src/main/kotlin/com/melih/core/di/CoreComponent.kt
Normal file
@@ -0,0 +1,22 @@
|
||||
package com.melih.core.di
|
||||
|
||||
import android.app.Application
|
||||
import android.net.NetworkInfo
|
||||
import com.melih.repository.persistence.LaunchesDatabase
|
||||
import dagger.BindsInstance
|
||||
import dagger.Component
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
@Component(modules = [CoreModule::class])
|
||||
interface CoreComponent {
|
||||
|
||||
fun getNetworkInfo(): NetworkInfo?
|
||||
|
||||
fun getLaunchesDatabase(): LaunchesDatabase
|
||||
|
||||
@Component.Factory
|
||||
interface Factory {
|
||||
fun create(@BindsInstance app: Application): CoreComponent
|
||||
}
|
||||
}
|
||||
26
core/src/main/kotlin/com/melih/core/di/CoreModule.kt
Normal file
26
core/src/main/kotlin/com/melih/core/di/CoreModule.kt
Normal file
@@ -0,0 +1,26 @@
|
||||
package com.melih.core.di
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkInfo
|
||||
import androidx.room.Room
|
||||
import com.melih.repository.persistence.DB_NAME
|
||||
import com.melih.repository.persistence.LaunchesDatabase
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
class CoreModule {
|
||||
|
||||
@Provides
|
||||
fun provideNetworkInfo(app: Application): NetworkInfo? =
|
||||
(app.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).activeNetworkInfo
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideLaunchesDatabase(app: Application) =
|
||||
Room.databaseBuilder(app.applicationContext, LaunchesDatabase::class.java, DB_NAME)
|
||||
.build()
|
||||
}
|
||||
30
core/src/main/kotlin/com/melih/core/di/ViewModelFactory.kt
Normal file
30
core/src/main/kotlin/com/melih/core/di/ViewModelFactory.kt
Normal file
@@ -0,0 +1,30 @@
|
||||
package com.melih.core.di
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
/**
|
||||
* [Factory][ViewModelProvider.Factory] that provides view models allowing injection. [viewModelMap] is provided via dagger
|
||||
* injection. To be able to inject a view model, it must be bound to map via [dagger.Binds] [dagger.multibindings.IntoMap]
|
||||
* by using [ViewModelKey][com.melih.core.di.keys.ViewModelKey].
|
||||
*
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class ViewModelFactory @Inject constructor(
|
||||
private val viewModelMap: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
val viewModelProvider: Provider<ViewModel> = viewModelMap[modelClass]
|
||||
?: throw IllegalArgumentException("Unknown ViewModel")
|
||||
|
||||
return try {
|
||||
viewModelProvider.get() as T
|
||||
?: throw IllegalArgumentException("Provider's contained value is null")
|
||||
} catch (e: ClassCastException) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.melih.core.di.keys
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.MapKey
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
@MapKey
|
||||
@Target(AnnotationTarget.FUNCTION)
|
||||
annotation class ViewModelKey(val value: KClass<out ViewModel>)
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.melih.core.extensions
|
||||
|
||||
import android.widget.ImageView
|
||||
import androidx.databinding.BindingAdapter
|
||||
import com.squareup.picasso.Picasso
|
||||
|
||||
/**
|
||||
* Loads image in given [url] to this [ImageView]
|
||||
*
|
||||
* @param url url of image
|
||||
*/
|
||||
@BindingAdapter("imageUrl")
|
||||
fun ImageView.loadImage(url: String?) {
|
||||
if (!url.isNullOrBlank()) {
|
||||
Picasso.get()
|
||||
.load(url)
|
||||
.into(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.melih.core.extensions
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
|
||||
/**
|
||||
* Reduces required boilerplate code to observe a live data
|
||||
*
|
||||
* @param data [LiveData] to observe
|
||||
* @param block receive and process data
|
||||
*/
|
||||
fun <T> Fragment.observe(data: LiveData<T>, block: (T) -> Unit) {
|
||||
data.observe(this, Observer(block))
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for getting viewModel from factory and run a block over it if required for easy access
|
||||
*
|
||||
* crossinline for unwanted returns
|
||||
*/
|
||||
inline fun <reified T : ViewModel> ViewModelProvider.Factory.createFor(
|
||||
fragment: Fragment,
|
||||
crossinline block: T.() -> Unit = {}
|
||||
): T {
|
||||
val viewModel = ViewModelProviders.of(fragment, this)[T::class.java]
|
||||
viewModel.apply(block)
|
||||
return viewModel
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.melih.core.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
|
||||
class SnackbarBehaviour constructor(
|
||||
context: Context,
|
||||
attributeSet: AttributeSet
|
||||
) : CoordinatorLayout.Behavior<SwipeRefreshLayout>() {
|
||||
|
||||
override fun layoutDependsOn(parent: CoordinatorLayout, child: SwipeRefreshLayout, dependency: View): Boolean =
|
||||
dependency is Snackbar.SnackbarLayout
|
||||
|
||||
override fun onDependentViewChanged(parent: CoordinatorLayout, child: SwipeRefreshLayout, dependency: View): Boolean {
|
||||
val translationY = Math.min(0.0f, (dependency.translationY - dependency.height))
|
||||
child.translationY = translationY
|
||||
return true
|
||||
}
|
||||
}
|
||||
7
core/src/main/res/values/colors.xml
Normal file
7
core/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorPrimary">#008577</color>
|
||||
<color name="colorPrimaryDark">#00574B</color>
|
||||
<color name="colorAccent">#D81B60</color>
|
||||
<color name="lightGray">#8F8F8F</color>
|
||||
</resources>
|
||||
5
core/src/main/res/values/dimens.xml
Normal file
5
core/src/main/res/values/dimens.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="padding_standard">8dp</dimen>
|
||||
<dimen name="corner_radius_standard">11dp</dimen>
|
||||
</resources>
|
||||
15
core/src/main/res/values/strings.xml
Normal file
15
core/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<resources>
|
||||
<string name="dummy_long_text">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
|
||||
incididunt ut labore et dolore
|
||||
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
|
||||
consequat. Duis aute irure dolor in
|
||||
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat
|
||||
non proident, sunt in culpa qui officia
|
||||
deserunt mollit anim id est laborum
|
||||
</string>
|
||||
|
||||
<string name="retry">Retry</string>
|
||||
|
||||
<!--Actions-->
|
||||
<string name="action_detail">action.detail.open</string>
|
||||
</resources>
|
||||
35
core/src/main/res/values/styles.xml
Normal file
35
core/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,35 @@
|
||||
<resources>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
</style>
|
||||
|
||||
<!--Styles-->
|
||||
|
||||
<style name="TitleTextStyle">
|
||||
<item name="android:singleLine">true</item>
|
||||
</style>
|
||||
|
||||
<style name="ShortDescriptionTextStyle">
|
||||
<item name="android:ellipsize">end</item>
|
||||
<item name="android:maxLines">@integer/common_max_lines</item>
|
||||
<item name="android:gravity">center|left</item>
|
||||
</style>
|
||||
|
||||
<style name="DescriptionTextStyle">
|
||||
|
||||
</style>
|
||||
|
||||
<!--Text appearances-->
|
||||
|
||||
<style name="TitleTextAppearance" parent="TextAppearance.AppCompat.Title" />
|
||||
|
||||
<style name="DescriptionTextAppearance" parent="TextAppearance.AppCompat.Body1">
|
||||
<item name="android:textColor">@color/lightGray</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
4
core/src/main/res/values/values.xml
Normal file
4
core/src/main/res/values/values.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<integer name="common_max_lines">5</integer>
|
||||
</resources>
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.melih.core
|
||||
|
||||
import androidx.arch.core.executor.ArchTaskExecutor
|
||||
import androidx.arch.core.executor.TaskExecutor
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.melih.core.observers.OneShotObserverWithLifecycle
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
abstract class BaseTestWithMainThread {
|
||||
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
|
||||
@BeforeEach
|
||||
@ExperimentalCoroutinesApi
|
||||
fun setUp() {
|
||||
Dispatchers.setMain(dispatcher)
|
||||
ArchTaskExecutor.getInstance()
|
||||
.setDelegate(object : TaskExecutor() {
|
||||
override fun executeOnDiskIO(runnable: Runnable) = runnable.run()
|
||||
|
||||
override fun isMainThread(): Boolean = true
|
||||
|
||||
override fun postToMainThread(runnable: Runnable) = runnable.run()
|
||||
})
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
@ExperimentalCoroutinesApi
|
||||
fun tearDown() {
|
||||
Dispatchers.resetMain()
|
||||
dispatcher.close()
|
||||
ArchTaskExecutor.getInstance()
|
||||
.setDelegate(null)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T> LiveData<T>.testObserve(onChangeHandler: (T) -> Unit) {
|
||||
suspendCoroutine<Unit> {
|
||||
val observer = OneShotObserverWithLifecycle(onChangeHandler, it)
|
||||
observe(observer, observer)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.melih.core.base
|
||||
|
||||
import com.melih.core.base.viewmodel.BaseViewModel
|
||||
import io.mockk.spyk
|
||||
import io.mockk.verify
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class BaseViewModelTest {
|
||||
|
||||
val baseVm = spyk(TestViewModel())
|
||||
|
||||
@Test
|
||||
fun `refresh should invoke loadData`() {
|
||||
baseVm.refresh()
|
||||
|
||||
verify(exactly = 1) { baseVm.loadData() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `retry should invoke loadData`() {
|
||||
baseVm.retry()
|
||||
|
||||
verify(exactly = 1) { baseVm.loadData() }
|
||||
}
|
||||
}
|
||||
|
||||
class TestViewModel : BaseViewModel<Int>() {
|
||||
override public fun loadData() {
|
||||
// no - op
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.melih.core.observers
|
||||
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LifecycleRegistry
|
||||
import androidx.lifecycle.Observer
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
/**
|
||||
* This class is both [Observer] & [LifecycleOwner], used to observe on live data via
|
||||
* [testObserve].
|
||||
*
|
||||
* Taking continuation is due to suspending coroutine, else scope is getting closed right away after
|
||||
* reaching end of suspending job and test is over.
|
||||
*/
|
||||
class OneShotObserverWithLifecycle<T>(
|
||||
val block: (T) -> Unit, val
|
||||
continuation: Continuation<Unit>
|
||||
) : LifecycleOwner, Observer<T> {
|
||||
|
||||
private val lifecycle = LifecycleRegistry(this)
|
||||
|
||||
init {
|
||||
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
}
|
||||
|
||||
override fun getLifecycle(): Lifecycle = lifecycle
|
||||
|
||||
override fun onChanged(t: T) {
|
||||
block(t)
|
||||
continuation.resume(Unit)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user