diff --git a/app/src/main/java/gq/kirmanak/mealient/extensions/FragmentExtensions.kt b/app/src/main/java/gq/kirmanak/mealient/extensions/FragmentExtensions.kt index 511b2f3..17186e9 100644 --- a/app/src/main/java/gq/kirmanak/mealient/extensions/FragmentExtensions.kt +++ b/app/src/main/java/gq/kirmanak/mealient/extensions/FragmentExtensions.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @@ -27,4 +28,9 @@ fun OnBackPressedDispatcher.backPressedFlow(): Flow = callbackFlow { callback.isEnabled = false callback.remove() } -} \ No newline at end of file +} + +inline fun Fragment.collectWithViewLifecycle( + flow: Flow, + crossinline collector: suspend (T) -> Unit, +) = viewLifecycleOwner.lifecycleScope.launch { flow.collect(collector) } diff --git a/app/src/main/java/gq/kirmanak/mealient/extensions/ViewExtensions.kt b/app/src/main/java/gq/kirmanak/mealient/extensions/ViewExtensions.kt index c123800..afb1fad 100644 --- a/app/src/main/java/gq/kirmanak/mealient/extensions/ViewExtensions.kt +++ b/app/src/main/java/gq/kirmanak/mealient/extensions/ViewExtensions.kt @@ -10,8 +10,6 @@ import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.core.widget.doAfterTextChanged import androidx.lifecycle.LifecycleCoroutineScope -import androidx.lifecycle.LiveData -import androidx.lifecycle.asLiveData import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.google.android.material.textfield.TextInputLayout import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -26,20 +24,17 @@ import kotlinx.coroutines.flow.first import timber.log.Timber @OptIn(ExperimentalCoroutinesApi::class) -fun SwipeRefreshLayout.refreshesLiveData(): LiveData { - val callbackFlow: Flow = callbackFlow { - val listener = SwipeRefreshLayout.OnRefreshListener { - Timber.v("Refresh requested") - trySend(Unit).logErrors("refreshesFlow") - } - Timber.v("Adding refresh request listener") - setOnRefreshListener(listener) - awaitClose { - Timber.v("Removing refresh request listener") - setOnRefreshListener(null) - } +fun SwipeRefreshLayout.refreshRequestFlow(): Flow = callbackFlow { + Timber.v("refreshRequestFlow() called") + val listener = SwipeRefreshLayout.OnRefreshListener { + Timber.v("refreshRequestFlow: listener called") + trySend(Unit).logErrors("refreshesFlow") + } + setOnRefreshListener(listener) + awaitClose { + Timber.v("Removing refresh request listener") + setOnRefreshListener(null) } - return callbackFlow.asLiveData() } fun Activity.setSystemUiVisibility(isVisible: Boolean) { diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt index 98e694f..ece0518 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt @@ -14,6 +14,7 @@ import gq.kirmanak.mealient.data.network.NetworkError import gq.kirmanak.mealient.databinding.FragmentAuthenticationBinding import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty import gq.kirmanak.mealient.extensions.executeOnceOnBackPressed +import kotlinx.coroutines.launch import timber.log.Timber @AndroidEntryPoint @@ -33,7 +34,6 @@ class AuthenticationFragment : Fragment(R.layout.fragment_authentication) { binding.button.setOnClickListener { onLoginClicked() } (requireActivity() as? AppCompatActivity)?.supportActionBar?.title = getString(R.string.app_name) - viewModel.authenticationResult.observe(viewLifecycleOwner, ::onAuthenticationResult) } private fun onLoginClicked(): Unit = with(binding) { @@ -48,7 +48,9 @@ class AuthenticationFragment : Fragment(R.layout.fragment_authentication) { } ?: return button.isClickable = false - viewModel.authenticate(email, pass) + viewLifecycleOwner.lifecycleScope.launch { + onAuthenticationResult(viewModel.authenticate(email, pass)) + } } private fun onAuthenticationResult(result: Result) { diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationViewModel.kt index 51ee7ee..d17b360 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationViewModel.kt @@ -1,6 +1,9 @@ package gq.kirmanak.mealient.ui.auth -import androidx.lifecycle.* +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import gq.kirmanak.mealient.data.auth.AuthRepo import kotlinx.coroutines.flow.MutableStateFlow @@ -28,10 +31,6 @@ class AuthenticationViewModel @Inject constructor( var authRequested: Boolean by authRequestsFlow::value var showLoginButton: Boolean by showLoginButtonFlow::value - private val _authenticationResult = MutableLiveData>() - val authenticationResult: LiveData> - get() = _authenticationResult - init { viewModelScope.launch { authRequestsFlow.collect { isRequested -> @@ -41,18 +40,9 @@ class AuthenticationViewModel @Inject constructor( } } - fun authenticate(username: String, password: String) { - Timber.v("authenticate() called with: username = $username, password = $password") - viewModelScope.launch { - runCatching { - authRepo.authenticate(username, password) - }.onFailure { - Timber.e(it, "authenticate: can't authenticate") - _authenticationResult.value = Result.failure(it) - }.onSuccess { - Timber.d("authenticate: authenticated") - _authenticationResult.value = Result.success(Unit) - } - } + suspend fun authenticate(username: String, password: String): Result = runCatching { + authRepo.authenticate(username, password) + }.onFailure { + Timber.e(it, "authenticate: can't authenticate") } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewHolder.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewHolder.kt index a7b7fb0..64fd1f3 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewHolder.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewHolder.kt @@ -9,7 +9,7 @@ import timber.log.Timber class RecipeViewHolder( private val binding: ViewHolderRecipeBinding, private val recipeViewModel: RecipeViewModel, - private val clickListener: (RecipeSummaryEntity) -> Unit + private val clickListener: (RecipeSummaryEntity) -> Unit, ) : RecyclerView.ViewHolder(binding.root) { private val loadingPlaceholder by lazy { binding.root.resources.getString(R.string.view_holder_recipe_text_placeholder) diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewModel.kt index f24beec..6ff9334 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewModel.kt @@ -1,8 +1,6 @@ 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 @@ -10,34 +8,17 @@ 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( - private val recipeRepo: RecipeRepo, + recipeRepo: RecipeRepo, private val recipeImageLoader: RecipeImageLoader ) : ViewModel() { - private var _isRefreshing = MutableLiveData() - val isRefreshing: LiveData get() = _isRefreshing - private val _nextRecipeInfoChannel = Channel() - val nextRecipeInfo: Flow = - _nextRecipeInfoChannel.receiveAsFlow() - - val adapter = RecipesPagingAdapter(this) { - Timber.d("onClick: recipe clicked $it") - viewModelScope.launch { _nextRecipeInfoChannel.send(it) } - } - - init { - setupAdapter() - } + val pagingData = recipeRepo.createPager().flow.cachedIn(viewModelScope) fun loadRecipeImage(view: ImageView, recipeSummary: RecipeSummaryEntity?) { Timber.v("loadRecipeImage() called with: view = $view, recipeSummary = $recipeSummary") @@ -45,21 +26,4 @@ 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 - } - } - } - } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt index 01e8cfb..dab5abd 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt @@ -6,17 +6,16 @@ import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels -import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import by.kirich1409.viewbindingdelegate.viewBinding import dagger.hilt.android.AndroidEntryPoint import gq.kirmanak.mealient.R import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity import gq.kirmanak.mealient.databinding.FragmentRecipesBinding -import gq.kirmanak.mealient.extensions.refreshesLiveData +import gq.kirmanak.mealient.extensions.collectWithViewLifecycle +import gq.kirmanak.mealient.extensions.refreshRequestFlow import gq.kirmanak.mealient.ui.auth.AuthenticationState import gq.kirmanak.mealient.ui.auth.AuthenticationViewModel -import kotlinx.coroutines.flow.collect import timber.log.Timber @AndroidEntryPoint @@ -58,20 +57,19 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) { private fun setupRecipeAdapter() { Timber.v("setupRecipeAdapter() called") - binding.recipes.adapter = viewModel.adapter - viewModel.isRefreshing.observe(viewLifecycleOwner) { - Timber.d("setupRecipeAdapter: isRefreshing = $it") - binding.refresher.isRefreshing = it + val adapter = RecipesPagingAdapter(viewModel, ::navigateToRecipeInfo) + binding.recipes.adapter = adapter + collectWithViewLifecycle(viewModel.pagingData) { + Timber.v("setupRecipeAdapter: received data update") + adapter.submitData(lifecycle, it) } - binding.refresher.refreshesLiveData().observe(viewLifecycleOwner) { - Timber.d("setupRecipeAdapter: received refresh request") - viewModel.adapter.refresh() + collectWithViewLifecycle(adapter.onPagesUpdatedFlow) { + Timber.v("setupRecipeAdapter: pages updated") + binding.refresher.isRefreshing = false } - viewLifecycleOwner.lifecycleScope.launchWhenResumed { - viewModel.nextRecipeInfo.collect { - Timber.d("setupRecipeAdapter: navigating to recipe $it") - navigateToRecipeInfo(it) - } + collectWithViewLifecycle(binding.refresher.refreshRequestFlow()) { + Timber.v("setupRecipeAdapter: received refresh request") + adapter.refresh() } }