Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Save the ViewModel states on JS DOM, especially during navigation #55

Merged
merged 16 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,15 +143,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|[email protected](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|[email protected](){}[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()
}
)
}
1 change: 1 addition & 0 deletions demo/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ kotlin {
implementation(compose.runtime)
implementation(cpnProject(project, ":material2"))
implementation(cpnProject(project, ":material3"))
implementation(cpnProject(project, ":lifecycle-viewmodel"))
ShreckYe marked this conversation as resolved.
Show resolved Hide resolved
implementation(cpnProject(project, ":navigation"))
/*
see https://github.com/JetBrains/compose-multiplatform-core/blob/476d43b99a27696d12ef087e8028d90789645ba7/compose/ui/ui/build.gradle#L54
Expand Down
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|[email protected](){}[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
Loading