Skip to content

Commit

Permalink
Merge pull request #55 from huanshankeji/save-viewmodel-in-navigation…
Browse files Browse the repository at this point in the history
…-on-js-dom

Save the ViewModel states on JS DOM, especially during navigation
  • Loading branch information
ShreckYe authored Jan 8, 2025
2 parents 227473b + 1f1e30b commit 4d1c12a
Show file tree
Hide file tree
Showing 19 changed files with 344 additions and 42 deletions.
8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/VersionsAndDependencies.kt
Original file line number Diff line number Diff line change
@@ -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()

Expand Down
20 changes: 20 additions & 0 deletions common/api/compose-multiplatform-html-unified-common.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -992,6 +992,14 @@ final enum class com.huanshankeji.browser/Browser : kotlin/Enum<com.huanshankeji
final fun values(): kotlin/Array<com.huanshankeji.browser/Browser> // 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 <init>() // com.huanshankeji.compose.ui.window/SimpleViewModelStoreOwner.<init>|<init>(){}[0]

final val viewModelStore // com.huanshankeji.compose.ui.window/SimpleViewModelStoreOwner.viewModelStore|{}viewModelStore[0]
final fun <get-viewModelStore>(): androidx.lifecycle/ViewModelStore // com.huanshankeji.compose.ui.window/SimpleViewModelStoreOwner.viewModelStore.<get-viewModelStore>|<get-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]
Expand Down Expand Up @@ -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 <get-imitateComposeUiLayoutVerticalScrollPlatformModifier>(): com.varabyte.kobweb.compose.ui/Modifier // com.huanshankeji.compose.foundation/imitateComposeUiLayoutVerticalScrollPlatformModifier.<get-imitateComposeUiLayoutVerticalScrollPlatformModifier>|<get-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<org.jetbrains.compose.web.css/CSSUnit.px> // com.huanshankeji.compose.ui.unit/toPx|toPx@androidx.compose.ui.unit.Dp(){}[0]

Expand Down Expand Up @@ -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<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]
7 changes: 7 additions & 0 deletions common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ViewModelStoreOwner?> {
null
}

@InternalComposeApi
@Composable
fun findComposeDefaultViewModelStoreOwner(): ViewModelStoreOwner? =
LocalInternalViewModelStoreOwner.current
Original file line number Diff line number Diff line change
@@ -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<HTMLBodyElement>.() -> 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()
}
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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())
}
Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -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() }
}
Original file line number Diff line number Diff line change
@@ -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;
}

Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,22 @@
// - Show declarations: true

// Library unique name: <com.huanshankeji:compose-multiplatform-html-unified-lifecycle-viewmodel>
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§<androidx.lifecycle.ViewModel>}[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.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]
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]

// 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 <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]

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]
}

// 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]
Original file line number Diff line number Diff line change
@@ -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;
}

4 changes: 4 additions & 0 deletions lifecycle-viewmodel/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import com.huanshankeji.cpnProject
import com.huanshankeji.team.`Shreck Ye`
import com.huanshankeji.team.pomForTeamDefaultOpenSource

Expand All @@ -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 {
Expand Down
Loading

0 comments on commit 4d1c12a

Please sign in to comment.