From 6f8fc526ac291288ce410fbf91cd3520b9c60b96 Mon Sep 17 00:00:00 2001 From: Arkadii Ivanov Date: Mon, 29 May 2023 21:28:35 +0100 Subject: [PATCH] Updated Readme and docs with Child Pages --- README.md | 1 + .../decompose/router/pages/PagesNavigator.kt | 6 +- docs/component/back-button.md | 2 +- docs/extensions/compose.md | 34 ++++- docs/getting-started/installation.md | 2 +- docs/navigation/children/overview.md | 2 +- docs/navigation/overview.md | 1 + docs/navigation/pages/navigation.md | 134 ++++++++++++++++++ docs/navigation/pages/overview.md | 101 +++++++++++++ mkdocs.yml | 3 + 10 files changed, 278 insertions(+), 8 deletions(-) create mode 100644 docs/navigation/pages/navigation.md create mode 100644 docs/navigation/pages/overview.md diff --git a/README.md b/README.md index 2c1e97298..87c6175ac 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ Here are some key concepts of the library, more details can be found in the docu * [ComponentContext](https://arkivanov.github.io/Decompose/component/overview/#componentcontext) - every component has its own [ComponentContext], which makes components lifecycle-aware and allows state preservation, instances retaining (aka AndroidX `ViewModel`) and back button handling * [Child Stack](https://arkivanov.github.io/Decompose/navigation/stack/overview/) - enables navigation between child components, nested navigation is also supported * [Child Slot](https://arkivanov.github.io/Decompose/navigation/slot/overview/) - allows only one child component at a time, or none +* [Child Pages](https://arkivanov.github.io/Decompose/navigation/pages/overview/) - a list of child components with one selected component (e.g. pager-like navigation) * [Generic Navigation](https://arkivanov.github.io/Decompose/navigation/children/overview/) - provides a way to create your own custom navigation model, when none of the predefined models fit your needs * [Lifecycle](https://arkivanov.github.io/Decompose/component/lifecycle/) - provides a way to listen for lifecycle events in components * [StateKeeper](https://arkivanov.github.io/Decompose/component/state-preservation/) - makes it possible to preserve state or data in a component when it gets destroyed diff --git a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/pages/PagesNavigator.kt b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/pages/PagesNavigator.kt index 14912f5b7..58aa30c61 100644 --- a/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/pages/PagesNavigator.kt +++ b/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/pages/PagesNavigator.kt @@ -8,9 +8,9 @@ interface PagesNavigator { /** * Transforms the current [Pages] state into a new one. * - * During the navigation process, the Child Pages navigation model compares the [Pages] state with - * the previous one. The navigation model ensures that all removed components are destroyed, and - * updates lifecycles of the existing components to match the new state. + * During the navigation process, the Child Pages navigation model compares the new [Pages] state + * with the previous one. The navigation model ensures that all removed components are destroyed, + * and updates lifecycles of the existing components to match the new state. * * The navigation is usually performed synchronously, which means that by the time * the `navigate` method returns, the navigation is finished and all component lifecycles are diff --git a/docs/component/back-button.md b/docs/component/back-button.md index f11fa89d5..e5a15999b 100644 --- a/docs/component/back-button.md +++ b/docs/component/back-button.md @@ -4,7 +4,7 @@ Some devices (e.g. Android) have hardware back buttons. A very common use case i ## Navigation with back button -`Child Stack` can automatically navigate back when the back button is pressed. All you need to do is to supply the `handleBackButton=true` argument when you initialize the `ChildStack`. Please see the [Child Stack](/Decompose/navigation/stack/overview/) documentation page for more information. +`Child Stack` and `Child Pages` can automatically navigate back when the back button is pressed. All you need to do is to supply the `handleBackButton=true` argument when you initialize a navigation model. Similarly, `Child Slot` can automatically dismiss the child component when the back button is pressed. see the [Child Slot](/Decompose/navigation/slot/overview/) documentation page for more information. diff --git a/docs/extensions/compose.md b/docs/extensions/compose.md index c03ec9e84..8329ac957 100644 --- a/docs/extensions/compose.md +++ b/docs/extensions/compose.md @@ -91,7 +91,7 @@ fun main() { ### Navigating between Composable components -The [Child Stack](/Decompose/navigation/stack/overview/) feature provides [ChildStack](https://github.com/arkivanov/Decompose/blob/master/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/ChildStack.kt) as `Value` that can be observed in a `Composable` component. This makes it possible to switch child `Composable` components following the `ChildStack` changes. +The [Child Stack](/Decompose/navigation/stack/overview/) navigation model provides [ChildStack](https://github.com/arkivanov/Decompose/blob/master/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/ChildStack.kt) as `Value` that can be observed in a `Composable` component. This makes it possible to switch child `Composable` components following the `ChildStack` changes. Both Compose extension modules provide the [Children(...)](https://github.com/arkivanov/Decompose/blob/master/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/stack/Children.kt) function which has the following features: @@ -140,6 +140,36 @@ fun DetailsContent(component: DetailsComponent) { } ``` +### Pager-like navigation + +!!!warning + This navigation model is experimental, the API is subject to change. + +The [Child Pages](/Decompose/navigation/pages/overview/) navigation model provides [ChildPages](https://github.com/arkivanov/Decompose/blob/master/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/pages/ChildPages.kt) as `Value` that can be observed in a `Composable` component. + +Both Compose extension modules provide the [Pages(...)](https://github.com/arkivanov/Decompose/blob/master/extensions-compose-jetbrains/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/jetbrains/pages/Pages.kt) function which has the following features: + +- It listens for the `ChildPages` changes and displays child components using `HorizontalPager` or `VerticalPager` (see the related Jetpack Compose [documentation](https://developer.android.com/jetpack/compose/layouts/pager)). +- It animates page changes if there is an `animation` spec provided. + +```kotlin title="Example" +@Composable +fun PagesContent(component: PagesComponent) { + Pages( + pages = component.pages, + onPageSelected = component::selectPage, + scrollAnimation = PagesScrollAnimation.Default, + ) { _, page -> + PageContent(page) + } +} + +@Composable +fun PageContent(component: PageComponent) { + // Omitted code +} +``` + ### Animations Decompose provides [Child Animation API](https://github.com/arkivanov/Decompose/tree/master/extensions-compose-jetpack/src/main/java/com/arkivanov/decompose/extensions/compose/jetpack/stack/animation) for Compose, as well as some predefined animation specs. To enable child animations you need to pass the `animation` argument to the `Children` function. There are predefined animators provided by Decompose. @@ -217,7 +247,7 @@ fun RootContent(component: RootComponent) { -It is also possible to take into account the other child and the animation direction when selecting the animation. +It is also possible to take into account the other child and the animation direction when selecting the animation. ```kotlin @Composable diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index eb154600e..efcb6ec2c 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -4,7 +4,7 @@ Decompose provides a number of modules, they are all published to Maven Central ## The main Decompose module -The main functionality is provided by the `decompose` module. It contains some core features like [ComponentContext](/Decompose/component/overview/#componentcontext), [Child Stack](/Decompose/navigation/stack/overview/), [Child Slot](/Decompose/navigation/slot/overview/), etc. +The main functionality is provided by the `decompose` module. It contains some core features like [ComponentContext](/Decompose/component/overview/#componentcontext), [Child Stack](/Decompose/navigation/stack/overview/), etc. ### Gradle setup diff --git a/docs/navigation/children/overview.md b/docs/navigation/children/overview.md index 71605aca6..8534a19b3 100644 --- a/docs/navigation/children/overview.md +++ b/docs/navigation/children/overview.md @@ -68,7 +68,7 @@ The `children` function returns an observable `Value` of the resulting children ## Examples -Both [Child Stack](https://github.com/arkivanov/Decompose/blob/master/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/ChildStackFactory.kt) and [Child Slot](https://github.com/arkivanov/Decompose/blob/master/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/slot/ChildSlotFactory.kt) are implemented using the `Generic Navigation`. Please refer to their source code for implementation details. +All existing navigation models (like [Child Stack](https://github.com/arkivanov/Decompose/blob/master/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/stack/ChildStackFactory.kt)) are implemented using the `Generic Navigation`. Please refer to their source code for implementation details. ### Sample project diff --git a/docs/navigation/overview.md b/docs/navigation/overview.md index 9232b4a60..c536d7c49 100644 --- a/docs/navigation/overview.md +++ b/docs/navigation/overview.md @@ -6,6 +6,7 @@ Currently, Decompose provides two predefined navigation models: - [Child Stack](/Decompose/navigation/stack/overview/) - prefer this way if you need to organize child components in a stack and navigate between them. - [Child Slot](/Decompose/navigation/slot/overview/) - prefer this way if you need to activate-dismiss one child component at a time. +- [Child Pages](/Decompose/navigation/pages/overview/) - prefer this way if you need to organize child components in a list with one selected component. If none of this fit your needs, Decompose introduces [Generic Navigation](https://arkivanov.github.io/Decompose/navigation/children/overview/) that can be used to create your own custom navigation models. It offers a flexible API and allows you to create almost any kind of navigation. diff --git a/docs/navigation/pages/navigation.md b/docs/navigation/pages/navigation.md new file mode 100644 index 000000000..fbd77ef0d --- /dev/null +++ b/docs/navigation/pages/navigation.md @@ -0,0 +1,134 @@ +# Navigation with Child Pages + +## The PagesNavigator + +All navigation in `Child Pages` is performed using the [`PagesNavigator`](https://github.com/arkivanov/Decompose/blob/master/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/pages/PagesNavigator.kt) interface, which is extended by the `PagesNavigation` interface. + +`PagesNavigator` contains the `navigate` method with the following arguments: + +- `transformer` - converts the current navigation state (`Pages`) into a new one. +- `onComplete` - called when navigation is finished. + +There is also `navigate` extension function without the `onComplete` callback, for convenience. + +```kotlin title="Creating the navigation" +val navigation = PagesNavigation() +``` + +### The navigation process + +During the navigation process, the Child Pages navigation model compares the new [Pages] state with the previous one. The navigation model ensures that all removed components are destroyed, and updates lifecycles of the existing components to match the new state. + +The navigation is usually performed synchronously, which means that by the time the `navigate` method returns, the navigation is finished and all component lifecycles are moved into required states. However, the navigation is performed asynchronously in case of recursive invocations - e.g. `selectNext` is called from `onResume` lifecycle callback of a component being shown. All recursive invocations are queued and performed one by one once the current navigation is finished. + +## PagesNavigator extension functions + +There are `PagesNavigator` [extension functions](https://github.com/arkivanov/Decompose/blob/master/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/pages/PagesNavigatorExt.kt) to simplify the navigation. Some of which were already used in the [Child Pages overview example](../overview#example). + +### selectNext + +Selects the next component. If the currently selected component is the last one, then depending on the [circular] parameter, either nothing happens or the first component is selected. + +```title="Before" +1: [A, B*, C] +2: [A, B, C*] +3: [A, B, C*] +``` + +``` +1: navigation.selectNext() +2: navigation.selectNext(circular = false) +3: navigation.selectNext(circular = true) +``` + +```title="After" +1: [A, B, C*] +2: [A, B, C*] +3: [A*, B, C] +``` + +### selectPrev + +elects the previous component. If the currently selected component is the first one, then depending on the [circular] parameter, either nothing happens or the last component is selected. + +```title="Before" +1: [A, B*, C] +2: [A*, B, C] +3: [A*, B, C] +``` + +``` +1: navigation.selectPrev() +2: navigation.selectPrev(circular = false) +3: navigation.selectPrev(circular = true) +``` + +```title="After" +1: [A*, B, C] +2: [A*, B, C] +3: [A, B, C*] +``` + +### selectFirst + +Selects the first component. + +```title="Before" +[A, B*, C] +``` + +``` +navigation.selectFirst() +``` + +```title="After" +[A*, B, C] +``` + +### selectLast + +Selects the last component. + +```title="Before" +[A, B*, C] +``` + +``` +navigation.selectLast() +``` + +```title="After" +[A, B, C*] +``` + +### select(index) + +Selects the component at the specified [index]. Throws [IllegalArgumentException] if the index is out of bounds. + +```title="Before" +[A*, B, C] +``` + +``` +navigation.select(2) +``` + +```title="After" +[A, B, C*] +``` + +### clear + +Clears the current [Pages] state, i.e. removes all components. + +```title="Before" +[A, B*, C] +``` + +``` +navigation.clear() +``` + +```title="After" +[] +``` diff --git a/docs/navigation/pages/overview.md b/docs/navigation/pages/overview.md new file mode 100644 index 000000000..a2a7d9742 --- /dev/null +++ b/docs/navigation/pages/overview.md @@ -0,0 +1,101 @@ +# Child Pages overview + +## The Child Pages (experimental) + +`Child Pages` is a navigation model for managing a list of components (pages) with one selected (active) component. The list can be empty. + +!!!warning + This navigation model is experimental, the API is subject to change. + +Similarly to `Child Stack`, each component has its own `Lifecycle`. By default, the currently selected page is `ACTIVE`, its two neighbours are `INACTIVE`, and the rest are `DESTROYED`. You can implement your own logic, for example with circular behaviour. + +It is possible to have more than one `Child Pages` navigation model in a component, nested navigation is also supported. + +The `Child Pages` navigation model consists of two main entities: + +- [Pages](https://github.com/arkivanov/Decompose/blob/master/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/pages/Pages.kt) - represents a state of the `Child Pages` navigation model. The navigation is performed by creating a new navigation state from the previous one. + - `Pages#items` - the list of child configurations, must be unique, can be empty. + - `Pages#selectedIndex` - index of the selected child configuration. +- [ChildPages](https://github.com/arkivanov/Decompose/blob/master/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/pages/ChildPages.kt) - a simple data class that stores a list of components and their configurations, as well as the currently selected index. + - `ChildPages#items` - the list of child component, can be empty. + - `ChildStack#selectedIndex` - the index of the currently selected child component. Must be within the range of `items` indices if `items` is not empty, otherwise can be any number. +- [PagesNavigation](https://github.com/arkivanov/Decompose/blob/master/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/pages/PagesNavigation.kt) - an interface that accepts navigation commands and forwards them to all subscribed observers. + +### Component Configurations + +Similarly to `Child Stack`, each component created and managed by the `Child Pages` has a configuration, please read the documentation about [child configurations](/Decompose/navigation/overview/#component-configurations-and-child-factories). + +`Child Pages` adds one additional requirement for child configurations: + +- Configurations must be unique (by equality) within `Child Pages`. + +### Initializing Child Pages + +There are three steps to initialize `Child Pages`: + +- Create a new instance of `PagesNavigation` and assign it to a variable or a property. +- Initialize the `Child Pages` navigation model using the `ComponentContext#childPages` extension function and pass `PagesNavigation` into it along with other arguments. +- The `childPages` function returns `Value` that can be observed in the UI. Assign the returned `Value` to another property or a variable. + +## Example + +Here is a very basic example of a pager-like navigation: + +```kotlin title="PageComponent" +interface PageComponent { + val data: String +} + +class DefaultPageComponent( + componentContext: ComponentContext, + override val data: String, +) : PageComponent, 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, componentContext -> + DefaultPageComponent( + componentContext = componentContext, + data = config.data, + ) + } + + override fun selectPage(index: Int) { + navigation.select(index = index) + } + + @Parcelize + private data class Config(val data: String) : Parcelable +} +``` + +## Screen recreation and process death on (not only) Android + +`Child Pages` automatically preserves the state when a configuration change or process death occurs. Use the `persistent` argument to disable state preservation completely. When disabled, the state is reset to the initial state when recreated. + +Components are created in their order. E.g. the first component in the list is created first, then the next component in the list is created, and so on. Components are destroyed in reverse order. + +## Multiple Child Pages in a component + +When multiple `Child Pages` are used in one component, each such `Child Pages` must have a unique `key` argument associated. The keys are required to be unique only within the hosting component, so it is ok for different components to have `Child Pages` with same keys. An exception will be thrown if multiple `Child PAges` with the same key are detected in a component. diff --git a/mkdocs.yml b/mkdocs.yml index 5e5a39c48..e7da2e032 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -43,6 +43,9 @@ nav: - Overview: navigation/slot/overview.md - Navigation: navigation/slot/navigation.md - Custom ComponentContext: navigation/slot/component-context.md + - Child Pages: + - Overview: navigation/pages/overview.md + - Navigation: navigation/pages/navigation.md - Generic Navigation: - Overview: navigation/children/overview.md