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:
@@ -8,6 +8,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -28,3 +29,8 @@ fun OnBackPressedDispatcher.backPressedFlow(): Flow<Unit> = callbackFlow {
|
|||||||
callback.remove()
|
callback.remove()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline fun <T> Fragment.collectWithViewLifecycle(
|
||||||
|
flow: Flow<T>,
|
||||||
|
crossinline collector: suspend (T) -> Unit,
|
||||||
|
) = viewLifecycleOwner.lifecycleScope.launch { flow.collect(collector) }
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ import androidx.annotation.RequiresApi
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.widget.doAfterTextChanged
|
import androidx.core.widget.doAfterTextChanged
|
||||||
import androidx.lifecycle.LifecycleCoroutineScope
|
import androidx.lifecycle.LifecycleCoroutineScope
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.asLiveData
|
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
@@ -26,21 +24,18 @@ import kotlinx.coroutines.flow.first
|
|||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun SwipeRefreshLayout.refreshesLiveData(): LiveData<Unit> {
|
fun SwipeRefreshLayout.refreshRequestFlow(): Flow<Unit> = callbackFlow {
|
||||||
val callbackFlow: Flow<Unit> = callbackFlow {
|
Timber.v("refreshRequestFlow() called")
|
||||||
val listener = SwipeRefreshLayout.OnRefreshListener {
|
val listener = SwipeRefreshLayout.OnRefreshListener {
|
||||||
Timber.v("Refresh requested")
|
Timber.v("refreshRequestFlow: listener called")
|
||||||
trySend(Unit).logErrors("refreshesFlow")
|
trySend(Unit).logErrors("refreshesFlow")
|
||||||
}
|
}
|
||||||
Timber.v("Adding refresh request listener")
|
|
||||||
setOnRefreshListener(listener)
|
setOnRefreshListener(listener)
|
||||||
awaitClose {
|
awaitClose {
|
||||||
Timber.v("Removing refresh request listener")
|
Timber.v("Removing refresh request listener")
|
||||||
setOnRefreshListener(null)
|
setOnRefreshListener(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return callbackFlow.asLiveData()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Activity.setSystemUiVisibility(isVisible: Boolean) {
|
fun Activity.setSystemUiVisibility(isVisible: Boolean) {
|
||||||
Timber.v("setSystemUiVisibility() called with: isVisible = $isVisible")
|
Timber.v("setSystemUiVisibility() called with: isVisible = $isVisible")
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import gq.kirmanak.mealient.data.network.NetworkError
|
|||||||
import gq.kirmanak.mealient.databinding.FragmentAuthenticationBinding
|
import gq.kirmanak.mealient.databinding.FragmentAuthenticationBinding
|
||||||
import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty
|
import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty
|
||||||
import gq.kirmanak.mealient.extensions.executeOnceOnBackPressed
|
import gq.kirmanak.mealient.extensions.executeOnceOnBackPressed
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@@ -33,7 +34,6 @@ class AuthenticationFragment : Fragment(R.layout.fragment_authentication) {
|
|||||||
binding.button.setOnClickListener { onLoginClicked() }
|
binding.button.setOnClickListener { onLoginClicked() }
|
||||||
(requireActivity() as? AppCompatActivity)?.supportActionBar?.title =
|
(requireActivity() as? AppCompatActivity)?.supportActionBar?.title =
|
||||||
getString(R.string.app_name)
|
getString(R.string.app_name)
|
||||||
viewModel.authenticationResult.observe(viewLifecycleOwner, ::onAuthenticationResult)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onLoginClicked(): Unit = with(binding) {
|
private fun onLoginClicked(): Unit = with(binding) {
|
||||||
@@ -48,7 +48,9 @@ class AuthenticationFragment : Fragment(R.layout.fragment_authentication) {
|
|||||||
} ?: return
|
} ?: return
|
||||||
|
|
||||||
button.isClickable = false
|
button.isClickable = false
|
||||||
viewModel.authenticate(email, pass)
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
onAuthenticationResult(viewModel.authenticate(email, pass))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onAuthenticationResult(result: Result<Unit>) {
|
private fun onAuthenticationResult(result: Result<Unit>) {
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package gq.kirmanak.mealient.ui.auth
|
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 dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -28,10 +31,6 @@ class AuthenticationViewModel @Inject constructor(
|
|||||||
var authRequested: Boolean by authRequestsFlow::value
|
var authRequested: Boolean by authRequestsFlow::value
|
||||||
var showLoginButton: Boolean by showLoginButtonFlow::value
|
var showLoginButton: Boolean by showLoginButtonFlow::value
|
||||||
|
|
||||||
private val _authenticationResult = MutableLiveData<Result<Unit>>()
|
|
||||||
val authenticationResult: LiveData<Result<Unit>>
|
|
||||||
get() = _authenticationResult
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
authRequestsFlow.collect { isRequested ->
|
authRequestsFlow.collect { isRequested ->
|
||||||
@@ -41,18 +40,9 @@ class AuthenticationViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun authenticate(username: String, password: String) {
|
suspend fun authenticate(username: String, password: String): Result<Unit> = runCatching {
|
||||||
Timber.v("authenticate() called with: username = $username, password = $password")
|
|
||||||
viewModelScope.launch {
|
|
||||||
runCatching {
|
|
||||||
authRepo.authenticate(username, password)
|
authRepo.authenticate(username, password)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Timber.e(it, "authenticate: can't authenticate")
|
Timber.e(it, "authenticate: can't authenticate")
|
||||||
_authenticationResult.value = Result.failure(it)
|
|
||||||
}.onSuccess {
|
|
||||||
Timber.d("authenticate: authenticated")
|
|
||||||
_authenticationResult.value = Result.success(Unit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,7 @@ import timber.log.Timber
|
|||||||
class RecipeViewHolder(
|
class RecipeViewHolder(
|
||||||
private val binding: ViewHolderRecipeBinding,
|
private val binding: ViewHolderRecipeBinding,
|
||||||
private val recipeViewModel: RecipeViewModel,
|
private val recipeViewModel: RecipeViewModel,
|
||||||
private val clickListener: (RecipeSummaryEntity) -> Unit
|
private val clickListener: (RecipeSummaryEntity) -> Unit,
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
private val loadingPlaceholder by lazy {
|
private val loadingPlaceholder by lazy {
|
||||||
binding.root.resources.getString(R.string.view_holder_recipe_text_placeholder)
|
binding.root.resources.getString(R.string.view_holder_recipe_text_placeholder)
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
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 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.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(
|
||||||
private val recipeRepo: RecipeRepo,
|
recipeRepo: RecipeRepo,
|
||||||
private val recipeImageLoader: RecipeImageLoader
|
private val recipeImageLoader: RecipeImageLoader
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private var _isRefreshing = MutableLiveData<Boolean>()
|
|
||||||
val isRefreshing: LiveData<Boolean> get() = _isRefreshing
|
|
||||||
|
|
||||||
private val _nextRecipeInfoChannel = Channel<RecipeSummaryEntity>()
|
val pagingData = recipeRepo.createPager().flow.cachedIn(viewModelScope)
|
||||||
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")
|
||||||
@@ -45,21 +26,4 @@ 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -6,17 +6,16 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import gq.kirmanak.mealient.R
|
import gq.kirmanak.mealient.R
|
||||||
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.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.AuthenticationState
|
||||||
import gq.kirmanak.mealient.ui.auth.AuthenticationViewModel
|
import gq.kirmanak.mealient.ui.auth.AuthenticationViewModel
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@@ -58,20 +57,19 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) {
|
|||||||
|
|
||||||
private fun setupRecipeAdapter() {
|
private fun setupRecipeAdapter() {
|
||||||
Timber.v("setupRecipeAdapter() called")
|
Timber.v("setupRecipeAdapter() called")
|
||||||
binding.recipes.adapter = viewModel.adapter
|
val adapter = RecipesPagingAdapter(viewModel, ::navigateToRecipeInfo)
|
||||||
viewModel.isRefreshing.observe(viewLifecycleOwner) {
|
binding.recipes.adapter = adapter
|
||||||
Timber.d("setupRecipeAdapter: isRefreshing = $it")
|
collectWithViewLifecycle(viewModel.pagingData) {
|
||||||
binding.refresher.isRefreshing = it
|
Timber.v("setupRecipeAdapter: received data update")
|
||||||
|
adapter.submitData(lifecycle, it)
|
||||||
}
|
}
|
||||||
binding.refresher.refreshesLiveData().observe(viewLifecycleOwner) {
|
collectWithViewLifecycle(adapter.onPagesUpdatedFlow) {
|
||||||
Timber.d("setupRecipeAdapter: received refresh request")
|
Timber.v("setupRecipeAdapter: pages updated")
|
||||||
viewModel.adapter.refresh()
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user