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
8 changes: 4 additions & 4 deletions composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ plugins {
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
alias(libs.plugins.kotlinxSerialization)
alias(libs.plugins.metro)
}

kotlin {
Expand Down Expand Up @@ -55,20 +56,19 @@ kotlin {

implementation(libs.coil.compose)
implementation(libs.coil.network.ktor)
implementation(libs.koin.core)
implementation(libs.koin.compose.viewmodel)
implementation(libs.metrox.viewmodel.compose)
}
}
}

android {
namespace = "com.jetbrains.kmpapp"
compileSdk = 35
compileSdk = 36

defaultConfig {
applicationId = "com.jetbrains.kmpapp"
minSdk = 24
targetSdk = 35
targetSdk = 36
versionCode = 1
versionName = "1.0"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ 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()
App(appGraph)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package com.jetbrains.kmpapp

import android.app.Application
import com.jetbrains.kmpapp.di.initKoin
import com.jetbrains.kmpapp.di.AppGraph
import dev.zacsweers.metro.createGraph

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

override fun onCreate() {
super.onCreate()
initKoin()
appGraph.museumRepository.initialize()
}
}
43 changes: 25 additions & 18 deletions composeApp/src/commonMain/kotlin/com/jetbrains/kmpapp/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.navigation.NavHostController
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 @@ -22,25 +25,29 @@ object ListDestination
data class DetailDestination(val objectId: Int)

@Composable
fun App() {
MaterialTheme(
colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme()
fun App(appGraph: AppGraph) {
CompositionLocalProvider(
LocalMetroViewModelFactory provides appGraph.metroViewModelFactory,
) {
Surface {
val navController: NavHostController = rememberNavController()
NavHost(navController = navController, startDestination = ListDestination) {
composable<ListDestination> {
ListScreen(navigateToDetails = { objectId ->
navController.navigate(DetailDestination(objectId))
})
}
composable<DetailDestination> { backStackEntry ->
DetailScreen(
objectId = backStackEntry.toRoute<DetailDestination>().objectId,
navigateBack = {
navController.popBackStack()
}
)
MaterialTheme(
colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme()
) {
Surface {
val navController: NavHostController = rememberNavController()
NavHost(navController = navController, startDestination = ListDestination) {
composable<ListDestination> {
ListScreen(navigateToDetails = { objectId ->
navController.navigate(DetailDestination(objectId))
})
}
composable<DetailDestination> { backStackEntry ->
DetailScreen(
objectId = backStackEntry.toRoute<DetailDestination>().objectId,
navigateBack = {
navController.popBackStack()
}
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
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
Expand All @@ -9,6 +12,8 @@ interface MuseumApi {
suspend fun getData(): List<MuseumObject>
}

@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
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,10 +1,15 @@
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

@Inject
@SingleIn(AppScope::class)
class MuseumRepository(
private val museumApi: MuseumApi,
private val museumStorage: MuseumStorage,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
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
Expand All @@ -12,6 +15,8 @@ interface MuseumStorage {
fun getObjects(): Flow<List<MuseumObject>>
}

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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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

@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
fun provideJson(): Json = Json { ignoreUnknownKeys = true }

@Provides
@SingleIn(AppScope::class)
fun provideHttpClient(json: Json): HttpClient = HttpClient {
install(ContentNegotiation) {
// TODO Fix API so it serves application/json
json(json, contentType = ContentType.Any)
}
}
}
51 changes: 0 additions & 51 deletions composeApp/src/commonMain/kotlin/com/jetbrains/kmpapp/di/Koin.kt

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ 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 @@ -48,14 +49,13 @@ 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 = koinViewModel<DetailViewModel>()
val viewModel = metroViewModel<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,8 +3,13 @@ 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

@ContributesIntoMap(AppScope::class)
@ViewModelKey
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 org.koin.compose.viewmodel.koinViewModel
import dev.zacsweers.metrox.viewmodel.metroViewModel

@Composable
fun ListScreen(
navigateToDetails: (objectId: Int) -> Unit
) {
val viewModel = koinViewModel<ListViewModel>()
val viewModel = metroViewModel<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,10 +4,15 @@ 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

@ContributesIntoMap(AppScope::class)
@ViewModelKey
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,5 +1,13 @@
package com.jetbrains.kmpapp

import androidx.compose.ui.window.ComposeUIViewController
import com.jetbrains.kmpapp.di.AppGraph
import dev.zacsweers.metro.createGraph

fun MainViewController() = ComposeUIViewController { App() }
private val appGraph: AppGraph by lazy {
createGraph<AppGraph>().also {
it.museumRepository.initialize()
}
}

fun MainViewController() = ComposeUIViewController { App(appGraph) }
8 changes: 4 additions & 4 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ androidx-activityCompose = "1.10.1"
androidx-ui-tooling = "1.7.8"
coil = "3.2.0"
compose-multiplatform = "1.8.2"
koin = "4.1.0"
kotlin = "2.2.0"
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,14 +17,13 @@ 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-core = { module = "io.insert-koin:koin-core", 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 @@ -34,3 +33,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" }