diff --git a/README.md b/README.md index 77136eb4..3e763197 100644 --- a/README.md +++ b/README.md @@ -144,15 +144,11 @@ The `com.huanshankeji.compose.material.icons.Icon` class delegates to both kinds ### ViewModel -The ViewModel module currently supports a small subset of the Compose ViewModel APIs, and delegates to raw UI state on -Compose HTML / JS DOM. These APIs are highly experimental now. +The ViewModel module currently supports a subset of the Compose ViewModel APIs. For ViewModel to work properly on Compose HTML / JS DOM, call `com.huanshankeji.compose.ui.window.renderComposableInBodyWithViewModelStoreOwner` instead of `org.jetbrains.compose.web.renderComposableInBody` on JS. These APIs are experimental now. ### Navigation -The navigation module currently supports a small subset of the Compose Navigation APIs, which does not support -transition or animation on Compose HTML / JS DOM. These APIs are also highly experimental now. -See [CMP-4966](https://youtrack.jetbrains.com/issue/CMP-4966) for a bug to avoid. Also, ViewModel-related functions -are not implemented yet on Compose HTML / JS DOM. +The navigation module currently supports a small subset of the Compose Navigation APIs, which does not support transition or animation on Compose HTML / JS DOM. These APIs are also experimental now. See [CMP-4966](https://youtrack.jetbrains.com/issue/CMP-4966) for a bug to avoid. ## Add to your dependencies diff --git a/buildSrc/src/main/kotlin/VersionsAndDependencies.kt b/buildSrc/src/main/kotlin/VersionsAndDependencies.kt index 7720bc25..bb4a72c4 100644 --- a/buildSrc/src/main/kotlin/VersionsAndDependencies.kt +++ b/buildSrc/src/main/kotlin/VersionsAndDependencies.kt @@ -1,7 +1,7 @@ import com.huanshankeji.CommonDependencies import org.jetbrains.compose.ComposeBuildConfig -val projectVersion = "0.5.1-SNAPSHOT" +val projectVersion = "0.6.0-SNAPSHOT" val commonDependencies = CommonDependencies() diff --git a/common/api/compose-multiplatform-html-unified-common.klib.api b/common/api/compose-multiplatform-html-unified-common.klib.api index 97ff7cf5..5a02a3d9 100644 --- a/common/api/compose-multiplatform-html-unified-common.klib.api +++ b/common/api/compose-multiplatform-html-unified-common.klib.api @@ -992,6 +992,14 @@ final enum class com.huanshankeji.browser/Browser : kotlin/Enum // com.huanshankeji.browser/Browser.values|values#static(){}[0] } +// Targets: [js] +final class com.huanshankeji.compose.ui.window/SimpleViewModelStoreOwner : androidx.lifecycle/ViewModelStoreOwner { // com.huanshankeji.compose.ui.window/SimpleViewModelStoreOwner|null[0] + constructor () // com.huanshankeji.compose.ui.window/SimpleViewModelStoreOwner.|(){}[0] + + final val viewModelStore // com.huanshankeji.compose.ui.window/SimpleViewModelStoreOwner.viewModelStore|{}viewModelStore[0] + final fun (): androidx.lifecycle/ViewModelStore // com.huanshankeji.compose.ui.window/SimpleViewModelStoreOwner.viewModelStore.|(){}[0] +} + // Targets: [js] final object com.huanshankeji.compose.foundation.lazy/LazyItemScope { // com.huanshankeji.compose.foundation.lazy/LazyItemScope|null[0] final fun (com.huanshankeji.compose.ui/Modifier).fillParentMaxHeight(kotlin/Float = ...): com.huanshankeji.compose.ui/Modifier // com.huanshankeji.compose.foundation.lazy/LazyItemScope.fillParentMaxHeight|fillParentMaxHeight@com.huanshankeji.compose.ui.Modifier(kotlin.Float){}[0] @@ -1028,6 +1036,9 @@ final val com.huanshankeji.compose.foundation/imitateComposeUiLayoutHorizontalSc final val com.huanshankeji.compose.foundation/imitateComposeUiLayoutVerticalScrollPlatformModifier // com.huanshankeji.compose.foundation/imitateComposeUiLayoutVerticalScrollPlatformModifier|{}imitateComposeUiLayoutVerticalScrollPlatformModifier[0] final fun (): com.varabyte.kobweb.compose.ui/Modifier // com.huanshankeji.compose.foundation/imitateComposeUiLayoutVerticalScrollPlatformModifier.|(){}[0] +// Targets: [js] +final val com.huanshankeji.compose.ui.window/com_huanshankeji_compose_ui_window_SimpleViewModelStoreOwner$stableprop // com.huanshankeji.compose.ui.window/com_huanshankeji_compose_ui_window_SimpleViewModelStoreOwner$stableprop|#static{}com_huanshankeji_compose_ui_window_SimpleViewModelStoreOwner$stableprop[0] + // Targets: [js] final fun (androidx.compose.ui.unit/Dp).com.huanshankeji.compose.ui.unit/toPx(): org.jetbrains.compose.web.css/CSSSizeValue // com.huanshankeji.compose.ui.unit/toPx|toPx@androidx.compose.ui.unit.Dp(){}[0] @@ -1138,3 +1149,12 @@ final fun com.huanshankeji.compose.foundation/com_huanshankeji_compose_foundatio // Targets: [js] final fun com.huanshankeji.compose.foundation/rememberScrollState(kotlin/Int, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): com.huanshankeji.compose.foundation/ScrollState // com.huanshankeji.compose.foundation/rememberScrollState|rememberScrollState(kotlin.Int;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] + +// Targets: [js] +final fun com.huanshankeji.compose.ui.platform/findComposeDefaultViewModelStoreOwner(androidx.compose.runtime/Composer?, kotlin/Int): androidx.lifecycle/ViewModelStoreOwner? // com.huanshankeji.compose.ui.platform/findComposeDefaultViewModelStoreOwner|findComposeDefaultViewModelStoreOwner(androidx.compose.runtime.Composer?;kotlin.Int){}[0] + +// Targets: [js] +final fun com.huanshankeji.compose.ui.window/com_huanshankeji_compose_ui_window_SimpleViewModelStoreOwner$stableprop_getter(): kotlin/Int // com.huanshankeji.compose.ui.window/com_huanshankeji_compose_ui_window_SimpleViewModelStoreOwner$stableprop_getter|com_huanshankeji_compose_ui_window_SimpleViewModelStoreOwner$stableprop_getter(){}[0] + +// Targets: [js] +final fun com.huanshankeji.compose.ui.window/renderComposableInBodyWithViewModelStoreOwner(kotlin/Function3, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>): androidx.compose.runtime/Composition // com.huanshankeji.compose.ui.window/renderComposableInBodyWithViewModelStoreOwner|renderComposableInBodyWithViewModelStoreOwner(kotlin.Function3,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>){}[0] diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 1b37d89f..770c49d1 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -56,6 +56,13 @@ kotlin { // see: https://github.com/varabyte/kobweb/blob/main/frontend/kobweb-compose/build.gradle.kts api("com.varabyte.kobweb:kobweb-compose:${DependencyVersions.kobweb}") implementation("com.huanshankeji:compose-html-common:${DependencyVersions.huanshankejiComposeHtml}") + + /* + The UI module depends on the lifecycle module to use `androidx.lifecycle.ViewModelStoreOwner`. + See https://github.com/JetBrains/compose-multiplatform-core/blob/jb-main/compose/ui/ui/build.gradle#L87. + This is actually only needed for JS DOM. + */ + implementation(commonDependencies.jetbrainsAndroidx.lifecycle.viewmodel()) } } } diff --git a/common/src/jsMain/kotlin/com/huanshankeji/compose/ui/platform/DefaultViewModelOwnerStore.js.kt b/common/src/jsMain/kotlin/com/huanshankeji/compose/ui/platform/DefaultViewModelOwnerStore.js.kt new file mode 100644 index 00000000..e56f1e29 --- /dev/null +++ b/common/src/jsMain/kotlin/com/huanshankeji/compose/ui/platform/DefaultViewModelOwnerStore.js.kt @@ -0,0 +1,23 @@ +package com.huanshankeji.compose.ui.platform + +// copied and adapted from "DefaultViewModelOwnerStore.skiko.kt" in `androidx.compose.ui.platform` + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.InternalComposeApi +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.lifecycle.ViewModelStoreOwner + +/** + * Internal helper to provide [ViewModelStoreOwner] from Compose UI module. + * In applications please use [androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner]. + * + * @hide + */ +internal val LocalInternalViewModelStoreOwner = staticCompositionLocalOf { + null +} + +@InternalComposeApi +@Composable +fun findComposeDefaultViewModelStoreOwner(): ViewModelStoreOwner? = + LocalInternalViewModelStoreOwner.current diff --git a/common/src/jsMain/kotlin/com/huanshankeji/compose/ui/window/ComposeWindow.js.kt b/common/src/jsMain/kotlin/com/huanshankeji/compose/ui/window/ComposeWindow.js.kt new file mode 100644 index 00000000..ffcc7f16 --- /dev/null +++ b/common/src/jsMain/kotlin/com/huanshankeji/compose/ui/window/ComposeWindow.js.kt @@ -0,0 +1,36 @@ +package com.huanshankeji.compose.ui.window + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Composition +import androidx.compose.runtime.CompositionLocalProvider +import androidx.lifecycle.ViewModelStore +import androidx.lifecycle.ViewModelStoreOwner +import com.huanshankeji.compose.ExperimentalApi +import com.huanshankeji.compose.ui.platform.LocalInternalViewModelStoreOwner +import org.jetbrains.compose.web.dom.DOMScope +import org.jetbrains.compose.web.renderComposableInBody +import org.w3c.dom.HTMLBodyElement + +@ExperimentalApi +class SimpleViewModelStoreOwner : ViewModelStoreOwner { + override val viewModelStore: ViewModelStore = ViewModelStore() +} + +fun renderComposableInBodyWithViewModelStoreOwner( + content: @Composable DOMScope.() -> Unit +): Composition = + renderComposableInBody { + // copied and adapted from `ComposeWindow` in "ComposeWindow.web.kt" in `androidx.compose.ui.window` + // also see `ComposeViewport` on Wasm JS + @OptIn(ExperimentalApi::class) + CompositionLocalProvider( + /* TODO add back these 2 lines below if needed one day + in a function possibly named `renderComposableInBodyWithLifecycle` */ + //LocalSystemTheme provides systemThemeObserver.currentSystemTheme.value, + //LocalLifecycleOwner provides this, + LocalInternalViewModelStoreOwner provides SimpleViewModelStoreOwner(), + content = { + content() + } + ) + } diff --git a/demo/src/commonMain/kotlin/com/huanshankeji/compose/material/demo/Material3.kt b/demo/src/commonMain/kotlin/com/huanshankeji/compose/material/demo/Material3.kt index a32c2d6e..606063b9 100644 --- a/demo/src/commonMain/kotlin/com/huanshankeji/compose/material/demo/Material3.kt +++ b/demo/src/commonMain/kotlin/com/huanshankeji/compose/material/demo/Material3.kt @@ -2,6 +2,7 @@ package com.huanshankeji.compose.material.demo import androidx.compose.runtime.* import androidx.compose.ui.unit.dp +import com.huanshankeji.androidx.lifecycle.viewmodel.compose.viewModel import com.huanshankeji.compose.ExtRecommendedApi import com.huanshankeji.compose.foundation.layout.* import com.huanshankeji.compose.foundation.rememberScrollState @@ -27,10 +28,12 @@ import com.huanshankeji.compose.ui.Modifier import com.huanshankeji.compose.material3.Button as RowScopeButton @Composable -fun Material3(/*modifier: Modifier = Modifier*/) { +fun Material3(/*modifier: Modifier = Modifier*/ + viewModel: Material3ViewModel = viewModel { Material3ViewModel() } +) { Column(Modifier.verticalScroll(rememberScrollState()).innerContentPadding(), Arrangement.spacedBy(16.dp)) { - var count by remember { mutableStateOf(0) } - val onClick: () -> Unit = { count++ } + val count by viewModel.countState.collectAsState() + val onClick: () -> Unit = { viewModel.countState.value++ } val buttonContent: @Composable () -> Unit = { TaglessText(count.toString()) } @@ -56,7 +59,8 @@ fun Material3(/*modifier: Modifier = Modifier*/) { FilledTonalIconButton(onClick, content = iconButtonContent) OutlinedIconButton(onClick, content = iconButtonContent) } - val (checked, onCheckedChange) = remember { mutableStateOf(false) } + val checked = viewModel.checkedState.collectAsState().value + val onCheckedChange: (Boolean) -> Unit = { viewModel.checkedState.value = it } val iconToggleButtonContent: @Composable () -> Unit = { Icon(if (checked) Icons.Default.Add else Icons.Default.Remove, null) } diff --git a/demo/src/commonMain/kotlin/com/huanshankeji/compose/material/demo/Material3ViewModel.kt b/demo/src/commonMain/kotlin/com/huanshankeji/compose/material/demo/Material3ViewModel.kt new file mode 100644 index 00000000..f45f7a6d --- /dev/null +++ b/demo/src/commonMain/kotlin/com/huanshankeji/compose/material/demo/Material3ViewModel.kt @@ -0,0 +1,9 @@ +package com.huanshankeji.compose.material.demo + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow + +class Material3ViewModel : ViewModel() { + val countState = MutableStateFlow(0) + val checkedState = MutableStateFlow(false) +} \ No newline at end of file diff --git a/demo/src/jsMain/kotlin/com/huanshankeji/compose/material/demo/Main.kt b/demo/src/jsMain/kotlin/com/huanshankeji/compose/material/demo/Main.kt index 8766b2c7..0007736a 100644 --- a/demo/src/jsMain/kotlin/com/huanshankeji/compose/material/demo/Main.kt +++ b/demo/src/jsMain/kotlin/com/huanshankeji/compose/material/demo/Main.kt @@ -1,9 +1,10 @@ package com.huanshankeji.compose.material.demo import com.huanshankeji.compose.html.material3.require -import org.jetbrains.compose.web.renderComposableInBody +import com.huanshankeji.compose.ui.window.renderComposableInBodyWithViewModelStoreOwner fun main() { require("material-symbols/outlined.css") - renderComposableInBody { App() } + //renderComposableInBody { App() } // "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner" + renderComposableInBodyWithViewModelStoreOwner { App() } } diff --git a/lifecycle-viewmodel/api/android/compose-multiplatform-html-unified-lifecycle-viewmodel.api b/lifecycle-viewmodel/api/android/compose-multiplatform-html-unified-lifecycle-viewmodel.api index e69de29b..819e2a1a 100644 --- a/lifecycle-viewmodel/api/android/compose-multiplatform-html-unified-lifecycle-viewmodel.api +++ b/lifecycle-viewmodel/api/android/compose-multiplatform-html-unified-lifecycle-viewmodel.api @@ -0,0 +1,9 @@ +public final class com/huanshankeji/androidx/lifecycle/viewmodel/compose/ViewModelKt { + public static final fun defaultCreationExtras (Landroidx/lifecycle/ViewModelStoreOwner;)Landroidx/lifecycle/viewmodel/CreationExtras; +} + +public final class com/huanshankeji/androidx/lifecycle/viewmodel/compose/ViewModel_composeUiKt { + public static final fun defaultViewModelStoreOwner (Landroidx/compose/runtime/Composer;I)Landroidx/lifecycle/ViewModelStoreOwner; + public static final fun viewModel (Lkotlin/reflect/KClass;Landroidx/lifecycle/ViewModelStoreOwner;Ljava/lang/String;Landroidx/lifecycle/ViewModelProvider$Factory;Landroidx/lifecycle/viewmodel/CreationExtras;Landroidx/compose/runtime/Composer;II)Landroidx/lifecycle/ViewModel; +} + diff --git a/lifecycle-viewmodel/api/compose-multiplatform-html-unified-lifecycle-viewmodel.klib.api b/lifecycle-viewmodel/api/compose-multiplatform-html-unified-lifecycle-viewmodel.klib.api index 8678f89a..d662a05e 100644 --- a/lifecycle-viewmodel/api/compose-multiplatform-html-unified-lifecycle-viewmodel.klib.api +++ b/lifecycle-viewmodel/api/compose-multiplatform-html-unified-lifecycle-viewmodel.klib.api @@ -6,4 +6,22 @@ // - Show declarations: true // Library unique name: +final fun (androidx.lifecycle/ViewModelStoreOwner).com.huanshankeji.androidx.lifecycle.viewmodel.compose/defaultCreationExtras(): androidx.lifecycle.viewmodel/CreationExtras // com.huanshankeji.androidx.lifecycle.viewmodel.compose/defaultCreationExtras|defaultCreationExtras@androidx.lifecycle.ViewModelStoreOwner(){}[0] +final fun <#A: androidx.lifecycle/ViewModel> com.huanshankeji.androidx.lifecycle.viewmodel.compose/viewModel(kotlin.reflect/KClass<#A>, androidx.lifecycle/ViewModelStoreOwner?, kotlin/String?, androidx.lifecycle/ViewModelProvider.Factory?, androidx.lifecycle.viewmodel/CreationExtras?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): #A // com.huanshankeji.androidx.lifecycle.viewmodel.compose/viewModel|viewModel(kotlin.reflect.KClass<0:0>;androidx.lifecycle.ViewModelStoreOwner?;kotlin.String?;androidx.lifecycle.ViewModelProvider.Factory?;androidx.lifecycle.viewmodel.CreationExtras?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){0§}[0] +final fun com.huanshankeji.androidx.lifecycle.viewmodel.compose/defaultViewModelStoreOwner(androidx.compose.runtime/Composer?, kotlin/Int): androidx.lifecycle/ViewModelStoreOwner // com.huanshankeji.androidx.lifecycle.viewmodel.compose/defaultViewModelStoreOwner|defaultViewModelStoreOwner(androidx.compose.runtime.Composer?;kotlin.Int){}[0] +final inline fun <#A: reified androidx.lifecycle/ViewModel> com.huanshankeji.androidx.lifecycle.viewmodel.compose/viewModel(androidx.lifecycle/ViewModelStoreOwner?, kotlin/String?, noinline kotlin/Function1, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): #A // com.huanshankeji.androidx.lifecycle.viewmodel.compose/viewModel|viewModel(androidx.lifecycle.ViewModelStoreOwner?;kotlin.String?;kotlin.Function1;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){0§}[0] final inline fun <#A: reified androidx.lifecycle/ViewModel> com.huanshankeji.androidx.lifecycle.viewmodel.compose/viewModel(kotlin/String?, noinline kotlin/Function1, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): #A // com.huanshankeji.androidx.lifecycle.viewmodel.compose/viewModel|viewModel(kotlin.String?;kotlin.Function1;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){0§}[0] + +// Targets: [js] +final object com.huanshankeji.androidx.lifecycle.viewmodel.compose/LocalViewModelStoreOwner { // com.huanshankeji.androidx.lifecycle.viewmodel.compose/LocalViewModelStoreOwner|null[0] + final val current // com.huanshankeji.androidx.lifecycle.viewmodel.compose/LocalViewModelStoreOwner.current|{}current[0] + final fun (androidx.compose.runtime/Composer?, kotlin/Int): androidx.lifecycle/ViewModelStoreOwner? // com.huanshankeji.androidx.lifecycle.viewmodel.compose/LocalViewModelStoreOwner.current.|(androidx.compose.runtime.Composer?;kotlin.Int){}[0] + + final fun provides(androidx.lifecycle/ViewModelStoreOwner): androidx.compose.runtime/ProvidedValue // com.huanshankeji.androidx.lifecycle.viewmodel.compose/LocalViewModelStoreOwner.provides|provides(androidx.lifecycle.ViewModelStoreOwner){}[0] +} + +// Targets: [js] +final val com.huanshankeji.androidx.lifecycle.viewmodel.compose/com_huanshankeji_androidx_lifecycle_viewmodel_compose_LocalViewModelStoreOwner$stableprop // com.huanshankeji.androidx.lifecycle.viewmodel.compose/com_huanshankeji_androidx_lifecycle_viewmodel_compose_LocalViewModelStoreOwner$stableprop|#static{}com_huanshankeji_androidx_lifecycle_viewmodel_compose_LocalViewModelStoreOwner$stableprop[0] + +// Targets: [js] +final fun com.huanshankeji.androidx.lifecycle.viewmodel.compose/com_huanshankeji_androidx_lifecycle_viewmodel_compose_LocalViewModelStoreOwner$stableprop_getter(): kotlin/Int // com.huanshankeji.androidx.lifecycle.viewmodel.compose/com_huanshankeji_androidx_lifecycle_viewmodel_compose_LocalViewModelStoreOwner$stableprop_getter|com_huanshankeji_androidx_lifecycle_viewmodel_compose_LocalViewModelStoreOwner$stableprop_getter(){}[0] diff --git a/lifecycle-viewmodel/api/jvm/compose-multiplatform-html-unified-lifecycle-viewmodel.api b/lifecycle-viewmodel/api/jvm/compose-multiplatform-html-unified-lifecycle-viewmodel.api index e69de29b..819e2a1a 100644 --- a/lifecycle-viewmodel/api/jvm/compose-multiplatform-html-unified-lifecycle-viewmodel.api +++ b/lifecycle-viewmodel/api/jvm/compose-multiplatform-html-unified-lifecycle-viewmodel.api @@ -0,0 +1,9 @@ +public final class com/huanshankeji/androidx/lifecycle/viewmodel/compose/ViewModelKt { + public static final fun defaultCreationExtras (Landroidx/lifecycle/ViewModelStoreOwner;)Landroidx/lifecycle/viewmodel/CreationExtras; +} + +public final class com/huanshankeji/androidx/lifecycle/viewmodel/compose/ViewModel_composeUiKt { + public static final fun defaultViewModelStoreOwner (Landroidx/compose/runtime/Composer;I)Landroidx/lifecycle/ViewModelStoreOwner; + public static final fun viewModel (Lkotlin/reflect/KClass;Landroidx/lifecycle/ViewModelStoreOwner;Ljava/lang/String;Landroidx/lifecycle/ViewModelProvider$Factory;Landroidx/lifecycle/viewmodel/CreationExtras;Landroidx/compose/runtime/Composer;II)Landroidx/lifecycle/ViewModel; +} + diff --git a/lifecycle-viewmodel/build.gradle.kts b/lifecycle-viewmodel/build.gradle.kts index 38114e06..de4754f1 100644 --- a/lifecycle-viewmodel/build.gradle.kts +++ b/lifecycle-viewmodel/build.gradle.kts @@ -1,3 +1,4 @@ +import com.huanshankeji.cpnProject import com.huanshankeji.team.`Shreck Ye` import com.huanshankeji.team.pomForTeamDefaultOpenSource @@ -16,6 +17,9 @@ kotlin { */ api(compose.runtime) api(commonDependencies.jetbrainsAndroidx.lifecycle.viewmodel()) + // only needed on JS DOM actually + // https://github.com/JetBrains/compose-multiplatform-core/blob/f1e03d0784631a88201931a6a6a708cdd090be57/lifecycle/lifecycle-viewmodel-compose/build.gradle#L58 + api(cpnProject(project, ":common")) } } composeUiMain { diff --git a/lifecycle-viewmodel/src/commonMain/kotlin/ViewModel.kt b/lifecycle-viewmodel/src/commonMain/kotlin/ViewModel.kt index 1a6ea197..5576acea 100644 --- a/lifecycle-viewmodel/src/commonMain/kotlin/ViewModel.kt +++ b/lifecycle-viewmodel/src/commonMain/kotlin/ViewModel.kt @@ -1,13 +1,57 @@ package com.huanshankeji.androidx.lifecycle.viewmodel.compose import androidx.compose.runtime.Composable +import androidx.lifecycle.HasDefaultViewModelProviderFactory import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.viewmodel.CreationExtras +import kotlin.reflect.KClass // https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-viewmodel.html +// copied and adapted from "ViewModel.kt" in `androidx.lifecycle.viewmodel.compose` + + +// `expect` can be removed if `expect object LocalViewModelStoreOwner` is added. +@PublishedApi +@Composable +internal expect fun defaultViewModelStoreOwner(): ViewModelStoreOwner + +@PublishedApi +internal fun ViewModelStoreOwner.defaultCreationExtras(): CreationExtras = + if (this is HasDefaultViewModelProviderFactory) { + this.defaultViewModelCreationExtras + } else { + CreationExtras.Empty + } + + +@Composable +expect fun viewModel( + modelClass: KClass, + viewModelStoreOwner: ViewModelStoreOwner = defaultViewModelStoreOwner(), + key: String? = null, + factory: ViewModelProvider.Factory? = null, + extras: CreationExtras = viewModelStoreOwner.defaultCreationExtras() +): VM + @Composable expect inline fun viewModel( + viewModelStoreOwner: ViewModelStoreOwner = defaultViewModelStoreOwner(), key: String? = null, noinline initializer: CreationExtras.() -> VM ): VM + +@Deprecated( + "Use the one with a `viewModelStoreOwner` parameter instead. " + + "This function might be removed in the future. " + + "If you call this function with a `key` argument, make sure you used a named argument " + + "so your source still compiles when this is removed." +) +@Composable +inline fun viewModel( + key: String? = null, + noinline initializer: CreationExtras.() -> VM +): VM = + viewModel(defaultViewModelStoreOwner(), key, initializer) diff --git a/lifecycle-viewmodel/src/composeUiMain/kotlin/ViewModel.composeUi.kt b/lifecycle-viewmodel/src/composeUiMain/kotlin/ViewModel.composeUi.kt index 955b18e7..8dd26b59 100644 --- a/lifecycle-viewmodel/src/composeUiMain/kotlin/ViewModel.composeUi.kt +++ b/lifecycle-viewmodel/src/composeUiMain/kotlin/ViewModel.composeUi.kt @@ -2,9 +2,35 @@ package com.huanshankeji.androidx.lifecycle.viewmodel.compose import androidx.compose.runtime.Composable import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.viewmodel.CreationExtras -import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner +import kotlin.reflect.KClass +import androidx.lifecycle.viewmodel.compose.viewModel as composeUiViewModel +// copied and adapted from "ViewModel.kt" in `androidx.lifecycle.viewmodel.compose` +@PublishedApi @Composable -actual inline fun viewModel(key: String?, noinline initializer: CreationExtras.() -> VM): VM = - viewModel(key = key, initializer = initializer) +internal actual fun defaultViewModelStoreOwner(): ViewModelStoreOwner = + checkNotNull(LocalViewModelStoreOwner.current) { + "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner" + } + +@Composable +actual fun viewModel( + modelClass: KClass, + viewModelStoreOwner: ViewModelStoreOwner, + key: String?, + factory: ViewModelProvider.Factory?, + extras: CreationExtras +): VM = + composeUiViewModel(modelClass, viewModelStoreOwner, key, factory, extras) + +@Composable +actual inline fun viewModel( + viewModelStoreOwner: ViewModelStoreOwner, + key: String?, + noinline initializer: CreationExtras.() -> VM +): VM = + composeUiViewModel(viewModelStoreOwner, key, initializer) diff --git a/lifecycle-viewmodel/src/jsMain/kotlin/LocalViewModelStoreOwner.kt b/lifecycle-viewmodel/src/jsMain/kotlin/LocalViewModelStoreOwner.kt new file mode 100644 index 00000000..fb362152 --- /dev/null +++ b/lifecycle-viewmodel/src/jsMain/kotlin/LocalViewModelStoreOwner.kt @@ -0,0 +1,28 @@ +package com.huanshankeji.androidx.lifecycle.viewmodel.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.InternalComposeApi +import androidx.compose.runtime.ProvidedValue +import androidx.compose.runtime.compositionLocalOf +import androidx.lifecycle.ViewModelStoreOwner +import com.huanshankeji.compose.ui.platform.findComposeDefaultViewModelStoreOwner + +// copied and adapted from "LocalViewModelStoreOwner.kt" and "LocalViewModelStoreOwner.jb.kt" in `androidx.lifecycle.viewmodel.compose` + +object LocalViewModelStoreOwner { + private val LocalViewModelStoreOwner = + compositionLocalOf { null } + val current: ViewModelStoreOwner? + @Composable + get() = LocalViewModelStoreOwner.current ?: findViewTreeViewModelStoreOwner() + + infix fun provides(viewModelStoreOwner: ViewModelStoreOwner): + ProvidedValue { + return LocalViewModelStoreOwner.provides(viewModelStoreOwner) + } +} + +@OptIn(InternalComposeApi::class) +@Composable +internal fun findViewTreeViewModelStoreOwner(): ViewModelStoreOwner? = + findComposeDefaultViewModelStoreOwner() diff --git a/lifecycle-viewmodel/src/jsMain/kotlin/ViewModel.js.kt b/lifecycle-viewmodel/src/jsMain/kotlin/ViewModel.js.kt index ba52ce07..a7bd921c 100644 --- a/lifecycle-viewmodel/src/jsMain/kotlin/ViewModel.js.kt +++ b/lifecycle-viewmodel/src/jsMain/kotlin/ViewModel.js.kt @@ -1,10 +1,65 @@ package com.huanshankeji.androidx.lifecycle.viewmodel.compose import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember +import androidx.lifecycle.HasDefaultViewModelProviderFactory import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.viewmodel.CreationExtras +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import kotlin.reflect.KClass +// copied and adapted from "ViewModel.kt" in `androidx.lifecycle.viewmodel.compose` + + + +@PublishedApi @Composable -actual inline fun viewModel(key: String?, noinline initializer: CreationExtras.() -> VM): VM = - remember(key) { CreationExtras.Empty.initializer() } +internal actual fun defaultViewModelStoreOwner(): ViewModelStoreOwner = + checkNotNull(LocalViewModelStoreOwner.current) { + "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner" + } + + +@Composable +actual fun viewModel( + modelClass: KClass, + viewModelStoreOwner: ViewModelStoreOwner, + key: String?, + factory: ViewModelProvider.Factory?, + extras: CreationExtras +): VM = viewModelStoreOwner.get(modelClass, key, factory, extras) + +@Composable +actual inline fun viewModel( + viewModelStoreOwner: ViewModelStoreOwner, + key: String?, + noinline initializer: CreationExtras.() -> VM +): VM = viewModel( + VM::class, + viewModelStoreOwner, + key, + viewModelFactory { initializer(initializer) }, + viewModelStoreOwner.defaultCreationExtras() +) + +internal fun ViewModelStoreOwner.get( + modelClass: KClass, + key: String?, + factory: ViewModelProvider.Factory?, + extras: CreationExtras +): VM { + val provider = if (factory != null) { + ViewModelProvider.create(this.viewModelStore, factory, extras) + } else if (this is HasDefaultViewModelProviderFactory) { + ViewModelProvider.create(this.viewModelStore, this.defaultViewModelProviderFactory, extras) + } else { + ViewModelProvider.create(this) + } + return if (key != null) { + provider[key, modelClass] + } else { + provider[modelClass] + } +} diff --git a/navigation/build.gradle.kts b/navigation/build.gradle.kts index 0ad712fe..c41c7932 100644 --- a/navigation/build.gradle.kts +++ b/navigation/build.gradle.kts @@ -20,6 +20,8 @@ kotlin { //implementation("org.jetbrains.compose.annotation-internal:annotation:${DependencyVersions.composeMultiplatform}") api(cpnProject(project, ":common")) // for `Modifier` and `Alignment` //implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0") // This depends on Compose target '[jscanvas]'. + // https://github.com/JetBrains/compose-multiplatform-core/blob/f6d989a1ae9cd5895b4fba7821946ead389c4848/navigation/navigation-compose/build.gradle#L58 + api(cpnProject(project, ":lifecycle-viewmodel")) } } composeUiMain { diff --git a/navigation/src/jsMain/kotlin/com/huanshankeji/androidx/navigation/compose/NavHost.js.kt b/navigation/src/jsMain/kotlin/com/huanshankeji/androidx/navigation/compose/NavHost.js.kt index 3ef23d1a..d77353a4 100644 --- a/navigation/src/jsMain/kotlin/com/huanshankeji/androidx/navigation/compose/NavHost.js.kt +++ b/navigation/src/jsMain/kotlin/com/huanshankeji/androidx/navigation/compose/NavHost.js.kt @@ -1,7 +1,10 @@ package com.huanshankeji.androidx.navigation.compose import androidx.compose.runtime.* +import androidx.lifecycle.ViewModelStore +import androidx.lifecycle.ViewModelStoreOwner import androidx.navigation.* +import com.huanshankeji.androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import com.huanshankeji.compose.foundation.layout.Box import com.huanshankeji.compose.foundation.layout.fillMaxSize import com.huanshankeji.compose.ui.Alignment @@ -10,10 +13,11 @@ import com.huanshankeji.compose.ui.Modifier // copied and adapted from "NavHost.kt" in `androidx.navigation.compose` -/* -private class ComposeViewModelStoreOwner: ViewModelStoreOwner { +private class ComposeViewModelStoreOwner : ViewModelStoreOwner { override val viewModelStore: ViewModelStore = ViewModelStore() - fun dispose() { viewModelStore.clear() } + fun dispose() { + viewModelStore.clear() + } } @Composable @@ -24,9 +28,6 @@ private fun rememberViewModelStoreOwner(): ViewModelStoreOwner { } return viewModelStoreOwner } -*/ - - @Composable actual fun NavHost( @@ -56,30 +57,31 @@ actual fun NavHost( ) { //val lifecycleOwner = LocalLifecycleOwner.current - //val viewModelStoreOwner = LocalViewModelStoreOwner.current ?: rememberViewModelStoreOwner() + val viewModelStoreOwner = LocalViewModelStoreOwner.current ?: rememberViewModelStoreOwner() - //navController.setViewModelStore(viewModelStoreOwner.viewModelStore) + navController.setViewModelStore(viewModelStoreOwner.viewModelStore) // Then set the graph navController.graph = graph + // This seems not needed here since it seems also related to animations and transitions. (comment added when adapting from Compose UI `NavHost`) + /* // Find the ComposeNavigator, returning early if it isn't found // (such as is the case when using TestNavHostController) - val composeNavigator = navController.navigatorProvider.get>( - ComposeNavigator.NAME - ) as? ComposeNavigator ?: return + val composeNavigator = + navController.navigatorProvider.get>(ComposeNavigator.NAME) + as? ComposeNavigator ?: return + */ //val currentBackStack by composeNavigator.backStack.collectAsState() -// BackHandler(currentBackStack.size > 1) { -// navController.popBackStack() -// } + // `progress`, `isPredictiveBack`, etc. (comment added when adapting from Compose UI `NavHost`) /* DisposableEffect(lifecycleOwner) { // Setup the navController with proper owners navController.setLifecycleOwner(lifecycleOwner) - onDispose { } + onDispose {} } */ @@ -99,6 +101,14 @@ actual fun NavHost( val backStackEntry: NavBackStackEntry? = visibleEntries.lastOrNull() if (backStackEntry != null) { + /* + DisposableEffect(true) { + onDispose { + visibleEntries.forEach { entry -> composeNavigator.onTransitionComplete(entry) } + } + } + */ + // `fillMaxSize` is added here to make the Box align to the size of its parent // TODO consider adding a version of `NavHost` without `modifier` and `contentAlignment` // Originally it was `transition.AnimatedContent` here. @@ -109,8 +119,9 @@ actual fun NavHost( // while in the scope of the composable, we provide the navBackStackEntry as the // ViewModelStoreOwner and LifecycleOwner currentEntry?.LocalOwnersProvider(saveableStateHolder) { - (currentEntry.destination as ComposeNavigator.Destination) - .content( currentEntry) + (currentEntry.destination as ComposeNavigator.Destination).content( + currentEntry + ) } */ currentEntry?.let { @@ -119,19 +130,19 @@ actual fun NavHost( } } + /* DisposableEffect(true) { onDispose { - visibleEntries.forEach { entry -> - composeNavigator.onTransitionComplete(entry) - } + visibleEntries.forEach { entry -> composeNavigator.onTransitionComplete(entry) } } } + */ } /* - val dialogNavigator = navController.navigatorProvider.get>( - DialogNavigator.NAME - ) as? DialogNavigator ?: return + val dialogNavigator = + navController.navigatorProvider.get>(DialogNavigator.NAME) + as? DialogNavigator ?: return // Show any dialog destinations DialogHost(dialogNavigator)