Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/Add Coroutine and Flow #7

Merged
merged 7 commits into from
Jan 31, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ Just click on [![Use this template](https://img.shields.io/badge/-Use%20this%20t
- 100% Kotlin-only template
- Following [Clean Architecture approach](https://proandroiddev.com/mvvm-with-clean-architecture-c2c021e05c89)
- Following MVVM Architectural Design Pattern
- Pull Request Template
- Template
- [RxJava 2](https://github.com/ReactiveX/RxJava)
- [Coroutines](https://developer.android.com/kotlin/coroutines)
- [Flow](https://developer.android.com/kotlin/flow)
- Simplest Adapter Ever (based on this [workaround](https://proandroiddev.com/the-best-android-recycler-adapter-youve-ever-seen-probably-177e25279a28))
- Github Actions - CI
- [Hilt](https://dagger.dev/hilt/) - Dependency Injection framework
Expand Down Expand Up @@ -49,6 +52,7 @@ Just click on [![Use this template](https://img.shields.io/badge/-Use%20this%20t
- ~~Use ViewBinding. To see changes, click [here](https://github.com/Drjacky/MVVMTemplate/commit/cfc907532fa991cd8de3b295644bfdff88d67ceb).~~
- Migrate to [JetPack Compose](https://developer.android.com/jetpack/compose)
- ~~Use detekt. To see changes, click [here](https://github.com/Drjacky/MVVMTemplate/pull/6/files).~~
- ~~Add coroutines and flow. To see changes, click [here](https://github.com/Drjacky/MVVMTemplate/pull/7/files).~~

## CI 🏭

Expand All @@ -64,6 +68,7 @@ Available workflows listed as follows:
Feel free to open an issue or submit a pull request for any bugs/improvements.

## Result 📺
<img src="https://raw.githubusercontent.com/Drjacky/MVVMTemplate/master/path.gif" width="350px" height="700px" />
<img src="https://raw.githubusercontent.com/Drjacky/MVVMTemplate/master/list.gif" width="350px" height="700px" />

## License ⚖️
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package app.web.drjackycv.mvvmtemplate.di.module

import app.web.drjackycv.data.products.datasource.ProductsPagingSource
import app.web.drjackycv.data.products.datasource.ProductsPagingSourceByCoroutine
import app.web.drjackycv.data.products.repository.ProductsListRepositoryImpl
import app.web.drjackycv.domain.products.repository.ProductsListRepository
import dagger.Module
Expand All @@ -15,7 +16,10 @@ class RepositoryModule {

@Provides
@ViewModelScoped
fun productsList(pagingSource: ProductsPagingSource): ProductsListRepository =
ProductsListRepositoryImpl(pagingSource)
fun productsList(
pagingSource: ProductsPagingSource,
pagingSourceByCoroutine: ProductsPagingSourceByCoroutine
): ProductsListRepository =
ProductsListRepositoryImpl(pagingSource, pagingSourceByCoroutine)

}
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ object Depends {
"androidx.lifecycle:lifecycle-livedata-ktx:${Versions.lifecycleVersion}"
const val lifecycle_runtime_ktx =
"androidx.lifecycle:lifecycle-runtime-ktx:${Versions.lifecycleVersion}"
const val lifecycle_viewmodel_runtime_ktx =
"androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.lifecycleVersion}"
const val lifecycle_common_java8 =
"androidx.lifecycle:lifecycle-common-java8:${Versions.lifecycleVersion}"
const val lifecycle_viewmodel_ktx =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package app.web.drjackycv.data.products.datasource

import androidx.paging.PagingSource
import app.web.drjackycv.data.products.remote.ProductsApi
import app.web.drjackycv.domain.base.Failure
import app.web.drjackycv.domain.base.RecyclerItem
import io.reactivex.annotations.NonNull
import javax.inject.Inject
import javax.inject.Singleton


private const val STARTING_PAGE_INDEX = 1

@Singleton
class ProductsPagingSourceByCoroutine @Inject constructor(
private val productsApi: ProductsApi
//private val query: String
) : PagingSource<Int, RecyclerItem>() {

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, RecyclerItem> {
val position = params.key ?: STARTING_PAGE_INDEX
//val apiQuery = query

return try {
val response = productsApi.getBeersListByCoroutine(position)
.map {
it.toDomain()
}

toLoadResult(response, position)

} catch (e: Exception) {
LoadResult.Error(
Failure.FailureWithMessage(e.message)
)
}
}

private fun toLoadResult(
@NonNull response: List<RecyclerItem>,
position: Int
): LoadResult<Int, RecyclerItem> {
return LoadResult.Page(
data = response,
prevKey = if (position == STARTING_PAGE_INDEX) null else position - 1,
nextKey = if (response.isEmpty()) null else position + 1,
itemsBefore = LoadResult.Page.COUNT_UNDEFINED,
itemsAfter = LoadResult.Page.COUNT_UNDEFINED
)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,11 @@ interface ProductsApi {
@Query("per_page") perPage: Int = 40
): Single<List<BeerResponse>>

@GET("beers")
suspend fun getBeersListByCoroutine(
/*@Query("ids") ids: String,*/
@Query("page") page: Int = 1,
@Query("per_page") perPage: Int = 40
): List<BeerResponse>

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@ import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.rxjava2.flowable
import app.web.drjackycv.data.products.datasource.ProductsPagingSource
import app.web.drjackycv.data.products.datasource.ProductsPagingSourceByCoroutine
import app.web.drjackycv.domain.base.RecyclerItem
import app.web.drjackycv.domain.extension.allowReads
import app.web.drjackycv.domain.products.repository.ProductsListRepository
import io.reactivex.Flowable
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class ProductsListRepositoryImpl @Inject constructor(
private val pagingSource: ProductsPagingSource
private val pagingSource: ProductsPagingSource,
private val pagingSourceByCoroutine: ProductsPagingSourceByCoroutine
) : ProductsListRepository {

override fun getBeersList(ids: String): Flowable<PagingData<RecyclerItem>> =
Expand All @@ -31,4 +34,17 @@ class ProductsListRepositoryImpl @Inject constructor(
).flowable
}

override fun getBeersListByCoroutine(ids: String): Flow<PagingData<RecyclerItem>> =
allowReads {
Pager(
config = PagingConfig(
pageSize = 10,
enablePlaceholders = false,
maxSize = 30,
prefetchDistance = 5,
initialLoadSize = 10
),
pagingSourceFactory = { pagingSourceByCoroutine }
).flow
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ package app.web.drjackycv.domain.products.repository
import androidx.paging.PagingData
import app.web.drjackycv.domain.base.RecyclerItem
import io.reactivex.Flowable
import kotlinx.coroutines.flow.Flow

interface ProductsListRepository {

fun getBeersList(ids: String): Flowable<PagingData<RecyclerItem>>

fun getBeersListByCoroutine(ids: String): Flow<PagingData<RecyclerItem>>

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package app.web.drjackycv.domain.products.usecase

import androidx.paging.PagingData
import app.web.drjackycv.domain.base.RecyclerItem
import app.web.drjackycv.domain.base.usecase.GeneralUseCase
import app.web.drjackycv.domain.products.repository.ProductsListRepository
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject

class GetBeersListByCoroutineUseCase @Inject constructor(
private val productsListRepository: ProductsListRepository
) : GeneralUseCase<Flow<PagingData<RecyclerItem>>, GetBeersListByCoroutineParams> {

override fun invoke(params: GetBeersListByCoroutineParams): Flow<PagingData<RecyclerItem>> =
productsListRepository.getBeersListByCoroutine(params.ids)

}

inline class GetBeersListByCoroutineParams(val ids: String)
Binary file added path.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions presentation/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ dependencies {
implementation(Depends.Libraries.android_lifecycle_extensions)
implementation(Depends.Libraries.lifecycle_livedata_ktx)
implementation(Depends.Libraries.lifecycle_runtime_ktx)
implementation(Depends.Libraries.lifecycle_viewmodel_runtime_ktx)
implementation(Depends.Libraries.lifecycle_common_java8)
implementation(Depends.Libraries.lifecycle_viewmodel_ktx)
implementation(Depends.Libraries.multidex)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class MainActivity : AppCompatActivity() {

private val binding by viewBinding(ActivityMainBinding::inflate)
private val navController: NavController by lazy {
findNavController(R.id.activityMainProductsListHostFragment)
findNavController(R.id.activityMainChooseHostFragment)
}

override fun onCreate(savedInstanceState: Bundle?) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package app.web.drjackycv.presentation.products.choose

import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import app.web.drjackycv.presentation.R
import app.web.drjackycv.presentation.databinding.FragmentChooseBinding
import app.web.drjackycv.presentation.extension.setOnReactiveClickListener
import app.web.drjackycv.presentation.extension.viewBinding
import java.io.Serializable

class ChooseFragment : Fragment(R.layout.fragment_choose) {

private val binding by viewBinding(FragmentChooseBinding::bind)

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
setupListeners()
}

private fun setupListeners() {
binding.fragmentChooseCoroutineBtn.setOnReactiveClickListener {
val action =
ChooseFragmentDirections.navigateToProductsListFragment(ChoosePathType.COROUTINE)
findNavController().navigate(action)
}
binding.fragmentChooseRxBtn.setOnReactiveClickListener {
val action =
ChooseFragmentDirections.navigateToProductsListFragment(ChoosePathType.RX)
findNavController().navigate(action)
}
}
}

enum class ChoosePathType : Serializable {
COROUTINE,
RX
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import android.view.View
import androidx.core.view.doOnPreDraw
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.paging.CombinedLoadStates
import androidx.paging.LoadState
import androidx.paging.PagingData
Expand All @@ -18,24 +20,42 @@ import app.web.drjackycv.presentation.R
import app.web.drjackycv.presentation.base.adapter.LoadingStateAdapter
import app.web.drjackycv.presentation.databinding.FragmentProductListBinding
import app.web.drjackycv.presentation.extension.*
import app.web.drjackycv.presentation.products.choose.ChoosePathType
import com.google.android.material.transition.platform.Hold
import com.google.android.material.transition.platform.MaterialElevationScale
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import timber.log.Timber

@AndroidEntryPoint
class ProductsListFragment : Fragment(R.layout.fragment_product_list) {

private val binding by viewBinding(FragmentProductListBinding::bind)
private val productsListViewModel: ProductsListViewModel by viewModels()
private var path = ChoosePathType.RX
private var uiStateJob: Job? = null

private val productsListAdapter: ProductsListAdapter by lazy {
ProductsListAdapter(::navigateToProductDetail)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupPath()
setupRecycler()
setupViewModel()
}

override fun onStop() {
uiStateJob?.cancel()
super.onStop()
}

private fun setupPath() {
val safeArgs: ProductsListFragmentArgs by navArgs()
path = safeArgs.choosePathType
Timber.d("Which path: $path")
setupViewBaseOnPath()
}

private fun setupRecycler() {
Expand All @@ -50,6 +70,17 @@ class ProductsListFragment : Fragment(R.layout.fragment_product_list) {
view?.doOnPreDraw { startPostponedEnterTransition() }
}

private fun setupViewBaseOnPath() {
when (path) {
ChoosePathType.RX -> {
setupViewModel()
}
ChoosePathType.COROUTINE -> {
setupViewByCoroutine()
}
}
}

private fun setupViewModel() {
productsListViewModel.run {

Expand All @@ -60,6 +91,18 @@ class ProductsListFragment : Fragment(R.layout.fragment_product_list) {
}
}

private fun setupViewByCoroutine() {
uiStateJob = lifecycleScope.launchWhenStarted {
productsListViewModel.productsListByCoroutine.collect {
addProductsList(it)
}
}

productsListViewModel.run {
observe(ldFailure, ::handleFailure)
}
}

private fun addProductsList(productsList: PagingData<RecyclerItem>) {
binding.productListRecyclerView.visible()
productsListAdapter.submitData(lifecycle, productsList)
Expand Down Expand Up @@ -127,7 +170,7 @@ class ProductsListFragment : Fragment(R.layout.fragment_product_list) {
}

private fun retryFetchData() {
productsListViewModel.getProducts("")
productsListViewModel.getProductsBaseOnPath("", path)
}

}
Loading