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
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user