From b7bb6c8566232ebaf32fb13e51d2297742c4972e Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sat, 5 Nov 2022 10:40:15 +0100 Subject: [PATCH] Simplify edge case handling --- .../mealient/extensions/FragmentExtensions.kt | 12 ++- .../mealient/ui/recipes/RecipesFragment.kt | 76 ++++++++++--------- app/src/main/res/values-ru/strings.xml | 10 +-- app/src/main/res/values/strings.xml | 10 +-- 4 files changed, 60 insertions(+), 48 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 69ca4c8..b6034cb 100644 --- a/app/src/main/java/gq/kirmanak/mealient/extensions/FragmentExtensions.kt +++ b/app/src/main/java/gq/kirmanak/mealient/extensions/FragmentExtensions.kt @@ -1,5 +1,7 @@ package gq.kirmanak.mealient.extensions +import android.widget.Toast +import androidx.annotation.StringRes import androidx.fragment.app.Fragment import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope @@ -19,4 +21,12 @@ fun Fragment.launchWhenViewResumed( fun Flow.launchIn(lifecycleOwner: LifecycleOwner) { launchIn(lifecycleOwner.lifecycleScope) -} \ No newline at end of file +} + +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 +} 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 280f325..6981ae8 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 @@ -2,12 +2,14 @@ package gq.kirmanak.mealient.ui.recipes import android.os.Bundle import android.view.View -import android.widget.Toast +import androidx.annotation.StringRes import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import androidx.paging.LoadState +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.RecyclerView import by.kirich1409.viewbindingdelegate.viewBinding import dagger.hilt.android.AndroidEntryPoint 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.launchIn import gq.kirmanak.mealient.extensions.refreshRequestFlow +import gq.kirmanak.mealient.extensions.showLongToast import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.ui.activity.MainActivityViewModel import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader @@ -83,7 +86,7 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) { binding.refresher.isRefreshing = false } - observeLoadStateChanges(recipesAdapter) + recipesAdapter.observeLoadStateChanges() collectWhenViewResumed(binding.refresher.refreshRequestFlow(logger)) { logger.v { "setupRecipeAdapter: received refresh request" } @@ -100,50 +103,25 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) { } } - private fun observeLoadStateChanges(recipesAdapter: RecipesPagingAdapter) { - recipesAdapter.loadStateFlow - .map { it.append } - .distinctUntilChanged() - .filter { it.endOfPaginationReached } - .onEach { onPaginationEnd() } - .launchIn(viewLifecycleOwner) - - recipesAdapter.loadStateFlow - .map { it.refresh } - .distinctUntilChanged() - .filterIsInstance() - .onEach { onLoadFailure(it.error) } - .launchIn(viewLifecycleOwner) + private fun PagingDataAdapter.observeLoadStateChanges() { + appendPaginationEnd().onEach { onPaginationEnd() }.launchIn(viewLifecycleOwner) + refreshErrors().onEach { onLoadFailure(it) }.launchIn(viewLifecycleOwner) } private fun onPaginationEnd() { logger.v { "onPaginationEnd() called" } - Toast.makeText( - requireContext(), - getString(R.string.fragment_recipes_last_page_loaded_toast), - Toast.LENGTH_SHORT - ).show() + showLongToast(R.string.fragment_recipes_last_page_loaded_toast) } private fun onLoadFailure(error: Throwable) { - logger.v(error) { "onLoadFailure() called" } - - val reason = when (error) { - 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 - }?.let { getString(it) } - - val generalText = getString(R.string.fragment_recipes_load_failure_toast_general) - - val text = if (reason == null) { - generalText + logger.w(error) { "onLoadFailure() called" } + val reason = error.toLoadErrorReasonText()?.let { getString(it) } + val toastText = if (reason == null) { + getString(R.string.fragment_recipes_load_failure_toast_no_reason) } else { - getString(R.string.fragment_recipes_load_failure_toast, generalText, reason) + getString(R.string.fragment_recipes_load_failure_toast, reason) } - - Toast.makeText(requireContext(), text, Toast.LENGTH_SHORT).show() + showLongToast(toastText) } override fun onDestroyView() { @@ -152,4 +130,28 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) { // Prevent RV leaking through mObservers list in adapter 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 PagingDataAdapter.refreshErrors(): Flow { + return loadStateFlow + .map { it.refresh } + .distinctUntilChanged() + .filterIsInstance() + .map { it.error } +} + +private fun PagingDataAdapter.appendPaginationEnd(): Flow { + return loadStateFlow + .map { it.append.endOfPaginationReached } + .distinctUntilChanged() + .filter { it } + .map { } } \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 6d07ac4..bca5f84 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -42,9 +42,9 @@ Пример: changeme@email.com Пример: demo Последняя страница - %1$s %2$s - Ошибка загрузки. - Неавторизован - Неожиданный ответ - Нет соединения + Ошибка загрузки: %1$s. + неавторизован + неожиданный ответ + нет соединения + Ошибка загрузки. \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6a5c59e..7bd2a7b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -46,9 +46,9 @@ Example: changeme@email.com Example: demo Last page loaded - %1$s %2$s - Load failed. - Unauthorized - Unexpected response - No connection + Load error: %1$s. + Load failed. + unauthorized + unexpected response + no connection \ No newline at end of file