From 9ac7bd6bdd1953603b5f57d922dd1bf45a79eaec Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Fri, 5 Jan 2024 20:17:02 +0700 Subject: [PATCH 1/9] sample --- .idea/kotlinc.xml | 2 +- gradle/libs.versions.toml | 8 + khonshu-navigation-core/build.gradle.kts | 2 +- navigation-core/build.gradle.kts | 2 +- navigation/build.gradle.kts | 2 +- .../kmpviewmodelsample/android/MyApp.kt | 6 +- .../kmpviewmodelsample/android/Route.kt | 72 ------- .../android/StartActivity.kt | 193 +----------------- sample/iosApp/Podfile.lock | 2 +- .../iosApp/iosApp.xcodeproj/project.pbxproj | 36 ++++ sample/shared/build.gradle.kts | 72 +++++-- sample/shared/shared.podspec | 2 +- .../kmpviewmodelsample/common/Immutable.kt | 4 - .../sample}/common/AndroidAppDispatchers.kt | 2 +- .../CollectWithLifecycleEffect.android.kt | 105 ++++++++++ .../sample}/common/WeakReference.android.kt | 2 +- .../common/identityHashCode.android.kt | 2 +- .../sample/common/platformToast.android.kt | 10 + .../sample}/di.android.kt | 13 +- .../kmpviewmodelsample/common/Immutable.kt | 6 - .../debugCheckImmediateMainDispatcher.kt | 16 -- .../snippets/SearchViewModel.kt | 39 ---- .../snippets/UserViewModel.kt | 44 ---- .../solivagant/sample/SolivagantSampleApp.kt | 115 +++++++++++ .../solivagant/sample/StartScreen.kt | 82 ++++++++ .../sample}/common/AppDispatchers.kt | 2 +- .../common/CollectWithLifecycleEffect.kt | 84 +------- .../sample}/common/MyApplicationTheme.kt | 2 +- .../sample/common}/ProductItemUi.kt | 8 +- .../sample}/common/SingleEventFlow.kt | 2 +- .../sample}/common/WeakReference.kt | 2 +- .../solivagant/sample}/common/commonUi.kt | 12 +- .../debugCheckImmediateMainDispatcher.kt | 22 ++ .../sample}/common/identityHashCode.kt | 2 +- .../solivagant/sample/common/platformToast.kt | 8 + .../sample}/data/ProductItem.kt | 2 +- .../sample}/data/fakeJson.kt | 2 +- .../sample}/di.kt | 10 +- .../sample}/product_detail/GetProductById.kt | 8 +- .../product_detail/ProductDetailScreen.kt | 50 +++-- .../ProductDetailScreenRoute.kt | 16 ++ .../product_detail/ProductDetailState.kt | 11 + .../product_detail/ProductDetailViewModel.kt | 47 +---- .../sample}/products/GetProducts.kt | 8 +- .../sample}/products/ProductsScreen.kt | 68 +++--- .../sample/products/ProductsScreenRoute.kt | 16 ++ .../sample/products/ProductsState.kt | 25 +++ .../sample}/products/ProductsViewModel.kt | 68 ++---- .../SearchProductScreenRoute.kt | 16 ++ .../sample}/search_products/SearchProducts.kt | 8 +- .../search_products/SearchProductsScreen.kt | 48 +++-- .../search_products/SearchProductsState.kt | 23 +++ .../SearchProductsViewModel.kt | 31 +-- .../sample/common/DesktopAppDispatchers.kt | 10 + .../sample/common/WeakReference.desktop.kt | 4 + .../sample/common/identityHashCode.desktop.kt | 3 + .../hoc081098/solivagant/sample/di.desktop.kt | 36 ++++ .../kmpviewmodelsample/common/Immutable.kt | 6 - .../sample}/DIContainer.kt | 2 +- .../sample}/ImmediateMainScope.kt | 2 +- .../sample}/NSError.kt | 2 +- .../sample}/common/IosAppDispatchers.kt | 2 +- .../sample}/common/WeakReference.ios.kt | 2 +- .../sample}/common/identityHashCode.ios.kt | 2 +- .../sample}/di.ios.kt | 12 +- settings.gradle.kts | 2 + 66 files changed, 814 insertions(+), 711 deletions(-) delete mode 100644 sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/Route.kt delete mode 100644 sample/shared/src/androidMain/kotlin/com/hoc081098/kmpviewmodelsample/common/Immutable.kt rename sample/shared/src/androidMain/kotlin/com/hoc081098/{kmpviewmodelsample => solivagant/sample}/common/AndroidAppDispatchers.kt (86%) create mode 100644 sample/shared/src/androidMain/kotlin/com/hoc081098/solivagant/sample/common/CollectWithLifecycleEffect.android.kt rename sample/shared/src/androidMain/kotlin/com/hoc081098/{kmpviewmodelsample => solivagant/sample}/common/WeakReference.android.kt (77%) rename sample/shared/src/androidMain/kotlin/com/hoc081098/{kmpviewmodelsample => solivagant/sample}/common/identityHashCode.android.kt (63%) create mode 100644 sample/shared/src/androidMain/kotlin/com/hoc081098/solivagant/sample/common/platformToast.android.kt rename sample/shared/src/androidMain/kotlin/com/hoc081098/{kmpviewmodelsample => solivagant/sample}/di.android.kt (60%) delete mode 100644 sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/common/Immutable.kt delete mode 100644 sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/common/debugCheckImmediateMainDispatcher.kt delete mode 100644 sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/snippets/SearchViewModel.kt delete mode 100644 sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/snippets/UserViewModel.kt create mode 100644 sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/SolivagantSampleApp.kt create mode 100644 sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/StartScreen.kt rename sample/shared/src/commonMain/kotlin/com/hoc081098/{kmpviewmodelsample => solivagant/sample}/common/AppDispatchers.kt (85%) rename sample/{app/src/main/java/com/hoc081098/kmpviewmodelsample/android => shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample}/common/CollectWithLifecycleEffect.kt (54%) rename sample/{app/src/main/java/com/hoc081098/kmpviewmodelsample/android => shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample}/common/MyApplicationTheme.kt (99%) rename sample/shared/src/commonMain/kotlin/com/hoc081098/{kmpviewmodelsample => solivagant/sample/common}/ProductItemUi.kt (84%) rename sample/shared/src/commonMain/kotlin/com/hoc081098/{kmpviewmodelsample => solivagant/sample}/common/SingleEventFlow.kt (98%) rename sample/shared/src/commonMain/kotlin/com/hoc081098/{kmpviewmodelsample => solivagant/sample}/common/WeakReference.kt (65%) rename sample/{app/src/main/java/com/hoc081098/kmpviewmodelsample/android => shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample}/common/commonUi.kt (95%) create mode 100644 sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/debugCheckImmediateMainDispatcher.kt rename sample/shared/src/commonMain/kotlin/com/hoc081098/{kmpviewmodelsample => solivagant/sample}/common/identityHashCode.kt (51%) create mode 100644 sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/platformToast.kt rename sample/shared/src/commonMain/kotlin/com/hoc081098/{kmpviewmodelsample => solivagant/sample}/data/ProductItem.kt (95%) rename sample/shared/src/commonMain/kotlin/com/hoc081098/{kmpviewmodelsample => solivagant/sample}/data/fakeJson.kt (99%) rename sample/shared/src/commonMain/kotlin/com/hoc081098/{kmpviewmodelsample => solivagant/sample}/di.kt (66%) rename sample/shared/src/commonMain/kotlin/com/hoc081098/{kmpviewmodelsample => solivagant/sample}/product_detail/GetProductById.kt (70%) rename sample/{app/src/main/java/com/hoc081098/kmpviewmodelsample/android => shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample}/product_detail/ProductDetailScreen.kt (77%) create mode 100644 sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/ProductDetailScreenRoute.kt create mode 100644 sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/ProductDetailState.kt rename sample/shared/src/commonMain/kotlin/com/hoc081098/{kmpviewmodelsample => solivagant/sample}/product_detail/ProductDetailViewModel.kt (57%) rename sample/shared/src/commonMain/kotlin/com/hoc081098/{kmpviewmodelsample => solivagant/sample}/products/GetProducts.kt (74%) rename sample/{app/src/main/java/com/hoc081098/kmpviewmodelsample/android => shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample}/products/ProductsScreen.kt (53%) create mode 100644 sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/products/ProductsScreenRoute.kt create mode 100644 sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/products/ProductsState.kt rename sample/shared/src/commonMain/kotlin/com/hoc081098/{kmpviewmodelsample => solivagant/sample}/products/ProductsViewModel.kt (73%) create mode 100644 sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProductScreenRoute.kt rename sample/shared/src/commonMain/kotlin/com/hoc081098/{kmpviewmodelsample => solivagant/sample}/search_products/SearchProducts.kt (75%) rename sample/{app/src/main/java/com/hoc081098/kmpviewmodelsample/android => shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample}/search_products/SearchProductsScreen.kt (65%) create mode 100644 sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProductsState.kt rename sample/shared/src/commonMain/kotlin/com/hoc081098/{kmpviewmodelsample => solivagant/sample}/search_products/SearchProductsViewModel.kt (83%) create mode 100644 sample/shared/src/desktopMain/kotlin/com/hoc081098/solivagant/sample/common/DesktopAppDispatchers.kt create mode 100644 sample/shared/src/desktopMain/kotlin/com/hoc081098/solivagant/sample/common/WeakReference.desktop.kt create mode 100644 sample/shared/src/desktopMain/kotlin/com/hoc081098/solivagant/sample/common/identityHashCode.desktop.kt create mode 100644 sample/shared/src/desktopMain/kotlin/com/hoc081098/solivagant/sample/di.desktop.kt delete mode 100644 sample/shared/src/iosMain/kotlin/com/hoc081098/kmpviewmodelsample/common/Immutable.kt rename sample/shared/src/iosMain/kotlin/com/hoc081098/{kmpviewmodelsample => solivagant/sample}/DIContainer.kt (97%) rename sample/shared/src/iosMain/kotlin/com/hoc081098/{kmpviewmodelsample => solivagant/sample}/ImmediateMainScope.kt (85%) rename sample/shared/src/iosMain/kotlin/com/hoc081098/{kmpviewmodelsample => solivagant/sample}/NSError.kt (98%) rename sample/shared/src/iosMain/kotlin/com/hoc081098/{kmpviewmodelsample => solivagant/sample}/common/IosAppDispatchers.kt (87%) rename sample/shared/src/iosMain/kotlin/com/hoc081098/{kmpviewmodelsample => solivagant/sample}/common/WeakReference.ios.kt (84%) rename sample/shared/src/iosMain/kotlin/com/hoc081098/{kmpviewmodelsample => solivagant/sample}/common/identityHashCode.ios.kt (82%) rename sample/shared/src/iosMain/kotlin/com/hoc081098/{kmpviewmodelsample => solivagant/sample}/di.ios.kt (69%) diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index ae3f30ae..8d81632f 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bd5ea130..d4de80dd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,11 +24,13 @@ sample-android-target = "34" sample-android-compile = "34" jetbrains-compose = "1.5.11" +jetbrains-compose-compiler = "1.5.7.1" touchlab-stately = "1.2.5" napier = "2.6.1" flowExt = "0.7.4" koin = "3.5.0" +koin-compose = "1.1.0" koin-androidx-compose = "3.5.0" coil = "2.5.0" compose-rules-detekt = "0.3.8" @@ -36,7 +38,9 @@ compose-rules-detekt = "0.3.8" androidx-lifecycle = "2.6.2" androidx-annotation = "1.7.1" androidx-activity = "1.8.2" +androidx-appcompat = "1.6.1" androidx-compose-compiler = "1.5.6" +androidx-core-ktx = "1.12.0" androidx-navigation = "2.7.6" android-gradle = "8.2.0" @@ -57,6 +61,7 @@ ktlint = "0.50.0" [libraries] coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } +coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" } coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } coroutines-jdk8 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8", version.ref = "coroutines" } junit = { module = "junit:junit", version.ref = "junit" } @@ -68,6 +73,7 @@ touchlab-stately-concurrency = { module = "co.touchlab:stately-concurrency", ver napier = { module = "io.github.aakira:napier", version.ref = "napier" } flowExt = { module = "io.github.hoc081098:FlowExt", version.ref = "flowExt" } koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } +koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin-compose" } koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" } koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin-androidx-compose" } coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } @@ -79,6 +85,7 @@ androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-ru androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version = "androidx-lifecycle" } androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } +androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } androidx-compose-bom = { module = "androidx.compose:compose-bom", version = "2023.10.01" } androidx-compose-ui-ui = { module = "androidx.compose.ui:ui" } androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } @@ -87,6 +94,7 @@ androidx-compose-foundation = { module = "androidx.compose.foundation:foundation androidx-compose-material = { module = "androidx.compose.material:material" } androidx-compose-material3 = { module = "androidx.compose.material3:material3" } androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" } +androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core-ktx" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" } kmp-viewmodel-core = { module = "io.github.hoc081098:kmp-viewmodel", version.ref = "kmp-viewmodel" } diff --git a/khonshu-navigation-core/build.gradle.kts b/khonshu-navigation-core/build.gradle.kts index e0a766d1..e690f43c 100644 --- a/khonshu-navigation-core/build.gradle.kts +++ b/khonshu-navigation-core/build.gradle.kts @@ -20,7 +20,7 @@ plugins { } compose { - kotlinCompilerPlugin.set("1.5.7") + kotlinCompilerPlugin.set(libs.versions.jetbrains.compose.compiler) } kotlin { diff --git a/navigation-core/build.gradle.kts b/navigation-core/build.gradle.kts index 5238a430..0f17a767 100644 --- a/navigation-core/build.gradle.kts +++ b/navigation-core/build.gradle.kts @@ -18,7 +18,7 @@ plugins { } compose { - kotlinCompilerPlugin.set("1.5.7") + kotlinCompilerPlugin.set(libs.versions.jetbrains.compose.compiler) } kotlin { diff --git a/navigation/build.gradle.kts b/navigation/build.gradle.kts index 433ae3ff..844cab6f 100644 --- a/navigation/build.gradle.kts +++ b/navigation/build.gradle.kts @@ -20,7 +20,7 @@ plugins { } compose { - kotlinCompilerPlugin.set("1.5.7") + kotlinCompilerPlugin.set(libs.versions.jetbrains.compose.compiler) } kotlin { diff --git a/sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/MyApp.kt b/sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/MyApp.kt index 30a8ce68..6615b7f4 100644 --- a/sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/MyApp.kt +++ b/sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/MyApp.kt @@ -1,9 +1,9 @@ package com.hoc081098.kmpviewmodelsample.android import android.app.Application -import com.hoc081098.kmpviewmodelsample.BuildConfig -import com.hoc081098.kmpviewmodelsample.setupNapier -import com.hoc081098.kmpviewmodelsample.startKoinCommon +import com.hoc081098.solivagant.sample.setupNapier +import com.hoc081098.solivagant.sample.startKoinCommon +import io.github.aakira.napier.BuildConfig import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger import org.koin.core.logger.Level diff --git a/sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/Route.kt b/sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/Route.kt deleted file mode 100644 index a21ac1d4..00000000 --- a/sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/Route.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.hoc081098.kmpviewmodelsample.android - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.remember -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavType -import androidx.navigation.navArgument -import com.hoc081098.kmp.viewmodel.safe.DelicateSafeSavedStateHandleApi -import com.hoc081098.kmpviewmodelsample.product_detail.ProductDetailViewModel -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf - -@Composable -internal fun rememberCurrentRouteAsState(currentBackStackEntryAsState: State): State = - remember(currentBackStackEntryAsState) { - derivedStateOf { - currentBackStackEntryAsState.value - ?.destination - ?.route - ?.let(Route::ofOrNull) - } - } - -internal sealed class Route { - abstract val routePattern: String - - fun matches(route: String): Boolean = route == routePattern - - data object Start : Route() { - override val routePattern = "start" - val route = routePattern - } - - data object Products : Route() { - override val routePattern = "products" - val route = routePattern - } - - data object Search : Route() { - override val routePattern = "search" - val route = routePattern - } - - data object ProductDetail : Route() { - @OptIn(DelicateSafeSavedStateHandleApi::class) - val idNavArg = navArgument(name = ProductDetailViewModel.ID_SAVED_KEY.key) { type = NavType.IntType } - - private inline val idNavArgName get() = idNavArg.name - - override val routePattern = "product_detail/{$idNavArgName}" - - fun route(id: Int) = routePattern.replace( - oldValue = """{$idNavArgName}""", - newValue = id.toString(), - ) - } - - companion object { - private val VALUES: ImmutableList by lazy { - persistentListOf( - Start, - Products, - Search, - ProductDetail, - ) - } - - fun ofOrNull(route: String): Route? = VALUES.singleOrNull { it.matches(route) } - } -} diff --git a/sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/StartActivity.kt b/sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/StartActivity.kt index cfef0aa0..da4fb085 100644 --- a/sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/StartActivity.kt +++ b/sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/StartActivity.kt @@ -1,205 +1,16 @@ -@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) - package com.hoc081098.kmpviewmodelsample.android import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.consumeWindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Icon -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material3.Button -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.currentBackStackEntryAsState -import androidx.navigation.compose.rememberNavController -import com.hoc081098.kmpviewmodelsample.android.common.MyApplicationTheme -import com.hoc081098.kmpviewmodelsample.android.product_detail.ProductDetailScreen -import com.hoc081098.kmpviewmodelsample.android.products.ProductsScreen -import com.hoc081098.kmpviewmodelsample.android.search_products.SearchProductsScreen +import com.hoc081098.solivagant.sample.SolivagantSampleApp class StartActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { - MyApplicationTheme { - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background, - ) { - val navController = rememberNavController() - val currentBackStackEntryAsState = navController.currentBackStackEntryAsState() - - val route by rememberCurrentRouteAsState(currentBackStackEntryAsState) - val previousBackStackEntry by remember(currentBackStackEntryAsState) { - derivedStateOf { - currentBackStackEntryAsState.value - navController.previousBackStackEntry - } - } - - Scaffold( - topBar = { - TopAppBar( - title = { - Text( - text = when (route) { - Route.Start -> "KMP ViewModel Sample" - Route.Products -> "Products screen" - Route.Search -> "Search products screen" - Route.ProductDetail -> "Product detail screen" - null -> "KMP ViewModel Sample" - }, - ) - }, - navigationIcon = { - if (previousBackStackEntry != null) { - IconButton(onClick = navController::popBackStack) { - Icon( - imageVector = Icons.Default.ArrowBack, - contentDescription = "Back", - ) - } - } - }, - ) - }, - ) { innerPadding -> - AppNavHost( - modifier = Modifier - .padding(innerPadding) - .consumeWindowInsets(innerPadding) - .fillMaxSize(), - navController = navController, - ) - } - } - } - } - } -} - -@Composable -private fun AppNavHost( - modifier: Modifier = Modifier, - navController: NavHostController = rememberNavController(), -) { - NavHost( - modifier = modifier, - navController = navController, - startDestination = Route.Start.route, - ) { - composable(Route.Start.routePattern) { - StartScreen( - navigateToProducts = { navController.navigate(Route.Products.route) }, - navigateToSearch = { navController.navigate(Route.Search.route) }, - ) - } - - composable(Route.Products.routePattern) { - ProductsScreen( - navigateToProductDetail = { id -> - navController.navigate( - Route.ProductDetail.route(id = id), - ) - }, - ) - } - - composable(Route.Search.routePattern) { - SearchProductsScreen( - navigateToProductDetail = { id -> - navController.navigate( - Route.ProductDetail.route(id = id), - ) - }, - ) - } - - composable( - route = Route.ProductDetail.routePattern, - arguments = listOf( - Route.ProductDetail.idNavArg, - ), - ) { - ProductDetailScreen() + SolivagantSampleApp() } } } - -@Composable -private fun StartScreen( - navigateToProducts: () -> Unit, - navigateToSearch: () -> Unit, - modifier: Modifier = Modifier, -) { - Column( - modifier = modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - ProductsButton( - navigateToProducts = navigateToProducts, - ) - - Spacer(modifier = Modifier.height(16.dp)) - - SearchProductsButton( - navigateToSearch = navigateToSearch, - ) - } -} - -@Composable -private fun SearchProductsButton( - navigateToSearch: () -> Unit, - modifier: Modifier = Modifier, -) { - Button( - modifier = modifier, - onClick = navigateToSearch, - ) { - Text( - text = "Search products screen", - ) - } -} - -@Composable -private fun ProductsButton( - navigateToProducts: () -> Unit, - modifier: Modifier = Modifier, -) { - Button( - modifier = modifier, - onClick = navigateToProducts, - ) { - Text( - text = "Products screen", - ) - } -} diff --git a/sample/iosApp/Podfile.lock b/sample/iosApp/Podfile.lock index a27b18f2..3f522503 100644 --- a/sample/iosApp/Podfile.lock +++ b/sample/iosApp/Podfile.lock @@ -50,7 +50,7 @@ SPEC CHECKSUMS: RxDataSources: aa47cc1ed6c500fa0dfecac5c979b723542d79cf RxRelay: 45eaa5db8ee4fb50e5ebd57deec0159e97fa51e6 RxSwift: a4b44f7d24599f674deebd1818eab82e58410632 - shared: 590f56c2e6596d2bf475c0e71233e385feb4a13e + shared: 8b0dbc7374b73d572d2b95d5fcf0c9c76801ff09 PODFILE CHECKSUM: ba90cf8daf1f3b2d6cf7e270921fbb887b46843e diff --git a/sample/iosApp/iosApp.xcodeproj/project.pbxproj b/sample/iosApp/iosApp.xcodeproj/project.pbxproj index cb126856..9347408b 100644 --- a/sample/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/sample/iosApp/iosApp.xcodeproj/project.pbxproj @@ -265,6 +265,7 @@ 35F8385E29E52B5600CC2335 /* Frameworks */, 35F8385F29E52B5600CC2335 /* Resources */, 77936D1C83531C094C448405 /* [CP] Embed Pods Frameworks */, + 8697E55BC90D1FE214337624 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -284,6 +285,7 @@ 7555FF79242A565900829871 /* Resources */, C0ACCB18154326F13A615831 /* Frameworks */, 9B2B5D25329D0690C66216E5 /* [CP] Embed Pods Frameworks */, + 6F7CE651A23EFEFAE1DA3EB8 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -354,6 +356,23 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 6F7CE651A23EFEFAE1DA3EB8 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; 77936D1C83531C094C448405 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -393,6 +412,23 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + 8697E55BC90D1FE214337624 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-iosApp-RxSwift/Pods-iosApp-RxSwift-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-iosApp-RxSwift/Pods-iosApp-RxSwift-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp-RxSwift/Pods-iosApp-RxSwift-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; 9B2B5D25329D0690C66216E5 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/sample/shared/build.gradle.kts b/sample/shared/build.gradle.kts index a1e1533b..d885a09c 100644 --- a/sample/shared/build.gradle.kts +++ b/sample/shared/build.gradle.kts @@ -3,12 +3,19 @@ @Suppress("DSL_SCOPE_VIOLATION") plugins { alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.jetbrains.compose) alias(libs.plugins.kotlin.cocoapods) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.android.library) alias(libs.plugins.kotlin.parcelize) } +compose { + kotlinCompilerPlugin.set(libs.versions.jetbrains.compose.compiler) +} + kotlin { jvmToolchain { languageVersion.set(JavaLanguageVersion.of(libs.versions.java.toolchain.get())) @@ -22,6 +29,15 @@ kotlin { } } } + + jvm("desktop") { + compilations.configureEach { + compilerOptions.configure { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.fromTarget(libs.versions.java.target.get())) + } + } + } + iosX64() iosArm64() iosSimulatorArm64() @@ -37,8 +53,9 @@ kotlin { framework { baseName = "shared" - export(projects.viewmodel) - export(projects.viewmodelSavedstate) + export(libs.kmp.viewmodel.core) + // export(libs.kmp.viewmodel.savedstate) + export("io.github.hoc081098:kmp-viewmodel-savedstate:0.6.2-SNAPSHOT") export(libs.napier) export(libs.coroutines.core) @@ -48,14 +65,32 @@ kotlin { sourceSets { commonMain { dependencies { - api(projects.viewmodel) - api(projects.viewmodelSavedstate) + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.ui) + implementation(compose.material3) + implementation(compose.material) + + api(projects.navigation) + + api(libs.kmp.viewmodel.core) + // api(libs.kmp.viewmodel.savedstate) + api("io.github.hoc081098:kmp-viewmodel-savedstate:0.6.2-SNAPSHOT") + implementation(libs.kmp.viewmodel.compose) + + implementation("io.coil-kt.coil3:coil-core:3.0.0-alpha01") + implementation("io.coil-kt.coil3:coil-compose:3.0.0-alpha01") + + // Import coil-network and an HTTP client engine. + implementation("io.coil-kt.coil3:coil-network:3.0.0-alpha01") api(libs.napier) api(libs.coroutines.core) - api(libs.koin.core) api(libs.kotlinx.collections.immutable) + api(libs.koin.core) + implementation(libs.koin.compose) + implementation(libs.kotlinx.serialization.json) implementation(libs.flowExt) } @@ -68,10 +103,27 @@ kotlin { androidMain { dependencies { api(libs.koin.android) + + implementation(libs.androidx.appcompat) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.lifecycle.runtime.compose) + + implementation(libs.coroutines.android) + implementation("io.ktor:ktor-client-okhttp:2.3.7") } } val androidUnitTest by getting + val desktopMain by getting { + dependencies { + api(compose.preview) + + implementation(libs.coroutines.swing) + } + } + val desktopTest by getting + val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting @@ -98,7 +150,7 @@ tasks.withType>().configureEach } android { - namespace = "com.hoc081098.kmpviewmodelsample" + namespace = "com.hoc081098.solivagant.sample" compileSdk = libs.versions.sample.android.compile.get().toInt() defaultConfig { minSdk = libs.versions.android.min.get().toInt() @@ -106,19 +158,11 @@ android { buildFeatures { buildConfig = true } - composeOptions { - kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() - } // still needed for Android projects despite toolchain compileOptions { sourceCompatibility = JavaVersion.toVersion(libs.versions.java.target.get()) targetCompatibility = JavaVersion.toVersion(libs.versions.java.target.get()) } - - dependencies { - implementation(platform(libs.androidx.compose.bom)) - compileOnly(libs.androidx.compose.runtime) - } } workaroundForIssueKT51970() diff --git a/sample/shared/shared.podspec b/sample/shared/shared.podspec index d20448dc..5cccd4e5 100644 --- a/sample/shared/shared.podspec +++ b/sample/shared/shared.podspec @@ -46,5 +46,5 @@ Pod::Spec.new do |spec| SCRIPT } ] - + spec.resources = ['build/compose/ios/shared/compose-resources'] end \ No newline at end of file diff --git a/sample/shared/src/androidMain/kotlin/com/hoc081098/kmpviewmodelsample/common/Immutable.kt b/sample/shared/src/androidMain/kotlin/com/hoc081098/kmpviewmodelsample/common/Immutable.kt deleted file mode 100644 index 12b400d9..00000000 --- a/sample/shared/src/androidMain/kotlin/com/hoc081098/kmpviewmodelsample/common/Immutable.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.hoc081098.kmpviewmodelsample.common - -@Suppress("ACTUAL_WITHOUT_EXPECT") // TODO: https://youtrack.jetbrains.com/issue/KT-37316 -internal actual typealias Immutable = androidx.compose.runtime.Immutable diff --git a/sample/shared/src/androidMain/kotlin/com/hoc081098/kmpviewmodelsample/common/AndroidAppDispatchers.kt b/sample/shared/src/androidMain/kotlin/com/hoc081098/solivagant/sample/common/AndroidAppDispatchers.kt similarity index 86% rename from sample/shared/src/androidMain/kotlin/com/hoc081098/kmpviewmodelsample/common/AndroidAppDispatchers.kt rename to sample/shared/src/androidMain/kotlin/com/hoc081098/solivagant/sample/common/AndroidAppDispatchers.kt index 1fc6b45c..423f1df8 100644 --- a/sample/shared/src/androidMain/kotlin/com/hoc081098/kmpviewmodelsample/common/AndroidAppDispatchers.kt +++ b/sample/shared/src/androidMain/kotlin/com/hoc081098/solivagant/sample/common/AndroidAppDispatchers.kt @@ -1,4 +1,4 @@ -package com.hoc081098.kmpviewmodelsample.common +package com.hoc081098.solivagant.sample.common import kotlinx.coroutines.Dispatchers diff --git a/sample/shared/src/androidMain/kotlin/com/hoc081098/solivagant/sample/common/CollectWithLifecycleEffect.android.kt b/sample/shared/src/androidMain/kotlin/com/hoc081098/solivagant/sample/common/CollectWithLifecycleEffect.android.kt new file mode 100644 index 00000000..0ca40e86 --- /dev/null +++ b/sample/shared/src/androidMain/kotlin/com/hoc081098/solivagant/sample/common/CollectWithLifecycleEffect.android.kt @@ -0,0 +1,105 @@ +package com.hoc081098.solivagant.sample.common + +import androidx.lifecycle.compose.collectAsStateWithLifecycle as androidXCollectAsStateWithLifecycle +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.NonRestartableComposable +import androidx.compose.runtime.RememberObserver +import androidx.compose.runtime.State +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +@Composable +actual fun Flow.CollectWithLifecycleEffect( + vararg keys: Any?, + dispatcher: CollectWithLifecycleEffectDispatcher, + collector: (T) -> Unit +) { + val lifecycleOwner = LocalLifecycleOwner.current + val minActiveState = Lifecycle.State.STARTED + + val flow = this + val collectorState = rememberUpdatedState(collector) + + val block: suspend CoroutineScope.() -> Unit = { + lifecycleOwner.repeatOnLifecycle(minActiveState) { + // NOTE: we don't use `flow.collect(collectState.value)` because it can use the old value + flow.collect { collectorState.value(it) } + } + } + + when (dispatcher) { + CollectWithLifecycleEffectDispatcher.ImmediateMain -> { + LaunchedEffectInImmediateMain(flow, lifecycleOwner, minActiveState, *keys, block = block) + } + + CollectWithLifecycleEffectDispatcher.Main -> { + LaunchedEffectInMain(flow, lifecycleOwner, minActiveState, *keys, block = block) + } + + CollectWithLifecycleEffectDispatcher.Composer -> { + LaunchedEffect(flow, lifecycleOwner, minActiveState, *keys, block = block) + } + } +} + +@Composable +actual fun StateFlow.collectAsStateWithLifecycle( + context: CoroutineContext +): State = androidXCollectAsStateWithLifecycle(context = context) + + +@Composable +@NonRestartableComposable +@Suppress("ArrayReturn") +private fun LaunchedEffectInImmediateMain( + vararg keys: Any?, + block: suspend CoroutineScope.() -> Unit, +) { + remember(*keys) { LaunchedEffectImpl(block, Dispatchers.Main.immediate) } +} + +@Composable +@NonRestartableComposable +@Suppress("ArrayReturn") +private fun LaunchedEffectInMain( + vararg keys: Any?, + block: suspend CoroutineScope.() -> Unit, +) { + remember(*keys) { LaunchedEffectImpl(block, Dispatchers.Main) } +} + +private class LaunchedEffectImpl( + private val task: suspend CoroutineScope.() -> Unit, + dispatcher: CoroutineDispatcher, +) : RememberObserver { + private val scope = CoroutineScope(dispatcher) + private var job: Job? = null + + override fun onRemembered() { + job?.cancel("Old job was still running!") + job = scope.launch(block = task) + } + + override fun onForgotten() { + job?.cancel() + job = null + } + + override fun onAbandoned() { + job?.cancel() + job = null + } +} diff --git a/sample/shared/src/androidMain/kotlin/com/hoc081098/kmpviewmodelsample/common/WeakReference.android.kt b/sample/shared/src/androidMain/kotlin/com/hoc081098/solivagant/sample/common/WeakReference.android.kt similarity index 77% rename from sample/shared/src/androidMain/kotlin/com/hoc081098/kmpviewmodelsample/common/WeakReference.android.kt rename to sample/shared/src/androidMain/kotlin/com/hoc081098/solivagant/sample/common/WeakReference.android.kt index 145e3599..182d38a4 100644 --- a/sample/shared/src/androidMain/kotlin/com/hoc081098/kmpviewmodelsample/common/WeakReference.android.kt +++ b/sample/shared/src/androidMain/kotlin/com/hoc081098/solivagant/sample/common/WeakReference.android.kt @@ -1,4 +1,4 @@ -package com.hoc081098.kmpviewmodelsample.common +package com.hoc081098.solivagant.sample.common @Suppress("ACTUAL_WITHOUT_EXPECT") // TODO: https://youtrack.jetbrains.com/issue/KT-37316 internal actual typealias WeakReference = java.lang.ref.WeakReference diff --git a/sample/shared/src/androidMain/kotlin/com/hoc081098/kmpviewmodelsample/common/identityHashCode.android.kt b/sample/shared/src/androidMain/kotlin/com/hoc081098/solivagant/sample/common/identityHashCode.android.kt similarity index 63% rename from sample/shared/src/androidMain/kotlin/com/hoc081098/kmpviewmodelsample/common/identityHashCode.android.kt rename to sample/shared/src/androidMain/kotlin/com/hoc081098/solivagant/sample/common/identityHashCode.android.kt index b51492cb..539d0ce2 100644 --- a/sample/shared/src/androidMain/kotlin/com/hoc081098/kmpviewmodelsample/common/identityHashCode.android.kt +++ b/sample/shared/src/androidMain/kotlin/com/hoc081098/solivagant/sample/common/identityHashCode.android.kt @@ -1,3 +1,3 @@ -package com.hoc081098.kmpviewmodelsample.common +package com.hoc081098.solivagant.sample.common internal actual fun Any?.identityHashCode(): Int = System.identityHashCode(this) diff --git a/sample/shared/src/androidMain/kotlin/com/hoc081098/solivagant/sample/common/platformToast.android.kt b/sample/shared/src/androidMain/kotlin/com/hoc081098/solivagant/sample/common/platformToast.android.kt new file mode 100644 index 00000000..63c7a6ad --- /dev/null +++ b/sample/shared/src/androidMain/kotlin/com/hoc081098/solivagant/sample/common/platformToast.android.kt @@ -0,0 +1,10 @@ +package com.hoc081098.solivagant.sample.common + +import androidx.compose.runtime.Stable + +@Stable +actual class PlatformToastManager actual constructor() { + actual fun showToast(message: String) { + // TODO: Show toast + } +} diff --git a/sample/shared/src/androidMain/kotlin/com/hoc081098/kmpviewmodelsample/di.android.kt b/sample/shared/src/androidMain/kotlin/com/hoc081098/solivagant/sample/di.android.kt similarity index 60% rename from sample/shared/src/androidMain/kotlin/com/hoc081098/kmpviewmodelsample/di.android.kt rename to sample/shared/src/androidMain/kotlin/com/hoc081098/solivagant/sample/di.android.kt index 991b79a2..4b09652f 100644 --- a/sample/shared/src/androidMain/kotlin/com/hoc081098/kmpviewmodelsample/di.android.kt +++ b/sample/shared/src/androidMain/kotlin/com/hoc081098/solivagant/sample/di.android.kt @@ -1,10 +1,11 @@ -package com.hoc081098.kmpviewmodelsample +package com.hoc081098.solivagant.sample -import com.hoc081098.kmpviewmodelsample.common.AndroidAppDispatchers -import com.hoc081098.kmpviewmodelsample.common.AppDispatchers -import com.hoc081098.kmpviewmodelsample.product_detail.ProductDetailViewModel -import com.hoc081098.kmpviewmodelsample.products.ProductsViewModel -import com.hoc081098.kmpviewmodelsample.search_products.SearchProductsViewModel +import com.hoc081098.solivagant.sample.common.AndroidAppDispatchers +import com.hoc081098.solivagant.sample.common.AppDispatchers +import com.hoc081098.solivagant.sample.product_detail.ProductDetailViewModel +import com.hoc081098.solivagant.sample.products.ProductsViewModel +import com.hoc081098.solivagant.sample.search_products.SearchProductsViewModel +import io.github.aakira.napier.BuildConfig import io.github.aakira.napier.DebugAntilog import io.github.aakira.napier.Napier import org.koin.androidx.viewmodel.dsl.viewModelOf diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/common/Immutable.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/common/Immutable.kt deleted file mode 100644 index 45b8b43d..00000000 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/common/Immutable.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.hoc081098.kmpviewmodelsample.common - -@MustBeDocumented -@Target(AnnotationTarget.CLASS) -@Retention(AnnotationRetention.BINARY) -internal expect annotation class Immutable() diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/common/debugCheckImmediateMainDispatcher.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/common/debugCheckImmediateMainDispatcher.kt deleted file mode 100644 index f8363651..00000000 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/common/debugCheckImmediateMainDispatcher.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.hoc081098.kmpviewmodelsample.common - -import com.hoc081098.kmpviewmodelsample.isDebug -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.currentCoroutineContext - -@OptIn(ExperimentalStdlibApi::class) -internal suspend inline fun debugCheckImmediateMainDispatcher() { - if (isDebug()) { - val dispatcher = currentCoroutineContext()[CoroutineDispatcher] - check(dispatcher === Dispatchers.Main.immediate) { - "Expected CoroutineDispatcher to be Dispatchers.Main.immediate but was $dispatcher" - } - } -} diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/snippets/SearchViewModel.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/snippets/SearchViewModel.kt deleted file mode 100644 index 9782e81e..00000000 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/snippets/SearchViewModel.kt +++ /dev/null @@ -1,39 +0,0 @@ -@file:Suppress("unused") // Snippet - -package com.hoc081098.kmpviewmodelsample.snippets - -import com.hoc081098.kmp.viewmodel.SavedStateHandle -import com.hoc081098.kmp.viewmodel.ViewModel -import com.hoc081098.kmp.viewmodel.safe.NonNullSavedStateHandleKey -import com.hoc081098.kmp.viewmodel.safe.NullableSavedStateHandleKey -import com.hoc081098.kmp.viewmodel.safe.safe -import com.hoc081098.kmp.viewmodel.safe.string -import com.hoc081098.kmp.viewmodel.wrapper.NonNullStateFlowWrapper -import com.hoc081098.kmp.viewmodel.wrapper.NullableStateFlowWrapper -import com.hoc081098.kmp.viewmodel.wrapper.wrap - -class SearchViewModel( - private val savedStateHandle: SavedStateHandle, -) : ViewModel() { - internal val searchTermStateFlow: NonNullStateFlowWrapper = savedStateHandle - .safe { it.getStateFlow(searchTermKey) } - .wrap() - - internal val userIdStateFlow: NullableStateFlowWrapper = savedStateHandle - .safe { it.getStateFlow(userIdKey) } - .wrap() - - internal fun changeSearchTerm(searchTerm: String) { - savedStateHandle.safe { it[searchTermKey] = searchTerm } - } - - private fun setUserId(userId: String?) = savedStateHandle.safe { it[userIdKey] = userId } - - companion object { - private val searchTermKey: NonNullSavedStateHandleKey = NonNullSavedStateHandleKey.string( - key = "search_term", - defaultValue = "", - ) - private val userIdKey: NullableSavedStateHandleKey = NullableSavedStateHandleKey.string("user_id") - } -} diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/snippets/UserViewModel.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/snippets/UserViewModel.kt deleted file mode 100644 index 45fc56bf..00000000 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/snippets/UserViewModel.kt +++ /dev/null @@ -1,44 +0,0 @@ -@file:Suppress("unused") // Snippet - -package com.hoc081098.kmpviewmodelsample.snippets - -import com.hoc081098.kmp.viewmodel.SavedStateHandle -import com.hoc081098.kmp.viewmodel.ViewModel -import com.hoc081098.kmp.viewmodel.parcelable.Parcelable -import com.hoc081098.kmp.viewmodel.parcelable.Parcelize -import com.hoc081098.kmp.viewmodel.safe.NullableSavedStateHandleKey -import com.hoc081098.kmp.viewmodel.safe.parcelable -import com.hoc081098.kmp.viewmodel.safe.safe -import com.hoc081098.kmp.viewmodel.wrapper.wrap -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.launch - -@Parcelize -data class User( - val id: Long, - val name: String, -) : Parcelable - -class UserViewModel( - private val savedStateHandle: SavedStateHandle, - private val getUserUseCase: suspend () -> User?, -) : ViewModel() { - val userStateFlow = savedStateHandle.safe { it.getStateFlow(USER_KEY) }.wrap() - - fun getUser() { - viewModelScope.launch { - try { - val user = getUserUseCase() - savedStateHandle.safe { it[USER_KEY] = user } - } catch (e: CancellationException) { - throw e - } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { - e.printStackTrace() - } - } - } - - private companion object { - private val USER_KEY = NullableSavedStateHandleKey.parcelable("user_key") - } -} diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/SolivagantSampleApp.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/SolivagantSampleApp.kt new file mode 100644 index 00000000..6e8f9ef7 --- /dev/null +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/SolivagantSampleApp.kt @@ -0,0 +1,115 @@ +package com.hoc081098.solivagant.sample + +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Icon +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import coil3.ImageLoader +import coil3.annotation.ExperimentalCoilApi +import coil3.compose.setSingletonImageLoaderFactory +import coil3.fetch.NetworkFetcher +import com.hoc081098.solivagant.navigation.BaseRoute +import com.hoc081098.solivagant.navigation.NavEventNavigator +import com.hoc081098.solivagant.navigation.NavHost +import com.hoc081098.solivagant.navigation.NavRoot +import com.hoc081098.solivagant.sample.common.MyApplicationTheme +import com.hoc081098.solivagant.sample.product_detail.ProductDetailScreenDestination +import com.hoc081098.solivagant.sample.product_detail.ProductDetailScreenRoute +import com.hoc081098.solivagant.sample.products.ProductsScreenDestination +import com.hoc081098.solivagant.sample.products.ProductsScreenRoute +import com.hoc081098.solivagant.sample.search_products.SearchProductScreenDestination +import com.hoc081098.solivagant.sample.search_products.SearchProductScreenRoute +import kotlinx.collections.immutable.persistentSetOf +import org.koin.compose.koinInject + + +@OptIn( + ExperimentalLayoutApi::class, + ExperimentalMaterial3Api::class, + ExperimentalCoilApi::class +) +@Composable +fun SolivagantSampleApp( + modifier: Modifier = Modifier, + navigator: NavEventNavigator = koinInject(), +) { + setSingletonImageLoaderFactory { context -> + ImageLoader.Builder(context) + .components { add(NetworkFetcher.Factory()) } + .build() + } + + var currentRoute: BaseRoute? by remember { mutableStateOf(null) } + val destinations = remember { + persistentSetOf( + StartScreenDestination, + ProductsScreenDestination, + SearchProductScreenDestination, + ProductDetailScreenDestination, + ) + } + + MyApplicationTheme { + Surface( + modifier = modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background, + ) { + Scaffold( + topBar = { + TopAppBar( + title = { + Text( + text = when (currentRoute) { + StartScreenRoute -> "KMP ViewModel Sample" + is ProductsScreenRoute -> "Products screen" + is SearchProductScreenRoute -> "Search products screen" + is ProductDetailScreenRoute -> "Product detail screen" + else -> "KMP ViewModel Sample" + }, + ) + }, + navigationIcon = { + if (currentRoute !is NavRoot) { + IconButton( + onClick = remember { navigator::navigateBack } + ) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = "Back", + ) + } + } + }, + ) + }, + ) { innerPadding -> + NavHost( + modifier = Modifier + .padding(innerPadding) + .consumeWindowInsets(innerPadding) + .fillMaxSize(), + startRoute = StartScreenRoute, + destinations = destinations, + navEventNavigator = navigator, + destinationChangedCallback = { currentRoute = it }, + ) + } + } + } +} diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/StartScreen.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/StartScreen.kt new file mode 100644 index 00000000..88721629 --- /dev/null +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/StartScreen.kt @@ -0,0 +1,82 @@ +package com.hoc081098.solivagant.sample + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.hoc081098.kmp.viewmodel.parcelable.Parcelize +import com.hoc081098.solivagant.navigation.NavDestination +import com.hoc081098.solivagant.navigation.NavEventNavigator +import com.hoc081098.solivagant.navigation.NavRoot +import com.hoc081098.solivagant.navigation.NavRoute +import com.hoc081098.solivagant.navigation.ScreenDestination +import com.hoc081098.solivagant.sample.products.ProductsScreenRoute +import com.hoc081098.solivagant.sample.search_products.SearchProductScreenRoute +import kotlin.jvm.JvmField +import org.koin.compose.koinInject + +@Parcelize +data object StartScreenRoute : NavRoute, NavRoot + +@JvmField +val StartScreenDestination: NavDestination = + ScreenDestination { StartScreen() } + +@Composable +internal fun StartScreen( + modifier: Modifier = Modifier, + navigator: NavEventNavigator = koinInject(), +) { + Column( + modifier = modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + ProductsButton( + navigateToProducts = { navigator.navigateTo(ProductsScreenRoute) } + ) + + Spacer(modifier = Modifier.height(16.dp)) + + SearchProductsButton( + navigateToSearch = { navigator.navigateTo(SearchProductScreenRoute) } + ) + } +} + +@Composable +private fun SearchProductsButton( + navigateToSearch: () -> Unit, + modifier: Modifier = Modifier, +) { + Button( + modifier = modifier, + onClick = navigateToSearch, + ) { + Text( + text = "Search products screen", + ) + } +} + +@Composable +private fun ProductsButton( + navigateToProducts: () -> Unit, + modifier: Modifier = Modifier, +) { + Button( + modifier = modifier, + onClick = navigateToProducts, + ) { + Text( + text = "Products screen", + ) + } +} diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/common/AppDispatchers.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/AppDispatchers.kt similarity index 85% rename from sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/common/AppDispatchers.kt rename to sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/AppDispatchers.kt index bf6e939a..6d6d1780 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/common/AppDispatchers.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/AppDispatchers.kt @@ -1,4 +1,4 @@ -package com.hoc081098.kmpviewmodelsample.common +package com.hoc081098.solivagant.sample.common import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.MainCoroutineDispatcher diff --git a/sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/common/CollectWithLifecycleEffect.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/CollectWithLifecycleEffect.kt similarity index 54% rename from sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/common/CollectWithLifecycleEffect.kt rename to sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/CollectWithLifecycleEffect.kt index 904abed9..e5c329e7 100644 --- a/sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/common/CollectWithLifecycleEffect.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/CollectWithLifecycleEffect.kt @@ -1,22 +1,22 @@ -package com.hoc081098.kmpviewmodelsample.android.common +package com.hoc081098.solivagant.sample.common import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.NonRestartableComposable import androidx.compose.runtime.RememberObserver +import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.repeatOnLifecycle +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch @Immutable @@ -62,77 +62,13 @@ enum class CollectWithLifecycleEffectDispatcher { * @see [CollectWithLifecycleEffectDispatcher] */ @Composable -fun Flow.CollectWithLifecycleEffect( +expect fun Flow.CollectWithLifecycleEffect( vararg keys: Any?, - lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, - minActiveState: Lifecycle.State = Lifecycle.State.STARTED, dispatcher: CollectWithLifecycleEffectDispatcher = CollectWithLifecycleEffectDispatcher.ImmediateMain, collector: (T) -> Unit, -) { - val flow = this - val collectorState = rememberUpdatedState(collector) - - val block: suspend CoroutineScope.() -> Unit = { - lifecycleOwner.repeatOnLifecycle(minActiveState) { - // NOTE: we don't use `flow.collect(collectState.value)` because it can use the old value - flow.collect { collectorState.value(it) } - } - } - - when (dispatcher) { - CollectWithLifecycleEffectDispatcher.ImmediateMain -> { - LaunchedEffectInImmediateMain(flow, lifecycleOwner, minActiveState, *keys, block = block) - } - - CollectWithLifecycleEffectDispatcher.Main -> { - LaunchedEffectInMain(flow, lifecycleOwner, minActiveState, *keys, block = block) - } - - CollectWithLifecycleEffectDispatcher.Composer -> { - LaunchedEffect(flow, lifecycleOwner, minActiveState, *keys, block = block) - } - } -} - -@Composable -@NonRestartableComposable -@Suppress("ArrayReturn") -private fun LaunchedEffectInImmediateMain( - vararg keys: Any?, - block: suspend CoroutineScope.() -> Unit, -) { - remember(*keys) { LaunchedEffectImpl(block, Dispatchers.Main.immediate) } -} +) @Composable -@NonRestartableComposable -@Suppress("ArrayReturn") -private fun LaunchedEffectInMain( - vararg keys: Any?, - block: suspend CoroutineScope.() -> Unit, -) { - remember(*keys) { LaunchedEffectImpl(block, Dispatchers.Main) } -} - -private class LaunchedEffectImpl( - private val task: suspend CoroutineScope.() -> Unit, - dispatcher: CoroutineDispatcher, -) : RememberObserver { - private val scope = CoroutineScope(dispatcher) - private var job: Job? = null - - override fun onRemembered() { - job?.cancel("Old job was still running!") - job = scope.launch(block = task) - } - - override fun onForgotten() { - job?.cancel() - job = null - } - - override fun onAbandoned() { - job?.cancel() - job = null - } -} +expect fun StateFlow.collectAsStateWithLifecycle( + context: CoroutineContext = EmptyCoroutineContext +): State diff --git a/sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/common/MyApplicationTheme.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/MyApplicationTheme.kt similarity index 99% rename from sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/common/MyApplicationTheme.kt rename to sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/MyApplicationTheme.kt index 45e88d55..5d69f740 100644 --- a/sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/common/MyApplicationTheme.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/MyApplicationTheme.kt @@ -1,6 +1,6 @@ @file:Suppress("MagicNumber") -package com.hoc081098.kmpviewmodelsample.android.common +package com.hoc081098.solivagant.sample.common import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.shape.RoundedCornerShape diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/ProductItemUi.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/ProductItemUi.kt similarity index 84% rename from sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/ProductItemUi.kt rename to sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/ProductItemUi.kt index 803e5484..a32df189 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/ProductItemUi.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/ProductItemUi.kt @@ -1,8 +1,8 @@ -package com.hoc081098.kmpviewmodelsample +package com.hoc081098.solivagant.sample.common -import com.hoc081098.kmpviewmodelsample.ProductItemUi.CategoryUi -import com.hoc081098.kmpviewmodelsample.common.Immutable -import com.hoc081098.kmpviewmodelsample.data.ProductItem +import androidx.compose.runtime.Immutable +import com.hoc081098.solivagant.sample.common.ProductItemUi.CategoryUi +import com.hoc081098.solivagant.sample.data.ProductItem import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/common/SingleEventFlow.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/SingleEventFlow.kt similarity index 98% rename from sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/common/SingleEventFlow.kt rename to sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/SingleEventFlow.kt index 76b8c213..6e0579d0 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/common/SingleEventFlow.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/SingleEventFlow.kt @@ -1,4 +1,4 @@ -package com.hoc081098.kmpviewmodelsample.common +package com.hoc081098.solivagant.sample.common import com.hoc081098.kmp.viewmodel.Closeable import com.hoc081098.kmp.viewmodel.MainThread diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/common/WeakReference.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/WeakReference.kt similarity index 65% rename from sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/common/WeakReference.kt rename to sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/WeakReference.kt index a39dacda..93ab9ead 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/common/WeakReference.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/WeakReference.kt @@ -1,4 +1,4 @@ -package com.hoc081098.kmpviewmodelsample.common +package com.hoc081098.solivagant.sample.common internal expect class WeakReference constructor(reference: T) { fun get(): T? diff --git a/sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/common/commonUi.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/commonUi.kt similarity index 95% rename from sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/common/commonUi.kt rename to sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/commonUi.kt index 77c4e167..a5a46898 100644 --- a/sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/common/commonUi.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/commonUi.kt @@ -1,6 +1,4 @@ -@file:OptIn(ExperimentalMaterialApi::class) - -package com.hoc081098.kmpviewmodelsample.android.common +package com.hoc081098.solivagant.sample.common import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -39,10 +37,9 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import coil.compose.AsyncImagePainter -import coil.compose.SubcomposeAsyncImage -import coil.compose.SubcomposeAsyncImageContent -import com.hoc081098.kmpviewmodelsample.ProductItemUi +import coil3.compose.AsyncImagePainter +import coil3.compose.SubcomposeAsyncImage +import coil3.compose.SubcomposeAsyncImageContent import kotlinx.collections.immutable.ImmutableList @Composable @@ -91,6 +88,7 @@ internal fun ErrorMessageAndRetryButton( } } +@OptIn(ExperimentalMaterialApi::class) @Suppress("LongParameterList") @Composable internal fun ProductItemsList( diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/debugCheckImmediateMainDispatcher.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/debugCheckImmediateMainDispatcher.kt new file mode 100644 index 00000000..2fb098c6 --- /dev/null +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/debugCheckImmediateMainDispatcher.kt @@ -0,0 +1,22 @@ +package com.hoc081098.solivagant.sample.common + +import com.hoc081098.solivagant.sample.isDebug +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.currentCoroutineContext + +@OptIn(ExperimentalStdlibApi::class) +internal suspend inline fun debugCheckImmediateMainDispatcher() { + if (isDebug()) { + val dispatcher = checkNotNull(currentCoroutineContext()[CoroutineDispatcher]) { + "Expected CoroutineDispatcher to be not null" + } + + check( + dispatcher === Dispatchers.Main.immediate || + !dispatcher.isDispatchNeeded(Dispatchers.Main.immediate) + ) { + "Expected CoroutineDispatcher to be Dispatchers.Main.immediate but was $dispatcher" + } + } +} diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/common/identityHashCode.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/identityHashCode.kt similarity index 51% rename from sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/common/identityHashCode.kt rename to sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/identityHashCode.kt index 85d80214..a85c97c9 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/common/identityHashCode.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/identityHashCode.kt @@ -1,3 +1,3 @@ -package com.hoc081098.kmpviewmodelsample.common +package com.hoc081098.solivagant.sample.common internal expect fun Any?.identityHashCode(): Int diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/platformToast.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/platformToast.kt new file mode 100644 index 00000000..d58b47f9 --- /dev/null +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/platformToast.kt @@ -0,0 +1,8 @@ +package com.hoc081098.solivagant.sample.common + +import androidx.compose.runtime.Stable + +@Stable +expect class PlatformToastManager() { + fun showToast(message: String) +} diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/data/ProductItem.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/data/ProductItem.kt similarity index 95% rename from sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/data/ProductItem.kt rename to sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/data/ProductItem.kt index 35bd516e..8b6df01f 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/data/ProductItem.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/data/ProductItem.kt @@ -1,4 +1,4 @@ -package com.hoc081098.kmpviewmodelsample.data +package com.hoc081098.solivagant.sample.data import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/data/fakeJson.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/data/fakeJson.kt similarity index 99% rename from sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/data/fakeJson.kt rename to sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/data/fakeJson.kt index 7206bbc8..0a886265 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/data/fakeJson.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/data/fakeJson.kt @@ -1,4 +1,4 @@ -package com.hoc081098.kmpviewmodelsample.data +package com.hoc081098.solivagant.sample.data const val FakeProductsJson = """ [{ diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/di.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/di.kt similarity index 66% rename from sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/di.kt rename to sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/di.kt index a72d089d..fd2eba72 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/di.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/di.kt @@ -1,9 +1,9 @@ -package com.hoc081098.kmpviewmodelsample +package com.hoc081098.solivagant.sample -import com.hoc081098.kmpviewmodelsample.common.SingleEventChannel -import com.hoc081098.kmpviewmodelsample.product_detail.GetProductById -import com.hoc081098.kmpviewmodelsample.products.GetProducts -import com.hoc081098.kmpviewmodelsample.search_products.SearchProducts +import com.hoc081098.solivagant.sample.common.SingleEventChannel +import com.hoc081098.solivagant.sample.product_detail.GetProductById +import com.hoc081098.solivagant.sample.products.GetProducts +import com.hoc081098.solivagant.sample.search_products.SearchProducts import org.koin.core.context.startKoin import org.koin.core.module.Module import org.koin.core.module.dsl.factoryOf diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/product_detail/GetProductById.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/GetProductById.kt similarity index 70% rename from sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/product_detail/GetProductById.kt rename to sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/GetProductById.kt index c0c712b7..b72bb8c7 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/product_detail/GetProductById.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/GetProductById.kt @@ -1,10 +1,10 @@ @file:Suppress("PackageNaming") -package com.hoc081098.kmpviewmodelsample.product_detail +package com.hoc081098.solivagant.sample.product_detail -import com.hoc081098.kmpviewmodelsample.common.AppDispatchers -import com.hoc081098.kmpviewmodelsample.data.FakeProductsJson -import com.hoc081098.kmpviewmodelsample.data.ProductItem +import com.hoc081098.solivagant.sample.common.AppDispatchers +import com.hoc081098.solivagant.sample.data.FakeProductsJson +import com.hoc081098.solivagant.sample.data.ProductItem import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json diff --git a/sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/product_detail/ProductDetailScreen.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/ProductDetailScreen.kt similarity index 77% rename from sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/product_detail/ProductDetailScreen.kt rename to sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/ProductDetailScreen.kt index e1b76178..2e2ffb29 100644 --- a/sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/product_detail/ProductDetailScreen.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/ProductDetailScreen.kt @@ -1,6 +1,6 @@ @file:Suppress("PackageNaming") -package com.hoc081098.kmpviewmodelsample.android.product_detail +package com.hoc081098.solivagant.sample.product_detail import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -22,6 +22,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -31,36 +32,49 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import coil.compose.AsyncImagePainter -import coil.compose.SubcomposeAsyncImage -import coil.compose.SubcomposeAsyncImageContent -import com.hoc081098.kmpviewmodelsample.ProductItemUi -import com.hoc081098.kmpviewmodelsample.android.common.ErrorMessageAndRetryButton -import com.hoc081098.kmpviewmodelsample.android.common.LoadingIndicator -import com.hoc081098.kmpviewmodelsample.android.common.OnLifecycleEventWithBuilder -import com.hoc081098.kmpviewmodelsample.product_detail.ProductDetailState -import com.hoc081098.kmpviewmodelsample.product_detail.ProductDetailViewModel -import io.github.aakira.napier.Napier +import coil3.compose.AsyncImagePainter +import coil3.compose.SubcomposeAsyncImage +import coil3.compose.SubcomposeAsyncImageContent +import com.hoc081098.kmp.viewmodel.compose.kmpViewModel +import com.hoc081098.kmp.viewmodel.createSavedStateHandle +import com.hoc081098.kmp.viewmodel.viewModelFactory +import com.hoc081098.solivagant.sample.common.ErrorMessageAndRetryButton +import com.hoc081098.solivagant.sample.common.LoadingIndicator +import com.hoc081098.solivagant.sample.common.ProductItemUi +import com.hoc081098.solivagant.sample.common.collectAsStateWithLifecycle import kotlinx.collections.immutable.persistentListOf -import org.koin.androidx.compose.koinViewModel +import org.koin.compose.koinInject @Composable fun ProductDetailScreen( modifier: Modifier = Modifier, - viewModel: ProductDetailViewModel = koinViewModel(), + getProductById: GetProductById = koinInject(), + viewModel: ProductDetailViewModel = kmpViewModel( + factory = viewModelFactory { + ProductDetailViewModel( + savedStateHandle = createSavedStateHandle(), + getProductById = getProductById, + ) + }, + ), ) { val refresh = remember(viewModel) { @Suppress("SuspiciousCallableReferenceInLambda") viewModel::refresh } - OnLifecycleEventWithBuilder(refresh) { - onResume { refresh() } - onPause { Napier.d("[ProductDetailScreen] paused") } - onEach { owner, event -> Napier.d("[ProductDetailScreen] event=$event, owner=$owner") } + DisposableEffect(refresh) { + refresh() + onDispose { } } + // TODO: OnLifecycleEventWithBuilder + // OnLifecycleEventWithBuilder(refresh) { + // onResume { refresh() } + // onPause { Napier.d("[ProductDetailScreen] paused") } + // onEach { owner, event -> Napier.d("[ProductDetailScreen] event=$event, owner=$owner") } + // } + val state by viewModel.stateFlow.collectAsStateWithLifecycle() when (val s = state) { is ProductDetailState.Error -> { diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/ProductDetailScreenRoute.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/ProductDetailScreenRoute.kt new file mode 100644 index 00000000..c43c29b0 --- /dev/null +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/ProductDetailScreenRoute.kt @@ -0,0 +1,16 @@ +package com.hoc081098.solivagant.sample.product_detail + +import androidx.compose.runtime.Immutable +import com.hoc081098.kmp.viewmodel.parcelable.Parcelize +import com.hoc081098.solivagant.navigation.NavDestination +import com.hoc081098.solivagant.navigation.NavRoute +import com.hoc081098.solivagant.navigation.ScreenDestination +import kotlin.jvm.JvmField + +@Immutable +@Parcelize +data class ProductDetailScreenRoute(val id: Int) : NavRoute + +@JvmField +val ProductDetailScreenDestination: NavDestination = + ScreenDestination { ProductDetailScreen() } diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/ProductDetailState.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/ProductDetailState.kt new file mode 100644 index 00000000..b9989079 --- /dev/null +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/ProductDetailState.kt @@ -0,0 +1,11 @@ +package com.hoc081098.solivagant.sample.product_detail + +import androidx.compose.runtime.Immutable +import com.hoc081098.solivagant.sample.common.ProductItemUi + +@Immutable +sealed interface ProductDetailState { + data class Success(val product: ProductItemUi) : ProductDetailState + data object Loading : ProductDetailState + data class Error(val error: Throwable) : ProductDetailState +} diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/product_detail/ProductDetailViewModel.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/ProductDetailViewModel.kt similarity index 57% rename from sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/product_detail/ProductDetailViewModel.kt rename to sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/ProductDetailViewModel.kt index dc32b94c..bd3a4e82 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/product_detail/ProductDetailViewModel.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/product_detail/ProductDetailViewModel.kt @@ -1,23 +1,17 @@ @file:Suppress("PackageNaming") -package com.hoc081098.kmpviewmodelsample.product_detail +package com.hoc081098.solivagant.sample.product_detail import com.hoc081098.flowext.flatMapFirst import com.hoc081098.flowext.flowFromSuspend import com.hoc081098.flowext.startWith import com.hoc081098.kmp.viewmodel.SavedStateHandle import com.hoc081098.kmp.viewmodel.ViewModel -import com.hoc081098.kmp.viewmodel.safe.DelicateSafeSavedStateHandleApi -import com.hoc081098.kmp.viewmodel.safe.NullableSavedStateHandleKey -import com.hoc081098.kmp.viewmodel.safe.int -import com.hoc081098.kmp.viewmodel.safe.safe import com.hoc081098.kmp.viewmodel.wrapper.NonNullStateFlowWrapper import com.hoc081098.kmp.viewmodel.wrapper.wrap -import com.hoc081098.kmpviewmodelsample.ProductItemUi -import com.hoc081098.kmpviewmodelsample.common.Immutable -import com.hoc081098.kmpviewmodelsample.toProductItemUi +import com.hoc081098.solivagant.navigation.requireRoute +import com.hoc081098.solivagant.sample.common.toProductItemUi import io.github.aakira.napier.Napier -import kotlin.jvm.JvmField import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted @@ -31,30 +25,18 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.yield -@Immutable -sealed interface ProductDetailState { - data class Success(val product: ProductItemUi) : ProductDetailState - data object Loading : ProductDetailState - data class Error(val error: Throwable) : ProductDetailState -} - -@OptIn(ExperimentalCoroutinesApi::class, DelicateSafeSavedStateHandleApi::class) +@OptIn(ExperimentalCoroutinesApi::class) class ProductDetailViewModel( savedStateHandle: SavedStateHandle, private val getProductById: GetProductById, ) : ViewModel() { - private val id = checkNotNull(savedStateHandle.safe { it[ID_SAVED_KEY] }) { - """id must not be null. - |For non-Android platforms, you must set id to `SavedStateHandle` with key ${ID_SAVED_KEY.key}, - |and pass that `SavedStateHandle` to `ProductDetailViewModel` constructor. - """.trimMargin() - } + private val route = savedStateHandle.requireRoute() private val refreshFlow = MutableSharedFlow(extraBufferCapacity = 1) private val retryFlow = MutableSharedFlow(extraBufferCapacity = 1) - private val productItemFlow = flowFromSuspend { getProductById(id) } - .onStart { Napier.d("getProductById id=$id") } + private val productItemFlow = flowFromSuspend { getProductById(route.id) } + .onStart { Napier.d("getProductById id=$route") } .map { ProductDetailState.Success(it.toProductItemUi()) } val stateFlow: NonNullStateFlowWrapper = merge( @@ -100,19 +82,4 @@ class ProductDetailViewModel( } } } - - companion object { - // This key is used by non-Android platforms to set id to SavedStateHandle, - // used by Android platform to set id to Bundle (handled by Compose-Navigation). - @JvmField - val ID_SAVED_KEY = NullableSavedStateHandleKey.int("id") - - /** - * This factory method is used by non-Android platforms to create an instance of [ProductDetailViewModel]. - */ - fun create(id: Int, getProductById: GetProductById): ProductDetailViewModel = ProductDetailViewModel( - savedStateHandle = SavedStateHandle(mapOf(ID_SAVED_KEY.key to id)), - getProductById = getProductById, - ) - } } diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/products/GetProducts.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/products/GetProducts.kt similarity index 74% rename from sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/products/GetProducts.kt rename to sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/products/GetProducts.kt index a92413f0..faf7e393 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/products/GetProducts.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/products/GetProducts.kt @@ -1,8 +1,8 @@ -package com.hoc081098.kmpviewmodelsample.products +package com.hoc081098.solivagant.sample.products -import com.hoc081098.kmpviewmodelsample.common.AppDispatchers -import com.hoc081098.kmpviewmodelsample.data.FakeProductsJson -import com.hoc081098.kmpviewmodelsample.data.ProductItem +import com.hoc081098.solivagant.sample.common.AppDispatchers +import com.hoc081098.solivagant.sample.data.FakeProductsJson +import com.hoc081098.solivagant.sample.data.ProductItem import kotlinx.coroutines.delay import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock diff --git a/sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/products/ProductsScreen.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/products/ProductsScreen.kt similarity index 53% rename from sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/products/ProductsScreen.kt rename to sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/products/ProductsScreen.kt index 706f452a..29bc85a8 100644 --- a/sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/products/ProductsScreen.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/products/ProductsScreen.kt @@ -1,64 +1,72 @@ -@file:OptIn(ExperimentalMaterialApi::class) +package com.hoc081098.solivagant.sample.products -package com.hoc081098.kmpviewmodelsample.android.products - -import android.widget.Toast import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.withFrameMillis import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.hoc081098.kmpviewmodelsample.android.common.CollectWithLifecycleEffect -import com.hoc081098.kmpviewmodelsample.android.common.EmptyProducts -import com.hoc081098.kmpviewmodelsample.android.common.ErrorMessageAndRetryButton -import com.hoc081098.kmpviewmodelsample.android.common.LoadingIndicator -import com.hoc081098.kmpviewmodelsample.android.common.OnLifecycleEventWithBuilder -import com.hoc081098.kmpviewmodelsample.android.common.ProductItemsList -import com.hoc081098.kmpviewmodelsample.products.ProductSingleEvent -import com.hoc081098.kmpviewmodelsample.products.ProductsAction -import com.hoc081098.kmpviewmodelsample.products.ProductsViewModel -import io.github.aakira.napier.Napier +import com.hoc081098.kmp.viewmodel.compose.kmpViewModel +import com.hoc081098.kmp.viewmodel.viewModelFactory +import com.hoc081098.solivagant.navigation.NavEventNavigator +import com.hoc081098.solivagant.sample.common.CollectWithLifecycleEffect +import com.hoc081098.solivagant.sample.common.EmptyProducts +import com.hoc081098.solivagant.sample.common.ErrorMessageAndRetryButton +import com.hoc081098.solivagant.sample.common.LoadingIndicator +import com.hoc081098.solivagant.sample.common.PlatformToastManager +import com.hoc081098.solivagant.sample.common.ProductItemsList +import com.hoc081098.solivagant.sample.common.SingleEventChannel +import com.hoc081098.solivagant.sample.common.collectAsStateWithLifecycle import kotlinx.coroutines.launch -import org.koin.androidx.compose.koinViewModel +import org.koin.compose.koinInject +@OptIn(ExperimentalMaterialApi::class) @Suppress("ReturnCount", "ModifierReused") @Composable fun ProductsScreen( - navigateToProductDetail: (Int) -> Unit, modifier: Modifier = Modifier, - viewModel: ProductsViewModel = koinViewModel(), + getProducts: GetProducts = koinInject(), + singleEventChannel: SingleEventChannel = koinInject(), + navigator: NavEventNavigator = koinInject(), + viewModel: ProductsViewModel = kmpViewModel( + factory = viewModelFactory { + ProductsViewModel( + getProducts = getProducts, + singleEventChannel = singleEventChannel, + navigator = navigator, + ) + }, + ), ) { - LaunchedEffect(viewModel) { + DisposableEffect(viewModel) { if (!viewModel.stateFlow.value.hasContent) { viewModel.dispatch(ProductsAction.Load) } + onDispose { } } - OnLifecycleEventWithBuilder { - onEach { owner, event -> Napier.d("[ProductsScreen] event=$event, owner=$owner") } - } + // TODO: OnLifecycleEventWithBuilder + // OnLifecycleEventWithBuilder { + // onEach { owner, event -> Napier.d("[ProductsScreen] event=$event, owner=$owner") } + // } val state by viewModel.stateFlow.collectAsStateWithLifecycle() val scope = rememberCoroutineScope() val lazyListState = rememberLazyListState() val currentLazyListState by rememberUpdatedState(lazyListState) - val context = LocalContext.current - val eventHandler: (ProductSingleEvent) -> Unit = remember(context, scope, currentLazyListState) { - { - event -> + val toastManager = PlatformToastManager() + val eventHandler: (ProductSingleEvent) -> Unit = remember(toastManager, scope, currentLazyListState) { + { event -> when (event) { is ProductSingleEvent.Refresh.Failure -> { - Toast.makeText(context, "Failed to refresh", Toast.LENGTH_SHORT).show() + toastManager.showToast("Failed to refresh") } ProductSingleEvent.Refresh.Success -> { @@ -99,6 +107,6 @@ fun ProductsScreen( ), isRefreshing = state.isRefreshing, lazyListState = lazyListState, - onItemClick = { navigateToProductDetail(it.id) }, + onItemClick = { viewModel.dispatch(ProductsAction.NavigateToProductDetail(it.id)) }, ) } diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/products/ProductsScreenRoute.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/products/ProductsScreenRoute.kt new file mode 100644 index 00000000..af89989d --- /dev/null +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/products/ProductsScreenRoute.kt @@ -0,0 +1,16 @@ +package com.hoc081098.solivagant.sample.products + +import androidx.compose.runtime.Immutable +import com.hoc081098.kmp.viewmodel.parcelable.Parcelize +import com.hoc081098.solivagant.navigation.NavDestination +import com.hoc081098.solivagant.navigation.NavRoute +import com.hoc081098.solivagant.navigation.ScreenDestination +import kotlin.jvm.JvmField + +@Immutable +@Parcelize +data object ProductsScreenRoute : NavRoute + +@JvmField +val ProductsScreenDestination: NavDestination = + ScreenDestination { ProductsScreen() } diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/products/ProductsState.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/products/ProductsState.kt new file mode 100644 index 00000000..8f34d08e --- /dev/null +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/products/ProductsState.kt @@ -0,0 +1,25 @@ +package com.hoc081098.solivagant.sample.products + +import androidx.compose.runtime.Immutable +import com.hoc081098.solivagant.sample.common.ProductItemUi +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Immutable +data class ProductsState( + val products: ImmutableList, + val isLoading: Boolean, + val error: Throwable?, + val isRefreshing: Boolean, +) { + val hasContent: Boolean get() = products.isNotEmpty() && error == null + + companion object { + val INITIAL = ProductsState( + products = persistentListOf(), + isLoading = true, + error = null, + isRefreshing = false, + ) + } +} diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/products/ProductsViewModel.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/products/ProductsViewModel.kt similarity index 73% rename from sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/products/ProductsViewModel.kt rename to sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/products/ProductsViewModel.kt index 4bc4aae9..abefa527 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/products/ProductsViewModel.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/products/ProductsViewModel.kt @@ -1,31 +1,24 @@ -@file:OptIn(ExperimentalCoroutinesApi::class) - -package com.hoc081098.kmpviewmodelsample.products +package com.hoc081098.solivagant.sample.products import com.hoc081098.flowext.flatMapFirst import com.hoc081098.flowext.flowFromSuspend -import com.hoc081098.flowext.interval +import com.hoc081098.flowext.ignoreElements import com.hoc081098.flowext.startWith import com.hoc081098.kmp.viewmodel.Closeable import com.hoc081098.kmp.viewmodel.ViewModel import com.hoc081098.kmp.viewmodel.wrapper.NonNullFlowWrapper import com.hoc081098.kmp.viewmodel.wrapper.NonNullStateFlowWrapper import com.hoc081098.kmp.viewmodel.wrapper.wrap -import com.hoc081098.kmpviewmodelsample.ProductItemUi -import com.hoc081098.kmpviewmodelsample.common.Immutable -import com.hoc081098.kmpviewmodelsample.common.SingleEventChannel -import com.hoc081098.kmpviewmodelsample.toProductItemUi +import com.hoc081098.solivagant.navigation.NavEventNavigator +import com.hoc081098.solivagant.sample.common.SingleEventChannel +import com.hoc081098.solivagant.sample.common.toProductItemUi +import com.hoc081098.solivagant.sample.product_detail.ProductDetailScreenRoute import io.github.aakira.napier.Napier -import kotlin.time.Duration -import kotlin.time.Duration.Companion.seconds -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.flatMapLatest @@ -35,25 +28,6 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.scan import kotlinx.coroutines.flow.stateIn -@Immutable -data class ProductsState( - val products: ImmutableList, - val isLoading: Boolean, - val error: Throwable?, - val isRefreshing: Boolean, -) { - val hasContent: Boolean get() = products.isNotEmpty() && error == null - - companion object { - val INITIAL = ProductsState( - products = persistentListOf(), - isLoading = true, - error = null, - isRefreshing = false, - ) - } -} - sealed interface ProductSingleEvent { sealed interface Refresh : ProductSingleEvent { data object Success : Refresh @@ -64,15 +38,18 @@ sealed interface ProductSingleEvent { sealed interface ProductsAction { data object Load : ProductsAction data object Refresh : ProductsAction + data class NavigateToProductDetail(val id: Int) : ProductsAction } private fun interface Reducer { operator fun invoke(state: ProductsState): ProductsState } +@OptIn(ExperimentalCoroutinesApi::class) class ProductsViewModel( private val getProducts: GetProducts, private val singleEventChannel: SingleEventChannel, + private val navigator: NavEventNavigator, ) : ViewModel( Closeable { Napier.d("[DEMO] Closable 1 ...") }, Closeable { Napier.d("[DEMO] Closable 2 ...") }, @@ -93,12 +70,15 @@ class ProductsViewModel( _actionFlow .filterIsInstance() .refreshFlow(), + _actionFlow + .filterIsInstance() + .navigateToProductDetailFlow(), ) .scan(ProductsState.INITIAL) { state, reducer -> reducer(state) } .onEach { Napier.d( "State: products=${it.products.size}, isLoading=${it.isLoading}," + - " error=${it.error}, isRefreshing=${it.isRefreshing}", + " error=${it.error}, isRefreshing=${it.isRefreshing}", ) } .stateIn( @@ -166,27 +146,13 @@ class ProductsViewModel( } } } + + private fun Flow.navigateToProductDetailFlow(): Flow = + onEach { navigator.navigateTo(ProductDetailScreenRoute(id = it.id)) } + .ignoreElements() //endregion fun dispatch(action: ProductsAction) { _actionFlow.tryEmit(action) } - - /** - * Demo purpose only - */ - @Suppress("unused") // Called from platform code - val tickerFlow: StateFlow = interval(initialDelay = Duration.ZERO, period = 1.seconds) - .map { - if (it % 2 == 0L) { - it.toString() - } else { - null - } - } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(@Suppress("MagicNumber") 5_000), - initialValue = null, - ) } diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProductScreenRoute.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProductScreenRoute.kt new file mode 100644 index 00000000..39e9e8ba --- /dev/null +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProductScreenRoute.kt @@ -0,0 +1,16 @@ +package com.hoc081098.solivagant.sample.search_products + +import androidx.compose.runtime.Immutable +import com.hoc081098.kmp.viewmodel.parcelable.Parcelize +import com.hoc081098.solivagant.navigation.NavDestination +import com.hoc081098.solivagant.navigation.NavRoute +import com.hoc081098.solivagant.navigation.ScreenDestination +import kotlin.jvm.JvmField + +@Immutable +@Parcelize +data object SearchProductScreenRoute : NavRoute + +@JvmField +val SearchProductScreenDestination: NavDestination = + ScreenDestination { SearchProductsScreen() } diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/search_products/SearchProducts.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProducts.kt similarity index 75% rename from sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/search_products/SearchProducts.kt rename to sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProducts.kt index 81fa78a7..708d718e 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/search_products/SearchProducts.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProducts.kt @@ -1,10 +1,10 @@ @file:Suppress("PackageNaming") -package com.hoc081098.kmpviewmodelsample.search_products +package com.hoc081098.solivagant.sample.search_products -import com.hoc081098.kmpviewmodelsample.common.AppDispatchers -import com.hoc081098.kmpviewmodelsample.data.FakeProductsJson -import com.hoc081098.kmpviewmodelsample.data.ProductItem +import com.hoc081098.solivagant.sample.common.AppDispatchers +import com.hoc081098.solivagant.sample.data.FakeProductsJson +import com.hoc081098.solivagant.sample.data.ProductItem import kotlinx.coroutines.delay import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock diff --git a/sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/search_products/SearchProductsScreen.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProductsScreen.kt similarity index 65% rename from sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/search_products/SearchProductsScreen.kt rename to sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProductsScreen.kt index 77f9eb61..abe0ce34 100644 --- a/sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/search_products/SearchProductsScreen.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProductsScreen.kt @@ -4,7 +4,7 @@ ) @file:Suppress("PackageNaming") -package com.hoc081098.kmpviewmodelsample.android.search_products +package com.hoc081098.solivagant.sample.search_products import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -21,30 +21,40 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.hoc081098.kmpviewmodelsample.ProductItemUi -import com.hoc081098.kmpviewmodelsample.android.common.EmptyProducts -import com.hoc081098.kmpviewmodelsample.android.common.ErrorMessageAndRetryButton -import com.hoc081098.kmpviewmodelsample.android.common.LoadingIndicator -import com.hoc081098.kmpviewmodelsample.android.common.OnLifecycleEventWithBuilder -import com.hoc081098.kmpviewmodelsample.android.common.ProductItemsList -import com.hoc081098.kmpviewmodelsample.common.AppDispatchers -import com.hoc081098.kmpviewmodelsample.search_products.SearchProductsState -import com.hoc081098.kmpviewmodelsample.search_products.SearchProductsViewModel -import io.github.aakira.napier.Napier -import org.koin.androidx.compose.koinViewModel +import com.hoc081098.kmp.viewmodel.compose.kmpViewModel +import com.hoc081098.kmp.viewmodel.createSavedStateHandle +import com.hoc081098.kmp.viewmodel.viewModelFactory +import com.hoc081098.solivagant.navigation.NavEventNavigator +import com.hoc081098.solivagant.sample.common.AppDispatchers +import com.hoc081098.solivagant.sample.common.EmptyProducts +import com.hoc081098.solivagant.sample.common.ErrorMessageAndRetryButton +import com.hoc081098.solivagant.sample.common.LoadingIndicator +import com.hoc081098.solivagant.sample.common.ProductItemUi +import com.hoc081098.solivagant.sample.common.ProductItemsList +import com.hoc081098.solivagant.sample.common.collectAsStateWithLifecycle +import org.koin.compose.koinInject import org.koin.compose.rememberKoinInject @Suppress("ReturnCount") @Composable fun SearchProductsScreen( - navigateToProductDetail: (Int) -> Unit, modifier: Modifier = Modifier, - viewModel: SearchProductsViewModel = koinViewModel(), + searchProducts: SearchProducts = koinInject(), + navigator: NavEventNavigator = koinInject(), + viewModel: SearchProductsViewModel = kmpViewModel( + factory = viewModelFactory { + SearchProductsViewModel( + searchProducts = searchProducts, + savedStateHandle = createSavedStateHandle(), + navigator = navigator, + ) + }, + ), ) { - OnLifecycleEventWithBuilder { - onEach { owner, event -> Napier.d("[SearchProductsScreen] event=$event, owner=$owner") } - } + // TODO: OnLifecycleEventWithBuilder + // OnLifecycleEventWithBuilder { + // onEach { owner, event -> Napier.d("[SearchProductsScreen] event=$event, owner=$owner") } + // } val state by viewModel.stateFlow.collectAsStateWithLifecycle() val searchTerm by viewModel.searchTermStateFlow.collectAsStateWithLifecycle( @@ -80,7 +90,7 @@ fun SearchProductsScreen( ListContent( modifier = Modifier.fillMaxSize(), state = state, - onItemClick = { navigateToProductDetail(it.id) }, + onItemClick = { viewModel.navigateToProductDetail(it.id) }, ) } } diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProductsState.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProductsState.kt new file mode 100644 index 00000000..ed6405fc --- /dev/null +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProductsState.kt @@ -0,0 +1,23 @@ +package com.hoc081098.solivagant.sample.search_products + +import androidx.compose.runtime.Immutable +import com.hoc081098.solivagant.sample.common.ProductItemUi +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Immutable +data class SearchProductsState( + val products: ImmutableList, + val isLoading: Boolean, + val error: Throwable?, + val submittedTerm: String?, +) { + companion object { + val INITIAL = SearchProductsState( + products = persistentListOf(), + isLoading = false, + error = null, + submittedTerm = null, + ) + } +} diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/search_products/SearchProductsViewModel.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProductsViewModel.kt similarity index 83% rename from sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/search_products/SearchProductsViewModel.kt rename to sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProductsViewModel.kt index a0aea95c..21296d55 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/kmpviewmodelsample/search_products/SearchProductsViewModel.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/search_products/SearchProductsViewModel.kt @@ -1,6 +1,6 @@ @file:Suppress("PackageNaming") -package com.hoc081098.kmpviewmodelsample.search_products +package com.hoc081098.solivagant.sample.search_products import com.hoc081098.flowext.flowFromSuspend import com.hoc081098.flowext.startWith @@ -12,12 +12,11 @@ import com.hoc081098.kmp.viewmodel.safe.string import com.hoc081098.kmp.viewmodel.wrapper.NonNullStateFlowWrapper import com.hoc081098.kmp.viewmodel.wrapper.NullableStateFlowWrapper import com.hoc081098.kmp.viewmodel.wrapper.wrap -import com.hoc081098.kmpviewmodelsample.ProductItemUi -import com.hoc081098.kmpviewmodelsample.common.Immutable -import com.hoc081098.kmpviewmodelsample.toProductItemUi +import com.hoc081098.solivagant.navigation.NavEventNavigator +import com.hoc081098.solivagant.sample.common.toProductItemUi +import com.hoc081098.solivagant.sample.product_detail.ProductDetailScreenRoute import io.github.aakira.napier.Napier import kotlin.time.Duration.Companion.milliseconds -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -32,27 +31,11 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn -@Immutable -data class SearchProductsState( - val products: ImmutableList, - val isLoading: Boolean, - val error: Throwable?, - val submittedTerm: String?, -) { - companion object { - val INITIAL = SearchProductsState( - products = persistentListOf(), - isLoading = false, - error = null, - submittedTerm = null, - ) - } -} - @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) class SearchProductsViewModel( private val searchProducts: SearchProducts, private val savedStateHandle: SavedStateHandle, + private val navigator: NavEventNavigator, ) : ViewModel() { val searchTermStateFlow: NullableStateFlowWrapper = savedStateHandle.safe { it.getStateFlow(SEARCH_TERM_KEY) }.wrap() @@ -73,6 +56,10 @@ class SearchProductsViewModel( savedStateHandle.safe { it[SEARCH_TERM_KEY] = term } } + fun navigateToProductDetail(id: Int) { + navigator.navigateTo(ProductDetailScreenRoute(id)) + } + companion object { private val SEARCH_TERM_KEY = NullableSavedStateHandleKey.string("com.hoc081098.kmpviewmodelsample.search_term") } diff --git a/sample/shared/src/desktopMain/kotlin/com/hoc081098/solivagant/sample/common/DesktopAppDispatchers.kt b/sample/shared/src/desktopMain/kotlin/com/hoc081098/solivagant/sample/common/DesktopAppDispatchers.kt new file mode 100644 index 00000000..4c350a48 --- /dev/null +++ b/sample/shared/src/desktopMain/kotlin/com/hoc081098/solivagant/sample/common/DesktopAppDispatchers.kt @@ -0,0 +1,10 @@ +package com.hoc081098.solivagant.sample.common + +import kotlinx.coroutines.Dispatchers + +internal class DesktopAppDispatchers : AppDispatchers { + override val main get() = Dispatchers.Main + override val immediateMain get() = Dispatchers.Main.immediate + override val io get() = Dispatchers.IO + override val default get() = Dispatchers.Default +} diff --git a/sample/shared/src/desktopMain/kotlin/com/hoc081098/solivagant/sample/common/WeakReference.desktop.kt b/sample/shared/src/desktopMain/kotlin/com/hoc081098/solivagant/sample/common/WeakReference.desktop.kt new file mode 100644 index 00000000..182d38a4 --- /dev/null +++ b/sample/shared/src/desktopMain/kotlin/com/hoc081098/solivagant/sample/common/WeakReference.desktop.kt @@ -0,0 +1,4 @@ +package com.hoc081098.solivagant.sample.common + +@Suppress("ACTUAL_WITHOUT_EXPECT") // TODO: https://youtrack.jetbrains.com/issue/KT-37316 +internal actual typealias WeakReference = java.lang.ref.WeakReference diff --git a/sample/shared/src/desktopMain/kotlin/com/hoc081098/solivagant/sample/common/identityHashCode.desktop.kt b/sample/shared/src/desktopMain/kotlin/com/hoc081098/solivagant/sample/common/identityHashCode.desktop.kt new file mode 100644 index 00000000..539d0ce2 --- /dev/null +++ b/sample/shared/src/desktopMain/kotlin/com/hoc081098/solivagant/sample/common/identityHashCode.desktop.kt @@ -0,0 +1,3 @@ +package com.hoc081098.solivagant.sample.common + +internal actual fun Any?.identityHashCode(): Int = System.identityHashCode(this) diff --git a/sample/shared/src/desktopMain/kotlin/com/hoc081098/solivagant/sample/di.desktop.kt b/sample/shared/src/desktopMain/kotlin/com/hoc081098/solivagant/sample/di.desktop.kt new file mode 100644 index 00000000..83523469 --- /dev/null +++ b/sample/shared/src/desktopMain/kotlin/com/hoc081098/solivagant/sample/di.desktop.kt @@ -0,0 +1,36 @@ +package com.hoc081098.solivagant.sample + +import com.hoc081098.kmp.viewmodel.SavedStateHandle +import com.hoc081098.solivagant.sample.common.AppDispatchers +import com.hoc081098.solivagant.sample.common.DesktopAppDispatchers +import com.hoc081098.solivagant.sample.product_detail.ProductDetailViewModel +import com.hoc081098.solivagant.sample.products.ProductsViewModel +import com.hoc081098.solivagant.sample.search_products.SearchProductsViewModel +import io.github.aakira.napier.DebugAntilog +import io.github.aakira.napier.Napier +import org.koin.core.module.Module +import org.koin.core.module.dsl.bind +import org.koin.core.module.dsl.factoryOf +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module + +actual fun isDebug(): Boolean = true + +internal actual val PlatformModule: Module = module { + factoryOf(::ProductsViewModel) + factoryOf(::SearchProductsViewModel) + factory { params -> + ProductDetailViewModel.create( + id = params.get(), + getProductById = get(), + ) + } + factory { SavedStateHandle() } + singleOf(::DesktopAppDispatchers) { bind() } +} + +actual fun setupNapier() { + if (isDebug()) { + Napier.base(DebugAntilog()) + } +} diff --git a/sample/shared/src/iosMain/kotlin/com/hoc081098/kmpviewmodelsample/common/Immutable.kt b/sample/shared/src/iosMain/kotlin/com/hoc081098/kmpviewmodelsample/common/Immutable.kt deleted file mode 100644 index 621fb1b0..00000000 --- a/sample/shared/src/iosMain/kotlin/com/hoc081098/kmpviewmodelsample/common/Immutable.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.hoc081098.kmpviewmodelsample.common - -@MustBeDocumented -@Target(AnnotationTarget.CLASS) -@Retention(AnnotationRetention.BINARY) -internal actual annotation class Immutable actual constructor() diff --git a/sample/shared/src/iosMain/kotlin/com/hoc081098/kmpviewmodelsample/DIContainer.kt b/sample/shared/src/iosMain/kotlin/com/hoc081098/solivagant/sample/DIContainer.kt similarity index 97% rename from sample/shared/src/iosMain/kotlin/com/hoc081098/kmpviewmodelsample/DIContainer.kt rename to sample/shared/src/iosMain/kotlin/com/hoc081098/solivagant/sample/DIContainer.kt index 1411901c..e052debe 100644 --- a/sample/shared/src/iosMain/kotlin/com/hoc081098/kmpviewmodelsample/DIContainer.kt +++ b/sample/shared/src/iosMain/kotlin/com/hoc081098/solivagant/sample/DIContainer.kt @@ -1,7 +1,7 @@ @file:Suppress("unused") // Called from platform code @file:OptIn(ExperimentalNativeApi::class, BetaInteropApi::class) -package com.hoc081098.kmpviewmodelsample +package com.hoc081098.solivagant.sample import kotlin.experimental.ExperimentalNativeApi import kotlinx.cinterop.BetaInteropApi diff --git a/sample/shared/src/iosMain/kotlin/com/hoc081098/kmpviewmodelsample/ImmediateMainScope.kt b/sample/shared/src/iosMain/kotlin/com/hoc081098/solivagant/sample/ImmediateMainScope.kt similarity index 85% rename from sample/shared/src/iosMain/kotlin/com/hoc081098/kmpviewmodelsample/ImmediateMainScope.kt rename to sample/shared/src/iosMain/kotlin/com/hoc081098/solivagant/sample/ImmediateMainScope.kt index 10dc4144..c9f8bb9f 100644 --- a/sample/shared/src/iosMain/kotlin/com/hoc081098/kmpviewmodelsample/ImmediateMainScope.kt +++ b/sample/shared/src/iosMain/kotlin/com/hoc081098/solivagant/sample/ImmediateMainScope.kt @@ -1,4 +1,4 @@ -package com.hoc081098.kmpviewmodelsample +package com.hoc081098.solivagant.sample import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers diff --git a/sample/shared/src/iosMain/kotlin/com/hoc081098/kmpviewmodelsample/NSError.kt b/sample/shared/src/iosMain/kotlin/com/hoc081098/solivagant/sample/NSError.kt similarity index 98% rename from sample/shared/src/iosMain/kotlin/com/hoc081098/kmpviewmodelsample/NSError.kt rename to sample/shared/src/iosMain/kotlin/com/hoc081098/solivagant/sample/NSError.kt index 3836dc2d..30b6af05 100644 --- a/sample/shared/src/iosMain/kotlin/com/hoc081098/kmpviewmodelsample/NSError.kt +++ b/sample/shared/src/iosMain/kotlin/com/hoc081098/solivagant/sample/NSError.kt @@ -1,7 +1,7 @@ @file:Suppress("unused") // Called from platform code @file:OptIn(ExperimentalForeignApi::class) -package com.hoc081098.kmpviewmodelsample +package com.hoc081098.solivagant.sample import io.github.aakira.napier.Napier import kotlin.native.internal.ObjCErrorException diff --git a/sample/shared/src/iosMain/kotlin/com/hoc081098/kmpviewmodelsample/common/IosAppDispatchers.kt b/sample/shared/src/iosMain/kotlin/com/hoc081098/solivagant/sample/common/IosAppDispatchers.kt similarity index 87% rename from sample/shared/src/iosMain/kotlin/com/hoc081098/kmpviewmodelsample/common/IosAppDispatchers.kt rename to sample/shared/src/iosMain/kotlin/com/hoc081098/solivagant/sample/common/IosAppDispatchers.kt index 1d2a8ff1..9c52007e 100644 --- a/sample/shared/src/iosMain/kotlin/com/hoc081098/kmpviewmodelsample/common/IosAppDispatchers.kt +++ b/sample/shared/src/iosMain/kotlin/com/hoc081098/solivagant/sample/common/IosAppDispatchers.kt @@ -1,4 +1,4 @@ -package com.hoc081098.kmpviewmodelsample.common +package com.hoc081098.solivagant.sample.common import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO diff --git a/sample/shared/src/iosMain/kotlin/com/hoc081098/kmpviewmodelsample/common/WeakReference.ios.kt b/sample/shared/src/iosMain/kotlin/com/hoc081098/solivagant/sample/common/WeakReference.ios.kt similarity index 84% rename from sample/shared/src/iosMain/kotlin/com/hoc081098/kmpviewmodelsample/common/WeakReference.ios.kt rename to sample/shared/src/iosMain/kotlin/com/hoc081098/solivagant/sample/common/WeakReference.ios.kt index c3ede4c6..bcff4c7d 100644 --- a/sample/shared/src/iosMain/kotlin/com/hoc081098/kmpviewmodelsample/common/WeakReference.ios.kt +++ b/sample/shared/src/iosMain/kotlin/com/hoc081098/solivagant/sample/common/WeakReference.ios.kt @@ -1,4 +1,4 @@ -package com.hoc081098.kmpviewmodelsample.common +package com.hoc081098.solivagant.sample.common import kotlin.experimental.ExperimentalNativeApi diff --git a/sample/shared/src/iosMain/kotlin/com/hoc081098/kmpviewmodelsample/common/identityHashCode.ios.kt b/sample/shared/src/iosMain/kotlin/com/hoc081098/solivagant/sample/common/identityHashCode.ios.kt similarity index 82% rename from sample/shared/src/iosMain/kotlin/com/hoc081098/kmpviewmodelsample/common/identityHashCode.ios.kt rename to sample/shared/src/iosMain/kotlin/com/hoc081098/solivagant/sample/common/identityHashCode.ios.kt index eb2b3ee5..0ff4134f 100644 --- a/sample/shared/src/iosMain/kotlin/com/hoc081098/kmpviewmodelsample/common/identityHashCode.ios.kt +++ b/sample/shared/src/iosMain/kotlin/com/hoc081098/solivagant/sample/common/identityHashCode.ios.kt @@ -1,4 +1,4 @@ -package com.hoc081098.kmpviewmodelsample.common +package com.hoc081098.solivagant.sample.common import kotlin.experimental.ExperimentalNativeApi import kotlin.native.identityHashCode as nativeIdentityHashCode diff --git a/sample/shared/src/iosMain/kotlin/com/hoc081098/kmpviewmodelsample/di.ios.kt b/sample/shared/src/iosMain/kotlin/com/hoc081098/solivagant/sample/di.ios.kt similarity index 69% rename from sample/shared/src/iosMain/kotlin/com/hoc081098/kmpviewmodelsample/di.ios.kt rename to sample/shared/src/iosMain/kotlin/com/hoc081098/solivagant/sample/di.ios.kt index 8a937545..938901cf 100644 --- a/sample/shared/src/iosMain/kotlin/com/hoc081098/kmpviewmodelsample/di.ios.kt +++ b/sample/shared/src/iosMain/kotlin/com/hoc081098/solivagant/sample/di.ios.kt @@ -1,11 +1,11 @@ -package com.hoc081098.kmpviewmodelsample +package com.hoc081098.solivagant.sample import com.hoc081098.kmp.viewmodel.SavedStateHandle -import com.hoc081098.kmpviewmodelsample.common.AppDispatchers -import com.hoc081098.kmpviewmodelsample.common.IosAppDispatchers -import com.hoc081098.kmpviewmodelsample.product_detail.ProductDetailViewModel -import com.hoc081098.kmpviewmodelsample.products.ProductsViewModel -import com.hoc081098.kmpviewmodelsample.search_products.SearchProductsViewModel +import com.hoc081098.solivagant.sample.common.AppDispatchers +import com.hoc081098.solivagant.sample.common.IosAppDispatchers +import com.hoc081098.solivagant.sample.product_detail.ProductDetailViewModel +import com.hoc081098.solivagant.sample.products.ProductsViewModel +import com.hoc081098.solivagant.sample.search_products.SearchProductsViewModel import io.github.aakira.napier.DebugAntilog import io.github.aakira.napier.Napier import kotlin.experimental.ExperimentalNativeApi diff --git a/settings.gradle.kts b/settings.gradle.kts index e1b6c698..86631aa5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,6 +16,7 @@ dependencyResolutionManagement { mavenCentral() maven(url = "https://maven.pkg.jetbrains.space/public/p/compose/dev") gradlePluginPortal() + maven(url = "https://s01.oss.sonatype.org/content/repositories/snapshots/") } } @@ -23,6 +24,7 @@ rootProject.name = "solivagant" include(":navigation-core") include(":khonshu-navigation-core") include(":navigation") +include(":sample:app", ":sample:shared") plugins { id("org.gradle.toolchains.foojay-resolver-convention") version("0.7.0") From b70034b6f545d17e8cef82e7b97d6d0af3f3d118 Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Fri, 5 Jan 2024 20:22:52 +0700 Subject: [PATCH 2/9] sample --- sample/app/build.gradle.kts | 71 +++++----- .../android/common/OnLifecycleEvent.kt | 121 ------------------ 2 files changed, 34 insertions(+), 158 deletions(-) delete mode 100644 sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/common/OnLifecycleEvent.kt diff --git a/sample/app/build.gradle.kts b/sample/app/build.gradle.kts index 162126fd..2791896f 100644 --- a/sample/app/build.gradle.kts +++ b/sample/app/build.gradle.kts @@ -15,10 +15,7 @@ android { versionName = "1.0" } buildFeatures { - compose = true - } - composeOptions { - kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() + buildConfig = true } packaging { resources { @@ -41,41 +38,41 @@ android { dependencies { implementation(project(":sample:shared")) - implementation(platform(libs.androidx.compose.bom)) - - implementation(libs.androidx.lifecycle.runtime.compose) - - implementation(libs.androidx.compose.ui.ui) - debugImplementation(libs.androidx.compose.ui.tooling) - implementation(libs.androidx.compose.ui.tooling.preview) - implementation(libs.androidx.compose.foundation) - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.material) - implementation(libs.androidx.compose.runtime) +// implementation(platform(libs.androidx.compose.bom)) +// +// implementation(libs.androidx.lifecycle.runtime.compose) +// +// implementation(libs.androidx.compose.ui.ui) +// debugImplementation(libs.androidx.compose.ui.tooling) +// implementation(libs.androidx.compose.ui.tooling.preview) +// implementation(libs.androidx.compose.foundation) +// implementation(libs.androidx.compose.material3) +// implementation(libs.androidx.compose.material) +// implementation(libs.androidx.compose.runtime) implementation(libs.androidx.activity.compose) - implementation(libs.androidx.navigation.compose) +// implementation(libs.androidx.navigation.compose) - implementation(libs.koin.androidx.compose) - implementation(libs.coil.compose) +// implementation(libs.koin.androidx.compose) +// implementation(libs.coil.compose) - implementation(libs.kotlinx.collections.immutable) +// implementation(libs.kotlinx.collections.immutable) } -tasks.withType { - kotlinOptions { - val buildDirAbsolutePath = project.layout.buildDirectory.map { it.asFile.absolutePath }.get() - - if (project.findProperty("composeCompilerReports") == "true") { - freeCompilerArgs = freeCompilerArgs + listOf( - "-P", - "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=$buildDirAbsolutePath/compose_compiler", - ) - } - if (project.findProperty("composeCompilerMetrics") == "true") { - freeCompilerArgs = freeCompilerArgs + listOf( - "-P", - "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=$buildDirAbsolutePath/compose_compiler", - ) - } - } -} +//tasks.withType { +// kotlinOptions { +// val buildDirAbsolutePath = project.layout.buildDirectory.map { it.asFile.absolutePath }.get() +// +// if (project.findProperty("composeCompilerReports") == "true") { +// freeCompilerArgs = freeCompilerArgs + listOf( +// "-P", +// "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=$buildDirAbsolutePath/compose_compiler", +// ) +// } +// if (project.findProperty("composeCompilerMetrics") == "true") { +// freeCompilerArgs = freeCompilerArgs + listOf( +// "-P", +// "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=$buildDirAbsolutePath/compose_compiler", +// ) +// } +// } +//} diff --git a/sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/common/OnLifecycleEvent.kt b/sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/common/OnLifecycleEvent.kt deleted file mode 100644 index e196b9f7..00000000 --- a/sample/app/src/main/java/com/hoc081098/kmpviewmodelsample/android/common/OnLifecycleEvent.kt +++ /dev/null @@ -1,121 +0,0 @@ -package com.hoc081098.kmpviewmodelsample.android.common - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.runtime.setValue -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver -import androidx.lifecycle.LifecycleOwner - -@Suppress("unused") -@Composable -fun OnLifecycleEvent( - vararg keys: Any?, - lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, - onEvent: (owner: LifecycleOwner, event: Lifecycle.Event) -> Unit, -) { - val eventHandler by rememberUpdatedState(onEvent) - - DisposableEffect(*keys, lifecycleOwner) { - val observer = LifecycleEventObserver { owner, event -> - eventHandler(owner, event) - } - lifecycleOwner.lifecycle.addObserver(observer) - - onDispose { - lifecycleOwner.lifecycle.removeObserver(observer) - } - } -} - -typealias LifecycleEventListener = (owner: LifecycleOwner) -> Unit -typealias LifecycleEachEventListener = (owner: LifecycleOwner, event: Lifecycle.Event) -> Unit - -@DslMarker -annotation class LifecycleEventBuilderMarker - -@Stable -@LifecycleEventBuilderMarker -class LifecycleEventBuilder { - private var onCreate: LifecycleEventListener? by mutableStateOf(null) - private var onStart: LifecycleEventListener? by mutableStateOf(null) - private var onResume: LifecycleEventListener? by mutableStateOf(null) - private var onPause: LifecycleEventListener? by mutableStateOf(null) - private var onStop: LifecycleEventListener? by mutableStateOf(null) - private var onDestroy: LifecycleEventListener? by mutableStateOf(null) - private var onEach: LifecycleEachEventListener? by mutableStateOf(null) - - @LifecycleEventBuilderMarker - fun onCreate(block: LifecycleEventListener) { - onCreate = block - } - - @LifecycleEventBuilderMarker - fun onStart(block: LifecycleEventListener) { - onStart = block - } - - @LifecycleEventBuilderMarker - fun onResume(block: LifecycleEventListener) { - onResume = block - } - - @LifecycleEventBuilderMarker - fun onPause(block: LifecycleEventListener) { - onPause = block - } - - @LifecycleEventBuilderMarker - fun onStop(block: LifecycleEventListener) { - onStop = block - } - - @LifecycleEventBuilderMarker - fun onDestroy(block: LifecycleEventListener) { - onDestroy = block - } - - @LifecycleEventBuilderMarker - fun onEach(block: LifecycleEachEventListener) { - onEach = block - } - - internal fun buildLifecycleEventObserver() = LifecycleEventObserver { owner, event -> - when (event) { - Lifecycle.Event.ON_CREATE -> onCreate - Lifecycle.Event.ON_START -> onStart - Lifecycle.Event.ON_RESUME -> onResume - Lifecycle.Event.ON_PAUSE -> onPause - Lifecycle.Event.ON_STOP -> onStop - Lifecycle.Event.ON_DESTROY -> onDestroy - Lifecycle.Event.ON_ANY -> null - }?.invoke(owner) - - onEach?.invoke(owner, event) - } -} - -@Composable -fun OnLifecycleEventWithBuilder( - vararg keys: Any?, - lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, - builder: LifecycleEventBuilder.() -> Unit, -) { - val lifecycleEventBuilder = remember { LifecycleEventBuilder() } - val observer = remember { lifecycleEventBuilder.buildLifecycleEventObserver() } - - // When builder or lifecycleOwner or keys changes, we need to re-execute the effect - DisposableEffect(builder, lifecycleOwner, *keys) { - // This make sure all callbacks are always up to date. - builder(lifecycleEventBuilder) - - lifecycleOwner.lifecycle.addObserver(observer) - onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } - } -} From 6e61a26181ec36305bdebbdec81c58dc7f08a8ae Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Fri, 5 Jan 2024 20:28:15 +0700 Subject: [PATCH 3/9] android done --- sample/app/build.gradle.kts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sample/app/build.gradle.kts b/sample/app/build.gradle.kts index 2791896f..d8f45f45 100644 --- a/sample/app/build.gradle.kts +++ b/sample/app/build.gradle.kts @@ -2,6 +2,11 @@ plugins { alias(libs.plugins.android.app) alias(libs.plugins.kotlin.android) + alias(libs.plugins.jetbrains.compose) +} + +compose { + kotlinCompilerPlugin.set(libs.versions.jetbrains.compose.compiler) } android { From 75be2976f645f940bf4d0723d8b84b34bb49422c Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Fri, 5 Jan 2024 20:30:17 +0700 Subject: [PATCH 4/9] koin NavEventNavigator --- .../commonMain/kotlin/com/hoc081098/solivagant/sample/di.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/di.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/di.kt index fd2eba72..2fe9eb1a 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/di.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/di.kt @@ -1,5 +1,6 @@ package com.hoc081098.solivagant.sample +import com.hoc081098.solivagant.navigation.NavEventNavigator import com.hoc081098.solivagant.sample.common.SingleEventChannel import com.hoc081098.solivagant.sample.product_detail.GetProductById import com.hoc081098.solivagant.sample.products.GetProducts @@ -7,6 +8,7 @@ import com.hoc081098.solivagant.sample.search_products.SearchProducts import org.koin.core.context.startKoin import org.koin.core.module.Module import org.koin.core.module.dsl.factoryOf +import org.koin.core.module.dsl.singleOf import org.koin.dsl.KoinAppDeclaration import org.koin.dsl.module @@ -15,6 +17,7 @@ private val CommonModule = module { factoryOf(::SearchProducts) factoryOf(::GetProductById) factory { SingleEventChannel() } + singleOf(::NavEventNavigator) } internal expect val PlatformModule: Module From c4c85da9cb553ebb2eab8a014a56ef3874cfb30e Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sat, 6 Jan 2024 17:00:02 +0700 Subject: [PATCH 5/9] fix --- .../ViewModelStoreOwnerCloseable.android.kt | 8 +++++++ .../savedStateHandleAndroidSupport.android.kt | 15 ++++++++---- .../solivagant/navigation/NavHost.kt | 24 +++++++++++++++++-- .../internal/MultiStackNavigationExecutor.kt | 2 +- .../navigation/internal/StoreViewModel.kt | 7 ++++-- .../internal/ViewModelStoreOwnerCloseable.kt | 19 +++++++++++++++ sample/app/build.gradle.kts | 4 ++-- .../CollectWithLifecycleEffect.android.kt | 7 +++--- .../solivagant/sample/SolivagantSampleApp.kt | 5 ++-- .../solivagant/sample/StartScreen.kt | 4 ++-- .../common/CollectWithLifecycleEffect.kt | 10 +------- .../debugCheckImmediateMainDispatcher.kt | 2 +- .../sample/products/ProductsScreen.kt | 3 ++- .../sample/products/ProductsViewModel.kt | 2 +- 14 files changed, 80 insertions(+), 32 deletions(-) create mode 100644 navigation/src/androidMain/kotlin/com/hoc081098/solivagant/navigation/internal/ViewModelStoreOwnerCloseable.android.kt create mode 100644 navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/ViewModelStoreOwnerCloseable.kt diff --git a/navigation/src/androidMain/kotlin/com/hoc081098/solivagant/navigation/internal/ViewModelStoreOwnerCloseable.android.kt b/navigation/src/androidMain/kotlin/com/hoc081098/solivagant/navigation/internal/ViewModelStoreOwnerCloseable.android.kt new file mode 100644 index 00000000..2817cb45 --- /dev/null +++ b/navigation/src/androidMain/kotlin/com/hoc081098/solivagant/navigation/internal/ViewModelStoreOwnerCloseable.android.kt @@ -0,0 +1,8 @@ +package com.hoc081098.solivagant.navigation.internal + +import com.hoc081098.kmp.viewmodel.InternalKmpViewModelApi +import com.hoc081098.kmp.viewmodel.ViewModelStore + +@OptIn(InternalKmpViewModelApi::class) +internal actual fun createViewModelStore(): ViewModelStore = + ViewModelStore(androidx.lifecycle.ViewModelStore()) diff --git a/navigation/src/androidMain/kotlin/com/hoc081098/solivagant/navigation/internal/savedStateHandleAndroidSupport.android.kt b/navigation/src/androidMain/kotlin/com/hoc081098/solivagant/navigation/internal/savedStateHandleAndroidSupport.android.kt index 60dd32b5..71e081a1 100644 --- a/navigation/src/androidMain/kotlin/com/hoc081098/solivagant/navigation/internal/savedStateHandleAndroidSupport.android.kt +++ b/navigation/src/androidMain/kotlin/com/hoc081098/solivagant/navigation/internal/savedStateHandleAndroidSupport.android.kt @@ -58,12 +58,19 @@ internal actual fun SavedStateHandle.getAsMap(key: String): Map? = ?.let { bundle -> bundle .keySet() - .associateWith { - @Suppress("DEPRECATION") - bundle.get(it) - } + .associateWith { bundle.safeGet(it) } } +private fun Bundle.toMap(): Map = keySet().associateWith { safeGet(it) } + +private fun Bundle.safeGet(key: String): Any? { + @Suppress("DEPRECATION") + return when (val v = get(key)) { + is Bundle -> return v.toMap() + else -> v + } +} + @SuppressLint("RestrictedApi") internal actual fun createSavedStateHandleAndSetSavedStateProvider( id: String, diff --git a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavHost.kt b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavHost.kt index 135d32e0..e9fc6afa 100644 --- a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavHost.kt +++ b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavHost.kt @@ -11,9 +11,12 @@ import androidx.compose.runtime.saveable.rememberSaveableStateHolder import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import com.hoc081098.kmp.viewmodel.Closeable +import com.hoc081098.kmp.viewmodel.compose.SavedStateHandleFactoryProvider +import com.hoc081098.kmp.viewmodel.compose.ViewModelStoreOwnerProvider import com.hoc081098.solivagant.navigation.internal.MultiStackNavigationExecutor import com.hoc081098.solivagant.navigation.internal.OnBackPressedCallback import com.hoc081098.solivagant.navigation.internal.StackEntry +import com.hoc081098.solivagant.navigation.internal.ViewModelStoreOwnerCloseable import com.hoc081098.solivagant.navigation.internal.WeakReference import com.hoc081098.solivagant.navigation.internal.currentBackPressedDispatcher import com.hoc081098.solivagant.navigation.internal.rememberNavigationExecutor @@ -50,7 +53,11 @@ public fun NavHost( Box(modifier = modifier) { executor.visibleEntries.value.forEach { entry -> - Show(entry, executor, saveableStateHolder) + Show( + entry = entry, + executor = executor, + saveableStateHolder = saveableStateHolder, + ) } } } @@ -74,7 +81,20 @@ private fun Show( } saveableStateHolder.SaveableStateProvider(entry.id.value) { - entry.destination.content(entry.route) + ViewModelStoreOwnerProvider( + viewModelStoreOwner = executor + .storeFor(entry.id) + .getOrCreate( + ViewModelStoreOwnerCloseable::class, + ::ViewModelStoreOwnerCloseable, + ), + ) { + SavedStateHandleFactoryProvider( + savedStateHandleFactory = { executor.savedStateHandleFor(entry.destinationId) }, + ) { + entry.destination.content(entry.route) + } + } } } diff --git a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/MultiStackNavigationExecutor.kt b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/MultiStackNavigationExecutor.kt index 16743082..682c9e2a 100644 --- a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/MultiStackNavigationExecutor.kt +++ b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/MultiStackNavigationExecutor.kt @@ -64,7 +64,7 @@ internal class MultiStackNavigationExecutor( override fun savedStateHandleFor(destinationId: DestinationId): SavedStateHandle { val entry = entryFor(destinationId) - return viewModel.provideSavedStateHandle(entry.id) + return viewModel.provideSavedStateHandle(entry.id, entry.route) } override fun storeFor(destinationId: DestinationId): NavigationExecutor.Store { diff --git a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/StoreViewModel.kt b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/StoreViewModel.kt index 2e63f5bb..88765340 100644 --- a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/StoreViewModel.kt +++ b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/StoreViewModel.kt @@ -2,6 +2,8 @@ package com.hoc081098.solivagant.navigation.internal import com.hoc081098.kmp.viewmodel.SavedStateHandle import com.hoc081098.kmp.viewmodel.ViewModel +import com.hoc081098.solivagant.navigation.BaseRoute +import com.hoc081098.solivagant.navigation.EXTRA_ROUTE import com.hoc081098.solivagant.navigation.NavRoot import com.hoc081098.solivagant.navigation.internal.MultiStackNavigationExecutor.Companion.SAVED_STATE_STACK @@ -17,9 +19,10 @@ internal class StoreViewModel( return stores.getOrPut(id) { NavigationExecutorStore() } } - fun provideSavedStateHandle(id: StackEntry.Id): SavedStateHandle { + fun provideSavedStateHandle(id: StackEntry.Id, route: BaseRoute): SavedStateHandle { return savedStateHandles.getOrPut(id) { createSavedStateHandleAndSetSavedStateProvider(id.value, globalSavedStateHandle) + .apply { this[EXTRA_ROUTE] = route } } } @@ -73,7 +76,7 @@ internal class StoreViewModel( globalSavedStateHandle[SAVED_START_ROOT_KEY] = root } - fun getSavedStackState(): Map? = + internal fun getSavedStackState(): Map? = globalSavedStateHandle.getAsMap(SAVED_STATE_STACK) private companion object { diff --git a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/ViewModelStoreOwnerCloseable.kt b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/ViewModelStoreOwnerCloseable.kt new file mode 100644 index 00000000..a13da4f0 --- /dev/null +++ b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/ViewModelStoreOwnerCloseable.kt @@ -0,0 +1,19 @@ +package com.hoc081098.solivagant.navigation.internal + +import com.hoc081098.kmp.viewmodel.Closeable +import com.hoc081098.kmp.viewmodel.ViewModelStore +import com.hoc081098.kmp.viewmodel.ViewModelStoreOwner + +internal expect fun createViewModelStore(): ViewModelStore + +internal class ViewModelStoreOwnerCloseable : Closeable, ViewModelStoreOwner { + private val viewModelStoreLazy = lazy { createViewModelStore() } + + override fun close() { + if (viewModelStoreLazy.isInitialized()) { + viewModelStoreLazy.value.clear() + } + } + + override val viewModelStore: ViewModelStore by viewModelStoreLazy +} diff --git a/sample/app/build.gradle.kts b/sample/app/build.gradle.kts index d8f45f45..e230af82 100644 --- a/sample/app/build.gradle.kts +++ b/sample/app/build.gradle.kts @@ -63,7 +63,7 @@ dependencies { // implementation(libs.kotlinx.collections.immutable) } -//tasks.withType { +// tasks.withType { // kotlinOptions { // val buildDirAbsolutePath = project.layout.buildDirectory.map { it.asFile.absolutePath }.get() // @@ -80,4 +80,4 @@ dependencies { // ) // } // } -//} +// } diff --git a/sample/shared/src/androidMain/kotlin/com/hoc081098/solivagant/sample/common/CollectWithLifecycleEffect.android.kt b/sample/shared/src/androidMain/kotlin/com/hoc081098/solivagant/sample/common/CollectWithLifecycleEffect.android.kt index 0ca40e86..d11ab430 100644 --- a/sample/shared/src/androidMain/kotlin/com/hoc081098/solivagant/sample/common/CollectWithLifecycleEffect.android.kt +++ b/sample/shared/src/androidMain/kotlin/com/hoc081098/solivagant/sample/common/CollectWithLifecycleEffect.android.kt @@ -1,6 +1,5 @@ package com.hoc081098.solivagant.sample.common -import androidx.lifecycle.compose.collectAsStateWithLifecycle as androidXCollectAsStateWithLifecycle import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.NonRestartableComposable @@ -10,6 +9,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.collectAsStateWithLifecycle as androidXCollectAsStateWithLifecycle import androidx.lifecycle.repeatOnLifecycle import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineDispatcher @@ -25,7 +25,7 @@ import kotlinx.coroutines.launch actual fun Flow.CollectWithLifecycleEffect( vararg keys: Any?, dispatcher: CollectWithLifecycleEffectDispatcher, - collector: (T) -> Unit + collector: (T) -> Unit, ) { val lifecycleOwner = LocalLifecycleOwner.current val minActiveState = Lifecycle.State.STARTED @@ -57,10 +57,9 @@ actual fun Flow.CollectWithLifecycleEffect( @Composable actual fun StateFlow.collectAsStateWithLifecycle( - context: CoroutineContext + context: CoroutineContext, ): State = androidXCollectAsStateWithLifecycle(context = context) - @Composable @NonRestartableComposable @Suppress("ArrayReturn") diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/SolivagantSampleApp.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/SolivagantSampleApp.kt index 6e8f9ef7..0a31de89 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/SolivagantSampleApp.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/SolivagantSampleApp.kt @@ -38,11 +38,10 @@ import com.hoc081098.solivagant.sample.search_products.SearchProductScreenRoute import kotlinx.collections.immutable.persistentSetOf import org.koin.compose.koinInject - @OptIn( ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class, - ExperimentalCoilApi::class + ExperimentalCoilApi::class, ) @Composable fun SolivagantSampleApp( @@ -87,7 +86,7 @@ fun SolivagantSampleApp( navigationIcon = { if (currentRoute !is NavRoot) { IconButton( - onClick = remember { navigator::navigateBack } + onClick = remember { navigator::navigateBack }, ) { Icon( imageVector = Icons.Default.ArrowBack, diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/StartScreen.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/StartScreen.kt index 88721629..38372f09 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/StartScreen.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/StartScreen.kt @@ -40,13 +40,13 @@ internal fun StartScreen( verticalArrangement = Arrangement.Center, ) { ProductsButton( - navigateToProducts = { navigator.navigateTo(ProductsScreenRoute) } + navigateToProducts = { navigator.navigateTo(ProductsScreenRoute) }, ) Spacer(modifier = Modifier.height(16.dp)) SearchProductsButton( - navigateToSearch = { navigator.navigateTo(SearchProductScreenRoute) } + navigateToSearch = { navigator.navigateTo(SearchProductScreenRoute) }, ) } } diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/CollectWithLifecycleEffect.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/CollectWithLifecycleEffect.kt index e5c329e7..4178f69a 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/CollectWithLifecycleEffect.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/CollectWithLifecycleEffect.kt @@ -3,21 +3,13 @@ package com.hoc081098.solivagant.sample.common import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.NonRestartableComposable -import androidx.compose.runtime.RememberObserver import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch @Immutable enum class CollectWithLifecycleEffectDispatcher { @@ -70,5 +62,5 @@ expect fun Flow.CollectWithLifecycleEffect( @Composable expect fun StateFlow.collectAsStateWithLifecycle( - context: CoroutineContext = EmptyCoroutineContext + context: CoroutineContext = EmptyCoroutineContext, ): State diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/debugCheckImmediateMainDispatcher.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/debugCheckImmediateMainDispatcher.kt index 2fb098c6..3ef8f1c9 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/debugCheckImmediateMainDispatcher.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/common/debugCheckImmediateMainDispatcher.kt @@ -14,7 +14,7 @@ internal suspend inline fun debugCheckImmediateMainDispatcher() { check( dispatcher === Dispatchers.Main.immediate || - !dispatcher.isDispatchNeeded(Dispatchers.Main.immediate) + !dispatcher.isDispatchNeeded(Dispatchers.Main.immediate), ) { "Expected CoroutineDispatcher to be Dispatchers.Main.immediate but was $dispatcher" } diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/products/ProductsScreen.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/products/ProductsScreen.kt index 29bc85a8..30d1b8e8 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/products/ProductsScreen.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/products/ProductsScreen.kt @@ -63,7 +63,8 @@ fun ProductsScreen( val toastManager = PlatformToastManager() val eventHandler: (ProductSingleEvent) -> Unit = remember(toastManager, scope, currentLazyListState) { - { event -> + { + event -> when (event) { is ProductSingleEvent.Refresh.Failure -> { toastManager.showToast("Failed to refresh") diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/products/ProductsViewModel.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/products/ProductsViewModel.kt index abefa527..640ab5fe 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/products/ProductsViewModel.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/products/ProductsViewModel.kt @@ -78,7 +78,7 @@ class ProductsViewModel( .onEach { Napier.d( "State: products=${it.products.size}, isLoading=${it.isLoading}," + - " error=${it.error}, isRefreshing=${it.isRefreshing}", + " error=${it.error}, isRefreshing=${it.isRefreshing}", ) } .stateIn( From deba12ac5855947881f8c4e660c0df0e82a14283 Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sat, 6 Jan 2024 17:14:35 +0700 Subject: [PATCH 6/9] fix --- .../internal/savedStateHandleAndroidSupport.android.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/navigation/src/androidMain/kotlin/com/hoc081098/solivagant/navigation/internal/savedStateHandleAndroidSupport.android.kt b/navigation/src/androidMain/kotlin/com/hoc081098/solivagant/navigation/internal/savedStateHandleAndroidSupport.android.kt index 71e081a1..d8b7d6fd 100644 --- a/navigation/src/androidMain/kotlin/com/hoc081098/solivagant/navigation/internal/savedStateHandleAndroidSupport.android.kt +++ b/navigation/src/androidMain/kotlin/com/hoc081098/solivagant/navigation/internal/savedStateHandleAndroidSupport.android.kt @@ -67,6 +67,12 @@ private fun Bundle.safeGet(key: String): Any? { @Suppress("DEPRECATION") return when (val v = get(key)) { is Bundle -> return v.toMap() + is ArrayList<*> -> { + when (v[0]) { + is Bundle -> v.mapTo(ArrayList(v.size)) { (it as Bundle).toMap() } + else -> v + } + } else -> v } } From 2840a9107c095cf314da3538525763217c1ca3ad Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sat, 6 Jan 2024 17:33:56 +0700 Subject: [PATCH 7/9] fix --- .../navigation/internal/NavigationExecutor.kt | 2 + .../solivagant/navigation/NavHost.kt | 43 +++++++++++++------ .../internal/MultiStackNavigationExecutor.kt | 6 +++ .../navigation/internal/StoreViewModel.kt | 16 +++++-- .../internal/ViewModelStoreOwnerCloseable.kt | 8 ++-- sample/app/build.gradle.kts | 36 ---------------- 6 files changed, 56 insertions(+), 55 deletions(-) diff --git a/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/NavigationExecutor.kt b/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/NavigationExecutor.kt index f43485d5..c7bab936 100644 --- a/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/NavigationExecutor.kt +++ b/khonshu-navigation-core/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/NavigationExecutor.kt @@ -1,6 +1,7 @@ package com.hoc081098.solivagant.navigation.internal import com.hoc081098.kmp.viewmodel.SavedStateHandle +import com.hoc081098.kmp.viewmodel.SavedStateHandleFactory import com.hoc081098.solivagant.navigation.BaseRoute import com.hoc081098.solivagant.navigation.Navigator import com.hoc081098.solivagant.navigation.Serializable @@ -10,6 +11,7 @@ import kotlin.reflect.KClass public interface NavigationExecutor : Navigator { public fun routeFor(destinationId: DestinationId): T public fun savedStateHandleFor(destinationId: DestinationId): SavedStateHandle + public fun savedStateHandleFactoryFor(destinationId: DestinationId): SavedStateHandleFactory public fun storeFor(destinationId: DestinationId): Store public fun extra(destinationId: DestinationId): Serializable diff --git a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavHost.kt b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavHost.kt index e9fc6afa..2af2df45 100644 --- a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavHost.kt +++ b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/NavHost.kt @@ -5,10 +5,14 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.SaveableStateHolder import androidx.compose.runtime.saveable.rememberSaveableStateHolder import androidx.compose.runtime.snapshotFlow +import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.ui.Modifier import com.hoc081098.kmp.viewmodel.Closeable import com.hoc081098.kmp.viewmodel.compose.SavedStateHandleFactoryProvider @@ -16,7 +20,7 @@ import com.hoc081098.kmp.viewmodel.compose.ViewModelStoreOwnerProvider import com.hoc081098.solivagant.navigation.internal.MultiStackNavigationExecutor import com.hoc081098.solivagant.navigation.internal.OnBackPressedCallback import com.hoc081098.solivagant.navigation.internal.StackEntry -import com.hoc081098.solivagant.navigation.internal.ViewModelStoreOwnerCloseable +import com.hoc081098.solivagant.navigation.internal.StackEntryViewModelStoreOwner import com.hoc081098.solivagant.navigation.internal.WeakReference import com.hoc081098.solivagant.navigation.internal.currentBackPressedDispatcher import com.hoc081098.solivagant.navigation.internal.rememberNavigationExecutor @@ -74,23 +78,28 @@ private fun Show( // it is available when the destination is cleared. Which, because of animations, // only happens after this leaves composition. Which means we can't rely on // DisposableEffect to clean up this reference (as it'll be cleaned up too early) - remember(entry, executor, saveableStateHolder) { - executor.storeFor(entry.id).getOrCreate(SaveableCloseable::class) { - SaveableCloseable(entry.id.value, WeakReference(saveableStateHolder)) - } + val saveableCloseable = remember(entry, executor, saveableStateHolder) { + executor + .storeFor(entry.id) + .getOrCreate(SaveableCloseable::class) { + SaveableCloseable( + entry.id.value, + WeakReference(saveableStateHolder), + ) + } } + val viewModelStoreOwner = saveableCloseable + .viewModelStoreOwnerState + .value // <-- This will cause the recomposition when the value is cleared. + ?: return + saveableStateHolder.SaveableStateProvider(entry.id.value) { ViewModelStoreOwnerProvider( - viewModelStoreOwner = executor - .storeFor(entry.id) - .getOrCreate( - ViewModelStoreOwnerCloseable::class, - ::ViewModelStoreOwnerCloseable, - ), + viewModelStoreOwner = viewModelStoreOwner, ) { SavedStateHandleFactoryProvider( - savedStateHandleFactory = { executor.savedStateHandleFor(entry.destinationId) }, + savedStateHandleFactory = executor.savedStateHandleFactoryFor(entry.destinationId), ) { entry.destination.content(entry.route) } @@ -102,7 +111,17 @@ internal class SaveableCloseable( private val id: String, private val saveableStateHolderRef: WeakReference, ) : Closeable { + private val _viewModelStoreOwnerState: MutableState = + mutableStateOf(StackEntryViewModelStoreOwner()) + + inline val viewModelStoreOwnerState: State get() = _viewModelStoreOwnerState + override fun close() { + Snapshot.withMutableSnapshot { + _viewModelStoreOwnerState.value?.clearIfInitialized() + _viewModelStoreOwnerState.value = null + } + saveableStateHolderRef.get()?.removeState(id) saveableStateHolderRef.clear() } diff --git a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/MultiStackNavigationExecutor.kt b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/MultiStackNavigationExecutor.kt index 682c9e2a..72299f1a 100644 --- a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/MultiStackNavigationExecutor.kt +++ b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/MultiStackNavigationExecutor.kt @@ -2,6 +2,7 @@ package com.hoc081098.solivagant.navigation.internal import androidx.compose.runtime.State import com.hoc081098.kmp.viewmodel.SavedStateHandle +import com.hoc081098.kmp.viewmodel.SavedStateHandleFactory import com.hoc081098.solivagant.navigation.BaseRoute import com.hoc081098.solivagant.navigation.NavRoot import com.hoc081098.solivagant.navigation.NavRoute @@ -67,6 +68,11 @@ internal class MultiStackNavigationExecutor( return viewModel.provideSavedStateHandle(entry.id, entry.route) } + override fun savedStateHandleFactoryFor(destinationId: DestinationId): SavedStateHandleFactory { + val entry = entryFor(destinationId) + return viewModel.provideSavedStateHandleFactory(entry.id, entry.route) + } + override fun storeFor(destinationId: DestinationId): NavigationExecutor.Store { val entry = entryFor(destinationId) return storeFor(entry.id) diff --git a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/StoreViewModel.kt b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/StoreViewModel.kt index 88765340..d39aad43 100644 --- a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/StoreViewModel.kt +++ b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/StoreViewModel.kt @@ -1,6 +1,7 @@ package com.hoc081098.solivagant.navigation.internal import com.hoc081098.kmp.viewmodel.SavedStateHandle +import com.hoc081098.kmp.viewmodel.SavedStateHandleFactory import com.hoc081098.kmp.viewmodel.ViewModel import com.hoc081098.solivagant.navigation.BaseRoute import com.hoc081098.solivagant.navigation.EXTRA_ROUTE @@ -12,25 +13,33 @@ internal class StoreViewModel( ) : ViewModel() { private val stores = mutableMapOf() private val savedStateHandles = mutableMapOf() + private val savedStateHandleFactories = mutableMapOf() internal val savedNavRoot: NavRoot? get() = globalSavedStateHandle[SAVED_START_ROOT_KEY] - fun provideStore(id: StackEntry.Id): NavigationExecutor.Store { + internal fun provideStore(id: StackEntry.Id): NavigationExecutor.Store { return stores.getOrPut(id) { NavigationExecutorStore() } } - fun provideSavedStateHandle(id: StackEntry.Id, route: BaseRoute): SavedStateHandle { + internal fun provideSavedStateHandle(id: StackEntry.Id, route: BaseRoute): SavedStateHandle { return savedStateHandles.getOrPut(id) { createSavedStateHandleAndSetSavedStateProvider(id.value, globalSavedStateHandle) .apply { this[EXTRA_ROUTE] = route } } } - fun removeEntry(id: StackEntry.Id) { + internal fun provideSavedStateHandleFactory(id: StackEntry.Id, route: BaseRoute): SavedStateHandleFactory { + return savedStateHandleFactories.getOrPut(id) { + SavedStateHandleFactory { provideSavedStateHandle(id, route) } + } + } + + internal fun removeEntry(id: StackEntry.Id) { val store = stores.remove(id) store?.close() savedStateHandles.remove(id) + savedStateHandleFactories.remove(id) globalSavedStateHandle.removeSavedStateProvider(id.value) globalSavedStateHandle.remove(id.value) } @@ -47,6 +56,7 @@ internal class StoreViewModel( globalSavedStateHandle.remove(key.value) } savedStateHandles.clear() + savedStateHandleFactories.clear() } internal fun setInputStartRoot(root: NavRoot) { diff --git a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/ViewModelStoreOwnerCloseable.kt b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/ViewModelStoreOwnerCloseable.kt index a13da4f0..e6023efe 100644 --- a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/ViewModelStoreOwnerCloseable.kt +++ b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/ViewModelStoreOwnerCloseable.kt @@ -1,15 +1,15 @@ package com.hoc081098.solivagant.navigation.internal -import com.hoc081098.kmp.viewmodel.Closeable import com.hoc081098.kmp.viewmodel.ViewModelStore import com.hoc081098.kmp.viewmodel.ViewModelStoreOwner +import kotlin.LazyThreadSafetyMode.NONE internal expect fun createViewModelStore(): ViewModelStore -internal class ViewModelStoreOwnerCloseable : Closeable, ViewModelStoreOwner { - private val viewModelStoreLazy = lazy { createViewModelStore() } +internal class StackEntryViewModelStoreOwner : ViewModelStoreOwner { + private val viewModelStoreLazy = lazy(NONE) { createViewModelStore() } - override fun close() { + fun clearIfInitialized() { if (viewModelStoreLazy.isInitialized()) { viewModelStoreLazy.value.clear() } diff --git a/sample/app/build.gradle.kts b/sample/app/build.gradle.kts index e230af82..96318483 100644 --- a/sample/app/build.gradle.kts +++ b/sample/app/build.gradle.kts @@ -43,41 +43,5 @@ android { dependencies { implementation(project(":sample:shared")) -// implementation(platform(libs.androidx.compose.bom)) -// -// implementation(libs.androidx.lifecycle.runtime.compose) -// -// implementation(libs.androidx.compose.ui.ui) -// debugImplementation(libs.androidx.compose.ui.tooling) -// implementation(libs.androidx.compose.ui.tooling.preview) -// implementation(libs.androidx.compose.foundation) -// implementation(libs.androidx.compose.material3) -// implementation(libs.androidx.compose.material) -// implementation(libs.androidx.compose.runtime) implementation(libs.androidx.activity.compose) -// implementation(libs.androidx.navigation.compose) - -// implementation(libs.koin.androidx.compose) -// implementation(libs.coil.compose) - -// implementation(libs.kotlinx.collections.immutable) } - -// tasks.withType { -// kotlinOptions { -// val buildDirAbsolutePath = project.layout.buildDirectory.map { it.asFile.absolutePath }.get() -// -// if (project.findProperty("composeCompilerReports") == "true") { -// freeCompilerArgs = freeCompilerArgs + listOf( -// "-P", -// "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=$buildDirAbsolutePath/compose_compiler", -// ) -// } -// if (project.findProperty("composeCompilerMetrics") == "true") { -// freeCompilerArgs = freeCompilerArgs + listOf( -// "-P", -// "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=$buildDirAbsolutePath/compose_compiler", -// ) -// } -// } -// } From 6cd16cc53e25cba843b8450445d6b3bc0f7722ed Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sat, 6 Jan 2024 17:35:30 +0700 Subject: [PATCH 8/9] fix --- ...e.android.kt => StackEntryViewModelStoreOwner.android.kt} | 0 ...oreOwnerCloseable.kt => StackEntryViewModelStoreOwner.kt} | 0 .../internal/StackEntryViewModelStoreOwner.nonAndroid.kt | 5 +++++ 3 files changed, 5 insertions(+) rename navigation/src/androidMain/kotlin/com/hoc081098/solivagant/navigation/internal/{ViewModelStoreOwnerCloseable.android.kt => StackEntryViewModelStoreOwner.android.kt} (100%) rename navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/{ViewModelStoreOwnerCloseable.kt => StackEntryViewModelStoreOwner.kt} (100%) create mode 100644 navigation/src/nonAndroidMain/kotlin/com/hoc081098/solivagant/navigation/internal/StackEntryViewModelStoreOwner.nonAndroid.kt diff --git a/navigation/src/androidMain/kotlin/com/hoc081098/solivagant/navigation/internal/ViewModelStoreOwnerCloseable.android.kt b/navigation/src/androidMain/kotlin/com/hoc081098/solivagant/navigation/internal/StackEntryViewModelStoreOwner.android.kt similarity index 100% rename from navigation/src/androidMain/kotlin/com/hoc081098/solivagant/navigation/internal/ViewModelStoreOwnerCloseable.android.kt rename to navigation/src/androidMain/kotlin/com/hoc081098/solivagant/navigation/internal/StackEntryViewModelStoreOwner.android.kt diff --git a/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/ViewModelStoreOwnerCloseable.kt b/navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/StackEntryViewModelStoreOwner.kt similarity index 100% rename from navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/ViewModelStoreOwnerCloseable.kt rename to navigation/src/commonMain/kotlin/com/hoc081098/solivagant/navigation/internal/StackEntryViewModelStoreOwner.kt diff --git a/navigation/src/nonAndroidMain/kotlin/com/hoc081098/solivagant/navigation/internal/StackEntryViewModelStoreOwner.nonAndroid.kt b/navigation/src/nonAndroidMain/kotlin/com/hoc081098/solivagant/navigation/internal/StackEntryViewModelStoreOwner.nonAndroid.kt new file mode 100644 index 00000000..b8a60fc2 --- /dev/null +++ b/navigation/src/nonAndroidMain/kotlin/com/hoc081098/solivagant/navigation/internal/StackEntryViewModelStoreOwner.nonAndroid.kt @@ -0,0 +1,5 @@ +package com.hoc081098.solivagant.navigation.internal + +import com.hoc081098.kmp.viewmodel.ViewModelStore + +internal actual fun createViewModelStore(): ViewModelStore = ViewModelStore() From 398ba0909815fe02dfa79fbf059600d3c1626126 Mon Sep 17 00:00:00 2001 From: "hoc081098@gmail.com" Date: Sat, 6 Jan 2024 18:13:42 +0700 Subject: [PATCH 9/9] fix --- .../solivagant/sample/SolivagantSampleApp.kt | 87 ++++++++++--------- 1 file changed, 45 insertions(+), 42 deletions(-) diff --git a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/SolivagantSampleApp.kt b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/SolivagantSampleApp.kt index 0a31de89..b50dd1aa 100644 --- a/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/SolivagantSampleApp.kt +++ b/sample/shared/src/commonMain/kotlin/com/hoc081098/solivagant/sample/SolivagantSampleApp.kt @@ -36,6 +36,7 @@ import com.hoc081098.solivagant.sample.products.ProductsScreenRoute import com.hoc081098.solivagant.sample.search_products.SearchProductScreenDestination import com.hoc081098.solivagant.sample.search_products.SearchProductScreenRoute import kotlinx.collections.immutable.persistentSetOf +import org.koin.compose.KoinContext import org.koin.compose.koinInject @OptIn( @@ -64,50 +65,52 @@ fun SolivagantSampleApp( ) } - MyApplicationTheme { - Surface( - modifier = modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background, - ) { - Scaffold( - topBar = { - TopAppBar( - title = { - Text( - text = when (currentRoute) { - StartScreenRoute -> "KMP ViewModel Sample" - is ProductsScreenRoute -> "Products screen" - is SearchProductScreenRoute -> "Search products screen" - is ProductDetailScreenRoute -> "Product detail screen" - else -> "KMP ViewModel Sample" - }, - ) - }, - navigationIcon = { - if (currentRoute !is NavRoot) { - IconButton( - onClick = remember { navigator::navigateBack }, - ) { - Icon( - imageVector = Icons.Default.ArrowBack, - contentDescription = "Back", - ) + KoinContext { + MyApplicationTheme { + Surface( + modifier = modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background, + ) { + Scaffold( + topBar = { + TopAppBar( + title = { + Text( + text = when (currentRoute) { + StartScreenRoute -> "KMP ViewModel Sample" + is ProductsScreenRoute -> "Products screen" + is SearchProductScreenRoute -> "Search products screen" + is ProductDetailScreenRoute -> "Product detail screen" + else -> "KMP ViewModel Sample" + }, + ) + }, + navigationIcon = { + if (currentRoute !is NavRoot) { + IconButton( + onClick = remember { navigator::navigateBack }, + ) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = "Back", + ) + } } - } - }, + }, + ) + }, + ) { innerPadding -> + NavHost( + modifier = Modifier + .padding(innerPadding) + .consumeWindowInsets(innerPadding) + .fillMaxSize(), + startRoute = StartScreenRoute, + destinations = destinations, + navEventNavigator = navigator, + destinationChangedCallback = { currentRoute = it }, ) - }, - ) { innerPadding -> - NavHost( - modifier = Modifier - .padding(innerPadding) - .consumeWindowInsets(innerPadding) - .fillMaxSize(), - startRoute = StartScreenRoute, - destinations = destinations, - navEventNavigator = navigator, - destinationChangedCallback = { currentRoute = it }, - ) + } } } }