Skip to content

Commit 730992a

Browse files
committed
Common flow view
Refs: #11
1 parent 168dc1d commit 730992a

21 files changed

Lines changed: 611 additions & 18 deletions

File tree

.github/workflows/check.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
- name: Grant execute permission for gradlew
3434
run: chmod +x gradlew
3535
- name: Check with gradle
36-
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 --no-daemon --no-configuration-cache
36+
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:commonflow:assembleDebug --no-daemon --no-configuration-cache
3737
- name: Upload problems report
3838
uses: actions/upload-artifact@v4
3939
if: failure()

.idea/compiler.xml

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/deploymentTargetSelector.xml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/gradle.xml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,13 +1193,14 @@ dependencies {
11931193
The libraries include the following interfaces:
11941194

11951195
- [CommonFlowHost](commonflow/data/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/data/CommonFlowHost.kt) -
1196-
the interface the proxy should provide to the child flow. Child uses this flow to terminate but could
1197-
also include any other callbacks
1198-
- [CommonFlowDataApi](commonflow/data/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/data/CommonFlowApi.kt) -
1196+
the interface the proxy should provide to the child flow. Child uses this flow to signal its termination.
1197+
- [CommonFlowDataApi](commonflow/data/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/data/CommonFlowDataApi.kt) -
11991198
contains methods to initiate the flow and to adapt it to the hosting flow.
12001199
- [CommonFlowUiApi](commonflow/compose/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/compose/CommonFlowUiApi.kt) -
12011200
An interface with the single `Screen` method to inject and put to the composition
12021201

1202+
For more details on multiplatform compose viewmodel follow [this link](https://kotlinlang.org/docs/multiplatform/compose-viewmodel.html)
1203+
12031204
Check the [example](examples/di) that shows the use of this flow:
12041205

12051206
- The app has two states: `Content` and `Auth`.

build.gradle.kts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@ tasks.register("runCoroutinesTests") {
100100
description = "Run unit tests for the coroutines extension layer."
101101
}
102102

103+
tasks.register("runCommonflowTests") {
104+
dependsOn(":commonflow:viewmodel:allTests")
105+
description = "Run unit tests for the commonflow library."
106+
}
107+
103108
tasks.register("runRegisterUnitTests") {
104109
dependsOn(":examples:welcome:commonregister:allTests")
105110
description = "Run unit tests for the common register layer."
@@ -116,13 +121,18 @@ tasks.register("runLceUnitTests") {
116121
}
117122

118123
tasks.register("runWelcomeUnitTests") {
119-
dependsOn(":examples:welcome:welcome:testDebugUnitTest") // Note: double colon might be a typo, usually ':examples:welcome:testDebugUnitTest'
124+
dependsOn(":examples:welcome:welcome:testDebugUnitTest")
120125
description = "Run unit tests for welcome app."
121126
}
122127

123128
tasks.register("runTimerUnitTests") {
124129
dependsOn(":examples:timer:testAndroidHostTest")
125-
description = "Run unit tests for welcome app." // Description seems to be a copy-paste from runWelcomeUnitTests
130+
description = "Run unit tests for timer library."
131+
}
132+
133+
tasks.register("runDiUnitTests") {
134+
dependsOn(":examples:di:login:testDebugUnitTest")
135+
description = "Run unit tests for di app."
126136
}
127137

128138
tasks.register("displayVersion") {
@@ -136,11 +146,13 @@ tasks.register("runUnitTests") {
136146
dependsOn(
137147
"runStateMachineTests",
138148
"runCoroutinesTests",
149+
"runCommonflowTests",
139150
"runLoginUnitTests",
140151
"runRegisterUnitTests",
141152
"runLceUnitTests",
142153
"runWelcomeUnitTests",
143-
"runTimerUnitTests"
154+
"runTimerUnitTests",
155+
"runDiUnitTests"
144156
)
145157
group = "verification"
146158
description = "Run unit tests for all modules."

commonflow/data/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/data/CommonFlowDataApi.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ import com.motorro.commonstatemachine.CommonMachineState
2626
* @see com.motorro.commonstatemachine.ProxyMachineState
2727
* @see CommonFlowHost
2828
*/
29-
interface CommonFlowDataApi<G: Any, U: Any, I, R, F : CommonFlowHost<R>> {
29+
interface CommonFlowDataApi<G: Any, U: Any, I, R> {
3030
/**
3131
* Creates flow
3232
*/
33-
fun init(flowHost: F, input: I? = null): CommonMachineState<G, U>
33+
fun init(flowHost: CommonFlowHost<R>, input: I? = null): CommonMachineState<G, U>
3434

3535
/**
3636
* Returns default UI state
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/*
2+
* Copyright 2026 Nikolai Kotchetkov.
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
* limitations under the License.
12+
*/
13+
14+
@file:Suppress("unused")
15+
@file:OptIn(ExperimentalWasmDsl::class)
16+
17+
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
18+
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
19+
20+
plugins {
21+
alias(libs.plugins.kotlin.multiplatform)
22+
alias(libs.plugins.android.kotlin.multiplatform.library)
23+
alias(libs.plugins.compose)
24+
alias(libs.plugins.composeMultiplatform)
25+
alias(libs.plugins.kotlin.dokka)
26+
id("maven-publish")
27+
id("signing")
28+
}
29+
30+
val versionName: String by project.extra
31+
val androidMinSdkVersion: Int by project.extra
32+
val androidTargetSdkVersion: Int by project.extra
33+
val androidCompileSdkVersion: Int by project.extra
34+
35+
group = rootProject.group
36+
version = rootProject.version
37+
38+
println("== Project version: $versionName ==")
39+
40+
kotlin {
41+
jvmToolchain(17)
42+
43+
jvm()
44+
android {
45+
namespace = "com.motorro.commonstatemachine.commonflow.viewmodel"
46+
compileSdk = androidCompileSdkVersion
47+
minSdk = androidMinSdkVersion
48+
49+
withHostTest {
50+
isIncludeAndroidResources = true
51+
}
52+
53+
compilerOptions {
54+
jvmTarget.set(JvmTarget.JVM_17)
55+
}
56+
}
57+
58+
js(IR) {
59+
binaries.library()
60+
useCommonJs()
61+
browser {
62+
testTask(Action {
63+
useMocha {
64+
timeout = "10s"
65+
}
66+
})
67+
}
68+
}
69+
70+
wasmJs {
71+
binaries.library()
72+
useCommonJs()
73+
browser {
74+
testTask(Action {
75+
useMocha {
76+
timeout = "10s"
77+
}
78+
})
79+
}
80+
}
81+
82+
listOf(
83+
iosX64(),
84+
iosArm64(),
85+
iosSimulatorArm64()
86+
).forEach {
87+
it.binaries.framework {
88+
baseName = "commonflow-viewmodel"
89+
isStatic = true
90+
}
91+
}
92+
93+
sourceSets {
94+
commonMain.dependencies {
95+
api(project(":commonstatemachine"))
96+
api(project(":coroutines"))
97+
api(project(":commonflow:compose"))
98+
api(libs.composeMultiplatform.viewmodel)
99+
implementation(libs.composeMultiplatform.lifecycle)
100+
}
101+
commonTest.dependencies {
102+
implementation(libs.test.kotlin)
103+
implementation(libs.test.kotlin.coroutines)
104+
}
105+
androidMain.dependencies {
106+
api(libs.androidx.appcompat)
107+
implementation(libs.androidx.activity)
108+
implementation(libs.androidx.fragment)
109+
implementation(libs.compose.activity)
110+
}
111+
}
112+
}
113+
val javadocJar by tasks.registering(Jar::class) {
114+
dependsOn(tasks.dokkaGenerate)
115+
group = "documentation"
116+
archiveClassifier.set("javadoc")
117+
from(tasks.dokkaGenerate)
118+
}
119+
120+
val libId = "commonflow-viewmodel"
121+
val libName = "commonflow-viewmodel"
122+
val libDesc = "Common view components to wrap standard flow"
123+
val projectUrl: String by project.extra
124+
val projectScm: String by project.extra
125+
val ossrhUsername: String? by rootProject.extra
126+
val ossrhPassword: String? by rootProject.extra
127+
val developerId: String by project.extra
128+
val developerName: String by project.extra
129+
val developerEmail: String by project.extra
130+
val signingKey: String? by rootProject.extra
131+
val signingPassword: String? by rootProject.extra
132+
133+
publishing {
134+
publications.withType<MavenPublication> {
135+
artifact(javadocJar)
136+
pom {
137+
name.set(libName)
138+
description.set(libDesc)
139+
url.set(projectUrl)
140+
licenses {
141+
license {
142+
name.set("Apache-2.0")
143+
url.set("https://apache.org/licenses/LICENSE-2.0")
144+
}
145+
}
146+
developers {
147+
developer {
148+
id.set(developerId)
149+
name.set(developerName)
150+
email.set(developerEmail)
151+
}
152+
}
153+
scm {
154+
connection.set(projectScm)
155+
developerConnection.set(projectScm)
156+
url.set(projectUrl)
157+
}
158+
}
159+
}
160+
}
161+
162+
signing {
163+
useInMemoryPgpKeys(signingKey, signingPassword)
164+
sign(publishing.publications)
165+
}
166+
167+
val signingTasks = tasks.withType<Sign>()
168+
tasks.withType<AbstractPublishToMaven>().configureEach {
169+
dependsOn(signingTasks)
170+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright 2026 Nikolai Kotchetkov.
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
* limitations under the License.
12+
*/
13+
14+
package com.motorro.commonstatemachine.flow.viewmodel
15+
16+
import android.view.View
17+
import androidx.activity.compose.BackHandler
18+
import androidx.activity.compose.setContent
19+
import androidx.activity.viewModels
20+
import androidx.appcompat.app.AppCompatActivity
21+
import androidx.compose.runtime.Composable
22+
import androidx.compose.ui.platform.ComposeView
23+
import androidx.compose.ui.platform.ViewCompositionStrategy
24+
import androidx.fragment.app.Fragment
25+
import androidx.fragment.app.viewModels
26+
import androidx.lifecycle.viewmodel.CreationExtras
27+
28+
/**
29+
* Builds state machine composition
30+
* Ensure that [AppCompatActivity] has correct view model factory
31+
* @param extrasProducer optional extras producer
32+
* @param setResult implement to set the activity result or do any result processing
33+
* @param navigationBackHandler optional back navigation handler. Implement to pass back navigation to the view model
34+
* @param content content block
35+
*/
36+
fun <G: Any, U: Any, R> AppCompatActivity.setStateMachineContent(
37+
extrasProducer: (() -> CreationExtras)? = null,
38+
setResult: (R?) -> Unit = { },
39+
navigationBackHandler: @Composable (Boolean, () -> Unit) -> Unit = { enabled, onBack ->
40+
BackHandler(enabled = enabled) { onBack() }
41+
},
42+
content: @Composable (U, (G) -> Unit) -> Unit
43+
) {
44+
setContent {
45+
val viewModel: CommonFlowViewModel<G, U, *, R> by viewModels(extrasProducer = extrasProducer)
46+
CommonFlowComposition(
47+
viewModel = viewModel,
48+
navigationBackHandler = navigationBackHandler,
49+
content = content,
50+
finish = { result ->
51+
if (null != result) {
52+
setResult(result)
53+
}
54+
finish()
55+
}
56+
)
57+
}
58+
}
59+
60+
/**
61+
* Builds state machine composable
62+
* Ensure that [Fragment] has correct view model factory
63+
* @param onFinish called when the flow is finished
64+
* @param extrasProducer optional extras producer
65+
* @param navigationBackHandler optional back navigation handler. Implement to pass back navigation to the view model
66+
* @param content content block
67+
*/
68+
fun < G: Any, U: Any, R> Fragment.createStateMachineView(
69+
onFinish: (R?) -> Unit,
70+
extrasProducer: (() -> CreationExtras)? = null,
71+
navigationBackHandler: @Composable (Boolean, () -> Unit) -> Unit = { enabled, onBack ->
72+
BackHandler(enabled = enabled) { onBack() }
73+
},
74+
content: @Composable (U, (G) -> Unit) -> Unit
75+
): View = ComposeView(requireContext()).apply {
76+
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
77+
78+
setContent {
79+
val viewModel: CommonFlowViewModel<G, U, *, R> by viewModels(extrasProducer = extrasProducer)
80+
CommonFlowComposition(
81+
viewModel = viewModel,
82+
navigationBackHandler = navigationBackHandler,
83+
content = content,
84+
finish = onFinish
85+
)
86+
}
87+
}

0 commit comments

Comments
 (0)