mirror of
https://github.com/melihaksoy/Android-Kotlin-Modulerized-CleanArchitecture.git
synced 2026-03-24 10:21:44 +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
@@ -15,8 +15,5 @@ android {
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
testImplementation testLibraries.jUnitApi
|
||||
testImplementation testLibraries.mockk
|
||||
testImplementation testLibraries.kluent
|
||||
testImplementation testLibraries.coroutinesTest
|
||||
}
|
||||
|
||||
BIN
features/detail/jacoco.exec
Normal file
BIN
features/detail/jacoco.exec
Normal file
Binary file not shown.
@@ -1,7 +1,9 @@
|
||||
package com.melih.detail.di
|
||||
|
||||
import com.melih.detail.di.modules.DetailBinds
|
||||
import com.melih.detail.di.modules.DetailProvides
|
||||
import com.melih.detail.ui.DetailFragment
|
||||
import com.melih.list.di.scopes.DetailFragmentScope
|
||||
import dagger.Module
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
|
||||
@@ -15,9 +17,11 @@ abstract class DetailContributor {
|
||||
|
||||
@ContributesAndroidInjector(
|
||||
modules = [
|
||||
DetailBinds::class
|
||||
DetailBinds::class,
|
||||
DetailProvides::class
|
||||
]
|
||||
)
|
||||
@DetailFragmentScope
|
||||
abstract fun detailFragment(): DetailFragment
|
||||
// endregion
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.melih.detail.di
|
||||
|
||||
import com.melih.detail.ui.DetailActivity
|
||||
import com.melih.list.di.scopes.DetailScope
|
||||
import dagger.Module
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
|
||||
/**
|
||||
* Contributes fragments & view models in this module
|
||||
*/
|
||||
@Module
|
||||
abstract class DetailModule {
|
||||
|
||||
// region Contributes
|
||||
|
||||
@ContributesAndroidInjector(
|
||||
modules = [
|
||||
DetailContributor::class
|
||||
]
|
||||
)
|
||||
@DetailScope
|
||||
abstract fun detailActivity(): DetailActivity
|
||||
// endregion
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import com.melih.detail.ui.DetailViewModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.multibindings.IntoMap
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
||||
@Module
|
||||
abstract class DetailBinds {
|
||||
@@ -16,7 +15,6 @@ abstract class DetailBinds {
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(DetailViewModel::class)
|
||||
@ExperimentalCoroutinesApi
|
||||
abstract fun detailViewModel(detailViewModel: DetailViewModel): ViewModel
|
||||
// endregion
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.melih.detail.di.modules
|
||||
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import com.melih.detail.ui.DetailFragment
|
||||
import com.melih.detail.ui.DetailFragmentArgs
|
||||
import com.melih.repository.interactors.GetLaunchDetails
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
||||
@Module
|
||||
class DetailProvides {
|
||||
|
||||
/**
|
||||
* Provides launch detail params
|
||||
*/
|
||||
@Provides
|
||||
fun provideGetLaunchDetailParams(fragment: DetailFragment): GetLaunchDetails.Params {
|
||||
val args: DetailFragmentArgs by fragment.navArgs()
|
||||
return GetLaunchDetails.Params(args.launchId)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.melih.list.di.scopes
|
||||
|
||||
import javax.inject.Scope
|
||||
|
||||
@Scope
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class DetailFragmentScope
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.melih.list.di.scopes
|
||||
|
||||
import javax.inject.Scope
|
||||
|
||||
@Scope
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class DetailScope
|
||||
@@ -6,7 +6,6 @@ import com.melih.core.actions.EXTRA_LAUNCH_ID
|
||||
import com.melih.core.base.lifecycle.BaseActivity
|
||||
import com.melih.detail.R
|
||||
import com.melih.detail.databinding.DetailActivityBinding
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
||||
const val INVALID_LAUNCH_ID = -1L
|
||||
|
||||
@@ -14,13 +13,12 @@ class DetailActivity : BaseActivity<DetailActivityBinding>() {
|
||||
|
||||
// region Functions
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setSupportActionBar(binding.toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true);
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true);
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
}
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.activity_detail
|
||||
|
||||
@@ -3,38 +3,28 @@ package com.melih.detail.ui
|
||||
import android.os.Bundle
|
||||
import android.text.method.ScrollingMovementMethod
|
||||
import android.view.View
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import com.melih.core.base.lifecycle.BaseDaggerFragment
|
||||
import com.melih.core.extensions.createFor
|
||||
import com.melih.core.extensions.observe
|
||||
import com.melih.detail.R
|
||||
import com.melih.detail.databinding.DetailBinding
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import timber.log.Timber
|
||||
|
||||
class DetailFragment : BaseDaggerFragment<DetailBinding>() {
|
||||
|
||||
// region Properties
|
||||
|
||||
private val args: DetailFragmentArgs by navArgs()
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
private val viewModel: DetailViewModel
|
||||
get() = viewModelFactory.createFor(this)
|
||||
// endregion
|
||||
|
||||
// region Functions
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.tvDescription.movementMethod = ScrollingMovementMethod()
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.createParamsFor(args.launchId)
|
||||
viewModel.loadData()
|
||||
|
||||
// Observing error to show toast with retry action
|
||||
observe(viewModel.errorData) {
|
||||
showSnackbarWithAction(it) {
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
package com.melih.detail.ui
|
||||
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.melih.core.base.viewmodel.BaseViewModel
|
||||
import com.melih.repository.entities.LaunchEntity
|
||||
import com.melih.repository.interactors.GetLaunchDetails
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import com.melih.repository.interactors.base.handle
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
class DetailViewModel @Inject constructor(
|
||||
private val getLaunchDetails: GetLaunchDetails
|
||||
private val getLaunchDetails: GetLaunchDetails,
|
||||
private val getLaunchDetailsParams: GetLaunchDetails.Params
|
||||
) : BaseViewModel<LaunchEntity>() {
|
||||
|
||||
// region Properties
|
||||
|
||||
private var params = GetLaunchDetails.Params(INVALID_LAUNCH_ID)
|
||||
|
||||
val rocketName = Transformations.map(successData) {
|
||||
it.rocket.name
|
||||
}
|
||||
@@ -38,18 +34,12 @@ class DetailViewModel @Inject constructor(
|
||||
|
||||
// region Functions
|
||||
|
||||
fun createParamsFor(id: Long) {
|
||||
params = GetLaunchDetails.Params(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggering interactor in view model scope
|
||||
*/
|
||||
override fun loadData() {
|
||||
viewModelScope.launch {
|
||||
getLaunchDetails(params).collect {
|
||||
it.handle(::handleState, ::handleFailure, ::handleSuccess)
|
||||
}
|
||||
override suspend fun loadData() {
|
||||
getLaunchDetails(getLaunchDetailsParams).collect {
|
||||
it.handle(::handleState, ::handleFailure, ::handleSuccess)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.melih.list
|
||||
package com.melih.detail
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
@@ -8,19 +8,17 @@ import kotlinx.coroutines.test.setMain
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
|
||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||
abstract class BaseTestWithMainThread {
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
protected val dispatcher = TestCoroutineDispatcher()
|
||||
|
||||
@BeforeEach
|
||||
@ExperimentalCoroutinesApi
|
||||
fun setUp() {
|
||||
Dispatchers.setMain(dispatcher)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
@ExperimentalCoroutinesApi
|
||||
fun tearDown() {
|
||||
Dispatchers.resetMain()
|
||||
dispatcher.cleanupTestCoroutines()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.melih.detail
|
||||
|
||||
import com.melih.detail.ui.DetailViewModel
|
||||
import com.melih.list.BaseTestWithMainThread
|
||||
import com.melih.repository.interactors.GetLaunchDetails
|
||||
import io.mockk.mockk
|
||||
import io.mockk.slot
|
||||
@@ -17,21 +16,20 @@ import org.junit.jupiter.api.Test
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||
class DetailViewModelTest : BaseTestWithMainThread() {
|
||||
|
||||
private val getLaunchDetails: GetLaunchDetails = mockk(relaxed = true)
|
||||
private val getLaunchDetailsParams = GetLaunchDetails.Params(1013)
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
private val viewModel = spyk(DetailViewModel(getLaunchDetails))
|
||||
private val viewModel = spyk(DetailViewModel(getLaunchDetails, getLaunchDetailsParams))
|
||||
|
||||
@Test
|
||||
@ExperimentalCoroutinesApi
|
||||
fun `loadData should invoke getLauchDetails with provided params`() {
|
||||
dispatcher.runBlockingTest {
|
||||
|
||||
val paramsSlot = slot<GetLaunchDetails.Params>()
|
||||
|
||||
viewModel.createParamsFor(1013)
|
||||
viewModel.loadData()
|
||||
|
||||
// init should have called it already due to creation above
|
||||
|
||||
@@ -7,8 +7,7 @@ apply from: "$rootProject.projectDir/scripts/feature_module.gradle"
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
testImplementation testLibraries.jUnitApi
|
||||
testImplementation testLibraries.mockk
|
||||
testImplementation testLibraries.kluent
|
||||
implementation libraries.paging
|
||||
|
||||
testImplementation testLibraries.coroutinesTest
|
||||
}
|
||||
BIN
features/launches/jacoco.exec
Normal file
BIN
features/launches/jacoco.exec
Normal file
Binary file not shown.
@@ -2,6 +2,7 @@ package com.melih.list.di
|
||||
|
||||
import com.melih.list.di.modules.LaunchesBinds
|
||||
import com.melih.list.di.modules.LaunchesProvides
|
||||
import com.melih.list.di.scopes.LaunchesFragmentScope
|
||||
import com.melih.list.ui.LaunchesFragment
|
||||
import dagger.Module
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
@@ -20,6 +21,7 @@ abstract class LaunchesContributor {
|
||||
LaunchesBinds::class
|
||||
]
|
||||
)
|
||||
abstract fun listFragment(): LaunchesFragment
|
||||
@LaunchesFragmentScope
|
||||
abstract fun launchesFragment(): LaunchesFragment
|
||||
// endregion
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.melih.list.di
|
||||
|
||||
import com.melih.list.di.scopes.LaunchesScope
|
||||
import com.melih.list.ui.LaunchesActivity
|
||||
import dagger.Module
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
|
||||
/**
|
||||
* Contributes fragments & view models in this module
|
||||
*/
|
||||
@Module
|
||||
abstract class LaunchesFeatureModule {
|
||||
|
||||
// region Contributes
|
||||
|
||||
@ContributesAndroidInjector(
|
||||
modules = [
|
||||
LaunchesContributor::class
|
||||
]
|
||||
)
|
||||
@LaunchesScope
|
||||
abstract fun launchesActivity(): LaunchesActivity
|
||||
// endregion
|
||||
}
|
||||
@@ -2,11 +2,10 @@ package com.melih.list.di.modules
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.melih.core.di.keys.ViewModelKey
|
||||
import com.melih.list.ui.LaunchesViewModel
|
||||
import com.melih.list.ui.vm.LaunchesViewModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.multibindings.IntoMap
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
||||
@Module
|
||||
abstract class LaunchesBinds {
|
||||
@@ -16,7 +15,6 @@ abstract class LaunchesBinds {
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(LaunchesViewModel::class)
|
||||
@ExperimentalCoroutinesApi
|
||||
abstract fun listViewModel(listViewModel: LaunchesViewModel): ViewModel
|
||||
// endregion
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.melih.list.di.modules
|
||||
|
||||
import androidx.paging.Config
|
||||
import com.melih.repository.interactors.DEFAULT_LAUNCHES_AMOUNT
|
||||
import com.melih.repository.interactors.GetLaunches
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
||||
@Module
|
||||
class LaunchesProvides {
|
||||
|
||||
/**
|
||||
* Provides lauches, using default value of 15
|
||||
*/
|
||||
@Provides
|
||||
fun provideGetLaunchesParams() = GetLaunches.Params(page = 0)
|
||||
|
||||
@Provides
|
||||
fun getPagingConfig() = Config(
|
||||
DEFAULT_LAUNCHES_AMOUNT,
|
||||
prefetchDistance = 2,
|
||||
enablePlaceholders = false
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.melih.list.di.scopes
|
||||
|
||||
import javax.inject.Scope
|
||||
|
||||
@Scope
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class LaunchesFragmentScope
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.melih.list.di.scopes
|
||||
|
||||
import javax.inject.Scope
|
||||
|
||||
@Scope
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class LaunchesScope
|
||||
@@ -5,13 +5,11 @@ import androidx.navigation.fragment.NavHostFragment
|
||||
import com.melih.core.base.lifecycle.BaseActivity
|
||||
import com.melih.list.R
|
||||
import com.melih.list.databinding.LaunchesActivityBinding
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
||||
class LaunchesActivity : BaseActivity<LaunchesActivityBinding>() {
|
||||
|
||||
// region Functions
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.melih.list.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import com.melih.core.actions.Actions
|
||||
import com.melih.core.base.lifecycle.BaseDaggerFragment
|
||||
import com.melih.core.extensions.createFor
|
||||
import com.melih.core.extensions.observe
|
||||
import com.melih.list.R
|
||||
import com.melih.list.databinding.ListBinding
|
||||
import com.melih.list.ui.adapters.LaunchesAdapter
|
||||
import com.melih.list.ui.vm.LaunchesViewModel
|
||||
import com.melih.repository.entities.LaunchEntity
|
||||
import com.melih.repository.interactors.base.State
|
||||
|
||||
class LaunchesFragment : BaseDaggerFragment<ListBinding>(), SwipeRefreshLayout.OnRefreshListener {
|
||||
|
||||
// region Properties
|
||||
|
||||
private val viewModel: LaunchesViewModel
|
||||
get() = viewModelFactory.createFor(this)
|
||||
|
||||
private val launchesAdapter = LaunchesAdapter(::onItemSelected)
|
||||
// endregion
|
||||
|
||||
// region Functions
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
//setHasOptionsMenu(true)
|
||||
|
||||
binding.rocketList.adapter = launchesAdapter
|
||||
binding.swipeRefreshLayout.setOnRefreshListener(this)
|
||||
|
||||
observeDataChanges()
|
||||
}
|
||||
|
||||
//override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
// inflater.inflate(R.menu.menu_rocket_list, menu)
|
||||
//
|
||||
// with(menu.findItem(R.id.search)) {
|
||||
// onExpandOrCollapse(::onSearchExpand, ::onSearchCollapse)
|
||||
// setSearchQueryListener(actionView as SearchView)
|
||||
// }
|
||||
//
|
||||
// super.onCreateOptionsMenu(menu, inflater)
|
||||
//}
|
||||
|
||||
private fun observeDataChanges() {
|
||||
|
||||
// Observing state to show loading
|
||||
observe(viewModel.stateData) {
|
||||
binding.swipeRefreshLayout.isRefreshing = it is State.Loading
|
||||
}
|
||||
|
||||
// Observing error to show toast with retry action
|
||||
observe(viewModel.errorData) {
|
||||
showSnackbarWithAction(it) {
|
||||
viewModel.retry()
|
||||
}
|
||||
}
|
||||
|
||||
observe(viewModel.pagedList) {
|
||||
launchesAdapter.submitList(it)
|
||||
}
|
||||
|
||||
//observe(viewModel.filteredItems) {
|
||||
// launchesAdapter.submitList(it)
|
||||
//}
|
||||
}
|
||||
|
||||
private fun onItemSelected(item: LaunchEntity?) {
|
||||
startActivity(Actions.openDetailFor(item?.id ?: -1L))
|
||||
}
|
||||
|
||||
//private fun onSearchExpand() {
|
||||
// binding.swipeRefreshLayout.isEnabled = false
|
||||
//}
|
||||
|
||||
//private fun onSearchCollapse() {
|
||||
// binding.swipeRefreshLayout.isEnabled = true
|
||||
//}
|
||||
|
||||
//private fun setSearchQueryListener(searchView: SearchView) {
|
||||
// searchView.setOnQueryChangedListener {
|
||||
// viewModel.filterItemListBy(it)
|
||||
// }
|
||||
//}
|
||||
|
||||
override fun onRefresh() {
|
||||
viewModel.refresh()
|
||||
}
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.fragment_launches
|
||||
// endregion
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
package com.melih.list.ui
|
||||
package com.melih.list.ui.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import com.melih.core.base.recycler.BaseListAdapter
|
||||
import com.melih.core.base.recycler.BasePagingListAdapter
|
||||
import com.melih.core.base.recycler.BaseViewHolder
|
||||
import com.melih.list.databinding.LaunchRowBinding
|
||||
import com.melih.repository.entities.LaunchEntity
|
||||
|
||||
class LaunchesAdapter(itemClickListener: (LaunchEntity) -> Unit) : BaseListAdapter<LaunchEntity>(
|
||||
class LaunchesAdapter(itemClickListener: (LaunchEntity?) -> Unit) : BasePagingListAdapter<LaunchEntity>(
|
||||
object : DiffUtil.ItemCallback<LaunchEntity>() {
|
||||
override fun areItemsTheSame(oldItem: LaunchEntity, newItem: LaunchEntity): Boolean =
|
||||
oldItem.id == newItem.id
|
||||
@@ -30,11 +30,11 @@ class LaunchesAdapter(itemClickListener: (LaunchEntity) -> Unit) : BaseListAdapt
|
||||
|
||||
class LaunchesViewHolder(private val binding: LaunchRowBinding) : BaseViewHolder<LaunchEntity>(binding) {
|
||||
|
||||
override fun bind(item: LaunchEntity) {
|
||||
override fun bind(item: LaunchEntity?) {
|
||||
binding.entity = item
|
||||
|
||||
val missions = item.missions
|
||||
binding.tvDescription.text = if (missions.isNotEmpty()) missions[0].description else ""
|
||||
val missions = item?.missions
|
||||
binding.tvDescription.text = if (!missions.isNullOrEmpty()) missions[0].description else ""
|
||||
|
||||
binding.executePendingBindings()
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.melih.list.ui.paging
|
||||
|
||||
import com.melih.core.base.paging.BasePagingDataSource
|
||||
import com.melih.repository.entities.LaunchEntity
|
||||
import com.melih.repository.interactors.GetLaunches
|
||||
import com.melih.repository.interactors.base.Result
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
class LaunchesPagingSource @Inject constructor(
|
||||
private val getLaunches: GetLaunches,
|
||||
private val getLaunchesParams: GetLaunches.Params
|
||||
) : BasePagingDataSource<LaunchEntity>() {
|
||||
|
||||
//region Functions
|
||||
|
||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||
override fun loadDataForPage(page: Int): Flow<Result<List<LaunchEntity>>> =
|
||||
getLaunches(
|
||||
getLaunchesParams.copy(
|
||||
page = page
|
||||
)
|
||||
)
|
||||
//endregion
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.melih.list.ui.paging
|
||||
|
||||
import com.melih.core.base.paging.BasePagingDataSource
|
||||
import com.melih.core.base.paging.BasePagingFactory
|
||||
import com.melih.repository.entities.LaunchEntity
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
class LaunchesPagingSourceFactory @Inject constructor(
|
||||
private val sourceProvider: Provider<LaunchesPagingSource>
|
||||
) : BasePagingFactory<LaunchEntity>() {
|
||||
|
||||
override fun createSource(): BasePagingDataSource<LaunchEntity> = sourceProvider.get()
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.melih.list.ui.vm
|
||||
|
||||
import androidx.paging.PagedList
|
||||
import com.melih.core.base.paging.BasePagingFactory
|
||||
import com.melih.core.base.viewmodel.BasePagingViewModel
|
||||
import com.melih.list.ui.paging.LaunchesPagingSourceFactory
|
||||
import com.melih.repository.entities.LaunchEntity
|
||||
import javax.inject.Inject
|
||||
|
||||
class LaunchesViewModel @Inject constructor(
|
||||
private val launchesPagingSourceFactory: LaunchesPagingSourceFactory,
|
||||
private val launchesPagingConfig: PagedList.Config
|
||||
) : BasePagingViewModel<LaunchEntity>() {
|
||||
|
||||
// region Properties
|
||||
|
||||
override val factory: BasePagingFactory<LaunchEntity>
|
||||
get() = launchesPagingSourceFactory
|
||||
|
||||
override val config: PagedList.Config
|
||||
get() = launchesPagingConfig
|
||||
|
||||
//private val _filteredItems = MediatorLiveData<PagedList<LaunchEntity>>()
|
||||
|
||||
//val filteredItems: LiveData<PagedList<LaunchEntity>>
|
||||
// get() = _filteredItems
|
||||
// endregion
|
||||
|
||||
//init {
|
||||
// _filteredItems.addSource(pagedList, _filteredItems::setValue)
|
||||
//}
|
||||
|
||||
// region Functions
|
||||
|
||||
//fun filterItemListBy(query: String?) {
|
||||
//
|
||||
// _filteredItems.value = if (!query.isNullOrBlank()) {
|
||||
// pagedList.value
|
||||
// ?.snapshot() as PagedList<LaunchEntity>
|
||||
// } else {
|
||||
// pagedList.value
|
||||
// }
|
||||
//}
|
||||
// endregion
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="com.melih.list.ui.LaunchesViewModel" />
|
||||
type="com.melih.list.ui.vm.LaunchesViewModel" />
|
||||
</data>
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
@@ -65,7 +65,7 @@
|
||||
android:layout_marginLeft="@dimen/padding_standard"
|
||||
android:layout_marginEnd="@dimen/padding_standard"
|
||||
android:layout_marginRight="@dimen/padding_standard"
|
||||
android:text="@{entity.rocket.name}"
|
||||
android:text="@{entity.name}"
|
||||
android:textAppearance="@style/TitleTextAppearance"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/imgRocket"
|
||||
@@ -1,3 +1,5 @@
|
||||
@file:UseExperimental(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package com.melih.list
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -10,17 +12,14 @@ import org.junit.jupiter.api.BeforeEach
|
||||
|
||||
abstract class BaseTestWithMainThread {
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
protected val dispatcher = TestCoroutineDispatcher()
|
||||
|
||||
@BeforeEach
|
||||
@ExperimentalCoroutinesApi
|
||||
fun setUp() {
|
||||
Dispatchers.setMain(dispatcher)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
@ExperimentalCoroutinesApi
|
||||
fun tearDown() {
|
||||
Dispatchers.resetMain()
|
||||
dispatcher.cleanupTestCoroutines()
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.melih.list.di.modules
|
||||
|
||||
import com.melih.list.ui.LaunchesAdapter
|
||||
import com.melih.repository.interactors.GetLaunches
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
||||
@Module
|
||||
class LaunchesProvides {
|
||||
|
||||
/**
|
||||
* Provides lauches, using default value of 10
|
||||
*/
|
||||
@Provides
|
||||
fun provideGetLaunchesParams() = GetLaunches.Params()
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
package com.melih.list.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import com.melih.core.actions.Actions
|
||||
import com.melih.core.base.lifecycle.BaseDaggerFragment
|
||||
import com.melih.core.extensions.containsIgnoreCase
|
||||
import com.melih.core.extensions.createFor
|
||||
import com.melih.core.extensions.observe
|
||||
import com.melih.core.extensions.setOnQueryChangedListener
|
||||
import com.melih.list.R
|
||||
import com.melih.list.databinding.ListBinding
|
||||
import com.melih.repository.entities.LaunchEntity
|
||||
import com.melih.repository.interactors.base.Result
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
||||
class LaunchesFragment : BaseDaggerFragment<ListBinding>(), SwipeRefreshLayout.OnRefreshListener {
|
||||
|
||||
// region Properties
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
private val viewModel: LaunchesViewModel
|
||||
get() = viewModelFactory.createFor(this)
|
||||
|
||||
private val launchesAdapter = LaunchesAdapter(::onItemSelected)
|
||||
private val itemList = mutableListOf<LaunchEntity>()
|
||||
// endregion
|
||||
|
||||
// region Functions
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
binding.rocketList.adapter = launchesAdapter
|
||||
binding.swipeRefreshLayout.setOnRefreshListener(this)
|
||||
|
||||
observeDataChanges()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.menu_rocket_list, menu)
|
||||
setSearchQueryListener((menu.findItem(R.id.search).actionView as SearchView))
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
private fun observeDataChanges() {
|
||||
|
||||
// Observing state to show loading
|
||||
observe(viewModel.stateData) {
|
||||
binding.swipeRefreshLayout.isRefreshing = it is Result.State.Loading
|
||||
}
|
||||
|
||||
// Observing error to show toast with retry action
|
||||
observe(viewModel.errorData) {
|
||||
showSnackbarWithAction(it) {
|
||||
viewModel.retry()
|
||||
}
|
||||
}
|
||||
|
||||
observe(viewModel.successData) {
|
||||
itemList.addAll(it)
|
||||
launchesAdapter.submitList(itemList)
|
||||
binding.rocketList.scheduleLayoutAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onItemSelected(item: LaunchEntity) {
|
||||
startActivity(Actions.openDetailFor(item.id))
|
||||
}
|
||||
|
||||
private fun setSearchQueryListener(searchView: SearchView) {
|
||||
searchView.setOnQueryChangedListener {
|
||||
filterItemListBy(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterItemListBy(query: String?) =
|
||||
if (!query.isNullOrBlank()) {
|
||||
itemList.filter {
|
||||
it.rocket.name.containsIgnoreCase(query) || it.missions.any { it.description.containsIgnoreCase(query) }
|
||||
}
|
||||
} else {
|
||||
itemList
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
override fun onRefresh() {
|
||||
viewModel.refresh()
|
||||
}
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.fragment_launches
|
||||
// endregion
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package com.melih.list.ui
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.melih.core.base.viewmodel.BaseViewModel
|
||||
import com.melih.repository.entities.LaunchEntity
|
||||
import com.melih.repository.interactors.GetLaunches
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
class LaunchesViewModel @Inject constructor(
|
||||
private val getLaunches: GetLaunches,
|
||||
private val getLaunchesParams: GetLaunches.Params
|
||||
) : BaseViewModel<List<LaunchEntity>>() {
|
||||
|
||||
// region Initialization
|
||||
|
||||
init {
|
||||
loadData()
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Functions
|
||||
|
||||
/**
|
||||
* Triggering interactor in view model scope
|
||||
*/
|
||||
override fun loadData() {
|
||||
viewModelScope.launch {
|
||||
getLaunches(getLaunchesParams).collect {
|
||||
it.handle(::handleState, ::handleFailure, ::handleSuccess)
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package com.melih.list
|
||||
|
||||
import com.melih.list.ui.LaunchesViewModel
|
||||
import com.melih.repository.interactors.GetLaunches
|
||||
import io.mockk.mockk
|
||||
import io.mockk.spyk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
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 LaunchesViewModelTest : BaseTestWithMainThread() {
|
||||
|
||||
val getLaunches: GetLaunches = mockk(relaxed = true)
|
||||
val getLaunchesParams: GetLaunches.Params = mockk(relaxed = true)
|
||||
|
||||
@Test
|
||||
@ExperimentalCoroutinesApi
|
||||
fun `loadData should invoke getLauches with provided params`() {
|
||||
spyk(LaunchesViewModel(getLaunches, getLaunchesParams))
|
||||
|
||||
dispatcher.runBlockingTest {
|
||||
|
||||
// init should have called it already due to creation above
|
||||
verify(exactly = 1) { getLaunches(getLaunchesParams) }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user