From 4d8872db9c0e71ec290fe299a23c1e63e51329ff Mon Sep 17 00:00:00 2001 From: Cilly Leang Date: Fri, 23 Jan 2026 23:15:40 +1100 Subject: [PATCH] feat: basic repo + di --- composeApp/build.gradle.kts | 4 ++ .../kotlin/moe/lava/neon/MainActivity.kt | 4 ++ .../commonMain/kotlin/moe/lava/neon/ui/App.kt | 54 ++++++++++++------- .../kotlin/moe/lava/neon/ui/di/AppUiGraph.kt | 29 ++++++++++ .../kotlin/moe/lava/neon/ui/screens/Login.kt | 47 +++++++++++++++- .../kotlin/moe/lava/neon/ui/screens/Sample.kt | 23 +++++++- .../kotlin/moe/lava/neon/core/di/AppGraph.kt | 14 +++++ .../neon/core/repository/AuthRepository.kt | 19 +++++++ .../neon/core/repository/UserRepository.kt | 10 ++++ gradle/libs.versions.toml | 3 ++ 10 files changed, 185 insertions(+), 22 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/moe/lava/neon/ui/di/AppUiGraph.kt create mode 100644 core/src/commonMain/kotlin/moe/lava/neon/core/di/AppGraph.kt create mode 100644 core/src/commonMain/kotlin/moe/lava/neon/core/repository/AuthRepository.kt create mode 100644 core/src/commonMain/kotlin/moe/lava/neon/core/repository/UserRepository.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 44ec581..f526867 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -40,6 +40,10 @@ kotlin { implementation(libs.androidx.nav3.ui) implementation(libs.compose.material3.adaptive) implementation(libs.compose.material3.adaptive.nav3) + + implementation(libs.metrox.viewmodel.compose) + + implementation(libs.kermit) } commonTest.dependencies { implementation(libs.kotlin.test) diff --git a/composeApp/src/androidMain/kotlin/moe/lava/neon/MainActivity.kt b/composeApp/src/androidMain/kotlin/moe/lava/neon/MainActivity.kt index 5c350b6..050f8b0 100644 --- a/composeApp/src/androidMain/kotlin/moe/lava/neon/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/moe/lava/neon/MainActivity.kt @@ -5,8 +5,12 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.tooling.preview.Preview import androidx.navigation3.ui.defaultPredictivePopTransitionSpec +import dev.zacsweers.metro.Inject +import dev.zacsweers.metrox.viewmodel.LocalMetroViewModelFactory +import dev.zacsweers.metrox.viewmodel.MetroViewModelFactory import moe.lava.neon.ui.App class MainActivity : ComponentActivity() { diff --git a/composeApp/src/commonMain/kotlin/moe/lava/neon/ui/App.kt b/composeApp/src/commonMain/kotlin/moe/lava/neon/ui/App.kt index 8e5465b..aae905c 100644 --- a/composeApp/src/commonMain/kotlin/moe/lava/neon/ui/App.kt +++ b/composeApp/src/commonMain/kotlin/moe/lava/neon/ui/App.kt @@ -2,14 +2,21 @@ package moe.lava.neon.ui import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator import androidx.navigation3.ui.NavDisplay import androidx.savedstate.serialization.SavedStateConfiguration +import dev.zacsweers.metro.createGraph +import dev.zacsweers.metrox.viewmodel.LocalMetroViewModelFactory import kotlinx.serialization.Serializable import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.polymorphic +import moe.lava.neon.core.di.AppGraph +import moe.lava.neon.ui.di.AppUiGraph import moe.lava.neon.ui.screens.Login import moe.lava.neon.ui.screens.Sample @@ -18,7 +25,7 @@ private object Route { data object Login : NavKey @Serializable - data class Sample(val token: String) : NavKey + data object Sample : NavKey } private val config = SavedStateConfiguration { @@ -30,25 +37,36 @@ private val config = SavedStateConfiguration { } } + @Composable fun App() { - MaterialTheme { - val backStack = rememberNavBackStack(config, Route.Login) - NavDisplay( - backStack = backStack, - onBack = { backStack.removeLastOrNull() }, - entryProvider = entryProvider { - entry { - Login( - onSuccess = { token -> - backStack.add(Route.Sample(token)) - } - ) + val uiGraph = createGraph() + val graph = uiGraph.core + CompositionLocalProvider(LocalMetroViewModelFactory provides uiGraph.metroViewModelFactory) { + MaterialTheme { + val init = if (graph.auth.token != null) Route.Sample else Route.Login + val backStack = rememberNavBackStack(config, init) + NavDisplay( + backStack = backStack, + entryDecorators = listOf( + rememberSaveableStateHolderNavEntryDecorator(), + rememberViewModelStoreNavEntryDecorator(), + ), + onBack = { backStack.removeLastOrNull() }, + entryProvider = entryProvider { + entry { + Login( + onSuccess = { + backStack.clear() + backStack.add(Route.Sample) + } + ) + } + entry { key -> + Sample() + } } - entry { key -> - Sample(key.token) - } - } - ) + ) + } } } diff --git a/composeApp/src/commonMain/kotlin/moe/lava/neon/ui/di/AppUiGraph.kt b/composeApp/src/commonMain/kotlin/moe/lava/neon/ui/di/AppUiGraph.kt new file mode 100644 index 0000000..344555a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/moe/lava/neon/ui/di/AppUiGraph.kt @@ -0,0 +1,29 @@ +package moe.lava.neon.ui.di + +import androidx.lifecycle.ViewModel +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.DependencyGraph +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.Provider +import dev.zacsweers.metro.SingleIn +import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactory +import dev.zacsweers.metrox.viewmodel.MetroViewModelFactory +import dev.zacsweers.metrox.viewmodel.ViewModelAssistedFactory +import dev.zacsweers.metrox.viewmodel.ViewModelGraph +import moe.lava.neon.core.di.AppGraph +import kotlin.reflect.KClass + +@DependencyGraph(AppScope::class) +interface AppUiGraph : ViewModelGraph { + val core: AppGraph +} + +@Inject +@ContributesBinding(AppScope::class) +@SingleIn(AppScope::class) +class AppViewModelFactory( + override val viewModelProviders: Map, Provider>, + override val assistedFactoryProviders: Map, Provider>, + override val manualAssistedFactoryProviders: Map, Provider>, +) : MetroViewModelFactory() diff --git a/composeApp/src/commonMain/kotlin/moe/lava/neon/ui/screens/Login.kt b/composeApp/src/commonMain/kotlin/moe/lava/neon/ui/screens/Login.kt index 83ae255..6edb676 100644 --- a/composeApp/src/commonMain/kotlin/moe/lava/neon/ui/screens/Login.kt +++ b/composeApp/src/commonMain/kotlin/moe/lava/neon/ui/screens/Login.kt @@ -9,14 +9,28 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.lifecycle.ViewModel +import co.touchlab.kermit.Logger +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesIntoMap +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.Inject +import dev.zacsweers.metrox.viewmodel.ViewModelKey +import dev.zacsweers.metrox.viewmodel.metroViewModel +import kotlinx.coroutines.launch +import moe.lava.neon.core.repository.AuthRepository @Composable fun Login( - onSuccess: (token: String) -> Unit, + onSuccess: () -> Unit, ) { + val viewModel: LoginViewModel = metroViewModel() + val scope = rememberCoroutineScope() + Column( modifier = Modifier // .background(MaterialTheme.colorScheme.primaryContainer) @@ -31,8 +45,37 @@ fun Login( onValueChange = { token = it }, label = { Text("Enter token") }, ) - Button(onClick = { onSuccess(token) }) { + Button(onClick = { + scope.launch { + val res = viewModel.login(token) + when (res) { + LoginViewModel.LoginResult.Failed -> {} + LoginViewModel.LoginResult.Success -> onSuccess() + } + } + }) { Text("Submit") } } } + +@Inject +@ViewModelKey(LoginViewModel::class) +@ContributesIntoMap(AppScope::class) +class LoginViewModel( + private val auth: AuthRepository +) : ViewModel() { + sealed interface LoginResult { + data object Failed : LoginResult + data object Success : LoginResult + } + + suspend fun login(token: String): LoginResult { + return try { + auth.login(token) + LoginResult.Success + } catch(_: Throwable) { + LoginResult.Failed + } + } +} diff --git a/composeApp/src/commonMain/kotlin/moe/lava/neon/ui/screens/Sample.kt b/composeApp/src/commonMain/kotlin/moe/lava/neon/ui/screens/Sample.kt index 8a7b4a9..edb321f 100644 --- a/composeApp/src/commonMain/kotlin/moe/lava/neon/ui/screens/Sample.kt +++ b/composeApp/src/commonMain/kotlin/moe/lava/neon/ui/screens/Sample.kt @@ -17,13 +17,23 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.lifecycle.ViewModel +import co.touchlab.kermit.Logger +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesIntoMap +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.Inject +import dev.zacsweers.metrox.viewmodel.ViewModelKey +import dev.zacsweers.metrox.viewmodel.metroViewModel +import moe.lava.neon.core.repository.AuthRepository import moe.lava.neon.resources.Res import moe.lava.neon.resources.compose_multiplatform import moe.lava.neon.ui.Greeting import org.jetbrains.compose.resources.painterResource @Composable -fun Sample(token: String) { +fun Sample() { + val viewModel: SampleViewModel = metroViewModel() var showContent by remember { mutableStateOf(false) } Column( modifier = Modifier @@ -43,8 +53,17 @@ fun Sample(token: String) { ) { Image(painterResource(Res.drawable.compose_multiplatform), null) Text("Compose: $greeting") - Text("Passed token: $token") + Text("Passed token: ${viewModel.token}") } } } } + +@Inject +@ViewModelKey(SampleViewModel::class) +@ContributesIntoMap(AppScope::class) +class SampleViewModel( + private val auth: AuthRepository +) : ViewModel() { + val token get() = auth.token +} diff --git a/core/src/commonMain/kotlin/moe/lava/neon/core/di/AppGraph.kt b/core/src/commonMain/kotlin/moe/lava/neon/core/di/AppGraph.kt new file mode 100644 index 0000000..d7552b9 --- /dev/null +++ b/core/src/commonMain/kotlin/moe/lava/neon/core/di/AppGraph.kt @@ -0,0 +1,14 @@ +package moe.lava.neon.core.di + +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.DependencyGraph +import dev.zacsweers.metro.GraphExtension +import dev.zacsweers.metro.SingleIn +import moe.lava.neon.core.repository.AuthRepository +import moe.lava.neon.core.repository.UserRepository + +@GraphExtension +interface AppGraph { + val auth: AuthRepository + val users: UserRepository +} diff --git a/core/src/commonMain/kotlin/moe/lava/neon/core/repository/AuthRepository.kt b/core/src/commonMain/kotlin/moe/lava/neon/core/repository/AuthRepository.kt new file mode 100644 index 0000000..dc5f86e --- /dev/null +++ b/core/src/commonMain/kotlin/moe/lava/neon/core/repository/AuthRepository.kt @@ -0,0 +1,19 @@ +package moe.lava.neon.core.repository + +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn + +@Inject +@SingleIn(AppScope::class) +class AuthRepository { + var token: String? = null + private set + + fun login(username: String, password: String) { + // api.login(username, password) + } + suspend fun login(token: String) { + this.token = token + } +} diff --git a/core/src/commonMain/kotlin/moe/lava/neon/core/repository/UserRepository.kt b/core/src/commonMain/kotlin/moe/lava/neon/core/repository/UserRepository.kt new file mode 100644 index 0000000..53ff0c6 --- /dev/null +++ b/core/src/commonMain/kotlin/moe/lava/neon/core/repository/UserRepository.kt @@ -0,0 +1,10 @@ +package moe.lava.neon.core.repository + +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn + +@Inject +@SingleIn(AppScope::class) +class UserRepository { +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a37d305..2d3ebf9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,6 +14,7 @@ androidx-testExt = "1.3.0" composeHotReload = "1.0.0" composeMultiplatform = "1.10.0" junit = "4.13.2" +kermit = "2.0.8" kotlin = "2.3.0" kotlinx-coroutines = "1.10.2" material3 = "1.10.0-alpha05" @@ -21,6 +22,7 @@ material3-adaptive = "1.3.0-alpha03" metro = "0.10.0" [libraries] +kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlin-testJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } junit = { module = "junit:junit", version.ref = "junit" } @@ -43,6 +45,7 @@ compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "composeMul compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "composeMultiplatform" } compose-uiToolingPreview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "composeMultiplatform" } kotlinx-coroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } +metrox-viewmodel-compose = { module = "dev.zacsweers.metro:metrox-viewmodel-compose", version.ref = "metro" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" }