Simplify edge case handling
This commit is contained in:
@@ -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
|
||||||
@@ -20,3 +22,11 @@ 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
@@ -153,3 +131,27 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) {
|
|||||||
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 { }
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user