Skip to content

Commit bfc06fb

Browse files
committed
Add documentation for state management
1 parent a4b6c2c commit bfc06fb

10 files changed

Lines changed: 134 additions & 24 deletions

File tree

.idea/runConfigurations/xcodebuild.xml

Lines changed: 0 additions & 7 deletions
This file was deleted.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package io.github.xxfast.decompose.router.app.screens.stack.list
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.lifecycle.ViewModel
5+
import io.github.xxfast.decompose.router.rememberOnRoute
6+
7+
class ListViewModel: ViewModel()
8+
9+
@Composable
10+
fun ListScreen() {
11+
val viewModel: ListViewModel = rememberOnRoute(type = ListViewModel::class) { ListViewModel() }
12+
}

app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/list/ListInstance.kt renamed to app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/list/ListComponent.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.github.xxfast.decompose.router.screens.stack.list
22

3+
import com.arkivanov.decompose.ComponentContext
34
import io.github.xxfast.decompose.router.RouterContext
45
import io.github.xxfast.decompose.router.state
56
import io.github.xxfast.decompose.router.screens.stack.Item
@@ -10,7 +11,7 @@ import kotlinx.coroutines.flow.StateFlow
1011
import kotlinx.coroutines.launch
1112
import kotlin.coroutines.CoroutineContext
1213

13-
class ListInstance(context: RouterContext) : CoroutineScope {
14+
class ListComponent(context: RouterContext) : ComponentContext by context, CoroutineScope {
1415
private val initialState: ListState = context.state(ListState()) { states.value }
1516
private val _state: MutableStateFlow<ListState> = MutableStateFlow(initialState)
1617
val states: StateFlow<ListState> = _state

app/src/commonMain/kotlin/io/github/xxfast/decompose/router/screens/stack/list/ListScreen.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,11 @@ import kotlinx.coroutines.launch
4646
fun ListScreen(
4747
onSelect: (screen: Item) -> Unit,
4848
) {
49-
val instance: ListInstance = rememberOnRoute(ListInstance::class) { context -> ListInstance(context) }
49+
val listComponent: ListComponent = rememberOnRoute(ListComponent::class) { context ->
50+
ListComponent(context)
51+
}
5052

51-
val state: ListState by instance.states.collectAsState()
53+
val state: ListState by listComponent.states.collectAsState()
5254
val listState: LazyListState = rememberLazyListState()
5355
val coroutineScope: CoroutineScope = rememberCoroutineScope()
5456

@@ -67,7 +69,7 @@ fun ListScreen(
6769
floatingActionButton = {
6870
FloatingActionButton(
6971
onClick = {
70-
instance.add()
72+
listComponent.add()
7173
coroutineScope.launch { listState.animateScrollToItem(state.screens.lastIndex) }
7274
},
7375
content = { Icon(Icons.Rounded.Add, null) },

docs/cfg/buildprofiles.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<variables>
88
<noindex-content>false</noindex-content>
99
<color-preset>soft</color-preset>
10-
<primary-color>blue</primary-color>
10+
<primary-color>red</primary-color>
1111
<header-logo>decompose_router.svg</header-logo>
1212
<custom-favicons>fav.svg</custom-favicons>
1313
</variables>

docs/decompose-router.tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@
1212
<toc-element topic="using-stack-navigation.md"/>
1313
<toc-element topic="using-pages-navigation.md"/>
1414
<toc-element topic="using-slot-navigation.md"/>
15-
</toc-element><toc-element topic="Scoping-Instances-to-Screens.md"/>
15+
</toc-element><toc-element topic="managing-screen-state.md"/>
1616
</instance-profile>

docs/topics/Scoping-Instances-to-Screens.md

Lines changed: 0 additions & 3 deletions
This file was deleted.

docs/topics/installation.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,8 @@ decompose-router = { module = "io.github.xxfast:decompose-router", version.ref =
2626
# For Compose Wear
2727
decompose-router-wear = { module = "io.github.xxfast:decompose-router-wear", version.ref = "decompose-router" }
2828

29-
# You will need to also bring in decompose and essenty
30-
decompose = { module = "com.arkivanov.decompose:decompose", version.ref = "decompose" }
29+
# You will need to also bring in decompose extensions for compose-multiplatform
3130
decompose-compose-multiplatform = { module = "com.arkivanov.decompose:extensions-compose-jetbrains", version.ref = "decompose" }
32-
essenty-parcelable = { module = "com.arkivanov.essenty:parcelable", version.ref = "essenty" }
3331
```
3432

3533
**build.gradle.kts**
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# Managing Screen State
2+
3+
<title instance="decompose-router">Managing Screen State</title>
4+
**State** (or UiState) model usually capture everything on a given screen that can change over time. This state is usually contained by a **State Holder** (e.g:- a `Component`, `ViewModel`, `Presenter` or whatever) and mutate that state over time by applying business logic
5+
6+
## Scoping a State Holder to a Router
7+
8+
If you want your screen level state holder to be scoped to a given screen, use `rememberOnRoute`
9+
1. This makes your instances survive configuration changes (on Android)
10+
2. Holds-on the instance as long as it is in the backstack
11+
12+
```kotlin
13+
class List
14+
15+
@Composable
16+
fun ListScreen() {
17+
val list: List = rememberOnRoute(List::class) { _ -> List() }
18+
}
19+
```
20+
21+
If you want this instance to be recomputed, you can also provide a key to it
22+
```kotlin
23+
class Details(val id: String)
24+
25+
@Composable
26+
fun DetailsScreen(id: String) {
27+
val details: Details = rememberOnRoute(Details::class, key = id) { _ -> Details(id) }
28+
}
29+
```
30+
> Due to this [issue](https://github.com/JetBrains/compose-multiplatform/issues/2900), you will still need to provide this
31+
> type `ListScreen:class` manually for now.
32+
> Once resolved, you will be able to use the `inline` `refied` (and nicer) signature
33+
> ```kotlin
34+
> val list: List = rememberOnRoute { _ -> List() }
35+
> ```
36+
{style="warning"}
37+
38+
### Integrating with Decompose Components
39+
40+
Integrating Decompose components works the same way. `RouterContext` can provide you a `ComponentContext` if needed be
41+
42+
```kotlin
43+
import com.arkivanov.decompose.ComponentContext
44+
45+
class ListComponent(context: RouterContext): ComponentContext by context
46+
47+
@Composable
48+
fun MyScreen() {
49+
val listComponent: ListComponent = rememberOnRoute(ListComponent::class) { context ->
50+
ListComponent(context)
51+
}
52+
}
53+
```
54+
{collapsible="true" collapsed-title="class ListComponent(context: RouterContext): com.arkivanov.decompose.ComponentContext by context"}
55+
56+
### Integrating with Android Architecture Component ViewModels
57+
58+
Integrating [AAC `ViewModel`](https://developer.android.com/topic/libraries/architecture/viewmodel)s works the same way. You can scope `ViewModel`s directly and these will be stay on the stack/page/slot as long it is needed
59+
60+
```kotlin
61+
import androidx.lifecycle.ViewModel
62+
63+
class ListViewModel: ViewModel()
64+
65+
@Composable
66+
fun ListScreen() {
67+
val viewModel: ListViewModel = rememberOnRoute(type = ListViewModel::class) { ListViewModel() }
68+
}
69+
```
70+
{collapsible="true" collapsed-title="class ListViewModel: androidx.lifecycle.ViewModel()"}
71+
72+
## Restoring Initial State after System-initiated Process Death
73+
74+
> [System-initiated process death](https://developer.android.com/topic/libraries/architecture/saving-states#onsaveinstancestate) may kill your android process when the system is running out of memory.
75+
>
76+
> Router will restore the screens, backstack and any savable states in them (like scroll position) automatically
77+
>
78+
> However, you will still need to save and restore state withing your state holders
79+
80+
81+
**Initial state** is the first-ever state your screen is rendered with. This is usually the **default state** for your screen.
82+
However, if your app is being restored after system initiated process death, we want to _derive_ the initial state
83+
from **saved state** of the previous process (instead of the default)
84+
85+
Within your **State Holder**, you can derive the initial state by using `RouterContext.state`.
86+
Make sure to point the supplier lambda back to your state flow so that it knows where to grab the latest state from to save
87+
88+
```kotlin
89+
class List(context: RouterContext) {
90+
private val initialState: ListState = context.state(ListState()) { states.value }
91+
private val _state: MutableStateFlow<ListState> = MutableStateFlow(initialState)
92+
val states: StateFlow<ListState> = _state
93+
}
94+
```
95+
96+
### Integrating with Molecule
97+
98+
For [Molecule](https://github.com/cashapp/molecule), initial state can be provided to your `moleculeFlow` in conjunction with `stateIn`
99+
100+
```kotlin
101+
class List(context: RouterContext) {
102+
private val initialState: ListState = context.state(ListState()) { states.value }
103+
val states: StateFlow<EquipmentSelectionState> = moleculeFlow(ContextClock) { ListPresenter() }
104+
.stateIn(this, SharingStarted.Lazily, initialState)
105+
}
106+
```
107+
{collapsible="true" collapsed-title="moleculeFlow(ContextClock) { ListPresenter(initialState) }.stateIn(this, SharingStarted.Lazily, initialState)"}

docs/topics/overview.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ A Compose-multiplatform navigation library that leverage [Decompose](https://git
2525

2626
```kotlin
2727
// Declare your screen configurations for type-safety
28-
@Parcelize
28+
@Serializable
2929
sealed class Screen: Parcelable {
3030
object List : Screen()
3131
data class Details(val detail: String) : Screen()
@@ -55,15 +55,15 @@ fun DetailsScreen(detail: String) {
5555
// This makes your instances survive configuration changes (on android) 🔁
5656
// And holds-on the instance as long as it is in the backstack 🔗
5757
// Pass in key if you want to reissue a new instance when key changes 🔑 (optional)
58-
val instance: DetailInstance = rememberOnRoute(key = detail) { savedState -> DetailInstance(savedState, detail) }
58+
val instance: DetailInstance = rememberOnRoute(key = detail) { context -> DetailInstance(context, detail) }
5959

6060
val state: DetailState by instance.states.collectAsState()
6161
Text(text = state.detail)
6262
}
6363

6464
// If you want your state to survive process death ☠️ derive your initial state from [SavedStateHandle]
65-
class DetailInstance(savedState: SavedStateHandle, detail: String) : InstanceKeeper.Instance {
66-
private val initialState: DetailState = savedState.get() ?: DetailState(detail)
65+
class DetailInstance(context: RouterContext, detail: String) : InstanceKeeper.Instance {
66+
private val initialState: DetailState = context.state(DetailState(detail)) { states.value }
6767
private val stateFlow = MutableStateFlow(initialState)
6868
val states: StateFlow<DetailState> = stateFlow
6969
}
@@ -74,4 +74,4 @@ class DetailInstance(savedState: SavedStateHandle, detail: String) : InstanceKee
7474
<a href="https://proandroiddev.com/diy-compose-multiplatform-navigation-with-decompose-94ac8126e6b5">Medium article that started this off</a>
7575
<a href="https://xxfast.github.io/Decompose-Router/api/">API Doc</a>
7676
</category>
77-
</seealso>
77+
</seealso>

0 commit comments

Comments
 (0)