Skip to content

Commit

Permalink
Updated Readme and docs with Child Pages
Browse files Browse the repository at this point in the history
  • Loading branch information
arkivanov committed May 29, 2023
1 parent d1fe83d commit 6f8fc52
Show file tree
Hide file tree
Showing 10 changed files with 278 additions and 8 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ interface PagesNavigator<C : Any> {
/**
* 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
Expand Down
2 changes: 1 addition & 1 deletion docs/component/back-button.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
34 changes: 32 additions & 2 deletions docs/extensions/compose.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<ChildStack>` 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<ChildStack>` 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:

Expand Down Expand Up @@ -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<ChildPages>` 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.
Expand Down Expand Up @@ -217,7 +247,7 @@ fun RootContent(component: RootComponent) {

<img src="https://raw.githubusercontent.com/arkivanov/Decompose/master/docs/media/ComposeAnimationSeparate.gif" width="512">

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
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/navigation/children/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions docs/navigation/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
134 changes: 134 additions & 0 deletions docs/navigation/pages/navigation.md
Original file line number Diff line number Diff line change
@@ -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<Configuration>()
```

### 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"
[]
```
101 changes: 101 additions & 0 deletions docs/navigation/pages/overview.md
Original file line number Diff line number Diff line change
@@ -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<ChildPages>` 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<ChildPages<*, PageComponent>>

fun selectPage(index: Int)
}

class DefaultPagesComponent(
componentContext: ComponentContext,
) : PagesComponent, ComponentContext by componentContext {

private val navigation = PagesNavigation<Config>()

override val pages: Value<ChildPages<*, PageComponent>> =
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.
3 changes: 3 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit 6f8fc52

Please sign in to comment.