feat: basic repo + di

This commit is contained in:
Cilly Leang 2026-01-23 23:15:40 +11:00
parent 1b3b465112
commit 4d8872db9c
Signed by: cilly
GPG key ID: 6500251E087653C9
10 changed files with 185 additions and 22 deletions

View file

@ -40,6 +40,10 @@ kotlin {
implementation(libs.androidx.nav3.ui) implementation(libs.androidx.nav3.ui)
implementation(libs.compose.material3.adaptive) implementation(libs.compose.material3.adaptive)
implementation(libs.compose.material3.adaptive.nav3) implementation(libs.compose.material3.adaptive.nav3)
implementation(libs.metrox.viewmodel.compose)
implementation(libs.kermit)
} }
commonTest.dependencies { commonTest.dependencies {
implementation(libs.kotlin.test) implementation(libs.kotlin.test)

View file

@ -5,8 +5,12 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.navigation3.ui.defaultPredictivePopTransitionSpec 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 import moe.lava.neon.ui.App
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {

View file

@ -2,14 +2,21 @@ package moe.lava.neon.ui
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable 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.NavKey
import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.NavDisplay import androidx.navigation3.ui.NavDisplay
import androidx.savedstate.serialization.SavedStateConfiguration import androidx.savedstate.serialization.SavedStateConfiguration
import dev.zacsweers.metro.createGraph
import dev.zacsweers.metrox.viewmodel.LocalMetroViewModelFactory
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic 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.Login
import moe.lava.neon.ui.screens.Sample import moe.lava.neon.ui.screens.Sample
@ -18,7 +25,7 @@ private object Route {
data object Login : NavKey data object Login : NavKey
@Serializable @Serializable
data class Sample(val token: String) : NavKey data object Sample : NavKey
} }
private val config = SavedStateConfiguration { private val config = SavedStateConfiguration {
@ -30,25 +37,36 @@ private val config = SavedStateConfiguration {
} }
} }
@Composable @Composable
fun App() { fun App() {
MaterialTheme { val uiGraph = createGraph<AppUiGraph>()
val backStack = rememberNavBackStack(config, Route.Login) val graph = uiGraph.core
NavDisplay( CompositionLocalProvider(LocalMetroViewModelFactory provides uiGraph.metroViewModelFactory) {
backStack = backStack, MaterialTheme {
onBack = { backStack.removeLastOrNull() }, val init = if (graph.auth.token != null) Route.Sample else Route.Login
entryProvider = entryProvider { val backStack = rememberNavBackStack(config, init)
entry<Route.Login> { NavDisplay(
Login( backStack = backStack,
onSuccess = { token -> entryDecorators = listOf(
backStack.add(Route.Sample(token)) rememberSaveableStateHolderNavEntryDecorator(),
} rememberViewModelStoreNavEntryDecorator(),
) ),
onBack = { backStack.removeLastOrNull() },
entryProvider = entryProvider {
entry<Route.Login> {
Login(
onSuccess = {
backStack.clear()
backStack.add(Route.Sample)
}
)
}
entry<Route.Sample> { key ->
Sample()
}
} }
entry<Route.Sample> { key -> )
Sample(key.token) }
}
}
)
} }
} }

View file

@ -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<KClass<out ViewModel>, Provider<ViewModel>>,
override val assistedFactoryProviders: Map<KClass<out ViewModel>, Provider<ViewModelAssistedFactory>>,
override val manualAssistedFactoryProviders: Map<KClass<out ManualViewModelAssistedFactory>, Provider<ManualViewModelAssistedFactory>>,
) : MetroViewModelFactory()

View file

@ -9,14 +9,28 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier 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 @Composable
fun Login( fun Login(
onSuccess: (token: String) -> Unit, onSuccess: () -> Unit,
) { ) {
val viewModel: LoginViewModel = metroViewModel()
val scope = rememberCoroutineScope()
Column( Column(
modifier = Modifier modifier = Modifier
// .background(MaterialTheme.colorScheme.primaryContainer) // .background(MaterialTheme.colorScheme.primaryContainer)
@ -31,8 +45,37 @@ fun Login(
onValueChange = { token = it }, onValueChange = { token = it },
label = { Text("Enter token") }, 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") 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
}
}
}

View file

@ -17,13 +17,23 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue 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.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.Res
import moe.lava.neon.resources.compose_multiplatform import moe.lava.neon.resources.compose_multiplatform
import moe.lava.neon.ui.Greeting import moe.lava.neon.ui.Greeting
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
@Composable @Composable
fun Sample(token: String) { fun Sample() {
val viewModel: SampleViewModel = metroViewModel()
var showContent by remember { mutableStateOf(false) } var showContent by remember { mutableStateOf(false) }
Column( Column(
modifier = Modifier modifier = Modifier
@ -43,8 +53,17 @@ fun Sample(token: String) {
) { ) {
Image(painterResource(Res.drawable.compose_multiplatform), null) Image(painterResource(Res.drawable.compose_multiplatform), null)
Text("Compose: $greeting") 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
}

View file

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

View file

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

View file

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

View file

@ -14,6 +14,7 @@ androidx-testExt = "1.3.0"
composeHotReload = "1.0.0" composeHotReload = "1.0.0"
composeMultiplatform = "1.10.0" composeMultiplatform = "1.10.0"
junit = "4.13.2" junit = "4.13.2"
kermit = "2.0.8"
kotlin = "2.3.0" kotlin = "2.3.0"
kotlinx-coroutines = "1.10.2" kotlinx-coroutines = "1.10.2"
material3 = "1.10.0-alpha05" material3 = "1.10.0-alpha05"
@ -21,6 +22,7 @@ material3-adaptive = "1.3.0-alpha03"
metro = "0.10.0" metro = "0.10.0"
[libraries] [libraries]
kermit = { module = "co.touchlab:kermit", version.ref = "kermit" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlin-testJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } kotlin-testJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
junit = { module = "junit:junit", version.ref = "junit" } 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-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" } 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" } 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] [plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" } androidApplication = { id = "com.android.application", version.ref = "agp" }