refactor: Simplify use of BackgroundMessageView (#539)

Previous code expected callers to typically provide the drawable and the
error message string resource, resulting in duplicate code at many
callsites.

Replace with three canned messages for empty containers, generic errors,
and network errors respectively. The images for these are fixed, the
caller may choose a different string resource for the error if there is
a more specific option.

Update and simplify the call sites.
This commit is contained in:
Nik Clayton 2024-03-16 22:12:54 +01:00 committed by GitHub
parent e41722e16f
commit 9e0a1f4015
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 86 additions and 83 deletions

View File

@ -42,6 +42,7 @@ import app.pachli.core.designsystem.R as DR
import app.pachli.core.navigation.AttachmentViewData
import app.pachli.core.navigation.ViewMediaActivityIntent
import app.pachli.core.network.model.Attachment
import app.pachli.core.ui.BackgroundMessage
import app.pachli.databinding.FragmentTimelineBinding
import com.google.android.material.color.MaterialColors
import com.mikepenz.iconics.IconicsDrawable
@ -113,7 +114,7 @@ class AccountMediaFragment :
is LoadState.NotLoading -> {
if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) {
binding.statusView.show()
binding.statusView.setup(app.pachli.core.ui.R.drawable.elephant_friend_empty, app.pachli.core.ui.R.string.message_empty, null)
binding.statusView.setup(BackgroundMessage.Empty())
}
}
is LoadState.Error -> {

View File

@ -54,6 +54,7 @@ import app.pachli.core.network.model.TimelineAccount
import app.pachli.core.network.retrofit.MastodonApi
import app.pachli.core.preferences.PrefKeys
import app.pachli.core.preferences.SharedPreferencesRepository
import app.pachli.core.ui.BackgroundMessage
import app.pachli.databinding.FragmentAccountListBinding
import app.pachli.interfaces.AccountActionListener
import app.pachli.interfaces.AppBarLayoutHost
@ -376,11 +377,7 @@ class AccountListFragment :
if (adapter.itemCount == 0) {
binding.messageView.show()
binding.messageView.setup(
app.pachli.core.ui.R.drawable.elephant_friend_empty,
app.pachli.core.ui.R.string.message_empty,
null,
)
binding.messageView.setup(BackgroundMessage.Empty())
} else {
binding.messageView.hide()
}

View File

@ -35,6 +35,7 @@ import app.pachli.core.common.extensions.viewBinding
import app.pachli.core.common.util.unsafeLazy
import app.pachli.core.navigation.StatusListActivityIntent
import app.pachli.core.preferences.PrefKeys
import app.pachli.core.ui.BackgroundMessage
import app.pachli.databinding.ActivityAnnouncementsBinding
import app.pachli.util.Error
import app.pachli.util.Loading
@ -109,7 +110,7 @@ class AnnouncementsActivity :
binding.progressBar.hide()
binding.swipeRefreshLayout.isRefreshing = false
if (it.data.isNullOrEmpty()) {
binding.errorMessageView.setup(app.pachli.core.ui.R.drawable.elephant_friend_empty, R.string.no_announcements)
binding.errorMessageView.setup(BackgroundMessage.Empty(R.string.no_announcements))
binding.errorMessageView.show()
} else {
binding.errorMessageView.hide()
@ -122,7 +123,7 @@ class AnnouncementsActivity :
is Error -> {
binding.progressBar.hide()
binding.swipeRefreshLayout.isRefreshing = false
binding.errorMessageView.setup(app.pachli.core.ui.R.drawable.errorphant_error, app.pachli.core.ui.R.string.error_generic) {
binding.errorMessageView.setup(BackgroundMessage.GenericError()) {
refreshAnnouncements()
}
binding.errorMessageView.show()

View File

@ -49,6 +49,7 @@ import app.pachli.core.network.model.Poll
import app.pachli.core.network.model.Status
import app.pachli.core.preferences.PrefKeys
import app.pachli.core.preferences.SharedPreferencesRepository
import app.pachli.core.ui.BackgroundMessage
import app.pachli.databinding.FragmentTimelineBinding
import app.pachli.fragment.SFragment
import app.pachli.interfaces.ActionButtonActivity
@ -126,11 +127,7 @@ class ConversationsFragment :
binding.swipeRefreshLayout.isRefreshing = false
if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) {
binding.statusView.show()
binding.statusView.setup(
app.pachli.core.ui.R.drawable.elephant_friend_empty,
app.pachli.core.ui.R.string.message_empty,
null,
)
binding.statusView.setup(BackgroundMessage.Empty())
}
}

View File

@ -30,6 +30,7 @@ import app.pachli.core.database.model.DraftEntity
import app.pachli.core.navigation.ComposeActivityIntent
import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions
import app.pachli.core.network.parseAsMastodonHtml
import app.pachli.core.ui.BackgroundMessage
import app.pachli.databinding.ActivityDraftsBinding
import app.pachli.db.DraftsAlert
import at.connyduck.calladapter.networkresult.fold
@ -67,7 +68,7 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
setDisplayShowHomeEnabled(true)
}
binding.draftsErrorMessageView.setup(app.pachli.core.ui.R.drawable.elephant_friend_empty, R.string.no_drafts)
binding.draftsErrorMessageView.setup(BackgroundMessage.Empty(R.string.no_drafts))
val adapter = DraftsAdapter(this)

View File

@ -13,6 +13,7 @@ import app.pachli.core.common.extensions.visible
import app.pachli.core.designsystem.R as DR
import app.pachli.core.navigation.EditFilterActivityIntent
import app.pachli.core.network.model.Filter
import app.pachli.core.ui.BackgroundMessage
import app.pachli.databinding.ActivityFiltersBinding
import com.google.android.material.color.MaterialColors
import dagger.hilt.android.AndroidEntryPoint
@ -62,13 +63,13 @@ class FiltersActivity : BaseActivity(), FiltersListener {
when (state.loadingState) {
FiltersViewModel.LoadingState.INITIAL, FiltersViewModel.LoadingState.LOADING -> binding.messageView.hide()
FiltersViewModel.LoadingState.ERROR_NETWORK -> {
binding.messageView.setup(app.pachli.core.ui.R.drawable.errorphant_offline, app.pachli.core.ui.R.string.error_network) {
binding.messageView.setup(BackgroundMessage.Network()) {
loadFilters()
}
binding.messageView.show()
}
FiltersViewModel.LoadingState.ERROR_OTHER -> {
binding.messageView.setup(app.pachli.core.ui.R.drawable.errorphant_error, app.pachli.core.ui.R.string.error_generic) {
binding.messageView.setup(BackgroundMessage.GenericError()) {
loadFilters()
}
binding.messageView.show()
@ -76,11 +77,7 @@ class FiltersActivity : BaseActivity(), FiltersListener {
FiltersViewModel.LoadingState.LOADED -> {
binding.filtersList.adapter = FiltersAdapter(this@FiltersActivity, state.filters)
if (state.filters.isEmpty()) {
binding.messageView.setup(
app.pachli.core.ui.R.drawable.elephant_friend_empty,
app.pachli.core.ui.R.string.message_empty,
null,
)
binding.messageView.setup(BackgroundMessage.Empty())
binding.messageView.show()
} else {
binding.messageView.hide()

View File

@ -14,6 +14,7 @@ import app.pachli.core.common.extensions.show
import app.pachli.core.common.extensions.viewBinding
import app.pachli.core.network.model.HttpHeaderLink
import app.pachli.core.network.retrofit.MastodonApi
import app.pachli.core.ui.BackgroundMessage
import app.pachli.databinding.FragmentInstanceListBinding
import app.pachli.view.EndlessOnScrollListener
import at.connyduck.calladapter.networkresult.fold
@ -123,11 +124,7 @@ class InstanceListFragment :
if (adapter.itemCount == 0) {
binding.messageView.show()
binding.messageView.setup(
app.pachli.core.ui.R.drawable.elephant_friend_empty,
app.pachli.core.ui.R.string.message_empty,
null,
)
binding.messageView.setup(BackgroundMessage.Empty())
} else {
binding.messageView.hide()
}

View File

@ -56,6 +56,7 @@ import app.pachli.core.network.model.Filter
import app.pachli.core.network.model.Notification
import app.pachli.core.network.model.Poll
import app.pachli.core.network.model.Status
import app.pachli.core.ui.BackgroundMessage
import app.pachli.databinding.FragmentTimelineNotificationsBinding
import app.pachli.fragment.SFragment
import app.pachli.interfaces.AccountActionListener
@ -420,10 +421,7 @@ class NotificationsFragment :
binding.statusView.hide()
if (loadState.refresh is LoadState.NotLoading) {
if (adapter.itemCount == 0) {
binding.statusView.setup(
app.pachli.core.ui.R.drawable.elephant_friend_empty,
app.pachli.core.ui.R.string.message_empty,
)
binding.statusView.setup(BackgroundMessage.Empty())
binding.recyclerView.hide()
binding.statusView.show()
} else {

View File

@ -36,6 +36,7 @@ import app.pachli.core.common.extensions.viewBinding
import app.pachli.core.navigation.ComposeActivityIntent
import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions
import app.pachli.core.network.model.ScheduledStatus
import app.pachli.core.ui.BackgroundMessage
import app.pachli.databinding.ActivityScheduledStatusBinding
import com.google.android.material.color.MaterialColors
import com.google.android.material.divider.MaterialDividerItemDecoration
@ -107,7 +108,7 @@ class ScheduledStatusActivity :
if (loadState.refresh is LoadState.NotLoading) {
binding.progressBar.hide()
if (adapter.itemCount == 0) {
binding.errorMessageView.setup(app.pachli.core.ui.R.drawable.elephant_friend_empty, R.string.no_scheduled_posts)
binding.errorMessageView.setup(BackgroundMessage.Empty(R.string.no_scheduled_posts))
binding.errorMessageView.show()
} else {
binding.errorMessageView.hide()

View File

@ -60,7 +60,7 @@ import app.pachli.core.navigation.AttachmentViewData
import app.pachli.core.network.model.Poll
import app.pachli.core.network.model.Status
import app.pachli.core.network.model.TimelineKind
import app.pachli.core.ui.extensions.getDrawableRes
import app.pachli.core.ui.BackgroundMessage
import app.pachli.core.ui.extensions.getErrorString
import app.pachli.databinding.FragmentTimelineBinding
import app.pachli.fragment.SFragment
@ -441,8 +441,7 @@ class TimelineFragment :
.setAction(app.pachli.core.ui.R.string.action_retry) { adapter.retry() }
snackbar!!.show()
} else {
val drawableRes = error.getDrawableRes()
binding.statusView.setup(drawableRes, message) {
binding.statusView.setup(error) {
snackbar?.dismiss()
adapter.retry()
}
@ -453,10 +452,7 @@ class TimelineFragment :
PresentationState.PRESENTED -> {
if (adapter.itemCount == 0) {
binding.statusView.setup(
app.pachli.core.ui.R.drawable.elephant_friend_empty,
app.pachli.core.ui.R.string.message_empty,
)
binding.statusView.setup(BackgroundMessage.Empty())
if (timelineKind == TimelineKind.Home) {
binding.statusView.showHelp(R.string.help_empty_home)
}

View File

@ -43,6 +43,7 @@ import app.pachli.core.common.extensions.hide
import app.pachli.core.common.extensions.show
import app.pachli.core.common.extensions.viewBinding
import app.pachli.core.designsystem.R as DR
import app.pachli.core.ui.BackgroundMessage
import app.pachli.databinding.FragmentTrendingLinksBinding
import app.pachli.interfaces.ActionButtonActivity
import app.pachli.interfaces.AppBarLayoutHost
@ -110,11 +111,7 @@ class TrendingLinksFragment :
binding.progressBar.hide()
binding.swipeRefreshLayout.isRefreshing = false
if (it.data.isEmpty()) {
binding.messageView.setup(
app.pachli.core.ui.R.drawable.elephant_friend_empty,
app.pachli.core.ui.R.string.message_empty,
null,
)
binding.messageView.setup(BackgroundMessage.Empty())
binding.messageView.show()
} else {
binding.messageView.hide()

View File

@ -44,6 +44,7 @@ import app.pachli.core.common.extensions.show
import app.pachli.core.common.extensions.viewBinding
import app.pachli.core.designsystem.R as DR
import app.pachli.core.navigation.StatusListActivityIntent
import app.pachli.core.ui.BackgroundMessage
import app.pachli.databinding.FragmentTrendingTagsBinding
import app.pachli.interfaces.ActionButtonActivity
import app.pachli.interfaces.AppBarLayoutHost
@ -199,11 +200,7 @@ class TrendingTagsFragment :
if (viewData.isEmpty()) {
binding.recyclerView.hide()
binding.messageView.show()
binding.messageView.setup(
app.pachli.core.ui.R.drawable.elephant_friend_empty,
app.pachli.core.ui.R.string.message_empty,
null,
)
binding.messageView.setup(BackgroundMessage.Empty())
} else {
binding.recyclerView.show()
binding.messageView.hide()
@ -233,10 +230,7 @@ class TrendingTagsFragment :
binding.progressBar.hide()
binding.swipeRefreshLayout.isRefreshing = false
binding.messageView.setup(
app.pachli.core.ui.R.drawable.errorphant_offline,
app.pachli.core.ui.R.string.error_network,
) { refreshContent() }
binding.messageView.setup(BackgroundMessage.Network()) { refreshContent() }
}
private fun otherError() {
@ -245,10 +239,7 @@ class TrendingTagsFragment :
binding.progressBar.hide()
binding.swipeRefreshLayout.isRefreshing = false
binding.messageView.setup(
app.pachli.core.ui.R.drawable.errorphant_error,
app.pachli.core.ui.R.string.error_generic,
) { refreshContent() }
binding.messageView.setup(BackgroundMessage.GenericError()) { refreshContent() }
}
private fun actionButtonPresent(): Boolean {

View File

@ -42,6 +42,7 @@ import app.pachli.core.navigation.AccountActivityIntent
import app.pachli.core.navigation.StatusListActivityIntent
import app.pachli.core.preferences.PrefKeys
import app.pachli.core.preferences.SharedPreferencesRepository
import app.pachli.core.ui.BackgroundMessage
import app.pachli.databinding.FragmentViewEditsBinding
import app.pachli.interfaces.LinkListener
import com.google.android.material.color.MaterialColors
@ -112,10 +113,7 @@ class ViewEditsFragment :
when (uiState.throwable) {
is ViewEditsViewModel.MissingEditsException -> {
binding.statusView.setup(
app.pachli.core.ui.R.drawable.elephant_friend_empty,
R.string.error_missing_edits,
)
binding.statusView.setup(BackgroundMessage.Empty(R.string.error_missing_edits))
}
else -> {
binding.statusView.setup(uiState.throwable) {

View File

@ -40,6 +40,41 @@ import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import java.util.regex.Pattern
/** Kinds of message that can be shown */
sealed interface BackgroundMessage {
@get:DrawableRes val drawableRes: Int
@get:StringRes val stringRes: Int
/**
* Generic "Nothing here" message and image, for use when a collection has
* no items.
*
* @param stringRes Alternative string resource to use as the message
*/
data class Empty(override val stringRes: Int = R.string.message_empty) : BackgroundMessage {
override val drawableRes: Int = R.drawable.elephant_friend_empty
}
/**
* Generic "An error occurred" message and image
*
* @param stringRes Alternative string resource to use as the message
*/
data class GenericError(override val stringRes: Int = R.string.error_generic) : BackgroundMessage {
override val drawableRes: Int = R.drawable.errorphant_error
}
/**
* Generic "A network error occurred" message and image
*
* @param stringRes Alternative string resource to use as the message
*/
data class Network(override val stringRes: Int = R.string.error_network) : BackgroundMessage {
override val drawableRes: Int = R.drawable.errorphant_offline
}
}
/**
* This view is used for screens with content which may be empty or might have failed to download.
*/
@ -56,7 +91,7 @@ class BackgroundMessageView @JvmOverloads constructor(
orientation = VERTICAL
if (isInEditMode) {
setup(app.pachli.core.ui.R.drawable.errorphant_offline, app.pachli.core.ui.R.string.error_network) {}
setup(BackgroundMessage.Network())
}
}
@ -64,7 +99,11 @@ class BackgroundMessageView @JvmOverloads constructor(
setup(throwable.getDrawableRes(), throwable.getErrorString(context), listener)
}
fun setup(
fun setup(message: BackgroundMessage, listener: ((v: View) -> Unit)? = null) {
setup(message.drawableRes, message.stringRes, listener)
}
private fun setup(
@DrawableRes imageRes: Int,
@StringRes messageRes: Int,
clickListener: ((v: View) -> Unit)? = null,
@ -74,7 +113,7 @@ class BackgroundMessageView @JvmOverloads constructor(
* Setup image, message and button.
* If [clickListener] is `null` then the button will be hidden.
*/
fun setup(
private fun setup(
@DrawableRes imageRes: Int,
message: String,
clickListener: ((v: View) -> Unit)? = null,

View File

@ -25,20 +25,20 @@ import retrofit2.HttpException
/** @return A drawable resource to accompany the error message for this throwable */
fun Throwable.getDrawableRes(): Int = when (this) {
is IOException -> app.pachli.core.ui.R.drawable.errorphant_offline
is IOException -> R.drawable.errorphant_offline
is HttpException -> {
if (this.code() == 404) {
app.pachli.core.ui.R.drawable.elephant_friend_empty
R.drawable.elephant_friend_empty
} else {
app.pachli.core.ui.R.drawable.errorphant_offline
R.drawable.errorphant_offline
}
}
else -> app.pachli.core.ui.R.drawable.errorphant_error
else -> R.drawable.errorphant_error
}
/** @return A string error message for this throwable */
fun Throwable.getErrorString(context: Context): String = getServerErrorMessage() ?: when (this) {
is IOException -> context.getString(app.pachli.core.ui.R.string.error_network_fmt, this.message)
is IOException -> context.getString(R.string.error_network_fmt, this.message)
is HttpException -> if (this.code() == 404) context.getString(R.string.error_404_not_found_fmt, this.message) else context.getString(R.string.error_generic_fmt, this.message)
else -> context.getString(R.string.error_generic_fmt, this.message)
}

View File

@ -202,7 +202,7 @@ class AccountsInListFragment : DialogFragment() {
private fun handleError(error: Throwable) {
binding.messageView.show()
binding.messageView.setup(error) { _: View ->
binding.messageView.setup(error) {
binding.messageView.hide()
viewModel.refresh()
}

View File

@ -49,6 +49,7 @@ import app.pachli.core.network.model.MastoList
import app.pachli.core.network.model.UserListRepliesPolicy
import app.pachli.core.network.retrofit.apiresult.ApiError
import app.pachli.core.network.retrofit.apiresult.NetworkError
import app.pachli.core.ui.BackgroundMessage
import app.pachli.core.ui.extensions.await
import app.pachli.feature.lists.databinding.ActivityListsBinding
import app.pachli.feature.lists.databinding.DialogListBinding
@ -205,13 +206,9 @@ class ListsActivity : BaseActivity(), MenuProvider {
binding.swipeRefreshLayout.isRefreshing = false
if (it is NetworkError) {
binding.messageView.setup(app.pachli.core.ui.R.drawable.errorphant_offline, app.pachli.core.ui.R.string.error_network) {
viewModel.refresh()
}
binding.messageView.setup(BackgroundMessage.Network()) { viewModel.refresh() }
} else {
binding.messageView.setup(app.pachli.core.ui.R.drawable.errorphant_error, app.pachli.core.ui.R.string.error_generic) {
viewModel.refresh()
}
binding.messageView.setup(BackgroundMessage.GenericError()) { viewModel.refresh() }
}
}
@ -222,11 +219,7 @@ class ListsActivity : BaseActivity(), MenuProvider {
binding.swipeRefreshLayout.isRefreshing = false
if (lists.lists.isEmpty()) {
binding.messageView.show()
binding.messageView.setup(
app.pachli.core.ui.R.drawable.elephant_friend_empty,
app.pachli.core.ui.R.string.message_empty,
null,
)
binding.messageView.setup(BackgroundMessage.Empty())
} else {
binding.messageView.hide()
}

View File

@ -33,6 +33,7 @@ import app.pachli.core.common.extensions.show
import app.pachli.core.common.extensions.viewBinding
import app.pachli.core.common.extensions.visible
import app.pachli.core.designsystem.R as DR
import app.pachli.core.ui.BackgroundMessage
import app.pachli.core.ui.BindingHolder
import app.pachli.feature.lists.ListsForAccountViewModel.Error
import app.pachli.feature.lists.ListsForAccountViewModel.FlowError
@ -140,9 +141,7 @@ class ListsForAccountFragment : DialogFragment() {
binding.progressBar.hide()
if (it.listsWithMembership.isEmpty()) {
binding.messageView.show()
binding.messageView.setup(app.pachli.core.ui.R.drawable.elephant_friend_empty, R.string.no_lists) {
load()
}
binding.messageView.setup(BackgroundMessage.Empty(R.string.no_lists)) { load() }
} else {
binding.listsView.show()
adapter.submitList(it.listsWithMembership.values.toList())