refactor(commons): migrate from Moko to Compose Multiplatform Resources

Remove Moko Resources dependency that blocks AGP 9.0 migration.
Migrate to built-in Compose Multiplatform Resources which is compatible
with the new KMP Android plugin.

- Remove moko-resources plugin and libraries from commons module
- Move strings.xml from moko-resources/ to composeResources/values/
- Update imports from SharedRes.strings.* to Res.string.*
- Add compose.components.resources dependency to desktopApp
- Configure resource package as com.vitorpamplona.amethyst.commons.resources

Closes #1675

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
nrobi144
2026-01-25 13:29:55 +02:00
parent 8ebba994e8
commit 71e68dc35b
10 changed files with 68 additions and 47 deletions
+6 -7
View File
@@ -5,7 +5,6 @@ plugins {
alias(libs.plugins.androidLibrary) alias(libs.plugins.androidLibrary)
alias(libs.plugins.jetbrainsComposeCompiler) alias(libs.plugins.jetbrainsComposeCompiler)
alias(libs.plugins.composeMultiplatform) alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.mokoResources)
} }
android { android {
@@ -81,9 +80,8 @@ kotlin {
// Immutable collections // Immutable collections
api(libs.kotlinx.collections.immutable) api(libs.kotlinx.collections.immutable)
// Moko Resources for KMP string resources // Compose Multiplatform Resources
api(libs.moko.resources) implementation(compose.components.resources)
api(libs.moko.resources.compose)
} }
} }
@@ -141,7 +139,8 @@ kotlin {
} }
} }
multiplatformResources { compose.resources {
resourcesPackage.set("com.vitorpamplona.amethyst.commons") publicResClass = true
resourcesClassName.set("SharedRes") packageOfResClass = "com.vitorpamplona.amethyst.commons.resources"
generateResClass = always
} }
@@ -34,8 +34,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.SharedRes import com.vitorpamplona.amethyst.commons.resources.Res
import dev.icerock.moko.resources.compose.stringResource import com.vitorpamplona.amethyst.commons.resources.action_refresh
import com.vitorpamplona.amethyst.commons.resources.action_try_again
import com.vitorpamplona.amethyst.commons.resources.error_loading_feed
import com.vitorpamplona.amethyst.commons.resources.feed_empty
import org.jetbrains.compose.resources.stringResource
/** /**
* A centered loading state with a progress indicator and message. * A centered loading state with a progress indicator and message.
@@ -71,7 +75,7 @@ fun EmptyState(
onRefresh: (() -> Unit)? = null, onRefresh: (() -> Unit)? = null,
refreshLabel: String? = null, refreshLabel: String? = null,
) { ) {
val actualRefreshLabel = refreshLabel ?: stringResource(SharedRes.strings.action_refresh) val actualRefreshLabel = refreshLabel ?: stringResource(Res.string.action_refresh)
Column( Column(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
@@ -110,7 +114,7 @@ fun ErrorState(
onRetry: (() -> Unit)? = null, onRetry: (() -> Unit)? = null,
retryLabel: String? = null, retryLabel: String? = null,
) { ) {
val actualRetryLabel = retryLabel ?: stringResource(SharedRes.strings.action_try_again) val actualRetryLabel = retryLabel ?: stringResource(Res.string.action_try_again)
Column( Column(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
@@ -140,7 +144,7 @@ fun FeedEmptyState(
title: String? = null, title: String? = null,
onRefresh: (() -> Unit)? = null, onRefresh: (() -> Unit)? = null,
) { ) {
val actualTitle = title ?: stringResource(SharedRes.strings.feed_empty) val actualTitle = title ?: stringResource(Res.string.feed_empty)
EmptyState( EmptyState(
title = actualTitle, title = actualTitle,
@@ -158,7 +162,7 @@ fun FeedErrorState(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onRetry: (() -> Unit)? = null, onRetry: (() -> Unit)? = null,
) { ) {
val formattedMessage = stringResource(SharedRes.strings.error_loading_feed).format(errorMessage) val formattedMessage = stringResource(Res.string.error_loading_feed).format(errorMessage)
ErrorState( ErrorState(
message = formattedMessage, message = formattedMessage,
@@ -28,8 +28,14 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.SharedRes import com.vitorpamplona.amethyst.commons.resources.Res
import dev.icerock.moko.resources.compose.stringResource import com.vitorpamplona.amethyst.commons.resources.screen_messages_description
import com.vitorpamplona.amethyst.commons.resources.screen_messages_title
import com.vitorpamplona.amethyst.commons.resources.screen_notifications_description
import com.vitorpamplona.amethyst.commons.resources.screen_notifications_title
import com.vitorpamplona.amethyst.commons.resources.screen_search_description
import com.vitorpamplona.amethyst.commons.resources.screen_search_title
import org.jetbrains.compose.resources.stringResource
/** /**
* Generic placeholder screen with title and description. * Generic placeholder screen with title and description.
@@ -60,8 +66,8 @@ fun PlaceholderScreen(
@Composable @Composable
fun SearchPlaceholder(modifier: Modifier = Modifier) { fun SearchPlaceholder(modifier: Modifier = Modifier) {
PlaceholderScreen( PlaceholderScreen(
title = stringResource(SharedRes.strings.screen_search_title), title = stringResource(Res.string.screen_search_title),
description = stringResource(SharedRes.strings.screen_search_description), description = stringResource(Res.string.screen_search_description),
modifier = modifier, modifier = modifier,
) )
} }
@@ -72,8 +78,8 @@ fun SearchPlaceholder(modifier: Modifier = Modifier) {
@Composable @Composable
fun MessagesPlaceholder(modifier: Modifier = Modifier) { fun MessagesPlaceholder(modifier: Modifier = Modifier) {
PlaceholderScreen( PlaceholderScreen(
title = stringResource(SharedRes.strings.screen_messages_title), title = stringResource(Res.string.screen_messages_title),
description = stringResource(SharedRes.strings.screen_messages_description), description = stringResource(Res.string.screen_messages_description),
modifier = modifier, modifier = modifier,
) )
} }
@@ -84,8 +90,8 @@ fun MessagesPlaceholder(modifier: Modifier = Modifier) {
@Composable @Composable
fun NotificationsPlaceholder(modifier: Modifier = Modifier) { fun NotificationsPlaceholder(modifier: Modifier = Modifier) {
PlaceholderScreen( PlaceholderScreen(
title = stringResource(SharedRes.strings.screen_notifications_title), title = stringResource(Res.string.screen_notifications_title),
description = stringResource(SharedRes.strings.screen_notifications_description), description = stringResource(Res.string.screen_notifications_description),
modifier = modifier, modifier = modifier,
) )
} }
+1
View File
@@ -25,6 +25,7 @@ dependencies {
implementation(compose.desktop.currentOs) implementation(compose.desktop.currentOs)
implementation(compose.material3) implementation(compose.material3)
implementation(compose.materialIconsExtended) implementation(compose.materialIconsExtended)
implementation(compose.components.resources)
// Quartz Nostr library (will use JVM target) // Quartz Nostr library (will use JVM target)
implementation(project(":quartz")) implementation(project(":quartz"))
@@ -37,14 +37,16 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.SharedRes import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.login_subtitle_desktop
import com.vitorpamplona.amethyst.commons.resources.login_title
import com.vitorpamplona.amethyst.desktop.account.AccountManager import com.vitorpamplona.amethyst.desktop.account.AccountManager
import com.vitorpamplona.amethyst.desktop.account.AccountState import com.vitorpamplona.amethyst.desktop.account.AccountState
import com.vitorpamplona.amethyst.desktop.ui.auth.LoginCard import com.vitorpamplona.amethyst.desktop.ui.auth.LoginCard
import com.vitorpamplona.amethyst.desktop.ui.auth.NewKeyWarningCard import com.vitorpamplona.amethyst.desktop.ui.auth.NewKeyWarningCard
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
@Composable @Composable
fun LoginScreen( fun LoginScreen(
@@ -61,7 +63,7 @@ fun LoginScreen(
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
) { ) {
Text( Text(
stringResource(SharedRes.strings.login_title), stringResource(Res.string.login_title),
style = MaterialTheme.typography.headlineLarge, style = MaterialTheme.typography.headlineLarge,
color = MaterialTheme.colorScheme.onBackground, color = MaterialTheme.colorScheme.onBackground,
) )
@@ -69,7 +71,7 @@ fun LoginScreen(
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Text( Text(
stringResource(SharedRes.strings.login_subtitle_desktop), stringResource(Res.string.login_subtitle_desktop),
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
@@ -42,8 +42,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.SharedRes import com.vitorpamplona.amethyst.commons.resources.Res
import dev.icerock.moko.resources.compose.stringResource import com.vitorpamplona.amethyst.commons.resources.login_hide_key
import com.vitorpamplona.amethyst.commons.resources.login_key_label
import com.vitorpamplona.amethyst.commons.resources.login_key_placeholder
import com.vitorpamplona.amethyst.commons.resources.login_show_key
import org.jetbrains.compose.resources.stringResource
/** /**
* Text field for entering Nostr keys (nsec or npub) with visibility toggle. * Text field for entering Nostr keys (nsec or npub) with visibility toggle.
@@ -53,8 +57,8 @@ fun KeyInputField(
value: String, value: String,
onValueChange: (String) -> Unit, onValueChange: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
label: String = stringResource(SharedRes.strings.login_key_label), label: String = stringResource(Res.string.login_key_label),
placeholder: String = stringResource(SharedRes.strings.login_key_placeholder), placeholder: String = stringResource(Res.string.login_key_placeholder),
errorMessage: String? = null, errorMessage: String? = null,
) { ) {
var showKey by remember { mutableStateOf(false) } var showKey by remember { mutableStateOf(false) }
@@ -76,7 +80,7 @@ fun KeyInputField(
IconButton(onClick = { showKey = !showKey }) { IconButton(onClick = { showKey = !showKey }) {
Icon( Icon(
if (showKey) Icons.Default.VisibilityOff else Icons.Default.Visibility, if (showKey) Icons.Default.VisibilityOff else Icons.Default.Visibility,
contentDescription = if (showKey) stringResource(SharedRes.strings.login_hide_key) else stringResource(SharedRes.strings.login_show_key), contentDescription = if (showKey) stringResource(Res.string.login_hide_key) else stringResource(Res.string.login_show_key),
) )
} }
}, },
@@ -44,8 +44,12 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.SharedRes import com.vitorpamplona.amethyst.commons.resources.Res
import dev.icerock.moko.resources.compose.stringResource import com.vitorpamplona.amethyst.commons.resources.login_button
import com.vitorpamplona.amethyst.commons.resources.login_card_subtitle
import com.vitorpamplona.amethyst.commons.resources.login_card_title
import com.vitorpamplona.amethyst.commons.resources.login_generate_button
import org.jetbrains.compose.resources.stringResource
/** /**
* Login card with Nostr key input field and action buttons. * Login card with Nostr key input field and action buttons.
@@ -63,8 +67,8 @@ fun LoginCard(
onGenerateNew: () -> Unit, onGenerateNew: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
cardWidth: Dp = 400.dp, cardWidth: Dp = 400.dp,
title: String = stringResource(SharedRes.strings.login_card_title), title: String = stringResource(Res.string.login_card_title),
subtitle: String = stringResource(SharedRes.strings.login_card_subtitle), subtitle: String = stringResource(Res.string.login_card_subtitle),
) { ) {
var keyInput by remember { mutableStateOf("") } var keyInput by remember { mutableStateOf("") }
var errorMessage by remember { mutableStateOf<String?>(null) } var errorMessage by remember { mutableStateOf<String?>(null) }
@@ -121,14 +125,14 @@ fun LoginCard(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
enabled = keyInput.isNotBlank(), enabled = keyInput.isNotBlank(),
) { ) {
Text(stringResource(SharedRes.strings.login_button)) Text(stringResource(Res.string.login_button))
} }
OutlinedButton( OutlinedButton(
onClick = onGenerateNew, onClick = onGenerateNew,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) { ) {
Text(stringResource(SharedRes.strings.login_generate_button)) Text(stringResource(Res.string.login_generate_button))
} }
} }
} }
@@ -37,8 +37,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.SharedRes import com.vitorpamplona.amethyst.commons.resources.Res
import dev.icerock.moko.resources.compose.stringResource import com.vitorpamplona.amethyst.commons.resources.new_key_continue_button
import com.vitorpamplona.amethyst.commons.resources.new_key_public_label
import com.vitorpamplona.amethyst.commons.resources.new_key_secret_label
import com.vitorpamplona.amethyst.commons.resources.new_key_warning_message
import com.vitorpamplona.amethyst.commons.resources.new_key_warning_title
import org.jetbrains.compose.resources.stringResource
/** /**
* Warning card displayed after generating a new Nostr key pair. * Warning card displayed after generating a new Nostr key pair.
@@ -69,7 +74,7 @@ fun NewKeyWarningCard(
modifier = Modifier.padding(24.dp), modifier = Modifier.padding(24.dp),
) { ) {
Text( Text(
stringResource(SharedRes.strings.new_key_warning_title), stringResource(Res.string.new_key_warning_title),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = Color.Red, color = Color.Red,
) )
@@ -77,7 +82,7 @@ fun NewKeyWarningCard(
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
Text( Text(
stringResource(SharedRes.strings.new_key_warning_message), stringResource(Res.string.new_key_warning_message),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
) )
@@ -85,7 +90,7 @@ fun NewKeyWarningCard(
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
Text( Text(
stringResource(SharedRes.strings.new_key_public_label), stringResource(Res.string.new_key_public_label),
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
@@ -95,7 +100,7 @@ fun NewKeyWarningCard(
nsec?.let { secretKey -> nsec?.let { secretKey ->
Text( Text(
stringResource(SharedRes.strings.new_key_secret_label), stringResource(Res.string.new_key_secret_label),
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = Color.Red, color = Color.Red,
) )
@@ -108,7 +113,7 @@ fun NewKeyWarningCard(
onClick = onContinue, onClick = onContinue,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { ) {
Text(stringResource(SharedRes.strings.new_key_continue_button)) Text(stringResource(Res.string.new_key_continue_button))
} }
} }
} }
-4
View File
@@ -40,7 +40,6 @@ lightcompressor-enhanced = "1.6.0"
markdown = "f92ef49c9d" markdown = "f92ef49c9d"
media3 = "1.9.0" media3 = "1.9.0"
mockk = "1.14.7" mockk = "1.14.7"
mokoResources = "0.25.2"
kotlinx-coroutines-test = "1.10.2" kotlinx-coroutines-test = "1.10.2"
navigationCompose = "2.9.6" navigationCompose = "2.9.6"
okhttp = "5.3.2" okhttp = "5.3.2"
@@ -137,8 +136,6 @@ markdown-ui = { group = "com.github.vitorpamplona.compose-richtext", name = "ric
markdown-ui-material3 = { group = "com.github.vitorpamplona.compose-richtext", name = "richtext-ui-material3", version.ref = "markdown" } markdown-ui-material3 = { group = "com.github.vitorpamplona.compose-richtext", name = "richtext-ui-material3", version.ref = "markdown" }
mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }
mockk-android = { group = "io.mockk", name = "mockk-android", version.ref = "mockk" } mockk-android = { group = "io.mockk", name = "mockk-android", version.ref = "mockk" }
moko-resources = { group = "dev.icerock.moko", name = "resources", version.ref = "mokoResources" }
moko-resources-compose = { group = "dev.icerock.moko", name = "resources-compose", version.ref = "mokoResources" }
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines-test"} kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines-test"}
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
okhttpCoroutines = { group = "com.squareup.okhttp3", name = "okhttp-coroutines", version.ref = "okhttp" } okhttpCoroutines = { group = "com.squareup.okhttp3", name = "okhttp-coroutines", version.ref = "okhttp" }
@@ -178,4 +175,3 @@ androidKotlinMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.lib
vanniktech-mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" } vanniktech-mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" }
stability-analyzer = { id = "com.github.skydoves.compose.stability.analyzer", version = "0.6.6" } stability-analyzer = { id = "com.github.skydoves.compose.stability.analyzer", version = "0.6.6" }
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" } composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" }
mokoResources = { id = "dev.icerock.mobile.multiplatform-resources", version.ref = "mokoResources" }