diff --git a/docs/component/state-preservation.md b/docs/component/state-preservation.md index 849634c55..90f827ca7 100644 --- a/docs/component/state-preservation.md +++ b/docs/component/state-preservation.md @@ -4,28 +4,52 @@ Sometimes it might be necessary to preserve state or data in a component when it The `decompose` module adds Essenty's `state-keeper` module as `api` dependency, so you don't need to explicitly add it to your project. Please familiarise yourself with Essenty library, especially with the `StateKeeper`. -## Usage examples - -```kotlin title="Saving state in a component" -import com.arkivanov.decompose.ComponentContext -import com.arkivanov.essenty.parcelable.Parcelable -import com.arkivanov.essenty.parcelable.Parcelize -import com.arkivanov.essenty.statekeeper.consume - -class SomeComponent( - componentContext: ComponentContext -) : ComponentContext by componentContext { +Since Decompose `v2.2.0-alpha01` the recommended way is to use [kotlinx-serialization](https://github.com/Kotlin/kotlinx.serialization) library. Most of the Parcelable/Parcelize APIs are now deprecated. - private var state: State = stateKeeper.consume(key = "SAVED_STATE") ?: State() +## Usage examples - init { - stateKeeper.register(key = "SAVED_STATE") { state } +=== "Before v1.2.0-alpha01" + + ```kotlin title="Saving state in a component" + import com.arkivanov.decompose.ComponentContext + import com.arkivanov.essenty.parcelable.Parcelable + import com.arkivanov.essenty.parcelable.Parcelize + import com.arkivanov.essenty.statekeeper.consume + + class SomeComponent( + componentContext: ComponentContext + ) : ComponentContext by componentContext { + + private var state: State = stateKeeper.consume(key = "SAVED_STATE") ?: State() + + init { + stateKeeper.register(key = "SAVED_STATE") { state } + } + + @Parcelize + private class State(val someValue: Int = 0) : Parcelable } - - @Parcelize - private class State(val someValue: Int = 0) : Parcelable -} -``` + ``` + +=== "After v1.2.0-alpha01" + + ```kotlin title="Saving state in a component" + import kotlinx.serialization.Serializable + + class SomeComponent( + componentContext: ComponentContext + ) : ComponentContext by componentContext { + + private var state: State = stateKeeper.consume(key = "SAVED_STATE", strategy = State.serializer()) ?: State() + + init { + stateKeeper.register(key = "SAVED_STATE", strategy = State.serializer()) { state } + } + + @Serializable + private class State(val someValue: Int = 0) + } + ``` ```kotlin title="Saving state of a retained instance" import com.arkivanov.essenty.instancekeeper.InstanceKeeper @@ -51,6 +75,7 @@ class SomeComponent( } class SomeStatefulEntity( + // There is no any `kotlinx-serialization` replacement for `ParcelableContainer` currently, please continue using it until v3.0 savedState: ParcelableContainer?, ) : InstanceKeeper.Instance { diff --git a/docs/getting-started/quick-start.md b/docs/getting-started/quick-start.md index fb5fab8a5..b34a5cb91 100644 --- a/docs/getting-started/quick-start.md +++ b/docs/getting-started/quick-start.md @@ -138,70 +138,142 @@ Child component configurations is another important concepts of Decompose. It al Each child component is represented by a persistent configuration class. A configuration class denotes which child component should be instantiated, and holds persistent arguments required for instantiation. A configuration class must be defined for every child component. +!!!warning + Before `v1.2.0-alpha01`, configuration classes must implement `Parcelable` interface and be annotated with [@Parcelize](https://developer.android.com/kotlin/parcelize) annotation. Starting with `v1.2.0-alpha01`, Parcelable/Parcelize support is deprecated and the recommended way is to annotate configuration classes with [@Serializable](https://github.com/Kotlin/kotlinx.serialization) annotation. ### Using the Child Stack -```kotlin -interface RootComponent { - - val stack: Value> +=== "Before 1.2.0-alpha01" - // It's possible to pop multiple screens at a time on iOS - fun onBackClicked(toIndex: Int) + ```kotlin + interface RootComponent { + + val stack: Value> + + // It's possible to pop multiple screens at a time on iOS + fun onBackClicked(toIndex: Int) - // Defines all possible child components - sealed class Child { - class ListChild(val component: ListComponent) : Child() - class DetailsChild(val component: DetailsComponent) : Child() + // Defines all possible child components + sealed class Child { + class ListChild(val component: ListComponent) : Child() + class DetailsChild(val component: DetailsComponent) : Child() + } } -} - -class DefaultRootComponent( - componentContext: ComponentContext, -) : RootComponent, ComponentContext by componentContext { - - private val navigation = StackNavigation() - - override val stack: Value> = - childStack( - source = navigation, - initialConfiguration = Config.List, // The initial child component is List - handleBackButton = true, // Automatically pop from the stack on back button presses - childFactory = ::child, - ) - - private fun child(config: Config, componentContext: ComponentContext): RootComponent.Child = - when (config) { - is Config.List -> ListChild(listComponent(componentContext)) - is Config.Details -> DetailsChild(detailsComponent(componentContext, config)) + + class DefaultRootComponent( + componentContext: ComponentContext, + ) : RootComponent, ComponentContext by componentContext { + + private val navigation = StackNavigation() + + override val stack: Value> = + childStack( + source = navigation, + initialConfiguration = Config.List, // The initial child component is List + handleBackButton = true, // Automatically pop from the stack on back button presses + childFactory = ::child, + ) + + private fun child(config: Config, componentContext: ComponentContext): RootComponent.Child = + when (config) { + is Config.List -> ListChild(listComponent(componentContext)) + is Config.Details -> DetailsChild(detailsComponent(componentContext, config)) + } + + private fun listComponent(componentContext: ComponentContext): ListComponent = + DefaultListComponent( + componentContext = componentContext, + onItemSelected = { item: String -> // Supply dependencies and callbacks + navigation.push(Config.Details(item = item)) // Push the details component + }, + ) + + private fun detailsComponent(componentContext: ComponentContext, config: Config.Details): DetailsComponent = + DefaultDetailsComponent( + componentContext = componentContext, + item = config.item, // Supply arguments from the configuration + onFinished = navigation::pop, // Pop the details component + ) + + override fun onBackClicked(toIndex: Int) { + navigation.popTo(index = toIndex) } + + @Parcelize // kotlin-parcelize plugin must be applied if you are targeting Android + private sealed interface Config : Parcelable { + data object List : Config + data class Details(val item: String) : Config + } + } + ``` - private fun listComponent(componentContext: ComponentContext): ListComponent = - DefaultListComponent( - componentContext = componentContext, - onItemSelected = { item: String -> // Supply dependencies and callbacks - navigation.push(Config.Details(item = item)) // Push the details component - }, - ) +=== "Since 1.2.0-alpha01" - private fun detailsComponent(componentContext: ComponentContext, config: Config.Details): DetailsComponent = - DefaultDetailsComponent( - componentContext = componentContext, - item = config.item, // Supply arguments from the configuration - onFinished = navigation::pop, // Pop the details component - ) + ```kotlin + interface RootComponent { + + val stack: Value> - override fun onBackClicked(toIndex: Int) { - navigation.popTo(index = toIndex) + // It's possible to pop multiple screens at a time on iOS + fun onBackClicked(toIndex: Int) + + // Defines all possible child components + sealed class Child { + class ListChild(val component: ListComponent) : Child() + class DetailsChild(val component: DetailsComponent) : Child() + } } + + class DefaultRootComponent( + componentContext: ComponentContext, + ) : RootComponent, ComponentContext by componentContext { + + private val navigation = StackNavigation() + + override val stack: Value> = + childStack( + source = navigation, + serializer = Config.serializer(), + initialConfiguration = Config.List, // The initial child component is List + handleBackButton = true, // Automatically pop from the stack on back button presses + childFactory = ::child, + ) + + private fun child(config: Config, componentContext: ComponentContext): RootComponent.Child = + when (config) { + is Config.List -> ListChild(listComponent(componentContext)) + is Config.Details -> DetailsChild(detailsComponent(componentContext, config)) + } + + private fun listComponent(componentContext: ComponentContext): ListComponent = + DefaultListComponent( + componentContext = componentContext, + onItemSelected = { item: String -> // Supply dependencies and callbacks + navigation.push(Config.Details(item = item)) // Push the details component + }, + ) + + private fun detailsComponent(componentContext: ComponentContext, config: Config.Details): DetailsComponent = + DefaultDetailsComponent( + componentContext = componentContext, + item = config.item, // Supply arguments from the configuration + onFinished = navigation::pop, // Pop the details component + ) + + override fun onBackClicked(toIndex: Int) { + navigation.popTo(index = toIndex) + } + + @Serializable // kotlinx-serialization plugin must be applied + private sealed interface Config { + @Serializable + data object List : Config - @Parcelize // The `kotlin-parcelize` plugin must be applied if you are targeting Android - private sealed interface Config : Parcelable { - data object List : Config - data class Details(val item: String) : Config + @Serializable + data class Details(val item: String) : Config + } } -} -``` + ``` ### Child Stack with Jetpack Compose diff --git a/docs/navigation/overview.md b/docs/navigation/overview.md index c536d7c49..e0bf19a74 100644 --- a/docs/navigation/overview.md +++ b/docs/navigation/overview.md @@ -25,17 +25,26 @@ Configurations must meet the following requirements: 1. Be immutable 2. [Correctly](https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#hashCode--) implement `equals()` and `hashCode()` methods -3. Implement `Parcelable` interface +3. Implement `Parcelable` interface (or be `@Serializable` starting with `v1.3.0-alpha01`) Different kinds of navigation may have additional requirements for configurations. It's recommended to define configurations as `data class`, and use only `val` properties and immutable data structures. -### Configurations are Parcelable +### Configurations are Parcelable (or @Serializable) `Configurations` can be persisted via Android's [saved state](https://developer.android.com/guide/components/activities/activity-lifecycle#save-simple,-lightweight-ui-state-using-onsaveinstancestate), thus allowing the navigation state to be restored after configuration changes or process death. Decompose uses [Essenty](https://github.com/arkivanov/Essenty) library, which provides both `Parcelable` interface and `@Parcelize` annotation in common code using expect/actual, which works well with Kotlin Multiplatform. Please familiarise yourself with Essenty library. -#### Android target +Starting with `v1.3.0-alpha01`, the recommended way is to use [kotlinx-serialization](https://github.com/Kotlin/kotlinx.serialization) library. The Essenty library is still used. + +#### All targets (using kotlinx-serialization since v1.3.0-alpha01) + +Please make sure you [setup](https://github.com/Kotlin/kotlinx.serialization#setup) `kotlinx-serialization` correctly and applied the plugin. + +!!!warning + On Android the amount of data that can be preserved is [limited](https://developer.android.com/guide/components/activities/parcelables-and-bundles). Please mind the size of configurations. + +#### Android target (using Parcelable/Parcelize, deprecated since v1.3.0-alpha01) If you support the `android` target, make sure you have applied [kotlin-parcelize](https://developer.android.com/kotlin/parcelize) Gradle plugin. Otherwise, your code won't compile for Android. diff --git a/docs/navigation/pages/overview.md b/docs/navigation/pages/overview.md index 307c73ce8..f42a8725c 100644 --- a/docs/navigation/pages/overview.md +++ b/docs/navigation/pages/overview.md @@ -52,43 +52,86 @@ class DefaultPageComponent( ) : PageComponent, ComponentContext by componentContext ``` -```kotlin title="PagesComponent" -interface PagesComponent { - val pages: Value> +=== "Before v1.2.0-alpha01" - fun selectPage(index: Int) -} - -class DefaultPagesComponent( - componentContext: ComponentContext, -) : PagesComponent, ComponentContext by componentContext { + ```kotlin title="PagesComponent" + interface PagesComponent { + val pages: Value> + + fun selectPage(index: Int) + } + + class DefaultPagesComponent( + componentContext: ComponentContext, + ) : PagesComponent, ComponentContext by componentContext { + + private val navigation = PagesNavigation() + + override val pages: Value> = + childPages( + source = navigation, + initialPages = { + Pages( + items = List(10) { index -> Config(data = "Item $index") }, + selectedIndex = 0, + ) + }, + ) { config, childComponentContext -> + DefaultPageComponent( + componentContext = childComponentContext, + data = config.data, + ) + } + + override fun selectPage(index: Int) { + navigation.select(index = index) + } + + @Parcelize // kotlin-parcelize plugin must be applied if you are targetting Android + private data class Config(val data: String) : Parcelable + } + ``` - private val navigation = PagesNavigation() +=== "Since v1.2.0-alpha01" - override val pages: Value> = - childPages( - source = navigation, - initialPages = { - Pages( - items = List(10) { index -> Config(data = "Item $index") }, - selectedIndex = 0, + ```kotlin title="PagesComponent" + interface PagesComponent { + val pages: Value> + + fun selectPage(index: Int) + } + + class DefaultPagesComponent( + componentContext: ComponentContext, + ) : PagesComponent, ComponentContext by componentContext { + + private val navigation = PagesNavigation() + + override val pages: Value> = + childPages( + source = navigation, + serializer = Config.serializer(), // Or null to disable navigation state saving + initialPages = { + Pages( + items = List(10) { index -> Config(data = "Item $index") }, + selectedIndex = 0, + ) + }, + ) { config, childComponentContext -> + DefaultPageComponent( + componentContext = childComponentContext, + data = config.data, ) - }, - ) { config, childComponentContext -> - DefaultPageComponent( - componentContext = childComponentContext, - data = config.data, - ) + } + + override fun selectPage(index: Int) { + navigation.select(index = index) } - - override fun selectPage(index: Int) { - navigation.select(index = index) + + @Serializable // kotlinx-serialization plugin must be applied + private data class Config(val data: String) } - - @Parcelize - private data class Config(val data: String) : Parcelable -} -``` + ``` ## Screen recreation and process death on (not only) Android diff --git a/docs/navigation/slot/overview.md b/docs/navigation/slot/overview.md index 6b60fae60..4de44cab6 100644 --- a/docs/navigation/slot/overview.md +++ b/docs/navigation/slot/overview.md @@ -45,41 +45,81 @@ class DefaultDialogComponent( } ``` -```kotlin title="Root component" -interface RootComponent { +=== "Before v1.2.0-alpha01" - val dialog: Value> -} - -class DefaultRootComponent( - componentContext: ComponentContext, -) : RootComponent, ComponentContext by componentContext { - - private val dialogNavigation = SlotNavigation() - - override val dialog: Value> = - childSlot( - source = dialogNavigation, - // persistent = false, // Disable navigation state saving, if needed - handleBackButton = true, // Close the dialog on back button press - ) { config, childComponentContext -> - DefaultDialogComponent( - componentContext = childComponentContext, - message = config.message, - onDismissed = dialogNavigation::dismiss, - ) + ```kotlin title="Root component" + interface RootComponent { + + val dialog: Value> + } + + class DefaultRootComponent( + componentContext: ComponentContext, + ) : RootComponent, ComponentContext by componentContext { + + private val dialogNavigation = SlotNavigation() + + override val dialog: Value> = + childSlot( + source = dialogNavigation, + // persistent = false, // Disable navigation state saving, if needed + handleBackButton = true, // Close the dialog on back button press + ) { config, childComponentContext -> + DefaultDialogComponent( + componentContext = childComponentContext, + message = config.message, + onDismissed = dialogNavigation::dismiss, + ) + } + + private fun showDialog(message: String) { + dialogNavigation.activate(DialogConfig(message = message)) } - - private fun showDialog(message: String) { - dialogNavigation.activate(DialogConfig(message = message)) + + @Parcelize // kotlin-parcelize plugin must be applied if you are targetting Android + private data class DialogConfig( + val message: String, + ) : Parcelable } + ``` - @Parcelize - private data class DialogConfig( - val message: String, - ) : Parcelable -} -``` +=== "Since v1.2.0-alpha01" + + ```kotlin title="Root component" + interface RootComponent { + + val dialog: Value> + } + + class DefaultRootComponent( + componentContext: ComponentContext, + ) : RootComponent, ComponentContext by componentContext { + + private val dialogNavigation = SlotNavigation() + + override val dialog: Value> = + childSlot( + source = dialogNavigation, + serializer = DialogConfig.serializer(), // Or null to disable navigation state saving + handleBackButton = true, // Close the dialog on back button press + ) { config, childComponentContext -> + DefaultDialogComponent( + componentContext = childComponentContext, + message = config.message, + onDismissed = dialogNavigation::dismiss, + ) + } + + private fun showDialog(message: String) { + dialogNavigation.activate(DialogConfig(message = message)) + } + + @Serializable // kotlinx-serialization plugin must be applied + private data class DialogConfig( + val message: String, + ) + } + ``` ## Multiple Child Slots in a component diff --git a/docs/navigation/stack/overview.md b/docs/navigation/stack/overview.md index 66b87eaf5..7b3b32ac9 100644 --- a/docs/navigation/stack/overview.md +++ b/docs/navigation/stack/overview.md @@ -76,59 +76,119 @@ class DefaultItemDetailsComponent( } ``` -```kotlin title="Root component" -interface RootComponent { +=== "Before v1.2.0-alpha01" - val childStack: Value> - - sealed class Child { - class ListChild(val component: ItemList) : Child() - class DetailsChild(val component: ItemDetails) : Child() + ```kotlin title="Root component" + interface RootComponent { + + val childStack: Value> + + sealed class Child { + class ListChild(val component: ItemList) : Child() + class DetailsChild(val component: ItemDetails) : Child() + } } -} - -class DefaultRootComponent( - componentContext: ComponentContext -) : RootComponent, ComponentContext by componentContext { - - private val navigation = StackNavigation() - - override val childStack: Value> = - childStack( - source = navigation, - initialConfiguration = Config.List, - handleBackButton = true, // Pop the back stack on back button press - childFactory = ::createChild, - ) - - private fun createChild(config: Config, componentContext: ComponentContext): RootComponent.Child = - when (config) { - is Config.List -> ListChild(itemList(componentContext)) - is Config.Details -> DetailsChild(itemDetails(componentContext, config)) + + class DefaultRootComponent( + componentContext: ComponentContext + ) : RootComponent, ComponentContext by componentContext { + + private val navigation = StackNavigation() + + override val childStack: Value> = + childStack( + source = navigation, + initialConfiguration = Config.List, + handleBackButton = true, // Pop the back stack on back button press + childFactory = ::createChild, + ) + + private fun createChild(config: Config, componentContext: ComponentContext): RootComponent.Child = + when (config) { + is Config.List -> ListChild(itemList(componentContext)) + is Config.Details -> DetailsChild(itemDetails(componentContext, config)) + } + + private fun itemList(componentContext: ComponentContext): ItemListComponent = + DefaultItemListComponent( + componentContext = componentContext, + onItemSelected = { navigation.push(Config.Details(itemId = it)) } + ) + + private fun itemDetails(componentContext: ComponentContext, config: Config.Details): ItemDetailsComponent = + DefaultItemDetailsComponent( + componentContext = componentContext, + itemId = config.itemId, + onFinished = { navigation.pop() } + ) + + private sealed class Config : Parcelable { + @Parcelize + data object List : Config() + + @Parcelize + data class Details(val itemId: Long) : Config() } + } + ``` - private fun itemList(componentContext: ComponentContext): ItemListComponent = - DefaultItemListComponent( - componentContext = componentContext, - onItemSelected = { navigation.push(Config.Details(itemId = it)) } - ) - - private fun itemDetails(componentContext: ComponentContext, config: Config.Details): ItemDetailsComponent = - DefaultItemDetailsComponent( - componentContext = componentContext, - itemId = config.itemId, - onFinished = { navigation.pop() } - ) - - private sealed class Config : Parcelable { - @Parcelize - data object List : Config() +=== "Since v1.2.0-alpha01" - @Parcelize - data class Details(val itemId: Long) : Config() + ```kotlin title="Root component" + interface RootComponent { + + val childStack: Value> + + sealed class Child { + class ListChild(val component: ItemList) : Child() + class DetailsChild(val component: ItemDetails) : Child() + } } -} -``` + + class DefaultRootComponent( + componentContext: ComponentContext + ) : RootComponent, ComponentContext by componentContext { + + private val navigation = StackNavigation() + + override val childStack: Value> = + childStack( + source = navigation, + serializer = Config.serializer(), // Or null to disable navigation state saving + initialConfiguration = Config.List, + handleBackButton = true, // Pop the back stack on back button press + childFactory = ::createChild, + ) + + private fun createChild(config: Config, componentContext: ComponentContext): RootComponent.Child = + when (config) { + is Config.List -> ListChild(itemList(componentContext)) + is Config.Details -> DetailsChild(itemDetails(componentContext, config)) + } + + private fun itemList(componentContext: ComponentContext): ItemListComponent = + DefaultItemListComponent( + componentContext = componentContext, + onItemSelected = { navigation.push(Config.Details(itemId = it)) } + ) + + private fun itemDetails(componentContext: ComponentContext, config: Config.Details): ItemDetailsComponent = + DefaultItemDetailsComponent( + componentContext = componentContext, + itemId = config.itemId, + onFinished = { navigation.pop() } + ) + + @Serializable // kotlinx-serialization plugin must be applied + private sealed class Config { + @Serializable + data object List : Config() + + @Serializable + data class Details(val itemId: Long) : Config() + } + } + ``` ## Components in the back stack