diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index b840bda..d8780c6 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -33,7 +33,7 @@ jobs:
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Check with gradle
- run: ./gradlew runUnitTests :examples:lce:assembleDebug :examples:welcome:welcome:assembleDebug :examples:multi:navbar:assembleDebug :examples:multi:parallel:assembleDebug :examples:lifecycle:assembleDebug --no-daemon --no-configuration-cache
+ run: ./gradlew runUnitTests :examples:lce:assembleDebug :examples:welcome:welcome:assembleDebug :examples:multi:navbar:assembleDebug :examples:multi:parallel:assembleDebug :examples:lifecycle:assembleDebug :examples:di:app:assembleDebug :examples:book:app:assembleDebug :examples:book:book:demo:assembleDebug --no-daemon --no-configuration-cache
- name: Upload problems report
uses: actions/upload-artifact@v4
if: failure()
diff --git a/.gitignore b/.gitignore
index 0846040..cdbd6ea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,15 +1,7 @@
*.iml
.gradle
/local.properties
-/.idea/artifacts
-/.idea/caches
-/.idea/libraries
-/.idea/modules.xml
-/.idea/workspace.xml
-/.idea/navEditor.xml
-/.idea/assetWizardSettings.xml
-/.idea/copilot
-/.idea/*.xd
+/.idea
.DS_Store
/build
/.kotlin
diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml
deleted file mode 100644
index 4a53bee..0000000
--- a/.idea/AndroidProjectSystem.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml
deleted file mode 100644
index ba530bd..0000000
--- a/.idea/appInsightsSettings.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
deleted file mode 100644
index b589d56..0000000
--- a/.idea/compiler.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/copyright/Apache.xml b/.idea/copyright/Apache.xml
deleted file mode 100644
index e7e1510..0000000
--- a/.idea/copyright/Apache.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml
deleted file mode 100644
index 2102cda..0000000
--- a/.idea/copyright/profiles_settings.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml
deleted file mode 100644
index bcdc4a4..0000000
--- a/.idea/deploymentTargetDropDown.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
deleted file mode 100644
index bf3356d..0000000
--- a/.idea/deploymentTargetSelector.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
deleted file mode 100644
index 2b26efb..0000000
--- a/.idea/gradle.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
deleted file mode 100644
index 103e00c..0000000
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
deleted file mode 100644
index 6d0ee1c..0000000
--- a/.idea/kotlinc.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/migrations.xml b/.idea/migrations.xml
deleted file mode 100644
index f8051a6..0000000
--- a/.idea/migrations.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index b2c751a..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
deleted file mode 100644
index 16660f1..0000000
--- a/.idea/runConfigurations.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/studiobot.xml b/.idea/studiobot.xml
deleted file mode 100644
index 539e3b8..0000000
--- a/.idea/studiobot.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 35eb1dd..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/README.md b/README.md
index 2398ba4..9af699e 100644
--- a/README.md
+++ b/README.md
@@ -40,6 +40,7 @@ Please checkout the Medium article on pattern/library usage.
+ [Gestures and view-states](#gestures-and-view-states)
+ [View implementation](#view-implementation)
+ [Adopting foreign state-flow](#adopting-foreign-state-flow)
+ + [Common flow API](#common-child-flow-api)
- [Running state-machines in parallel (composition)](#running-state-machines-in-parallel-composition)
* [MultiMachineState](#multimachinestate)
* [ProxyMachineContainer](#proxymachinecontainer)
@@ -135,6 +136,7 @@ val commonMain by getting {
- [Navbar](examples/multi/navbar) - several machines running in proxy state, one of them active at a time
- [Mixed](examples/multi/mixed) - two machines of different gesture/UI system mixed in one state
- [Lifecycle](examples/lifecycle) - track your Android app lifecycle to pause pending operations when the app is suspended
+- [DI](examples/di) - late child flow binding, allows for a dynamic module ([read more](#common-child-flow-api))
- [Contacts](https://github.com/Android-Developer-Basic/Contacts) - more or less real world KMP/CMP application with network and database
## The basic task - Load-Content-Error
@@ -1174,6 +1176,42 @@ or to advance to `Complete` state as described in [Common Api](#common-api). The
this interface by switching host machine to email or complete states in corresponding
`backToEmailEntry` and `complete` functions.
+#### Common child flow API
+
+For your convenience there are couple of ready-made interfaces to adopt child flow.
+The interfaces are located in a separate libraries:
+
+```groovy
+dependencies {
+ // If you use it for state machines only
+ implementation "com.motorro.commonstatemachine:commonflow-data:x.x.x"
+ // If you go with compose
+ implementation "com.motorro.commonstatemachine:commonflow-compose:x.x.x"
+}
+```
+
+The libraries include the following interfaces:
+
+- [CommonFlowHost](commonflow/data/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/data/CommonFlowHost.kt) -
+ the interface the proxy should provide to the child flow. Child uses this flow to signal its termination.
+- [CommonFlowDataApi](commonflow/data/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/data/CommonFlowDataApi.kt) -
+ contains methods to initiate the flow and to adapt it to the hosting flow.
+- [CommonFlowUiApi](commonflow/compose/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/compose/CommonFlowUiApi.kt) -
+ An interface with the single `Screen` method to inject and put to the composition
+
+For more details on multiplatform compose viewmodel follow [this link](https://kotlinlang.org/docs/multiplatform/compose-viewmodel.html)
+
+Check the [example](examples/di) that shows the use of this flow:
+
+- The app has two states: `Content` and `Auth`.
+- `Content` requires authenticated user.
+- We have a basic [authentication flow](examples/di/api) flow defined.
+- We have two implementation modules [Login](examples/di/login) and [Social](examples/di/social) that authenticate users
+ using different authentication "providers".
+- The [app](examples/di/app) module has two build variants to support each.
+- The concrete sub-flow is provided with the late DI binding using `Hilt`
+
+
## Running state-machines in parallel (composition)
In case you want several state-machines to run in parallel producing a single combined UI state or you
diff --git a/build.gradle.kts b/build.gradle.kts
index 988edfb..1e74ba5 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -100,29 +100,38 @@ tasks.register("runCoroutinesTests") {
description = "Run unit tests for the coroutines extension layer."
}
-tasks.register("runRegisterUnitTests") {
- dependsOn(":examples:welcome:commonregister:allTests")
- description = "Run unit tests for the common register layer."
+tasks.register("runCommonflowTests") {
+ dependsOn(":commonflow:viewmodel:allTests")
+ description = "Run unit tests for the commonflow library."
}
-tasks.register("runLoginUnitTests") {
- dependsOn(":examples:welcome:login:testDebugUnitTest")
- description = "Run unit tests for the login module."
-}
-
-tasks.register("runLceUnitTests") {
+tasks.register("runLceExampleUnitTests") {
dependsOn(":examples:lce:testDebugUnitTest")
description = "Run unit tests for LCE app."
}
-tasks.register("runWelcomeUnitTests") {
- dependsOn(":examples:welcome:welcome:testDebugUnitTest") // Note: double colon might be a typo, usually ':examples:welcome:testDebugUnitTest'
+tasks.register("runWelcomeExampleUnitTests") {
+ dependsOn(":examples:welcome:commonregister:allTests")
+ dependsOn(":examples:welcome:login:testDebugUnitTest")
+ dependsOn(":examples:welcome:welcome:testDebugUnitTest")
description = "Run unit tests for welcome app."
}
-tasks.register("runTimerUnitTests") {
+tasks.register("runTimerExampleUnitTests") {
dependsOn(":examples:timer:testAndroidHostTest")
- description = "Run unit tests for welcome app." // Description seems to be a copy-paste from runWelcomeUnitTests
+ description = "Run unit tests for timer library."
+}
+
+tasks.register("runDiExampleUnitTests") {
+ dependsOn(":examples:di:login:testDebugUnitTest")
+ dependsOn(":examples:di:social:testDebugUnitTest")
+ description = "Run unit tests for di app."
+}
+
+tasks.register("runBooksExampleUnitTests") {
+ dependsOn(":examples:books:book:testDebugUnitTest")
+ dependsOn(":examples:books:app:testDebugUnitTest")
+ description = "Run unit tests for books app."
}
tasks.register("displayVersion") {
@@ -136,11 +145,12 @@ tasks.register("runUnitTests") {
dependsOn(
"runStateMachineTests",
"runCoroutinesTests",
- "runLoginUnitTests",
- "runRegisterUnitTests",
- "runLceUnitTests",
- "runWelcomeUnitTests",
- "runTimerUnitTests"
+ "runCommonflowTests",
+ "runLceExampleUnitTests",
+ "runWelcomeExampleUnitTests",
+ "runTimerExampleUnitTests",
+ "runDiExampleUnitTests",
+ "runBooksExampleUnitTests"
)
group = "verification"
description = "Run unit tests for all modules."
diff --git a/commonflow/compose/build.gradle.kts b/commonflow/compose/build.gradle.kts
new file mode 100644
index 0000000..3c027d0
--- /dev/null
+++ b/commonflow/compose/build.gradle.kts
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("unused")
+@file:OptIn(ExperimentalWasmDsl::class)
+
+import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins {
+ alias(libs.plugins.kotlin.multiplatform)
+ alias(libs.plugins.android.kotlin.multiplatform.library)
+ alias(libs.plugins.compose)
+ alias(libs.plugins.composeMultiplatform)
+ alias(libs.plugins.kotlin.dokka)
+ id("maven-publish")
+ id("signing")
+}
+
+val versionName: String by project.extra
+val androidMinSdkVersion: Int by project.extra
+val androidTargetSdkVersion: Int by project.extra
+val androidCompileSdkVersion: Int by project.extra
+
+group = rootProject.group
+version = rootProject.version
+
+println("== Project version: $versionName ==")
+
+kotlin {
+ jvmToolchain(17)
+
+ jvm()
+ android {
+ namespace = "com.motorro.commonstatemachine.commonflow.compose"
+ compileSdk = androidCompileSdkVersion
+ minSdk = androidMinSdkVersion
+
+ withHostTest {
+ isIncludeAndroidResources = true
+ }
+
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_17)
+ }
+ }
+
+ js(IR) {
+ binaries.library()
+ useCommonJs()
+ browser {
+ testTask(Action {
+ useMocha {
+ timeout = "10s"
+ }
+ })
+ }
+ }
+
+ wasmJs {
+ binaries.library()
+ useCommonJs()
+ browser {
+ testTask(Action {
+ useMocha {
+ timeout = "10s"
+ }
+ })
+ }
+ }
+
+ listOf(
+ iosX64(),
+ iosArm64(),
+ iosSimulatorArm64()
+ ).forEach {
+ it.binaries.framework {
+ baseName = "commonflow-compose"
+ isStatic = true
+ }
+ }
+
+ sourceSets {
+ commonMain.dependencies {
+ api(project(":commonstatemachine"))
+ api(project(":commonflow:data"))
+ api(libs.composeMultiplatform.runtime)
+ api(libs.composeMultiplatform.foundation)
+ }
+ }
+}
+val javadocJar by tasks.registering(Jar::class) {
+ dependsOn(tasks.dokkaGenerate)
+ group = "documentation"
+ archiveClassifier.set("javadoc")
+ from(tasks.dokkaGenerate)
+}
+
+val libId = "commonflow-compose"
+val libName = "commonflow-compose"
+val libDesc = "Common compose flow for modularized state machines"
+val projectUrl: String by project.extra
+val projectScm: String by project.extra
+val ossrhUsername: String? by rootProject.extra
+val ossrhPassword: String? by rootProject.extra
+val developerId: String by project.extra
+val developerName: String by project.extra
+val developerEmail: String by project.extra
+val signingKey: String? by rootProject.extra
+val signingPassword: String? by rootProject.extra
+
+publishing {
+ publications.withType {
+ artifact(javadocJar)
+ pom {
+ name.set(libName)
+ description.set(libDesc)
+ url.set(projectUrl)
+ licenses {
+ license {
+ name.set("Apache-2.0")
+ url.set("https://apache.org/licenses/LICENSE-2.0")
+ }
+ }
+ developers {
+ developer {
+ id.set(developerId)
+ name.set(developerName)
+ email.set(developerEmail)
+ }
+ }
+ scm {
+ connection.set(projectScm)
+ developerConnection.set(projectScm)
+ url.set(projectUrl)
+ }
+ }
+ }
+}
+
+signing {
+ useInMemoryPgpKeys(signingKey, signingPassword)
+ sign(publishing.publications)
+}
+
+val signingTasks = tasks.withType()
+tasks.withType().configureEach {
+ dependsOn(signingTasks)
+}
\ No newline at end of file
diff --git a/commonflow/compose/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/compose/CommonFlowUiApi.kt b/commonflow/compose/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/compose/CommonFlowUiApi.kt
new file mode 100644
index 0000000..ff4027d
--- /dev/null
+++ b/commonflow/compose/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/compose/CommonFlowUiApi.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.commonstatemachine.flow.compose
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+
+/**
+ * Common flow UI API. Used as a common interface between master and child flow using proxy.
+ * @param G - gesture type
+ * @param U - UI state type
+ * @see com.motorro.commonstatemachine.ProxyMachineState
+ * @see com.motorro.commonstatemachine.flow.data.CommonFlowHost
+ * @see com.motorro.commonstatemachine.flow.data.CommonFlowDataApi
+ */
+interface CommonFlowUiApi {
+ /**
+ * Provides composition
+ */
+ @Composable
+ fun Screen(
+ state: U,
+ onGesture: (G) -> Unit,
+ modifier: Modifier = Modifier
+ )
+}
\ No newline at end of file
diff --git a/commonflow/data/build.gradle.kts b/commonflow/data/build.gradle.kts
new file mode 100644
index 0000000..8012d94
--- /dev/null
+++ b/commonflow/data/build.gradle.kts
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("unused")
+@file:OptIn(ExperimentalWasmDsl::class)
+
+import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins {
+ alias(libs.plugins.kotlin.multiplatform)
+ alias(libs.plugins.android.kotlin.multiplatform.library)
+ alias(libs.plugins.kotlin.dokka)
+ id("maven-publish")
+ id("signing")
+}
+
+val versionName: String by project.extra
+val androidMinSdkVersion: Int by project.extra
+val androidTargetSdkVersion: Int by project.extra
+val androidCompileSdkVersion: Int by project.extra
+
+group = rootProject.group
+version = rootProject.version
+
+println("== Project version: $versionName ==")
+
+kotlin {
+ jvmToolchain(17)
+
+ jvm()
+ android {
+ namespace = "com.motorro.commonstatemachine.commonflow.data"
+ compileSdk = androidCompileSdkVersion
+ minSdk = androidMinSdkVersion
+
+ withHostTest {
+ isIncludeAndroidResources = true
+ }
+
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_17)
+ }
+ }
+
+ js(IR) {
+ binaries.library()
+ useCommonJs()
+ browser {
+ testTask(Action {
+ useMocha {
+ timeout = "10s"
+ }
+ })
+ }
+ }
+
+ wasmJs {
+ binaries.library()
+ useCommonJs()
+ browser {
+ testTask(Action {
+ useMocha {
+ timeout = "10s"
+ }
+ })
+ }
+ }
+
+ listOf(
+ iosX64(),
+ iosArm64(),
+ iosSimulatorArm64()
+ ).forEach {
+ it.binaries.framework {
+ baseName = "commonflow-data"
+ isStatic = true
+ }
+ }
+
+ sourceSets {
+ commonMain.dependencies {
+ api(project(":commonstatemachine"))
+ }
+ }
+}
+val javadocJar by tasks.registering(Jar::class) {
+ dependsOn(tasks.dokkaGenerate)
+ group = "documentation"
+ archiveClassifier.set("javadoc")
+ from(tasks.dokkaGenerate)
+}
+
+val libId = "commonflow-data"
+val libName = "commonflow-data"
+val libDesc = "Common data flow for modularized state machines"
+val projectUrl: String by project.extra
+val projectScm: String by project.extra
+val ossrhUsername: String? by rootProject.extra
+val ossrhPassword: String? by rootProject.extra
+val developerId: String by project.extra
+val developerName: String by project.extra
+val developerEmail: String by project.extra
+val signingKey: String? by rootProject.extra
+val signingPassword: String? by rootProject.extra
+
+publishing {
+ publications.withType {
+ artifact(javadocJar)
+ pom {
+ name.set(libName)
+ description.set(libDesc)
+ url.set(projectUrl)
+ licenses {
+ license {
+ name.set("Apache-2.0")
+ url.set("https://apache.org/licenses/LICENSE-2.0")
+ }
+ }
+ developers {
+ developer {
+ id.set(developerId)
+ name.set(developerName)
+ email.set(developerEmail)
+ }
+ }
+ scm {
+ connection.set(projectScm)
+ developerConnection.set(projectScm)
+ url.set(projectUrl)
+ }
+ }
+ }
+}
+
+signing {
+ useInMemoryPgpKeys(signingKey, signingPassword)
+ sign(publishing.publications)
+}
+
+val signingTasks = tasks.withType()
+tasks.withType().configureEach {
+ dependsOn(signingTasks)
+}
\ No newline at end of file
diff --git a/commonflow/data/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/data/CommonFlowDataApi.kt b/commonflow/data/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/data/CommonFlowDataApi.kt
new file mode 100644
index 0000000..633dd12
--- /dev/null
+++ b/commonflow/data/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/data/CommonFlowDataApi.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.commonstatemachine.flow.data
+
+import com.motorro.commonstatemachine.CommonMachineState
+
+/**
+ * Common flow data API. Used as a common interface between master and child flow using proxy.
+ * The proxy provides [CommonFlowHost] to the child flow and the child calls its methods when finished
+ * or otherwise needed
+ * @param G - gesture type
+ * @param U - UI state type
+ * @param I - input type
+ * @param R - result type
+ * @see com.motorro.commonstatemachine.ProxyMachineState
+ * @see CommonFlowHost
+ */
+interface CommonFlowDataApi {
+ /**
+ * Creates flow
+ */
+ fun init(flowHost: CommonFlowHost, input: I): CommonMachineState
+
+ /**
+ * Returns default UI state
+ */
+ fun getDefaultUiState(): U
+
+ /**
+ * Returns back gesture mapping
+ */
+ fun getBackGesture(): G? = null
+}
\ No newline at end of file
diff --git a/commonflow/data/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/data/CommonFlowHost.kt b/commonflow/data/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/data/CommonFlowHost.kt
new file mode 100644
index 0000000..e669c5f
--- /dev/null
+++ b/commonflow/data/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/data/CommonFlowHost.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.commonstatemachine.flow.data
+
+/**
+ * Common flow host.
+ * Used as a common interface between master and child flow using proxy.
+ * @param R - result type
+ * @see com.motorro.commonstatemachine.ProxyMachineState
+ * @see CommonFlowDataApi
+ */
+fun interface CommonFlowHost {
+ /**
+ * Completes common flow
+ * @param result Optional result.
+ */
+ fun onComplete(result: R)
+}
\ No newline at end of file
diff --git a/commonflow/viewmodel/build.gradle.kts b/commonflow/viewmodel/build.gradle.kts
new file mode 100644
index 0000000..578552f
--- /dev/null
+++ b/commonflow/viewmodel/build.gradle.kts
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("unused")
+@file:OptIn(ExperimentalWasmDsl::class)
+
+import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins {
+ alias(libs.plugins.kotlin.multiplatform)
+ alias(libs.plugins.android.kotlin.multiplatform.library)
+ alias(libs.plugins.compose)
+ alias(libs.plugins.composeMultiplatform)
+ alias(libs.plugins.kotlin.dokka)
+ id("maven-publish")
+ id("signing")
+}
+
+val versionName: String by project.extra
+val androidMinSdkVersion: Int by project.extra
+val androidTargetSdkVersion: Int by project.extra
+val androidCompileSdkVersion: Int by project.extra
+
+group = rootProject.group
+version = rootProject.version
+
+println("== Project version: $versionName ==")
+
+kotlin {
+ jvmToolchain(17)
+
+ jvm()
+ android {
+ namespace = "com.motorro.commonstatemachine.commonflow.viewmodel"
+ compileSdk = androidCompileSdkVersion
+ minSdk = androidMinSdkVersion
+
+ withHostTest {
+ isIncludeAndroidResources = true
+ }
+
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_17)
+ }
+ }
+
+ js(IR) {
+ binaries.library()
+ useCommonJs()
+ browser {
+ testTask(Action {
+ useMocha {
+ timeout = "10s"
+ }
+ })
+ }
+ }
+
+ wasmJs {
+ binaries.library()
+ useCommonJs()
+ browser {
+ testTask(Action {
+ useMocha {
+ timeout = "10s"
+ }
+ })
+ }
+ }
+
+ listOf(
+ iosX64(),
+ iosArm64(),
+ iosSimulatorArm64()
+ ).forEach {
+ it.binaries.framework {
+ baseName = "commonflow-viewmodel"
+ isStatic = true
+ }
+ }
+
+ sourceSets {
+ commonMain.dependencies {
+ api(project(":commonstatemachine"))
+ api(project(":coroutines"))
+ api(project(":commonflow:compose"))
+ api(libs.composeMultiplatform.viewmodel)
+ implementation(libs.composeMultiplatform.lifecycle)
+ }
+ commonTest.dependencies {
+ implementation(libs.test.kotlin)
+ implementation(libs.test.kotlin.coroutines)
+ }
+ androidMain.dependencies {
+ api(libs.androidx.appcompat)
+ implementation(libs.androidx.activity)
+ implementation(libs.androidx.fragment)
+ implementation(libs.compose.activity)
+ }
+ }
+}
+val javadocJar by tasks.registering(Jar::class) {
+ dependsOn(tasks.dokkaGenerate)
+ group = "documentation"
+ archiveClassifier.set("javadoc")
+ from(tasks.dokkaGenerate)
+}
+
+val libId = "commonflow-viewmodel"
+val libName = "commonflow-viewmodel"
+val libDesc = "Common view components to wrap standard flow"
+val projectUrl: String by project.extra
+val projectScm: String by project.extra
+val ossrhUsername: String? by rootProject.extra
+val ossrhPassword: String? by rootProject.extra
+val developerId: String by project.extra
+val developerName: String by project.extra
+val developerEmail: String by project.extra
+val signingKey: String? by rootProject.extra
+val signingPassword: String? by rootProject.extra
+
+publishing {
+ publications.withType {
+ artifact(javadocJar)
+ pom {
+ name.set(libName)
+ description.set(libDesc)
+ url.set(projectUrl)
+ licenses {
+ license {
+ name.set("Apache-2.0")
+ url.set("https://apache.org/licenses/LICENSE-2.0")
+ }
+ }
+ developers {
+ developer {
+ id.set(developerId)
+ name.set(developerName)
+ email.set(developerEmail)
+ }
+ }
+ scm {
+ connection.set(projectScm)
+ developerConnection.set(projectScm)
+ url.set(projectUrl)
+ }
+ }
+ }
+}
+
+signing {
+ useInMemoryPgpKeys(signingKey, signingPassword)
+ sign(publishing.publications)
+}
+
+val signingTasks = tasks.withType()
+tasks.withType().configureEach {
+ dependsOn(signingTasks)
+}
\ No newline at end of file
diff --git a/commonflow/viewmodel/src/androidMain/kotlin/com/motorro/commonstatemachine/flow/viewmodel/ViewModelComposition_android.kt b/commonflow/viewmodel/src/androidMain/kotlin/com/motorro/commonstatemachine/flow/viewmodel/ViewModelComposition_android.kt
new file mode 100644
index 0000000..7de8d39
--- /dev/null
+++ b/commonflow/viewmodel/src/androidMain/kotlin/com/motorro/commonstatemachine/flow/viewmodel/ViewModelComposition_android.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.commonstatemachine.flow.viewmodel
+
+import android.view.View
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.BackHandler
+import androidx.activity.compose.setContent
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.ViewModelProvider.Factory
+import androidx.lifecycle.viewmodel.CreationExtras
+
+/**
+ * Builds state machine composition
+ * Ensure that [AppCompatActivity] has correct view model factory
+ * @param extrasProducer optional extras producer
+ * @param factoryProducer optional factory producer
+ * @param setResult implement to set the activity result or do any result processing
+ * @param navigationBackHandler optional back navigation handler. Implement to pass back navigation to the view model
+ * @param content content block
+ */
+inline fun > ComponentActivity.setStateMachineContent(
+ noinline extrasProducer: (() -> CreationExtras)? = null,
+ noinline factoryProducer: (() -> Factory)? = null,
+ noinline setResult: (R?) -> Unit = { },
+ noinline navigationBackHandler: @Composable (Boolean, () -> Unit) -> Unit = { enabled, onBack ->
+ BackHandler(enabled = enabled) { onBack() }
+ },
+ noinline content: @Composable (U, (G) -> Unit) -> Unit
+) {
+ setContent {
+ val viewModel: VM by viewModels(
+ extrasProducer = extrasProducer,
+ factoryProducer = factoryProducer
+ )
+ CommonFlowComposition(
+ viewModel = viewModel,
+ navigationBackHandler = navigationBackHandler,
+ content = content,
+ finish = { result ->
+ if (null != result) {
+ setResult(result)
+ }
+ finish()
+ }
+ )
+ }
+}
+
+/**
+ * Builds state machine composable
+ * Ensure that [Fragment] has correct view model factory
+ * @param onFinish called when the flow is finished
+ * @param extrasProducer optional extras producer
+ * @param factoryProducer optional factory producer
+ * @param navigationBackHandler optional back navigation handler. Implement to pass back navigation to the view model
+ * @param content content block
+ */
+inline fun > Fragment.createStateMachineView(
+ noinline onFinish: (R?) -> Unit,
+ noinline extrasProducer: (() -> CreationExtras)? = null,
+ noinline factoryProducer: (() -> Factory)? = null,
+ noinline navigationBackHandler: @Composable (Boolean, () -> Unit) -> Unit = { enabled, onBack ->
+ BackHandler(enabled = enabled) { onBack() }
+ },
+ noinline content: @Composable (U, (G) -> Unit) -> Unit
+): View = ComposeView(requireContext()).apply {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+
+ setContent {
+ val viewModel: VM by viewModels(
+ extrasProducer = extrasProducer,
+ factoryProducer = factoryProducer
+ )
+ CommonFlowComposition(
+ viewModel = viewModel,
+ navigationBackHandler = navigationBackHandler,
+ content = content,
+ finish = onFinish
+ )
+ }
+}
diff --git a/commonflow/viewmodel/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/viewmodel/CommonFlowComposition.kt b/commonflow/viewmodel/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/viewmodel/CommonFlowComposition.kt
new file mode 100644
index 0000000..0a6604f
--- /dev/null
+++ b/commonflow/viewmodel/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/viewmodel/CommonFlowComposition.kt
@@ -0,0 +1,44 @@
+package com.motorro.commonstatemachine.flow.viewmodel
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.motorro.commonstatemachine.flow.viewmodel.data.BaseFlowGesture
+import com.motorro.commonstatemachine.flow.viewmodel.data.BaseFlowUiState
+
+/**
+ * Binds [CommonFlowViewModel] to the screen composition
+ * @param G - gesture type
+ * @param U - UI state type
+ * @param R - result type
+ * @param viewModel View-model instance
+ * @param navigationBackHandler Back navigation handler slot. Call the supplied function when back is detected
+ * @param content Content view. Accepts the UI state and gesture handler as parameters
+ * @param finish Called when the flow is finished
+ * @see CommonFlowViewModel
+ */
+@Composable
+@OptIn(ExperimentalComposeUiApi::class)
+fun < G: Any, U: Any, R> CommonFlowComposition(
+ viewModel: CommonFlowViewModel,
+ navigationBackHandler: @Composable (enabled: Boolean, onBack: () -> Unit) -> Unit = { _, _ -> },
+ content: @Composable (U, (G) -> Unit) -> Unit,
+ finish: (R?) -> Unit
+) {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+ val process = remember<(G) -> Unit> {
+ { viewModel.process(BaseFlowGesture.Child(it)) }
+ }
+
+ navigationBackHandler(uiState.backHandlerEnabled) { viewModel.process(BaseFlowGesture.Back) }
+
+ when (val state = uiState) {
+ is BaseFlowUiState.Child -> content(state.child, process)
+ is BaseFlowUiState.Terminated -> LaunchedEffect(Unit) {
+ finish(state.result)
+ }
+ }
+}
\ No newline at end of file
diff --git a/commonflow/viewmodel/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/viewmodel/CommonFlowViewModel.kt b/commonflow/viewmodel/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/viewmodel/CommonFlowViewModel.kt
new file mode 100644
index 0000000..8073c1b
--- /dev/null
+++ b/commonflow/viewmodel/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/viewmodel/CommonFlowViewModel.kt
@@ -0,0 +1,80 @@
+package com.motorro.commonstatemachine.flow.viewmodel
+
+import androidx.lifecycle.ViewModel
+import com.motorro.commonstatemachine.ProxyMachineState
+import com.motorro.commonstatemachine.coroutines.FlowStateMachine
+import com.motorro.commonstatemachine.flow.data.CommonFlowDataApi
+import com.motorro.commonstatemachine.flow.data.CommonFlowHost
+import com.motorro.commonstatemachine.flow.viewmodel.data.BaseFlowGesture
+import com.motorro.commonstatemachine.flow.viewmodel.data.BaseFlowUiState
+import kotlinx.coroutines.flow.StateFlow
+
+/**
+ * Wraps child flow and adapts it to the base flow to use in standard views
+ * @param G - gesture type
+ * @param U - UI state type
+ * @param I - input type
+ * @param R - result type
+ * @param api - flow data API
+ * @param init - initial input
+ * @param closeables the resources to be closed when the [ViewModel] is cleared, right **before** the [onCleared] method is called.
+ * @see CommonFlowDataApi
+ * @see CommonFlowHost
+ */
+open class CommonFlowViewModel(
+ private val api: CommonFlowDataApi,
+ private val init: I,
+ vararg closeables: AutoCloseable
+) : ViewModel(*closeables) {
+
+ companion object {
+ /**
+ * A cleanup key to associate with the state-machine
+ */
+ const val CLOSABLE_KEY = "CommonFlowStateMachine"
+ }
+
+ /**
+ * Child flow proxy
+ */
+ private val stateMachine = FlowStateMachine(
+ BaseFlowUiState.Child(
+ child = api.getDefaultUiState(),
+ backHandlerEnabled = null != api.getBackGesture()
+ )
+ ) {
+ object : ProxyMachineState, BaseFlowUiState, G, U>(api.getDefaultUiState()) {
+
+ private var terminated = false
+ private val flowHost = CommonFlowHost { result ->
+ if (terminated.not()) {
+ terminated = true
+ setUiState(BaseFlowUiState.Terminated(result))
+ }
+ }
+
+ override fun init() = api.init(flowHost, init)
+ override fun mapGesture(parent: BaseFlowGesture): G? = when(parent) {
+ BaseFlowGesture.Back -> api.getBackGesture()
+ is BaseFlowGesture.Child -> parent.child
+ }
+ override fun mapUiState(child: U): BaseFlowUiState = BaseFlowUiState.Child(child, null != api.getBackGesture())
+ }
+ }
+
+ init {
+ addCloseable(CLOSABLE_KEY, AutoCloseable {
+ stateMachine.clear()
+ })
+ }
+
+ /**
+ * Exported UI state
+ */
+ val uiState: StateFlow> get() = stateMachine.uiState
+
+ /**
+ * Delegates gesture processing to the state-machine
+ */
+ fun process(gesture: BaseFlowGesture) = stateMachine.process(gesture)
+}
\ No newline at end of file
diff --git a/commonflow/viewmodel/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/viewmodel/data/BaseFlowGesture.kt b/commonflow/viewmodel/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/viewmodel/data/BaseFlowGesture.kt
new file mode 100644
index 0000000..bf2e55e
--- /dev/null
+++ b/commonflow/viewmodel/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/viewmodel/data/BaseFlowGesture.kt
@@ -0,0 +1,16 @@
+package com.motorro.commonstatemachine.flow.viewmodel.data
+
+/**
+ * Wraps child gesture to adapt it to the hosting view component
+ */
+sealed class BaseFlowGesture {
+ /**
+ * Back navigation gesture
+ */
+ data object Back : BaseFlowGesture()
+
+ /**
+ * Child flow gesture
+ */
+ data class Child(val child: G) : BaseFlowGesture()
+}
diff --git a/commonflow/viewmodel/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/viewmodel/data/BaseFlowUiState.kt b/commonflow/viewmodel/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/viewmodel/data/BaseFlowUiState.kt
new file mode 100644
index 0000000..eda52c1
--- /dev/null
+++ b/commonflow/viewmodel/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/viewmodel/data/BaseFlowUiState.kt
@@ -0,0 +1,20 @@
+package com.motorro.commonstatemachine.flow.viewmodel.data
+
+/**
+ * Wraps child UI state to adapt it to the hosting view component
+ */
+sealed class BaseFlowUiState {
+
+ abstract val backHandlerEnabled: Boolean
+
+ /**
+ * Child UI state
+ */
+ data class Child(val child: U, override val backHandlerEnabled: Boolean) : BaseFlowUiState()
+ /**
+ * Flow terminated
+ */
+ data class Terminated(val result: R?) : BaseFlowUiState() {
+ override val backHandlerEnabled: Boolean = false
+ }
+}
diff --git a/commonflow/viewmodel/src/commonTest/kotlin/com/motorro/commonstatemachine/flow/viewmodel/CommonFlowViewModelTest.kt b/commonflow/viewmodel/src/commonTest/kotlin/com/motorro/commonstatemachine/flow/viewmodel/CommonFlowViewModelTest.kt
new file mode 100644
index 0000000..aafe745
--- /dev/null
+++ b/commonflow/viewmodel/src/commonTest/kotlin/com/motorro/commonstatemachine/flow/viewmodel/CommonFlowViewModelTest.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.commonstatemachine.flow.viewmodel
+
+import com.motorro.commonstatemachine.flow.viewmodel.data.BaseFlowGesture
+import com.motorro.commonstatemachine.flow.viewmodel.data.BaseFlowUiState
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class CommonFlowViewModelTest {
+
+ private companion object {
+ const val INIT = 1
+ const val RESULT = "result"
+ }
+
+ private lateinit var dispatcher: TestDispatcher
+ private lateinit var api: TestDataApi
+ private lateinit var model: CommonFlowViewModel
+
+ @BeforeTest
+ fun init() {
+ dispatcher = UnconfinedTestDispatcher()
+ Dispatchers.setMain(dispatcher)
+
+ api = TestDataApi()
+ model = CommonFlowViewModel(api, INIT)
+ }
+
+ @AfterTest
+ fun deinit() {
+ Dispatchers.resetMain()
+ }
+
+ private fun test(block: suspend TestScope.() -> Unit) = runTest(
+ dispatcher,
+ testBody = block
+ )
+
+ @Test
+ fun startsApiAndWrapsUiState() = test {
+ assertEquals(INIT, api.state.init)
+ assertEquals(
+ BaseFlowUiState.Child(TestUiState.RUNNING, true),
+ model.uiState.value
+ )
+ }
+
+ @Test
+ fun passesChildGesture() = test {
+ model.process(BaseFlowGesture.Child(TestGesture.Stop(RESULT)))
+ assertEquals(BaseFlowUiState.Terminated(RESULT), model.uiState.value)
+ }
+
+ @Test
+ fun mapsBackGesture() = test {
+ model.process(BaseFlowGesture.Back)
+ assertEquals(BaseFlowUiState.Terminated(null), model.uiState.value)
+ }
+}
\ No newline at end of file
diff --git a/commonflow/viewmodel/src/commonTest/kotlin/com/motorro/commonstatemachine/flow/viewmodel/TestFlowApi.kt b/commonflow/viewmodel/src/commonTest/kotlin/com/motorro/commonstatemachine/flow/viewmodel/TestFlowApi.kt
new file mode 100644
index 0000000..1dc1576
--- /dev/null
+++ b/commonflow/viewmodel/src/commonTest/kotlin/com/motorro/commonstatemachine/flow/viewmodel/TestFlowApi.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.commonstatemachine.flow.viewmodel
+
+import com.motorro.commonstatemachine.CommonMachineState
+import com.motorro.commonstatemachine.flow.data.CommonFlowDataApi
+import com.motorro.commonstatemachine.flow.data.CommonFlowHost
+
+sealed class TestGesture {
+ data object Back : TestGesture()
+ data class Stop(val result: String?) : TestGesture()
+}
+
+enum class TestUiState {
+ LOADING,
+ RUNNING
+}
+
+class TestFlowState(
+ private val flowHost: CommonFlowHost,
+ val init: Int
+) : CommonMachineState() {
+
+ var cleared = false
+
+ override fun doStart() {
+ setUiState(TestUiState.RUNNING)
+ }
+
+ override fun doProcess(gesture: TestGesture) {
+ when (gesture) {
+ is TestGesture.Back -> flowHost.onComplete(null)
+ is TestGesture.Stop -> flowHost.onComplete(gesture.result)
+ }
+ }
+
+ override fun doClear() {
+ cleared = true
+ }
+}
+
+class TestDataApi : CommonFlowDataApi {
+ lateinit var state: TestFlowState
+
+ override fun init(flowHost: CommonFlowHost, input: Int): CommonMachineState {
+ return TestFlowState(flowHost, input).also { state = it }
+ }
+
+ override fun getDefaultUiState(): TestUiState = TestUiState.LOADING
+ override fun getBackGesture(): TestGesture = TestGesture.Back
+}
\ No newline at end of file
diff --git a/examples/androidcore/build.gradle.kts b/examples/androidcore/build.gradle.kts
index 798fd47..8609265 100644
--- a/examples/androidcore/build.gradle.kts
+++ b/examples/androidcore/build.gradle.kts
@@ -66,6 +66,7 @@ dependencies {
implementation(platform(libs.compose.bom))
implementation(libs.bundles.compose.core)
+ implementation(libs.compose.material.icons)
implementation(libs.compose.activity)
implementation(libs.compose.foundation)
implementation(libs.compose.foundation.layouts)
diff --git a/examples/androidcore/src/main/java/com/motorro/statemachine/androidcore/compose/Error.kt b/examples/androidcore/src/main/java/com/motorro/statemachine/androidcore/compose/Error.kt
new file mode 100644
index 0000000..6bed1a0
--- /dev/null
+++ b/examples/androidcore/src/main/java/com/motorro/statemachine/androidcore/compose/Error.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2022 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.androidcore.compose
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Warning
+import androidx.compose.material3.Button
+import androidx.compose.material3.Card
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.motorro.statemachine.androidcore.ui.theme.CommonStateMachineTheme
+import java.io.IOException
+
+@Composable
+fun Error(error: Throwable, onDismiss: () -> Unit, modifier: Modifier = Modifier) {
+ Column(
+ modifier = modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ content = {
+ Icon(
+ modifier = Modifier.size(255.dp),
+ imageVector = Icons.Rounded.Warning,
+ tint = MaterialTheme.colorScheme.error,
+ contentDescription = "Error",
+ )
+
+ Card(modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ .background(MaterialTheme.colorScheme.errorContainer)
+ ) {
+ Text(
+ modifier = Modifier
+ .padding(16.dp)
+ .align(Alignment.CenterHorizontally),
+ text = error.message ?: "Unknown error"
+ )
+ }
+ Button(modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ onClick = onDismiss
+ ) {
+ Text("Dismiss")
+ }
+ }
+ )
+}
+
+@Preview
+@Composable
+fun ErrorPreview() {
+ CommonStateMachineTheme {
+ Error(
+ error = IOException("Network error"),
+ onDismiss = {}
+ )
+ }
+}
diff --git a/examples/androidcore/src/main/java/com/motorro/statemachine/androidcore/compose/Loading.kt b/examples/androidcore/src/main/java/com/motorro/statemachine/androidcore/compose/Loading.kt
index c4378e6..387f18b 100644
--- a/examples/androidcore/src/main/java/com/motorro/statemachine/androidcore/compose/Loading.kt
+++ b/examples/androidcore/src/main/java/com/motorro/statemachine/androidcore/compose/Loading.kt
@@ -25,9 +25,9 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
-fun Loading() {
+fun Loading(modifier: Modifier = Modifier) {
Column(
- modifier = Modifier.fillMaxSize(),
+ modifier = modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
content = {
diff --git a/examples/books/app/.gitignore b/examples/books/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/examples/books/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/examples/books/app/build.gradle.kts b/examples/books/app/build.gradle.kts
new file mode 100644
index 0000000..ffe01d3
--- /dev/null
+++ b/examples/books/app/build.gradle.kts
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("unused")
+
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+/*
+ * Copyright 2022 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id("com.android.application")
+ id("org.jetbrains.kotlin.plugin.compose")
+ alias(libs.plugins.google.ksp)
+ alias(libs.plugins.hilt)
+}
+
+val versionCode: String by project.extra
+val versionName: String by project.extra
+val androidMinSdkVersion: Int by project.extra
+val androidCompileSdkVersion: Int by project.extra
+val androidTargetSdkVersion: Int by project.extra
+
+android {
+ compileSdk = androidCompileSdkVersion
+
+ defaultConfig {
+ applicationId = "com.motorro.statemachine.books.app"
+
+ minSdk = androidMinSdkVersion
+ targetSdk = androidTargetSdkVersion
+ versionCode = versionCode
+ versionName = versionName
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ vectorDrawables {
+ useSupportLibrary = true
+ }
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+ }
+ }
+ compileOptions {
+ isCoreLibraryDesugaringEnabled = true
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ kotlin {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_17)
+ }
+ }
+ buildFeatures {
+ compose = true
+ }
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+ namespace = "com.motorro.statemachine.books.app"
+}
+
+dependencies {
+ implementation(project(":commonstatemachine"))
+ implementation(project(":commonflow:data"))
+ implementation(project(":commonflow:compose"))
+ implementation(project(":commonflow:viewmodel"))
+
+ implementation(project(":examples:commoncore"))
+ implementation(project(":examples:androidcore"))
+ implementation(project(":examples:books:domain"))
+ implementation(project(":examples:books:book"))
+
+ coreLibraryDesugaring(libs.desugaring)
+
+ implementation(libs.androidx.core)
+ implementation(libs.androidx.lifecycle.runtime)
+ implementation(libs.androidx.lifecycle.livedata)
+ implementation(libs.androidx.lifecycle.viewmodel)
+
+ implementation(libs.kotlin.immutable)
+ implementation(libs.kotlin.coroutines.core)
+ implementation(libs.kotlin.coroutines.android)
+
+ implementation(platform(libs.compose.bom))
+
+ implementation(libs.bundles.compose.core)
+ implementation(libs.compose.activity)
+ implementation(libs.compose.viewmodel)
+ implementation(libs.compose.foundation)
+ implementation(libs.compose.foundation.layouts)
+ implementation(libs.compose.material.icons)
+
+ implementation(libs.hilt.android)
+ implementation(libs.hilt.compose)
+ ksp(libs.hilt.compiler)
+ ksp(libs.hilt.compiler.androidx)
+
+ debugImplementation(libs.compose.tooling)
+
+ testImplementation(libs.bundles.test.core)
+ testImplementation(libs.test.kotlin.coroutines)
+}
diff --git a/examples/books/app/proguard-rules.pro b/examples/books/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/examples/books/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/examples/books/app/src/main/AndroidManifest.xml b/examples/books/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..84032b3
--- /dev/null
+++ b/examples/books/app/src/main/AndroidManifest.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/App.kt b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/App.kt
new file mode 100644
index 0000000..b00c8e2
--- /dev/null
+++ b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/App.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.app
+
+import android.app.Application
+import dagger.hilt.android.HiltAndroidApp
+import timber.log.Timber
+
+@HiltAndroidApp
+class App : Application() {
+ override fun onCreate() {
+ super.onCreate()
+ setupLogger()
+ }
+
+ private fun setupLogger() {
+ Timber.plant(Timber.DebugTree())
+ }
+}
\ No newline at end of file
diff --git a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/MainActivity.kt b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/MainActivity.kt
new file mode 100644
index 0000000..ad1bf33
--- /dev/null
+++ b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/MainActivity.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.app
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.enableEdgeToEdge
+import com.motorro.commonstatemachine.flow.viewmodel.setStateMachineContent
+import com.motorro.statemachine.androidcore.ui.theme.CommonStateMachineTheme
+import com.motorro.statemachine.books.app.api.MainGesture
+import com.motorro.statemachine.books.app.api.MainUiState
+import com.motorro.statemachine.books.app.ui.MainScreen
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ enableEdgeToEdge()
+ super.onCreate(savedInstanceState)
+
+ setStateMachineContent { state, onGesture ->
+ CommonStateMachineTheme {
+ MainScreen(state, onGesture)
+ }
+ }
+ }
+}
diff --git a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/MainViewModel.kt b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/MainViewModel.kt
new file mode 100644
index 0000000..ed17c66
--- /dev/null
+++ b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/MainViewModel.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.app
+
+import com.motorro.commonstatemachine.flow.viewmodel.CommonFlowViewModel
+import com.motorro.statemachine.books.app.api.MainApi
+import com.motorro.statemachine.books.app.api.MainGesture
+import com.motorro.statemachine.books.app.api.MainUiState
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+
+@HiltViewModel
+class MainViewModel @Inject constructor(mainApi: MainApi) : CommonFlowViewModel(
+ api = mainApi,
+ init = Unit
+)
\ No newline at end of file
diff --git a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/api/MainApi.kt b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/api/MainApi.kt
new file mode 100644
index 0000000..5a58725
--- /dev/null
+++ b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/api/MainApi.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.app.api
+
+import com.motorro.commonstatemachine.CommonMachineState
+import com.motorro.commonstatemachine.flow.data.CommonFlowDataApi
+import com.motorro.commonstatemachine.flow.data.CommonFlowHost
+import com.motorro.statemachine.books.app.data.MainDataState
+import com.motorro.statemachine.books.app.state.MainFactory
+import javax.inject.Inject
+
+/**
+ * Item common flow
+ */
+class MainApi @Inject internal constructor(private val factory: MainFactory.Factory) : CommonFlowDataApi {
+
+ override fun init(flowHost: CommonFlowHost, input: Unit): CommonMachineState =
+ factory.create(flowHost).bookList(MainDataState())
+
+ override fun getDefaultUiState(): MainUiState = MainUiState.List.Loading
+ override fun getBackGesture(): MainGesture = MainGesture.Back
+}
\ No newline at end of file
diff --git a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/api/MainFlowHost.kt b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/api/MainFlowHost.kt
new file mode 100644
index 0000000..32b28fa
--- /dev/null
+++ b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/api/MainFlowHost.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.app.api
+
+import com.motorro.commonstatemachine.flow.data.CommonFlowHost
+
+/**
+ * Hosting interface
+ */
+typealias MainFlowHost = CommonFlowHost
\ No newline at end of file
diff --git a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/api/MainGesture.kt b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/api/MainGesture.kt
new file mode 100644
index 0000000..b2d7fe0
--- /dev/null
+++ b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/api/MainGesture.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.app.api
+
+import com.motorro.statemachine.books.book.api.BookGesture
+
+/**
+ * Main flow gesture
+ */
+sealed class MainGesture {
+ /**
+ * Backwards navigation
+ */
+ internal object Back : MainGesture()
+
+ /**
+ * Item selected
+ */
+ internal data class BookSelected(val id: Int) : MainGesture()
+
+ /**
+ * Child flow
+ */
+ internal data class Book(val child: BookGesture) : MainGesture()
+}
diff --git a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/api/MainUiState.kt b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/api/MainUiState.kt
new file mode 100644
index 0000000..6e245e2
--- /dev/null
+++ b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/api/MainUiState.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.app.api
+
+import androidx.compose.runtime.Immutable
+import com.motorro.statemachine.books.book.api.BookUiState
+import com.motorro.statemachine.books.domain.entity.ListBook
+import kotlinx.collections.immutable.ImmutableList
+
+/**
+ * Main flow UI state
+ */
+sealed class MainUiState {
+ /**
+ * Main flow
+ */
+ sealed class List : MainUiState() {
+ /**
+ * Main loader
+ */
+ internal data object Loading : List()
+
+ /**
+ * Main list
+ */
+ @Immutable
+ internal data class Master(val items: ImmutableList) : List()
+ }
+
+ /**
+ * Book detail
+ */
+ internal data class Book(val child: BookUiState) : MainUiState()
+}
\ No newline at end of file
diff --git a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/data/MainDataState.kt b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/data/MainDataState.kt
new file mode 100644
index 0000000..cc6f6f7
--- /dev/null
+++ b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/data/MainDataState.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.app.data
+
+import com.motorro.statemachine.books.domain.entity.ListBook
+import kotlinx.collections.immutable.ImmutableList
+
+/**
+ * Main data state
+ */
+data class MainDataState(val items: ImmutableList? = null)
\ No newline at end of file
diff --git a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/di/ApplicationModule.kt b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/di/ApplicationModule.kt
new file mode 100644
index 0000000..15f36d9
--- /dev/null
+++ b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/di/ApplicationModule.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.app.di
+
+import com.motorro.statemachine.books.app.repository.BookRepositoryImpl
+import com.motorro.statemachine.books.domain.entity.Book
+import com.motorro.statemachine.books.domain.repository.BookRepository
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import kotlinx.collections.immutable.persistentListOf
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+internal class ApplicationModule {
+ @Provides
+ @Singleton
+ fun itemRepository(): BookRepository = BookRepositoryImpl(
+ listOf(
+ Book(
+ id = 1,
+ title = "The Hobbit",
+ authors = persistentListOf("J.R.R. Tolkien"),
+ content = "A reluctant hobbit, Bilbo Baggins, sets out to the Lonely Mountain with a spirited group of dwarves to reclaim their mountain home, and a treasure within, from the dragon Smaug."
+ ),
+ Book(
+ id = 2,
+ title = "The Lord of the Rings",
+ authors = persistentListOf("J.R.R. Tolkien"),
+ content = "Frodo Baggins, a hobbit, and his companions set out on a quest to destroy the powerful One Ring and save Middle-earth from the Dark Lord Sauron."
+ ),
+ Book(
+ id = 3,
+ title = "Pride and Prejudice",
+ authors = persistentListOf("Jane Austen"),
+ content = "Follows the turbulent relationship between Elizabeth Bennet, the daughter of a country gentleman, and Fitzwilliam Darcy, a rich aristocratic landowner."
+ ),
+ Book(
+ id = 4,
+ title = "To Kill a Mockingbird",
+ authors = persistentListOf("Harper Lee"),
+ content = "The story of a young girl, Scout Finch, her brother, Jem, and their father, Atticus, a lawyer who defends a black man falsely accused of raping a white woman in the 1930s Alabama."
+ ),
+ Book(
+ id = 5,
+ title = "1984",
+ authors = persistentListOf("George Orwell"),
+ content = "A dystopian novel set in Airstrip One (formerly Great Britain), a province of the superstate Oceania in a world of perpetual war, omnipresent government surveillance, and public manipulation."
+ )
+ )
+ )
+}
\ No newline at end of file
diff --git a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/repository/BookRepositoryImpl.kt b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/repository/BookRepositoryImpl.kt
new file mode 100644
index 0000000..436860b
--- /dev/null
+++ b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/repository/BookRepositoryImpl.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.app.repository
+
+import com.motorro.statemachine.books.domain.entity.Book
+import com.motorro.statemachine.books.domain.entity.ListBook
+import com.motorro.statemachine.books.domain.repository.BookRepository
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.toImmutableList
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.emitAll
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.update
+import kotlin.time.Duration.Companion.seconds
+
+class BookRepositoryImpl(books: List) : BookRepository {
+
+ private val data = MutableStateFlow(books.associateBy { it.id })
+
+ override val books: Flow> = flow {
+ delay(DELAY)
+ emitAll(data.map { it.values.map { ListBook(it.id, it.title, it.authors) }.toImmutableList() })
+ }
+
+ override suspend fun getBook(id: Int): Book {
+ delay(DELAY)
+ return data.first().getValue(id)
+ }
+
+ override suspend fun deleteBook(id: Int) {
+ delay(DELAY)
+ data.update {
+ it - id
+ }
+ }
+
+ private companion object {
+ val DELAY = 1.seconds
+ }
+}
\ No newline at end of file
diff --git a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/state/BaseMainState.kt b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/state/BaseMainState.kt
new file mode 100644
index 0000000..a7e4831
--- /dev/null
+++ b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/state/BaseMainState.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.app.state
+
+import com.motorro.commonstatemachine.coroutines.CoroutineState
+import com.motorro.statemachine.books.app.api.MainGesture
+import com.motorro.statemachine.books.app.api.MainUiState
+import com.motorro.statemachine.commoncore.log.Logger
+
+internal abstract class BaseMainState(context: MainContext) : CoroutineState(), MainContext by context {
+ override fun doProcess(gesture: MainGesture) {
+ Logger.w("Unhandled gesture: $gesture")
+ }
+}
\ No newline at end of file
diff --git a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/state/ItemListState.kt b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/state/ItemListState.kt
new file mode 100644
index 0000000..46903c6
--- /dev/null
+++ b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/state/ItemListState.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.app.state
+
+import com.motorro.statemachine.books.app.api.MainGesture
+import com.motorro.statemachine.books.app.api.MainUiState
+import com.motorro.statemachine.books.app.data.MainDataState
+import com.motorro.statemachine.books.book.api.BookInput
+import com.motorro.statemachine.books.domain.entity.ListBook
+import com.motorro.statemachine.books.domain.repository.BookRepository
+import com.motorro.statemachine.commoncore.log.Logger
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import javax.inject.Inject
+import kotlin.properties.Delegates
+
+internal class ItemListState(
+ context: MainContext,
+ data: MainDataState,
+ private val repository: BookRepository
+) : BaseMainState(context) {
+
+ private var data: MainDataState by Delegates.observable(data) { _, _, newValue ->
+ render(newValue.items)
+ }
+
+ override fun doStart() {
+ render(data.items)
+ subscribeSession()
+ }
+
+ private fun subscribeSession() {
+ repository.books
+ .onEach { data = data.copy(items = it) }
+ .launchIn(stateScope)
+ }
+
+ override fun doProcess(gesture: MainGesture) {
+ when (gesture) {
+ MainGesture.Back -> {
+ Logger.d("Back. Terminating...")
+ setMachineState(factory.terminated())
+ }
+ is MainGesture.BookSelected -> {
+ val selected = data.items?.firstOrNull { it.id == gesture.id } ?: return
+ Logger.d("Item selected: ${selected.id}")
+ setMachineState(factory.book(
+ data = data,
+ input = BookInput(selected.id, selected.title)
+ ))
+ }
+ else -> super.doProcess(gesture)
+ }
+ }
+
+ private fun render(items: ImmutableList?) {
+ val uiState = when {
+ null != items -> MainUiState.List.Master(items)
+ else -> MainUiState.List.Loading
+ }
+ setUiState(uiState)
+ }
+
+ class Factory @Inject constructor(private val repository: BookRepository) {
+ fun invoke(context: MainContext, data: MainDataState) = ItemListState(
+ context,
+ data,
+ repository
+ )
+ }
+}
\ No newline at end of file
diff --git a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/state/ItemProxy.kt b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/state/ItemProxy.kt
new file mode 100644
index 0000000..59e0311
--- /dev/null
+++ b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/state/ItemProxy.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.app.state
+
+import com.motorro.commonstatemachine.ProxyMachineState
+import com.motorro.statemachine.books.app.api.MainGesture
+import com.motorro.statemachine.books.app.api.MainUiState
+import com.motorro.statemachine.books.app.data.MainDataState
+import com.motorro.statemachine.books.book.api.BookApi
+import com.motorro.statemachine.books.book.api.BookGesture
+import com.motorro.statemachine.books.book.api.BookInput
+import com.motorro.statemachine.books.book.api.BookUiState
+import com.motorro.statemachine.books.book.api.ItemFlowHost
+import com.motorro.statemachine.commoncore.log.Logger
+import javax.inject.Inject
+
+internal class ItemProxy(
+ private val context: MainContext,
+ private val data: MainDataState,
+ private val input: BookInput,
+ private val api: BookApi
+) : ProxyMachineState(api.getDefaultUiState()) {
+
+ private val flowHast = ItemFlowHost {
+ Logger.d("Item flow host terminated")
+ setMachineState(context.factory.bookList(data))
+ }
+
+ override fun init() = api.init(flowHast, input)
+
+ override fun mapGesture(parent: MainGesture): BookGesture? = when(parent) {
+ is MainGesture.Back -> api.getBackGesture()
+ is MainGesture.Book -> parent.child
+ else -> null
+ }
+
+ override fun mapUiState(child: BookUiState): MainUiState = MainUiState.Book(child)
+
+ class Factory @Inject constructor(private val api: BookApi) {
+ operator fun invoke(context: MainContext, data: MainDataState, input: BookInput) = ItemProxy(
+ context,
+ data,
+ input,
+ api
+ )
+ }
+}
\ No newline at end of file
diff --git a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/state/MainContext.kt b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/state/MainContext.kt
new file mode 100644
index 0000000..76543a8
--- /dev/null
+++ b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/state/MainContext.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.app.state
+
+/**
+ * Main flow context
+ */
+internal interface MainContext {
+ val factory: MainFactory
+}
\ No newline at end of file
diff --git a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/state/MainFactory.kt b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/state/MainFactory.kt
new file mode 100644
index 0000000..34d8d2e
--- /dev/null
+++ b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/state/MainFactory.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.app.state
+
+import com.motorro.commonstatemachine.CommonMachineState
+import com.motorro.statemachine.books.app.api.MainFlowHost
+import com.motorro.statemachine.books.app.api.MainGesture
+import com.motorro.statemachine.books.app.api.MainUiState
+import com.motorro.statemachine.books.app.data.MainDataState
+import com.motorro.statemachine.books.book.api.BookInput
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import javax.inject.Provider
+
+/**
+ * Main state factory
+ */
+internal interface MainFactory {
+ fun bookList(data: MainDataState): CommonMachineState
+ fun book(data: MainDataState, input: BookInput): CommonMachineState
+ fun terminated(): CommonMachineState
+
+ @AssistedFactory
+ interface Factory {
+ fun create(host: MainFlowHost): Impl
+ }
+
+ class Impl @AssistedInject constructor(
+ @Assisted private val host: MainFlowHost,
+ private val createItemList: Provider,
+ private val createItem: Provider,
+ ) : MainFactory {
+ private val context = object : MainContext {
+ override val factory: MainFactory = this@Impl
+ }
+
+ override fun bookList(data: MainDataState) = createItemList.get().invoke(
+ context,
+ data
+ )
+
+ override fun book(data: MainDataState, input: BookInput) = createItem.get().invoke(
+ context,
+ data,
+ input
+ )
+
+ override fun terminated() = object : CommonMachineState() {
+ override fun doStart() {
+ host.onComplete(Unit)
+ }
+ }
+ }
+}
diff --git a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/ui/BookListView.kt b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/ui/BookListView.kt
new file mode 100644
index 0000000..c332b4f
--- /dev/null
+++ b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/ui/BookListView.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.app.ui
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.Card
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.motorro.statemachine.androidcore.ui.theme.CommonStateMachineTheme
+import com.motorro.statemachine.books.domain.entity.ListBook
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.toImmutableList
+
+@Composable
+fun BookListView(items: ImmutableList, onItemSelected: (Int) -> Unit) {
+ LazyColumn(modifier = Modifier.fillMaxSize()) {
+ items(items) { item ->
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp)
+ .clickable { onItemSelected(item.id) }
+ ) {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Text(
+ text = item.title,
+ style = MaterialTheme.typography.titleMedium
+ )
+ Text(
+ text = item.authors.joinToString(),
+ style = MaterialTheme.typography.bodyMedium
+ )
+ }
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun BookListViewPreview() {
+ val items = (1..10).map { ListBook(it, "Item $it", persistentListOf("Author $it")) }.toImmutableList()
+ CommonStateMachineTheme {
+ BookListView(items = items, onItemSelected = {})
+ }
+}
diff --git a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/ui/MainScreen.kt b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/ui/MainScreen.kt
new file mode 100644
index 0000000..1fc3d31
--- /dev/null
+++ b/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/ui/MainScreen.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.app.ui
+
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.AnimatedContentTransitionScope
+import androidx.compose.animation.ContentTransform
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.slideInHorizontally
+import androidx.compose.animation.slideOutHorizontally
+import androidx.compose.animation.togetherWith
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import com.motorro.statemachine.androidcore.compose.Loading
+import com.motorro.statemachine.books.app.R
+import com.motorro.statemachine.books.app.api.MainGesture
+import com.motorro.statemachine.books.app.api.MainGesture.Book
+import com.motorro.statemachine.books.app.api.MainGesture.BookSelected
+import com.motorro.statemachine.books.app.api.MainUiState
+import com.motorro.statemachine.books.book.ui.BookScreen
+import kotlin.reflect.KClass
+
+@Composable
+@OptIn(ExperimentalMaterial3Api::class)
+internal fun MainScreen(state: MainUiState, onGesture: (MainGesture) -> Unit, modifier: Modifier = Modifier) {
+ AnimatedContent(
+ targetState = state,
+ modifier = modifier,
+ contentKey = { it.contentKey },
+ transitionSpec = mainTransitionSpec
+ ) { targetState ->
+ when (targetState) {
+ is MainUiState.List -> MainScreenScaffold(targetState, onGesture)
+ is MainUiState.Book -> BookScreen(targetState.child) {
+ onGesture(Book(it))
+ }
+ }
+ }
+}
+
+@Composable
+@OptIn(ExperimentalMaterial3Api::class)
+private fun MainScreenScaffold(state: MainUiState.List, onGesture: (MainGesture) -> Unit) {
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text(stringResource(R.string.app_name)) }
+ )
+ },
+ content = {
+ AnimatedContent(targetState = state, modifier = Modifier.padding(it)) { targetState ->
+ when(targetState) {
+ MainUiState.List.Loading -> Loading()
+ is MainUiState.List.Master -> BookListView(targetState.items) {
+ onGesture(BookSelected(it))
+ }
+ }
+ }
+ }
+ )
+}
+
+private val KClass<*>.contentKey: Any? get() = simpleName
+
+private val MainUiState.contentKey: Any? get() = when(this) {
+ is MainUiState.List -> MainUiState.List::class.contentKey
+ is MainUiState.Book -> MainUiState.Book::class.contentKey
+}
+
+private val mainTransitionSpec: AnimatedContentTransitionScope.() -> ContentTransform = {
+ when (initialState.contentKey to targetState.contentKey) {
+ MainUiState.List::class.contentKey to MainUiState.Book::class.contentKey -> {
+ slideInHorizontally(
+ animationSpec = tween(220, delayMillis = 90),
+ initialOffsetX = { fullWidth -> fullWidth }
+ ) togetherWith slideOutHorizontally(
+ animationSpec = tween(220, delayMillis = 90),
+ targetOffsetX = { fullWidth -> -fullWidth }
+ )
+ }
+ MainUiState.Book::class.contentKey to MainUiState.List::class.contentKey -> {
+ slideInHorizontally(
+ animationSpec = tween(220, delayMillis = 90),
+ initialOffsetX = { fullWidth -> - fullWidth }
+ ) togetherWith slideOutHorizontally(
+ animationSpec = tween(220, delayMillis = 90),
+ targetOffsetX = { fullWidth -> fullWidth }
+ )
+ }
+ else -> ContentTransform(EnterTransition.None, ExitTransition.None)
+ }
+}
\ No newline at end of file
diff --git a/examples/books/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/examples/books/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..40ce517
--- /dev/null
+++ b/examples/books/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/books/app/src/main/res/drawable/ic_launcher_background.xml b/examples/books/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..997ef0b
--- /dev/null
+++ b/examples/books/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,183 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/books/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/examples/books/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..0147ca8
--- /dev/null
+++ b/examples/books/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/books/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/examples/books/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..0147ca8
--- /dev/null
+++ b/examples/books/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/books/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/examples/books/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/examples/books/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/examples/books/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/examples/books/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/examples/books/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/examples/books/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/examples/books/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/examples/books/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/examples/books/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/examples/books/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/examples/books/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/examples/books/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/examples/books/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/examples/books/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/examples/books/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/examples/books/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/examples/books/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/examples/books/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/examples/books/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/examples/books/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/examples/books/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/examples/books/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/examples/books/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/examples/books/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/examples/books/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/examples/books/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/examples/books/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/examples/books/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/examples/books/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/examples/books/app/src/main/res/values/colors.xml b/examples/books/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..164d1ef
--- /dev/null
+++ b/examples/books/app/src/main/res/values/colors.xml
@@ -0,0 +1,23 @@
+
+
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/examples/books/app/src/main/res/values/strings.xml b/examples/books/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..bf54e8a
--- /dev/null
+++ b/examples/books/app/src/main/res/values/strings.xml
@@ -0,0 +1,16 @@
+
+
+
+ Books
+
\ No newline at end of file
diff --git a/examples/books/app/src/main/res/values/themes.xml b/examples/books/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..eff3025
--- /dev/null
+++ b/examples/books/app/src/main/res/values/themes.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/books/app/src/main/res/xml/backup_rules.xml b/examples/books/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..18b86f4
--- /dev/null
+++ b/examples/books/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/books/app/src/main/res/xml/data_extraction_rules.xml b/examples/books/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..2d914d6
--- /dev/null
+++ b/examples/books/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/books/app/src/test/kotlin/com/motorro/statemachine/books/app/mock.kt b/examples/books/app/src/test/kotlin/com/motorro/statemachine/books/app/mock.kt
new file mode 100644
index 0000000..dd62339
--- /dev/null
+++ b/examples/books/app/src/test/kotlin/com/motorro/statemachine/books/app/mock.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.app
+
+import com.motorro.statemachine.books.domain.entity.ListBook
+import kotlinx.collections.immutable.persistentListOf
+
+val BOOK_1 = ListBook(
+ id = 10,
+ title = "Book 1",
+ authors = persistentListOf("Author 1", "Author 2")
+)
+
+val BOOK_2 = ListBook(
+ id = 20,
+ title = "Book 2",
+ authors = persistentListOf("Author 3", "Author 4")
+)
\ No newline at end of file
diff --git a/examples/books/app/src/test/kotlin/com/motorro/statemachine/books/app/state/BaseStateTest.kt b/examples/books/app/src/test/kotlin/com/motorro/statemachine/books/app/state/BaseStateTest.kt
new file mode 100644
index 0000000..87bbd1d
--- /dev/null
+++ b/examples/books/app/src/test/kotlin/com/motorro/statemachine/books/app/state/BaseStateTest.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.app.state
+
+import com.motorro.commonstatemachine.CommonStateMachine
+import com.motorro.statemachine.books.app.api.MainGesture
+import com.motorro.statemachine.books.app.api.MainUiState
+import io.mockk.mockk
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Before
+
+@OptIn(ExperimentalCoroutinesApi::class)
+internal abstract class BaseStateTest {
+ protected lateinit var stateMachine: CommonStateMachine
+ protected lateinit var factory: MainFactory
+ protected lateinit var context: MainContext
+ protected lateinit var nextState: BaseMainState
+ protected lateinit var dispatcher: TestDispatcher
+
+ @Before
+ fun init() {
+ dispatcher = UnconfinedTestDispatcher()
+ Dispatchers.setMain(dispatcher)
+ stateMachine = mockk(relaxed = true)
+ factory = mockk()
+ context = object : MainContext {
+ override val factory: MainFactory = this@BaseStateTest.factory
+ }
+ nextState = mockk(relaxed = true)
+ doInit()
+ }
+
+ @After
+ fun deinit() {
+ Dispatchers.resetMain()
+ }
+
+ protected fun test(block: suspend TestScope.() -> Unit) = runTest(
+ dispatcher,
+ testBody = block
+ )
+
+ protected open fun doInit() = Unit
+}
\ No newline at end of file
diff --git a/examples/books/app/src/test/kotlin/com/motorro/statemachine/books/app/state/ItemListStateTest.kt b/examples/books/app/src/test/kotlin/com/motorro/statemachine/books/app/state/ItemListStateTest.kt
new file mode 100644
index 0000000..b4ccfa8
--- /dev/null
+++ b/examples/books/app/src/test/kotlin/com/motorro/statemachine/books/app/state/ItemListStateTest.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.app.state
+
+import com.motorro.statemachine.books.app.BOOK_1
+import com.motorro.statemachine.books.app.BOOK_2
+import com.motorro.statemachine.books.app.api.MainGesture
+import com.motorro.statemachine.books.app.api.MainUiState
+import com.motorro.statemachine.books.app.data.MainDataState
+import com.motorro.statemachine.books.book.api.BookInput
+import com.motorro.statemachine.books.domain.entity.ListBook
+import com.motorro.statemachine.books.domain.repository.BookRepository
+import io.mockk.Ordering
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.mockk
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.yield
+import org.junit.Test
+import kotlin.test.assertEquals
+
+internal class ItemListStateTest : BaseStateTest() {
+ private lateinit var items: MutableStateFlow>
+
+ override fun doInit() {
+ items = MutableStateFlow(persistentListOf(BOOK_1))
+ }
+
+ private fun createState(data: MainDataState = MainDataState()): BaseMainState {
+ val repository = mockk {
+ every { this@mockk.books } returns this@ItemListStateTest.items
+ }
+ return ItemListState(context, data, repository)
+ }
+
+ @Test
+ fun startsWithLoadingWhenDataIsEmpty() = test {
+ val state = createState()
+
+ state.start(stateMachine)
+
+ coVerify(ordering = Ordering.ORDERED) {
+ stateMachine.setUiState(MainUiState.List.Loading)
+ stateMachine.setUiState(MainUiState.List.Master(persistentListOf(BOOK_1)))
+ }
+ }
+
+ @Test
+ fun startsWithCachedItemsWhenDataIsNotEmpty() = test {
+ val state = createState(MainDataState(items = persistentListOf(BOOK_2)))
+
+ state.start(stateMachine)
+
+ coVerify(ordering = Ordering.ORDERED) {
+ stateMachine.setUiState(MainUiState.List.Master(persistentListOf(BOOK_2)))
+ stateMachine.setUiState(MainUiState.List.Master(persistentListOf(BOOK_1)))
+ }
+ }
+
+ @Test
+ fun updatesWithRepository() = test {
+ val state = createState()
+
+ state.start(stateMachine)
+ items.emit(persistentListOf(BOOK_2))
+
+ coVerify(ordering = Ordering.ORDERED) {
+ stateMachine.setUiState(MainUiState.List.Loading)
+ stateMachine.setUiState(MainUiState.List.Master(persistentListOf(BOOK_1)))
+ stateMachine.setUiState(MainUiState.List.Master(persistentListOf(BOOK_2)))
+ }
+ }
+
+ @Test
+ fun selectsItem() = test {
+ every { factory.book(any(), any()) } returns nextState
+ val state = createState()
+
+ state.start(stateMachine)
+ yield()
+ state.process(MainGesture.BookSelected(BOOK_1.id))
+
+ coVerify {
+ factory.book(
+ data = withArg {
+ assertEquals(persistentListOf(BOOK_1), it.items)
+ },
+ input = eq(BookInput(BOOK_1.id, BOOK_1.title))
+ )
+ stateMachine.setMachineState(nextState)
+ }
+ }
+
+ @Test
+ fun doesNothingOnInvalidItem() = test {
+ every { factory.book(any(), any()) } returns nextState
+ val state = createState()
+
+ state.start(stateMachine)
+ yield()
+ state.process(MainGesture.BookSelected(100500))
+
+ coVerify(exactly = 0) {
+ factory.book(any(), any())
+ stateMachine.setMachineState(nextState)
+ }
+ }
+
+ @Test
+ fun terminatesOnBack() {
+ every { factory.terminated() } returns nextState
+ val state = createState()
+
+ state.start(stateMachine)
+ state.process(MainGesture.Back)
+
+ coVerify {
+ factory.terminated()
+ stateMachine.setMachineState(nextState)
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/books/book/.gitignore b/examples/books/book/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/examples/books/book/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/examples/books/book/build.gradle.kts b/examples/books/book/build.gradle.kts
new file mode 100644
index 0000000..7e7c6a2
--- /dev/null
+++ b/examples/books/book/build.gradle.kts
@@ -0,0 +1,90 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ alias(libs.plugins.android.lib)
+ alias(libs.plugins.compose)
+ alias(libs.plugins.google.ksp)
+ alias(libs.plugins.hilt)
+}
+
+val androidMinSdkVersion: Int by project.extra
+val androidCompileSdkVersion: Int by project.extra
+
+android {
+ compileSdk = androidCompileSdkVersion
+
+ defaultConfig {
+ minSdk = androidMinSdkVersion
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ isCoreLibraryDesugaringEnabled = true
+ }
+ kotlin {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_17)
+ }
+ }
+ buildFeatures {
+ compose = true
+ }
+ namespace = "com.motorro.statemachine.books.book"
+}
+
+dependencies {
+ implementation(project(":commonstatemachine"))
+ implementation(project(":coroutines"))
+ implementation(project(":commonflow:data"))
+
+ implementation(project(":examples:commoncore"))
+ implementation(project(":examples:androidcore"))
+
+ implementation(project(":examples:books:domain"))
+
+ coreLibraryDesugaring(libs.desugaring)
+
+ implementation(libs.kotlin.coroutines.core)
+
+ implementation(platform(libs.compose.bom))
+ implementation(libs.bundles.compose.core)
+ implementation(libs.compose.foundation)
+ implementation(libs.compose.foundation.layouts)
+ implementation(libs.compose.material.icons)
+
+ implementation(libs.hilt.android)
+ implementation(libs.hilt.compose)
+ ksp(libs.hilt.compiler)
+ ksp(libs.hilt.compiler.androidx)
+
+ debugImplementation(libs.compose.tooling)
+
+ testImplementation(libs.bundles.test.core)
+ testImplementation(libs.test.kotlin.coroutines)
+}
diff --git a/examples/books/book/consumer-rules.pro b/examples/books/book/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/examples/books/book/demo/.gitignore b/examples/books/book/demo/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/examples/books/book/demo/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/examples/books/book/demo/build.gradle.kts b/examples/books/book/demo/build.gradle.kts
new file mode 100644
index 0000000..c3c588f
--- /dev/null
+++ b/examples/books/book/demo/build.gradle.kts
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("unused")
+
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+/*
+ * Copyright 2022 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id("com.android.application")
+ id("org.jetbrains.kotlin.plugin.compose")
+ alias(libs.plugins.google.ksp)
+ alias(libs.plugins.hilt)
+}
+
+val versionCode: String by project.extra
+val versionName: String by project.extra
+val androidMinSdkVersion: Int by project.extra
+val androidCompileSdkVersion: Int by project.extra
+val androidTargetSdkVersion: Int by project.extra
+
+android {
+ compileSdk = androidCompileSdkVersion
+
+ defaultConfig {
+ applicationId = "com.motorro.statemachine.books.book.demo"
+
+ minSdk = androidMinSdkVersion
+ targetSdk = androidTargetSdkVersion
+ versionCode = versionCode
+ versionName = versionName
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ vectorDrawables {
+ useSupportLibrary = true
+ }
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+ }
+ }
+ compileOptions {
+ isCoreLibraryDesugaringEnabled = true
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ kotlin {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_17)
+ }
+ }
+ buildFeatures {
+ compose = true
+ }
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+ namespace = "com.motorro.statemachine.books.book.demo"
+}
+
+dependencies {
+ implementation(project(":commonstatemachine"))
+ implementation(project(":commonflow:data"))
+ implementation(project(":commonflow:compose"))
+ implementation(project(":commonflow:viewmodel"))
+
+ implementation(project(":examples:commoncore"))
+ implementation(project(":examples:androidcore"))
+ implementation(project(":examples:books:domain"))
+ implementation(project(":examples:books:book"))
+
+ coreLibraryDesugaring(libs.desugaring)
+
+ implementation(libs.androidx.core)
+ implementation(libs.androidx.lifecycle.runtime)
+ implementation(libs.androidx.lifecycle.livedata)
+ implementation(libs.androidx.lifecycle.viewmodel)
+
+ implementation(libs.kotlin.immutable)
+ implementation(libs.kotlin.coroutines.core)
+ implementation(libs.kotlin.coroutines.android)
+
+ implementation(platform(libs.compose.bom))
+
+ implementation(libs.bundles.compose.core)
+ implementation(libs.compose.activity)
+ implementation(libs.compose.viewmodel)
+ implementation(libs.compose.foundation)
+ implementation(libs.compose.foundation.layouts)
+ implementation(libs.compose.material.icons)
+
+ implementation(libs.hilt.android)
+ implementation(libs.hilt.compose)
+ ksp(libs.hilt.compiler)
+ ksp(libs.hilt.compiler.androidx)
+
+ debugImplementation(libs.compose.tooling)
+}
diff --git a/examples/books/book/demo/proguard-rules.pro b/examples/books/book/demo/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/examples/books/book/demo/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/examples/books/book/demo/src/main/AndroidManifest.xml b/examples/books/book/demo/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..0c5ede3
--- /dev/null
+++ b/examples/books/book/demo/src/main/AndroidManifest.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/books/book/demo/src/main/kotlin/com/motorro/statemachine/books/book/demo/App.kt b/examples/books/book/demo/src/main/kotlin/com/motorro/statemachine/books/book/demo/App.kt
new file mode 100644
index 0000000..5567f41
--- /dev/null
+++ b/examples/books/book/demo/src/main/kotlin/com/motorro/statemachine/books/book/demo/App.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.book.demo
+
+import android.app.Application
+import dagger.hilt.android.HiltAndroidApp
+import timber.log.Timber
+
+@HiltAndroidApp
+class App : Application() {
+ override fun onCreate() {
+ super.onCreate()
+ setupLogger()
+ }
+
+ private fun setupLogger() {
+ Timber.plant(Timber.DebugTree())
+ }
+}
\ No newline at end of file
diff --git a/examples/books/book/demo/src/main/kotlin/com/motorro/statemachine/books/book/demo/MainActivity.kt b/examples/books/book/demo/src/main/kotlin/com/motorro/statemachine/books/book/demo/MainActivity.kt
new file mode 100644
index 0000000..47aa33c
--- /dev/null
+++ b/examples/books/book/demo/src/main/kotlin/com/motorro/statemachine/books/book/demo/MainActivity.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.book.demo
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.enableEdgeToEdge
+import com.motorro.commonstatemachine.flow.viewmodel.setStateMachineContent
+import com.motorro.statemachine.androidcore.ui.theme.CommonStateMachineTheme
+import com.motorro.statemachine.books.book.api.BookGesture
+import com.motorro.statemachine.books.book.api.BookInput
+import com.motorro.statemachine.books.book.api.BookUiState
+import com.motorro.statemachine.books.book.ui.BookScreen
+import dagger.hilt.android.AndroidEntryPoint
+import dagger.hilt.android.lifecycle.withCreationCallback
+
+@AndroidEntryPoint
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ enableEdgeToEdge()
+ super.onCreate(savedInstanceState)
+
+ setStateMachineContent(
+ extrasProducer = {
+ defaultViewModelCreationExtras.withCreationCallback {
+ it.create(BookInput(BOOK.id, BOOK.title))
+ }
+ },
+ content = { state, onGesture ->
+ CommonStateMachineTheme {
+ BookScreen(state, onGesture)
+ }
+ }
+ )
+ }
+}
diff --git a/examples/books/book/demo/src/main/kotlin/com/motorro/statemachine/books/book/demo/MainViewModel.kt b/examples/books/book/demo/src/main/kotlin/com/motorro/statemachine/books/book/demo/MainViewModel.kt
new file mode 100644
index 0000000..143073a
--- /dev/null
+++ b/examples/books/book/demo/src/main/kotlin/com/motorro/statemachine/books/book/demo/MainViewModel.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.book.demo
+
+import com.motorro.commonstatemachine.flow.viewmodel.CommonFlowViewModel
+import com.motorro.statemachine.books.book.api.BookApi
+import com.motorro.statemachine.books.book.api.BookGesture
+import com.motorro.statemachine.books.book.api.BookInput
+import com.motorro.statemachine.books.book.api.BookUiState
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import dagger.hilt.android.lifecycle.HiltViewModel
+
+@HiltViewModel(assistedFactory = MainViewModel.Factory::class)
+class MainViewModel @AssistedInject internal constructor(
+ @Assisted input: BookInput,
+ bookApi: BookApi
+) : CommonFlowViewModel(
+ api = bookApi,
+ init = input
+) {
+ @AssistedFactory
+ interface Factory {
+ fun create(input: BookInput): MainViewModel
+ }
+}
\ No newline at end of file
diff --git a/examples/books/book/demo/src/main/kotlin/com/motorro/statemachine/books/book/demo/data.kt b/examples/books/book/demo/src/main/kotlin/com/motorro/statemachine/books/book/demo/data.kt
new file mode 100644
index 0000000..bd22232
--- /dev/null
+++ b/examples/books/book/demo/src/main/kotlin/com/motorro/statemachine/books/book/demo/data.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.book.demo
+
+import com.motorro.statemachine.books.domain.entity.Book
+import kotlinx.collections.immutable.persistentListOf
+
+internal val BOOK = Book(
+ id = 1,
+ title = "The Hobbit",
+ authors = persistentListOf("J.R.R. Tolkien"),
+ content = "A reluctant hobbit, Bilbo Baggins, sets out to the Lonely Mountain with a spirited group of dwarves to reclaim their mountain home, and a treasure within, from the dragon Smaug."
+)
\ No newline at end of file
diff --git a/examples/books/book/demo/src/main/kotlin/com/motorro/statemachine/books/book/demo/di/ApplicationModule.kt b/examples/books/book/demo/src/main/kotlin/com/motorro/statemachine/books/book/demo/di/ApplicationModule.kt
new file mode 100644
index 0000000..56d4fee
--- /dev/null
+++ b/examples/books/book/demo/src/main/kotlin/com/motorro/statemachine/books/book/demo/di/ApplicationModule.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.book.demo.di
+
+import com.motorro.statemachine.books.book.demo.BOOK
+import com.motorro.statemachine.books.domain.entity.Book
+import com.motorro.statemachine.books.domain.entity.ListBook
+import com.motorro.statemachine.books.domain.repository.BookRepository
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import javax.inject.Singleton
+import kotlin.time.Duration.Companion.seconds
+
+@Module
+@InstallIn(SingletonComponent::class)
+internal class ApplicationModule {
+ @Provides
+ @Singleton
+ fun itemRepository(): BookRepository = object : BookRepository {
+ override val books: Flow>
+ get() = throw NotImplementedError("Not implemented in this demo")
+
+ override suspend fun getBook(id: Int): Book {
+ delay(1.seconds)
+ return BOOK
+ }
+
+ override suspend fun deleteBook(id: Int) {
+ delay(1.seconds)
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/books/book/demo/src/main/res/drawable-v24/ic_launcher_foreground.xml b/examples/books/book/demo/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..40ce517
--- /dev/null
+++ b/examples/books/book/demo/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/books/book/demo/src/main/res/drawable/ic_launcher_background.xml b/examples/books/book/demo/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..997ef0b
--- /dev/null
+++ b/examples/books/book/demo/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,183 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/books/book/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/examples/books/book/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..0147ca8
--- /dev/null
+++ b/examples/books/book/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/books/book/demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/examples/books/book/demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..0147ca8
--- /dev/null
+++ b/examples/books/book/demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/books/book/demo/src/main/res/mipmap-hdpi/ic_launcher.webp b/examples/books/book/demo/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/examples/books/book/demo/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/examples/books/book/demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/examples/books/book/demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/examples/books/book/demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/examples/books/book/demo/src/main/res/mipmap-mdpi/ic_launcher.webp b/examples/books/book/demo/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/examples/books/book/demo/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/examples/books/book/demo/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/examples/books/book/demo/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/examples/books/book/demo/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/examples/books/book/demo/src/main/res/mipmap-xhdpi/ic_launcher.webp b/examples/books/book/demo/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/examples/books/book/demo/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/examples/books/book/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/examples/books/book/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/examples/books/book/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/examples/books/book/demo/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/examples/books/book/demo/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/examples/books/book/demo/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/examples/books/book/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/examples/books/book/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/examples/books/book/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/examples/books/book/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/examples/books/book/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/examples/books/book/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/examples/books/book/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/examples/books/book/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/examples/books/book/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/examples/books/book/demo/src/main/res/values/colors.xml b/examples/books/book/demo/src/main/res/values/colors.xml
new file mode 100644
index 0000000..164d1ef
--- /dev/null
+++ b/examples/books/book/demo/src/main/res/values/colors.xml
@@ -0,0 +1,23 @@
+
+
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/examples/books/book/demo/src/main/res/values/strings.xml b/examples/books/book/demo/src/main/res/values/strings.xml
new file mode 100644
index 0000000..87fa69b
--- /dev/null
+++ b/examples/books/book/demo/src/main/res/values/strings.xml
@@ -0,0 +1,16 @@
+
+
+
+ Book demo
+
\ No newline at end of file
diff --git a/examples/books/book/demo/src/main/res/values/themes.xml b/examples/books/book/demo/src/main/res/values/themes.xml
new file mode 100644
index 0000000..f9d35b1
--- /dev/null
+++ b/examples/books/book/demo/src/main/res/values/themes.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/books/book/demo/src/main/res/xml/backup_rules.xml b/examples/books/book/demo/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..18b86f4
--- /dev/null
+++ b/examples/books/book/demo/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/books/book/demo/src/main/res/xml/data_extraction_rules.xml b/examples/books/book/demo/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..2d914d6
--- /dev/null
+++ b/examples/books/book/demo/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/books/book/proguard-rules.pro b/examples/books/book/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/examples/books/book/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/api/BookApi.kt b/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/api/BookApi.kt
new file mode 100644
index 0000000..fe12321
--- /dev/null
+++ b/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/api/BookApi.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.book.api
+
+import com.motorro.commonstatemachine.CommonMachineState
+import com.motorro.commonstatemachine.flow.data.CommonFlowDataApi
+import com.motorro.commonstatemachine.flow.data.CommonFlowHost
+import com.motorro.statemachine.books.book.state.BookFactory
+import javax.inject.Inject
+
+/**
+ * Item common flow
+ */
+class BookApi @Inject internal constructor(private val factory: BookFactory.Factory) : CommonFlowDataApi {
+ override fun init(flowHost: CommonFlowHost, input: BookInput): CommonMachineState =
+ factory.create(flowHost).loading(input)
+
+ override fun getDefaultUiState(): BookUiState = BookUiState.Loading("")
+ override fun getBackGesture(): BookGesture = BookGesture.Back
+}
\ No newline at end of file
diff --git a/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/api/BookFlowHost.kt b/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/api/BookFlowHost.kt
new file mode 100644
index 0000000..877d02d
--- /dev/null
+++ b/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/api/BookFlowHost.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.book.api
+
+import com.motorro.commonstatemachine.flow.data.CommonFlowHost
+
+/**
+ * Hosting interface
+ */
+typealias ItemFlowHost = CommonFlowHost
\ No newline at end of file
diff --git a/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/api/BookGesture.kt b/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/api/BookGesture.kt
new file mode 100644
index 0000000..39b6daa
--- /dev/null
+++ b/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/api/BookGesture.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.book.api
+
+/**
+ * Item item flow gesture
+ */
+sealed class BookGesture {
+ internal data object Back : BookGesture()
+ internal data object Delete : BookGesture()
+ internal data object Confirm : BookGesture()
+ internal data object Cancel : BookGesture()
+}
diff --git a/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/api/BookInput.kt b/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/api/BookInput.kt
new file mode 100644
index 0000000..04c4953
--- /dev/null
+++ b/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/api/BookInput.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.book.api
+
+/**
+ * Flow input
+ */
+data class BookInput(val id: Int, val title: String = "")
\ No newline at end of file
diff --git a/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/api/BookUiState.kt b/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/api/BookUiState.kt
new file mode 100644
index 0000000..89bb7e1
--- /dev/null
+++ b/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/api/BookUiState.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.book.api
+
+import com.motorro.statemachine.books.domain.entity.Book
+
+/**
+ * Item sub-flow UI state
+ */
+sealed class BookUiState {
+ /**
+ * Loading
+ */
+ data class Loading(val title: String) : BookUiState()
+
+ /**
+ * Content
+ */
+ internal data class Content(val book: Book, val showDeleteConfirmation: Boolean) : BookUiState()
+
+ /**
+ * Error
+ */
+ internal data class Error(val title: String, val error: Throwable) : BookUiState()
+}
diff --git a/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/data/BookDataState.kt b/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/data/BookDataState.kt
new file mode 100644
index 0000000..075b7bc
--- /dev/null
+++ b/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/data/BookDataState.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.book.data
+
+import com.motorro.statemachine.books.book.api.BookInput
+import com.motorro.statemachine.books.domain.entity.Book
+
+/**
+ * Item sub-flow internal data
+ */
+data class BookDataState(
+ val input: BookInput,
+ val book: Book? = null
+)
\ No newline at end of file
diff --git a/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/BaseBookState.kt b/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/BaseBookState.kt
new file mode 100644
index 0000000..90277d3
--- /dev/null
+++ b/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/BaseBookState.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.book.state
+
+import com.motorro.commonstatemachine.coroutines.CoroutineState
+import com.motorro.statemachine.books.book.api.BookGesture
+import com.motorro.statemachine.books.book.api.BookUiState
+import com.motorro.statemachine.commoncore.log.Logger
+
+internal abstract class BaseBookState(context: BookContext) : CoroutineState(), BookContext by context {
+ override fun doProcess(gesture: BookGesture) {
+ Logger.w("Unsupported gesture: $gesture")
+ }
+}
\ No newline at end of file
diff --git a/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/BookContext.kt b/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/BookContext.kt
new file mode 100644
index 0000000..6e8ce27
--- /dev/null
+++ b/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/BookContext.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.book.state
+
+/**
+ * Item sub-flow common components
+ */
+internal interface BookContext {
+ val factory: BookFactory
+}
\ No newline at end of file
diff --git a/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/BookFactory.kt b/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/BookFactory.kt
new file mode 100644
index 0000000..9abdb71
--- /dev/null
+++ b/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/BookFactory.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.book.state
+
+import com.motorro.commonstatemachine.CommonMachineState
+import com.motorro.statemachine.books.book.api.BookGesture
+import com.motorro.statemachine.books.book.api.BookInput
+import com.motorro.statemachine.books.book.api.BookUiState
+import com.motorro.statemachine.books.book.api.ItemFlowHost
+import com.motorro.statemachine.books.book.data.BookDataState
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import javax.inject.Provider
+
+internal interface BookFactory {
+ fun loading(input: BookInput): CommonMachineState
+ fun content(data: BookDataState): CommonMachineState
+ fun deleteConfirmation(data: BookDataState): CommonMachineState
+ fun error(dataState: BookDataState, error: Throwable): CommonMachineState
+ fun deleting(data: BookDataState): CommonMachineState
+ fun terminated(): CommonMachineState
+
+ @AssistedFactory
+ interface Factory {
+ fun create(host: ItemFlowHost): Impl
+ }
+
+ class Impl @AssistedInject constructor(
+ @Assisted private val host: ItemFlowHost,
+ private val createLoadingState: Provider,
+ private val createDeletingState: Provider
+ ) : BookFactory {
+
+ private val context = object : BookContext {
+ override val factory: BookFactory = this@Impl
+ }
+
+ override fun loading(input: BookInput): CommonMachineState = createLoadingState.get().invoke(
+ context,
+ BookDataState(input)
+ )
+
+ override fun content(data: BookDataState) = ContentState(
+ context,
+ data
+ )
+
+ override fun deleteConfirmation(data: BookDataState) = DeleteConfirmationState(
+ context,
+ data
+ )
+
+ override fun error(dataState: BookDataState, error: Throwable) = ErrorState(
+ context,
+ dataState,
+ error
+ )
+
+ override fun deleting(data: BookDataState) = createDeletingState.get().invoke(
+ context,
+ data
+ )
+
+ override fun terminated() = object : CommonMachineState() {
+ override fun doStart() {
+ host.onComplete(Unit)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/ContentState.kt b/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/ContentState.kt
new file mode 100644
index 0000000..d8812ae
--- /dev/null
+++ b/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/ContentState.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.book.state
+
+import com.motorro.statemachine.books.book.api.BookGesture
+import com.motorro.statemachine.books.book.api.BookUiState
+import com.motorro.statemachine.books.book.data.BookDataState
+import com.motorro.statemachine.commoncore.log.Logger
+
+internal class ContentState(
+ context: BookContext,
+ private val data: BookDataState
+) : BaseBookState(context) {
+
+ val item get() = checkNotNull(data.book) {
+ "Illegal state: Item is null!"
+ }
+
+ override fun doStart() {
+ setUiState(BookUiState.Content(item, false))
+ }
+
+ override fun doProcess(gesture: BookGesture) {
+ when(gesture) {
+ BookGesture.Back -> {
+ Logger.d("Back gesture. Terminating...")
+ setMachineState(factory.terminated())
+ }
+ BookGesture.Delete -> {
+ Logger.d("Delete gesture. Showing delete confirmation...")
+ setMachineState(factory.deleteConfirmation(data))
+ }
+ else -> super.doProcess(gesture)
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/DeleteConfirmationState.kt b/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/DeleteConfirmationState.kt
new file mode 100644
index 0000000..c03bf53
--- /dev/null
+++ b/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/DeleteConfirmationState.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.book.state
+
+import com.motorro.statemachine.books.book.api.BookGesture
+import com.motorro.statemachine.books.book.api.BookUiState
+import com.motorro.statemachine.books.book.data.BookDataState
+import com.motorro.statemachine.commoncore.log.Logger
+
+internal class DeleteConfirmationState(
+ context: BookContext,
+ private val data: BookDataState
+) : BaseBookState(context) {
+
+ override fun doStart() {
+ val item = requireNotNull(data.book) {
+ "Item is null - unexpected state"
+ }
+ setUiState(BookUiState.Content(item, true))
+ }
+
+ override fun doProcess(gesture: BookGesture) {
+ when(gesture) {
+ BookGesture.Back, BookGesture.Cancel -> {
+ Logger.d("Delete canceled. Back to content...")
+ setMachineState(factory.content(data))
+ }
+ BookGesture.Confirm -> {
+ Logger.d("Delete confirmed. Deleting...")
+ setMachineState(factory.deleting(data))
+ }
+ else -> super.doProcess(gesture)
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/DeletingState.kt b/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/DeletingState.kt
new file mode 100644
index 0000000..c162702
--- /dev/null
+++ b/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/DeletingState.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.book.state
+
+import com.motorro.statemachine.books.book.api.BookUiState
+import com.motorro.statemachine.books.book.data.BookDataState
+import com.motorro.statemachine.books.domain.repository.BookRepository
+import com.motorro.statemachine.commoncore.log.Logger
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+internal class DeletingState(
+ context: BookContext,
+ private val data: BookDataState,
+ private val repository: BookRepository
+) : BaseBookState(context) {
+
+ val item get() = checkNotNull(data.book) {
+ "Illegal state: Item is null!"
+ }
+
+ override fun doStart() {
+ setUiState(BookUiState.Loading(item.title))
+ delete()
+ }
+
+ private fun delete() = stateScope.launch {
+ Logger.d("Deleting item: ${item.id}")
+ try {
+ repository.deleteBook(item.id)
+ Logger.d("Item ${item.id} deleted. Terminating...")
+ setMachineState(factory.terminated())
+ } catch (e: Throwable) {
+ Logger.e(e, "Failed to delete item: ${item.id}")
+ setMachineState(factory.error(data, e))
+ }
+ }
+
+ class Factory @Inject constructor(private val repository: BookRepository) {
+ operator fun invoke(context: BookContext, data: BookDataState) = DeletingState(
+ context,
+ data,
+ repository
+ )
+ }
+}
\ No newline at end of file
diff --git a/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/ErrorState.kt b/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/ErrorState.kt
new file mode 100644
index 0000000..942f150
--- /dev/null
+++ b/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/ErrorState.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.book.state
+
+import com.motorro.statemachine.books.book.api.BookGesture
+import com.motorro.statemachine.books.book.api.BookUiState
+import com.motorro.statemachine.books.book.data.BookDataState
+import com.motorro.statemachine.commoncore.log.Logger
+
+internal class ErrorState(
+ context: BookContext,
+ private val data: BookDataState,
+ private val error: Throwable
+) : BaseBookState(context) {
+
+ override fun doStart() {
+ setUiState(
+ BookUiState.Error(
+ data.book?.title ?: data.input.title,
+ error
+ )
+ )
+ }
+
+ override fun doProcess(gesture: BookGesture) {
+ when(gesture) {
+ BookGesture.Back, BookGesture.Confirm -> {
+ Logger.d("Dismissed. Terminating...")
+ setMachineState(factory.terminated())
+ }
+ else -> super.doProcess(gesture)
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/LoadingState.kt b/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/LoadingState.kt
new file mode 100644
index 0000000..c8c8c48
--- /dev/null
+++ b/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/LoadingState.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.book.state
+
+import com.motorro.statemachine.books.book.api.BookGesture
+import com.motorro.statemachine.books.book.api.BookUiState
+import com.motorro.statemachine.books.book.data.BookDataState
+import com.motorro.statemachine.books.domain.repository.BookRepository
+import com.motorro.statemachine.commoncore.log.Logger
+import kotlinx.coroutines.ensureActive
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+internal class LoadingState(
+ context: BookContext,
+ private val data: BookDataState,
+ private val repository: BookRepository
+) : BaseBookState(context) {
+
+ override fun doStart() {
+ setUiState(BookUiState.Loading(data.input.title))
+ load()
+ }
+
+ private fun load() = stateScope.launch {
+ val id = data.input.id
+ Logger.d("Loading item: $id")
+ try {
+ val item = repository.getBook(id)
+ Logger.d("Item loaded: $item")
+ setMachineState(factory.content(data.copy(book = item)))
+ } catch (e: Throwable) {
+ ensureActive()
+ Logger.e(e, "Failed to load item: $id")
+ setMachineState(factory.error(data, e))
+ }
+ }
+
+ override fun doProcess(gesture: BookGesture) {
+ when (gesture) {
+ BookGesture.Back -> {
+ Logger.w("Back gesture. Aborting load...")
+ setMachineState(factory.terminated())
+ }
+ else -> super.doProcess(gesture)
+ }
+ }
+
+ class Factory @Inject constructor(private val repository: BookRepository) {
+ operator fun invoke(context: BookContext, dataState: BookDataState) = LoadingState(
+ context,
+ dataState,
+ repository
+ )
+ }
+}
\ No newline at end of file
diff --git a/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/ui/BookContentScreen.kt b/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/ui/BookContentScreen.kt
new file mode 100644
index 0000000..c93ae76
--- /dev/null
+++ b/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/ui/BookContentScreen.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.book.ui
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.motorro.statemachine.books.book.R
+import com.motorro.statemachine.books.book.api.BookGesture
+import com.motorro.statemachine.books.book.api.BookUiState
+import com.motorro.statemachine.books.domain.entity.Book
+import kotlinx.collections.immutable.persistentListOf
+
+@Composable
+@OptIn(ExperimentalMaterial3Api::class)
+internal fun BookContentScreen(state: BookUiState.Content, onGesture: (BookGesture) -> Unit) {
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text(text = state.book.title) },
+ navigationIcon = {
+ IconButton(onClick = { onGesture(BookGesture.Back) }) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = stringResource(id = R.string.content_back)
+ )
+ }
+ },
+ actions = {
+ IconButton(onClick = { onGesture(BookGesture.Delete) }) {
+ Icon(
+ imageVector = Icons.Default.Delete,
+ contentDescription = stringResource(id = R.string.content_delete_item)
+ )
+ }
+ }
+ )
+ }
+ ) { padding ->
+ if (state.showDeleteConfirmation) {
+ DeleteConfirmationDialog(onGesture)
+ }
+ BookContent(
+ padding = padding,
+ book = state.book
+ )
+ }
+}
+
+@Composable
+private fun BookContent(padding: PaddingValues, book: Book) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .padding(padding)
+ .verticalScroll(scrollState)
+ .fillMaxWidth()
+ .padding(16.dp)
+ ) {
+ Text(
+ text = book.title,
+ style = MaterialTheme.typography.titleLarge
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ text = book.authors.joinToString(),
+ style = MaterialTheme.typography.titleMedium
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ text = book.content,
+ style = MaterialTheme.typography.bodyMedium
+ )
+ }
+}
+
+@Composable
+private fun DeleteConfirmationDialog(onGesture: (BookGesture) -> Unit) {
+ AlertDialog(
+ onDismissRequest = { onGesture(BookGesture.Cancel) },
+ title = { Text(text = stringResource(id = R.string.item_delete_confirmation_title)) },
+ text = { Text(text = stringResource(id = R.string.item_delete_confirmation_message)) },
+ confirmButton = {
+ TextButton(onClick = { onGesture(BookGesture.Confirm) }) {
+ Text(text = stringResource(id = R.string.item_delete_confirmation_confirm))
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = { onGesture(BookGesture.Cancel) }) {
+ Text(text = stringResource(id = R.string.item_delete_confirmation_cancel))
+ }
+ }
+ )
+}
+
+@Preview
+@Composable
+private fun BookContentScreenPreview() {
+ BookContentScreen(
+ state = BookUiState.Content(
+ book = Book(1, "Title", persistentListOf("Author"), "Content"),
+ showDeleteConfirmation = false
+ ),
+ onGesture = {}
+ )
+}
+
+@Preview
+@Composable
+private fun BookContentScreenWithConfirmationPreview() {
+ BookContentScreen(
+ state = BookUiState.Content(
+ book = Book(1, "Title", persistentListOf("Author"), "Content"),
+ showDeleteConfirmation = true
+ ),
+ onGesture = {}
+ )
+}
diff --git a/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/ui/BookScreen.kt b/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/ui/BookScreen.kt
new file mode 100644
index 0000000..da169e2
--- /dev/null
+++ b/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/ui/BookScreen.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.book.ui
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import com.motorro.statemachine.androidcore.compose.Error
+import com.motorro.statemachine.androidcore.compose.Loading
+import com.motorro.statemachine.books.book.R
+import com.motorro.statemachine.books.book.api.BookGesture
+import com.motorro.statemachine.books.book.api.BookUiState
+
+@Composable
+fun BookScreen(state: BookUiState, onGesture: (BookGesture) -> Unit) {
+ when(state) {
+ is BookUiState.Loading -> LoadingScreen(state, onGesture)
+ is BookUiState.Content -> BookContentScreen(state, onGesture)
+ is BookUiState.Error -> ErrorScreen(state, onGesture)
+ }
+}
+
+@Composable
+@OptIn(ExperimentalMaterial3Api::class)
+private fun LoadingScreen(state: BookUiState.Loading, onGesture: (BookGesture) -> Unit) {
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text(text = state.title) },
+ navigationIcon = {
+ IconButton(onClick = { onGesture(BookGesture.Back) }) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = stringResource(id = R.string.content_back)
+ )
+ }
+ }
+ )
+ }
+ ) { padding ->
+ Loading(
+ modifier = Modifier.padding(padding)
+ )
+ }
+}
+
+@Composable
+@OptIn(ExperimentalMaterial3Api::class)
+private fun ErrorScreen(state: BookUiState.Error, onGesture: (BookGesture) -> Unit) {
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text(text = state.title) },
+ navigationIcon = {
+ IconButton(onClick = { onGesture(BookGesture.Back) }) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = stringResource(id = R.string.content_back)
+ )
+ }
+ }
+ )
+ }
+ ) { padding ->
+ Error(
+ error = state.error,
+ onDismiss = { onGesture(BookGesture.Confirm) },
+ modifier = Modifier.padding(padding)
+ )
+ }
+}
diff --git a/examples/books/book/src/main/res/values/strings.xml b/examples/books/book/src/main/res/values/strings.xml
new file mode 100644
index 0000000..e7e5763
--- /dev/null
+++ b/examples/books/book/src/main/res/values/strings.xml
@@ -0,0 +1,22 @@
+
+
+
+
+ Back
+ Delete item
+ Delete item?
+ Are you sure you want to delete this item? This action cannot be undone.
+ Delete
+ Cancel
+
diff --git a/examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/mock.kt b/examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/mock.kt
new file mode 100644
index 0000000..974ceae
--- /dev/null
+++ b/examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/mock.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.book
+
+import com.motorro.statemachine.books.domain.entity.Book
+import kotlinx.collections.immutable.persistentListOf
+
+internal const val BOOK_ID = 10
+internal const val BOOK_TITLE = "Book title"
+
+internal val Book = Book(
+ id = BOOK_ID,
+ title = BOOK_TITLE,
+ authors = persistentListOf("Author 1", "Author 2"),
+ content = "Book content"
+)
\ No newline at end of file
diff --git a/examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/state/BaseStateTest.kt b/examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/state/BaseStateTest.kt
new file mode 100644
index 0000000..1533a52
--- /dev/null
+++ b/examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/state/BaseStateTest.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.book.state
+
+import com.motorro.commonstatemachine.CommonStateMachine
+import com.motorro.statemachine.books.book.api.BookGesture
+import com.motorro.statemachine.books.book.api.BookUiState
+import io.mockk.mockk
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Before
+
+@OptIn(ExperimentalCoroutinesApi::class)
+internal abstract class BaseStateTest {
+ protected lateinit var stateMachine: CommonStateMachine
+ protected lateinit var factory: BookFactory
+ protected lateinit var context: BookContext
+ protected lateinit var nextState: BaseBookState
+ protected lateinit var dispatcher: TestDispatcher
+
+ @Before
+ fun init() {
+ dispatcher = UnconfinedTestDispatcher()
+ Dispatchers.setMain(dispatcher)
+ stateMachine = mockk(relaxed = true)
+ factory = mockk()
+ context = object : BookContext {
+ override val factory: BookFactory = this@BaseStateTest.factory
+ }
+ nextState = mockk(relaxed = true)
+ doInit()
+ }
+
+ @After
+ fun deinit() {
+ Dispatchers.resetMain()
+ }
+
+ protected fun test(block: suspend TestScope.() -> Unit) = runTest(
+ dispatcher,
+ testBody = block
+ )
+
+ protected open fun doInit() = Unit
+}
\ No newline at end of file
diff --git a/examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/state/ContentStateTest.kt b/examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/state/ContentStateTest.kt
new file mode 100644
index 0000000..cc42f52
--- /dev/null
+++ b/examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/state/ContentStateTest.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.book.state
+
+import com.motorro.statemachine.books.book.BOOK_ID
+import com.motorro.statemachine.books.book.BOOK_TITLE
+import com.motorro.statemachine.books.book.Book
+import com.motorro.statemachine.books.book.api.BookGesture
+import com.motorro.statemachine.books.book.api.BookInput
+import com.motorro.statemachine.books.book.api.BookUiState
+import com.motorro.statemachine.books.book.data.BookDataState
+import io.mockk.every
+import io.mockk.verify
+import org.junit.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertIs
+
+internal class ContentStateTest : BaseStateTest() {
+
+ private lateinit var data: BookDataState
+ private lateinit var state: ContentState
+
+ override fun doInit() {
+ data = BookDataState(BookInput(BOOK_ID, BOOK_TITLE), Book)
+ state = ContentState(context, data)
+ }
+
+ @Test
+ fun displaysContentOnStart() = test {
+ state.start(stateMachine)
+
+ verify {
+ stateMachine.setUiState(withArg {
+ val content = assertIs(it)
+ assertEquals(Book, content.book)
+ assertEquals(false, content.showDeleteConfirmation)
+ })
+ }
+ }
+
+ @Test
+ fun terminatesOnBack() = test {
+ every { factory.terminated() } returns nextState
+
+ state.start(stateMachine)
+ state.process(BookGesture.Back)
+
+ verify {
+ factory.terminated()
+ stateMachine.setMachineState(nextState)
+ }
+ }
+
+ @Test
+ fun requestsConfirmationOnDelete() = test {
+ every { factory.deleteConfirmation(any()) } returns nextState
+
+ state.start(stateMachine)
+ state.process(BookGesture.Delete)
+
+ verify {
+ factory.deleteConfirmation(data)
+ stateMachine.setMachineState(nextState)
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/state/DeleteConfirmationStateTest.kt b/examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/state/DeleteConfirmationStateTest.kt
new file mode 100644
index 0000000..10538fd
--- /dev/null
+++ b/examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/state/DeleteConfirmationStateTest.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.book.state
+
+import com.motorro.statemachine.books.book.BOOK_ID
+import com.motorro.statemachine.books.book.BOOK_TITLE
+import com.motorro.statemachine.books.book.Book
+import com.motorro.statemachine.books.book.api.BookGesture
+import com.motorro.statemachine.books.book.api.BookInput
+import com.motorro.statemachine.books.book.api.BookUiState
+import com.motorro.statemachine.books.book.data.BookDataState
+import io.mockk.every
+import io.mockk.verify
+import org.junit.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertIs
+
+internal class DeleteConfirmationStateTest : BaseStateTest() {
+
+ private lateinit var data: BookDataState
+ private lateinit var state: DeleteConfirmationState
+
+ override fun doInit() {
+ data = BookDataState(BookInput(BOOK_ID, BOOK_TITLE), Book)
+ state = DeleteConfirmationState(context, data)
+ }
+
+ @Test
+ fun displaysConfirmationOnStart() = test {
+ state.start(stateMachine)
+
+ verify {
+ stateMachine.setUiState(withArg {
+ val content = assertIs(it)
+ assertEquals(Book, content.book)
+ assertEquals(true, content.showDeleteConfirmation)
+ })
+ }
+ }
+
+ @Test
+ fun returnsToContentOnBack() = test {
+ every { factory.content(any()) } returns nextState
+
+ state.start(stateMachine)
+ state.process(BookGesture.Back)
+
+ verify {
+ factory.content(data)
+ stateMachine.setMachineState(nextState)
+ }
+ }
+
+ @Test
+ fun returnsToContentOnCancel() = test {
+ every { factory.content(any()) } returns nextState
+
+ state.start(stateMachine)
+ state.process(BookGesture.Cancel)
+
+ verify {
+ factory.content(data)
+ stateMachine.setMachineState(nextState)
+ }
+ }
+
+ @Test
+ fun proceedsToDeletingOnConfirm() = test {
+ every { factory.deleting(any()) } returns nextState
+
+ state.start(stateMachine)
+ state.process(BookGesture.Confirm)
+
+ verify {
+ factory.deleting(data)
+ stateMachine.setMachineState(nextState)
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/state/DeletingStateTest.kt b/examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/state/DeletingStateTest.kt
new file mode 100644
index 0000000..cf550d9
--- /dev/null
+++ b/examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/state/DeletingStateTest.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.book.state
+
+import com.motorro.statemachine.books.book.BOOK_ID
+import com.motorro.statemachine.books.book.BOOK_TITLE
+import com.motorro.statemachine.books.book.Book
+import com.motorro.statemachine.books.book.api.BookInput
+import com.motorro.statemachine.books.book.api.BookUiState
+import com.motorro.statemachine.books.book.data.BookDataState
+import com.motorro.statemachine.books.domain.repository.BookRepository
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.mockk
+import org.junit.Test
+
+internal class DeletingStateTest : BaseStateTest() {
+
+ private lateinit var data: BookDataState
+ private lateinit var repository: BookRepository
+ private lateinit var state: DeletingState
+
+ override fun doInit() {
+ data = BookDataState(BookInput(BOOK_ID, BOOK_TITLE), Book)
+ repository = mockk()
+ state = DeletingState(context, data, repository)
+ }
+
+ @Test
+ fun deletesItemAndTerminates() = test {
+ coEvery { repository.deleteBook(any()) } returns Unit
+ every { factory.terminated() } returns nextState
+
+ state.start(stateMachine)
+
+ coVerify {
+ stateMachine.setUiState(BookUiState.Loading(BOOK_TITLE))
+ repository.deleteBook(Book.id)
+ factory.terminated()
+ stateMachine.setMachineState(nextState)
+ }
+ }
+
+ @Test
+ fun advancesToErrorIfFails() = test {
+ val error = RuntimeException("Test error")
+ coEvery { repository.deleteBook(any()) } throws error
+ every { factory.error(any(), any()) } returns nextState
+
+ state.start(stateMachine)
+
+ coVerify {
+ stateMachine.setUiState(BookUiState.Loading(BOOK_TITLE))
+ repository.deleteBook(Book.id)
+ factory.error(any(), error)
+ stateMachine.setMachineState(nextState)
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/state/ErrorStateTest.kt b/examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/state/ErrorStateTest.kt
new file mode 100644
index 0000000..f016776
--- /dev/null
+++ b/examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/state/ErrorStateTest.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.book.state
+
+import com.motorro.statemachine.books.book.BOOK_ID
+import com.motorro.statemachine.books.book.BOOK_TITLE
+import com.motorro.statemachine.books.book.Book
+import com.motorro.statemachine.books.book.api.BookGesture
+import com.motorro.statemachine.books.book.api.BookInput
+import com.motorro.statemachine.books.book.api.BookUiState
+import com.motorro.statemachine.books.book.data.BookDataState
+import io.mockk.every
+import io.mockk.verify
+import org.junit.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertIs
+
+internal class ErrorStateTest : BaseStateTest() {
+
+ private lateinit var data: BookDataState
+ private lateinit var error: Throwable
+ private lateinit var state: ErrorState
+
+ override fun doInit() {
+ data = BookDataState(BookInput(BOOK_ID, BOOK_TITLE), Book)
+ error = RuntimeException("Test error")
+ state = ErrorState(context, data, error)
+ }
+
+ @Test
+ fun displaysErrorOnStart() = test {
+ state.start(stateMachine)
+
+ verify {
+ stateMachine.setUiState(withArg {
+ val errorState = assertIs(it)
+ assertEquals(error, errorState.error)
+ })
+ }
+ }
+
+ @Test
+ fun terminatesOnBack() = test {
+ every { factory.terminated() } returns nextState
+
+ state.start(stateMachine)
+ state.process(BookGesture.Back)
+
+ verify {
+ factory.terminated()
+ stateMachine.setMachineState(nextState)
+ }
+ }
+
+ @Test
+ fun terminatesOnConfirm() = test {
+ every { factory.terminated() } returns nextState
+
+ state.start(stateMachine)
+ state.process(BookGesture.Confirm)
+
+ verify {
+ factory.terminated()
+ stateMachine.setMachineState(nextState)
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/state/LoadingStateTest.kt b/examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/state/LoadingStateTest.kt
new file mode 100644
index 0000000..7db5a48
--- /dev/null
+++ b/examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/state/LoadingStateTest.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.book.state
+
+import com.motorro.statemachine.books.book.BOOK_ID
+import com.motorro.statemachine.books.book.BOOK_TITLE
+import com.motorro.statemachine.books.book.Book
+import com.motorro.statemachine.books.book.api.BookGesture
+import com.motorro.statemachine.books.book.api.BookInput
+import com.motorro.statemachine.books.book.api.BookUiState
+import com.motorro.statemachine.books.book.data.BookDataState
+import com.motorro.statemachine.books.domain.repository.BookRepository
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.mockk
+import org.junit.Test
+import kotlin.coroutines.suspendCoroutine
+import kotlin.test.assertEquals
+
+internal class LoadingStateTest : BaseStateTest() {
+ private lateinit var data: BookDataState
+ lateinit var repository: BookRepository
+ lateinit var state: BaseBookState
+
+ override fun doInit() {
+ repository = mockk()
+ data = BookDataState(BookInput(BOOK_ID, BOOK_TITLE), Book)
+ state = LoadingState(context, data, repository)
+ }
+
+ @Test
+ fun loadsItemAndAdvancesToContent() = test {
+ coEvery { repository.getBook(any()) } returns Book
+ every { factory.content(any()) } returns nextState
+
+ state.start(stateMachine)
+
+ coVerify {
+ stateMachine.setUiState(BookUiState.Loading(BOOK_TITLE))
+ repository.getBook(BOOK_ID)
+ factory.content(withArg {
+ assertEquals(Book, it.book)
+ })
+ stateMachine.setMachineState(nextState)
+ }
+ }
+
+ @Test
+ fun loadsItemAndAdvancesToErrorIfFails() = test {
+ val error = RuntimeException("Test error")
+ coEvery { repository.getBook(any()) } throws error
+ every { factory.error(any(), any()) } returns nextState
+
+ state.start(stateMachine)
+
+ coVerify {
+ repository.getBook(BOOK_ID)
+ factory.error(any(), error)
+ stateMachine.setMachineState(nextState)
+ }
+ }
+
+ @Test
+ fun terminatesOnBack() = test {
+ coEvery { repository.getBook(any()) } coAnswers {
+ suspendCoroutine {
+ // No-op
+ }
+ }
+ every { factory.terminated() } returns nextState
+
+ state.start(stateMachine)
+ state.process(BookGesture.Back)
+
+ coVerify {
+ factory.terminated()
+ stateMachine.setMachineState(nextState)
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/books/domain/.gitignore b/examples/books/domain/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/examples/books/domain/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/examples/books/domain/build.gradle.kts b/examples/books/domain/build.gradle.kts
new file mode 100644
index 0000000..f367afd
--- /dev/null
+++ b/examples/books/domain/build.gradle.kts
@@ -0,0 +1,60 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ alias(libs.plugins.android.lib)
+}
+
+val androidMinSdkVersion: Int by project.extra
+val androidCompileSdkVersion: Int by project.extra
+
+android {
+ compileSdk = androidCompileSdkVersion
+
+ defaultConfig {
+ minSdk = androidMinSdkVersion
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ isCoreLibraryDesugaringEnabled = true
+ }
+ kotlin {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_17)
+ }
+ }
+ namespace = "com.motorro.statemachine.books.domain"
+}
+
+dependencies {
+ coreLibraryDesugaring(libs.desugaring)
+
+ api(libs.kotlin.immutable)
+ api(libs.kotlin.coroutines.core)
+}
diff --git a/examples/books/domain/consumer-rules.pro b/examples/books/domain/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/examples/books/domain/proguard-rules.pro b/examples/books/domain/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/examples/books/domain/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/examples/books/domain/src/main/kotlin/com/motorro/statemachine/books/domain/entity/Book.kt b/examples/books/domain/src/main/kotlin/com/motorro/statemachine/books/domain/entity/Book.kt
new file mode 100644
index 0000000..5a4da8b
--- /dev/null
+++ b/examples/books/domain/src/main/kotlin/com/motorro/statemachine/books/domain/entity/Book.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.domain.entity
+
+import kotlinx.collections.immutable.ImmutableList
+
+/**
+ * Content data item
+ */
+data class Book(val id: Int, val title: String, val authors: ImmutableList, val content: String)
\ No newline at end of file
diff --git a/examples/books/domain/src/main/kotlin/com/motorro/statemachine/books/domain/entity/ListBook.kt b/examples/books/domain/src/main/kotlin/com/motorro/statemachine/books/domain/entity/ListBook.kt
new file mode 100644
index 0000000..8b83a30
--- /dev/null
+++ b/examples/books/domain/src/main/kotlin/com/motorro/statemachine/books/domain/entity/ListBook.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.domain.entity
+
+import kotlinx.collections.immutable.ImmutableList
+
+/**
+ * List item
+ */
+data class ListBook(val id: Int, val title: String, val authors: ImmutableList)
\ No newline at end of file
diff --git a/examples/books/domain/src/main/kotlin/com/motorro/statemachine/books/domain/repository/BookRepository.kt b/examples/books/domain/src/main/kotlin/com/motorro/statemachine/books/domain/repository/BookRepository.kt
new file mode 100644
index 0000000..05c6d6a
--- /dev/null
+++ b/examples/books/domain/src/main/kotlin/com/motorro/statemachine/books/domain/repository/BookRepository.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.books.domain.repository
+
+import com.motorro.statemachine.books.domain.entity.Book
+import com.motorro.statemachine.books.domain.entity.ListBook
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Item repository
+ */
+interface BookRepository {
+ val books: Flow>
+ suspend fun getBook(id: Int): Book
+ suspend fun deleteBook(id: Int)
+}
diff --git a/examples/di/api/.gitignore b/examples/di/api/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/examples/di/api/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/examples/di/api/build.gradle.kts b/examples/di/api/build.gradle.kts
new file mode 100644
index 0000000..ab5d430
--- /dev/null
+++ b/examples/di/api/build.gradle.kts
@@ -0,0 +1,71 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ alias(libs.plugins.android.lib)
+ alias(libs.plugins.compose)
+}
+
+val androidMinSdkVersion: Int by project.extra
+val androidCompileSdkVersion: Int by project.extra
+
+android {
+ compileSdk = androidCompileSdkVersion
+
+ defaultConfig {
+ minSdk = androidMinSdkVersion
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ isCoreLibraryDesugaringEnabled = true
+ }
+ kotlin {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_17)
+ }
+ }
+ buildFeatures {
+ compose = true
+ }
+ namespace = "com.motorro.statemachine.di.api"
+}
+
+dependencies {
+ api(project(":commonstatemachine"))
+ api(project(":commonflow:data"))
+ api(project(":commonflow:compose"))
+
+ coreLibraryDesugaring(libs.desugaring)
+
+ implementation(libs.kotlin.coroutines.core)
+
+ implementation(platform(libs.compose.bom))
+
+ implementation(libs.bundles.compose.core)
+}
diff --git a/examples/di/api/consumer-rules.pro b/examples/di/api/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/examples/di/api/proguard-rules.pro b/examples/di/api/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/examples/di/api/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/examples/di/api/src/main/kotlin/com/motorro/statemachine/di/api/AuthApi.kt b/examples/di/api/src/main/kotlin/com/motorro/statemachine/di/api/AuthApi.kt
new file mode 100644
index 0000000..0517567
--- /dev/null
+++ b/examples/di/api/src/main/kotlin/com/motorro/statemachine/di/api/AuthApi.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.api
+
+import androidx.compose.runtime.staticCompositionLocalOf
+import com.motorro.commonstatemachine.flow.compose.CommonFlowUiApi
+import com.motorro.commonstatemachine.flow.data.CommonFlowDataApi
+import com.motorro.statemachine.di.api.data.Session
+
+/**
+ * Auth gesture marker
+ */
+interface AuthGesture
+
+/**
+ * Auth UI state marker
+ */
+interface AuthUiState
+
+/**
+ * Auth flow data API
+ */
+typealias AuthDataApi = CommonFlowDataApi
+
+/**
+ * Auth flow ui API
+ */
+typealias AuthUiApi = CommonFlowUiApi
+
+/**
+ * Local Authentication
+ */
+val LocalAuth = staticCompositionLocalOf {
+ error("No auth ui api provided")
+}
+
+
+
+
diff --git a/examples/di/api/src/main/kotlin/com/motorro/statemachine/di/api/AuthFlowHost.kt b/examples/di/api/src/main/kotlin/com/motorro/statemachine/di/api/AuthFlowHost.kt
new file mode 100644
index 0000000..cdb1b0c
--- /dev/null
+++ b/examples/di/api/src/main/kotlin/com/motorro/statemachine/di/api/AuthFlowHost.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.api
+
+import com.motorro.commonstatemachine.flow.data.CommonFlowHost
+import com.motorro.statemachine.di.api.data.Session
+
+/**
+ * Handles authentication
+ */
+typealias AuthFlowHost = CommonFlowHost
\ No newline at end of file
diff --git a/examples/di/api/src/main/kotlin/com/motorro/statemachine/di/api/SessionManager.kt b/examples/di/api/src/main/kotlin/com/motorro/statemachine/di/api/SessionManager.kt
new file mode 100644
index 0000000..4e74da4
--- /dev/null
+++ b/examples/di/api/src/main/kotlin/com/motorro/statemachine/di/api/SessionManager.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.api
+
+import com.motorro.statemachine.di.api.data.Session
+import kotlinx.coroutines.flow.StateFlow
+
+/**
+ * Session manager
+ */
+interface SessionManager {
+ /**
+ * Session state
+ */
+ val session: StateFlow
+
+ /**
+ * Updates session state
+ * @param session New session state
+ */
+ suspend fun update(session: Session)
+}
\ No newline at end of file
diff --git a/examples/di/api/src/main/kotlin/com/motorro/statemachine/di/api/data/Session.kt b/examples/di/api/src/main/kotlin/com/motorro/statemachine/di/api/data/Session.kt
new file mode 100644
index 0000000..ae26484
--- /dev/null
+++ b/examples/di/api/src/main/kotlin/com/motorro/statemachine/di/api/data/Session.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.api.data
+
+/**
+ * Session state
+ */
+sealed class Session {
+ /**
+ * No session
+ */
+ data object None : Session()
+
+ /**
+ * Session is active
+ */
+ data class Active(val accessToken: String, val refreshToken: String) : Session()
+}
\ No newline at end of file
diff --git a/examples/di/app/.gitignore b/examples/di/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/examples/di/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/examples/di/app/build.gradle.kts b/examples/di/app/build.gradle.kts
new file mode 100644
index 0000000..ec5bec9
--- /dev/null
+++ b/examples/di/app/build.gradle.kts
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("unused")
+
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+/*
+ * Copyright 2022 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id("com.android.application")
+ id("org.jetbrains.kotlin.plugin.compose")
+ alias(libs.plugins.google.ksp)
+ alias(libs.plugins.hilt)
+}
+
+val versionCode: String by project.extra
+val versionName: String by project.extra
+val androidMinSdkVersion: Int by project.extra
+val androidCompileSdkVersion: Int by project.extra
+val androidTargetSdkVersion: Int by project.extra
+
+android {
+ compileSdk = androidCompileSdkVersion
+
+ defaultConfig {
+ applicationId = "com.motorro.statemachine.di"
+
+ minSdk = androidMinSdkVersion
+ targetSdk = androidTargetSdkVersion
+ versionCode = versionCode
+ versionName = versionName
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ vectorDrawables {
+ useSupportLibrary = true
+ }
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+ }
+ }
+ compileOptions {
+ isCoreLibraryDesugaringEnabled = true
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ kotlin {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_17)
+ }
+ }
+ buildFeatures {
+ compose = true
+ }
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+ namespace = "com.motorro.statemachine.di.app"
+
+ flavorDimensions += "auth"
+ productFlavors {
+ create("login") {
+ dimension = "auth"
+ applicationIdSuffix = ".login"
+ }
+ create("social") {
+ dimension = "auth"
+ applicationIdSuffix = ".social"
+ }
+ }
+}
+
+dependencies {
+ implementation(project(":commonstatemachine"))
+ implementation(project(":coroutines"))
+ implementation(project(":examples:commoncore"))
+ implementation(project(":examples:androidcore"))
+ implementation(project(":examples:di:api"))
+
+ "loginImplementation"(project(":examples:di:login"))
+ "socialImplementation"(project(":examples:di:social"))
+
+ coreLibraryDesugaring(libs.desugaring)
+
+ implementation(libs.timber)
+ implementation(libs.androidx.core)
+ implementation(libs.androidx.lifecycle.runtime)
+ implementation(libs.androidx.lifecycle.livedata)
+ implementation(libs.androidx.lifecycle.viewmodel)
+
+ implementation(libs.kotlin.coroutines.core)
+ implementation(libs.kotlin.coroutines.android)
+
+ implementation(platform(libs.compose.bom))
+
+ implementation(libs.bundles.compose.core)
+ implementation(libs.compose.activity)
+ implementation(libs.compose.viewmodel)
+ implementation(libs.compose.foundation)
+ implementation(libs.compose.foundation.layouts)
+
+ implementation(libs.hilt.android)
+ implementation(libs.hilt.compose)
+ ksp(libs.hilt.compiler)
+ ksp(libs.hilt.compiler.androidx)
+
+ debugImplementation(libs.compose.material.icons)
+
+ debugImplementation(libs.compose.tooling)
+
+ testImplementation(libs.bundles.test.core)
+ testImplementation(libs.test.kotlin.coroutines)
+}
diff --git a/examples/di/app/proguard-rules.pro b/examples/di/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/examples/di/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/examples/di/app/src/main/AndroidManifest.xml b/examples/di/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..e09dc21
--- /dev/null
+++ b/examples/di/app/src/main/AndroidManifest.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/App.kt b/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/App.kt
new file mode 100644
index 0000000..ccc579b
--- /dev/null
+++ b/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/App.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.app
+
+import android.app.Application
+import dagger.hilt.android.HiltAndroidApp
+import timber.log.Timber
+
+@HiltAndroidApp
+class App : Application() {
+ override fun onCreate() {
+ super.onCreate()
+ setupLogger()
+ }
+
+ private fun setupLogger() {
+ Timber.plant(Timber.DebugTree())
+ }
+}
\ No newline at end of file
diff --git a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/MainActivity.kt b/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/MainActivity.kt
new file mode 100644
index 0000000..ab05030
--- /dev/null
+++ b/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/MainActivity.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2022 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.app
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.viewModels
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.motorro.statemachine.androidcore.ui.theme.CommonStateMachineTheme
+import com.motorro.statemachine.di.api.AuthUiApi
+import com.motorro.statemachine.di.api.LocalAuth
+import com.motorro.statemachine.di.app.ui.MainScreen
+import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class MainActivity : ComponentActivity() {
+
+ /**
+ * Provided by variant module
+ */
+ @field:Inject
+ lateinit var authUiApi: AuthUiApi
+
+ private val viewModel: MainViewModel by viewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContent {
+ val state by viewModel.uiState.collectAsStateWithLifecycle()
+
+ CommonStateMachineTheme {
+ WithLocals {
+ MainScreen(
+ state = state,
+ onGesture = viewModel::process,
+ onTerminated = ::finish
+ )
+ }
+ }
+ }
+ }
+
+ /**
+ * Provides injected locals
+ */
+ @Composable
+ internal fun WithLocals(block: @Composable () -> Unit) {
+ CompositionLocalProvider(LocalAuth provides authUiApi) {
+ block()
+ }
+ }
+}
diff --git a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/MainViewModel.kt b/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/MainViewModel.kt
new file mode 100644
index 0000000..174bad7
--- /dev/null
+++ b/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/MainViewModel.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.app
+
+import androidx.lifecycle.ViewModel
+import com.motorro.commonstatemachine.coroutines.FlowStateMachine
+import com.motorro.statemachine.di.app.data.MainGesture
+import com.motorro.statemachine.di.app.data.MainUiState
+import com.motorro.statemachine.di.app.state.MainFactory
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.StateFlow
+import javax.inject.Inject
+
+@HiltViewModel
+internal class MainViewModel @Inject constructor(factory: MainFactory) : ViewModel() {
+
+ private val stateMachine = FlowStateMachine(MainUiState.Loading) {
+ factory.content()
+ }
+
+ val uiState: StateFlow get() = stateMachine.uiState
+ fun process(gesture: MainGesture) {
+ stateMachine.process(gesture)
+ }
+}
\ No newline at end of file
diff --git a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/data/MainGesture.kt b/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/data/MainGesture.kt
new file mode 100644
index 0000000..9b297cc
--- /dev/null
+++ b/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/data/MainGesture.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.app.data
+
+import com.motorro.statemachine.di.api.AuthGesture
+
+/**
+ * Main flow gesture
+ */
+internal sealed class MainGesture {
+ object Back : MainGesture()
+ object Logout : MainGesture()
+ data class Auth(val child: AuthGesture) : MainGesture()
+}
diff --git a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/data/MainUiState.kt b/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/data/MainUiState.kt
new file mode 100644
index 0000000..763cf86
--- /dev/null
+++ b/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/data/MainUiState.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.app.data
+
+import com.motorro.statemachine.di.api.AuthUiState
+import com.motorro.statemachine.di.api.data.Session
+
+internal sealed class MainUiState {
+ data object Loading : MainUiState()
+ data class Auth(val child: AuthUiState) : MainUiState()
+ data class Content(val session: Session.Active) : MainUiState()
+ data object Terminated : MainUiState()
+}
\ No newline at end of file
diff --git a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/di/ApplicationModule.kt b/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/di/ApplicationModule.kt
new file mode 100644
index 0000000..91baaf3
--- /dev/null
+++ b/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/di/ApplicationModule.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.app.di
+
+import com.motorro.statemachine.di.api.SessionManager
+import com.motorro.statemachine.di.app.session.SessionManagerImpl
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+internal abstract class ApplicationModule {
+ @Binds
+ @Singleton
+ abstract fun sessionManager(impl: SessionManagerImpl): SessionManager
+}
\ No newline at end of file
diff --git a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/di/MainModule.kt b/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/di/MainModule.kt
new file mode 100644
index 0000000..09c2e3b
--- /dev/null
+++ b/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/di/MainModule.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.app.di
+
+import com.motorro.statemachine.di.app.state.MainFactory
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+
+@Module
+@InstallIn(ViewModelComponent::class)
+internal abstract class MainModule {
+ @Binds
+ abstract fun mainFactory(impl: MainFactory.Impl): MainFactory
+}
\ No newline at end of file
diff --git a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/session/SessionManagerImpl.kt b/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/session/SessionManagerImpl.kt
new file mode 100644
index 0000000..114a267
--- /dev/null
+++ b/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/session/SessionManagerImpl.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.app.session
+
+import com.motorro.statemachine.di.api.SessionManager
+import com.motorro.statemachine.di.api.data.Session
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import javax.inject.Inject
+
+internal class SessionManagerImpl @Inject constructor() : SessionManager {
+ private val _session = MutableStateFlow(Session.None)
+ override val session: StateFlow get() = _session.asStateFlow()
+
+ override suspend fun update(session: Session) {
+ _session.emit(session)
+ }
+}
\ No newline at end of file
diff --git a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/state/AuthProxy.kt b/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/state/AuthProxy.kt
new file mode 100644
index 0000000..ebb65ad
--- /dev/null
+++ b/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/state/AuthProxy.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.app.state
+
+import com.motorro.commonstatemachine.ProxyMachineState
+import com.motorro.statemachine.commoncore.log.Logger
+import com.motorro.statemachine.di.api.AuthDataApi
+import com.motorro.statemachine.di.api.AuthFlowHost
+import com.motorro.statemachine.di.api.AuthGesture
+import com.motorro.statemachine.di.api.AuthUiState
+import com.motorro.statemachine.di.app.data.MainGesture
+import com.motorro.statemachine.di.app.data.MainUiState
+import javax.inject.Inject
+
+internal class AuthProxy(
+ private val context: MainContext,
+ private val api: AuthDataApi
+) : ProxyMachineState(api.getDefaultUiState()) {
+
+ private val flowHost = AuthFlowHost { result ->
+ when(result) {
+ null -> {
+ Logger.d("Auth cancelled. Terminating...")
+ setMachineState(context.factory.terminated())
+ }
+
+ else -> {
+ Logger.d("Auth completed. Logging in...")
+ setMachineState(context.factory.loggingIn(result))
+ }
+ }
+ }
+
+ override fun init() = api.init(flowHost, Unit)
+
+ override fun mapGesture(parent: MainGesture): AuthGesture? = when (parent) {
+ is MainGesture.Auth -> parent.child
+ MainGesture.Back -> api.getBackGesture()
+ else -> null
+ }
+
+ override fun mapUiState(child: AuthUiState): MainUiState = MainUiState.Auth(child)
+
+ class Factory @Inject constructor(private val api: AuthDataApi) {
+ fun invoke(context: MainContext) = AuthProxy(
+ context,
+ api
+ )
+ }
+}
\ No newline at end of file
diff --git a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/state/BaseMainState.kt b/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/state/BaseMainState.kt
new file mode 100644
index 0000000..b8e9329
--- /dev/null
+++ b/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/state/BaseMainState.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.app.state
+
+import com.motorro.commonstatemachine.coroutines.CoroutineState
+import com.motorro.statemachine.commoncore.log.Logger
+import com.motorro.statemachine.di.app.data.MainGesture
+import com.motorro.statemachine.di.app.data.MainUiState
+
+internal abstract class BaseMainState(context: MainContext) : CoroutineState(), MainContext by context {
+ override fun doProcess(gesture: MainGesture) {
+ Logger.w("Unhandled gesture: $gesture")
+ }
+}
\ No newline at end of file
diff --git a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/state/Content.kt b/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/state/Content.kt
new file mode 100644
index 0000000..8a090bc
--- /dev/null
+++ b/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/state/Content.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.app.state
+
+import com.motorro.statemachine.commoncore.log.Logger
+import com.motorro.statemachine.di.api.SessionManager
+import com.motorro.statemachine.di.api.data.Session
+import com.motorro.statemachine.di.app.data.MainGesture
+import com.motorro.statemachine.di.app.data.MainUiState
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import javax.inject.Inject
+
+internal class Content(context: MainContext, private val sessionManager: SessionManager) : BaseMainState(context) {
+ override fun doStart() {
+ subscribeSession()
+ }
+
+ private fun subscribeSession() = sessionManager
+ .session
+ .onEach {
+ when(it) {
+ is Session.None -> {
+ Logger.d("No session. Switching to Auth...")
+ setMachineState(factory.auth())
+ }
+ is Session.Active -> render(it)
+ }
+ }
+ .launchIn(stateScope)
+
+ override fun doProcess(gesture: MainGesture) {
+ when (gesture) {
+ MainGesture.Back -> {
+ Logger.d("Back. Terminating...")
+ setMachineState(factory.terminated())
+ }
+ MainGesture.Logout -> {
+ Logger.d("Logging out...")
+ setMachineState(factory.loggingOut())
+ }
+ else -> super.doProcess(gesture)
+ }
+ }
+
+ private fun render(session: Session.Active) {
+ setUiState(MainUiState.Content(session))
+ }
+
+ class Factory @Inject constructor(private val sessionManager: SessionManager) {
+ fun invoke(context: MainContext) = Content(
+ context,
+ sessionManager
+ )
+ }
+}
\ No newline at end of file
diff --git a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/state/MainContext.kt b/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/state/MainContext.kt
new file mode 100644
index 0000000..34d5742
--- /dev/null
+++ b/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/state/MainContext.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.app.state
+
+/**
+ * Main flow context
+ */
+internal interface MainContext {
+ val factory: MainFactory
+}
\ No newline at end of file
diff --git a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/state/MainFactory.kt b/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/state/MainFactory.kt
new file mode 100644
index 0000000..a752123
--- /dev/null
+++ b/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/state/MainFactory.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.app.state
+
+import com.motorro.commonstatemachine.CommonMachineState
+import com.motorro.statemachine.di.api.data.Session
+import com.motorro.statemachine.di.app.data.MainGesture
+import com.motorro.statemachine.di.app.data.MainUiState
+import javax.inject.Inject
+import javax.inject.Provider
+
+/**
+ * Main state factory
+ */
+internal interface MainFactory {
+ fun auth(): CommonMachineState
+ fun loggingIn(session: Session.Active): CommonMachineState
+ fun loggingOut(): CommonMachineState
+ fun content(): CommonMachineState
+ fun terminated(): CommonMachineState
+
+ class Impl @Inject constructor(
+ private val createContent: Provider,
+ private val createAuth: Provider,
+ private val createUpdateSession: Provider,
+ ) : MainFactory {
+
+ private val context = object : MainContext {
+ override val factory: MainFactory get() = this@Impl
+ }
+
+ override fun auth() = createAuth.get().invoke(
+ context
+ )
+
+ override fun loggingIn(session: Session.Active) = createUpdateSession.get().invoke(
+ context,
+ session
+ )
+
+ override fun loggingOut() = createUpdateSession.get().invoke(
+ context,
+ Session.None
+ )
+
+ override fun content() = createContent.get().invoke(
+ context
+ )
+
+ override fun terminated() = object : CommonMachineState() {
+ override fun doStart() {
+ setUiState(MainUiState.Terminated)
+ }
+ }
+ }
+}
diff --git a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/state/UpdatingSession.kt b/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/state/UpdatingSession.kt
new file mode 100644
index 0000000..bce2095
--- /dev/null
+++ b/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/state/UpdatingSession.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.app.state
+
+import com.motorro.statemachine.commoncore.log.Logger
+import com.motorro.statemachine.di.api.SessionManager
+import com.motorro.statemachine.di.api.data.Session
+import com.motorro.statemachine.di.app.data.MainUiState
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+internal class UpdatingSession(
+ context: MainContext,
+ private val session: Session,
+ private val sessionManager: SessionManager
+) : BaseMainState(context) {
+
+ override fun doStart() {
+ setUiState(MainUiState.Loading)
+ login()
+ }
+
+ private fun login() = stateScope.launch {
+ Logger.d("Updating session...")
+ sessionManager.update(session)
+ setMachineState(factory.content())
+ }
+
+ class Factory @Inject constructor(private val sessionManager: SessionManager) {
+ fun invoke(context: MainContext, session: Session) = UpdatingSession(
+ context,
+ session,
+ sessionManager
+ )
+ }
+}
\ No newline at end of file
diff --git a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/ui/MainScreen.kt b/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/ui/MainScreen.kt
new file mode 100644
index 0000000..6cf88a7
--- /dev/null
+++ b/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/ui/MainScreen.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.app.ui
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import com.motorro.statemachine.androidcore.compose.Loading
+import com.motorro.statemachine.di.api.LocalAuth
+import com.motorro.statemachine.di.app.R
+import com.motorro.statemachine.di.app.data.MainGesture
+import com.motorro.statemachine.di.app.data.MainUiState
+
+@Composable
+@OptIn(ExperimentalMaterial3Api::class)
+internal fun MainScreen(state: MainUiState, onGesture: (MainGesture) -> Unit, onTerminated: () -> Unit) {
+ val onBack: () -> Unit = remember {
+ { onGesture(MainGesture.Back) }
+ }
+
+ BackHandler(onBack = onBack)
+
+ Scaffold(
+ modifier = Modifier.fillMaxSize(),
+ topBar = {
+ TopAppBar(
+ title = { Text(stringResource(R.string.app_name)) },
+ navigationIcon = {
+ IconButton(onClick = onBack) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = "Back",
+ tint = Color.Black
+ )
+ }
+ }
+ )
+ },
+ content = { paddingValues ->
+ val padding = remember {
+ Modifier.padding(paddingValues)
+ }
+
+ when(state) {
+ // Take injected screen implementation
+ is MainUiState.Auth -> LocalAuth.current.Screen(
+ state = state.child,
+ onGesture = { onGesture(MainGesture.Auth(it)) },
+ modifier = padding
+ )
+ is MainUiState.Content -> MainScreenView(
+ state = state,
+ onGesture = onGesture,
+ modifier = padding
+ )
+ MainUiState.Loading -> Loading(
+ modifier = padding
+ )
+ MainUiState.Terminated -> LaunchedEffect(Unit) {
+ onTerminated()
+ }
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/ui/MainScreenView.kt b/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/ui/MainScreenView.kt
new file mode 100644
index 0000000..5dc5ae5
--- /dev/null
+++ b/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/ui/MainScreenView.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.app.ui
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Button
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.motorro.statemachine.di.api.data.Session
+import com.motorro.statemachine.di.app.R
+import com.motorro.statemachine.di.app.data.MainGesture
+import com.motorro.statemachine.di.app.data.MainUiState
+
+@Composable
+internal fun MainScreenView(state: MainUiState.Content, onGesture: (MainGesture) -> Unit, modifier: Modifier = Modifier) {
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = stringResource(R.string.title_content),
+ style = MaterialTheme.typography.headlineMedium
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+ Text(
+ text = stringResource(R.string.access_token),
+ style = MaterialTheme.typography.labelLarge
+ )
+ Text(
+ text = state.session.accessToken,
+ style = MaterialTheme.typography.bodyMedium,
+ textAlign = TextAlign.Center
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ text = stringResource(R.string.refresh_token),
+ style = MaterialTheme.typography.labelLarge
+ )
+ Text(
+ text = state.session.refreshToken,
+ style = MaterialTheme.typography.bodyMedium,
+ textAlign = TextAlign.Center
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+ Button(
+ onClick = { onGesture(MainGesture.Logout) },
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text(text = stringResource(R.string.logout))
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun MainScreenViewPreview() {
+ MainScreenView(
+ state = MainUiState.Content(
+ Session.Active(
+ "access.token.123",
+ "refresh.token.456"
+ )
+ ),
+ onGesture = {}
+ )
+}
diff --git a/examples/di/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/examples/di/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..40ce517
--- /dev/null
+++ b/examples/di/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/di/app/src/main/res/drawable/ic_launcher_background.xml b/examples/di/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..997ef0b
--- /dev/null
+++ b/examples/di/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,183 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/di/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/examples/di/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..0147ca8
--- /dev/null
+++ b/examples/di/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/di/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/examples/di/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..0147ca8
--- /dev/null
+++ b/examples/di/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/di/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/examples/di/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/examples/di/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/examples/di/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/examples/di/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/examples/di/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/examples/di/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/examples/di/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/examples/di/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/examples/di/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/examples/di/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/examples/di/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/examples/di/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/examples/di/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/examples/di/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/examples/di/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/examples/di/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/examples/di/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/examples/di/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/examples/di/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/examples/di/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/examples/di/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/examples/di/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/examples/di/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/examples/di/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/examples/di/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/examples/di/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/examples/di/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/examples/di/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/examples/di/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/examples/di/app/src/main/res/values/colors.xml b/examples/di/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..164d1ef
--- /dev/null
+++ b/examples/di/app/src/main/res/values/colors.xml
@@ -0,0 +1,23 @@
+
+
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/examples/di/app/src/main/res/values/strings.xml b/examples/di/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..6876969
--- /dev/null
+++ b/examples/di/app/src/main/res/values/strings.xml
@@ -0,0 +1,20 @@
+
+
+
+ DI
+ Access token
+ Refresh token
+ Logout
+ Content
+
\ No newline at end of file
diff --git a/examples/di/app/src/main/res/values/themes.xml b/examples/di/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..01f1f57
--- /dev/null
+++ b/examples/di/app/src/main/res/values/themes.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/di/app/src/main/res/xml/backup_rules.xml b/examples/di/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..18b86f4
--- /dev/null
+++ b/examples/di/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/di/app/src/main/res/xml/data_extraction_rules.xml b/examples/di/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..2d914d6
--- /dev/null
+++ b/examples/di/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/di/app/src/test/kotlin/com/motorro/statemachine/di/app/mock.kt b/examples/di/app/src/test/kotlin/com/motorro/statemachine/di/app/mock.kt
new file mode 100644
index 0000000..8c145d0
--- /dev/null
+++ b/examples/di/app/src/test/kotlin/com/motorro/statemachine/di/app/mock.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.app
+
+import com.motorro.statemachine.di.api.data.Session
+
+internal val ACTIVE_SESSION = Session.Active(
+ accessToken = "access",
+ refreshToken = "refresh"
+)
\ No newline at end of file
diff --git a/examples/di/app/src/test/kotlin/com/motorro/statemachine/di/app/state/BaseStateTest.kt b/examples/di/app/src/test/kotlin/com/motorro/statemachine/di/app/state/BaseStateTest.kt
new file mode 100644
index 0000000..d8804ad
--- /dev/null
+++ b/examples/di/app/src/test/kotlin/com/motorro/statemachine/di/app/state/BaseStateTest.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.app.state
+
+import com.motorro.commonstatemachine.CommonStateMachine
+import com.motorro.statemachine.di.api.AuthFlowHost
+import com.motorro.statemachine.di.app.data.MainGesture
+import com.motorro.statemachine.di.app.data.MainUiState
+import io.mockk.Runs
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Before
+
+@OptIn(ExperimentalCoroutinesApi::class)
+internal abstract class BaseStateTest {
+ protected lateinit var stateMachine: CommonStateMachine
+ protected lateinit var factory: MainFactory
+ protected lateinit var context: MainContext
+ protected lateinit var nextState: BaseMainState
+ protected lateinit var flowHost: AuthFlowHost
+ protected lateinit var dispatcher: TestDispatcher
+
+ @Before
+ fun init() {
+ dispatcher = UnconfinedTestDispatcher()
+ Dispatchers.setMain(dispatcher)
+ stateMachine = mockk(relaxed = true)
+ factory = mockk()
+ flowHost = mockk {
+ every { this@mockk.onComplete(anyNullable()) } just Runs
+ }
+ context = object : MainContext {
+ override val factory: MainFactory = this@BaseStateTest.factory
+ }
+ nextState = mockk(relaxed = true)
+ doInit()
+ }
+
+ @After
+ fun deinit() {
+ Dispatchers.resetMain()
+ }
+
+ protected fun test(block: suspend TestScope.() -> Unit) = runTest(
+ dispatcher,
+ testBody = block
+ )
+
+ protected open fun doInit() = Unit
+}
\ No newline at end of file
diff --git a/examples/di/app/src/test/kotlin/com/motorro/statemachine/di/app/state/ContentTest.kt b/examples/di/app/src/test/kotlin/com/motorro/statemachine/di/app/state/ContentTest.kt
new file mode 100644
index 0000000..7b4a617
--- /dev/null
+++ b/examples/di/app/src/test/kotlin/com/motorro/statemachine/di/app/state/ContentTest.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.app.state
+
+import com.motorro.statemachine.di.api.SessionManager
+import com.motorro.statemachine.di.api.data.Session
+import com.motorro.statemachine.di.app.ACTIVE_SESSION
+import com.motorro.statemachine.di.app.data.MainGesture
+import com.motorro.statemachine.di.app.data.MainUiState
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.runs
+import kotlinx.coroutines.flow.MutableStateFlow
+import org.junit.Test
+
+internal class ContentTest : BaseStateTest() {
+ private lateinit var session: MutableStateFlow
+ private lateinit var sessionManager: SessionManager
+
+ private lateinit var state: BaseMainState
+
+ private fun createState(active: Boolean) {
+ session = MutableStateFlow(if (active) ACTIVE_SESSION else Session.None)
+ sessionManager = mockk {
+ every { this@mockk.session } returns this@ContentTest.session
+ coEvery { this@mockk.update(any()) } just runs
+ }
+ state = Content(context, sessionManager)
+ }
+
+ @Test
+ fun displaysActiveSession() = test {
+ createState(active = true)
+
+ state.start(stateMachine)
+
+ coVerify {
+ stateMachine.setUiState(MainUiState.Content(ACTIVE_SESSION))
+ }
+ }
+
+ @Test
+ fun switchesToAuthWhenNotAuthorized() = test {
+ createState(active = false)
+ every { factory.auth() } returns nextState
+
+ state.start(stateMachine)
+ session.emit(Session.None)
+
+ coVerify {
+ factory.auth()
+ stateMachine.setMachineState(nextState)
+ }
+ }
+
+ @Test
+ fun switchesToAuthWhenLoggedOut() = test {
+ createState(active = true)
+ every { factory.auth() } returns nextState
+
+ state.start(stateMachine)
+ session.emit(Session.None)
+
+ coVerify {
+ factory.auth()
+ stateMachine.setMachineState(nextState)
+ }
+ }
+
+ @Test
+ fun terminatesOnBack() = test {
+ createState(active = true)
+ every { factory.terminated() } returns nextState
+
+ state.start(stateMachine)
+ state.process(MainGesture.Back)
+
+ coVerify {
+ factory.terminated()
+ stateMachine.setMachineState(nextState)
+ }
+ }
+
+ @Test
+ fun transfersToLogout() = test {
+ createState(active = true)
+ every { factory.loggingOut() } returns nextState
+
+ state.start(stateMachine)
+ state.process(MainGesture.Logout)
+
+ coVerify {
+ factory.loggingOut()
+ stateMachine.setMachineState(nextState)
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/di/login/.gitignore b/examples/di/login/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/examples/di/login/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/examples/di/login/build.gradle.kts b/examples/di/login/build.gradle.kts
new file mode 100644
index 0000000..8ef3dac
--- /dev/null
+++ b/examples/di/login/build.gradle.kts
@@ -0,0 +1,85 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ alias(libs.plugins.android.lib)
+ alias(libs.plugins.compose)
+ alias(libs.plugins.google.ksp)
+ alias(libs.plugins.hilt)
+}
+
+val androidMinSdkVersion: Int by project.extra
+val androidCompileSdkVersion: Int by project.extra
+
+android {
+ compileSdk = androidCompileSdkVersion
+
+ defaultConfig {
+ minSdk = androidMinSdkVersion
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ isCoreLibraryDesugaringEnabled = true
+ }
+ kotlin {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_17)
+ }
+ }
+ buildFeatures {
+ compose = true
+ }
+ namespace = "com.motorro.statemachine.di.login"
+}
+
+dependencies {
+ implementation(project(":examples:commoncore"))
+ implementation(project(":examples:androidcore"))
+ implementation(project(":examples:di:api"))
+ implementation(project(":coroutines"))
+
+ coreLibraryDesugaring(libs.desugaring)
+
+ implementation(libs.kotlin.coroutines.core)
+
+ implementation(platform(libs.compose.bom))
+ implementation(libs.bundles.compose.core)
+ implementation(libs.compose.foundation)
+ implementation(libs.compose.foundation.layouts)
+
+ implementation(libs.hilt.android)
+ implementation(libs.hilt.compose)
+ ksp(libs.hilt.compiler)
+ ksp(libs.hilt.compiler.androidx)
+
+ debugImplementation(libs.compose.tooling)
+
+ testImplementation(libs.bundles.test.core)
+ testImplementation(libs.test.kotlin.coroutines)
+}
diff --git a/examples/di/login/consumer-rules.pro b/examples/di/login/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/examples/di/login/proguard-rules.pro b/examples/di/login/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/examples/di/login/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/LoginConstants.kt b/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/LoginConstants.kt
new file mode 100644
index 0000000..3fbdfe0
--- /dev/null
+++ b/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/LoginConstants.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.login
+
+/**
+ * Mock data
+ */
+internal object LoginConstants {
+ const val USERNAME = "user"
+ const val PASSWORD = "password"
+
+ const val ACCESS_TOKEN = "login_session_token"
+ const val REFRESH_TOKEN = "login_refresh_token"
+}
\ No newline at end of file
diff --git a/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/LoginDataApi.kt b/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/LoginDataApi.kt
new file mode 100644
index 0000000..dc7809f
--- /dev/null
+++ b/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/LoginDataApi.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.login
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.motorro.statemachine.di.api.AuthDataApi
+import com.motorro.statemachine.di.api.AuthFlowHost
+import com.motorro.statemachine.di.api.AuthGesture
+import com.motorro.statemachine.di.api.AuthUiApi
+import com.motorro.statemachine.di.api.AuthUiState
+import com.motorro.statemachine.di.login.data.LoginDataState
+import com.motorro.statemachine.di.login.data.LoginGesture
+import com.motorro.statemachine.di.login.data.LoginUiState
+import com.motorro.statemachine.di.login.state.LoginFactory
+import com.motorro.statemachine.di.login.ui.LoginScreen
+import jakarta.inject.Inject
+
+/**
+ * Auth data API implementation for login flow
+ */
+internal class LoginDataApi @Inject constructor(private val createFactory: LoginFactory.Factory) : AuthDataApi {
+ /**
+ * Initializes flow
+ */
+ override fun init(flowHost: AuthFlowHost, input: Unit) = createFactory(flowHost).form(LoginDataState())
+
+ /**
+ * Returns default UI state
+ */
+ override fun getDefaultUiState() = LoginUiState.Loading
+
+ /**
+ * Returns back gesture for this flow
+ */
+ override fun getBackGesture(): AuthGesture = LoginGesture.Back
+}
+
+/**
+ * Auth UI API implementation for login flow
+ */
+internal class LoginUiApi @Inject constructor() : AuthUiApi {
+ @Composable
+ override fun Screen(
+ state: AuthUiState,
+ onGesture: (AuthGesture) -> Unit,
+ modifier: Modifier
+ ) = LoginScreen(
+ state = state as LoginUiState,
+ onGesture = onGesture,
+ modifier = modifier
+ )
+}
\ No newline at end of file
diff --git a/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/data/LoginDataState.kt b/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/data/LoginDataState.kt
new file mode 100644
index 0000000..1943f8e
--- /dev/null
+++ b/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/data/LoginDataState.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.login.data
+
+/**
+ * Data state
+ */
+data class LoginDataState(
+ val username: String = "",
+ val password: String = ""
+)
\ No newline at end of file
diff --git a/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/data/LoginGesture.kt b/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/data/LoginGesture.kt
new file mode 100644
index 0000000..24d3a2a
--- /dev/null
+++ b/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/data/LoginGesture.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.login.data
+
+import com.motorro.statemachine.di.api.AuthGesture
+
+internal sealed class LoginGesture : AuthGesture {
+ data object Back : LoginGesture()
+ data object Action : LoginGesture()
+ data class UsernameChanged(val username: String) : LoginGesture()
+ data class PasswordChanged(val password: String) : LoginGesture()
+}
diff --git a/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/data/LoginUiState.kt b/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/data/LoginUiState.kt
new file mode 100644
index 0000000..7857d44
--- /dev/null
+++ b/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/data/LoginUiState.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.login.data
+
+import com.motorro.statemachine.di.api.AuthUiState
+
+internal sealed class LoginUiState: AuthUiState {
+ data object Loading : LoginUiState()
+ data class Form(val username: String, val password: String, val loginEnabled: Boolean) : LoginUiState()
+ data class Error(val error: Throwable) : LoginUiState()
+}
\ No newline at end of file
diff --git a/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/di/LoginFlowModule.kt b/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/di/LoginFlowModule.kt
new file mode 100644
index 0000000..18395f2
--- /dev/null
+++ b/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/di/LoginFlowModule.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.login.di
+
+import com.motorro.statemachine.di.api.AuthDataApi
+import com.motorro.statemachine.di.api.AuthUiApi
+import com.motorro.statemachine.di.login.LoginDataApi
+import com.motorro.statemachine.di.login.LoginUiApi
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ActivityComponent
+import dagger.hilt.android.components.ViewModelComponent
+
+@Module
+@InstallIn(ViewModelComponent::class)
+internal abstract class LoginFlowDataModule {
+ @Binds
+ abstract fun bindLoginDataApi(impl: LoginDataApi): AuthDataApi
+}
+
+@Module
+@InstallIn(ActivityComponent::class)
+internal abstract class LoginFlowUiModule {
+ @Binds
+ abstract fun bindLoginUiApi(impl: LoginUiApi): AuthUiApi
+}
\ No newline at end of file
diff --git a/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/state/BaseLoginState.kt b/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/state/BaseLoginState.kt
new file mode 100644
index 0000000..9736270
--- /dev/null
+++ b/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/state/BaseLoginState.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.login.state
+
+import com.motorro.commonstatemachine.coroutines.CoroutineState
+import com.motorro.statemachine.commoncore.log.Logger
+import com.motorro.statemachine.di.api.AuthGesture
+import com.motorro.statemachine.di.api.AuthUiState
+import com.motorro.statemachine.di.login.data.LoginGesture
+
+internal abstract class BaseLoginState(context: LoginContext) : CoroutineState(), LoginContext by context {
+
+ final override fun doProcess(gesture: AuthGesture) {
+ when (gesture) {
+ is LoginGesture -> doProcess(gesture)
+ else -> Logger.w("Not a `LoginGesture`: $gesture")
+ }
+ }
+
+ protected open fun doProcess(gesture: LoginGesture) {
+ Logger.w("Gesture not handled: $gesture")
+ }
+}
diff --git a/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/state/Error.kt b/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/state/Error.kt
new file mode 100644
index 0000000..f5ae0a2
--- /dev/null
+++ b/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/state/Error.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.login.state
+
+import com.motorro.statemachine.commoncore.log.Logger
+import com.motorro.statemachine.di.login.data.LoginDataState
+import com.motorro.statemachine.di.login.data.LoginGesture
+import com.motorro.statemachine.di.login.data.LoginUiState
+
+/**
+ * Displays error
+ */
+internal class Error(
+ context: LoginContext,
+ private val data: LoginDataState,
+ private val error: Throwable
+) : BaseLoginState(context) {
+ override fun doStart() {
+ setUiState(LoginUiState.Error(error))
+ }
+
+ override fun doProcess(gesture: LoginGesture) {
+ when(gesture) {
+ LoginGesture.Back, LoginGesture.Action -> {
+ Logger.d("Dismissed. Back to form...")
+ setMachineState(factory.form(data))
+ }
+ else -> super.doProcess(gesture)
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/state/FormState.kt b/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/state/FormState.kt
new file mode 100644
index 0000000..9ca534e
--- /dev/null
+++ b/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/state/FormState.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.login.state
+
+import com.motorro.statemachine.commoncore.log.Logger
+import com.motorro.statemachine.di.login.data.LoginDataState
+import com.motorro.statemachine.di.login.data.LoginGesture
+import com.motorro.statemachine.di.login.data.LoginUiState
+import kotlin.properties.Delegates
+
+internal class FormState(context: LoginContext, data: LoginDataState) : BaseLoginState(context) {
+
+ private var data: LoginDataState by Delegates.observable(data) { _, _, newValue ->
+ render(newValue)
+ }
+
+ override fun doStart() {
+ render(data)
+ }
+
+ override fun doProcess(gesture: LoginGesture) {
+ when (gesture) {
+ LoginGesture.Action -> if (data.isLoginEnabled()) {
+ Logger.d("Logging in...")
+ setMachineState(factory.loggingIn(data))
+ }
+ LoginGesture.Back -> {
+ Logger.d("Back pressed. Terminating...")
+ setMachineState(factory.terminated())
+ }
+ is LoginGesture.PasswordChanged -> {
+ data = data.copy(password = gesture.password)
+ }
+ is LoginGesture.UsernameChanged -> {
+ data = data.copy(username = gesture.username)
+ }
+ }
+ }
+
+ private fun LoginDataState.isLoginEnabled(): Boolean = username.isNotBlank() && password.isNotBlank()
+
+ private fun render(data: LoginDataState) {
+ setUiState(LoginUiState.Form(
+ username = data.username,
+ password = data.password,
+ loginEnabled = data.isLoginEnabled()
+ ))
+ }
+}
\ No newline at end of file
diff --git a/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/state/LoggingIn.kt b/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/state/LoggingIn.kt
new file mode 100644
index 0000000..5025f2c
--- /dev/null
+++ b/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/state/LoggingIn.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.login.state
+
+import com.motorro.statemachine.commoncore.log.Logger
+import com.motorro.statemachine.di.api.data.Session
+import com.motorro.statemachine.di.login.LoginConstants
+import com.motorro.statemachine.di.login.data.LoginDataState
+import com.motorro.statemachine.di.login.data.LoginGesture
+import com.motorro.statemachine.di.login.data.LoginUiState
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlin.time.Duration.Companion.seconds
+
+/**
+ * Emulates login
+ */
+internal class LoggingIn(context: LoginContext, private val data: LoginDataState) : BaseLoginState(context) {
+ override fun doStart() {
+ setUiState(LoginUiState.Loading)
+ login()
+ }
+
+ private fun login() = stateScope.launch {
+ Logger.d("Logging in...")
+ delay(2.seconds)
+ if (data.username == LoginConstants.USERNAME && data.password == LoginConstants.PASSWORD) {
+ Logger.d("Login successful")
+ setMachineState(factory.complete(Session.Active(
+ accessToken = LoginConstants.ACCESS_TOKEN,
+ refreshToken = LoginConstants.REFRESH_TOKEN
+ )))
+ } else {
+ Logger.d("Login failed")
+ setMachineState(factory.error(
+ data,
+ IllegalArgumentException("Invalid username or password")
+ ))
+ }
+ }
+
+ override fun doProcess(gesture: LoginGesture) {
+ when(gesture) {
+ LoginGesture.Back -> {
+ Logger.d("Back pressed. Back to form...")
+ setMachineState(factory.form(data))
+ }
+ else -> super.doProcess(gesture)
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/state/LoginContext.kt b/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/state/LoginContext.kt
new file mode 100644
index 0000000..253887e
--- /dev/null
+++ b/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/state/LoginContext.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.login.state
+
+import com.motorro.statemachine.di.api.AuthFlowHost
+
+/**
+ * Common inter-state context
+ */
+internal interface LoginContext {
+ val factory: LoginFactory
+ val flowHost: AuthFlowHost
+}
\ No newline at end of file
diff --git a/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/state/LoginFactory.kt b/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/state/LoginFactory.kt
new file mode 100644
index 0000000..46bc515
--- /dev/null
+++ b/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/state/LoginFactory.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.login.state
+
+import com.motorro.commonstatemachine.CommonMachineState
+import com.motorro.statemachine.di.api.AuthFlowHost
+import com.motorro.statemachine.di.api.AuthGesture
+import com.motorro.statemachine.di.api.AuthUiState
+import com.motorro.statemachine.di.api.data.Session
+import com.motorro.statemachine.di.login.data.LoginDataState
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+/**
+ * Sub-flow state factory
+ */
+internal interface LoginFactory {
+ fun form(data: LoginDataState): CommonMachineState
+ fun loggingIn(data: LoginDataState): CommonMachineState
+ fun complete(session: Session.Active): CommonMachineState
+ fun error(data: LoginDataState, error: Throwable): CommonMachineState
+ fun terminated(): CommonMachineState
+
+ @AssistedFactory
+ interface Factory {
+ operator fun invoke(flowHost: AuthFlowHost): Impl
+ }
+
+ class Impl @AssistedInject constructor(@Assisted flowHost: AuthFlowHost) : LoginFactory {
+
+ private val context = object : LoginContext {
+ override val factory: LoginFactory = this@Impl
+ override val flowHost: AuthFlowHost = flowHost
+ }
+
+ override fun form(data: LoginDataState) = FormState(
+ context,
+ data
+ )
+
+ override fun loggingIn(data: LoginDataState) = LoggingIn(
+ context,
+ data
+ )
+
+ override fun complete(session: Session.Active) = object : CommonMachineState() {
+ override fun doStart() {
+ context.flowHost.onComplete(session)
+ }
+ }
+
+ override fun error(data: LoginDataState, error: Throwable) = Error(
+ context,
+ data,
+ error
+ )
+
+ override fun terminated() = object : CommonMachineState() {
+ override fun doStart() {
+ context.flowHost.onComplete(null)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/ui/LoginFormView.kt b/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/ui/LoginFormView.kt
new file mode 100644
index 0000000..37c9634
--- /dev/null
+++ b/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/ui/LoginFormView.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.login.ui
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Button
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.motorro.statemachine.androidcore.ui.theme.CommonStateMachineTheme
+import com.motorro.statemachine.di.login.data.LoginGesture
+import com.motorro.statemachine.di.login.data.LoginUiState
+
+@Composable
+internal fun LoginFormView(state: LoginUiState.Form, onGesture: (LoginGesture) -> Unit, modifier: Modifier = Modifier) {
+ Column(
+ modifier = modifier.fillMaxSize().padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ OutlinedTextField(
+ value = state.username,
+ onValueChange = { onGesture(LoginGesture.UsernameChanged(it)) },
+ modifier = Modifier.fillMaxWidth(),
+ label = { Text(text = "Username") }
+ )
+ OutlinedTextField(
+ value = state.password,
+ onValueChange = { onGesture(LoginGesture.PasswordChanged(it)) },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 8.dp),
+ label = { Text(text = "Password") },
+ visualTransformation = PasswordVisualTransformation()
+ )
+ Button(
+ onClick = { onGesture(LoginGesture.Action) },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 16.dp),
+ enabled = state.loginEnabled
+ ) {
+ Text(text = "Login")
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun LoginFormViewPreview() {
+ CommonStateMachineTheme {
+ LoginFormView(
+ state = LoginUiState.Form("user", "password", true),
+ onGesture = {}
+ )
+ }
+}
diff --git a/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/ui/LoginScreen.kt b/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/ui/LoginScreen.kt
new file mode 100644
index 0000000..95734ac
--- /dev/null
+++ b/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/ui/LoginScreen.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.login.ui
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.motorro.statemachine.androidcore.compose.Error
+import com.motorro.statemachine.androidcore.compose.Loading
+import com.motorro.statemachine.di.login.data.LoginGesture
+import com.motorro.statemachine.di.login.data.LoginUiState
+
+@Composable
+internal fun LoginScreen(state: LoginUiState, onGesture: (LoginGesture) -> Unit, modifier: Modifier = Modifier) {
+ when(state) {
+ is LoginUiState.Form -> LoginFormView(state, onGesture, modifier)
+ LoginUiState.Loading -> Loading(modifier)
+ is LoginUiState.Error -> Error(
+ error = state.error,
+ onDismiss = { onGesture(LoginGesture.Action) },
+ modifier = modifier
+ )
+ }
+}
\ No newline at end of file
diff --git a/examples/di/login/src/test/kotlin/com/motorro/statemachine/di/login/state/BaseStateTest.kt b/examples/di/login/src/test/kotlin/com/motorro/statemachine/di/login/state/BaseStateTest.kt
new file mode 100644
index 0000000..a5cc5e7
--- /dev/null
+++ b/examples/di/login/src/test/kotlin/com/motorro/statemachine/di/login/state/BaseStateTest.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.login.state
+
+import com.motorro.commonstatemachine.CommonStateMachine
+import com.motorro.statemachine.di.api.AuthFlowHost
+import com.motorro.statemachine.di.api.AuthGesture
+import com.motorro.statemachine.di.api.AuthUiState
+import io.mockk.Runs
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Before
+
+@OptIn(ExperimentalCoroutinesApi::class)
+internal abstract class BaseStateTest {
+ protected lateinit var stateMachine: CommonStateMachine
+ protected lateinit var factory: LoginFactory
+ protected lateinit var context: LoginContext
+ protected lateinit var nextState: BaseLoginState
+ protected lateinit var flowHost: AuthFlowHost
+ protected lateinit var dispatcher: TestDispatcher
+
+ @Before
+ fun init() {
+ dispatcher = UnconfinedTestDispatcher()
+ Dispatchers.setMain(dispatcher)
+ stateMachine = mockk(relaxed = true)
+ factory = mockk()
+ flowHost = mockk {
+ every { this@mockk.onComplete(anyNullable()) } just Runs
+ }
+ context = object : LoginContext {
+ override val factory: LoginFactory = this@BaseStateTest.factory
+ override val flowHost: AuthFlowHost = this@BaseStateTest.flowHost
+ }
+ nextState = mockk(relaxed = true)
+ doInit()
+ }
+
+ @After
+ fun deinit() {
+ Dispatchers.resetMain()
+ }
+
+ protected fun test(block: suspend TestScope.() -> Unit) = runTest(
+ dispatcher,
+ testBody = block
+ )
+
+ protected open fun doInit() = Unit
+}
\ No newline at end of file
diff --git a/examples/di/login/src/test/kotlin/com/motorro/statemachine/di/login/state/FormStateTest.kt b/examples/di/login/src/test/kotlin/com/motorro/statemachine/di/login/state/FormStateTest.kt
new file mode 100644
index 0000000..8f4cbc7
--- /dev/null
+++ b/examples/di/login/src/test/kotlin/com/motorro/statemachine/di/login/state/FormStateTest.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.motorro.statemachine.di.login.state
+
+import com.motorro.statemachine.di.login.data.LoginDataState
+import com.motorro.statemachine.di.login.data.LoginGesture
+import com.motorro.statemachine.di.login.data.LoginUiState
+import io.mockk.Ordering
+import io.mockk.coVerify
+import io.mockk.every
+import org.junit.Test
+import kotlin.test.assertEquals
+
+internal class FormStateTest : BaseStateTest() {
+
+ private lateinit var state: BaseLoginState
+
+ override fun doInit() {
+ state = FormState(context, LoginDataState())
+ }
+
+ @Test
+ fun rendersDataOnStart() = test {
+ state.start(stateMachine)
+
+ coVerify {
+ stateMachine.setUiState(LoginUiState.Form(username = "", password = "", loginEnabled = false))
+ }
+ }
+
+ @Test
+ fun willNotEnableLoginWhenUserEmpty() = test {
+ state.start(stateMachine)
+ state.process(LoginGesture.PasswordChanged("pass"))
+ state.process(LoginGesture.Action)
+
+ coVerify(exactly = 0) {
+ factory.loggingIn(any())
+ stateMachine.setMachineState(any())
+ }
+ }
+
+ @Test
+ fun willNotEnableLoginWhenPasswordEmpty() = test {
+ state.start(stateMachine)
+ state.process(LoginGesture.UsernameChanged("user"))
+ state.process(LoginGesture.Action)
+
+ coVerify(exactly = 0) {
+ factory.loggingIn(any())
+ stateMachine.setMachineState(any())
+ }
+ }
+
+ @Test
+ fun updatesUsername() = test {
+ state.start(stateMachine)
+ state.process(LoginGesture.UsernameChanged("user"))
+
+ coVerify(ordering = Ordering.ORDERED) {
+ stateMachine.setUiState(LoginUiState.Form(username = "", password = "", loginEnabled = false))
+ stateMachine.setUiState(LoginUiState.Form(username = "user", password = "", loginEnabled = false))
+ }
+ }
+
+ @Test
+ fun updatesPassword() = test {
+ state.start(stateMachine)
+ state.process(LoginGesture.PasswordChanged("pass"))
+
+ coVerify(ordering = Ordering.ORDERED) {
+ stateMachine.setUiState(LoginUiState.Form(username = "", password = "", loginEnabled = false))
+ stateMachine.setUiState(LoginUiState.Form(username = "", password = "pass", loginEnabled = false))
+ }
+ }
+
+ @Test
+ fun enablesLoginWhenUserAndPassSet() = test {
+ state.start(stateMachine)
+ state.process(LoginGesture.UsernameChanged("user"))
+ state.process(LoginGesture.PasswordChanged("pass"))
+
+ coVerify(ordering = Ordering.ORDERED) {
+ stateMachine.setUiState(LoginUiState.Form(username = "", password = "", loginEnabled = false))
+ stateMachine.setUiState(LoginUiState.Form(username = "user", password = "", loginEnabled = false))
+ stateMachine.setUiState(LoginUiState.Form(username = "user", password = "pass", loginEnabled = true))
+ }
+ }
+
+ @Test
+ fun startsLogin() = test {
+ every { factory.loggingIn(any()) } returns nextState
+
+ state.start(stateMachine)
+ state.process(LoginGesture.UsernameChanged("user"))
+ state.process(LoginGesture.PasswordChanged("pass"))
+ state.process(LoginGesture.Action)
+
+ coVerify {
+ factory.loggingIn(withArg {
+ assertEquals("user", it.username)
+ assertEquals("pass", it.password)
+ })
+ stateMachine.setMachineState(nextState)
+ }
+ }
+
+ @Test
+ fun terminatesOnBack() {
+ every { factory.terminated() } returns nextState
+
+ state.start(stateMachine)
+ state.process(LoginGesture.Back)
+
+ coVerify {
+ factory.terminated()
+ stateMachine.setMachineState(nextState)
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/di/social/.gitignore b/examples/di/social/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/examples/di/social/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/examples/di/social/build.gradle.kts b/examples/di/social/build.gradle.kts
new file mode 100644
index 0000000..e948f41
--- /dev/null
+++ b/examples/di/social/build.gradle.kts
@@ -0,0 +1,86 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ alias(libs.plugins.android.lib)
+ alias(libs.plugins.compose)
+ alias(libs.plugins.google.ksp)
+ alias(libs.plugins.hilt)
+}
+
+val androidMinSdkVersion: Int by project.extra
+val androidCompileSdkVersion: Int by project.extra
+
+android {
+ compileSdk = androidCompileSdkVersion
+
+ defaultConfig {
+ minSdk = androidMinSdkVersion
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ isCoreLibraryDesugaringEnabled = true
+ }
+ kotlin {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_17)
+ }
+ }
+ buildFeatures {
+ compose = true
+ }
+ namespace = "com.motorro.statemachine.di.social"
+}
+
+dependencies {
+ implementation(project(":examples:commoncore"))
+ implementation(project(":examples:androidcore"))
+ implementation(project(":examples:di:api"))
+ implementation(project(":coroutines"))
+
+ coreLibraryDesugaring(libs.desugaring)
+
+ implementation(libs.kotlin.coroutines.core)
+
+ implementation(platform(libs.compose.bom))
+ implementation(libs.bundles.compose.core)
+ implementation(libs.compose.foundation)
+ implementation(libs.compose.foundation.layouts)
+ implementation(libs.compose.material.icons)
+
+ implementation(libs.hilt.android)
+ implementation(libs.hilt.compose)
+ ksp(libs.hilt.compiler)
+ ksp(libs.hilt.compiler.androidx)
+
+ debugImplementation(libs.compose.tooling)
+
+ testImplementation(libs.bundles.test.core)
+ testImplementation(libs.test.kotlin.coroutines)
+}
diff --git a/examples/di/social/consumer-rules.pro b/examples/di/social/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/examples/di/social/proguard-rules.pro b/examples/di/social/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/examples/di/social/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/SocialConstants.kt b/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/SocialConstants.kt
new file mode 100644
index 0000000..8c4ad86
--- /dev/null
+++ b/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/SocialConstants.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package main.kotlin.com.motorro.statemachine.di.social
+
+/**
+ * Mock data
+ */
+internal object SocialConstants {
+ const val ACCESS_TOKEN = "social_session_token"
+ const val REFRESH_TOKEN = "social_refresh_token"
+}
\ No newline at end of file
diff --git a/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/SocialDataApi.kt b/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/SocialDataApi.kt
new file mode 100644
index 0000000..5f4b003
--- /dev/null
+++ b/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/SocialDataApi.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package main.kotlin.com.motorro.statemachine.di.social
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.motorro.statemachine.di.api.AuthDataApi
+import com.motorro.statemachine.di.api.AuthFlowHost
+import com.motorro.statemachine.di.api.AuthGesture
+import com.motorro.statemachine.di.api.AuthUiApi
+import com.motorro.statemachine.di.api.AuthUiState
+import jakarta.inject.Inject
+import main.kotlin.com.motorro.statemachine.di.social.data.SocialGesture
+import main.kotlin.com.motorro.statemachine.di.social.data.SocialUiState
+import main.kotlin.com.motorro.statemachine.di.social.state.SocialFactory
+import main.kotlin.com.motorro.statemachine.di.social.ui.SocialScreen
+
+/**
+ * Auth data API implementation for Social flow
+ */
+internal class SocialDataApi @Inject constructor(private val createFactory: SocialFactory.Factory) : AuthDataApi {
+ /**
+ * Initializes flow
+ */
+ override fun init(flowHost: AuthFlowHost, input: Unit) = createFactory(flowHost).form()
+
+ /**
+ * Returns default UI state
+ */
+ override fun getDefaultUiState() = SocialUiState.Loading
+
+ /**
+ * Returns back gesture for this flow
+ */
+ override fun getBackGesture(): AuthGesture = SocialGesture.Back
+}
+
+/**
+ * Auth UI API implementation for Social flow
+ */
+internal class SocialUiApi @Inject constructor() : AuthUiApi {
+ @Composable
+ override fun Screen(
+ state: AuthUiState,
+ onGesture: (AuthGesture) -> Unit,
+ modifier: Modifier
+ ) = SocialScreen(
+ state = state as SocialUiState,
+ onGesture = onGesture,
+ modifier = modifier
+ )
+}
\ No newline at end of file
diff --git a/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/data/SocialGesture.kt b/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/data/SocialGesture.kt
new file mode 100644
index 0000000..89bfcb0
--- /dev/null
+++ b/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/data/SocialGesture.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package main.kotlin.com.motorro.statemachine.di.social.data
+
+import com.motorro.statemachine.di.api.AuthGesture
+
+internal sealed class SocialGesture : AuthGesture {
+ data object Back : SocialGesture()
+ data object Action : SocialGesture()
+}
diff --git a/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/data/SocialUiState.kt b/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/data/SocialUiState.kt
new file mode 100644
index 0000000..cdc63d7
--- /dev/null
+++ b/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/data/SocialUiState.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package main.kotlin.com.motorro.statemachine.di.social.data
+
+import com.motorro.statemachine.di.api.AuthUiState
+
+internal sealed class SocialUiState: AuthUiState {
+ data object Loading : SocialUiState()
+ data object Form : SocialUiState()
+}
\ No newline at end of file
diff --git a/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/di/SocialFlowModule.kt b/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/di/SocialFlowModule.kt
new file mode 100644
index 0000000..8c8c582
--- /dev/null
+++ b/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/di/SocialFlowModule.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package main.kotlin.com.motorro.statemachine.di.social.di
+
+import com.motorro.statemachine.di.api.AuthDataApi
+import com.motorro.statemachine.di.api.AuthUiApi
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ActivityComponent
+import dagger.hilt.android.components.ViewModelComponent
+import main.kotlin.com.motorro.statemachine.di.social.SocialDataApi
+import main.kotlin.com.motorro.statemachine.di.social.SocialUiApi
+
+@Module
+@InstallIn(ViewModelComponent::class)
+internal abstract class SocialFlowDataModule {
+ @Binds
+ abstract fun bindSocialDataApi(impl: SocialDataApi): AuthDataApi
+}
+
+@Module
+@InstallIn(ActivityComponent::class)
+internal abstract class SocialFlowUiModule {
+ @Binds
+ abstract fun bindSocialUiApi(impl: SocialUiApi): AuthUiApi
+}
\ No newline at end of file
diff --git a/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/state/BaseSocialState.kt b/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/state/BaseSocialState.kt
new file mode 100644
index 0000000..4db3fb2
--- /dev/null
+++ b/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/state/BaseSocialState.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package main.kotlin.com.motorro.statemachine.di.social.state
+
+import com.motorro.commonstatemachine.coroutines.CoroutineState
+import com.motorro.statemachine.commoncore.log.Logger
+import com.motorro.statemachine.di.api.AuthGesture
+import com.motorro.statemachine.di.api.AuthUiState
+import main.kotlin.com.motorro.statemachine.di.social.data.SocialGesture
+
+internal abstract class BaseSocialState(context: SocialContext) : CoroutineState(), SocialContext by context {
+
+ final override fun doProcess(gesture: AuthGesture) {
+ when (gesture) {
+ is SocialGesture -> doProcess(gesture)
+ else -> Logger.w("Not a `SocialGesture`: $gesture")
+ }
+ }
+
+ protected open fun doProcess(gesture: SocialGesture) {
+ Logger.w("Gesture not handled: $gesture")
+ }
+}
diff --git a/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/state/FormState.kt b/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/state/FormState.kt
new file mode 100644
index 0000000..3427c6f
--- /dev/null
+++ b/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/state/FormState.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package main.kotlin.com.motorro.statemachine.di.social.state
+
+import com.motorro.statemachine.commoncore.log.Logger
+import main.kotlin.com.motorro.statemachine.di.social.data.SocialGesture
+import main.kotlin.com.motorro.statemachine.di.social.data.SocialUiState
+
+internal class FormState(context: SocialContext) : BaseSocialState(context) {
+
+ override fun doStart() {
+ setUiState(SocialUiState.Form)
+ }
+
+ override fun doProcess(gesture: SocialGesture) {
+ when (gesture) {
+ SocialGesture.Action -> {
+ Logger.d("Logging in...")
+ setMachineState(factory.loggingIn())
+ }
+ SocialGesture.Back -> {
+ Logger.d("Back pressed. Terminating...")
+ setMachineState(factory.terminated())
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/state/LoggingIn.kt b/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/state/LoggingIn.kt
new file mode 100644
index 0000000..c30c1f7
--- /dev/null
+++ b/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/state/LoggingIn.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package main.kotlin.com.motorro.statemachine.di.social.state
+
+import com.motorro.statemachine.commoncore.log.Logger
+import com.motorro.statemachine.di.api.data.Session
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import main.kotlin.com.motorro.statemachine.di.social.SocialConstants
+import main.kotlin.com.motorro.statemachine.di.social.data.SocialGesture
+import main.kotlin.com.motorro.statemachine.di.social.data.SocialUiState
+import kotlin.time.Duration.Companion.seconds
+
+/**
+ * Emulates Social login
+ */
+internal class LoggingIn(context: SocialContext) : BaseSocialState(context) {
+ override fun doStart() {
+ setUiState(SocialUiState.Loading)
+ Social()
+ }
+
+ private fun Social() = stateScope.launch {
+ Logger.d("Logging in...")
+ delay(2.seconds)
+ setMachineState(factory.complete(Session.Active(
+ accessToken = SocialConstants.ACCESS_TOKEN,
+ refreshToken = SocialConstants.REFRESH_TOKEN
+ )))
+ }
+
+ override fun doProcess(gesture: SocialGesture) {
+ when(gesture) {
+ SocialGesture.Back -> {
+ Logger.d("Back pressed. Back to form...")
+ setMachineState(factory.form())
+ }
+ else -> super.doProcess(gesture)
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/state/SocialContext.kt b/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/state/SocialContext.kt
new file mode 100644
index 0000000..56b89c6
--- /dev/null
+++ b/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/state/SocialContext.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package main.kotlin.com.motorro.statemachine.di.social.state
+
+import com.motorro.statemachine.di.api.AuthFlowHost
+
+/**
+ * Common inter-state context
+ */
+internal interface SocialContext {
+ val factory: SocialFactory
+ val flowHost: AuthFlowHost
+}
\ No newline at end of file
diff --git a/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/state/SocialFactory.kt b/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/state/SocialFactory.kt
new file mode 100644
index 0000000..2492310
--- /dev/null
+++ b/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/state/SocialFactory.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package main.kotlin.com.motorro.statemachine.di.social.state
+
+import com.motorro.commonstatemachine.CommonMachineState
+import com.motorro.statemachine.di.api.AuthFlowHost
+import com.motorro.statemachine.di.api.AuthGesture
+import com.motorro.statemachine.di.api.AuthUiState
+import com.motorro.statemachine.di.api.data.Session
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+/**
+ * Sub-flow state factory
+ */
+internal interface SocialFactory {
+ fun form(): CommonMachineState
+ fun loggingIn(): CommonMachineState
+ fun complete(session: Session.Active): CommonMachineState
+ fun terminated(): CommonMachineState
+
+ @AssistedFactory
+ interface Factory {
+ operator fun invoke(flowHost: AuthFlowHost): Impl
+ }
+
+ class Impl @AssistedInject constructor(@Assisted flowHost: AuthFlowHost) : SocialFactory {
+
+ private val context = object : SocialContext {
+ override val factory: SocialFactory = this@Impl
+ override val flowHost: AuthFlowHost = flowHost
+ }
+
+ override fun form() = FormState(
+ context
+ )
+
+ override fun loggingIn() = LoggingIn(
+ context,
+ )
+
+ override fun complete(session: Session.Active) = object : CommonMachineState() {
+ override fun doStart() {
+ context.flowHost.onComplete(session)
+ }
+ }
+
+ override fun terminated() = object : CommonMachineState() {
+ override fun doStart() {
+ context.flowHost.onComplete(null)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/ui/SocialFormView.kt b/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/ui/SocialFormView.kt
new file mode 100644
index 0000000..8fe1585
--- /dev/null
+++ b/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/ui/SocialFormView.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package main.kotlin.com.motorro.statemachine.di.social.ui
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.AccountCircle
+import androidx.compose.material3.Card
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.motorro.statemachine.androidcore.ui.theme.CommonStateMachineTheme
+import main.kotlin.com.motorro.statemachine.di.social.data.SocialGesture
+import main.kotlin.com.motorro.statemachine.di.social.data.SocialUiState
+
+@Composable
+internal fun SocialFormView(state: SocialUiState.Form, onGesture: (SocialGesture) -> Unit, modifier: Modifier = Modifier) {
+ Column(
+ modifier = modifier.fillMaxSize().padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Card(modifier = modifier.fillMaxWidth().clickable { onGesture(SocialGesture.Action) } ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(
+ Icons.Rounded.AccountCircle,
+ contentDescription = "Login with Social",
+ modifier = Modifier.size(96.dp)
+ )
+ Text(
+ text = "Login with Social",
+ style = MaterialTheme.typography.headlineMedium
+ )
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun SocialFormViewPreview() {
+ CommonStateMachineTheme {
+ SocialFormView(
+ state = SocialUiState.Form,
+ onGesture = {}
+ )
+ }
+}
diff --git a/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/ui/SocialScreen.kt b/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/ui/SocialScreen.kt
new file mode 100644
index 0000000..1841ca3
--- /dev/null
+++ b/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/ui/SocialScreen.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2026 Nikolai Kotchetkov.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package main.kotlin.com.motorro.statemachine.di.social.ui
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.motorro.statemachine.androidcore.compose.Loading
+import main.kotlin.com.motorro.statemachine.di.social.data.SocialGesture
+import main.kotlin.com.motorro.statemachine.di.social.data.SocialUiState
+
+@Composable
+internal fun SocialScreen(state: SocialUiState, onGesture: (SocialGesture) -> Unit, modifier: Modifier = Modifier) {
+ when(state) {
+ is SocialUiState.Form -> SocialFormView(state, onGesture, modifier)
+ SocialUiState.Loading -> Loading(modifier)
+ }
+}
\ No newline at end of file
diff --git a/examples/lifecycle/build.gradle.kts b/examples/lifecycle/build.gradle.kts
index ddf09d7..9a15623 100644
--- a/examples/lifecycle/build.gradle.kts
+++ b/examples/lifecycle/build.gradle.kts
@@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
/*
-* Copyright 2023 Nikolai Kotchetkov.
+* Copyright 2026 Nikolai Kotchetkov.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index f0f6b55..6ee01a0 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,11 +1,18 @@
[versions]
androidGradlePlugin = "9.0.0"
+androidxAppcompat = "1.7.1"
androidxCore = "1.17.0"
+androidxActivity = "1.12.3"
+androidxFragment = "1.8.9"
androidxHilt = "1.3.0"
-compose = "2026.01.00"
-composeActivity = "1.12.2"
+compose = "2026.01.01"
+composeMultiplatform = "1.10.0"
+viewmodelMultiplatform = "2.9.6"
+lificyclelMultiplatform = "2.9.6"
+composeActivity = "1.12.3"
composeViewmodel = "2.10.0"
coroutines = "1.10.2"
+immutable = "0.4.0"
desugaring = "2.1.5"
dokka = "2.1.0"
git = "5.3.3"
@@ -21,11 +28,12 @@ testAndroidxCore = "1.7.0"
testAndroidxEspresso = "3.7.0"
testAndroidxExtJunit = "1.3.0"
testJunit = "4.13.2"
-testMockk = "1.14.7"
+testMockk = "1.14.9"
testMockkCommon = "1.13.5"
[plugins]
compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" }
android_app = { id = "com.android.application", version.ref = "androidGradlePlugin" }
android_lib = { id = "com.android.library", version.ref = "androidGradlePlugin" }
kotlin_jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
@@ -39,14 +47,17 @@ git = { id = "org.ajoberstar.grgit", version.ref = "git" }
nexus_publish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "nexusPublish" }
[libraries]
-kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
+kotlin-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "immutable" }
kotlin-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
kotlin-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
kotlin-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinDatetime" }
desugaring = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugaring" }
+androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppcompat" }
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidxCore" }
+androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "androidxActivity" }
+androidx-fragment = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "androidxFragment" }
androidx-lifecycle-common = { module = "androidx.lifecycle:lifecycle-common", version.ref = "lifecycle" }
androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
androidx-lifecycle-livedata = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycle" }
@@ -64,6 +75,11 @@ compose-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-compose",
compose-foundation = { module = "androidx.compose.foundation:foundation" }
compose-foundation-layouts = { module = "androidx.compose.foundation:foundation-layout" }
+composeMultiplatform-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "composeMultiplatform" }
+composeMultiplatform-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "composeMultiplatform" }
+composeMultiplatform-viewmodel = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "viewmodelMultiplatform" }
+composeMultiplatform-lifecycle = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lificyclelMultiplatform" }
+
hilt_android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
hilt_compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" }
hilt_compiler_androidx = { module = "androidx.hilt:hilt-compiler", version.ref = "androidxHilt" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 62389bb..8270742 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
#Sun Aug 24 18:49:33 CEST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 5f24179..00857ea 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -37,6 +37,9 @@ include(
":tmap",
":commonstatemachine",
":coroutines",
+ ":commonflow:data",
+ ":commonflow:compose",
+ ":commonflow:viewmodel",
":examples:commoncore",
":examples:androidcore",
":examples:welcome:welcome",
@@ -49,5 +52,13 @@ include(
":examples:multi:mixed",
":examples:multi:navbar",
":examples:multi:parallel",
- ":examples:lifecycle"
+ ":examples:lifecycle",
+ ":examples:di:api",
+ ":examples:di:login",
+ ":examples:di:social",
+ ":examples:di:app",
+ ":examples:books:domain",
+ ":examples:books:book",
+ ":examples:books:book:demo",
+ ":examples:books:app"
)