From f389dd1465e4939476c9af2d325b4a9496699873 Mon Sep 17 00:00:00 2001 From: ikarenkov Date: Wed, 12 Jun 2024 19:13:25 +0400 Subject: [PATCH 1/4] Improved Core-concepts docs, added sample for reducer usage --- Writerside/codeSnippets/SampleAction.kt | 34 +++++- Writerside/topics/Core-concepts.md | 102 +++++++++--------- .../topics/Screen-model-and-view-model.md | 2 - .../modo/sample/screens/MainScreen.kt | 5 +- .../sample/screens/SamplePermanentDialog.kt | 3 +- .../screens/containers/MultiScreenActions.kt | 8 ++ .../screens/containers/SampleMultiScreen.kt | 34 +++++- .../custom/RemovableItemContainerScreen.kt | 28 ++++- 8 files changed, 150 insertions(+), 66 deletions(-) create mode 100644 sample/src/main/java/com/github/terrakok/modo/sample/screens/containers/MultiScreenActions.kt diff --git a/Writerside/codeSnippets/SampleAction.kt b/Writerside/codeSnippets/SampleAction.kt index 01aee09..9ba3d3d 100644 --- a/Writerside/codeSnippets/SampleAction.kt +++ b/Writerside/codeSnippets/SampleAction.kt @@ -3,8 +3,40 @@ fun interface SampleAction : ReducerAction { override fun reduce(oldState: SampleState): SampleState = oldState.copy(screen3 = null) } + class CreateScreen : SampleAction { override fun reduce(oldState: SampleState): SampleState = oldState.copy(screen3 = NestedScreen(canBeRemoved = true)) } -} \ No newline at end of file +} + +sealed interface SampleAction : NavigationAction { + class Remove : SampleAction + class CreateScreen : SampleAction +} + +@Parcelize +internal class RemovableItemContainerScreen( + private val navModel: NavModel = NavModel( + RemovableItemContainerState( + NestedScreen(canBeRemoved = false), + NestedScreen(canBeRemoved = false), + NestedScreen(canBeRemoved = true), + NestedScreen(canBeRemoved = false), + ) + ) +) : ContainerScreen(navModel) { + + override val reducer: NavigationReducer = NavigationReducer { action, state -> + when (action) { + is RemovableItemContainerAction.Remove -> { + state.copy(screen3 = null) + } + is RemovableItemContainerAction.CreateScreen -> { + state.copy(screen3 = NestedScreen(canBeRemoved = true)) + } + } + + } + +} diff --git a/Writerside/topics/Core-concepts.md b/Writerside/topics/Core-concepts.md index f64d649..1bafa57 100644 --- a/Writerside/topics/Core-concepts.md +++ b/Writerside/topics/Core-concepts.md @@ -1,7 +1,7 @@ -# Core concepts +# Core Concepts -Modo is a state-based navigation library for jetpack compose. It represents UI as a structure of `Screen`s and `ContainerScreen`s (which is an -implementation of `Screen`). +Modo is a state-based navigation library for Jetpack Compose. It represents the UI as a structure of `Screen`s and `ContainerScreen`s (which are +implementations of `Screen`). @@ -9,47 +9,48 @@ implementation of `Screen`). ## Screen -`Screen` is a basic unit of UI. It displays content defined in overridden `fun Content(modifier: Modifier)` +A `Screen` is the basic unit of the UI. It displays content defined in the overridden `fun Content(modifier: Modifier)`. ```kotlin ``` { src="SampleScreen.kt" include-lines="1-20"} -### Screen key +### Screen Key -Each screen is identified by `ScreenKey` - a unique key that represents the screen. -This key is widely used in internal implementation. It is required to be unique per a screen, even after process death. For this you must use -build-in `generateScreenKey` function. +Each screen is identified by a `ScreenKey` - a unique key representing the screen. This key is extensively used in internal implementation. It must be +unique for each screen, even after process death. To ensure this, use the built-in `generateScreenKey` function. ### Arguments { id="arguments"} -To pass arguments to the screen, declare it in the Screen's constructor: - -```Kotlin +To pass arguments to the screen, declare them in the Screen's constructor: + +```kotlin ``` { src="SampleScreen.kt" include-lines="2-5,7-8" } -### Saving and restoring +### Saving and Restoring -Each screen is `Parcelable`, that helps to save and restore it during lifecycle changes. Use -parcelable [gradle plugin](https://developer.android.com/kotlin/parcelize) and `@Parcelable` annotation to generate `Parcelable` +Each screen is `Parcelable`, which helps to save and restore it during lifecycle changes. Use the +parcelable [Gradle plugin](https://developer.android.com/kotlin/parcelize) and the `@Parcelize` annotation to generate the `Parcelable` implementation on the fly. -It's vital to use build-in function like `rememberRootScreen` to integrate Modo with your application. Read [](How-to-integrate-modo-to-your-app.md) -for details. +It's crucial to use built-in functions like `rememberRootScreen` to integrate Modo with your application. +Read [](How-to-integrate-modo-to-your-app.md) for details. -## ContainerScreen { id="container-screen" } +## ContainerScreen -`ContainerScreen`s are the type of screens that can contain other screens. It's a basic building block for complex navigation structures. -[`StackScreen`](StackScreen.md) and `MultiScreen` are build-in implementations of `ContainerScreen`. +{ id="container-screen" } + +`ContainerScreen`s are types of screens that can contain other screens. They are fundamental building blocks for complex navigation +structures. [`StackScreen`](StackScreen.md) and `MultiScreen` are built-in implementations of `ContainerScreen`. ![diagram_2.png](diagram_2.png){ height = 300 } -Each ContainerScreen is defined by 2 typed parameters: State and Action. +Each ContainerScreen is defined by two typed parameters: State and Action. -```Kotlin +```kotlin @Stable abstract class ContainerScreen>( private val navModel: NavModel @@ -61,7 +62,7 @@ abstract class ContainerScreen State

-NavigationState - class that can contains nested screens and other additional info. State can be updated by calling dispatch(action). +NavigationState - a class that can contain nested screens and other additional information. The state can be updated by calling dispatch(action).

@Parcelize @@ -70,54 +71,55 @@ data class SampleState( val screen2: Screen, val screen3: Screen?, ) : NavigationState { - // You need to return all nested screens in order to provide correct work. + // You need to return all nested screens to ensure correct functionality. override fun getChildScreens(): List = listOfNotNull(screen1, screen2, screen3) } + +Read the State Update section for more details. + Action

-NavigationAction - marker interface to distinguish actions for this container on specific State. You can also use -ReducerAction for defining actions with update function in-place: - +NavigationAction - a marker interface to distinguish actions for this container on a specific State. You can also use +ReducerAction to define actions with an in-place update function: +

- -Updating State -

-To update state of ContainerScreen you must use dispatch(action: Action). -There are 2 ways how to define you action: -

- -(Recommended) ReducerAction - allows to define update function in-place. - - - -Custom reducer + Action. You can provide a reducer in you ContainerScreen implementation. -TODO - +`NavModel` - a state storage responsible for state updates and triggering UI updates. Each ContainerScreen has a NavModel as a constructor parameter. -
+### Rendering Nested Screens + +To render nested screens inside a container screen, you **must** use the `InternalContent` function. This function provides all necessary +integrations, such as: + +* Correct usage of `rememberSaveable` inside nested screens by using `SaveableStateHolder`. +* Integration of `ScreenModel`, ensuring consistency for the same screen and clearing it when the `Screen` leaves the hierarchy. +* Android integration, including `Lifecycle` and `ViewModel` support. + +The built-in `StackScreen` and `MultiScreen` use `InternalContent` under the hood to ensure correct nested screen functionality. + +## State Update +To update the state of a `ContainerScreen`, use `dispatch(action: Action)`. +There are two ways to define your action: -`NavModel` - storage of state, that responsible for state updates and triggering updating UI. Each ContainerScreen has a NavModel as a constructor -parameter. +### ReducerAction (Recommended) -To render nested screens inside a container screen, you **must** use `InternalContent` function. This function provides all necessary integrations, -like: +ReducerAction allows defining the update function in-place. + -* Correct work of `rememberSaveable` inside nested screens by using `SaveableStateHolder` -* `ScreenModel`'s integration, that should be the same for the same screen and be cleared when `Screen` leaves the hierarchy -* Android integration, like `Lifecycle` and `ViewModel` support +### Custom Reducer + Action -Build-in `StackScreen` and `MultiScreen` uses `InternalContent` under the hood, to provide correct work of nested screens. +You can provide a reducer in your ContainerScreen implementation. + ## Root Screen -To integrate Modo into your application, you use one of the build-in functions from Modo file. It returns a `RootScreen`, that simply provides +To integrate Modo into your application, use one of the built-in functions from the Modo file. It returns a `RootScreen`, which provides a `SaveableStateHolder`. \ No newline at end of file diff --git a/Writerside/topics/Screen-model-and-view-model.md b/Writerside/topics/Screen-model-and-view-model.md index d3222fd..74b528d 100644 --- a/Writerside/topics/Screen-model-and-view-model.md +++ b/Writerside/topics/Screen-model-and-view-model.md @@ -18,6 +18,4 @@ To use it inside screen, call `rememberScreenModel` inside `Content` function: You can use `ViewModel` from Android Jetpack in Modo: - - \ No newline at end of file diff --git a/sample/src/main/java/com/github/terrakok/modo/sample/screens/MainScreen.kt b/sample/src/main/java/com/github/terrakok/modo/sample/screens/MainScreen.kt index 8d4f442..c34b8d9 100644 --- a/sample/src/main/java/com/github/terrakok/modo/sample/screens/MainScreen.kt +++ b/sample/src/main/java/com/github/terrakok/modo/sample/screens/MainScreen.kt @@ -152,7 +152,10 @@ private fun rememberButtons( ModoButtonSpec("Multiscreen") { navigation?.forward(SampleMultiScreen()) }, ModoButtonSpec("Screen Effects") { navigation?.forward(ScreenEffectsSampleScreen(i + 1)) }, ModoButtonSpec("Custom Container Actions") { navigation?.forward(SampleCustomContainerScreen()) }, - ModoButtonSpec("Custom Container") { navigation?.forward(RemovableItemContainerScreen()) }, + ModoButtonSpec("Removable screen") { navigation?.forward(RemovableItemContainerScreen(useCustomReducer = false)) }, + ModoButtonSpec("Removable screen with reducer") { + navigation?.forward(RemovableItemContainerScreen(useCustomReducer = true)) + }, // Just experiments // ModoButtonSpec("2 items screen") { navigation.forward(TwoTopItemsStackScreen(i + 1)) }, // ModoButtonSpec("Demo") { navigation.forward(SaveableStateHolderDemoScreen()) }, diff --git a/sample/src/main/java/com/github/terrakok/modo/sample/screens/SamplePermanentDialog.kt b/sample/src/main/java/com/github/terrakok/modo/sample/screens/SamplePermanentDialog.kt index 250081c..596dfae 100644 --- a/sample/src/main/java/com/github/terrakok/modo/sample/screens/SamplePermanentDialog.kt +++ b/sample/src/main/java/com/github/terrakok/modo/sample/screens/SamplePermanentDialog.kt @@ -23,8 +23,7 @@ class SamplePermanentDialog( override val screenKey: ScreenKey = generateScreenKey() ) : DialogScreen { - override val permanentDialog: Boolean - get() = true + override val permanentDialog: Boolean get() = true override fun provideDialogConfig(): DialogScreen.DialogConfig = DialogScreen.DialogConfig.System( useSystemDim = true, diff --git a/sample/src/main/java/com/github/terrakok/modo/sample/screens/containers/MultiScreenActions.kt b/sample/src/main/java/com/github/terrakok/modo/sample/screens/containers/MultiScreenActions.kt new file mode 100644 index 0000000..6cbca01 --- /dev/null +++ b/sample/src/main/java/com/github/terrakok/modo/sample/screens/containers/MultiScreenActions.kt @@ -0,0 +1,8 @@ +package com.github.terrakok.modo.sample.screens.containers + +import com.github.terrakok.modo.multiscreen.MultiScreenAction + +/** + * The sample of the action that is handled by reducer + */ +internal class RemoveTab(val pos: Int) : MultiScreenAction \ No newline at end of file diff --git a/sample/src/main/java/com/github/terrakok/modo/sample/screens/containers/SampleMultiScreen.kt b/sample/src/main/java/com/github/terrakok/modo/sample/screens/containers/SampleMultiScreen.kt index 1b182e0..85da55e 100644 --- a/sample/src/main/java/com/github/terrakok/modo/sample/screens/containers/SampleMultiScreen.kt +++ b/sample/src/main/java/com/github/terrakok/modo/sample/screens/containers/SampleMultiScreen.kt @@ -5,14 +5,19 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontStyle @@ -25,13 +30,14 @@ import com.github.terrakok.modo.multiscreen.MultiScreenAction import com.github.terrakok.modo.multiscreen.MultiScreenNavModel import com.github.terrakok.modo.multiscreen.MultiScreenState import com.github.terrakok.modo.multiscreen.selectContainer +import com.github.terrakok.modo.sample.components.CancelButton import com.github.terrakok.modo.sample.screens.MainScreen import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @Suppress("MagicNumber") @Parcelize -class SampleMultiScreen( +internal class SampleMultiScreen( private val navModel: MultiScreenNavModel = MultiScreenNavModel( containers = listOf( SampleStack(MainScreen(1)), @@ -44,7 +50,14 @@ class SampleMultiScreen( @IgnoredOnParcel override val reducer: NavigationReducer = NavigationReducer { action, state -> - if (action is AddTab) state else null + if (action is RemoveTab && action.pos in state.screens.indices) { + state.copy( + screens = state.screens.filterIndexed { index, _ -> index != action.pos }, + selected = if (state.selected == action.pos) 0 else state.selected + ) + } else { + null + } } @Composable @@ -52,9 +65,20 @@ class SampleMultiScreen( var showAllStacks by rememberSaveable { mutableStateOf(false) } - Column { - TopContent(showAllStacks, Modifier.weight(1f)) - Row { + Column(modifier) { + Box(Modifier.weight(1f)) { + TopContent(showAllStacks) + if (navigationState.screens.size > 1) { + CancelButton( + onClick = { dispatch(RemoveTab(navigationState.selected)) }, + contentDescription = "Cansel screen", + modifier = Modifier + .align(Alignment.TopEnd) + .windowInsetsPadding(WindowInsets.statusBars) + ) + } + } + Row(Modifier.windowInsetsPadding(WindowInsets.navigationBars)) { Text( modifier = Modifier .clickable { showAllStacks = !showAllStacks } diff --git a/sample/src/main/java/com/github/terrakok/modo/sample/screens/containers/custom/RemovableItemContainerScreen.kt b/sample/src/main/java/com/github/terrakok/modo/sample/screens/containers/custom/RemovableItemContainerScreen.kt index 37bc01e..09ed0e6 100644 --- a/sample/src/main/java/com/github/terrakok/modo/sample/screens/containers/custom/RemovableItemContainerScreen.kt +++ b/sample/src/main/java/com/github/terrakok/modo/sample/screens/containers/custom/RemovableItemContainerScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.unit.dp import com.github.terrakok.modo.ContainerScreen import com.github.terrakok.modo.LocalContainerScreen import com.github.terrakok.modo.NavModel +import com.github.terrakok.modo.NavigationReducer import com.github.terrakok.modo.NavigationState import com.github.terrakok.modo.ReducerAction import com.github.terrakok.modo.Screen @@ -31,13 +32,13 @@ data class RemovableItemContainerState( override fun getChildScreens(): List = listOfNotNull(screen1, screen2, screen3, screen4) } -internal fun interface RemovableItemContainerAction : ReducerAction { - class Remove : RemovableItemContainerAction { +internal sealed interface RemovableItemContainerAction : ReducerAction { + data object Remove : RemovableItemContainerAction { override fun reduce(oldState: RemovableItemContainerState): RemovableItemContainerState = oldState.copy(screen3 = null) } - class CreateScreen : RemovableItemContainerAction { + data object CreateScreen : RemovableItemContainerAction { override fun reduce(oldState: RemovableItemContainerState): RemovableItemContainerState = oldState.copy(screen3 = NestedScreen(canBeRemoved = true)) } @@ -45,6 +46,7 @@ internal fun interface RemovableItemContainerAction : ReducerAction = NavModel( RemovableItemContainerState( NestedScreen(canBeRemoved = false), @@ -55,6 +57,22 @@ internal class RemovableItemContainerScreen( ) ) : ContainerScreen(navModel) { + override val reducer: NavigationReducer? + get() = if (useCustomReducer) { + NavigationReducer { action, state -> + when (action) { + is RemovableItemContainerAction.Remove -> { + state.copy(screen3 = null) + } + is RemovableItemContainerAction.CreateScreen -> { + state.copy(screen3 = NestedScreen(canBeRemoved = true)) + } + } + } + } else { + null + } + @Composable override fun Content(modifier: Modifier) { Column { @@ -77,7 +95,7 @@ internal class RemovableItemContainerScreen( Column { Button( modifier = Modifier.fillMaxWidth(), - onClick = { dispatch(RemovableItemContainerAction.CreateScreen()) } + onClick = { dispatch(RemovableItemContainerAction.CreateScreen) } ) { Text(text = "Create screen") } @@ -97,7 +115,7 @@ internal class NestedScreen( val parent = LocalContainerScreen.current as RemovableItemContainerScreen InnerContent( title = screenKey.value, - onRemoveClick = takeIf { canBeRemoved }?.let { { parent.dispatch(RemovableItemContainerAction.Remove()) } }, + onRemoveClick = takeIf { canBeRemoved }?.let { { parent.dispatch(RemovableItemContainerAction.Remove) } }, modifier = Modifier .fillMaxWidth() .height(400.dp) From b5d83cdc2b779f0cde5f8fc172a8a87e565d7088 Mon Sep 17 00:00:00 2001 From: ikarenkov Date: Wed, 12 Jun 2024 21:53:11 +0400 Subject: [PATCH 2/4] Improved modo overview page --- Writerside/modo-docs.tree | 2 +- Writerside/topics/ModoOverview.md | 38 ++++++++++++++++--------------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/Writerside/modo-docs.tree b/Writerside/modo-docs.tree index b7210d0..8064bdd 100644 --- a/Writerside/modo-docs.tree +++ b/Writerside/modo-docs.tree @@ -3,7 +3,7 @@ - + diff --git a/Writerside/topics/ModoOverview.md b/Writerside/topics/ModoOverview.md index a073eae..da20fad 100644 --- a/Writerside/topics/ModoOverview.md +++ b/Writerside/topics/ModoOverview.md @@ -1,4 +1,6 @@ -# Modo overview +Here's the improved version of your documentation text: + +# Modo Overview [![Maven Central](https://img.shields.io/maven-central/v/com.github.terrakok/modo-compose)](https://repo1.maven.org/maven2/com/github/terrakok) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) @@ -8,39 +10,40 @@