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

store MultiStackNavigationExecutor in ViewModel, add SavedStateSupport for non-Android platforms #22

Merged
merged 16 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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
Loading