Refactor RecipesFragment
This commit extracts SwipeRefreshLayout extension to a separate file. Additionally, it refactors RecipesFragment in order to move all the logic to the ViewModel from the View.
This commit is contained in:
@@ -1,41 +0,0 @@
|
||||
package gq.kirmanak.mealient.ui
|
||||
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.channels.onFailure
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import timber.log.Timber
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
object SwipeRefreshLayoutHelper {
|
||||
private fun SwipeRefreshLayout.refreshesFlow(): Flow<Unit> {
|
||||
Timber.v("refreshesFlow() called")
|
||||
return callbackFlow {
|
||||
val listener = SwipeRefreshLayout.OnRefreshListener {
|
||||
Timber.v("Refresh requested")
|
||||
trySend(Unit).onFailure { Timber.e(it, "Can't send refresh callback") }
|
||||
}
|
||||
Timber.v("Adding refresh request listener")
|
||||
setOnRefreshListener(listener)
|
||||
awaitClose {
|
||||
Timber.v("Removing refresh request listener")
|
||||
setOnRefreshListener(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T : Any, VH : RecyclerView.ViewHolder> PagingDataAdapter<T, VH>.listenToRefreshRequests(
|
||||
refreshLayout: SwipeRefreshLayout
|
||||
) {
|
||||
Timber.v("listenToRefreshRequests() called")
|
||||
refreshLayout.refreshesFlow().collectLatest {
|
||||
Timber.d("Received refresh request")
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
31
app/src/main/java/gq/kirmanak/mealient/ui/ViewExtensions.kt
Normal file
31
app/src/main/java/gq/kirmanak/mealient/ui/ViewExtensions.kt
Normal file
@@ -0,0 +1,31 @@
|
||||
package gq.kirmanak.mealient.ui
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.channels.onClosed
|
||||
import kotlinx.coroutines.channels.onFailure
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import timber.log.Timber
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
fun SwipeRefreshLayout.refreshesLiveData(): LiveData<Unit> {
|
||||
val callbackFlow: Flow<Unit> = callbackFlow {
|
||||
val listener = SwipeRefreshLayout.OnRefreshListener {
|
||||
Timber.v("Refresh requested")
|
||||
trySend(Unit)
|
||||
.onFailure { Timber.e(it, "refreshesFlow: can't send refresh callback") }
|
||||
.onClosed { Timber.e(it, "refreshesFlow: flow has been closed") }
|
||||
}
|
||||
Timber.v("Adding refresh request listener")
|
||||
setOnRefreshListener(listener)
|
||||
awaitClose {
|
||||
Timber.v("Removing refresh request listener")
|
||||
setOnRefreshListener(null)
|
||||
}
|
||||
}
|
||||
return callbackFlow.asLiveData()
|
||||
}
|
||||
@@ -1,22 +1,43 @@
|
||||
package gq.kirmanak.mealient.ui.recipes
|
||||
|
||||
import android.widget.ImageView
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.cachedIn
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import gq.kirmanak.mealient.data.recipes.RecipeImageLoader
|
||||
import gq.kirmanak.mealient.data.recipes.RecipeRepo
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class RecipeViewModel @Inject constructor(
|
||||
recipeRepo: RecipeRepo,
|
||||
private val recipeRepo: RecipeRepo,
|
||||
private val recipeImageLoader: RecipeImageLoader
|
||||
) : ViewModel() {
|
||||
val recipeFlow = recipeRepo.createPager().flow
|
||||
private var _isRefreshing = MutableLiveData<Boolean>()
|
||||
val isRefreshing: LiveData<Boolean> get() = _isRefreshing
|
||||
|
||||
private val _nextRecipeInfoChannel = Channel<RecipeSummaryEntity>()
|
||||
val nextRecipeInfo: Flow<RecipeSummaryEntity> =
|
||||
_nextRecipeInfoChannel.receiveAsFlow()
|
||||
|
||||
val adapter = RecipesPagingAdapter(this) {
|
||||
Timber.d("onClick: recipe clicked $it")
|
||||
viewModelScope.launch { _nextRecipeInfoChannel.send(it) }
|
||||
}
|
||||
|
||||
init {
|
||||
setupAdapter()
|
||||
}
|
||||
|
||||
fun loadRecipeImage(view: ImageView, recipeSummary: RecipeSummaryEntity?) {
|
||||
Timber.v("loadRecipeImage() called with: view = $view, recipeSummary = $recipeSummary")
|
||||
@@ -24,4 +45,21 @@ class RecipeViewModel @Inject constructor(
|
||||
recipeImageLoader.loadRecipeImage(view, recipeSummary?.slug)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupAdapter() {
|
||||
with(viewModelScope) {
|
||||
launch {
|
||||
recipeRepo.createPager().flow.cachedIn(this).collect {
|
||||
Timber.d("setupAdapter: received data update")
|
||||
adapter.submitData(it)
|
||||
}
|
||||
}
|
||||
launch {
|
||||
adapter.onPagesUpdatedFlow.collect {
|
||||
Timber.d("setupAdapter: pages have been updated")
|
||||
_isRefreshing.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,11 @@ import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
|
||||
import gq.kirmanak.mealient.databinding.FragmentRecipesBinding
|
||||
import gq.kirmanak.mealient.ui.SwipeRefreshLayoutHelper.listenToRefreshRequests
|
||||
import gq.kirmanak.mealient.ui.auth.AuthenticationViewModel
|
||||
import gq.kirmanak.mealient.ui.refreshesLiveData
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import timber.log.Timber
|
||||
@@ -74,22 +73,19 @@ class RecipesFragment : Fragment() {
|
||||
|
||||
private fun setupRecipeAdapter() {
|
||||
Timber.v("setupRecipeAdapter() called")
|
||||
binding.recipes.layoutManager = LinearLayoutManager(requireContext())
|
||||
val adapter = RecipesPagingAdapter(viewModel) { navigateToRecipeInfo(it) }
|
||||
binding.recipes.adapter = adapter
|
||||
viewLifecycleOwner.lifecycleScope.launchWhenResumed {
|
||||
viewModel.recipeFlow.collect {
|
||||
Timber.d("Received update")
|
||||
adapter.submitData(it)
|
||||
}
|
||||
binding.recipes.adapter = viewModel.adapter
|
||||
viewModel.isRefreshing.observe(viewLifecycleOwner) {
|
||||
Timber.d("setupRecipeAdapter: isRefreshing = $it")
|
||||
binding.refresher.isRefreshing = it
|
||||
}
|
||||
binding.refresher.refreshesLiveData().observe(viewLifecycleOwner) {
|
||||
Timber.d("setupRecipeAdapter: received refresh request")
|
||||
viewModel.adapter.refresh()
|
||||
}
|
||||
viewLifecycleOwner.lifecycleScope.launchWhenResumed {
|
||||
adapter.listenToRefreshRequests(binding.refresher)
|
||||
}
|
||||
viewLifecycleOwner.lifecycleScope.launchWhenResumed {
|
||||
adapter.onPagesUpdatedFlow.collect {
|
||||
Timber.d("Pages have been updated")
|
||||
binding.refresher.isRefreshing = false
|
||||
viewModel.nextRecipeInfo.collect {
|
||||
Timber.d("setupRecipeAdapter: navigating to recipe $it")
|
||||
navigateToRecipeInfo(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
android:id="@+id/recipes"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/view_holder_recipe" />
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user