From f14afd2ebe3551c53a2d3b98bd828a7b49522b47 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Mon, 4 Apr 2022 20:52:14 +0500 Subject: [PATCH] Fix IllegalStateException when clicking login after logout The previous login result was stored as live data and prevented AuthenticationFragment from being shown properly. However, an attempt to destroy RecipesFragment was made. This attempt caused IllegalStateException when accessing view in onDestroyView. --- .../mealient/extensions/FragmentExtensions.kt | 8 +++- .../mealient/extensions/ViewExtensions.kt | 25 +++++------- .../ui/auth/AuthenticationFragment.kt | 6 ++- .../ui/auth/AuthenticationViewModel.kt | 26 ++++-------- .../mealient/ui/recipes/RecipeViewHolder.kt | 2 +- .../mealient/ui/recipes/RecipeViewModel.kt | 40 +------------------ .../mealient/ui/recipes/RecipesFragment.kt | 28 ++++++------- 7 files changed, 45 insertions(+), 90 deletions(-) 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() } }