Skip to content

Commit 4d1c12a

Browse files
authored
Merge pull request #55 from huanshankeji/save-viewmodel-in-navigation-on-js-dom
Save the ViewModel states on JS DOM, especially during navigation
2 parents 227473b + 1f1e30b commit 4d1c12a

File tree

19 files changed

+344
-42
lines changed

19 files changed

+344
-42
lines changed

README.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -144,15 +144,11 @@ The `com.huanshankeji.compose.material.icons.Icon` class delegates to both kinds
144144

145145
### ViewModel
146146

147-
The ViewModel module currently supports a small subset of the Compose ViewModel APIs, and delegates to raw UI state on
148-
Compose HTML / JS DOM. These APIs are highly experimental now.
147+
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.
149148

150149
### Navigation
151150

152-
The navigation module currently supports a small subset of the Compose Navigation APIs, which does not support
153-
transition or animation on Compose HTML / JS DOM. These APIs are also highly experimental now.
154-
See [CMP-4966](https://youtrack.jetbrains.com/issue/CMP-4966) for a bug to avoid. Also, ViewModel-related functions
155-
are not implemented yet on Compose HTML / JS DOM.
151+
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.
156152

157153
## Add to your dependencies
158154

buildSrc/src/main/kotlin/VersionsAndDependencies.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import com.huanshankeji.CommonDependencies
22
import org.jetbrains.compose.ComposeBuildConfig
33

4-
val projectVersion = "0.5.1-SNAPSHOT"
4+
val projectVersion = "0.6.0-SNAPSHOT"
55

66
val commonDependencies = CommonDependencies()
77

common/api/compose-multiplatform-html-unified-common.klib.api

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -992,6 +992,14 @@ final enum class com.huanshankeji.browser/Browser : kotlin/Enum<com.huanshankeji
992992
final fun values(): kotlin/Array<com.huanshankeji.browser/Browser> // com.huanshankeji.browser/Browser.values|values#static(){}[0]
993993
}
994994

995+
// Targets: [js]
996+
final class com.huanshankeji.compose.ui.window/SimpleViewModelStoreOwner : androidx.lifecycle/ViewModelStoreOwner { // com.huanshankeji.compose.ui.window/SimpleViewModelStoreOwner|null[0]
997+
constructor <init>() // com.huanshankeji.compose.ui.window/SimpleViewModelStoreOwner.<init>|<init>(){}[0]
998+
999+
final val viewModelStore // com.huanshankeji.compose.ui.window/SimpleViewModelStoreOwner.viewModelStore|{}viewModelStore[0]
1000+
final fun <get-viewModelStore>(): androidx.lifecycle/ViewModelStore // com.huanshankeji.compose.ui.window/SimpleViewModelStoreOwner.viewModelStore.<get-viewModelStore>|<get-viewModelStore>(){}[0]
1001+
}
1002+
9951003
// Targets: [js]
9961004
final object com.huanshankeji.compose.foundation.lazy/LazyItemScope { // com.huanshankeji.compose.foundation.lazy/LazyItemScope|null[0]
9971005
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
10281036
final val com.huanshankeji.compose.foundation/imitateComposeUiLayoutVerticalScrollPlatformModifier // com.huanshankeji.compose.foundation/imitateComposeUiLayoutVerticalScrollPlatformModifier|{}imitateComposeUiLayoutVerticalScrollPlatformModifier[0]
10291037
final fun <get-imitateComposeUiLayoutVerticalScrollPlatformModifier>(): com.varabyte.kobweb.compose.ui/Modifier // com.huanshankeji.compose.foundation/imitateComposeUiLayoutVerticalScrollPlatformModifier.<get-imitateComposeUiLayoutVerticalScrollPlatformModifier>|<get-imitateComposeUiLayoutVerticalScrollPlatformModifier>(){}[0]
10301038

1039+
// Targets: [js]
1040+
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]
1041+
10311042
// Targets: [js]
10321043
final fun (androidx.compose.ui.unit/Dp).com.huanshankeji.compose.ui.unit/toPx(): org.jetbrains.compose.web.css/CSSSizeValue<org.jetbrains.compose.web.css/CSSUnit.px> // com.huanshankeji.compose.ui.unit/toPx|toPx@androidx.compose.ui.unit.Dp(){}[0]
10331044

@@ -1138,3 +1149,12 @@ final fun com.huanshankeji.compose.foundation/com_huanshankeji_compose_foundatio
11381149

11391150
// Targets: [js]
11401151
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]
1152+
1153+
// Targets: [js]
1154+
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]
1155+
1156+
// Targets: [js]
1157+
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]
1158+
1159+
// Targets: [js]
1160+
final fun com.huanshankeji.compose.ui.window/renderComposableInBodyWithViewModelStoreOwner(kotlin/Function3<org.jetbrains.compose.web.dom/DOMScope<org.w3c.dom/HTMLBodyElement>, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>): androidx.compose.runtime/Composition // com.huanshankeji.compose.ui.window/renderComposableInBodyWithViewModelStoreOwner|renderComposableInBodyWithViewModelStoreOwner(kotlin.Function3<org.jetbrains.compose.web.dom.DOMScope<org.w3c.dom.HTMLBodyElement>,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>){}[0]

common/build.gradle.kts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ kotlin {
5656
// see: https://github.com/varabyte/kobweb/blob/main/frontend/kobweb-compose/build.gradle.kts
5757
api("com.varabyte.kobweb:kobweb-compose:${DependencyVersions.kobweb}")
5858
implementation("com.huanshankeji:compose-html-common:${DependencyVersions.huanshankejiComposeHtml}")
59+
60+
/*
61+
The UI module depends on the lifecycle module to use `androidx.lifecycle.ViewModelStoreOwner`.
62+
See https://github.com/JetBrains/compose-multiplatform-core/blob/jb-main/compose/ui/ui/build.gradle#L87.
63+
This is actually only needed for JS DOM.
64+
*/
65+
implementation(commonDependencies.jetbrainsAndroidx.lifecycle.viewmodel())
5966
}
6067
}
6168
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.huanshankeji.compose.ui.platform
2+
3+
// copied and adapted from "DefaultViewModelOwnerStore.skiko.kt" in `androidx.compose.ui.platform`
4+
5+
import androidx.compose.runtime.Composable
6+
import androidx.compose.runtime.InternalComposeApi
7+
import androidx.compose.runtime.staticCompositionLocalOf
8+
import androidx.lifecycle.ViewModelStoreOwner
9+
10+
/**
11+
* Internal helper to provide [ViewModelStoreOwner] from Compose UI module.
12+
* In applications please use [androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner].
13+
*
14+
* @hide
15+
*/
16+
internal val LocalInternalViewModelStoreOwner = staticCompositionLocalOf<ViewModelStoreOwner?> {
17+
null
18+
}
19+
20+
@InternalComposeApi
21+
@Composable
22+
fun findComposeDefaultViewModelStoreOwner(): ViewModelStoreOwner? =
23+
LocalInternalViewModelStoreOwner.current
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.huanshankeji.compose.ui.window
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.Composition
5+
import androidx.compose.runtime.CompositionLocalProvider
6+
import androidx.lifecycle.ViewModelStore
7+
import androidx.lifecycle.ViewModelStoreOwner
8+
import com.huanshankeji.compose.ExperimentalApi
9+
import com.huanshankeji.compose.ui.platform.LocalInternalViewModelStoreOwner
10+
import org.jetbrains.compose.web.dom.DOMScope
11+
import org.jetbrains.compose.web.renderComposableInBody
12+
import org.w3c.dom.HTMLBodyElement
13+
14+
@ExperimentalApi
15+
class SimpleViewModelStoreOwner : ViewModelStoreOwner {
16+
override val viewModelStore: ViewModelStore = ViewModelStore()
17+
}
18+
19+
fun renderComposableInBodyWithViewModelStoreOwner(
20+
content: @Composable DOMScope<HTMLBodyElement>.() -> Unit
21+
): Composition =
22+
renderComposableInBody {
23+
// copied and adapted from `ComposeWindow` in "ComposeWindow.web.kt" in `androidx.compose.ui.window`
24+
// also see `ComposeViewport` on Wasm JS
25+
@OptIn(ExperimentalApi::class)
26+
CompositionLocalProvider(
27+
/* TODO add back these 2 lines below if needed one day
28+
in a function possibly named `renderComposableInBodyWithLifecycle` */
29+
//LocalSystemTheme provides systemThemeObserver.currentSystemTheme.value,
30+
//LocalLifecycleOwner provides this,
31+
LocalInternalViewModelStoreOwner provides SimpleViewModelStoreOwner(),
32+
content = {
33+
content()
34+
}
35+
)
36+
}

demo/src/commonMain/kotlin/com/huanshankeji/compose/material/demo/Material3.kt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.huanshankeji.compose.material.demo
22

33
import androidx.compose.runtime.*
44
import androidx.compose.ui.unit.dp
5+
import com.huanshankeji.androidx.lifecycle.viewmodel.compose.viewModel
56
import com.huanshankeji.compose.ExtRecommendedApi
67
import com.huanshankeji.compose.foundation.layout.*
78
import com.huanshankeji.compose.foundation.rememberScrollState
@@ -27,10 +28,12 @@ import com.huanshankeji.compose.ui.Modifier
2728
import com.huanshankeji.compose.material3.Button as RowScopeButton
2829

2930
@Composable
30-
fun Material3(/*modifier: Modifier = Modifier*/) {
31+
fun Material3(/*modifier: Modifier = Modifier*/
32+
viewModel: Material3ViewModel = viewModel { Material3ViewModel() }
33+
) {
3134
Column(Modifier.verticalScroll(rememberScrollState()).innerContentPadding(), Arrangement.spacedBy(16.dp)) {
32-
var count by remember { mutableStateOf(0) }
33-
val onClick: () -> Unit = { count++ }
35+
val count by viewModel.countState.collectAsState()
36+
val onClick: () -> Unit = { viewModel.countState.value++ }
3437
val buttonContent: @Composable () -> Unit = {
3538
TaglessText(count.toString())
3639
}
@@ -56,7 +59,8 @@ fun Material3(/*modifier: Modifier = Modifier*/) {
5659
FilledTonalIconButton(onClick, content = iconButtonContent)
5760
OutlinedIconButton(onClick, content = iconButtonContent)
5861
}
59-
val (checked, onCheckedChange) = remember { mutableStateOf(false) }
62+
val checked = viewModel.checkedState.collectAsState().value
63+
val onCheckedChange: (Boolean) -> Unit = { viewModel.checkedState.value = it }
6064
val iconToggleButtonContent: @Composable () -> Unit = {
6165
Icon(if (checked) Icons.Default.Add else Icons.Default.Remove, null)
6266
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.huanshankeji.compose.material.demo
2+
3+
import androidx.lifecycle.ViewModel
4+
import kotlinx.coroutines.flow.MutableStateFlow
5+
6+
class Material3ViewModel : ViewModel() {
7+
val countState = MutableStateFlow(0)
8+
val checkedState = MutableStateFlow(false)
9+
}
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package com.huanshankeji.compose.material.demo
22

33
import com.huanshankeji.compose.html.material3.require
4-
import org.jetbrains.compose.web.renderComposableInBody
4+
import com.huanshankeji.compose.ui.window.renderComposableInBodyWithViewModelStoreOwner
55

66
fun main() {
77
require("material-symbols/outlined.css")
8-
renderComposableInBody { App() }
8+
//renderComposableInBody { App() } // "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
9+
renderComposableInBodyWithViewModelStoreOwner { App() }
910
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
public final class com/huanshankeji/androidx/lifecycle/viewmodel/compose/ViewModelKt {
2+
public static final fun defaultCreationExtras (Landroidx/lifecycle/ViewModelStoreOwner;)Landroidx/lifecycle/viewmodel/CreationExtras;
3+
}
4+
5+
public final class com/huanshankeji/androidx/lifecycle/viewmodel/compose/ViewModel_composeUiKt {
6+
public static final fun defaultViewModelStoreOwner (Landroidx/compose/runtime/Composer;I)Landroidx/lifecycle/ViewModelStoreOwner;
7+
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;
8+
}
9+

lifecycle-viewmodel/api/compose-multiplatform-html-unified-lifecycle-viewmodel.klib.api

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,22 @@
66
// - Show declarations: true
77

88
// Library unique name: <com.huanshankeji:compose-multiplatform-html-unified-lifecycle-viewmodel>
9+
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]
10+
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§<androidx.lifecycle.ViewModel>}[0]
11+
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]
12+
final inline fun <#A: reified androidx.lifecycle/ViewModel> com.huanshankeji.androidx.lifecycle.viewmodel.compose/viewModel(androidx.lifecycle/ViewModelStoreOwner?, kotlin/String?, noinline kotlin/Function1<androidx.lifecycle.viewmodel/CreationExtras, #A>, 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.lifecycle.viewmodel.CreationExtras,0:0>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){0§<androidx.lifecycle.ViewModel>}[0]
913
final inline fun <#A: reified androidx.lifecycle/ViewModel> com.huanshankeji.androidx.lifecycle.viewmodel.compose/viewModel(kotlin/String?, noinline kotlin/Function1<androidx.lifecycle.viewmodel/CreationExtras, #A>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int): #A // com.huanshankeji.androidx.lifecycle.viewmodel.compose/viewModel|viewModel(kotlin.String?;kotlin.Function1<androidx.lifecycle.viewmodel.CreationExtras,0:0>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){0§<androidx.lifecycle.ViewModel>}[0]
14+
15+
// Targets: [js]
16+
final object com.huanshankeji.androidx.lifecycle.viewmodel.compose/LocalViewModelStoreOwner { // com.huanshankeji.androidx.lifecycle.viewmodel.compose/LocalViewModelStoreOwner|null[0]
17+
final val current // com.huanshankeji.androidx.lifecycle.viewmodel.compose/LocalViewModelStoreOwner.current|{}current[0]
18+
final fun <get-current>(androidx.compose.runtime/Composer?, kotlin/Int): androidx.lifecycle/ViewModelStoreOwner? // com.huanshankeji.androidx.lifecycle.viewmodel.compose/LocalViewModelStoreOwner.current.<get-current>|<get-current>(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
19+
20+
final fun provides(androidx.lifecycle/ViewModelStoreOwner): androidx.compose.runtime/ProvidedValue<androidx.lifecycle/ViewModelStoreOwner?> // com.huanshankeji.androidx.lifecycle.viewmodel.compose/LocalViewModelStoreOwner.provides|provides(androidx.lifecycle.ViewModelStoreOwner){}[0]
21+
}
22+
23+
// Targets: [js]
24+
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]
25+
26+
// Targets: [js]
27+
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]
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
public final class com/huanshankeji/androidx/lifecycle/viewmodel/compose/ViewModelKt {
2+
public static final fun defaultCreationExtras (Landroidx/lifecycle/ViewModelStoreOwner;)Landroidx/lifecycle/viewmodel/CreationExtras;
3+
}
4+
5+
public final class com/huanshankeji/androidx/lifecycle/viewmodel/compose/ViewModel_composeUiKt {
6+
public static final fun defaultViewModelStoreOwner (Landroidx/compose/runtime/Composer;I)Landroidx/lifecycle/ViewModelStoreOwner;
7+
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;
8+
}
9+

lifecycle-viewmodel/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import com.huanshankeji.cpnProject
12
import com.huanshankeji.team.`Shreck Ye`
23
import com.huanshankeji.team.pomForTeamDefaultOpenSource
34

@@ -16,6 +17,9 @@ kotlin {
1617
*/
1718
api(compose.runtime)
1819
api(commonDependencies.jetbrainsAndroidx.lifecycle.viewmodel())
20+
// only needed on JS DOM actually
21+
// https://github.com/JetBrains/compose-multiplatform-core/blob/f1e03d0784631a88201931a6a6a708cdd090be57/lifecycle/lifecycle-viewmodel-compose/build.gradle#L58
22+
api(cpnProject(project, ":common"))
1923
}
2024
}
2125
composeUiMain {

0 commit comments

Comments
 (0)