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.
This commit is contained in:
Kirill Kamakin
2022-04-04 20:52:14 +05:00
parent fb10333c2c
commit f14afd2ebe
7 changed files with 45 additions and 90 deletions

View File

@@ -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<Unit> = callbackFlow {
callback.isEnabled = false
callback.remove()
}
}
}
inline fun <T> Fragment.collectWithViewLifecycle(
flow: Flow<T>,
crossinline collector: suspend (T) -> Unit,
) = viewLifecycleOwner.lifecycleScope.launch { flow.collect(collector) }

View File

@@ -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<Unit> {
val callbackFlow: Flow<Unit> = 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<Unit> = 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) {

View File

@@ -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<Unit>) {

View File

@@ -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<Result<Unit>>()
val authenticationResult: LiveData<Result<Unit>>
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<Unit> = runCatching {
authRepo.authenticate(username, password)
}.onFailure {
Timber.e(it, "authenticate: can't authenticate")
}
}

View File

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

View File

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

View File

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