mirror of
https://github.com/melihaksoy/Android-Kotlin-Modulerized-CleanArchitecture.git
synced 2026-03-19 07:54:28 +01:00
Milestones/ms1 (#16)
* Closes #11 * Closes #13 * Closes #17 * Closes #18 * Closes #19 * Closes #6 * Closes #3 * Closes #12 * Closes #15
This commit is contained in:
committed by
Melih Aksoy
parent
11889446cb
commit
625776609d
@@ -8,7 +8,6 @@ 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"
|
||||
|
||||
@@ -20,7 +19,6 @@ 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)
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.melih.repository.interactors.base.Reason
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
||||
/**
|
||||
* Parent of all fragments.
|
||||
@@ -41,7 +40,6 @@ abstract class BaseFragment<T : ViewDataBinding> : Fragment() {
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
protected fun showSnackbarWithAction(reason: Reason, block: () -> Unit) {
|
||||
Snackbar.make(
|
||||
binding.root,
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
package com.melih.core.base.paging
|
||||
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.paging.PageKeyedDataSource
|
||||
import com.melih.repository.interactors.base.Reason
|
||||
import com.melih.repository.interactors.base.Result
|
||||
import com.melih.repository.interactors.base.State
|
||||
import com.melih.repository.interactors.base.onFailure
|
||||
import com.melih.repository.interactors.base.onState
|
||||
import com.melih.repository.interactors.base.onSuccess
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
const val INITIAL_PAGE = 0
|
||||
|
||||
/**
|
||||
* Base class for all [pageKeyedDataSources][PageKeyedDataSource] in project.
|
||||
*
|
||||
* Purpose of this class is to ease handling of [result][Result]. It overrides [loadInitial], [loadAfter] and [loadBefore]
|
||||
* so sources extends from base does not need to override them, they just need to provide way of loading data by overriding [loadDataForPage].
|
||||
*
|
||||
* [handleState] & [handleFailure] updates corresponding [liveData][LiveData] objects [stateData] & [reasonData],
|
||||
* which can be used with [androidx.lifecycle.Transformations] to observe changes on the source state & error.
|
||||
*
|
||||
* This source has it's own [coroutineScope][CoroutineScope] that's backed up by a [SupervisorJob] to handle networking operations.
|
||||
* It's cancelled automatically when source factory [invalidates][invalidate] the source.
|
||||
*/
|
||||
|
||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||
abstract class BasePagingDataSource<T> : PageKeyedDataSource<Int, T>() {
|
||||
|
||||
// region Abstractions
|
||||
|
||||
abstract fun loadDataForPage(page: Int): Flow<Result<List<T>>> // Load next page(s)
|
||||
// endregion
|
||||
|
||||
// region Properties
|
||||
|
||||
private val _stateData = MutableLiveData<State>()
|
||||
private val _reasonData = MutableLiveData<Reason>()
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
|
||||
/**
|
||||
* Observe [stateData] to get notified of state of data
|
||||
*/
|
||||
val stateData: LiveData<State>
|
||||
get() = _stateData
|
||||
|
||||
/**
|
||||
* Observe [reasonData] to get notified if an error occurs
|
||||
*/
|
||||
val reasonData: LiveData<Reason>
|
||||
get() = _reasonData
|
||||
// endregion
|
||||
|
||||
// region Functions
|
||||
|
||||
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, T>) {
|
||||
// Looping through channel as we'll receive any state, error or data here
|
||||
loadDataForPage(INITIAL_PAGE)
|
||||
.onEach { result ->
|
||||
result.onState(::handleState)
|
||||
.onFailure(::handleFailure)
|
||||
.onSuccess {
|
||||
// When we receive data without any failures, we transform it and return list, also what's the value for next page
|
||||
callback.onResult(
|
||||
it,
|
||||
INITIAL_PAGE,
|
||||
INITIAL_PAGE + 1
|
||||
)
|
||||
}
|
||||
}
|
||||
.launchIn(coroutineScope)
|
||||
}
|
||||
|
||||
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, T>) {
|
||||
// Key for which page to load is in params
|
||||
val page = params.key
|
||||
|
||||
loadDataForPage(page)
|
||||
.onEach { result ->
|
||||
result
|
||||
.onState(::handleState)
|
||||
.onFailure(::handleFailure)
|
||||
.onSuccess {
|
||||
// When we receive data without any failures, we transform it and return list, also what's the value for next page
|
||||
callback.onResult(
|
||||
it,
|
||||
page + 1
|
||||
)
|
||||
}
|
||||
}
|
||||
.launchIn(coroutineScope)
|
||||
}
|
||||
|
||||
/**
|
||||
* This loads previous pages, we don't have a use for it yet, so it's a no-op override
|
||||
*/
|
||||
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, T>) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
/**
|
||||
* Default state handler which assigns given [state] to [stateData]
|
||||
*
|
||||
* @param state state of operation
|
||||
*/
|
||||
@CallSuper
|
||||
protected fun handleState(state: State) {
|
||||
_stateData.value = state
|
||||
}
|
||||
|
||||
/**
|
||||
* Default error handler which assign received [reason] to [reasonData]
|
||||
*
|
||||
* @param reason check [Reason] class for possible error types
|
||||
*/
|
||||
@CallSuper
|
||||
protected fun handleFailure(reason: Reason) {
|
||||
_reasonData.value = reason
|
||||
}
|
||||
|
||||
/**
|
||||
* Canceling [coroutineScope]
|
||||
*/
|
||||
@CallSuper
|
||||
override fun invalidate() {
|
||||
coroutineScope.cancel()
|
||||
super.invalidate()
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.melih.core.base.paging
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.paging.DataSource
|
||||
|
||||
/**
|
||||
* Base [factory][DataSource.Factory] class for any [dataSource][DataSource]s in project.
|
||||
*
|
||||
* It's purpose is to provide latest source so [basePagingViewModel][be.mediahuis.core.base.viewmodel.BasePagingViewModel] can obtain
|
||||
* [stateData][BasePagingDataSource.stateData] and [reasonData][BasePagingDataSource.reasonData] from it.
|
||||
*
|
||||
* This is done under the hood by telling this factory how to create a source by overriding [createSource].
|
||||
*
|
||||
* Purpose of this transmission is to encapuslate [basePagingDataSource][BasePagingDataSource].
|
||||
*/
|
||||
abstract class BasePagingFactory<T> : DataSource.Factory<Int, T>() {
|
||||
|
||||
// region Abstractions
|
||||
|
||||
abstract fun createSource(): BasePagingDataSource<T>
|
||||
// endregion
|
||||
|
||||
// region Properties
|
||||
|
||||
private val _currentSource = MutableLiveData<BasePagingDataSource<T>>()
|
||||
|
||||
val currentSource: LiveData<BasePagingDataSource<T>>
|
||||
get() = _currentSource
|
||||
// endregion
|
||||
|
||||
// region Functions
|
||||
|
||||
override fun create(): DataSource<Int, T> = createSource().apply { _currentSource.postValue(this) }
|
||||
|
||||
/**
|
||||
* Invalidating the [currentSource]
|
||||
* by calling [be.mediahuis.core.base.paging.BasePagingDataSource.invalidate]
|
||||
*/
|
||||
fun invalidateDataSource() = currentSource.value?.invalidate()
|
||||
// endregion
|
||||
}
|
||||
@@ -3,8 +3,8 @@ package com.melih.core.base.recycler
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.paging.PagedListAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
/**
|
||||
@@ -12,10 +12,10 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
*
|
||||
*
|
||||
*/
|
||||
abstract class BaseListAdapter<T>(
|
||||
abstract class BasePagingListAdapter<T>(
|
||||
callback: DiffUtil.ItemCallback<T>,
|
||||
private val clickListener: (T) -> Unit
|
||||
) : ListAdapter<T, BaseViewHolder<T>>(callback) {
|
||||
private val clickListener: (T?) -> Unit
|
||||
) : PagedListAdapter<T, BaseViewHolder<T>>(callback) {
|
||||
|
||||
private var itemClickListener: ((T) -> Unit)? = null
|
||||
|
||||
@@ -48,12 +48,12 @@ abstract class BaseListAdapter<T>(
|
||||
override fun onBindViewHolder(holder: BaseViewHolder<T>, position: Int) {
|
||||
val item = getItem(position)
|
||||
|
||||
|
||||
holder.itemView.setOnClickListener {
|
||||
clickListener(item)
|
||||
}
|
||||
|
||||
holder.bind(item)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,5 +68,5 @@ abstract class BaseViewHolder<T>(binding: ViewDataBinding) : RecyclerView.ViewHo
|
||||
* @param item entity
|
||||
* @param position position from adapter
|
||||
*/
|
||||
abstract fun bind(item: T)
|
||||
abstract fun bind(item: T?)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.melih.core.base.viewmodel
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.paging.PagedList
|
||||
import androidx.paging.toLiveData
|
||||
import com.melih.core.base.paging.BasePagingFactory
|
||||
import com.melih.repository.interactors.base.Reason
|
||||
import com.melih.repository.interactors.base.State
|
||||
|
||||
/**
|
||||
* Base [ViewModel] for view models that will use [PagedList].
|
||||
*
|
||||
* Since data handling is done via [be.mediahuis.core.base.paging.BasePagingDataSource], this view model doesn't need
|
||||
* a [kotlinx.coroutines.channels.ReceiveChannel] and will not provide any default operations of data, but instead will
|
||||
* provde [pagedList] which should be observed and submitted.
|
||||
*
|
||||
* If paging won't be used, use [BaseViewModel] instead.
|
||||
*/
|
||||
abstract class BasePagingViewModel<T> : ViewModel() {
|
||||
|
||||
// region Abstractions
|
||||
|
||||
abstract val factory: BasePagingFactory<T>
|
||||
abstract val config: PagedList.Config
|
||||
// endregion
|
||||
|
||||
// region Properties
|
||||
|
||||
/**
|
||||
* Observe [stateData] to get notified of state of data
|
||||
*/
|
||||
val stateData: LiveData<State> by lazy {
|
||||
Transformations.switchMap(factory.currentSource) {
|
||||
it.stateData
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Observe [errorData] to get notified if an error occurs
|
||||
*/
|
||||
val errorData: LiveData<Reason> by lazy {
|
||||
Transformations.switchMap(factory.currentSource) {
|
||||
it.reasonData
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Observe [pagedList] to submit list it provides
|
||||
*/
|
||||
val pagedList: LiveData<PagedList<T>> by lazy {
|
||||
factory.toLiveData(config)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Functions
|
||||
|
||||
fun refresh() {
|
||||
factory.currentSource.value?.invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry loading data, incase there's difference between refresh and retry, should go here
|
||||
*/
|
||||
fun retry() {
|
||||
factory.currentSource.value?.invalidate()
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
@@ -3,8 +3,10 @@ package com.melih.core.base.viewmodel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.melih.repository.interactors.base.Reason
|
||||
import com.melih.repository.interactors.base.Result
|
||||
import com.melih.repository.interactors.base.State
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Base [ViewModel] for view models that will process data.
|
||||
@@ -15,13 +17,19 @@ abstract class BaseViewModel<T> : ViewModel() {
|
||||
|
||||
// region Abstractions
|
||||
|
||||
abstract fun loadData()
|
||||
abstract suspend fun loadData()
|
||||
// endregion
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
|
||||
// region Properties
|
||||
|
||||
private val _successData = MutableLiveData<T>()
|
||||
private val _stateData = MutableLiveData<Result.State>()
|
||||
private val _stateData = MutableLiveData<State>()
|
||||
private val _errorData = MutableLiveData<Reason>()
|
||||
|
||||
/**
|
||||
@@ -33,7 +41,7 @@ abstract class BaseViewModel<T> : ViewModel() {
|
||||
/**
|
||||
* Observe [stateData] to get notified of state of data
|
||||
*/
|
||||
val stateData: LiveData<Result.State>
|
||||
val stateData: LiveData<State>
|
||||
get() = _stateData
|
||||
|
||||
/**
|
||||
@@ -59,7 +67,7 @@ abstract class BaseViewModel<T> : ViewModel() {
|
||||
*
|
||||
* @param state state of operation
|
||||
*/
|
||||
protected fun handleState(state: Result.State) {
|
||||
protected fun handleState(state: State) {
|
||||
_stateData.value = state
|
||||
}
|
||||
|
||||
@@ -76,14 +84,18 @@ abstract class BaseViewModel<T> : ViewModel() {
|
||||
* Reload data
|
||||
*/
|
||||
fun refresh() {
|
||||
loadData()
|
||||
viewModelScope.launch {
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry loading data, incase there's difference between refresh and retry, should go here
|
||||
*/
|
||||
fun retry() {
|
||||
loadData()
|
||||
viewModelScope.launch {
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.melih.core.di
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.net.NetworkInfo
|
||||
import com.melih.repository.persistence.LaunchesDatabase
|
||||
import dagger.BindsInstance
|
||||
import dagger.Component
|
||||
import javax.inject.Singleton
|
||||
@@ -11,9 +11,9 @@ import javax.inject.Singleton
|
||||
@Component(modules = [CoreModule::class])
|
||||
interface CoreComponent {
|
||||
|
||||
fun getNetworkInfo(): NetworkInfo?
|
||||
fun getAppContext(): Context
|
||||
|
||||
fun getLaunchesDatabase(): LaunchesDatabase
|
||||
fun getNetworkInfo(): NetworkInfo?
|
||||
|
||||
@Component.Factory
|
||||
interface Factory {
|
||||
|
||||
@@ -4,23 +4,16 @@ 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
|
||||
fun proivdeAppContext(app: Application): Context = app.applicationContext
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideLaunchesDatabase(app: Application) =
|
||||
Room.databaseBuilder(app.applicationContext, LaunchesDatabase::class.java, DB_NAME)
|
||||
.build()
|
||||
fun provideNetworkInfo(app: Application): NetworkInfo? =
|
||||
(app.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).activeNetworkInfo
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.melih.core.extensions
|
||||
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import com.melih.core.utils.ClearFocusQueryTextListener
|
||||
|
||||
@@ -8,4 +9,24 @@ import com.melih.core.utils.ClearFocusQueryTextListener
|
||||
*/
|
||||
fun CharSequence.containsIgnoreCase(other: CharSequence) = contains(other, true)
|
||||
|
||||
/**
|
||||
* Adds [ClearFocusQueryTextListener] as [SearchView.OnQueryTextListener]
|
||||
*/
|
||||
fun SearchView.setOnQueryChangedListener(block: (String?) -> Unit) = setOnQueryTextListener(ClearFocusQueryTextListener(this, block))
|
||||
|
||||
/**
|
||||
* Shortening set menu item expands / collapses
|
||||
*/
|
||||
fun MenuItem.onExpandOrCollapse(onExpand: () -> Unit, onCollapse: () -> Unit) {
|
||||
setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
|
||||
onCollapse()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
|
||||
onExpand()
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.google.android.material.snackbar.Snackbar
|
||||
/**
|
||||
* Simple behaviour for pushing views when snackbar is animating so none of views will remain under snackbar
|
||||
*/
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
class SnackbarBehaviour constructor(
|
||||
context: Context,
|
||||
attributeSet: AttributeSet
|
||||
|
||||
@@ -6,19 +6,19 @@ 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.TestCoroutineDispatcher
|
||||
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
|
||||
|
||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||
abstract class BaseTestWithMainThread {
|
||||
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
|
||||
private val dispatcher = TestCoroutineDispatcher()
|
||||
|
||||
@BeforeEach
|
||||
@ExperimentalCoroutinesApi
|
||||
fun setUp() {
|
||||
Dispatchers.setMain(dispatcher)
|
||||
ArchTaskExecutor.getInstance()
|
||||
@@ -32,12 +32,12 @@ abstract class BaseTestWithMainThread {
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
@ExperimentalCoroutinesApi
|
||||
fun tearDown() {
|
||||
Dispatchers.resetMain()
|
||||
dispatcher.close()
|
||||
ArchTaskExecutor.getInstance()
|
||||
.setDelegate(null)
|
||||
|
||||
Dispatchers.resetMain()
|
||||
dispatcher.cleanupTestCoroutines()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,37 +1,32 @@
|
||||
package com.melih.core.base
|
||||
|
||||
import com.melih.core.BaseTestWithMainThread
|
||||
import com.melih.core.base.viewmodel.BaseViewModel
|
||||
import io.mockk.coVerify
|
||||
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())
|
||||
class BaseViewModelTest : BaseTestWithMainThread() {
|
||||
|
||||
@Test
|
||||
fun `refresh should invoke loadData`() {
|
||||
val baseVm = spyk(TestViewModel())
|
||||
baseVm.refresh()
|
||||
|
||||
verify(exactly = 1) { baseVm.loadData() }
|
||||
coVerify(exactly = 1) { baseVm.loadData() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `retry should invoke loadData`() {
|
||||
val baseVm = spyk(TestViewModel())
|
||||
baseVm.retry()
|
||||
|
||||
verify(exactly = 1) { baseVm.loadData() }
|
||||
coVerify(exactly = 1) { baseVm.loadData() }
|
||||
}
|
||||
}
|
||||
|
||||
class TestViewModel : BaseViewModel<Int>() {
|
||||
override public fun loadData() {
|
||||
class TestViewModel : BaseViewModel<Unit>() {
|
||||
override suspend fun loadData() {
|
||||
// no - op
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
@file:UseExperimental(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package com.melih.core.paging
|
||||
|
||||
import androidx.paging.PageKeyedDataSource
|
||||
import com.melih.core.BaseTestWithMainThread
|
||||
import com.melih.core.base.paging.BasePagingDataSource
|
||||
import com.melih.core.testObserve
|
||||
import com.melih.repository.interactors.base.Failure
|
||||
import com.melih.repository.interactors.base.GenericError
|
||||
import com.melih.repository.interactors.base.Result
|
||||
import com.melih.repository.interactors.base.State
|
||||
import com.melih.repository.interactors.base.Success
|
||||
import io.mockk.mockk
|
||||
import io.mockk.spyk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.amshove.kluent.shouldBeInstanceOf
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class BasePagingDataSourceTest : BaseTestWithMainThread() {
|
||||
|
||||
val source = spyk(TestSource())
|
||||
val failureSource = spyk(TestFailureSource())
|
||||
|
||||
val data = 10
|
||||
val errorMessage = "Generic Error"
|
||||
|
||||
@Nested
|
||||
inner class BasePagingSource {
|
||||
|
||||
@Nested
|
||||
inner class LoadInitial {
|
||||
|
||||
@Test
|
||||
|
||||
fun `should update state accordingly`() {
|
||||
val params = mockk<PageKeyedDataSource.LoadInitialParams<Int>>(relaxed = true)
|
||||
val callback = mockk<PageKeyedDataSource.LoadInitialCallback<Int, Int>>(relaxed = true)
|
||||
|
||||
runBlocking {
|
||||
|
||||
// Fake loading
|
||||
source.loadInitial(params, callback)
|
||||
|
||||
source.stateData.testObserve {
|
||||
it shouldBeInstanceOf State.Loading::class
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
fun `should update error Error accordingly`() {
|
||||
val params = PageKeyedDataSource.LoadInitialParams<Int>(10, false)
|
||||
val callback = mockk<PageKeyedDataSource.LoadInitialCallback<Int, Int>>(relaxed = true)
|
||||
|
||||
runBlocking {
|
||||
|
||||
// Fake loading
|
||||
failureSource.loadInitial(params, callback)
|
||||
|
||||
failureSource.reasonData.testObserve {
|
||||
it shouldBeInstanceOf GenericError::class
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class LoadAfter {
|
||||
|
||||
@Test
|
||||
|
||||
fun `should update state accordingly`() {
|
||||
val params = PageKeyedDataSource.LoadParams(2, 10)
|
||||
val callback = mockk<PageKeyedDataSource.LoadCallback<Int, Int>>(relaxed = true)
|
||||
|
||||
runBlocking {
|
||||
|
||||
// Fake loading
|
||||
source.loadAfter(params, callback)
|
||||
|
||||
source.stateData.testObserve {
|
||||
it shouldBeInstanceOf State.Loading::class
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
fun `should update error Error accordingly`() {
|
||||
val params = PageKeyedDataSource.LoadParams(2, 10)
|
||||
val callback = mockk<PageKeyedDataSource.LoadCallback<Int, Int>>(relaxed = true)
|
||||
|
||||
runBlocking {
|
||||
|
||||
// Fake loading
|
||||
failureSource.loadAfter(params, callback)
|
||||
|
||||
failureSource.reasonData.testObserve {
|
||||
it shouldBeInstanceOf GenericError::class
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
fun `should use loadDataForPage in loadInitial and transform emmited value`() {
|
||||
val params = mockk<PageKeyedDataSource.LoadInitialParams<Int>>(relaxed = true)
|
||||
val callback = mockk<PageKeyedDataSource.LoadInitialCallback<Int, Int>>(relaxed = true)
|
||||
|
||||
// Fake loading
|
||||
source.loadInitial(params, callback)
|
||||
|
||||
// Make sure load initial called only once
|
||||
verify(exactly = 1) { source.loadDataForPage(any()) }
|
||||
|
||||
// Notified callback
|
||||
verify(exactly = 1) { callback.onResult(any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
fun `should use loadDataForPage in loadAfter and transform emmited value`() {
|
||||
val params = PageKeyedDataSource.LoadParams(2, 10)
|
||||
val callback = mockk<PageKeyedDataSource.LoadCallback<Int, Int>>(relaxed = true)
|
||||
|
||||
// Fake loading
|
||||
source.loadAfter(params, callback)
|
||||
|
||||
// Make sure load initial called only once
|
||||
verify(exactly = 1) { source.loadDataForPage(any()) }
|
||||
|
||||
// Notified callback
|
||||
verify(exactly = 1) { callback.onResult(any(), any()) }
|
||||
}
|
||||
}
|
||||
|
||||
inner class TestSource : BasePagingDataSource<Int>() {
|
||||
|
||||
|
||||
val result = flow {
|
||||
emit(State.Loading())
|
||||
emit(Success(listOf(data)))
|
||||
}
|
||||
|
||||
|
||||
override fun loadDataForPage(page: Int): Flow<Result<List<Int>>> = result
|
||||
}
|
||||
|
||||
inner class TestFailureSource : BasePagingDataSource<Int>() {
|
||||
|
||||
val result = flow {
|
||||
emit(State.Loading())
|
||||
emit(Failure(GenericError()))
|
||||
}
|
||||
|
||||
override fun loadDataForPage(page: Int): Flow<Result<List<Int>>> = result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.melih.core.paging
|
||||
|
||||
import com.melih.core.BaseTestWithMainThread
|
||||
import com.melih.core.base.paging.BasePagingDataSource
|
||||
import com.melih.core.base.paging.BasePagingFactory
|
||||
import com.melih.core.testObserve
|
||||
import io.mockk.mockk
|
||||
import io.mockk.spyk
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.amshove.kluent.shouldEqual
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class BasePagingFactoryTest : BaseTestWithMainThread() {
|
||||
|
||||
val factory = spyk(TestFactory())
|
||||
|
||||
@Test
|
||||
fun `create should update current source when it creates a new one`() {
|
||||
val source = factory.create()
|
||||
|
||||
runBlocking {
|
||||
factory.currentSource.testObserve {
|
||||
it shouldEqual source
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class TestFactory : BasePagingFactory<String>() {
|
||||
|
||||
override fun createSource(): BasePagingDataSource<String> = mockk(relaxed = true)
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user