Initial commit
Some checks failed
CI / build (push) Has been cancelled

This commit is contained in:
2026-06-11 11:03:01 +02:00
commit d1ff0e30ba
138 changed files with 5658 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
plugins {
alias(libs.plugins.architecture.android.library)
alias(libs.plugins.architecture.compose)
}
android {
namespace = "com.example.architecture.core.presentation"
}
dependencies {
implementation(project(":core:domain"))
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.kotlinx.coroutines.android)
}

View File

@@ -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)
}

View File

@@ -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)
}
}
}
}

View File

@@ -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
}

View File

@@ -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)
}

View 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>