refactor: switch from metro to koin

Honestly metro looks too overcomplicated and I still don't know how to
use it properly. Switching to koin for now as I'm more comfortable with
it.
This commit is contained in:
Cilly Leang 2026-02-01 00:50:57 +11:00
parent 53abaccd21
commit 2725342c3f
Signed by: cilly
GPG key ID: 6500251E087653C9
23 changed files with 165 additions and 199 deletions

View file

@ -8,7 +8,7 @@ plugins {
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
alias(libs.plugins.composeHotReload)
alias(libs.plugins.metro)
alias(libs.plugins.koinCompiler)
}
kotlin {
@ -54,8 +54,10 @@ kotlin {
implementation(libs.kermit)
implementation(libs.metrox.viewmodel.compose)
implementation(project.dependencies.platform(libs.koin.bom))
implementation(libs.koin.compose)
implementation(libs.koin.compose.viewmodel)
implementation(libs.koin.compose.navigation3)
}
commonTest.dependencies {
implementation(libs.kotlin.test)

View file

@ -4,23 +4,23 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import moe.lava.neon.ui.App
import moe.lava.neon.ui.di.initKoin
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
initKoin {
androidContext(this@MainActivity)
androidLogger()
}
setContent {
App()
}
}
}
@Preview
@Composable
fun AppAndroidPreview() {
App()
}

View file

@ -5,7 +5,6 @@ import androidx.compose.material3.MaterialExpressiveTheme
import androidx.compose.material3.MotionScheme
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
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
@ -14,12 +13,10 @@ import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.NavDisplay
import androidx.savedstate.serialization.SavedStateConfiguration
import co.touchlab.kermit.Logger
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.ui.di.AppUiGraph
import moe.lava.neon.core.repository.AuthRepository
import moe.lava.neon.ui.screens.Login
import moe.lava.neon.ui.screens.Sample
import moe.lava.neon.ui.screens.chat.Chat
@ -29,6 +26,7 @@ import moe.lava.neon.ui.screens.navigator.NavigatorModel
import moe.lava.neon.ui.screens.navigator.NavigatorPreviewProvider
import moe.lava.neon.ui.util.ThreePaneSceneStrategy
import moe.lava.neon.ui.util.rememberThreePaneSceneStrategy
import org.koin.compose.koinInject
import kotlin.system.exitProcess
object Route {
@ -68,81 +66,78 @@ fun App() {
exitProcess(1)
}
val uiGraph = createGraph<AppUiGraph>()
val graph = uiGraph.core
CaptchaBinder(graph.api)
CompositionLocalProvider(LocalMetroViewModelFactory provides uiGraph.metroViewModelFactory) {
MaterialExpressiveTheme(
colorScheme = getColorScheme(),
motionScheme = MotionScheme.expressive(),
) {
val init = if (graph.auth.token != null) Route.Sample else Route.Login
val backStack = rememberNavBackStack(config, Route.Sample)
val threePaneStrategy = rememberThreePaneSceneStrategy<NavKey>()
NavDisplay(
backStack = backStack,
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator(),
),
onBack = { backStack.removeLastOrNull() },
sceneStrategy = threePaneStrategy,
entryProvider = entryProvider {
entry<Route.Login> {
Login(
onSuccess = {
backStack.clear()
backStack.add(Route.Sample)
}
)
}
entry<Route.Sample> {
Sample(
navTest = {
backStack.add(Route.Navigator(it))
backStack.add(Route.Chat)
backStack.add(Route.MembersList)
},
onRequestLogout = {
backStack.clear()
backStack.add(Route.Login)
}
)
}
entry<Route.Navigator>(
metadata = ThreePaneSceneStrategy.listPane()
) { key ->
if (key.left) {
Navigator(
NavigatorPreviewProvider.base2.copy(
guildNavPosition = NavigatorModel.GuildNavPosition.LeftSidebar
)
)
} else {
Navigator(
NavigatorPreviewProvider.base2.copy(
guildNavPosition = NavigatorModel.GuildNavPosition.BottomSheet
)
)
val auth: AuthRepository = koinInject()
CaptchaBinder(koinInject())
MaterialExpressiveTheme(
colorScheme = getColorScheme(),
motionScheme = MotionScheme.expressive(),
) {
val init = if (auth.token != null) Route.Sample else Route.Login
val backStack = rememberNavBackStack(config, Route.Sample)
val threePaneStrategy = rememberThreePaneSceneStrategy<NavKey>()
NavDisplay(
backStack = backStack,
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator(),
),
onBack = { backStack.removeLastOrNull() },
sceneStrategy = threePaneStrategy,
entryProvider = entryProvider {
entry<Route.Login> {
Login(
onSuccess = {
backStack.clear()
backStack.add(Route.Sample)
}
}
)
}
entry<Route.Sample> {
Sample(
navTest = {
backStack.add(Route.Navigator(it))
backStack.add(Route.Chat)
backStack.add(Route.MembersList)
},
onRequestLogout = {
backStack.clear()
backStack.add(Route.Login)
}
)
}
entry<Route.Chat>(
metadata = ThreePaneSceneStrategy.detailPane()
) {
Chat(
onOpenMembers = { backStack.add(Route.MembersList) }
entry<Route.Navigator>(
metadata = ThreePaneSceneStrategy.listPane()
) { key ->
if (key.left) {
Navigator(
NavigatorPreviewProvider.base2.copy(
guildNavPosition = NavigatorModel.GuildNavPosition.LeftSidebar
)
)
} else {
Navigator(
NavigatorPreviewProvider.base2.copy(
guildNavPosition = NavigatorModel.GuildNavPosition.BottomSheet
)
)
}
entry<Route.MembersList>(
metadata = ThreePaneSceneStrategy.extraPane()
) {
MembersList()
}
}
)
}
entry<Route.Chat>(
metadata = ThreePaneSceneStrategy.detailPane()
) {
Chat(
onOpenMembers = { backStack.add(Route.MembersList) }
)
}
entry<Route.MembersList>(
metadata = ThreePaneSceneStrategy.extraPane()
) {
MembersList()
}
}
)
}
}

View file

@ -1,29 +0,0 @@
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

@ -0,0 +1,13 @@
package moe.lava.neon.ui.di
import org.koin.core.KoinApplication
import org.koin.core.context.startKoin
import org.koin.dsl.KoinAppDeclaration
import org.koin.dsl.includes
fun initKoin(config: KoinAppDeclaration? = null): KoinApplication {
return startKoin {
includes(config)
modules(uiModule)
}
}

View file

@ -0,0 +1,13 @@
package moe.lava.neon.ui.di
import moe.lava.neon.core.di.coreModule
import moe.lava.neon.ui.screens.LoginViewModel
import moe.lava.neon.ui.screens.SampleViewModel
import org.koin.dsl.module
import org.koin.plugin.module.dsl.viewModel
val uiModule = module {
includes(coreModule)
viewModel<LoginViewModel>()
viewModel<SampleViewModel>()
}

View file

@ -23,11 +23,6 @@ import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import co.touchlab.kermit.Logger
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesIntoMap
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
import moe.lava.neon.core.repository.AuthResponse
@ -35,12 +30,13 @@ import moe.lava.neon.resources.Res
import moe.lava.neon.resources.visibility
import moe.lava.neon.resources.visibility_off
import org.jetbrains.compose.resources.painterResource
import org.koin.compose.viewmodel.koinViewModel
@Composable
fun Login(
onSuccess: () -> Unit,
) {
val viewModel: LoginViewModel = metroViewModel()
val viewModel: LoginViewModel = koinViewModel()
val scope = rememberCoroutineScope()
Column(
@ -113,9 +109,6 @@ fun Login(
}
}
@Inject
@ViewModelKey(LoginViewModel::class)
@ContributesIntoMap(AppScope::class)
class LoginViewModel(
private val auth: AuthRepository
) : ViewModel() {

View file

@ -20,11 +20,6 @@ import androidx.compose.ui.Modifier
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesIntoMap
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.api.gateway.GatewayHandler
import moe.lava.neon.core.repository.AuthRepository
@ -32,13 +27,14 @@ 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
import org.koin.compose.viewmodel.koinViewModel
@Composable
fun Sample(
navTest: (Boolean) -> Unit,
onRequestLogout: () -> Unit,
) {
val viewModel: SampleViewModel = metroViewModel()
val viewModel: SampleViewModel = koinViewModel()
var showContent by remember { mutableStateOf(false) }
Column(
modifier = Modifier
@ -86,9 +82,6 @@ fun Sample(
}
}
@Inject
@ViewModelKey(SampleViewModel::class)
@ContributesIntoMap(AppScope::class)
class SampleViewModel(
private val auth: AuthRepository,
private val gateway: GatewayHandler,

View file

@ -6,12 +6,16 @@ import androidx.compose.ui.unit.Density
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import moe.lava.neon.ui.App
import moe.lava.neon.ui.di.initKoin
// The UI is designed with touchscreens in mind; on desktop elements may look gigantic
// So scale them down a bit
const val scaleFactor = 0.75f
fun main() = application {
initKoin {
printLogger()
}
Window(
onCloseRequest = ::exitApplication,
title = "Neon",