Skip to content

Commit

Permalink
feat: Split documentation into better sections
Browse files Browse the repository at this point in the history
  • Loading branch information
Nek-12 committed Apr 26, 2024
1 parent 3ca3ee7 commit ced1746
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 215 deletions.
22 changes: 13 additions & 9 deletions docs/_navbar.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
* [Home](/)
* [Quickstart](quickstart.md)
* [Prebuilt Plugins](plugins.md)
* [Custom Plugins](custom_plugins.md)
* [Android](android.md)
* [Saving State](savedstate.md)
* [Remote Debugging](debugging.md)
* [Essenty integration](essenty.md)
* [FAQ](faq.md)
* [Javadocs](https://opensource.respawn.pro/FlowMVI/javadocs/index.html)
* [Contributing](CONTRIBUTING.md)
* [Compose](compose.md)
* Plugins
* [Prebuilt Plugins](plugins.md)
* [Custom Plugins](custom_plugins.md)
* [Saving State](savedstate.md)
* [Remote Debugging](debugging.md)
* Integrations
* [Android](android.md)
* [Essenty](essenty.md)
* Misc
* [FAQ](faq.md)
* [Javadocs](https://opensource.respawn.pro/FlowMVI/javadocs/index.html)
* [Contributing](CONTRIBUTING.md)
87 changes: 3 additions & 84 deletions docs/android.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,91 +101,10 @@ DI framework will fail, likely in runtime.
This is a more robust and multiplatform friendly approach that is slightly more boilerplatish but does not require you
to subclass ViewModels. This example is also demonstrated in the sample app.

## UI Layer
## View Integration

It doesn't matter which UI framework you use. Neither your Contract nor your `Container` will change in any way.

### Compose

!> Compose does not play well with MVVM+ style because of the instability of the `LambdaIntent` and `ViewModel` classes.
It is discouraged to use Lambda intents with Compose as that will not only leak the context of the store but
also degrade performance.

?> Compose stability configuration has been added in 1.6.0, and the library specifies stability rules with itself.
`MVIState`, `MVIIntent`, `MVIAction`, `LambdaIntent` and `Store` are marked as `@Stable`. It's still best to
annotate your contract with `@Immutable` explicitly however to ensure that the compiler pics up the mapping.
See [quickstart](quickstart.md) to learn how to configure stability yourself.

```kotlin
@Composable
fun CounterScreen() {
// using Koin DSL from above
val store = storeViewModel<CounterContainer, _, _, _>()

val state by store.subscribe(DefaultLifecycle) { action ->
when (action) {
is ShowMessage -> {
/* ... */
}
}
}
CounterScreenContent(store, state)
}

@Composable
private fun IntentReceiver<CounterIntent>.CounterScreenContent(state: CounterState) {
when (state) {
is DisplayingCounter -> {
Button(onClick = { intent(ClickedCounter) }) { // intent() available from the receiver parameter
Text("Counter: ${state.counter}")
}
}
/* ... */
}
}
```

Under the hood, the `subscribe` function will efficiently subscribe to the store (it is lifecycle-aware) and
use the composition scope to process your events. Event processing will stop in `onPause()` of the parent activity.
In `onResume()`, the composable will resubscribe. Your composable will recompose when state changes, but not
resubscribe to events. The lifecycle state is customizable.

?> Compose plays well with MVI style because state changes will trigger recompositions. Just mutate your state,
and the UI will update to reflect changes.

* Use the lambda parameter of `subscribe` to subscribe to `MVIActions`.
Those will be processed as they arrive and the `consume` lambda
will **suspend** until an action is processed. Use a receiver coroutine scope to
launch new coroutines that will parallelize your flow (e.g. for snackbars).
* A best practice is to make your state handling (UI redraw composable) a pure function and extract it to a separate
Composable such as `ScreenContent(state: ScreenState)` to keep your `*Screen` function clean, as shown above.
* If you want to send `MVIIntent`s from a nested composable, just use `IntentReceiver` as a context or pass a function
reference.

If you have defined your `*Content` function, you will get a composable that can be easily used in previews.
That composable will not need DI, Local Providers from compose, or anything else for that matter, to draw itself.
But there's a catch: It has an `IntentReceiver<I>` as a parameter. To deal with this, there is an `EmptyReceiver`
composable. EmptyReceiver does nothing when an intent is sent, which
is exactly what we want for previews. We can now define our `PreviewParameterProvider` and the Preview composable.

```kotlin
private class PreviewProvider : StateProvider<CounterState>(
DisplayingCounter(1, 2, "param"),
Loading,
)

@Composable
@Preview
private fun CounterScreenPreview(
@PreviewParameter(PreviewProvider::class) state: CounterState,
) = EmptyReceiver {
ComposeScreenContent(state)
}
```

## View

For a View-based project, the logic is essentially the same.
For a View-based project, subscribe in an appropriate lifecycle callback and create two functions to render states
and consume actions.

* Subscribe in `Fragment.onViewCreated` or `Activity.onCreate`. The library will handle the lifecycle for you.
* Make sure your `render` function is pure, and `consume` function does not loop itself with intents.
Expand Down
142 changes: 142 additions & 0 deletions docs/compose.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Compose and Lifecycle Integration

## Step 1: Add Dependencies

![Maven Central](https://img.shields.io/maven-central/v/pro.respawn.flowmvi/core?label=Maven%20Central)

```toml
[versions]
flowmvi = "< Badge above 👆🏻 >"

[dependencies]
flowmvi-compose = { module = "pro.respawn.flowmvi:compose", version.ref = "flowmvi" }
```

## Step 2: Configure the Compiler

Set up stability definitions for your project:

<details>
<summary>project_root/stability_definitions.txt</summary>

```text
pro.respawn.flowmvi.api.MVIIntent
pro.respawn.flowmvi.api.MVIState
pro.respawn.flowmvi.api.MVIAction
pro.respawn.flowmvi.api.Store
pro.respawn.flowmvi.api.Container
pro.respawn.flowmvi.api.ImmutableStore
pro.respawn.flowmvi.dsl.LambdaIntent
pro.respawn.flowmvi.api.SubscriberLifecycle
pro.respawn.flowmvi.api.IntentReceiver
```

</details>

Then configure compose compiler to account for the definitions in your root `build.gradle.kts`:

<details>
<summary>/build.gradle.kts</summary>

```kotlin
allprojects {
tasks.withType<KotlinCompile>().configureEach {
compilerOptions {
freeCompilerArgs.addAll(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:stabilityConfigurationPath=" +
"${rootProject.rootDir.absolutePath}/stability_definitions.txt"
)
}
}
}
```

</details>

Now the states/intents you create will be stable in compose. Immutability of these classes is already required by the
library, so this will ensure you get the best performance.

## Step 3: Subscribe to Stores

!> Compose does not play well with MVVM+ style because of the instability of the `LambdaIntent` and `ViewModel` classes.
It is discouraged to use Lambda intents with Compose as that will not only leak the context of the store but
also degrade performance.

Subscribing to a store is as simple as calling `subscribe()`

```kotlin
@Composable
fun CounterScreen(
container: CounterContainer,
) = with(container.store) {

val state by subscribe(DefaultLifecycle) { action ->
when (action) {
is ShowMessage -> {
/* ... */
}
}
}

CounterScreenContent(state)
}
```

Under the hood, the `subscribe` function will efficiently subscribe to the store (it is lifecycle-aware) and
use the composition scope to process your events. Event processing will stop when the UI is no longer visible (by
default). When the UI is visible again, the function will re-subscribe. Your composable will recompose when the state
changes.

Use the lambda parameter of `subscribe` to subscribe to `MVIActions`. Those will be processed as they arrive and
the `consume` lambda will **suspend** until an action is processed. Use a receiver coroutine scope to
launch new coroutines that will parallelize your flow (e.g. for snackbars).

## Step 4: Create Pure UI Composables

A best practice is to make your state handling (UI redraw composable) a pure function and extract it to a separate
Composable such as `ScreenContent(state: ScreenState)` to keep your `*Screen` function clean, as shown below.
It will also enable smart-casting by the compiler. If you want to send `MVIIntent`s from a nested composable, just
use `IntentReceiver` as a context or pass a function reference:

```kotlin
@Composable
private fun IntentReceiver<CounterIntent>.CounterScreenContent(state: CounterState) {
when (state) {
is DisplayingCounter -> {
Button(onClick = { intent(ClickedCounter) }) { // intent() available from the receiver parameter
Text("Counter: ${state.counter}")
}
}
/* ... */
}
}
```

## Step 5: Create Previews

When you have defined your `*Content` function, you will get a composable that can be easily used in previews.
That composable will not need DI, Local Providers from compose, or anything else for that matter, to draw itself.
But there's a catch: It has an `IntentReceiver<I>` as a parameter. To deal with this, there is an `EmptyReceiver`
composable. EmptyReceiver does nothing when an intent is sent, which is exactly what we want for previews. We can now
define our `PreviewParameterProvider` and the Preview composable.

```kotlin
// vararg preview provider for convenience
open class PreviewProvider<T>(
vararg values: T,
) : CollectionPreviewParameterProvider<T>(values.toList())

private class StateProvider : PreviewProvider<CounterState>(
DisplayingCounter(counter = 1),
Loading,
)

@Composable
@Preview
private fun CounterScreenPreview(
@PreviewParameter(StateProvider::class) state: CounterState,
) = EmptyReceiver {
ComposeScreenContent(state)
}
```
68 changes: 66 additions & 2 deletions docs/plugins.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,68 @@
# An overview of prebuilt plugins
# Getting started with plugins

## Plugin Ordering

!> The order of plugins matters! Changing the order of plugins may completely change how your store works.
Plugins can replace, veto, consume or otherwise change anything in the store.
They can close the store or swallow exceptions!

Consider the following:

```kotlin
val broken = store(Loading) {
reduce {

}
// ❌ - logging plugin will not log any intents
// because they have been consumed by the reduce plugin
install(consoleLoggingPlugin())
}

val working = store(Loading) {
install(consoleLoggingPlugin())

reduce {
// ✅ - logging plugin will get the intent before reduce() is run, and it does not consume the intent
}
}
```

That example was simple, but this rule can manifest in other, not so obvious ways. Consider the following:

```kotlin
val broken = store(Loading) {

serializeState() // ‼️ restores state on start

init {
updateState {
Loading // 🤦‍ and the state is immediately overwritten
}
}

// this happened because serializeState() uses onStart() under the hood, and init does too.
// Init is run after serializeState because it was installed later.
}
// or
val broken = store(Loading) {

install(customUndocumentedPlugin()) // ‼️ you don't know what this plugin does

reduce {
// ❌ intents are not reduced because the plugin consumed them
}
init {
updateState {
// ❌ states are not changed because the plugin veto'd the change
}
action(MyAction) // ❌ actions are replaced with something else
}
}
```

So make sure to consider how your plugins affect the store's logic when using and writing them.

## Prebuilt Plugins

FlowMVI comes with a whole suite of prebuilt plugins to cover the most common development needs.

Expand Down Expand Up @@ -28,7 +92,7 @@ Here's a full list:
* **Literally any plugin** - just call `install { }` and use the plugin's scope to hook up to store events.

All plugins are based on the essential callbacks that FlowMVI allows them to intercept, so most of them contain minimal
amounts of code. They are explained on the [custom plugins page](custom_plugins.md).
amounts of code. The callbacks are explained on the [custom plugins page](custom_plugins.md).

Here's an explanation of how each default plugin works:

Expand Down
Loading

0 comments on commit ced1746

Please sign in to comment.