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:
Kirill Kamakin
2021-11-27 00:22:52 +03:00
parent 897698ab02
commit cc5c9edb1f
5 changed files with 84 additions and 59 deletions

View File

@@ -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()
}
}
}

View 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()
}

View File

@@ -1,22 +1,43 @@
package gq.kirmanak.mealient.ui.recipes package gq.kirmanak.mealient.ui.recipes
import android.widget.ImageView import android.widget.ImageView
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.paging.cachedIn
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.data.recipes.RecipeImageLoader import gq.kirmanak.mealient.data.recipes.RecipeImageLoader
import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.data.recipes.RecipeRepo
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity 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 kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class RecipeViewModel @Inject constructor( class RecipeViewModel @Inject constructor(
recipeRepo: RecipeRepo, private val recipeRepo: RecipeRepo,
private val recipeImageLoader: RecipeImageLoader private val recipeImageLoader: RecipeImageLoader
) : ViewModel() { ) : 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?) { fun loadRecipeImage(view: ImageView, recipeSummary: RecipeSummaryEntity?) {
Timber.v("loadRecipeImage() called with: view = $view, recipeSummary = $recipeSummary") Timber.v("loadRecipeImage() called with: view = $view, recipeSummary = $recipeSummary")
@@ -24,4 +45,21 @@ class RecipeViewModel @Inject constructor(
recipeImageLoader.loadRecipeImage(view, recipeSummary?.slug) 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
}
}
}
}
} }

View File

@@ -10,12 +10,11 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.databinding.FragmentRecipesBinding 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.auth.AuthenticationViewModel
import gq.kirmanak.mealient.ui.refreshesLiveData
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import timber.log.Timber import timber.log.Timber
@@ -74,22 +73,19 @@ class RecipesFragment : Fragment() {
private fun setupRecipeAdapter() { private fun setupRecipeAdapter() {
Timber.v("setupRecipeAdapter() called") Timber.v("setupRecipeAdapter() called")
binding.recipes.layoutManager = LinearLayoutManager(requireContext()) binding.recipes.adapter = viewModel.adapter
val adapter = RecipesPagingAdapter(viewModel) { navigateToRecipeInfo(it) } viewModel.isRefreshing.observe(viewLifecycleOwner) {
binding.recipes.adapter = adapter Timber.d("setupRecipeAdapter: isRefreshing = $it")
viewLifecycleOwner.lifecycleScope.launchWhenResumed { binding.refresher.isRefreshing = it
viewModel.recipeFlow.collect { }
Timber.d("Received update") binding.refresher.refreshesLiveData().observe(viewLifecycleOwner) {
adapter.submitData(it) Timber.d("setupRecipeAdapter: received refresh request")
} viewModel.adapter.refresh()
} }
viewLifecycleOwner.lifecycleScope.launchWhenResumed { viewLifecycleOwner.lifecycleScope.launchWhenResumed {
adapter.listenToRefreshRequests(binding.refresher) viewModel.nextRecipeInfo.collect {
} Timber.d("setupRecipeAdapter: navigating to recipe $it")
viewLifecycleOwner.lifecycleScope.launchWhenResumed { navigateToRecipeInfo(it)
adapter.onPagesUpdatedFlow.collect {
Timber.d("Pages have been updated")
binding.refresher.isRefreshing = false
} }
} }
} }

View File

@@ -19,6 +19,7 @@
android:id="@+id/recipes" android:id="@+id/recipes"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/view_holder_recipe" /> tools:listitem="@layout/view_holder_recipe" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>