Simplify edge case handling

This commit is contained in:
Kirill Kamakin
2022-11-05 10:40:15 +01:00
parent 33bdaf9726
commit b7bb6c8566
4 changed files with 60 additions and 48 deletions

View File

@@ -1,5 +1,7 @@
package gq.kirmanak.mealient.extensions package gq.kirmanak.mealient.extensions
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@@ -19,4 +21,12 @@ fun Fragment.launchWhenViewResumed(
fun <T> Flow<T>.launchIn(lifecycleOwner: LifecycleOwner) { fun <T> Flow<T>.launchIn(lifecycleOwner: LifecycleOwner) {
launchIn(lifecycleOwner.lifecycleScope) launchIn(lifecycleOwner.lifecycleScope)
} }
fun Fragment.showLongToast(@StringRes text: Int) = showLongToast(getString(text))
fun Fragment.showLongToast(text: String) = showToast(text, Toast.LENGTH_LONG)
private fun Fragment.showToast(text: String, length: Int): Boolean {
return context?.let { Toast.makeText(it, text, length).show() } != null
}

View File

@@ -2,12 +2,14 @@ package gq.kirmanak.mealient.ui.recipes
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.Toast import androidx.annotation.StringRes
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.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.RecyclerView
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
@@ -17,6 +19,7 @@ import gq.kirmanak.mealient.datasource.NetworkError
import gq.kirmanak.mealient.extensions.collectWhenViewResumed import gq.kirmanak.mealient.extensions.collectWhenViewResumed
import gq.kirmanak.mealient.extensions.launchIn import gq.kirmanak.mealient.extensions.launchIn
import gq.kirmanak.mealient.extensions.refreshRequestFlow import gq.kirmanak.mealient.extensions.refreshRequestFlow
import gq.kirmanak.mealient.extensions.showLongToast
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader
@@ -83,7 +86,7 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) {
binding.refresher.isRefreshing = false binding.refresher.isRefreshing = false
} }
observeLoadStateChanges(recipesAdapter) recipesAdapter.observeLoadStateChanges()
collectWhenViewResumed(binding.refresher.refreshRequestFlow(logger)) { collectWhenViewResumed(binding.refresher.refreshRequestFlow(logger)) {
logger.v { "setupRecipeAdapter: received refresh request" } logger.v { "setupRecipeAdapter: received refresh request" }
@@ -100,50 +103,25 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) {
} }
} }
private fun observeLoadStateChanges(recipesAdapter: RecipesPagingAdapter) { private fun <T : Any, VH : RecyclerView.ViewHolder> PagingDataAdapter<T, VH>.observeLoadStateChanges() {
recipesAdapter.loadStateFlow appendPaginationEnd().onEach { onPaginationEnd() }.launchIn(viewLifecycleOwner)
.map { it.append } refreshErrors().onEach { onLoadFailure(it) }.launchIn(viewLifecycleOwner)
.distinctUntilChanged()
.filter { it.endOfPaginationReached }
.onEach { onPaginationEnd() }
.launchIn(viewLifecycleOwner)
recipesAdapter.loadStateFlow
.map { it.refresh }
.distinctUntilChanged()
.filterIsInstance<LoadState.Error>()
.onEach { onLoadFailure(it.error) }
.launchIn(viewLifecycleOwner)
} }
private fun onPaginationEnd() { private fun onPaginationEnd() {
logger.v { "onPaginationEnd() called" } logger.v { "onPaginationEnd() called" }
Toast.makeText( showLongToast(R.string.fragment_recipes_last_page_loaded_toast)
requireContext(),
getString(R.string.fragment_recipes_last_page_loaded_toast),
Toast.LENGTH_SHORT
).show()
} }
private fun onLoadFailure(error: Throwable) { private fun onLoadFailure(error: Throwable) {
logger.v(error) { "onLoadFailure() called" } logger.w(error) { "onLoadFailure() called" }
val reason = error.toLoadErrorReasonText()?.let { getString(it) }
val reason = when (error) { val toastText = if (reason == null) {
is NetworkError.Unauthorized -> R.string.fragment_recipes_load_failure_toast_unauthorized getString(R.string.fragment_recipes_load_failure_toast_no_reason)
is NetworkError.NoServerConnection -> R.string.fragment_recipes_load_failure_toast_no_connection
is NetworkError.NotMealie, is NetworkError.MalformedUrl -> R.string.fragment_recipes_load_failure_toast_unexpected_response
else -> null
}?.let { getString(it) }
val generalText = getString(R.string.fragment_recipes_load_failure_toast_general)
val text = if (reason == null) {
generalText
} else { } else {
getString(R.string.fragment_recipes_load_failure_toast, generalText, reason) getString(R.string.fragment_recipes_load_failure_toast, reason)
} }
showLongToast(toastText)
Toast.makeText(requireContext(), text, Toast.LENGTH_SHORT).show()
} }
override fun onDestroyView() { override fun onDestroyView() {
@@ -152,4 +130,28 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) {
// Prevent RV leaking through mObservers list in adapter // Prevent RV leaking through mObservers list in adapter
binding.recipes.adapter = null binding.recipes.adapter = null
} }
}
@StringRes
private fun Throwable.toLoadErrorReasonText(): Int? = when (this) {
is NetworkError.Unauthorized -> R.string.fragment_recipes_load_failure_toast_unauthorized
is NetworkError.NoServerConnection -> R.string.fragment_recipes_load_failure_toast_no_connection
is NetworkError.NotMealie, is NetworkError.MalformedUrl -> R.string.fragment_recipes_load_failure_toast_unexpected_response
else -> null
}
private fun <T : Any, VH : RecyclerView.ViewHolder> PagingDataAdapter<T, VH>.refreshErrors(): Flow<Throwable> {
return loadStateFlow
.map { it.refresh }
.distinctUntilChanged()
.filterIsInstance<LoadState.Error>()
.map { it.error }
}
private fun <T : Any, VH : RecyclerView.ViewHolder> PagingDataAdapter<T, VH>.appendPaginationEnd(): Flow<Unit> {
return loadStateFlow
.map { it.append.endOfPaginationReached }
.distinctUntilChanged()
.filter { it }
.map { }
} }

View File

@@ -42,9 +42,9 @@
<string name="fragment_authentication_email_input_helper_text">Пример: changeme@email.com</string> <string name="fragment_authentication_email_input_helper_text">Пример: changeme@email.com</string>
<string name="fragment_authentication_password_input_helper_text">Пример: demo</string> <string name="fragment_authentication_password_input_helper_text">Пример: demo</string>
<string name="fragment_recipes_last_page_loaded_toast">Последняя страница</string> <string name="fragment_recipes_last_page_loaded_toast">Последняя страница</string>
<string name="fragment_recipes_load_failure_toast">%1$s %2$s</string> <string name="fragment_recipes_load_failure_toast">Ошибка загрузки: %1$s.</string>
<string name="fragment_recipes_load_failure_toast_general">Ошибка загрузки.</string> <string name="fragment_recipes_load_failure_toast_unauthorized">неавторизован</string>
<string name="fragment_recipes_load_failure_toast_unauthorized">Неавторизован</string> <string name="fragment_recipes_load_failure_toast_unexpected_response">неожиданный ответ</string>
<string name="fragment_recipes_load_failure_toast_unexpected_response">Неожиданный ответ</string> <string name="fragment_recipes_load_failure_toast_no_connection">нет соединения</string>
<string name="fragment_recipes_load_failure_toast_no_connection">Нет соединения</string> <string name="fragment_recipes_load_failure_toast_no_reason">Ошибка загрузки.</string>
</resources> </resources>

View File

@@ -46,9 +46,9 @@
<string name="fragment_authentication_email_input_helper_text">Example: changeme@email.com</string> <string name="fragment_authentication_email_input_helper_text">Example: changeme@email.com</string>
<string name="fragment_authentication_password_input_helper_text">Example: demo</string> <string name="fragment_authentication_password_input_helper_text">Example: demo</string>
<string name="fragment_recipes_last_page_loaded_toast">Last page loaded</string> <string name="fragment_recipes_last_page_loaded_toast">Last page loaded</string>
<string name="fragment_recipes_load_failure_toast" comment="EXAMPLE: Load failed! Unauthorized">%1$s %2$s</string> <string name="fragment_recipes_load_failure_toast" comment="EXAMPLE: Load error: unauthorized.">Load error: %1$s.</string>
<string name="fragment_recipes_load_failure_toast_general">Load failed.</string> <string name="fragment_recipes_load_failure_toast_no_reason">Load failed.</string>
<string name="fragment_recipes_load_failure_toast_unauthorized">Unauthorized</string> <string name="fragment_recipes_load_failure_toast_unauthorized">unauthorized</string>
<string name="fragment_recipes_load_failure_toast_unexpected_response">Unexpected response</string> <string name="fragment_recipes_load_failure_toast_unexpected_response">unexpected response</string>
<string name="fragment_recipes_load_failure_toast_no_connection">No connection</string> <string name="fragment_recipes_load_failure_toast_no_connection">no connection</string>
</resources> </resources>