Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ plugins {
alias(libs.plugins.composeCompiler) apply false
alias(libs.plugins.composeMultiplatform) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
alias(libs.plugins.koin) apply false
alias(libs.plugins.kotlinxSerialization) apply false
}
9 changes: 7 additions & 2 deletions composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ plugins {
alias(libs.plugins.androidApplication)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
alias(libs.plugins.koin)
alias(libs.plugins.kotlinxSerialization)
alias(libs.plugins.metro)
}

kotlin {
Expand Down Expand Up @@ -56,7 +56,8 @@ kotlin {

implementation(libs.coil.compose)
implementation(libs.coil.network.ktor)
implementation(libs.metrox.viewmodel.compose)
implementation(libs.koin.compose.viewmodel)
implementation(libs.koin.annotations)
}
}
}
Expand Down Expand Up @@ -88,6 +89,10 @@ android {
}
}

koinCompiler {
userLogs = true
}

dependencies {
debugImplementation(libs.androidx.compose.ui.tooling)
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,12 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
val appGraph = (application as MuseumApp).appGraph
setContent {
// Remove when https://issuetracker.google.com/issues/364713509 is fixed
LaunchedEffect(isSystemInDarkTheme()) {
enableEdgeToEdge()
}
App(appGraph)
App()
}
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
package com.jetbrains.kmpapp

import android.app.Application
import com.jetbrains.kmpapp.di.AppGraph
import dev.zacsweers.metro.createGraph
import com.jetbrains.kmpapp.data.MuseumRepository
import com.jetbrains.kmpapp.di.KoinApp
import org.koin.plugin.module.dsl.startKoin

class MuseumApp : Application() {
val appGraph: AppGraph by lazy {
createGraph<AppGraph>()
val koin by lazy {
startKoin<KoinApp>().koin
}

override fun onCreate() {
super.onCreate()
appGraph.museumRepository.initialize()
koin.get<MuseumRepository>().initialize()
}
}
8 changes: 1 addition & 7 deletions composeApp/src/commonMain/kotlin/com/jetbrains/kmpapp/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import com.jetbrains.kmpapp.di.AppGraph
import com.jetbrains.kmpapp.screens.detail.DetailScreen
import com.jetbrains.kmpapp.screens.list.ListScreen
import dev.zacsweers.metrox.viewmodel.LocalMetroViewModelFactory
import kotlinx.serialization.Serializable

@Serializable
Expand All @@ -25,10 +23,7 @@ object ListDestination
data class DetailDestination(val objectId: Int)

@Composable
fun App(appGraph: AppGraph) {
CompositionLocalProvider(
LocalMetroViewModelFactory provides appGraph.metroViewModelFactory,
) {
fun App() {
MaterialTheme(
colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme()
) {
Expand All @@ -51,5 +46,4 @@ fun App(appGraph: AppGraph) {
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
package com.jetbrains.kmpapp.data

import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.utils.io.CancellationException
import org.koin.core.annotation.Singleton

interface MuseumApi {
suspend fun getData(): List<MuseumObject>
}

@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
@Singleton
class KtorMuseumApi(private val client: HttpClient) : MuseumApi {
companion object {
private const val API_URL =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
package com.jetbrains.kmpapp.data

import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import org.koin.core.annotation.Singleton

@Inject
@SingleIn(AppScope::class)
@Singleton
class MuseumRepository(
private val museumApi: MuseumApi,
private val museumStorage: MuseumStorage,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package com.jetbrains.kmpapp.data

import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import org.koin.core.annotation.Singleton

interface MuseumStorage {
suspend fun saveObjects(newObjects: List<MuseumObject>)
Expand All @@ -15,8 +13,7 @@ interface MuseumStorage {
fun getObjects(): Flow<List<MuseumObject>>
}

@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
@Singleton
class InMemoryMuseumStorage : MuseumStorage {
private val storedObjects = MutableStateFlow(emptyList<MuseumObject>())

Expand Down
Original file line number Diff line number Diff line change
@@ -1,45 +1,31 @@
package com.jetbrains.kmpapp.di

import androidx.lifecycle.ViewModel
import com.jetbrains.kmpapp.data.MuseumRepository
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.DependencyGraph
import dev.zacsweers.metro.Provider
import dev.zacsweers.metro.Provides
import dev.zacsweers.metro.SingleIn
import dev.zacsweers.metrox.viewmodel.ManualViewModelAssistedFactory
import dev.zacsweers.metrox.viewmodel.MetroViewModelFactory
import dev.zacsweers.metrox.viewmodel.ViewModelGraph
import io.ktor.client.HttpClient
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.http.ContentType
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
import kotlin.reflect.KClass
import org.koin.core.annotation.ComponentScan
import org.koin.core.annotation.Configuration
import org.koin.core.annotation.KoinApplication
import org.koin.core.annotation.Module
import org.koin.core.annotation.Singleton

@DependencyGraph(AppScope::class)
interface AppGraph : ViewModelGraph {
val museumRepository: MuseumRepository

@Provides
@SingleIn(AppScope::class)
fun provideMetroViewModelFactory(
viewModelProviders: Map<KClass<out ViewModel>, Provider<ViewModel>>,
manualAssistedFactoryProviders: Map<KClass<out ManualViewModelAssistedFactory>, Provider<ManualViewModelAssistedFactory>>,
): MetroViewModelFactory = object : MetroViewModelFactory() {
override val viewModelProviders get() = viewModelProviders
override val manualAssistedFactoryProviders get() = manualAssistedFactoryProviders
}

@Provides
@Module
@ComponentScan("com.jetbrains.kmpapp")
@Configuration
class AppModule {
@Singleton
fun provideJson(): Json = Json { ignoreUnknownKeys = true }

@Provides
@SingleIn(AppScope::class)
@Singleton
fun provideHttpClient(json: Json): HttpClient = HttpClient {
install(ContentNegotiation) {
// TODO Fix API so it serves application/json
json(json, contentType = ContentType.Any)
}
}
}

@KoinApplication
object KoinApp
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.compose.AsyncImage
import com.jetbrains.kmpapp.data.MuseumObject
import com.jetbrains.kmpapp.screens.EmptyScreenContent
import dev.zacsweers.metrox.viewmodel.metroViewModel
import kmp_app_template.composeapp.generated.resources.Res
import kmp_app_template.composeapp.generated.resources.back
import kmp_app_template.composeapp.generated.resources.label_artist
Expand All @@ -49,13 +48,14 @@ import kmp_app_template.composeapp.generated.resources.label_medium
import kmp_app_template.composeapp.generated.resources.label_repository
import kmp_app_template.composeapp.generated.resources.label_title
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel

@Composable
fun DetailScreen(
objectId: Int,
navigateBack: () -> Unit,
) {
val viewModel = metroViewModel<DetailViewModel>()
val viewModel = koinViewModel<DetailViewModel>()

val obj by viewModel.getObject(objectId).collectAsStateWithLifecycle(initialValue = null)
AnimatedContent(obj != null) { objectAvailable ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@ package com.jetbrains.kmpapp.screens.detail
import androidx.lifecycle.ViewModel
import com.jetbrains.kmpapp.data.MuseumObject
import com.jetbrains.kmpapp.data.MuseumRepository
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metrox.viewmodel.ViewModelKey
import kotlinx.coroutines.flow.Flow
import org.koin.core.annotation.KoinViewModel

@ContributesIntoMap(AppScope::class)
@ViewModelKey
@KoinViewModel
class DetailViewModel(private val museumRepository: MuseumRepository) : ViewModel() {
fun getObject(objectId: Int): Flow<MuseumObject?> =
museumRepository.getObjectById(objectId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.compose.AsyncImage
import com.jetbrains.kmpapp.data.MuseumObject
import com.jetbrains.kmpapp.screens.EmptyScreenContent
import dev.zacsweers.metrox.viewmodel.metroViewModel
import org.koin.compose.viewmodel.koinViewModel

@Composable
fun ListScreen(
navigateToDetails: (objectId: Int) -> Unit
) {
val viewModel = metroViewModel<ListViewModel>()
val viewModel = koinViewModel<ListViewModel>()
val objects by viewModel.objects.collectAsStateWithLifecycle()

AnimatedContent(objects.isNotEmpty()) { objectsAvailable ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,12 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.jetbrains.kmpapp.data.MuseumObject
import com.jetbrains.kmpapp.data.MuseumRepository
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metrox.viewmodel.ViewModelKey
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import org.koin.core.annotation.KoinViewModel

@ContributesIntoMap(AppScope::class)
@ViewModelKey
@KoinViewModel
class ListViewModel(museumRepository: MuseumRepository) : ViewModel() {
val objects: StateFlow<List<MuseumObject>> =
museumRepository.getObjects()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
package com.jetbrains.kmpapp

import androidx.compose.ui.window.ComposeUIViewController
import com.jetbrains.kmpapp.di.AppGraph
import dev.zacsweers.metro.createGraph
import com.jetbrains.kmpapp.di.KoinApp
import org.koin.core.Koin
import org.koin.plugin.module.dsl.startKoin

private val appGraph: AppGraph by lazy {
createGraph<AppGraph>().also {
it.museumRepository.initialize()
}
}
private val koin: Koin = startKoin<KoinApp>().koin

fun MainViewController() = ComposeUIViewController { App(appGraph) }
fun MainViewController() = ComposeUIViewController { App() }
8 changes: 5 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ androidx-activityCompose = "1.10.1"
androidx-ui-tooling = "1.7.8"
coil = "3.2.0"
compose-multiplatform = "1.8.2"
koin = "4.2.0"
koin-plugin = "0.4.1"
kotlin = "2.2.20"
ktor = "3.1.3"
materialIconsCore = "1.7.3"
metro = "0.12.1"
navigationCompose = "2.9.0-beta03"
lifecycleCompose = "2.9.1"

Expand All @@ -17,13 +18,14 @@ androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", versi
androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "androidx-ui-tooling" }
coil-compose = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil" }
coil-network-ktor = { group = "io.coil-kt.coil3", name = "coil-network-ktor3", version.ref = "coil" }
koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin" }
koin-annotations = { module = "io.insert-koin:koin-annotations", version.ref = "koin" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycleCompose" }
metrox-viewmodel-compose = { module = "dev.zacsweers.metro:metrox-viewmodel-compose", version.ref = "metro" }
material-icons-core = { module = "org.jetbrains.compose.material:material-icons-core", version.ref = "materialIconsCore" }
navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "navigationCompose" }

Expand All @@ -33,4 +35,4 @@ composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "k
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
metro = { id = "dev.zacsweers.metro", version.ref = "metro" }
koin = { id = "io.insert-koin.compiler.plugin", version.ref = "koin-plugin" }
Loading