Skip to content

Commit

Permalink
✨ Implement rememberSaveableBloc
Browse files Browse the repository at this point in the history
  • Loading branch information
Peter Bryant committed Oct 5, 2021
1 parent 0683340 commit e1b140d
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.ptrbrynt.kotlin_bloc.compose

import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import com.ptrbrynt.kotlin_bloc.core.BlocBase

/**
* A [Saver] which enables the state of a Bloc or Cubit to be saved
* and restored.
*/
internal fun <State : Any, B : BlocBase<State>> blocSaver(
save: SaverScope.(B) -> State = { it.state },
restore: (State) -> B,
) = Saver(
save = save,
restore = { restore(it) },
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.ptrbrynt.kotlin_bloc.compose

import android.os.Bundle
import android.os.Parcelable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.saveable.SaverScope
import androidx.compose.runtime.saveable.rememberSaveable
import com.ptrbrynt.kotlin_bloc.core.BlocBase

/**
* Remembers the [State] of a Bloc or Cubit.
*
* The [State] will survive activity or process recreation (e.g. when the screen is rotated).
*
* You must provide a [create] function, which should return an instance of the Bloc or Cubit
* with the given state as its initial state.
*
* If the [State] **cannot** be saved in a [Bundle] (i.e. it's not a primitive or [Parcelable])
* then you should provide a custom [save] function which converts your [State] into something which
* can be saved in a [Bundle].
*
* If you omit the [save] parameter, [rememberSaveableBloc] assumes that the [State] type can be
* saved in a [Bundle].
*
* ```kotlin
* enum class CounterEvent { Incremented, Decremented }
*
* class CounterBloc(initial: Int): Bloc<CounterEvent, Int>(initial) {
*
* init {
* on<CounterEvent> {
* // ...
* }
* }
* }
*
* @Composable
* fun Counter() {
* val bloc = rememberSaveableBloc(initialState = 0) { CounterBloc(it) }
*
* // ...
* }
* ```
*
* @throws AssertionError if no [save] parameter is provided and the [State] type cannot be saved.
*/
@Composable
fun <State : Any, B : BlocBase<State>> rememberSaveableBloc(
save: SaverScope.(B) -> State = {
assert(canBeSaved(it))
it.state
},
initialState: State,
create: (State) -> B,
) = rememberSaveable(saver = blocSaver(save, create), init = { create(initialState) })
44 changes: 43 additions & 1 deletion docs/bloc-compose.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,46 @@ fun Counter() {
}
```

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.
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.

## Surviving process recreation

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.

We can prevent this by using the `rememberSaveableBloc` method.

There are a couple of prerequisites:

1. Your `State` class must support being saved in a `Bundle`. In other words, it should be a primitive or a `Parcelable`.
* 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
2. Your Bloc must take its initial state as a constructor argument.

So let's tweak our `CounterBloc` to support being saved:

```kotlin
enum class CounterEvent { Incremented }

class CounterBloc(initial: Int): Bloc<CounterEvent, Int>(initial) {
init {
on<CounterEvent> { event ->
when (event) {
CounterEvent.Incremented -> emit(state + 1)
}
}
}
}
```

?> Since our state is an `Int`, it is already saveable in a `Bundle`.

We can now use the `rememberSaveableBloc` method to persist the current `state` through configuration changes:

```kotlin
@Composable
fun Counter() {
val bloc = rememberSaveableBloc(initialState = 0) { CounterBloc(it) }

// Use the bloc as normal
}
```

Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import androidx.compose.ui.tooling.preview.Preview
import com.ptrbrynt.kotlin_bloc.compose.BlocComposer
import com.ptrbrynt.kotlin_bloc.compose.BlocListener
import com.ptrbrynt.kotlin_bloc.compose.BlocSelector
import com.ptrbrynt.kotlin_bloc.compose.rememberSaveableBloc
import com.ptrbrynt.kotlin_bloc.core.BlocBase
import com.ptrbrynt.kotlin_bloc.sample.ui.blocs.CounterBloc
import com.ptrbrynt.kotlin_bloc.sample.ui.blocs.CounterEvent
Expand All @@ -44,9 +45,9 @@ class MainActivity : ComponentActivity() {
* Creates a Counter based on [CounterBloc]
*/
@Composable

fun BlocCounter() {
val bloc = remember { CounterBloc() }
val bloc = rememberSaveableBloc(initialState = 0) { CounterBloc(it) }

CounterBase(
bloc,
onIncrement = {
Expand Down Expand Up @@ -106,7 +107,7 @@ fun CounterBase(
fun BlocSelectorCounter(
scaffoldState: ScaffoldState = rememberScaffoldState(),
) {
val bloc = remember { CounterBloc() }
val bloc = rememberSaveableBloc(initialState = 0) { CounterBloc(it) }

Scaffold(
scaffoldState = scaffoldState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import com.ptrbrynt.kotlin_bloc.core.Bloc

enum class CounterEvent { Increment, Decrement }

class CounterBloc : Bloc<CounterEvent, Int>(0) {
class CounterBloc(initial: Int) : Bloc<CounterEvent, Int>(initial) {

init {
on<CounterEvent> { event ->
when (event) {
Expand Down

0 comments on commit e1b140d

Please sign in to comment.