Skip to content

Commit

Permalink
store MultiStackNavigationExecutor in ViewModel, add SavedStateSuppor…
Browse files Browse the repository at this point in the history
…t for non-Android platforms (#22)

* init simple sample

* baseName = "SolivagantSimpleSampleAppShared"

* simplify gradle

* simplify gradle

* rememberSaveable

* rememberSaveable

* rememberSaveable

* rememberSaveable

* rememberSaveable

* create MultiStackNavigationExecutor in ViewModel

* create MultiStackNavigationExecutor in ViewModel

* create MultiStackNavigationExecutor in ViewModel

* create MultiStackNavigationExecutor in ViewModel

* create MultiStackNavigationExecutor in ViewModel

* create MultiStackNavigationExecutor in ViewModel

* done ios sample
  • Loading branch information
hoc081098 authored Feb 1, 2024
1 parent b541673 commit 1c93d29
Show file tree
Hide file tree
Showing 20 changed files with 422 additions and 76 deletions.
12 changes: 12 additions & 0 deletions navigation/api/jvm/navigation.api
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@ public final class com/hoc081098/solivagant/navigation/NavHostKt {
public static final fun NavHost (Lcom/hoc081098/solivagant/navigation/NavRoot;Lkotlinx/collections/immutable/ImmutableSet;Landroidx/compose/ui/Modifier;Lcom/hoc081098/solivagant/navigation/NavEventNavigator;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V
}

public final class com/hoc081098/solivagant/navigation/SavedStateSupport : androidx/compose/runtime/saveable/SaveableStateRegistry, com/hoc081098/kmp/viewmodel/SavedStateHandleFactory, com/hoc081098/kmp/viewmodel/ViewModelStoreOwner {
public static final field $stable I
public fun <init> ()V
public fun canBeSaved (Ljava/lang/Object;)Z
public final fun clear ()V
public fun consumeRestored (Ljava/lang/String;)Ljava/lang/Object;
public fun create ()Lcom/hoc081098/kmp/viewmodel/SavedStateHandle;
public fun getViewModelStore ()Lcom/hoc081098/kmp/viewmodel/ViewModelStore;
public fun performSave ()Ljava/util/Map;
public fun registerProvider (Ljava/lang/String;Lkotlin/jvm/functions/Function0;)Landroidx/compose/runtime/saveable/SaveableStateRegistry$Entry;
}

public final class com/hoc081098/solivagant/navigation/internal/RememberPlatformLifecycleOwner_jvmKt {
public static final fun LifecycleControllerEffect (Lcom/hoc081098/solivagant/lifecycle/LifecycleRegistry;Landroidx/compose/ui/window/WindowState;Landroidx/compose/runtime/Composer;I)V
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import android.os.Bundle
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle

internal actual fun SavedStateHandle.setSavedStateProvider(
internal actual fun SavedStateHandle.setSavedStateProviderWithMap(
key: String,
savedStateFactory: () -> Map<String, Any?>,
) = setSavedStateProvider(key) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,6 @@ internal class MultiStackNavigationExecutor(
get() = stack.canNavigateBack

init {
viewModel
.globalSavedStateHandle
.setSavedStateProvider(SAVED_STATE_STACK, stack::saveState)

_lifecycleOwner
.flatMapLatest { it?.lifecycle?.eventFlow ?: emptyFlow() }
.onEach(stack::handleLifecycleEvent)
Expand Down Expand Up @@ -117,10 +113,7 @@ internal class MultiStackNavigationExecutor(
_lifecycleOwner.value = lifecycleOwner
}

internal companion object {
const val SAVED_STATE_STACK = "com.hoc081098.solivagant.navigation.stack"
}

//region RememberObserver
override fun onAbandoned() {
_lifecycleOwner.value = null
scope.cancel()
Expand All @@ -132,4 +125,5 @@ internal class MultiStackNavigationExecutor(
}

override fun onRemembered() = Unit
//endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,41 +23,16 @@ internal fun rememberNavigationExecutor(
},
),
): MultiStackNavigationExecutor {
val lifecycleOwner = LocalLifecycleOwner.current

// setInputStartRoot must be called before getMultiStackNavigationExecutor
viewModel.setInputStartRoot(startRoot)

val lifecycleOwner = LocalLifecycleOwner.current
val executor = remember(viewModel) {
val contentDestinations = destinations.filterIsInstance<ContentDestination<*>>()

val navState = viewModel.getSavedStackState()
val hostLifecycleState = lifecycleOwner.lifecycle.currentState

val stack = if (navState == null) {
MultiStack.createWith(
root = viewModel.savedNavRoot!!,
destinations = contentDestinations,
hostLifecycleState = hostLifecycleState,
onStackEntryRemoved = viewModel::removeEntry,
)
} else {
MultiStack.fromState(
root = viewModel.savedNavRoot!!,
bundle = navState,
destinations = contentDestinations,
hostLifecycleState = hostLifecycleState,
onStackEntryRemoved = viewModel::removeEntry,
)
}

@Suppress("ViewModelForwarding")
MultiStackNavigationExecutor(
stack = stack,
viewModel = viewModel,
onRootChanged = viewModel::setStartRoot,
viewModel.createMultiStackNavigationExecutor(
contentDestinations = destinations.filterIsInstance<ContentDestination<*>>(),
hostLifecycleState = lifecycleOwner.lifecycle.currentState,
)
}

executor.setLifecycleOwner(lifecycleOwner)

return executor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ package com.hoc081098.solivagant.navigation.internal
import com.hoc081098.kmp.viewmodel.SavedStateHandle
import com.hoc081098.kmp.viewmodel.SavedStateHandleFactory
import com.hoc081098.kmp.viewmodel.ViewModel
import com.hoc081098.solivagant.lifecycle.Lifecycle
import com.hoc081098.solivagant.navigation.BaseRoute
import com.hoc081098.solivagant.navigation.ContentDestination
import com.hoc081098.solivagant.navigation.EXTRA_ROUTE
import com.hoc081098.solivagant.navigation.NavRoot
import com.hoc081098.solivagant.navigation.internal.MultiStackNavigationExecutor.Companion.SAVED_STATE_STACK

internal class StoreViewModel(
internal val globalSavedStateHandle: SavedStateHandle,
Expand All @@ -15,7 +16,14 @@ internal class StoreViewModel(
private val savedStateHandles = mutableMapOf<StackEntry.Id, SavedStateHandle>()
private val savedStateHandleFactories = mutableMapOf<StackEntry.Id, SavedStateHandleFactory>()

internal val savedNavRoot: NavRoot? get() = globalSavedStateHandle[SAVED_START_ROOT_KEY]
private var stack: MultiStack? = null
private var executor: MultiStackNavigationExecutor? = null

init {
globalSavedStateHandle.setSavedStateProviderWithMap(SAVED_STATE_STACK) {
checkNotNull(stack) { "Stack is null. This should never happen" }.saveState()
}
}

internal fun provideStore(id: StackEntry.Id): NavigationExecutor.Store {
return stores.getOrPut(id) { NavigationExecutorStore() }
Expand All @@ -34,7 +42,7 @@ internal class StoreViewModel(
}
}

internal fun removeEntry(id: StackEntry.Id) {
private fun removeEntry(id: StackEntry.Id) {
val store = stores.remove(id)
store?.close()

Expand Down Expand Up @@ -82,14 +90,49 @@ internal class StoreViewModel(
}
}

internal fun setStartRoot(root: NavRoot) {
private fun setStartRoot(root: NavRoot) {
globalSavedStateHandle[SAVED_START_ROOT_KEY] = root
}

internal fun getSavedStackState(): Map<String, Any?>? =
globalSavedStateHandle.getAsMap(SAVED_STATE_STACK)
fun createMultiStackNavigationExecutor(
contentDestinations: List<ContentDestination<*>>,
hostLifecycleState: Lifecycle.State,
): MultiStackNavigationExecutor =
executor
?: MultiStackNavigationExecutor(
stack = createMultiStackIfNeeded(contentDestinations, hostLifecycleState),
viewModel = this,
onRootChanged = ::setStartRoot,
).also { this.executor = it }

private fun createMultiStackIfNeeded(
contentDestinations: List<ContentDestination<*>>,
hostLifecycleState: Lifecycle.State,
): MultiStack {
this.stack?.let { return it }

val navState = globalSavedStateHandle.getAsMap(SAVED_STATE_STACK)
val savedNavRoot: NavRoot? = globalSavedStateHandle[SAVED_START_ROOT_KEY]
return if (navState == null) {
MultiStack.createWith(
root = savedNavRoot!!,
destinations = contentDestinations,
hostLifecycleState = hostLifecycleState,
onStackEntryRemoved = ::removeEntry,
)
} else {
MultiStack.fromState(
root = savedNavRoot!!,
bundle = navState,
destinations = contentDestinations,
hostLifecycleState = hostLifecycleState,
onStackEntryRemoved = ::removeEntry,
)
}.also { this.stack = it }
}

private companion object {
private const val SAVED_STATE_STACK = "com.hoc081098.solivagant.navigation.stack"
private const val SAVED_START_ROOT_KEY = "com.hoc081098.solivagant.navigation.store.start_root"
private const val SAVED_INPUT_START_ROOT_KEY = "com.hoc081098.solivagant.navigation.store.input_start_root"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.hoc081098.solivagant.navigation.internal

import com.hoc081098.kmp.viewmodel.SavedStateHandle

internal expect fun SavedStateHandle.setSavedStateProvider(
internal expect fun SavedStateHandle.setSavedStateProviderWithMap(
key: String,
savedStateFactory: () -> Map<String, Any?>,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.hoc081098.solivagant.navigation

import com.hoc081098.solivagant.lifecycle.LifecycleOwner
import com.hoc081098.solivagant.navigation.internal.AppLifecycleOwnerImpl

@Suppress("FunctionName") // Factory function
public fun AppLifecycleOwner(): LifecycleOwner = AppLifecycleOwnerImpl()
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import platform.UIKit.UIApplicationWillResignActiveNotification
import platform.UIKit.UIApplicationWillTerminateNotification
import platform.darwin.NSObjectProtocol

private class AppLifecycleOwnerImpl : LifecycleOwner {
internal class AppLifecycleOwnerImpl : LifecycleOwner {
private val lifecycleRegistry = LifecycleRegistry()
override val lifecycle: Lifecycle get() = lifecycleRegistry

Expand Down Expand Up @@ -221,9 +221,6 @@ private fun moveToDestroyed(lifecycleRegistry: LifecycleRegistry) {
}
}

@Suppress("FunctionName") // Factory function
public fun AppLifecycleOwner(): LifecycleOwner = AppLifecycleOwnerImpl()

@Composable
internal actual fun rememberPlatformLifecycleOwner(): LifecycleOwner {
return remember { AppLifecycleOwnerImpl() }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.hoc081098.solivagant.navigation

import androidx.compose.runtime.saveable.SaveableStateRegistry
import com.hoc081098.kmp.viewmodel.MainThread
import com.hoc081098.kmp.viewmodel.SavedStateHandle
import com.hoc081098.kmp.viewmodel.SavedStateHandleFactory
import com.hoc081098.kmp.viewmodel.ViewModelStore
import com.hoc081098.kmp.viewmodel.ViewModelStoreOwner
import kotlin.LazyThreadSafetyMode.NONE

@MainThread
public class SavedStateSupport :
SavedStateHandleFactory,
SaveableStateRegistry,
ViewModelStoreOwner {
private var registry: SaveableStateRegistry? = SaveableStateRegistry(
restoredValues = emptyMap(),
canBeSaved = { true },
)

private val savedStateHandle by lazy(NONE) { SavedStateHandle() }
private val viewModelStoreLazy = lazy(NONE) { ViewModelStore() }
override val viewModelStore: ViewModelStore by viewModelStoreLazy

public fun clear() {
if (registry == null) {
// Already cleared
return
}

registry = null
if (viewModelStoreLazy.isInitialized()) {
viewModelStore.clear()
}
}

override fun create(): SavedStateHandle = savedStateHandle

override fun canBeSaved(value: Any): Boolean = registry?.canBeSaved(value) == true

override fun consumeRestored(key: String): Any? = registry?.consumeRestored(key)

override fun performSave(): Map<String, List<Any?>> = registry
?.performSave()
?.also {
registry = SaveableStateRegistry(
restoredValues = it,
canBeSaved = { true },
)
}
.orEmpty()

override fun registerProvider(key: String, valueProvider: () -> Any?): SaveableStateRegistry.Entry =
registry
?.registerProvider(key, valueProvider)
?: NoOpSaveableStateRegistryEntry
}

private val NoOpSaveableStateRegistryEntry = object : SaveableStateRegistry.Entry {
override fun unregister() {
// No-op
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.hoc081098.solivagant.navigation.internal

import com.hoc081098.kmp.viewmodel.SavedStateHandle

internal actual fun SavedStateHandle.setSavedStateProvider(
internal actual fun SavedStateHandle.setSavedStateProviderWithMap(
key: String,
savedStateFactory: () -> Map<String, Any?>,
) {
Expand Down
1 change: 0 additions & 1 deletion obsolete/navigation-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -170,4 +170,3 @@ android {
targetCompatibility = JavaVersion.toVersion(libs.versions.java.target.get())
}
}

1 change: 1 addition & 0 deletions samples/sample/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,5 @@ android {
dependencies {
implementation(project(":samples:sample:shared"))
implementation(libs.androidx.activity.compose)
implementation(libs.koin.android)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ package com.hoc081098.solivagant.sample

import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.LocalSaveableStateRegistry
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import com.hoc081098.kmp.viewmodel.compose.LocalSavedStateHandleFactory
import com.hoc081098.kmp.viewmodel.compose.LocalViewModelStoreOwner
import com.hoc081098.solivagant.lifecycle.LifecycleOwnerProvider
import com.hoc081098.solivagant.lifecycle.LifecycleRegistry
import com.hoc081098.solivagant.lifecycle.rememberLifecycleOwner
import com.hoc081098.solivagant.navigation.SavedStateSupport
import com.hoc081098.solivagant.navigation.internal.LifecycleControllerEffect
import com.hoc081098.solivagant.sample.common.OnLifecycleEventWithBuilder
import io.github.aakira.napier.Napier
Expand All @@ -19,14 +25,19 @@ fun main() {

application {
val windowState = rememberWindowState()

val lifecycleRegistry = remember { LifecycleRegistry() }
val lifecycleOwner = rememberLifecycleOwner(lifecycleRegistry)

LifecycleControllerEffect(
lifecycleRegistry = lifecycleRegistry,
windowState = windowState,
)

val savedStateSupport = remember { SavedStateSupport() }
DisposableEffect(Unit) {
onDispose(savedStateSupport::clear)
}

Window(
onCloseRequest = ::exitApplication,
title = "Solivagant sample",
Expand All @@ -37,7 +48,20 @@ fun main() {
onEach { Napier.d(message = "Lifecycle event: $it", tag = "[main]") }
}

SolivagantSampleApp()
CompositionLocalProvider(
LocalViewModelStoreOwner provides savedStateSupport,
LocalSaveableStateRegistry provides savedStateSupport,
LocalSavedStateHandleFactory provides savedStateSupport,
) {
SolivagantSampleApp()

// Must be at the last,
// because onDispose is called in reverse order, so we want to save state first,
// before [SaveableStateRegistry.Entry]s are unregistered.
DisposableEffect(Unit) {
onDispose(savedStateSupport::performSave)
}
}
}
}
}
Expand Down
Loading

0 comments on commit 1c93d29

Please sign in to comment.