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.compose.material3.adaptive)
implementation(libs.compose.material3.adaptive.nav3)
implementation(libs.metrox.viewmodel.compose)
implementation(libs.kermit)
}
commonTest.dependencies {
implementation(libs.kotlin.test)

View file

@ -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() {

View file

@ -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<Route.Login> {
Login(
onSuccess = { token ->
backStack.add(Route.Sample(token))
}
)
val uiGraph = createGraph<AppUiGraph>()
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<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.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
}
}
}

View file

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

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