Skip to content

Commit e1b140d

Browse files
author
Peter Bryant
committed
✨ Implement rememberSaveableBloc
1 parent 0683340 commit e1b140d

File tree

5 files changed

+121
-5
lines changed

5 files changed

+121
-5
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.ptrbrynt.kotlin_bloc.compose
2+
3+
import androidx.compose.runtime.saveable.Saver
4+
import androidx.compose.runtime.saveable.SaverScope
5+
import com.ptrbrynt.kotlin_bloc.core.BlocBase
6+
7+
/**
8+
* A [Saver] which enables the state of a Bloc or Cubit to be saved
9+
* and restored.
10+
*/
11+
internal fun <State : Any, B : BlocBase<State>> blocSaver(
12+
save: SaverScope.(B) -> State = { it.state },
13+
restore: (State) -> B,
14+
) = Saver(
15+
save = save,
16+
restore = { restore(it) },
17+
)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.ptrbrynt.kotlin_bloc.compose
2+
3+
import android.os.Bundle
4+
import android.os.Parcelable
5+
import androidx.compose.runtime.Composable
6+
import androidx.compose.runtime.saveable.SaverScope
7+
import androidx.compose.runtime.saveable.rememberSaveable
8+
import com.ptrbrynt.kotlin_bloc.core.BlocBase
9+
10+
/**
11+
* Remembers the [State] of a Bloc or Cubit.
12+
*
13+
* The [State] will survive activity or process recreation (e.g. when the screen is rotated).
14+
*
15+
* You must provide a [create] function, which should return an instance of the Bloc or Cubit
16+
* with the given state as its initial state.
17+
*
18+
* If the [State] **cannot** be saved in a [Bundle] (i.e. it's not a primitive or [Parcelable])
19+
* then you should provide a custom [save] function which converts your [State] into something which
20+
* can be saved in a [Bundle].
21+
*
22+
* If you omit the [save] parameter, [rememberSaveableBloc] assumes that the [State] type can be
23+
* saved in a [Bundle].
24+
*
25+
* ```kotlin
26+
* enum class CounterEvent { Incremented, Decremented }
27+
*
28+
* class CounterBloc(initial: Int): Bloc<CounterEvent, Int>(initial) {
29+
*
30+
* init {
31+
* on<CounterEvent> {
32+
* // ...
33+
* }
34+
* }
35+
* }
36+
*
37+
* @Composable
38+
* fun Counter() {
39+
* val bloc = rememberSaveableBloc(initialState = 0) { CounterBloc(it) }
40+
*
41+
* // ...
42+
* }
43+
* ```
44+
*
45+
* @throws AssertionError if no [save] parameter is provided and the [State] type cannot be saved.
46+
*/
47+
@Composable
48+
fun <State : Any, B : BlocBase<State>> rememberSaveableBloc(
49+
save: SaverScope.(B) -> State = {
50+
assert(canBeSaved(it))
51+
it.state
52+
},
53+
initialState: State,
54+
create: (State) -> B,
55+
) = rememberSaveable(saver = blocSaver(save, create), init = { create(initialState) })

docs/bloc-compose.md

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,46 @@ fun Counter() {
108108
}
109109
```
110110

111-
At this point, we have successfully separated our presentation layer from our business logic layer. Notice that the `Counter` composable knows nothing about what happens when a user taps the buttons. The widget simply tells the `CounterBloc` that the user has pressed the Increment button.
111+
At this point, we have successfully separated our presentation layer from our business logic layer. Notice that the `Counter` composable knows nothing about what happens when a user taps the buttons. The widget simply tells the `CounterBloc` that the user has pressed the Increment button.
112+
113+
## Surviving process recreation
114+
115+
The above examples use the `remember` method to persist the instance of `CounterBloc` through re-compositions. However, if the activity/process is recreated (e.g. by the screen being rotated), the `state` of the `CounterBloc` is lost and it reverts to its initial state.
116+
117+
We can prevent this by using the `rememberSaveableBloc` method.
118+
119+
There are a couple of prerequisites:
120+
121+
1. Your `State` class must support being saved in a `Bundle`. In other words, it should be a primitive or a `Parcelable`.
122+
* You can use the [`@Parcelize`](https://github.com/Kotlin/KEEP/blob/master/proposals/extensions/android-parcelable.md) annotation to easily make your state class parcelable
123+
2. Your Bloc must take its initial state as a constructor argument.
124+
125+
So let's tweak our `CounterBloc` to support being saved:
126+
127+
```kotlin
128+
enum class CounterEvent { Incremented }
129+
130+
class CounterBloc(initial: Int): Bloc<CounterEvent, Int>(initial) {
131+
init {
132+
on<CounterEvent> { event ->
133+
when (event) {
134+
CounterEvent.Incremented -> emit(state + 1)
135+
}
136+
}
137+
}
138+
}
139+
```
140+
141+
?> Since our state is an `Int`, it is already saveable in a `Bundle`.
142+
143+
We can now use the `rememberSaveableBloc` method to persist the current `state` through configuration changes:
144+
145+
```kotlin
146+
@Composable
147+
fun Counter() {
148+
val bloc = rememberSaveableBloc(initialState = 0) { CounterBloc(it) }
149+
150+
// Use the bloc as normal
151+
}
152+
```
153+

sample/src/main/java/com/ptrbrynt/kotlin_bloc/sample/MainActivity.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import androidx.compose.ui.tooling.preview.Preview
2323
import com.ptrbrynt.kotlin_bloc.compose.BlocComposer
2424
import com.ptrbrynt.kotlin_bloc.compose.BlocListener
2525
import com.ptrbrynt.kotlin_bloc.compose.BlocSelector
26+
import com.ptrbrynt.kotlin_bloc.compose.rememberSaveableBloc
2627
import com.ptrbrynt.kotlin_bloc.core.BlocBase
2728
import com.ptrbrynt.kotlin_bloc.sample.ui.blocs.CounterBloc
2829
import com.ptrbrynt.kotlin_bloc.sample.ui.blocs.CounterEvent
@@ -44,9 +45,9 @@ class MainActivity : ComponentActivity() {
4445
* Creates a Counter based on [CounterBloc]
4546
*/
4647
@Composable
47-
4848
fun BlocCounter() {
49-
val bloc = remember { CounterBloc() }
49+
val bloc = rememberSaveableBloc(initialState = 0) { CounterBloc(it) }
50+
5051
CounterBase(
5152
bloc,
5253
onIncrement = {
@@ -106,7 +107,7 @@ fun CounterBase(
106107
fun BlocSelectorCounter(
107108
scaffoldState: ScaffoldState = rememberScaffoldState(),
108109
) {
109-
val bloc = remember { CounterBloc() }
110+
val bloc = rememberSaveableBloc(initialState = 0) { CounterBloc(it) }
110111

111112
Scaffold(
112113
scaffoldState = scaffoldState,

sample/src/main/java/com/ptrbrynt/kotlin_bloc/sample/ui/blocs/CounterBloc.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import com.ptrbrynt.kotlin_bloc.core.Bloc
44

55
enum class CounterEvent { Increment, Decrement }
66

7-
class CounterBloc : Bloc<CounterEvent, Int>(0) {
7+
class CounterBloc(initial: Int) : Bloc<CounterEvent, Int>(initial) {
8+
89
init {
910
on<CounterEvent> { event ->
1011
when (event) {

0 commit comments

Comments
 (0)