Skip to content

Commit

Permalink
-> Add ability to save/restore navController's state manually at any …
Browse files Browse the repository at this point in the history
…time

-> Add custom state saver example
-> Rename `StorageMode` to better represent actual place of data storing
-> `rememberNavController` with navArgs/freeArgs will only receive NON-nullable `startDestination`
-> `nextEntryNavId` renamed to `pendingEntryNavId` to better represent self
-> `NavController.saveToSaveState` will only provide necessary data based on `storageMode`
-> update README.md
  • Loading branch information
vkatz authored and egorikftp committed Jul 29, 2024
1 parent db7de5a commit 11b4cc3
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 21 deletions.
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Why Tiamat?
- Easy to use
- Allow to pass ANY types as data, even lambdas (!under small condition)
- Customizable transitions
- Customizable save-state logic

Setup
-----
Expand Down Expand Up @@ -139,6 +140,7 @@ The scope provides a number of composable functions:
- `freeArgs` - free type arguments, useful to store metadata or pass deeplink info
- `clearFreeArgs` - clear free type arguments (eg: clear handled deeplink info)
- `navResult` - provide the data passed to `NavControllr:back(screen, navResult)` as result
- `clearNavResult` - clear passed nav result (eg: you want to show notification base on result and clear it not to re-show)
- `rememberViewModel` - create or provide view model scoped(linked) to current screen
- `rememberSharedViewModel` - create or provide view model scoped(linked) to current/provided `NavController`

Expand Down Expand Up @@ -177,13 +179,13 @@ NavController will keep the screens data, view models, and states during navigat
### Storage mode

- `null` - will take parent NavController mode or `ResetOnDataLoss` for root controller
- `StorageMode.Savable` - will store data in `savable` storage (eg: Android -> Bundle)
- `null` - will take parent NavController mode or `Memory` for root controller
- `StorageMode.SavedState` - will store data in `savable` storage (eg: Android -> Bundle)
> [!IMPORTANT]
> Only 'Savable' types of params & args will be available to use
>
> eg: Android - Parcelable + any bundlable primitives
- `StorageMode.ResetOnDataLoss` - store data in memory, allow to use any types of args & params (including lambdas). Reset nav controller upon data loss
- `StorageMode.Memory` - store data in memory, allow to use any types of args & params (including lambdas). Reset nav controller upon data loss

### Known limitations

Expand Down Expand Up @@ -258,6 +260,7 @@ Custom transition:
- [DataPassingResult.kt](example/app/composeApp/src/commonMain/kotlin/composegears/tiamat/example/DataPassingResult.kt) - How to provide result
- [ViewModels.kt](example/app/composeApp/src/commonMain/kotlin/composegears/tiamat/example/ViewModels.kt) - ViewModels usage
- [CustomTransition.kt](example/app/composeApp/src/commonMain/kotlin/composegears/tiamat/example/CustomTransition.kt) - Custom animations/transition
- [CustomStateSaver.kt](example/app/composeApp/src/commonMain/kotlin/composegears/tiamat/example/CustomStateSaver.kt) - Custom save/restore state
- [Root.kt](example/app/composeApp/src/commonMain/kotlin/composegears/tiamat/example/multimodule/Root.kt) - Multi-module communication example (using Signals/Broadcast-api)
- [BackStackAlteration.kt](example/app/composeApp/src/commonMain/kotlin/composegears/tiamat/example/BackStackAlteration.kt) - Alteration(modification) of backstack (deeplinks)
- [TwoPaneResizableExample.kt](example/app/composeApp/src/commonMain/kotlin/composegears/tiamat/example/TwoPaneResizableExample.kt) - 2 pane example (list+details, dynamic switch between 1-pane or 2-pane layout)
Expand Down Expand Up @@ -338,6 +341,14 @@ Web: `./gradlew example:app:composeApp:wasmJsBrowserRun`
iOS: run XCode project or else use [KMM](https://plugins.jetbrains.com/plugin/14936-kotlin-multiplatform-mobile) plugin iOS target
## Contributors
Thank you for your help! ❤️
<a href="https://github.com/ComposeGears/Tiamat/graphs/contributors">
<img src="https://contrib.rocks/image?repo=ComposeGears/Tiamat" />
</a>
# License
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ val SavedStateScreen by navDestination<Unit> {
Column {
TextCaption("In order to test this behaviour (IMPORTANT):")
TextCaption("• go ./example/composeApp/src/commonMain/kotlin/App.kt")
TextCaption("• change storageMode to StorageMode.Savable")
TextCaption("• change storageMode to StorageMode.SavedState")
TextCaption("• !!WARNING!! other screens may not work due to this changes!!")
TextCaption("• compile android app")
TextCaption("• go to developer settings of your device")
Expand All @@ -44,7 +44,7 @@ val SavedStateScreen by navDestination<Unit> {
TextCaption("eg: primitives, parcelable (see: SaveableStateRegistry.canBeSaved)")
val savableNavController = rememberNavController(
key = "savableNavController",
storageMode = StorageMode.Savable,
storageMode = StorageMode.SavedState,
startDestination = SavableDataExampleScreenRoot,
destinations = arrayOf(
SavableDataExampleScreenRoot,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ fun App(
Surface {
val rootNavController = rememberNavController(
key = "rootNavController",
storageMode = StorageMode.ResetOnDataLoss,
storageMode = StorageMode.Memory,
startDestination = MainScreen,
destinations = arrayOf(
MainScreen,
Expand All @@ -51,6 +51,7 @@ fun App(
CustomTransitionRoot,
CustomTransitionScreen1,
CustomTransitionScreen2,
CustomStateSaverRoot,
MultiModuleRoot,
BackStackAlterationRoot,
TwoPaneResizableRoot,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package composegears.tiamat.example

import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.composegears.tiamat.Navigation
import com.composegears.tiamat.StorageMode
import com.composegears.tiamat.navDestination
import com.composegears.tiamat.rememberNavController
import composegears.tiamat.example.ui.core.SimpleScreen
import composegears.tiamat.example.ui.core.TextBody
import composegears.tiamat.example.ui.core.TextButton

val CustomStateSaverRoot by navDestination<Unit> {
SimpleScreen("Custom state saver") {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
var savedState by remember { mutableStateOf<Map<String, Any?>>(emptyMap()) }
var showNavController by remember { mutableStateOf(true) }

TextButton("Toggle navigation -> ${if (showNavController) "Hide" else "Show"}") {
showNavController = !showNavController
}

Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.padding(8.dp)
.border(4.dp, MaterialTheme.colorScheme.onSurface)
.padding(4.dp),
contentAlignment = Alignment.Center,
) {
if (showNavController) {
val savableNavController = rememberNavController(
storageMode = StorageMode.Memory,
startDestination = DataPassingParamsRoot,
destinations = arrayOf(
DataPassingParamsRoot,
DataPassingParamsScreen,
)
) {
// restore state from custom storage
if (savedState.isNotEmpty())
loadFromSavedState(savedState)
}
Navigation(navController = savableNavController)
DisposableEffect(Unit) {
onDispose {
// save state into custom storage
savedState = savableNavController.getSavedState()
}
}
} else TextBody("Nothing")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ val MainScreen by navDestination<Unit> {
"ViewModel/SharedViewModel" to { navController.navigate(ViewModelsRoot) },
"Koin (ViewModel/SharedViewModel)" to { navController.navigate(KoinIntegrationScreen) },
"Custom transition" to { navController.navigate(CustomTransitionRoot) },
"Custom state saver" to { navController.navigate(CustomStateSaverRoot) },
"Multi-module" to { navController.navigate(MultiModuleRoot) },
"Back stack alteration" to { navController.navigate(BackStackAlterationRoot) },
"2 Pane (list + detail, resizable)" to { navController.navigate(TwoPaneResizableRoot) },
Expand Down
16 changes: 8 additions & 8 deletions tiamat/src/commonMain/kotlin/com/composegears/tiamat/Compose.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,19 @@ enum class StorageMode {
/**
* Savable storage, persist internal cleanups
*/
Savable,
SavedState,

/**
* In memory data storage, navController will reset on data loss
*/
ResetOnDataLoss
Memory
}

/**
* Create and provide [NavController] instance to be used in [Navigation]
*
* @param key optional key, used to identify NavController's (eg: nc.parent.key == ...)
* @param storageMode data storage mode, default is parent mode or if it is root [StorageMode.ResetOnDataLoss]
* @param storageMode data storage mode, default is parent mode or if it is root [StorageMode]
* @param startDestination destination to be used as initial
* @param destinations array of allowed destinations for this controller
* @param configuration an action to be called after [NavController] created/restored
Expand All @@ -56,7 +56,7 @@ fun rememberNavController(
* Create and provide [NavController] instance to be used in [Navigation]
*
* @param key optional key, used to identify NavController's (eg: nc.parent.key == ...)
* @param storageMode data storage mode, default is parent mode or if it is root [StorageMode.ResetOnDataLoss]
* @param storageMode data storage mode, default is parent mode or if it is root [StorageMode]
* @param startDestination destination to be used as initial
* @param startDestinationNavArgs initial destination navArgs
* @param startDestinationFreeArgs initial destination freeArgs
Expand All @@ -68,15 +68,15 @@ fun rememberNavController(
fun <T> rememberNavController(
key: String? = null,
storageMode: StorageMode? = null,
startDestination: NavDestination<T>?,
startDestination: NavDestination<T>,
startDestinationNavArgs: T? = null,
startDestinationFreeArgs: Any? = null,
destinations: Array<NavDestination<*>>,
configuration: NavController.() -> Unit = {}
) = rememberNavController(
key = key,
storageMode = storageMode,
startDestination = startDestination?.toNavEntry(
startDestination = startDestination.toNavEntry(
navArgs = startDestinationNavArgs,
freeArgs = startDestinationFreeArgs
),
Expand All @@ -88,7 +88,7 @@ fun <T> rememberNavController(
* Create and provide [NavController] instance to be used in [Navigation]
*
* @param key optional key, used to identify NavController's (eg: nc.parent.key == ...)
* @param storageMode data storage mode, default is parent mode or if it is root [StorageMode.ResetOnDataLoss]
* @param storageMode data storage mode, default is parent mode or if it is root [StorageMode]
* @param startDestination destination entry (destination + args) to be used as initial
* @param destinations array of allowed destinations for this controller
* @param configuration an action to be called after [NavController] created/restored
Expand All @@ -105,7 +105,7 @@ fun <T> rememberNavController(
val parent = LocalNavController.current
val parentNavEntry = LocalNavEntry.current
val navControllersStorage = parentNavEntry?.navControllersStorage ?: rootNavControllersStore()
val finalStorageMode = storageMode ?: parent?.storageMode ?: StorageMode.ResetOnDataLoss
val finalStorageMode = storageMode ?: parent?.storageMode ?: StorageMode.Memory

// attach to system save logic and perform model save on it
if (parent == null) rememberSaveable(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class NavController internal constructor(
companion object {
private const val KEY_KEY = "key"
private const val KEY_STORAGE_MODE = "storageMode"
private const val KEY_NEXT_ENTRY_NAV_ID = "nextEntryNavId"
private const val KEY_PENDING_ENTRY_NAV_ID = "pendingEntryNavId"
private const val KEY_START_DESTINATION = "startDestination"
private const val KEY_DESTINATIONS = "destinations"
private const val KEY_CURRENT = "current"
Expand Down Expand Up @@ -84,7 +84,7 @@ class NavController internal constructor(
internal var contentTransition: ContentTransform? = null
private set

private var nextEntryNavId = 0L
private var pendingEntryNavId = 0L

init {
// ensure there is no same-named destinations
Expand All @@ -101,7 +101,7 @@ class NavController internal constructor(
if (startDestination != null)
requireKnownDestination(startDestination.destination)
// load from saved state
if (savedState != null) runCatching {
if (!savedState.isNullOrEmpty()) runCatching {
restoreFromSavedState(savedState)
}
// go to start destination if nothing restored
Expand All @@ -111,24 +111,57 @@ class NavController internal constructor(

@Suppress("UNCHECKED_CAST")
private fun restoreFromSavedState(savedState: Map<String, Any?>) {
nextEntryNavId = savedState[KEY_NEXT_ENTRY_NAV_ID] as Long
pendingEntryNavId = savedState[KEY_PENDING_ENTRY_NAV_ID] as Long
val currentNavEntry = (savedState[KEY_CURRENT] as? Map<String, Any?>?)
?.let { NavEntry.restore(it, destinations) }
(savedState[KEY_BACKSTACK] as List<Map<String, Any?>>)
.mapTo(backStack) { NavEntry.restore(it, destinations) }
setCurrentNavEntryInternal(currentNavEntry)
}

internal fun saveToSaveState(): Map<String, Any?> = mapOf(
private fun getMinimalVerificationSavedState() = mapOf(
KEY_KEY to key,
KEY_STORAGE_MODE to storageMode.name,
KEY_NEXT_ENTRY_NAV_ID to nextEntryNavId,
KEY_START_DESTINATION to startDestination?.destination?.name,
KEY_DESTINATIONS to destinations.joinToString(DESTINATIONS_JOIN_SEPARATOR) { it.name },
)

private fun getFullSavedState() = getMinimalVerificationSavedState() + mapOf(
KEY_PENDING_ENTRY_NAV_ID to pendingEntryNavId,
KEY_CURRENT to currentNavEntry?.saveToSaveState(),
KEY_BACKSTACK to backStack.map { it.saveToSaveState() }
)

internal fun saveToSaveState(): Map<String, Any?> = when (storageMode) {
StorageMode.SavedState -> getFullSavedState()
StorageMode.Memory -> getMinimalVerificationSavedState()
}

/**
* Save current navController state(full, regardless of `storageMode`)
* and it's children states(depend on theirs `storageMode`)
*
* @return saved state
*/
fun getSavedState(): Map<String, Any?> = getFullSavedState()

/**
* Load navController (and it's children) state from saved state
*
* Use with caution, calling this method will reset backstack and current entry
*
* @param savedState saved state
*/
fun loadFromSavedState(savedState: Map<String, Any?>) {
// clear current state
close()
// load from saved state
restoreFromSavedState(savedState)
// navigate to start destination if nothing restored
if (currentNavEntry == null && backStack.isEmpty() && startDestination != null)
setCurrentNavEntryInternal(NavEntry(startDestination))
}

/**
* @param key nav controller's key to search for
*
Expand All @@ -152,7 +185,7 @@ class NavController internal constructor(
private fun setCurrentNavEntryInternal(
navEntry: NavEntry<*>?,
) {
if (navEntry != null && navEntry.navId < 0) navEntry.navId = nextEntryNavId++
if (navEntry != null && navEntry.navId < 0) navEntry.navId = pendingEntryNavId++
currentNavEntry = navEntry
current = navEntry?.destination
pendingBackTransition = null
Expand Down

0 comments on commit 11b4cc3

Please sign in to comment.