feat(core:presentation): UiText, ObserveAsEvents, DataError -> UiText (REDI-82)
- UiText sealed interface (DynamicString / StringResource) — Compose-free type so the UI-agnostic presentation module can hold UiText? in state without depending on Compose. - Two resolvers: @Composable UiText.asString() (Compose renderer) and UiText.asString(context) (Views renderer). - ObserveAsEvents: lifecycle-aware one-time event collection on Main.immediate. - DataError.toUiText() covering all displayed cases with else -> unknown; error strings here.
This commit is contained in:
@@ -9,4 +9,8 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation(project(":core:domain"))
|
||||
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.kotlinx.coroutines.android)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.example.architecture.core.presentation
|
||||
|
||||
import com.example.architecture.core.domain.DataError
|
||||
|
||||
/**
|
||||
* Maps a [DataError] to user-facing [UiText]. Every displayed case has its own message; anything
|
||||
* else (including the explicit `UNKNOWN` cases) falls back to a generic message.
|
||||
*/
|
||||
fun DataError.toUiText(): UiText {
|
||||
val resId = when (this) {
|
||||
DataError.Network.NO_INTERNET -> R.string.error_no_internet
|
||||
DataError.Network.REQUEST_TIMEOUT -> R.string.error_request_timeout
|
||||
DataError.Network.UNAUTHORIZED -> R.string.error_unauthorized
|
||||
DataError.Network.FORBIDDEN -> R.string.error_forbidden
|
||||
DataError.Network.NOT_FOUND -> R.string.error_not_found
|
||||
DataError.Network.CONFLICT -> R.string.error_conflict
|
||||
DataError.Network.TOO_MANY_REQUESTS -> R.string.error_too_many_requests
|
||||
DataError.Network.PAYLOAD_TOO_LARGE -> R.string.error_payload_too_large
|
||||
DataError.Network.SERVER_ERROR -> R.string.error_server
|
||||
DataError.Network.SERVICE_UNAVAILABLE -> R.string.error_service_unavailable
|
||||
DataError.Network.SERIALIZATION -> R.string.error_serialization
|
||||
DataError.Network.BAD_REQUEST -> R.string.error_bad_request
|
||||
DataError.Local.DISK_FULL -> R.string.error_disk_full
|
||||
DataError.Local.NOT_FOUND -> R.string.error_not_found
|
||||
else -> R.string.error_unknown
|
||||
}
|
||||
return UiText.StringResource(resId)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.example.architecture.core.presentation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Collects one-time [Flow] events (navigation, snackbars) lifecycle-awarely: only while the
|
||||
* lifecycle is at least STARTED, and on `Main.immediate` so no event is missed during setup.
|
||||
*/
|
||||
@Composable
|
||||
fun <T> ObserveAsEvents(
|
||||
flow: Flow<T>,
|
||||
key1: Any? = null,
|
||||
key2: Any? = null,
|
||||
onEvent: (T) -> Unit,
|
||||
) {
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
LaunchedEffect(flow, lifecycleOwner.lifecycle, key1, key2) {
|
||||
lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
flow.collect(onEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.example.architecture.core.presentation
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
/**
|
||||
* A string the UI will show that either is already concrete ([DynamicString]) or comes from a
|
||||
* string resource ([StringResource], so it can be localized). The type itself is Compose-free, so a
|
||||
* UI-agnostic ViewModel can hold `UiText?` in its state without depending on Compose; the actual
|
||||
* resolution happens in the renderer via [asString].
|
||||
*/
|
||||
sealed interface UiText {
|
||||
data class DynamicString(val value: String) : UiText
|
||||
|
||||
// Not a data class: Array has no structural equals. Compare by identity, like the framework does.
|
||||
class StringResource(
|
||||
@param:StringRes val id: Int,
|
||||
val args: Array<Any> = emptyArray(),
|
||||
) : UiText
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.example.architecture.core.presentation
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
||||
/** Resolves to a [String] inside Compose (used by the Compose renderer). */
|
||||
@Composable
|
||||
fun UiText.asString(): String = when (this) {
|
||||
is UiText.DynamicString -> value
|
||||
is UiText.StringResource -> stringResource(id, *args)
|
||||
}
|
||||
|
||||
/** Resolves to a [String] with a plain [Context] (used by the Views/XML renderer). */
|
||||
fun UiText.asString(context: Context): String = when (this) {
|
||||
is UiText.DynamicString -> value
|
||||
is UiText.StringResource -> context.getString(id, *args)
|
||||
}
|
||||
16
core/presentation/src/main/res/values/strings.xml
Normal file
16
core/presentation/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<resources>
|
||||
<string name="error_no_internet">No internet connection. Check your network and try again.</string>
|
||||
<string name="error_request_timeout">The request timed out. Please try again.</string>
|
||||
<string name="error_unauthorized">You are not authorized. Please sign in again.</string>
|
||||
<string name="error_forbidden">You don\'t have permission to do that.</string>
|
||||
<string name="error_not_found">We couldn\'t find what you were looking for.</string>
|
||||
<string name="error_conflict">That action conflicts with the current state.</string>
|
||||
<string name="error_too_many_requests">Too many requests. Please slow down and try again.</string>
|
||||
<string name="error_payload_too_large">The request was too large.</string>
|
||||
<string name="error_server">Something went wrong on our end. Please try again later.</string>
|
||||
<string name="error_service_unavailable">The service is temporarily unavailable. Please try again later.</string>
|
||||
<string name="error_serialization">We received an unexpected response. Please try again later.</string>
|
||||
<string name="error_bad_request">The request was invalid.</string>
|
||||
<string name="error_disk_full">Your device is out of storage space.</string>
|
||||
<string name="error_unknown">Something went wrong. Please try again.</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user