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

Sample #9

Merged
merged 9 commits into from
Jan 6, 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
2 changes: 1 addition & 1 deletion .idea/kotlinc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,23 @@ sample-android-target = "34"
sample-android-compile = "34"

jetbrains-compose = "1.5.11"
jetbrains-compose-compiler = "1.5.7.1"

touchlab-stately = "1.2.5"
napier = "2.6.1"
flowExt = "0.7.4"
koin = "3.5.0"
koin-compose = "1.1.0"
koin-androidx-compose = "3.5.0"
coil = "2.5.0"
compose-rules-detekt = "0.3.8"

androidx-lifecycle = "2.6.2"
androidx-annotation = "1.7.1"
androidx-activity = "1.8.2"
androidx-appcompat = "1.6.1"
androidx-compose-compiler = "1.5.6"
androidx-core-ktx = "1.12.0"
androidx-navigation = "2.7.6"
android-gradle = "8.2.0"

Expand All @@ -57,6 +61,7 @@ ktlint = "0.50.0"
[libraries]
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" }
coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
coroutines-jdk8 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8", version.ref = "coroutines" }
junit = { module = "junit:junit", version.ref = "junit" }
Expand All @@ -68,6 +73,7 @@ touchlab-stately-concurrency = { module = "co.touchlab:stately-concurrency", ver
napier = { module = "io.github.aakira:napier", version.ref = "napier" }
flowExt = { module = "io.github.hoc081098:FlowExt", version.ref = "flowExt" }
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin-compose" }
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin-androidx-compose" }
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
Expand All @@ -79,6 +85,7 @@ androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-ru
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version = "androidx-lifecycle" }
androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
androidx-compose-bom = { module = "androidx.compose:compose-bom", version = "2023.10.01" }
androidx-compose-ui-ui = { module = "androidx.compose.ui:ui" }
androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
Expand All @@ -87,6 +94,7 @@ androidx-compose-foundation = { module = "androidx.compose.foundation:foundation
androidx-compose-material = { module = "androidx.compose.material:material" }
androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" }
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core-ktx" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" }

kmp-viewmodel-core = { module = "io.github.hoc081098:kmp-viewmodel", version.ref = "kmp-viewmodel" }
Expand Down
2 changes: 1 addition & 1 deletion khonshu-navigation-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ plugins {
}

compose {
kotlinCompilerPlugin.set("1.5.7")
kotlinCompilerPlugin.set(libs.versions.jetbrains.compose.compiler)
}

kotlin {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.hoc081098.solivagant.navigation.internal

import com.hoc081098.kmp.viewmodel.SavedStateHandle
import com.hoc081098.kmp.viewmodel.SavedStateHandleFactory
import com.hoc081098.solivagant.navigation.BaseRoute
import com.hoc081098.solivagant.navigation.Navigator
import com.hoc081098.solivagant.navigation.Serializable
Expand All @@ -10,6 +11,7 @@ import kotlin.reflect.KClass
public interface NavigationExecutor : Navigator {
public fun <T : BaseRoute> routeFor(destinationId: DestinationId<T>): T
public fun <T : BaseRoute> savedStateHandleFor(destinationId: DestinationId<T>): SavedStateHandle
public fun <T : BaseRoute> savedStateHandleFactoryFor(destinationId: DestinationId<T>): SavedStateHandleFactory
public fun <T : BaseRoute> storeFor(destinationId: DestinationId<T>): Store
public fun <T : BaseRoute> extra(destinationId: DestinationId<T>): Serializable

Expand Down
2 changes: 1 addition & 1 deletion navigation-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ plugins {
}

compose {
kotlinCompilerPlugin.set("1.5.7")
kotlinCompilerPlugin.set(libs.versions.jetbrains.compose.compiler)
}

kotlin {
Expand Down
2 changes: 1 addition & 1 deletion navigation/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ plugins {
}

compose {
kotlinCompilerPlugin.set("1.5.7")
kotlinCompilerPlugin.set(libs.versions.jetbrains.compose.compiler)
}

kotlin {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.hoc081098.solivagant.navigation.internal

import com.hoc081098.kmp.viewmodel.InternalKmpViewModelApi
import com.hoc081098.kmp.viewmodel.ViewModelStore

@OptIn(InternalKmpViewModelApi::class)
internal actual fun createViewModelStore(): ViewModelStore =
ViewModelStore(androidx.lifecycle.ViewModelStore())
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,25 @@ internal actual fun SavedStateHandle.getAsMap(key: String): Map<String, Any?>? =
?.let { bundle ->
bundle
.keySet()
.associateWith {
@Suppress("DEPRECATION")
bundle.get(it)
}
.associateWith { bundle.safeGet(it) }
}

private fun Bundle.toMap(): Map<String, Any?> = keySet().associateWith { safeGet(it) }

private fun Bundle.safeGet(key: String): Any? {
@Suppress("DEPRECATION")
return when (val v = get(key)) {
is Bundle -> return v.toMap()
is ArrayList<*> -> {
when (v[0]) {
is Bundle -> v.mapTo(ArrayList(v.size)) { (it as Bundle).toMap() }
else -> v
}
}
else -> v
}
}

@SuppressLint("RestrictedApi")
internal actual fun createSavedStateHandleAndSetSavedStateProvider(
id: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,22 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.SaveableStateHolder
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.Modifier
import com.hoc081098.kmp.viewmodel.Closeable
import com.hoc081098.kmp.viewmodel.compose.SavedStateHandleFactoryProvider
import com.hoc081098.kmp.viewmodel.compose.ViewModelStoreOwnerProvider
import com.hoc081098.solivagant.navigation.internal.MultiStackNavigationExecutor
import com.hoc081098.solivagant.navigation.internal.OnBackPressedCallback
import com.hoc081098.solivagant.navigation.internal.StackEntry
import com.hoc081098.solivagant.navigation.internal.StackEntryViewModelStoreOwner
import com.hoc081098.solivagant.navigation.internal.WeakReference
import com.hoc081098.solivagant.navigation.internal.currentBackPressedDispatcher
import com.hoc081098.solivagant.navigation.internal.rememberNavigationExecutor
Expand Down Expand Up @@ -50,7 +57,11 @@ public fun NavHost(

Box(modifier = modifier) {
executor.visibleEntries.value.forEach { entry ->
Show(entry, executor, saveableStateHolder)
Show(
entry = entry,
executor = executor,
saveableStateHolder = saveableStateHolder,
)
}
}
}
Expand All @@ -67,22 +78,50 @@ private fun <T : BaseRoute> Show(
// it is available when the destination is cleared. Which, because of animations,
// only happens after this leaves composition. Which means we can't rely on
// DisposableEffect to clean up this reference (as it'll be cleaned up too early)
remember(entry, executor, saveableStateHolder) {
executor.storeFor(entry.id).getOrCreate(SaveableCloseable::class) {
SaveableCloseable(entry.id.value, WeakReference(saveableStateHolder))
}
val saveableCloseable = remember(entry, executor, saveableStateHolder) {
executor
.storeFor(entry.id)
.getOrCreate(SaveableCloseable::class) {
SaveableCloseable(
entry.id.value,
WeakReference(saveableStateHolder),
)
}
}

val viewModelStoreOwner = saveableCloseable
.viewModelStoreOwnerState
.value // <-- This will cause the recomposition when the value is cleared.
?: return

saveableStateHolder.SaveableStateProvider(entry.id.value) {
entry.destination.content(entry.route)
ViewModelStoreOwnerProvider(
viewModelStoreOwner = viewModelStoreOwner,
) {
SavedStateHandleFactoryProvider(
savedStateHandleFactory = executor.savedStateHandleFactoryFor(entry.destinationId),
) {
entry.destination.content(entry.route)
}
}
}
}

internal class SaveableCloseable(
private val id: String,
private val saveableStateHolderRef: WeakReference<SaveableStateHolder>,
) : Closeable {
private val _viewModelStoreOwnerState: MutableState<StackEntryViewModelStoreOwner?> =
mutableStateOf(StackEntryViewModelStoreOwner())

inline val viewModelStoreOwnerState: State<StackEntryViewModelStoreOwner?> get() = _viewModelStoreOwnerState

override fun close() {
Snapshot.withMutableSnapshot {
_viewModelStoreOwnerState.value?.clearIfInitialized()
_viewModelStoreOwnerState.value = null
}

saveableStateHolderRef.get()?.removeState(id)
saveableStateHolderRef.clear()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.hoc081098.solivagant.navigation.internal

import androidx.compose.runtime.State
import com.hoc081098.kmp.viewmodel.SavedStateHandle
import com.hoc081098.kmp.viewmodel.SavedStateHandleFactory
import com.hoc081098.solivagant.navigation.BaseRoute
import com.hoc081098.solivagant.navigation.NavRoot
import com.hoc081098.solivagant.navigation.NavRoute
Expand Down Expand Up @@ -64,7 +65,12 @@ internal class MultiStackNavigationExecutor(

override fun <T : BaseRoute> savedStateHandleFor(destinationId: DestinationId<T>): SavedStateHandle {
val entry = entryFor(destinationId)
return viewModel.provideSavedStateHandle(entry.id)
return viewModel.provideSavedStateHandle(entry.id, entry.route)
}

override fun <T : BaseRoute> savedStateHandleFactoryFor(destinationId: DestinationId<T>): SavedStateHandleFactory {
val entry = entryFor(destinationId)
return viewModel.provideSavedStateHandleFactory(entry.id, entry.route)
}

override fun <T : BaseRoute> storeFor(destinationId: DestinationId<T>): NavigationExecutor.Store {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.hoc081098.solivagant.navigation.internal

import com.hoc081098.kmp.viewmodel.ViewModelStore
import com.hoc081098.kmp.viewmodel.ViewModelStoreOwner
import kotlin.LazyThreadSafetyMode.NONE

internal expect fun createViewModelStore(): ViewModelStore

internal class StackEntryViewModelStoreOwner : ViewModelStoreOwner {
private val viewModelStoreLazy = lazy(NONE) { createViewModelStore() }

fun clearIfInitialized() {
if (viewModelStoreLazy.isInitialized()) {
viewModelStoreLazy.value.clear()
}
}

override val viewModelStore: ViewModelStore by viewModelStoreLazy
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
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.navigation.BaseRoute
import com.hoc081098.solivagant.navigation.EXTRA_ROUTE
import com.hoc081098.solivagant.navigation.NavRoot
import com.hoc081098.solivagant.navigation.internal.MultiStackNavigationExecutor.Companion.SAVED_STATE_STACK

Expand All @@ -10,24 +13,33 @@ internal class StoreViewModel(
) : ViewModel() {
private val stores = mutableMapOf<StackEntry.Id, NavigationExecutorStore>()
private val savedStateHandles = mutableMapOf<StackEntry.Id, SavedStateHandle>()
private val savedStateHandleFactories = mutableMapOf<StackEntry.Id, SavedStateHandleFactory>()

internal val savedNavRoot: NavRoot? get() = globalSavedStateHandle[SAVED_START_ROOT_KEY]

fun provideStore(id: StackEntry.Id): NavigationExecutor.Store {
internal fun provideStore(id: StackEntry.Id): NavigationExecutor.Store {
return stores.getOrPut(id) { NavigationExecutorStore() }
}

fun provideSavedStateHandle(id: StackEntry.Id): SavedStateHandle {
internal fun provideSavedStateHandle(id: StackEntry.Id, route: BaseRoute): SavedStateHandle {
return savedStateHandles.getOrPut(id) {
createSavedStateHandleAndSetSavedStateProvider(id.value, globalSavedStateHandle)
.apply { this[EXTRA_ROUTE] = route }
}
}

fun removeEntry(id: StackEntry.Id) {
internal fun provideSavedStateHandleFactory(id: StackEntry.Id, route: BaseRoute): SavedStateHandleFactory {
return savedStateHandleFactories.getOrPut(id) {
SavedStateHandleFactory { provideSavedStateHandle(id, route) }
}
}

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

savedStateHandles.remove(id)
savedStateHandleFactories.remove(id)
globalSavedStateHandle.removeSavedStateProvider(id.value)
globalSavedStateHandle.remove<Any>(id.value)
}
Expand All @@ -44,6 +56,7 @@ internal class StoreViewModel(
globalSavedStateHandle.remove<Any>(key.value)
}
savedStateHandles.clear()
savedStateHandleFactories.clear()
}

internal fun setInputStartRoot(root: NavRoot) {
Expand Down Expand Up @@ -73,7 +86,7 @@ internal class StoreViewModel(
globalSavedStateHandle[SAVED_START_ROOT_KEY] = root
}

fun getSavedStackState(): Map<String, Any?>? =
internal fun getSavedStackState(): Map<String, Any?>? =
globalSavedStateHandle.getAsMap(SAVED_STATE_STACK)

private companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.hoc081098.solivagant.navigation.internal

import com.hoc081098.kmp.viewmodel.ViewModelStore

internal actual fun createViewModelStore(): ViewModelStore = ViewModelStore()
Loading
Loading