From 356290ee19ad60b7434efdea835d09e13a46341f Mon Sep 17 00:00:00 2001 From: motorro Date: Tue, 8 Sep 2026 18:32:53 +0200 Subject: [PATCH 01/32] API module skill Refs: #14 --- .../common-child-flow/boilerplate/SKILL.md | 72 +++++++ .../boilerplate/assets/common-child-flow.puml | 185 ++++++++++++++++++ .../assets/example/api/AuthDataApi.kt | 8 + .../assets/example/api/AuthGesture.kt | 7 + .../assets/example/api/AuthInput.kt | 7 + .../assets/example/api/AuthResult.kt | 7 + .../assets/example/api/AuthUiApi.kt | 8 + .../assets/example/api/AuthUiState.kt | 12 ++ .../boilerplate/references/data-and-ui-api.md | 24 +++ .../references/gesture-and-ui-state-api.md | 23 +++ .../references/input-and-result-api.md | 22 +++ 11 files changed, 375 insertions(+) create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/common-child-flow.puml create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthDataApi.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthGesture.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthInput.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthResult.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthUiApi.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthUiState.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-api.md create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/references/gesture-and-ui-state-api.md create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/references/input-and-result-api.md diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md b/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md new file mode 100644 index 0000000..10f770b --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md @@ -0,0 +1,72 @@ +--- +name: commonstatemachine-commonchildflow-boilerplate +description: Use this skill to make a boilerplate for the new feature-flow using + the [CommonStateMachine](https://github.com/motorro/CommonStateMachine) architecture. + If the user asks you something like "Create a new authentication flow feature" - use this skill. +metadata: + author: Motorro + last-updated: '2026-08-12' + keywords: + - android + - architecture + - cross-platform + - kotlin-multiplatform + - mvi + - state-machine + - feature-flow +--- + +## Description +Use this skill to add a new feature boilerplate to the app based on the [CommonStateMachine](https://github.com/motorro/CommonStateMachine) architecture. +following the [Common Child Flow](https://github.com/motorro/CommonStateMachine/tree/master#common-child-flow-api) design. This design sets a common approach to use proxy state machines for a feature-flow. + +## Example +This instruction uses an example authentication module. All examples, schemes and code samples refer +to such an example. Adjust the names and packages to your actual need. + +## Strategy +You will create a set of gradle modules that together form a feature: + +- The API module: contains the definition of input and output data types for the feature. + Example: authentication API that provides the basic interface to the authentication feature. +- One or more implementation modules: contains the implementation of the API. + Example: login implementation module - implements API with login/password functionality. + +Take a look at the [class diagram](assets/common-child-flow.puml). It contains an overview +between all the interfaces, classes and objects in a setup using the Authentication module example. + +## Step 1. Module directory structure creation +Create the following module structure if not instructed otherwise: +``` +my-project/ # Project folder. Common package: com.motorro.statemachine +├── app # Example: an application module, package: com.motorro.statemachine.app +├── auth/ # Required: a base directory for a new feature (authentication) +│ ├── api/ # Required: an API gradle module that describes the feature, package: com.motorro.statemachine.auth.api +│ └── implementation/ # Required: an implementation gradle module that implements the API, package: com.motorro.statemachine.auth.implementation +└── ... +``` + +- Use the Gradle setup common for the other modules in a project +- Include the following dependencies if not provided already: + - Base state-machine components: "com.motorro.commonstatemachine:commonstatemachine:x.x.x" + - Coroutine extensions (if using coroutines): "com.motorro.commonstatemachine:coroutines:x.x.x" + - Common data API (state machine child flow): "com.motorro.commonstatemachine:commonflow-data:x.x.x" + - Common UI API (Compose API if used): "com.motorro.commonstatemachine:commonflow-compose:x.x.x" +- Include test dependencies that are used across the project and coroutines test dependencies if used. +- If the user asks you to create some specific implementation or several implementations of the same API, infer the names + and create several modules. For example, the user may ask you to create two authentication implementations of the same Authentication API: + + - `password` - an implementation for the password authentication + - `social` - and implementation for the social network authentication + + If so - create several implementation modules, use the same API module as a base for them. + +## Step 2. Create an API module code +Create a package for the feature API. By default, use the common application package adding the feature name and `api`. +See the module structure above for an example. +Then follow these guides for the API module implementation: + +1. [UI gesture definitions and UI state definitions API](references/gesture-and-ui-state-api.md) +2. [Feature-flow input and result API](references/input-and-result-api.md) +3. [Feature-flow data and UI API](references/data-and-ui-api.md) + diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/common-child-flow.puml b/skills/commonstatemachine/common-child-flow/boilerplate/assets/common-child-flow.puml new file mode 100644 index 0000000..6cc67c4 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/common-child-flow.puml @@ -0,0 +1,185 @@ +@startuml +'https://plantuml.com/class-diagram + +hide empty members + +interface CommonFlowDataApi +interface CommonFlowUiApi +abstract class CommonMachineState +abstract class CoroutineState + +CoroutineState -up-|> CommonMachineState + +package API { + interface AuthGesture + note left + Marker intent (gesture) interface for Auth feature + May include public properties available to the API client + end note + interface AuthUiState + note right + Marker UI-state interface for Auth feature + May include public properties available to the API client + end note + + class AuthInput + class AuthResult + + interface AuthDataApi { + +init(flowHost: CommonFlowHost, input: AuthInput): CommonMachineState + +getDefaultUiState(): AuthUiState + +getBackGesture(): AuthGesture? = null + } + AuthDataApi -up-|> CommonFlowDataApi: implements + AuthDataApi -down-> AuthGesture: driven by + AuthDataApi -down-> AuthUiState: displays + AuthDataApi -right-> AuthResult: returns + AuthInput -> AuthDataApi: initializes + + interface AuthUiApi { + +Screen(state: AuthUiState, onGesture: (AuthGesture) -> Unit, modifier: Modifier = Modifier) + } + AuthUiApi -up-|> CommonFlowUiApi: implements + AuthUiApi -down-> AuthGesture: emits + AuthUiApi -down-> AuthUiState: displays +} + +package Implementation { + package data { + interface AuthGestureImpl + note left + A sealed structure of all available gestures (UI intents) for the feature + end note + + object Back + note bottom + Example: Back navigation button + end note + + object Action + note bottom + Example: Main action button + end note + AuthGestureImpl -up-|> AuthGesture + AuthGestureImpl <|-down- Back + AuthGestureImpl <|-down- Action + + interface AuthUiStateImpl + note right + A sealed structure of all possible UI states for the feature + end note + + object Loading + note bottom + Example: Loading screen + end note + + class Form { + +username: String + +password: String + } + note bottom + Example: Login form + end note + AuthUiStateImpl -up-|> AuthUiState + AuthUiStateImpl <|-down- Loading + AuthUiStateImpl <|-down- Form + + class AuthStateData { + +username: String + +password: String + } + note right + A flow data state passed between machine states. + Stores inter-state data + end note + } + + package state { + interface AuthState + note top + Type-alias or an interface of the machine state + end note + AuthState -up-|> CommonMachineState + + abstract class BaseAuthState + note top + Common base class for all machine states + end note + BaseAuthState -up-|> CoroutineState + BaseAuthState -|> AuthContext: delegates implementation + + class LoadingState + LoadingState -up-|> BaseAuthState + note bottom + Example: sample machine state - Loading + end note + + class FormState + FormState -up-|> BaseAuthState + note bottom + Example: sample machine state - Login form + end note + + interface AuthStateFactory { + +loading(): AuthState + +form(data: AuthStateData): AuthState + } + AuthStateFactory -> AuthState: creates + note left + Abstracts the state from another state creation + end note + + class AuthStateFactoryImpl + note bottom + State factory implementation + end note + AuthStateFactoryImpl -up-|> AuthStateFactory + AuthStateFactory -> BaseAuthState: creates + + interface AuthContext { + +factory: AuthStateFactory + +flowHost: CommonFlowHost + +renderer: AuthUiRenderer + } + note bottom + State-machine context available to every state + Put immutable links to common tools here: + - factory + - flow host + - etc + end note + } + + package ui { + interface AuthUiRenderer { + +renderLoading(): AuthUiState + +renderForm(data: AuthStateData): AuthUiState + } + note top + Transforms data-state to AuthUiState. + Abstracts machine state from the interface data rendering + end note + AuthUiRenderer -> AuthStateData: renders + AuthUiRenderer -> AuthUiStateImpl: creates + + class AuthUiRendererImpl + AuthUiRendererImpl -up-|> AuthUiRenderer + AuthUiRendererImpl -> AuthUiStateImpl: creates + } + + class AuthDataApiImpl + note bottom + Data API implementation + Injects factory and initializes the flow by calling `init` + end note + AuthUiStateImpl -up-|> AuthDataApi + + class AuthUiApiImpl + note bottom + UI API implementation + Builds the view with Compose components inside the module + end note + AuthUiStateImpl -up-|> AuthUiApi +} +@enduml \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthDataApi.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthDataApi.kt new file mode 100644 index 0000000..b31c1b5 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthDataApi.kt @@ -0,0 +1,8 @@ +package com.motorro.statemachine.auth.api +import com.motorro.commonstatemachine.flow.data.CommonFlowDataApi + +/** + * Data API used to run a child-flow in a proxy machine + * Available to a feature consumer + */ +typealias AuthDataApi = CommonFlowDataApi \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthGesture.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthGesture.kt new file mode 100644 index 0000000..a001dfe --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthGesture.kt @@ -0,0 +1,7 @@ +package com.motorro.statemachine.auth.api + +/** + * A base type for authentication user gestures + * Available to a feature consumer + */ +interface AuthGesture \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthInput.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthInput.kt new file mode 100644 index 0000000..f46f3c6 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthInput.kt @@ -0,0 +1,7 @@ +package com.motorro.statemachine.auth.api + +/** + * Initializing data for the feature flow + * @param message Example of a property passed to authentication flow + */ +data class AuthInput(val message: String) \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthResult.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthResult.kt new file mode 100644 index 0000000..1894a93 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthResult.kt @@ -0,0 +1,7 @@ +package com.motorro.statemachine.auth.api + +/** + * Feature flow result + * @param authenticated Example of a property returned to a parent flow + */ +data class AuthInput(val authenticated: Boolean) \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthUiApi.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthUiApi.kt new file mode 100644 index 0000000..762d13e --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthUiApi.kt @@ -0,0 +1,8 @@ +package com.motorro.statemachine.auth.api +import com.motorro.commonstatemachine.flow.compose.CommonFlowUiApi + +/** + * UI API used display a child flow inside a parent composition + * Available to a feature consumer + */ +typealias AuthUiApi = CommonFlowUiApi \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthUiState.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthUiState.kt new file mode 100644 index 0000000..42ad31d --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthUiState.kt @@ -0,0 +1,12 @@ +package com.motorro.statemachine.auth.api + +/** + * A base type for authentication UI state + * Available to a feature consumer + */ +interface AuthUiState { + /** + * Some common property available to a feature consumer + */ + val modal: Boolean +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-api.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-api.md new file mode 100644 index 0000000..0bf9534 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-api.md @@ -0,0 +1,24 @@ +# Module data and UI API +Use this reference to create the required data and UI APIs. + +## Mechanics +The [Common Child Flow](https://github.com/motorro/CommonStateMachine/tree/master#common-child-flow-api) consists of two main interfaces: + +- A subtype of [CommonFlowDataApi](https://github.com/motorro/CommonStateMachine/blob/master/commonflow/data/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/data/CommonFlowDataApi.kt). + This interface binds together the gesture-system, ui-system, input and output providing the child flow initialization. + The API implementation is then created in the implementation module and could be injected into the parent flow. +- A subtype of [CommonFlowUiApi](https://github.com/motorro/CommonStateMachine/blob/master/commonflow/compose/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/compose/CommonFlowUiApi.kt). + This interface binds gesture-system and ui-system with the composable responsible for the view rendering. + The API implementation is then created in the implementation module and could be injected into the composition using locals or any other way. + +## Data flow +Take the created [Gestures and UI-states](gesture-and-ui-state-api.md), [input and result](data-and-ui-api.md) and create an interface or a typealias +to bind the generics of the `CommonFlowDataApi`. This API would be available to the flow consumer to start the feature flow. +Take a look at the [class diagram](../assets/common-child-flow.puml) for more details. +Take a look at the [example](../assets/example/api/AuthDataApi.kt) - data API definition + +## UI flow +Take the created [Gestures and UI-states](gesture-and-ui-state-api.md) and create an interface or a typealias +to bind the generics of the `CommonFlowUiApi`. This API would be available to the flow consumer to display the feature. +Take a look at the [class diagram](../assets/common-child-flow.puml) for more details. +Take a look at the [example](../assets/example/api/AuthUiApi.kt) - UI API definition diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/gesture-and-ui-state-api.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/gesture-and-ui-state-api.md new file mode 100644 index 0000000..35857a8 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/gesture-and-ui-state-api.md @@ -0,0 +1,23 @@ +# Gesture and UI-state API +Use this reference to create the UI gesture and UI state API. + +## Mechanics +The framework behind is a Model-View-Intent architecture, so: + +- The state is updated as a result of user intents (gestures) +- The view-state is fully described by data classes/objects - ui-states + +## Gestures +The implementation is hidden from the module consumer using the internal gesture structures. +To use the feature we export the basic marker interface that may only contain some common public properties. +This interface would be implemented later in the implementation module. +Take a look at the [class diagram](../assets/common-child-flow.puml) for more details. +Take a look at the [example](../assets/example/api/AuthGesture.kt) - common API definition + +## UI-states +The implementation is hidden from the module consumer using the internal ui-state structures. +To use the feature we export the basic marker interface that may only contain some common public properties. +This interface would be implemented later in the implementation module. +Take a look at the [class diagram](../assets/common-child-flow.puml) for more details. +Take a look at the [example](../assets/example/api/AuthUiState.kt) - common API definition + diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/input-and-result-api.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/input-and-result-api.md new file mode 100644 index 0000000..3294c25 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/input-and-result-api.md @@ -0,0 +1,22 @@ +# Module initialization data and flow result +Use this reference to create the required input data and define the flow result. + +## Mechanics +The [Common Child Flow](https://github.com/motorro/CommonStateMachine/tree/master#common-child-flow-api) runs a +child state-machine flow in a parent state-machine to get some result, display some UI, etc. +Example: the main application flow could run a child flow in-place to log the user-in. + +- The child flow could be initialized by some data passed from parent. +- The child flow could return some data back to the parent on completion. + +## Input +If the user asks to provide any input - create an immutable type - a data class or a sealed structure. +If no input type explicitly required - confirm and use `Unit` +Take a look at the [class diagram](../assets/common-child-flow.puml) for more details. +Take a look at the [example](../assets/example/api/AuthInput.kt) - common API definition + +## Result +If the user asks to provide a flow result - create an immutable type - a data class or a sealed structure. +If no result type explicitly required - confirm and use `Unit` +Take a look at the [class diagram](../assets/common-child-flow.puml) for more details. +Take a look at the [example](../assets/example/api/AuthResult.kt) - common API definition From 41a6c08f156dae678a1eb17b5908a04f1aa3e892 Mon Sep 17 00:00:00 2001 From: motorro Date: Mon, 14 Sep 2026 18:03:14 +0200 Subject: [PATCH 02/32] Adds implementation module Refs: #14 --- examples/lce/build.gradle.kts | 4 + examples/skills/README.md | 7 ++ examples/skills/auth/api/build.gradle.kts | 61 +++++++++++ .../statemachine/auth}/api/AuthDataApi.kt | 0 .../statemachine/auth}/api/AuthGesture.kt | 0 .../statemachine/auth}/api/AuthInput.kt | 0 .../statemachine/auth}/api/AuthResult.kt | 2 +- .../statemachine/auth}/api/AuthUiApi.kt | 0 .../statemachine/auth}/api/AuthUiState.kt | 2 +- .../auth/implementation/build.gradle.kts | 68 ++++++++++++ .../implementation/data/AuthGestureImpl.kt | 33 ++++++ .../auth/implementation/data/AuthStateData.kt | 22 ++++ .../implementation/data/AuthUiStateImpl.kt | 32 ++++++ .../data/PasswordRequirements.kt | 11 ++ .../auth/implementation/state/AuthContext.kt | 25 +++++ .../auth/implementation/state/AuthState.kt | 10 ++ .../implementation/state/AuthStateFactory.kt | 46 ++++++++ .../state/AuthStateFactoryImpl.kt | 69 ++++++++++++ .../implementation/state/BaseAuthState.kt | 34 ++++++ .../auth/implementation/state/FormState.kt | 69 ++++++++++++ .../state/PreloadingErrorState.kt | 48 +++++++++ .../implementation/state/PreloadingState.kt | 75 +++++++++++++ .../auth/implementation/ui/AuthUiRenderer.kt | 32 ++++++ .../usecase/GetPasswordRequirements.kt | 13 +++ .../auth/implementation/data/authGestures.kt | 11 ++ .../auth/implementation/data/dataStates.kt | 32 ++++++ .../auth/implementation/data/uiStates.kt | 6 ++ .../implementation/state/BaseStateTest.kt | 63 +++++++++++ gradle/libs.versions.toml | 5 + settings.gradle.kts | 4 +- skills/README.md | 10 ++ .../common-child-flow/boilerplate/SKILL.md | 48 ++++++++- .../boilerplate/assets/common-child-flow.puml | 4 +- .../boilerplate/assets/example/api | 1 + .../boilerplate/assets/example/implementation | 1 + .../assets/example/implementation-test | 1 + .../references/base-state-implementation.md | 39 +++++++ .../base-test-class-implementation.md | 44 ++++++++ .../boilerplate/references/data-and-ui-api.md | 10 +- .../references/data-and-ui-implementation.md | 73 +++++++++++++ .../references/gesture-and-ui-state-api.md | 6 +- .../references/input-and-result-api.md | 6 +- ...actory-context-interface-implementation.md | 85 +++++++++++++++ .../references/state-implementation.md | 102 ++++++++++++++++++ .../test-fixtures-implementation.md | 15 +++ 45 files changed, 1209 insertions(+), 20 deletions(-) create mode 100644 examples/skills/README.md create mode 100644 examples/skills/auth/api/build.gradle.kts rename {skills/commonstatemachine/common-child-flow/boilerplate/assets/example => examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth}/api/AuthDataApi.kt (100%) rename {skills/commonstatemachine/common-child-flow/boilerplate/assets/example => examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth}/api/AuthGesture.kt (100%) rename {skills/commonstatemachine/common-child-flow/boilerplate/assets/example => examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth}/api/AuthInput.kt (100%) rename {skills/commonstatemachine/common-child-flow/boilerplate/assets/example => examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth}/api/AuthResult.kt (74%) rename {skills/commonstatemachine/common-child-flow/boilerplate/assets/example => examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth}/api/AuthUiApi.kt (100%) rename {skills/commonstatemachine/common-child-flow/boilerplate/assets/example => examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth}/api/AuthUiState.kt (88%) create mode 100644 examples/skills/auth/implementation/build.gradle.kts create mode 100644 examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/data/AuthGestureImpl.kt create mode 100644 examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/data/AuthStateData.kt create mode 100644 examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/data/AuthUiStateImpl.kt create mode 100644 examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/data/PasswordRequirements.kt create mode 100644 examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/AuthContext.kt create mode 100644 examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/AuthState.kt create mode 100644 examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/AuthStateFactory.kt create mode 100644 examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/AuthStateFactoryImpl.kt create mode 100644 examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/BaseAuthState.kt create mode 100644 examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/FormState.kt create mode 100644 examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/PreloadingErrorState.kt create mode 100644 examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/PreloadingState.kt create mode 100644 examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/ui/AuthUiRenderer.kt create mode 100644 examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/usecase/GetPasswordRequirements.kt create mode 100644 examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation/data/authGestures.kt create mode 100644 examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation/data/dataStates.kt create mode 100644 examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation/data/uiStates.kt create mode 100644 examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation/state/BaseStateTest.kt create mode 100644 skills/README.md create mode 120000 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api create mode 120000 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation create mode 120000 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-test create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/references/base-state-implementation.md create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/references/base-test-class-implementation.md create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-implementation.md create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/references/renderer-factory-context-interface-implementation.md create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/references/state-implementation.md create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/references/test-fixtures-implementation.md diff --git a/examples/lce/build.gradle.kts b/examples/lce/build.gradle.kts index c0b1e92..2880944 100644 --- a/examples/lce/build.gradle.kts +++ b/examples/lce/build.gradle.kts @@ -75,6 +75,10 @@ dependencies { implementation(project(":examples:commoncore")) implementation(project(":examples:androidcore")) + api(project(":commonstatemachine")) + api(project(":commonflow:commonflow-data")) + api(project(":commonflow:commonflow-compose")) + coreLibraryDesugaring(libs.desugaring) implementation(libs.timber) diff --git a/examples/skills/README.md b/examples/skills/README.md new file mode 100644 index 0000000..3ae4cfd --- /dev/null +++ b/examples/skills/README.md @@ -0,0 +1,7 @@ +# Examples for skills + +This folder contains examples for [AI skills](../skills). +The files are here for convenience: + +- code completion +- testing \ No newline at end of file diff --git a/examples/skills/auth/api/build.gradle.kts b/examples/skills/auth/api/build.gradle.kts new file mode 100644 index 0000000..c5765a0 --- /dev/null +++ b/examples/skills/auth/api/build.gradle.kts @@ -0,0 +1,61 @@ +@file:Suppress("unused") + +/* +* Copyright 2023 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. +*/ +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) +} + +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 = "com.motorro" +version = versionName + +kotlin { + jvmToolchain(21) + + android { + namespace = "com.motorro.statemachine.auth.api" + compileSdk = androidCompileSdkVersion + minSdk = androidMinSdkVersion + + withHostTest { + isIncludeAndroidResources = true + } + + compilerOptions { + jvmTarget.set(JvmTarget.JVM_21) + } + } + + sourceSets { + commonMain.dependencies { + implementation(project(":commonstatemachine")) + implementation(project(":coroutines")) + implementation(project(":commonflow:commonflow-data")) + implementation(project(":commonflow:commonflow-compose")) + implementation(libs.kotlin.coroutines.core) + implementation(libs.composeMultiplatform.runtime) + implementation(libs.composeMultiplatform.foundation) + } + } +} + diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthDataApi.kt b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api/AuthDataApi.kt similarity index 100% rename from skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthDataApi.kt rename to examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api/AuthDataApi.kt diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthGesture.kt b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api/AuthGesture.kt similarity index 100% rename from skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthGesture.kt rename to examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api/AuthGesture.kt diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthInput.kt b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api/AuthInput.kt similarity index 100% rename from skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthInput.kt rename to examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api/AuthInput.kt diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthResult.kt b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api/AuthResult.kt similarity index 74% rename from skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthResult.kt rename to examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api/AuthResult.kt index 1894a93..a1fffd3 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthResult.kt +++ b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api/AuthResult.kt @@ -4,4 +4,4 @@ package com.motorro.statemachine.auth.api * Feature flow result * @param authenticated Example of a property returned to a parent flow */ -data class AuthInput(val authenticated: Boolean) \ No newline at end of file +data class AuthResult(val authenticated: Boolean) \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthUiApi.kt b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api/AuthUiApi.kt similarity index 100% rename from skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthUiApi.kt rename to examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api/AuthUiApi.kt diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthUiState.kt b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api/AuthUiState.kt similarity index 88% rename from skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthUiState.kt rename to examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api/AuthUiState.kt index 42ad31d..5d8aded 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthUiState.kt +++ b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api/AuthUiState.kt @@ -8,5 +8,5 @@ interface AuthUiState { /** * Some common property available to a feature consumer */ - val modal: Boolean + val interactive: Boolean } \ No newline at end of file diff --git a/examples/skills/auth/implementation/build.gradle.kts b/examples/skills/auth/implementation/build.gradle.kts new file mode 100644 index 0000000..ea6d163 --- /dev/null +++ b/examples/skills/auth/implementation/build.gradle.kts @@ -0,0 +1,68 @@ +@file:Suppress("unused") + +/* +* Copyright 2023 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. +*/ +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.mockery) +} + +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 = "com.motorro" +version = versionName + +kotlin { + jvmToolchain(21) + + android { + namespace = "com.motorro.statemachine.auth.implementation" + compileSdk = androidCompileSdkVersion + minSdk = androidMinSdkVersion + + withHostTest { + isIncludeAndroidResources = true + } + + compilerOptions { + jvmTarget.set(JvmTarget.JVM_21) + } + } + + sourceSets { + commonMain.dependencies { + implementation(project(":commonstatemachine")) + implementation(project(":coroutines")) + implementation(project(":commonflow:commonflow-data")) + implementation(project(":commonflow:commonflow-compose")) + implementation(project(":examples:skills:auth:api")) + implementation(libs.kotlin.coroutines.core) + implementation(libs.composeMultiplatform.runtime) + implementation(libs.composeMultiplatform.foundation) + implementation(libs.napier) + } + commonTest.dependencies { + implementation(libs.test.kotlin) + implementation(libs.test.kotlin.coroutines) + } + } +} + diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/data/AuthGestureImpl.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/data/AuthGestureImpl.kt new file mode 100644 index 0000000..e1b4425 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/data/AuthGestureImpl.kt @@ -0,0 +1,33 @@ +package com.motorro.statemachine.auth.implementation.data + +import com.motorro.statemachine.auth.api.AuthGesture + +/** + * All gestures available in this feature + */ +internal sealed class AuthGestureImpl : AuthGesture { + /** + * Backwards navigation element clicked + */ + data object Back : AuthGestureImpl() + + /** + * Default call-to-action clicked + */ + data object Action : AuthGestureImpl() + + /** + * Gestures for the login form + */ + sealed class Form : AuthGestureImpl() { + /** + * Username input changed + */ + data class UsernameChanged(val value: String) : Form() + + /** + * Password input changed + */ + data class PasswordChanged(val value: String) : Form() + } +} \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/data/AuthStateData.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/data/AuthStateData.kt new file mode 100644 index 0000000..249a29e --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/data/AuthStateData.kt @@ -0,0 +1,22 @@ +package com.motorro.statemachine.auth.implementation.data + +import com.motorro.statemachine.auth.api.AuthInput + +/** + * Interstate data to pass between the machine states + * @property input Flow input from the flow initialization + * @property passwordRequirements Password requirements (preloaded later) + * @property username User input for the username is stored here + * @property password User input for the password is stored here + */ +internal data class AuthStateData( + val input: AuthInput, + val passwordRequirements: PasswordRequirements, + val username: String = "", + val password: String = "" +) + +/** + * Some utility function to check the form state + */ +internal fun AuthStateData.isValidToAuthenticate() = username.isNotBlank() && passwordRequirements.regex.matches(password) diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/data/AuthUiStateImpl.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/data/AuthUiStateImpl.kt new file mode 100644 index 0000000..c9a986e --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/data/AuthUiStateImpl.kt @@ -0,0 +1,32 @@ +package com.motorro.statemachine.auth.implementation.data + +import com.motorro.statemachine.auth.api.AuthUiState + +/** + * All gestures available in this feature + */ +internal sealed class AuthUiStateImpl : AuthUiState { + /** + * Some common property implementation + */ + override val interactive: Boolean = true + + /** + * Loading data + */ + data object Loading : AuthUiStateImpl() + + /** + * Login form + * @property username Username + * @property password Password + * @property loginEnabled If true, the form is complete and the user can log-in + */ + data class Form(val username: String, val password: String, val loginEnabled: Boolean) : AuthUiStateImpl() + + /** + * Displays error + * @param message Error message to display + */ + data class Error(val message: String) : AuthUiStateImpl() +} \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/data/PasswordRequirements.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/data/PasswordRequirements.kt new file mode 100644 index 0000000..c806319 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/data/PasswordRequirements.kt @@ -0,0 +1,11 @@ +package com.motorro.statemachine.auth.implementation.data + +/** + * Password requirements + * @property regex Regular expression to match the password + * @property description Description of the password requirements + */ +data class PasswordRequirements( + val regex: Regex, + val description: String +) \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/AuthContext.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/AuthContext.kt new file mode 100644 index 0000000..c98b66c --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/AuthContext.kt @@ -0,0 +1,25 @@ +package com.motorro.statemachine.auth.implementation.state + +import com.motorro.commonstatemachine.flow.data.CommonFlowHost +import com.motorro.statemachine.auth.api.AuthResult +import com.motorro.statemachine.auth.implementation.ui.AuthUiRenderer + +/** + * Common tools used by states + */ +internal interface AuthContext { + /** + * State-factory + */ + val factory: AuthStateFactory + + /** + * Flow-host + */ + val flowHost: CommonFlowHost + + /** + * UI-renderer + */ + val renderer: AuthUiRenderer +} \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/AuthState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/AuthState.kt new file mode 100644 index 0000000..0589199 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/AuthState.kt @@ -0,0 +1,10 @@ +package com.motorro.statemachine.auth.implementation.state + +import com.motorro.commonstatemachine.CommonMachineState +import com.motorro.statemachine.auth.implementation.data.AuthGestureImpl +import com.motorro.statemachine.auth.implementation.data.AuthUiStateImpl + +/** + * Convenient type-alias - bounds gesture and ui state system + */ +internal typealias AuthState = CommonMachineState \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/AuthStateFactory.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/AuthStateFactory.kt new file mode 100644 index 0000000..07b571d --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/AuthStateFactory.kt @@ -0,0 +1,46 @@ +package com.motorro.statemachine.auth.implementation.state + +import com.motorro.statemachine.auth.api.AuthInput +import com.motorro.statemachine.auth.api.AuthResult +import com.motorro.statemachine.auth.implementation.data.AuthStateData + +/** + * State-factory interface + * Abstracts the machine state from the state creation + */ +internal interface AuthStateFactory { + /** + * Creates a flow preloading state. + * The interstate data will be created by the state. + * @param input Initialization params + */ + fun preloading(input: AuthInput): AuthState + + /** + * Creates a preload error state. + * Again, there's no interstate data here as the preloading failed. + * @param input Initialization params + * @param error Error that occurred during the initialization + */ + fun preloadError(input: AuthInput, error: Throwable): AuthState + + /** + * Creates authentication form state. + * Now we have the interstate data, pass it as a parameter. + * @param data Interstate data + */ + fun form(data: AuthStateData): AuthState + + /** + * Creates the authentication state. + * @param data Interstate data + */ + fun authenticating(data: AuthStateData): AuthState + + /** + * Creates the termination state that finishes the flow. + * All interstate data is lost as not needed anymore. + * @param result Flow result + */ + fun terminated(result: AuthResult): AuthState +} \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/AuthStateFactoryImpl.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/AuthStateFactoryImpl.kt new file mode 100644 index 0000000..189b474 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/AuthStateFactoryImpl.kt @@ -0,0 +1,69 @@ +package com.motorro.statemachine.auth.implementation.state + +import com.motorro.commonstatemachine.flow.data.CommonFlowHost +import com.motorro.statemachine.auth.api.AuthInput +import com.motorro.statemachine.auth.api.AuthResult +import com.motorro.statemachine.auth.implementation.data.AuthStateData +import com.motorro.statemachine.auth.implementation.ui.AuthUiRenderer +import io.github.aakira.napier.Napier + +/** + * Main state-factory implementation + */ +internal class AuthStateFactoryImpl( + private val preloadingStateFactory: PreloadingState.Factory, + flowHost: CommonFlowHost, + renderer: AuthUiRenderer +) : AuthStateFactory { + + /** + * Context implementation + */ + private val context = object : AuthContext { + override val factory: AuthStateFactory = this@AuthStateFactoryImpl + override val flowHost: CommonFlowHost = flowHost + override val renderer: AuthUiRenderer = renderer + } + + /** + * Creates preloading state with help of a preloading state factory + */ + override fun preloading(input: AuthInput) = preloadingStateFactory.create( + context, + input + ) + + /** + * Creates preload error state directly + */ + override fun preloadError(input: AuthInput, error: Throwable) = PreloadingErrorState( + context, + input, + error + ) + + /** + * Creates authentication form state + */ + override fun form(data: AuthStateData) = FormState( + context, + data + ) + + /** + * Left for the future + */ + override fun authenticating(data: AuthStateData): AuthState { + TODO("Implement the authenticating state") + } + + /** + * Ad-hoc terminated state implementation + */ + override fun terminated(result: AuthResult) = object : AuthState() { + override fun doStart() { + Napier.d { "Terminating flow..." } + context.flowHost.onComplete(result) + } + } +} \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/BaseAuthState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/BaseAuthState.kt new file mode 100644 index 0000000..8658cd9 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/BaseAuthState.kt @@ -0,0 +1,34 @@ +package com.motorro.statemachine.auth.implementation.state + +import com.motorro.commonstatemachine.coroutines.CoroutineState +import com.motorro.statemachine.auth.implementation.data.AuthGestureImpl +import com.motorro.statemachine.auth.implementation.data.AuthUiStateImpl +import com.motorro.statemachine.auth.implementation.ui.AuthUiRenderer +import io.github.aakira.napier.Napier + +/** + * Base class for the most logic states + */ +internal abstract class BaseAuthState(context: AuthContext) : CoroutineState(), AuthContext by context { + + /** + * Common gesture processing (logs a warning for an unexpected gesture) + */ + override fun doProcess(gesture: AuthGestureImpl) { + Napier.w { "Unsupported gesture: $gesture" } + } + + /** + * Convenience function to switch a machine state + */ + protected inline fun setMachineState(block: AuthStateFactory.() -> AuthState) { + setMachineState(block(factory)) + } + + /** + * Convenience function to render UI state + */ + protected inline fun setUiState(block: AuthUiRenderer.() -> AuthUiStateImpl) { + setUiState(renderer.block()) + } +} \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/FormState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/FormState.kt new file mode 100644 index 0000000..c4691ed --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/FormState.kt @@ -0,0 +1,69 @@ +package com.motorro.statemachine.auth.implementation.state + +import com.motorro.statemachine.auth.api.AuthResult +import com.motorro.statemachine.auth.implementation.data.AuthGestureImpl +import com.motorro.statemachine.auth.implementation.data.AuthStateData +import com.motorro.statemachine.auth.implementation.data.isValidToAuthenticate +import io.github.aakira.napier.Napier +import org.jetbrains.annotations.VisibleForTesting + +/** + * Authentication form state - an interactive state that: + * - handles user input + * - updates the UI state + * - validates the input + * - updates the interstate data + * @param context Auth context + * @param data Interstate data. Visible for testing. + */ +internal class FormState( + context: AuthContext, + @VisibleForTesting var data: AuthStateData +) : BaseAuthState(context) { + + /** + * State start-up + */ + override fun doStart() { + setUiState { + renderForm(data, data.isValidToAuthenticate()) + } + } + + /** + * Updates the interstate data and renders the form + */ + private inline fun updateData(block: AuthStateData.() -> AuthStateData) { + val oldData = data + val newData = data.block() + if (oldData != newData) { + data = newData + setUiState { + renderForm(newData, newData.isValidToAuthenticate()) + } + } + } + + /** + * Processes the gesture + */ + override fun doProcess(gesture: AuthGestureImpl) { + when(gesture) { + is AuthGestureImpl.Back -> setMachineState { + terminated(AuthResult(false)) + } + is AuthGestureImpl.Action -> if (data.isValidToAuthenticate()) { + Napier.d { "Action gesture. Authenticating..." } + setMachineState { + authenticating(data) + } + } + is AuthGestureImpl.Form.UsernameChanged -> updateData { + copy(username = gesture.value) + } + is AuthGestureImpl.Form.PasswordChanged -> updateData { + copy(password = gesture.value) + } + } + } +} \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/PreloadingErrorState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/PreloadingErrorState.kt new file mode 100644 index 0000000..b165471 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/PreloadingErrorState.kt @@ -0,0 +1,48 @@ +package com.motorro.statemachine.auth.implementation.state + +import com.motorro.statemachine.auth.api.AuthInput +import com.motorro.statemachine.auth.api.AuthResult +import com.motorro.statemachine.auth.implementation.data.AuthGestureImpl +import io.github.aakira.napier.Napier + +/** + * Handles preloading error + * If the error is recoverable, allows to transfer back to loading to retry, otherwise terminates the flow. + * @param context Auth context + * @param error Error to handle + */ +internal class PreloadingErrorState( + context: AuthContext, + private val input: AuthInput, + private val error: Throwable +) : BaseAuthState(context) { + override fun doStart() { + renderer.renderFullScreenError(error, canRetry()) + } + + private fun canRetry(): Boolean { + // TODO: check the error and decide if it can be retried + return false + } + + override fun doProcess(gesture: AuthGestureImpl) { + when(gesture) { + is AuthGestureImpl.Back -> setMachineState { + Napier.d { "Back gesture. Terminating flow..." } + terminated(AuthResult(false)) + } + is AuthGestureImpl.Action -> if (canRetry()) { + Napier.d { "Action gesture. Retrying initialization..." } + setMachineState { + preloading(input) + } + } else { + Napier.d { "Action gesture. Terminating flow..." } + setMachineState { + terminated(AuthResult(false)) + } + } + else -> super.doProcess(gesture) + } + } +} \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/PreloadingState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/PreloadingState.kt new file mode 100644 index 0000000..36ed436 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/PreloadingState.kt @@ -0,0 +1,75 @@ +package com.motorro.statemachine.auth.implementation.state + +import com.motorro.statemachine.auth.api.AuthInput +import com.motorro.statemachine.auth.api.AuthResult +import com.motorro.statemachine.auth.implementation.data.AuthGestureImpl +import com.motorro.statemachine.auth.implementation.data.AuthStateData +import com.motorro.statemachine.auth.implementation.usecase.GetPasswordRequirements +import io.github.aakira.napier.Napier +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch + +/** + * Example of a state that preloads some data with the usecase + * and creates the valid [data-state](data-and-ui-implementation.md#step-3-interstate-data). + */ +internal class PreloadingState( + context: AuthContext, + private val input: AuthInput, + private val getPasswordRequirements: GetPasswordRequirements +) : BaseAuthState(context) { + + /** + * State start-up + */ + override fun doStart() { + setUiState { + renderLoading() + } + preload() + } + + /** + * Dummy gesture processing + */ + override fun doProcess(gesture: AuthGestureImpl) { + when(gesture) { + is AuthGestureImpl.Back -> setMachineState { + terminated(AuthResult(false)) + } + else -> super.doProcess(gesture) + } + } + + /** + * Dummy preloading function + */ + private fun preload() = stateScope.launch { + try { + val passwordRequirements = getPasswordRequirements() + // Advance to the next state + setMachineState { + form(AuthStateData(input, passwordRequirements)) + } + } catch (e: Throwable) { + ensureActive() + Napier.w(e) { "Error during initialization" } + // Advance to the error state + setMachineState { + preloadError(input, e) + } + } + } + + /** + * State factory + * State dependencies are injected here + */ + class Factory(private val getPasswordRequirements: GetPasswordRequirements) { + fun create(context: AuthContext, input: AuthInput): AuthState = PreloadingState( + context = context, + input = input, + getPasswordRequirements = getPasswordRequirements + ) + } +} \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/ui/AuthUiRenderer.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/ui/AuthUiRenderer.kt new file mode 100644 index 0000000..4b94cbe --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/ui/AuthUiRenderer.kt @@ -0,0 +1,32 @@ +package com.motorro.statemachine.auth.implementation.ui + +import com.motorro.statemachine.auth.implementation.data.AuthStateData +import com.motorro.statemachine.auth.implementation.data.AuthUiStateImpl + +/** + * Transforms the data state to the UI state + */ +internal interface AuthUiRenderer { + /** + * Renders loading. + * The result would be an object so we don't need interstate data here. + */ + fun renderLoading(): AuthUiStateImpl + + /** + * Renders login form. + * The data could be fully taken from the data state. + * @param data Data-state to transform + * @param isValid If the form is valid or not + */ + fun renderForm(data: AuthStateData, isValid: Boolean): AuthUiStateImpl + + /** + * Renders error that takes the full screen + * - doesn't require any data-state + * - requires an error and takes a message from it + * @param error Error to present to the user + * @param canRetry Error could be retried + */ + fun renderFullScreenError(error: Throwable, canRetry: Boolean): AuthUiStateImpl +} \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/usecase/GetPasswordRequirements.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/usecase/GetPasswordRequirements.kt new file mode 100644 index 0000000..03c7e83 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/usecase/GetPasswordRequirements.kt @@ -0,0 +1,13 @@ +package com.motorro.statemachine.auth.implementation.usecase + +import com.motorro.statemachine.auth.implementation.data.PasswordRequirements + +/** + * Loads password requirements + */ +interface GetPasswordRequirements { + /** + * Loads password requirements + */ + suspend operator fun invoke(): PasswordRequirements +} \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation/data/authGestures.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation/data/authGestures.kt new file mode 100644 index 0000000..d131f1a --- /dev/null +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation/data/authGestures.kt @@ -0,0 +1,11 @@ +package com.motorro.statemachine.auth.implementation.data + +/** + * Username input changed + */ +internal val USERNAME_CHANGED = AuthGestureImpl.Form.UsernameChanged("user") + +/** + * Password input changed + */ +internal val PASSWORD_CHANGED = AuthGestureImpl.Form.PasswordChanged("password") diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation/data/dataStates.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation/data/dataStates.kt new file mode 100644 index 0000000..cbbc75e --- /dev/null +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation/data/dataStates.kt @@ -0,0 +1,32 @@ +package com.motorro.statemachine.auth.implementation.data + +import com.motorro.statemachine.auth.api.AuthInput +import com.motorro.statemachine.auth.api.AuthResult + +/** + * Input fixture + */ +internal val INPUT = AuthInput("Please login!") + +/** + * Result fixture + */ +internal val RESULT = AuthResult(authenticated = true) + +internal val PASSWORD_REQUIREMENTS = PasswordRequirements( + regex = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,}\$".toRegex(), + description = "Minimum eight characters, at least one letter and one number" +) + +/** + * Initial state + */ +internal val EMPTY_STATE = AuthStateData(INPUT, PASSWORD_REQUIREMENTS) + +/** + * Valid form state + */ +internal val VALID_FORM_STATE = EMPTY_STATE.copy( + username = "user", + password = "password" +) \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation/data/uiStates.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation/data/uiStates.kt new file mode 100644 index 0000000..36333ed --- /dev/null +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation/data/uiStates.kt @@ -0,0 +1,6 @@ +package com.motorro.statemachine.auth.implementation.data + +/** + * Some UI-state to return from [com.motorro.statemachine.auth.implementation.ui.AuthUiRenderer] mock + */ +internal val UI_STATE = AuthUiStateImpl.Loading \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation/state/BaseStateTest.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation/state/BaseStateTest.kt new file mode 100644 index 0000000..92e38be --- /dev/null +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation/state/BaseStateTest.kt @@ -0,0 +1,63 @@ +package com.motorro.statemachine.auth.implementation.state + +import com.motorro.commonstatemachine.CommonStateMachine +import com.motorro.commonstatemachine.flow.data.CommonFlowHost +import com.motorro.statemachine.auth.api.AuthResult +import com.motorro.statemachine.auth.implementation.data.AuthGestureImpl +import com.motorro.statemachine.auth.implementation.data.AuthUiStateImpl +import com.motorro.statemachine.auth.implementation.ui.AuthUiRenderer +import dev.mokkery.MockMode +import dev.mokkery.mock +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 + +@OptIn(ExperimentalCoroutinesApi::class) +internal abstract class BaseStateTest { + protected lateinit var stateMachine: CommonStateMachine + protected lateinit var stateFactory: AuthStateFactory + protected lateinit var renderer: AuthUiRenderer + protected lateinit var context: AuthContext + protected lateinit var flowHost: CommonFlowHost + protected lateinit var nextState: AuthState + protected lateinit var dispatcher: TestDispatcher + + @BeforeTest + fun init() { + dispatcher = UnconfinedTestDispatcher() + Dispatchers.setMain(dispatcher) + + stateMachine = mock(mode = MockMode.autoUnit) + stateFactory = mock() + flowHost = mock(mode = MockMode.autoUnit) + renderer = mock(mode = MockMode.strict) + nextState = AuthState() + + context = object : AuthContext { + override val factory: AuthStateFactory = this@BaseStateTest.stateFactory + override val flowHost: CommonFlowHost = this@BaseStateTest.flowHost + override val renderer: AuthUiRenderer = this@BaseStateTest.renderer + } + + doInit() + } + + @AfterTest + fun deinit() { + Dispatchers.resetMain() + } + + protected fun test(block: TestScope.() -> Unit) = runTest( + dispatcher, + testBody = block + ) + + protected open fun doInit() = Unit +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c2117f6..7e3fb9a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,6 +23,7 @@ kotlinDatetime = "0.8.0" lifecycle = "2.11.0" nexusPublish = "2.0.0" timber = "5.0.1" +napier = "2.7.1" testAndroidxArch = "2.2.0" testAndroidxCore = "1.7.0" testAndroidxEspresso = "3.7.0" @@ -31,6 +32,7 @@ testJunit = "4.13.2" testMockk = "1.14.11" testMockkCommon = "1.13.5" robolectric = "4.16.1" +mokkery = "3.3.0" [plugins] compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } @@ -44,6 +46,7 @@ android_kotlin_multiplatform_library = { id = "com.android.kotlin.multiplatform. kotlin_dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } google-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } +mockery = { id = "dev.mokkery", version.ref = "mokkery" } git = { id = "org.ajoberstar.grgit", version.ref = "git" } nexus_publish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "nexusPublish" } @@ -53,6 +56,8 @@ kotlin-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-co kotlin-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } kotlin-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinDatetime" } +napier = { module = "io.github.aakira:napier", version.ref = "napier" } + desugaring = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugaring" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppcompat" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 734d5d4..02f6636 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -72,7 +72,9 @@ include( ":examples:books:domain", ":examples:books:book", ":examples:books:book:demo", - ":examples:books:app" + ":examples:books:app", + ":examples:skills:auth:api", + ":examples:skills:auth:implementation" ) diff --git a/skills/README.md b/skills/README.md new file mode 100644 index 0000000..96ead4c --- /dev/null +++ b/skills/README.md @@ -0,0 +1,10 @@ +# AI skills +Some AI skills for your convenience: + +- [Create a Common Child Flow module boilerplate](commonstatemachine/common-child-flow/boilerplate/SKILL.md): creates a set of API/implementation + feature module boilerplate following the [Common Child Flow](../README.md#common-child-flow-api). + - Defines gesture/ui-states sealed structures + - Binds data and UI APIs + - Builds a basic tool set for the module implementation + - Creates a test structure and mocks for the module implementation + - Builds a DI container for the module implementation \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md b/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md index 10f770b..5ae64e4 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md @@ -35,14 +35,21 @@ You will create a set of gradle modules that together form a feature: Take a look at the [class diagram](assets/common-child-flow.puml). It contains an overview between all the interfaces, classes and objects in a setup using the Authentication module example. +Follow the following common rules when writing code: + +- Follow the project coding conventions or use Kotlin default +- Follow the project naming conventions or use Kotlin default +- If the project uses KDoc as a coding convention - add it to the created types, methods, properties +- Add TODO comments in dummy classes and functions to point the user to provide the actual implementation + ## Step 1. Module directory structure creation Create the following module structure if not instructed otherwise: ``` -my-project/ # Project folder. Common package: com.motorro.statemachine -├── app # Example: an application module, package: com.motorro.statemachine.app -├── auth/ # Required: a base directory for a new feature (authentication) -│ ├── api/ # Required: an API gradle module that describes the feature, package: com.motorro.statemachine.auth.api -│ └── implementation/ # Required: an implementation gradle module that implements the API, package: com.motorro.statemachine.auth.implementation +my-project # Project folder. Common package: com.motorro.statemachine +├── app # Example: an application module, package: com.motorro.statemachine.app +├── auth # Required: a base directory for a new feature (authentication) +│ ├── api # Required: an API gradle module that describes the feature, package: com.motorro.statemachine.auth.api +│ └── implementation # Required: an implementation gradle module that implements the API, package: com.motorro.statemachine.auth.implementation └── ... ``` @@ -53,6 +60,9 @@ my-project/ # Project folder. Common package: com.motorro.statemachin - Common data API (state machine child flow): "com.motorro.commonstatemachine:commonflow-data:x.x.x" - Common UI API (Compose API if used): "com.motorro.commonstatemachine:commonflow-compose:x.x.x" - Include test dependencies that are used across the project and coroutines test dependencies if used. +- Include DI dependencies that are used across the project. +- Include Logging dependencies that are used across the project. +- Include Compose native or multiplatform (depending on the project setup or the user choice) dependencies to the implementation modules. - If the user asks you to create some specific implementation or several implementations of the same API, infer the names and create several modules. For example, the user may ask you to create two authentication implementations of the same Authentication API: @@ -60,6 +70,7 @@ my-project/ # Project folder. Common package: com.motorro.statemachin - `social` - and implementation for the social network authentication If so - create several implementation modules, use the same API module as a base for them. +- Include `api` as a dependency in the implementation modules. ## Step 2. Create an API module code Create a package for the feature API. By default, use the common application package adding the feature name and `api`. @@ -70,3 +81,30 @@ Then follow these guides for the API module implementation: 2. [Feature-flow input and result API](references/input-and-result-api.md) 3. [Feature-flow data and UI API](references/data-and-ui-api.md) +## Step 3. Create implementation module +Here you will create an API implementation. +If several implementations are requested by user (step 1) - repeat for each implementation. +Create a package for the feature implementation. By default, use the common application package adding the feature name and `implementation`. +If several implementations are requested by user (step 1) - name according to the implementation name. +Then follow these guides for the module implementation: + +1. [Create gestures, ui-states, data-state](references/data-and-ui-implementation.md). +2. [Create common-state binding, ui-renderer, state factory and context interfaces](references/renderer-factory-context-interface-implementation.md). +3. [Create basic test fixtures for the data-classes created in the previous state](references/test-fixtures-implementation.md). +4. [Create an abstract base state](references/renderer-factory-context-interface-implementation.md). +5. [Create a base abstract unit-test class](references/renderer-factory-context-interface-implementation.md). + +## Step 4. Create the machine states +For the user to kick-off faster, we will create some states: + +- If the user gave you the instructions about what would be the state-flow, or what states does he need - create the states for him. + Implement the required functionality or place TODOs in place of the code if it is not clear what to do exactly. + Don't hesitate to ask the user to clarify what he needs. +- If the user didn't give you any specifics, create a classic LCE example: + - create a dummy preloading state + - create a dummy content state + - create a dummy error state + +Refer to [this manual](references/state-implementation.md) to get the idea of the state mechanics. +Repeat this step for every state you need to create. + diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/common-child-flow.puml b/skills/commonstatemachine/common-child-flow/boilerplate/assets/common-child-flow.puml index 6cc67c4..5d2802d 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/assets/common-child-flow.puml +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/common-child-flow.puml @@ -122,8 +122,9 @@ package Implementation { end note interface AuthStateFactory { - +loading(): AuthState + +startup(input: AuthInput): AuthState +form(data: AuthStateData): AuthState + +terminated(result: AuthResult): AuthState } AuthStateFactory -> AuthState: creates note left @@ -155,6 +156,7 @@ package Implementation { interface AuthUiRenderer { +renderLoading(): AuthUiState +renderForm(data: AuthStateData): AuthUiState + +renderError(error: Throwable): AuthUiState } note top Transforms data-state to AuthUiState. diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api new file mode 120000 index 0000000..4fb48e8 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api @@ -0,0 +1 @@ +../../../../../../examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation new file mode 120000 index 0000000..e6da7f0 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation @@ -0,0 +1 @@ +../../../../../../examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-test b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-test new file mode 120000 index 0000000..e352e97 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-test @@ -0,0 +1 @@ +../../../../../../examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/base-state-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/base-state-implementation.md new file mode 100644 index 0000000..6fbf9f1 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/base-state-implementation.md @@ -0,0 +1,39 @@ +# Module implementation: base state +Use this reference to implement a common base state for the flow. +Take a look at the [class diagram](../assets/common-child-flow.puml) to get the idea about components purpose. +Take a look at the [example](../assets/example/implementation/state/BaseAuthState.kt). + +## Step 1. Create a base class +Create a base abstract class that would be used as a common ancestor for the most of the logical states. +Follow the following common rules: + +- Make the class internal to the implementation module. +- The name should start with `Base` prefix. +- The class should be abstract +- The class should extend the `com.motorro.commonstatemachine.coroutines.CoroutineState` +- The class should bind the generic parameters for the [gesture](data-and-ui-implementation.md#step-1-ui-gestures) and [ui-state](data-and-ui-implementation.md#step-2-ui-states) types. +- The class should take the [context](renderer-factory-context-interface-implementation.md#step-4-state-context) parameter and implement the context by delegate. + +## Step 2. Add convenience methods to change the machine and UI states +The `com.motorro.commonstatemachine.CommonMachineState` subclass is a part of a state-machine. +It updates the outside world with the following methods: + +- `setUiState` - updates the current UI state, thus the screen to the user. +- `setMachineState` - switches the logical state in a hosting state-machine. + +To reduce the boilerplate, create two protected inline convenience methods: + +- `setMachineState(block: AuthStateFactory.() -> AuthState)` - runs the block on the factory and changes the machine state. +- `setUiState(block: AuthUiRenderer.() -> AuthUiStateImpl)` - runs the renderer and updates the UI state + +Take a look at the [example](../assets/example/implementation/state/BaseAuthState.kt) for the implementation. + +## Step 3. Basic gesture processing +The `com.motorro.commonstatemachine.CommonMachineState` processes the user gestures in the `doProcess` method. +The concrete state usually only processes those gestures he is interested in. +Example: the login form state is interested only in gestures that are relevant to login actions, it does not process +gestures of the logout screen. +If the project uses the logging framework, add a simple `doProcess` override to the base class: if the state gets the +unexpected gesture - log a warning message using the logging framework. + +Take a look at the [example](../assets/example/implementation/state/BaseAuthState.kt) for the implementation. diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/base-test-class-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/base-test-class-implementation.md new file mode 100644 index 0000000..d92dd85 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/base-test-class-implementation.md @@ -0,0 +1,44 @@ +# Module implementation: base test class +Use this reference to get the idea snd create a base abstract uni-test class. +This class will handle common test-mocks and initialization for the most of machine-states unit-tests: + +- state-machine mock +- factory mock +- renderer mock +- context mock + +Most of the machine-state test will subclass this base class. + +## Step 1. Base test class +Create an abstract internal class and call `BaseStateTest`. + +## Step 2. Protected properties +Create the following protected properties to use in child test-classes: + +- `stateMachine` - `com.motorro.commonstatemachine.CommonStateMachine` mock bound with [implementation gesture and ui-state types](data-and-ui-implementation.md) +- `stateFactory` - [machine state factory mock](renderer-factory-context-interface-implementation.md#step-2-state-factory-interface) +- `renderer` - [ui state renderer mock](renderer-factory-context-interface-implementation.md#step-4-ui-renderer-interface) +- `flowHost` - `com.motorro.commonstatemachine.flow.data.CommonFlowHost` mock bound with the return type +- `context` - [interstate context mock](renderer-factory-context-interface-implementation.md#step-4-state-context) +- `nextState` - a bound `com.motorro.commonstatemachine.CommonMachineState` type to use with the state-factory mock. +- `dispatcher` - a coroutine `TestDispatcher` that will be used in test functions. + +## Step 3. Mock initialization +You will need to initialize all the mocks listed in the previous state and mock some of the functions there: + +1. Create the `init` function and mark it with the annotation to run before the test run. +2. Initialize `dispatcher` with the `UnconfinedTestDispatcher` if not instructed otherwise. +3. Initialize coroutines `Main` dispatcher with the test dispatcher from the previous step. +4. Initialize the `stateMachine` with the relaxed mock. +5. Initialize the `stateFactory` with the strict mock. +6. Initialize the `flowHost` with the relaxed mock. +7. Initialize the `renderer` with the strict mock. +8. Initialize the `nextState` with the bound type alias. +9. Initialize the `context` with the object and override the properties with the mocks from the previous steps. +10. Create the empty open fun `doInit` and call it from the `init` function. Subclasses will override `doInit` + and initialize themselves with the mocks already created. +11. Create `deinit` function and call `Dispatchers.resetMain()` there. +12. Create the common coroutine test function wrapping `runTest` and using the test dispatcher from the second step. + Subclasses will use this function to run the tests. + +Take a look at the [example](../assets/example/implementation-test/state/BaseStateTest.kt) of such a class. diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-api.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-api.md index 0bf9534..1530109 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-api.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-api.md @@ -1,4 +1,4 @@ -# Module data and UI API +# Module API: data and UI API Use this reference to create the required data and UI APIs. ## Mechanics @@ -11,14 +11,14 @@ The [Common Child Flow](https://github.com/motorro/CommonStateMachine/tree/maste This interface binds gesture-system and ui-system with the composable responsible for the view rendering. The API implementation is then created in the implementation module and could be injected into the composition using locals or any other way. -## Data flow +## Step 1. Data flow Take the created [Gestures and UI-states](gesture-and-ui-state-api.md), [input and result](data-and-ui-api.md) and create an interface or a typealias -to bind the generics of the `CommonFlowDataApi`. This API would be available to the flow consumer to start the feature flow. +to bind the generics of the `com.motorro.commonstatemachine.flow.data.CommonFlowDataApi`. This API would be available to the flow consumer to start the feature flow. Take a look at the [class diagram](../assets/common-child-flow.puml) for more details. Take a look at the [example](../assets/example/api/AuthDataApi.kt) - data API definition -## UI flow +## Step 2. UI flow Take the created [Gestures and UI-states](gesture-and-ui-state-api.md) and create an interface or a typealias -to bind the generics of the `CommonFlowUiApi`. This API would be available to the flow consumer to display the feature. +to bind the generics of the `com.motorro.commonstatemachine.flow.compose.CommonFlowUiApi`. This API would be available to the flow consumer to display the feature. Take a look at the [class diagram](../assets/common-child-flow.puml) for more details. Take a look at the [example](../assets/example/api/AuthUiApi.kt) - UI API definition diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-implementation.md new file mode 100644 index 0000000..36927fd --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-implementation.md @@ -0,0 +1,73 @@ +# Module implementation: data classes +Use this reference to create data classes for the feature implementation. +Take a look at the [class diagram](../assets/common-child-flow.puml) to get the idea about the data types within the module. + +## Step 1. UI gestures +Create the sealed class to enumerate all the UI events (gestures) the user could send to the model: + +- button presses +- navigation elements clicks +- input changes + +The created sealed class should implement the base Gesture type defined in the API module. +See the [corresponding instruction](gesture-and-ui-state-api.md#step-1-gestures). +Put the created class to the `data` sub-package of the implementation module. + +Follow these common rules: + +- Make the created structure internal. +- Create data objects for gestures that don't carry any data. Example: user clicks the Back button. +- Create data classes for gestures that carry some data. Example: user changes an input text and the new value is passed with the event.## UI gestures. +- Always include the data object for the backwards navigation gesture: `Back`. +- If user didn't give you any information on the input gestures he wants, `Back` would be the only child object in a structure. +- If user gives you a picture of a user interface - inspect it and create gestures for every screen. +- If the feature includes multiple screens - group the specific gestures by screen. +- The types of the properties of the ui-gestures should be immutable! + +Take a look at the [example](../assets/example/implementation/data/AuthGestureImpl.kt) - gesture collection. + +## Step 2. UI states +Create the sealed class to enumerate all the UI states the interface could display. + +The created sealed class should implement the base UI-state defined in the API module. +See the [corresponding instruction](gesture-and-ui-state-api.md#step-2-ui-states). +Put the created class to the `data` sub-package of the implementation module. + +Follow these common rules: + +- Make the created structure internal. +- Create data objects for ui-states that don't carry any data. Example: loading screen without any text messages. +- Create data classes for ui-states that carry some data. Example: login/password entry form. +- If user gives you a picture of a user interface - inspect it and create the data state for every dynamic UI element. +- If the feature includes multiple screens - group the specific ui-states by screen. +- The types of the properties of the ui-states should be immutable! Use `@Immutable` annotation for non-stable types to help Compose compiler optimize recompositions. + If immutable collection library is used in a project - use them for the collections in UI-states. + +Take a look at the [example](../assets/example/implementation/data/AuthUiStateImpl.kt) - UI-state collection. + +## Step 3. Interstate data +Create a data class to store the flow data passed between the machine states. +Follow the user's instructions to create a data class properties. +Put the created class to the `data` sub-package of the implementation module. + +Follow these common rules: + +- Make the created data class internal. +- The types of the properties of the data-state should be immutable! +- Include the flow startup input as a property. +- Set default values for the properties if user provides the information or try to guess them. + +Take a look at the [example](../assets/example/implementation/data/AuthStateData.kt) - interstate data for password authentication. + +## Step 4. Test fixtures +Create some fixtures and test data for the created data classes: + +1. Create a `data` sub-package in the test source set. +2. Create a fixture file for data-class gestures. +3. Create a fixture file for data-class ui-states. +4. Create a fixture file for data-class interstate data. + For the interstate data, scan the project for already defined fixtures available at the class path and + prefer using them instead of creating your own fixtures + + +Take a look at the [example](../assets/example/implementation/data/AuthStateData.kt) - interstate data for password authentication. diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/gesture-and-ui-state-api.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/gesture-and-ui-state-api.md index 35857a8..c665d61 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/gesture-and-ui-state-api.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/gesture-and-ui-state-api.md @@ -1,4 +1,4 @@ -# Gesture and UI-state API +# Module API: Gesture and UI-state Use this reference to create the UI gesture and UI state API. ## Mechanics @@ -7,14 +7,14 @@ The framework behind is a Model-View-Intent architecture, so: - The state is updated as a result of user intents (gestures) - The view-state is fully described by data classes/objects - ui-states -## Gestures +## Step 1. Gestures The implementation is hidden from the module consumer using the internal gesture structures. To use the feature we export the basic marker interface that may only contain some common public properties. This interface would be implemented later in the implementation module. Take a look at the [class diagram](../assets/common-child-flow.puml) for more details. Take a look at the [example](../assets/example/api/AuthGesture.kt) - common API definition -## UI-states +## Step 2. UI-states The implementation is hidden from the module consumer using the internal ui-state structures. To use the feature we export the basic marker interface that may only contain some common public properties. This interface would be implemented later in the implementation module. diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/input-and-result-api.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/input-and-result-api.md index 3294c25..1f655b7 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/input-and-result-api.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/input-and-result-api.md @@ -1,4 +1,4 @@ -# Module initialization data and flow result +# Module API: Module initialization data and flow result Use this reference to create the required input data and define the flow result. ## Mechanics @@ -9,13 +9,13 @@ Example: the main application flow could run a child flow in-place to log the us - The child flow could be initialized by some data passed from parent. - The child flow could return some data back to the parent on completion. -## Input +## Step 1. Input If the user asks to provide any input - create an immutable type - a data class or a sealed structure. If no input type explicitly required - confirm and use `Unit` Take a look at the [class diagram](../assets/common-child-flow.puml) for more details. Take a look at the [example](../assets/example/api/AuthInput.kt) - common API definition -## Result +## Step 2. Result If the user asks to provide a flow result - create an immutable type - a data class or a sealed structure. If no result type explicitly required - confirm and use `Unit` Take a look at the [class diagram](../assets/common-child-flow.puml) for more details. diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/renderer-factory-context-interface-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/renderer-factory-context-interface-implementation.md new file mode 100644 index 0000000..ee62c7e --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/renderer-factory-context-interface-implementation.md @@ -0,0 +1,85 @@ +# Module implementation: factory interface, ui-renderer interface, context +Use this reference to create auxiliary interfaces and classes: + +- state factory +- UI-state renderer +- Interstate context + +Take a look at the [class diagram](../assets/common-child-flow.puml) to get the idea about components purpose. + +## Step 1. CommonMachineState binding +The framework main building block is a `com.motorro.commonstatemachine.CommonMachineState` - it is a logical state of a state-machine running the flow. +The generic class should be bound with two parameters: + +- [Gesture](data-and-ui-implementation.md#step-1-ui-gestures) +- [UI-state](data-and-ui-implementation.md#step-2-ui-states) + +Create an internal type-alias and put it to the `state` sub-package. +Take a look at the [example](../assets/example/implementation/state/AuthState.kt). + +## Step 2. State factory interface +Whenever it is a time to switch to another state, the current state does not create the new state directly - it delegates +the state creation to the state-factory: + +- State-factory foundation is an interface with the methods like: `createLoading`, `createForm`, etc. +- Factory functions may pass some parameters: [interstate data](data-and-ui-implementation.md#step-3-interstate-data), errors, auxiliary data. +- Machine-states get the reference to the state factory and use it to create the new state. +- State-factory interface is internal in the module. + +Your task here is to: + +- Create a state-factory internal interface +- Put the interface `state` sub-package of the implementation module. +- Every factory function should return the `CommonMachineState` binding type created in the previous step. +- Create some startup function to build the initializing state. The method should include the [input](input-and-result-api.md#step-1-input) as a parameter. +- Create `terminated` method with the [result](input-and-result-api.md#step-2-result) as a parameter. + +Take a look at the [example](../assets/example/implementation/state/AuthState.kt). + +## Step 4. UI renderer interface +States are encouraged to use a separate renderer interface that translates the [interstate data](data-and-ui-implementation.md#step-3-interstate-data) +to the [UI state](data-and-ui-implementation.md#step-2-ui-states). + +Follow these common rules: +- Make the created interface internal. +- Put the interface into the `ui` sub-package of the implementation module. +- Create a function for every UI-state in a sealed system. +- Every function should return the sealed class ui-state type. +- Provide an [interstate data](data-and-ui-implementation.md#step-3-interstate-data) parameter if the result state is a data class. +- Provide more parameters if data is required to build the ui-state, and it could not be built using the data-state. + Example: when creating some error view-state, add an error parameter. The calling state will provide it separately. + +Take a look at the [example](../assets/example/implementation/ui/AuthUiRenderer.kt). + +## Step 4. State context +To make the tools created above accessible, create an accessor interface - a context. +The state being created will receive that context in a constructor and use it to access factory, renderer, etc. + +Follow these common rules: +- Make the created interface internal. +- Put the interface into the `state` sub-package of the implementation module. +- Add properties resolving to the following tools: + - state factory + - ui-renderer + - flow host bound with the [result type](input-and-result-api.md#step-2-result) used to interact with the parent state. + +Take a look at the [example](../assets/example/implementation/state/AuthContext.kt). + +## Step 5. State factory implementation +Add an implementation class to implement the factory interface from the previous step. +The implementation will: + +- Receive state-factories in constructor and use them to create the states with complex dependencies (see the `preloading` implementation in the [example](../assets/example/implementation/state/AuthStateFactoryImpl.kt)). +- Create simple states locally (see the `preloadError` implementation in the [example](../assets/example/implementation/state/AuthStateFactoryImpl.kt)) +- For the even simpler states you may want to implement them ad-hoc (see the `terminated` implementation in the [example](../assets/example/implementation/state/AuthStateFactoryImpl.kt)). + +Follow these common rules: + +- Create the class in the implementation module. +- If required by the DI framework used in project, add required inject annotations. +- Inject external factories and dependencies required by the state to the factory constructor. + Try to use lazy provider injection to delay the initialization. +- Pass the flow host to the factory constructor. +- Build the private context object to pass to the child states. + +See the [example](../assets/example/implementation/state/AuthStateFactoryImpl.kt). diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/state-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/state-implementation.md new file mode 100644 index 0000000..c6e775e --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/state-implementation.md @@ -0,0 +1,102 @@ +# Module implementation: state implementation +Use this reference to implement the machine-state for the `CommonStateMachine` framework. +Take a look at the [class diagram](../assets/common-child-flow.puml) to get the idea about components purpose. +Take a look at the [example](../assets/example/implementation/state/PreloadingState.kt). + +## Mechanics +Here's how the machine-state works: + +- All machine states subclass the basic `com.motorro.commonstatemachine.CommonMachineState` (or its child) bound with the gesture and UI-state types + as described in the basic [interface implementation](renderer-factory-context-interface-implementation.md#step-1-commonmachinestate-binding) + and [base state implementation](base-state-implementation.md) manuals. +- It'd be great if a state has a single responsibility whenever possible and limit the number of gestures it processes. + For example, the document loading could be divided into the following states: + - `LoadingState` - runs the usecase and updates the interstate data. + - `ContentState` - displays loaded data and interacts with a user. + - `ErrorState` - displays an error and if possible, offers to retry by transferring back to `LoadingState`. +- The state has the following callbacks which are called by the hosting state machine: + - `doStart` - called when the state is started. + - `doProcess` - called when the state receives a gesture. + - `doClear` - called when the state is removed from the state-machine. +- The state could call the following functions to interact with the hosting state machine: + - `setMachineState` - called to switch to another state. We tipically use the factory to create the new state. + - `setUiState` - called to update the UI state. We tipically use the renderer to update the UI. +- It is important that `setMachineState` and `setUiState` are called from when state is active (between calls to `doStart` and `doClear`). + If the call is made from the inactive state - it will fail. +- Always postpone the heavy initialization till `doStart` is called. + +## Step 1. Create a class +Put the class to the `state` sub-package of the implementation module. +Follow the following common rules: + +- Make the class internal to the implementation module. +- Subclass the [base state](base-state-implementation.md). +- Pass the [context](renderer-factory-context-interface-implementation.md#step-4-state-context) as a first parameter. +- Pass the [input data](input-and-result-api.md#step-1-input) as a parameter. + +## Step 2. Add a startup implementation (or a dummy) +When the `com.motorro.commonstatemachine.CommonMachineState` is started, the state-machine calls the `doStart` lifecycle method. +Implement the method to update the UI, start loading data, etc. +The `com.motorro.commonstatemachine.coroutines.CoroutineState`, which is an ancestor of the [abstract base state](base-state-implementation.md), has a +bound coroutine scope property: `stateScope`. Use it to run coroutines. The scope would be canceled when the state is removed from the state-machine. + +## Step 3. Add a gesture processing implementation +When the user interface emits a gesture, the state-machine calls the `onGesture` method of a current `com.motorro.commonstatemachine.CommonMachineState`. +Implement the override of a function that: + +- Will process the gestures specific to this state. +- Will call the super-method on every other unexpected gesture which are out of scope of this state. + +## Step 4. Implement a local state factory +To increase the visibility and to decrease the boilerplate, the states that require external dependencies (use-cases, managers, APIs, etc.) have their +own state factories put to the DI graph. This lifts a burden of state creation micromanagement away from +the main state factory implementation. +Create such a factory for the startup state: + +- Implement as a child class with the name `Factory`. +- If required by the DI framework used in project, add required inject annotations. +- Inject external dependencies required by the state to the factory constructor. +- Add a single `create` method that will accept the [context](renderer-factory-context-interface-implementation.md#step-4-state-context) and all the required interstate parameters that will come from the main factory. + +## Step 5. Create a method in the main state-factory interface +The states are to be created by the [main state-factory](renderer-factory-context-interface-implementation.md#step-2-state-factory-interface) so the states +do not have hard links to one each-other. +When you create a new state, you need to create a method in the main state-factory for it: + +- Name the function to indicate what happens inside the state. Examples: `preloading`, `error`, etc. +- Pass the common interstate if needed as a data parameter (see the `form` function in the [example](../assets/example/implementation/state/AuthStateFactory.kt)). +- Pass the specific parameters - those needed by this particular state only (see the `preload` function in the [example](../assets/example/implementation/state/AuthStateFactory.kt)). +- Implement the method in the [implementation class](renderer-factory-context-interface-implementation.md#step-5-state-factory-implementation). + +## Example 1. Preloading state +For example of a state that preloads some data with the usecase, take a look at the [PreloadingState](../assets/example/implementation/state/PreloadingState.kt) class. +The state does the following when started: + +- Sets the `Loading` UI state in `doStart` method. +- Calls the `preload` function that: + - calls a usecase to preload data some data or emulates loading if the usecase is not specified, and we are doing a dummy preloading. + - creates the valid [data-state](data-and-ui-implementation.md#step-3-interstate-data). + - switches to the next state by creating it with a factory and calling the `setMachineState` function. +- Implements the `doProcess` function to handle the back gesture or any other gestures that are relevant to this state. +- Does moderate debug-logging in lifecycle methods to track the flow. + +## Example 2. Preloading error state +If any operation fails, you'd better switch the state-machine to an error state. See [PreloadingErrorState](../assets/example/implementation/state/PreloadingErrorState.kt) +state to handle the preloading error. +The typical error state would: + +- Set the `Error` UI state in `doStart` method passing the error to display +- Handle the back gesture that would terminate the flow or return to a previous state if relevant keeping the variables needed. +- May switch back to the loading state if the error is considered recoverable. +- States of that kind are usually rather simple so you don't need any state-factory. + +### Example 3. Form state +For example of a state that handles the user input, validates and prepares the data, take a look at the [FormState](../assets/example/implementation/state/FormState.kt) class. +The state does the following when started: + +- Updates the UI state with the initial data. +- Listens to the gestures, changes the interstate data and updates the screen in the `doProcess` method. +- Changes the machine state when the action gesture is received. +- Validates the data + + diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/test-fixtures-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/test-fixtures-implementation.md new file mode 100644 index 0000000..1bd0cfc --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/test-fixtures-implementation.md @@ -0,0 +1,15 @@ +# Module implementation: test fixtures +Use this reference to create test fixtures for the [data classes of the feature implementation](data-and-ui-implementation.md): + +1. Create a `data` sub-package in the test source set. +2. Create a fixture file for data-class gestures. +3. Create some ui-state fixture to use when mocking the [renderer](renderer-factory-context-interface-implementation.md#step-4-ui-renderer-interface). +4. Create a fixture file for data-class interstate data, input and result data classes. + +For the fixture data, scan the project for already defined fixtures available and prefer using them instead of creating +your own fixtures. For example, look for the modules like `domain` or `entity` and use their fixtures. Check existing feature +modules for the clue how to create fixtures and where to get the common fixture module. + +Take a look at the [example](../assets/example/implementation-test/data/authGestures.kt) gesture fixtures. +Take a look at the [example](../assets/example/implementation-test/data/uiStates.kt) ui-state fixtures. +Take a look at the [example](../assets/example/implementation-test/data/dataStates.kt) interstate. From 382cacb87df286c0a38dcfd89567a8aa707b3223 Mon Sep 17 00:00:00 2001 From: motorro Date: Tue, 15 Sep 2026 19:08:34 +0200 Subject: [PATCH 03/32] Skills building script Refs: #14 --- .github/workflows/skills.yaml | 26 +++++++++++++++++++ .gitignore | 1 + build_skills.sh | 26 +++++++++++++++++++ .../boilerplate/assets/example/api | 0 .../boilerplate/assets/example/implementation | 0 .../assets/example/implementation-test | 0 6 files changed, 53 insertions(+) create mode 100644 .github/workflows/skills.yaml create mode 100644 build_skills.sh mode change 120000 => 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api mode change 120000 => 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation mode change 120000 => 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-test diff --git a/.github/workflows/skills.yaml b/.github/workflows/skills.yaml new file mode 100644 index 0000000..f962665 --- /dev/null +++ b/.github/workflows/skills.yaml @@ -0,0 +1,26 @@ +name: Skills + +on: + release: + types: [published] + +jobs: + publish: + name: Build and upload skills + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: refs/remotes/origin/master + - name: Build skills + run: ./build_skills.sh artifacts + - name: Upload skills to release + uses: softprops/action-gh-release@v2 + with: + files: artifacts/*.zip + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index cdbd6ea..419645d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /.idea .DS_Store /build +/artifacts /.kotlin build /captures diff --git a/build_skills.sh b/build_skills.sh new file mode 100644 index 0000000..e5136fa --- /dev/null +++ b/build_skills.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -e + +# Directory to store zip files, defaults to "artifacts" if not provided +ARTIFACTS_DIR="${1:-$(pwd)/artifacts}" + +echo "Building skills to $ARTIFACTS_DIR..." +mkdir -p "$ARTIFACTS_DIR" + +# Ensure ARTIFACTS_DIR is an absolute path +if [[ ! "$ARTIFACTS_DIR" = /* ]]; then + ARTIFACTS_DIR="$(pwd)/$ARTIFACTS_DIR" +fi + +find skills -name "SKILL.md" | while read -r skill_file; do + skill_dir=$(dirname "$skill_file") + # Get relative path from skills directory + rel_path=${skill_dir#skills/} + # Replace slashes with underscores + zip_name="skill_${rel_path//\//_}.zip" + echo "Zipping $skill_dir to $zip_name" + + (cd "$skill_dir" && zip -r "$ARTIFACTS_DIR/$zip_name" . -x "*.DS_Store") +done + +echo "Skills build complete." diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api deleted file mode 120000 index 4fb48e8..0000000 --- a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api +++ /dev/null @@ -1 +0,0 @@ -../../../../../../examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api new file mode 100644 index 0000000..4fb48e8 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api @@ -0,0 +1 @@ +../../../../../../examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation deleted file mode 120000 index e6da7f0..0000000 --- a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation +++ /dev/null @@ -1 +0,0 @@ -../../../../../../examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation new file mode 100644 index 0000000..e6da7f0 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation @@ -0,0 +1 @@ +../../../../../../examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-test b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-test deleted file mode 120000 index e352e97..0000000 --- a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-test +++ /dev/null @@ -1 +0,0 @@ -../../../../../../examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-test b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-test new file mode 100644 index 0000000..e352e97 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-test @@ -0,0 +1 @@ +../../../../../../examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation \ No newline at end of file From 561e8b904f3c8fcdc88f0895a0d2f7b5a09b1c95 Mon Sep 17 00:00:00 2001 From: motorro Date: Tue, 15 Sep 2026 19:57:34 +0200 Subject: [PATCH 04/32] States and testing Refs: #14 --- examples/skills/auth/api/build.gradle.kts | 2 +- .../{ => skills}/auth/api/AuthDataApi.kt | 2 +- .../{ => skills}/auth/api/AuthGesture.kt | 2 +- .../{ => skills}/auth/api/AuthInput.kt | 2 +- .../{ => skills}/auth/api/AuthResult.kt | 2 +- .../{ => skills}/auth/api/AuthUiApi.kt | 2 +- .../{ => skills}/auth/api/AuthUiState.kt | 2 +- .../auth/implementation/build.gradle.kts | 3 +- .../auth/implementation/state/AuthState.kt | 10 -- .../implementation/data/AuthGestureImpl.kt | 4 +- .../auth/implementation/data/AuthStateData.kt | 5 +- .../implementation/data/AuthUiStateImpl.kt | 4 +- .../auth/implementation/state/AuthContext.kt | 6 +- .../auth/implementation/state/AuthState.kt | 10 ++ .../implementation/state/AuthStateFactory.kt | 11 ++- .../state/AuthStateFactoryImpl.kt | 13 +-- .../implementation/state/BaseAuthState.kt | 8 +- .../auth/implementation/state/FormState.kt | 12 +-- .../state/PreloadingErrorState.kt | 22 +++-- .../implementation/state/PreloadingState.kt | 15 +-- .../auth/implementation/ui/AuthUiRenderer.kt | 8 +- .../auth/implementation/data/dataStates.kt | 32 ------ .../auth/implementation/data/uiStates.kt | 6 -- .../implementation/data/AuthStateDataTest.kt | 51 ++++++++++ .../data/authGesturesFixtures.kt} | 2 +- .../implementation/data/dataStatesFixtures.kt | 45 +++++++++ .../implementation/data/uiStatesFixtures.kt | 6 ++ .../implementation/state/BaseStateTest.kt | 10 +- .../implementation/state/FormStateTest.kt | 99 +++++++++++++++++++ .../state/PreloadingErrorStateTest.kt | 91 +++++++++++++++++ .../state/PreloadingStateTest.kt | 82 +++++++++++++++ examples/skills/domain/build.gradle.kts | 49 +++++++++ .../domain/entity}/PasswordRequirements.kt | 2 +- .../skills/domain/exception/AppException.kt | 35 +++++++ .../skills/domain/exception/IOException.kt | 6 ++ .../domain/exception/UnknownException.kt | 6 ++ .../usecase/GetPasswordRequirements.kt | 4 +- gradle/libs.versions.toml | 14 +-- settings.gradle.kts | 1 + .../common-child-flow/boilerplate/SKILL.md | 3 +- .../boilerplate/assets/example/api | 2 +- .../boilerplate/assets/example/implementation | 2 +- .../assets/example/implementation-test | 1 - .../assets/example/implementation-tests | 1 + .../base-test-class-implementation.md | 2 +- .../references/state-implementation.md | 2 +- .../references/state-test-implementation.md | 72 ++++++++++++++ .../test-fixtures-implementation.md | 6 +- 48 files changed, 646 insertions(+), 131 deletions(-) rename examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/{ => skills}/auth/api/AuthDataApi.kt (83%) rename examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/{ => skills}/auth/api/AuthGesture.kt (69%) rename examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/{ => skills}/auth/api/AuthInput.kt (76%) rename examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/{ => skills}/auth/api/AuthResult.kt (75%) rename examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/{ => skills}/auth/api/AuthUiApi.kt (82%) rename examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/{ => skills}/auth/api/AuthUiState.kt (81%) delete mode 100644 examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/AuthState.kt rename examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/{ => skills}/auth/implementation/data/AuthGestureImpl.kt (83%) rename examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/{ => skills}/auth/implementation/data/AuthStateData.kt (77%) rename examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/{ => skills}/auth/implementation/data/AuthUiStateImpl.kt (85%) rename examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/{ => skills}/auth/implementation/state/AuthContext.kt (63%) create mode 100644 examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthState.kt rename examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/{ => skills}/auth/implementation/state/AuthStateFactory.kt (73%) rename examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/{ => skills}/auth/implementation/state/AuthStateFactoryImpl.kt (75%) rename examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/{ => skills}/auth/implementation/state/BaseAuthState.kt (74%) rename examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/{ => skills}/auth/implementation/state/FormState.kt (80%) rename examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/{ => skills}/auth/implementation/state/PreloadingErrorState.kt (64%) rename examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/{ => skills}/auth/implementation/state/PreloadingState.kt (77%) rename examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/{ => skills}/auth/implementation/ui/AuthUiRenderer.kt (71%) delete mode 100644 examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation/data/dataStates.kt delete mode 100644 examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation/data/uiStates.kt create mode 100644 examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthStateDataTest.kt rename examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/{auth/implementation/data/authGestures.kt => skills/auth/implementation/data/authGesturesFixtures.kt} (77%) create mode 100644 examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/dataStatesFixtures.kt create mode 100644 examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/uiStatesFixtures.kt rename examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/{ => skills}/auth/implementation/state/BaseStateTest.kt (84%) create mode 100644 examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/FormStateTest.kt create mode 100644 examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingErrorStateTest.kt create mode 100644 examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingStateTest.kt create mode 100644 examples/skills/domain/build.gradle.kts rename examples/skills/{auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/data => domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/entity}/PasswordRequirements.kt (80%) create mode 100644 examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/exception/AppException.kt create mode 100644 examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/exception/IOException.kt create mode 100644 examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/exception/UnknownException.kt rename examples/skills/{auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation => domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain}/usecase/GetPasswordRequirements.kt (56%) mode change 100644 => 120000 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api mode change 100644 => 120000 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation delete mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-test create mode 120000 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/references/state-test-implementation.md diff --git a/examples/skills/auth/api/build.gradle.kts b/examples/skills/auth/api/build.gradle.kts index c5765a0..ae9e74f 100644 --- a/examples/skills/auth/api/build.gradle.kts +++ b/examples/skills/auth/api/build.gradle.kts @@ -33,7 +33,7 @@ kotlin { jvmToolchain(21) android { - namespace = "com.motorro.statemachine.auth.api" + namespace = "com.motorro.statemachine.skills.auth.api" compileSdk = androidCompileSdkVersion minSdk = androidMinSdkVersion diff --git a/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api/AuthDataApi.kt b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthDataApi.kt similarity index 83% rename from examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api/AuthDataApi.kt rename to examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthDataApi.kt index b31c1b5..123c71d 100644 --- a/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api/AuthDataApi.kt +++ b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthDataApi.kt @@ -1,4 +1,4 @@ -package com.motorro.statemachine.auth.api +package com.motorro.statemachine.skills.auth.api import com.motorro.commonstatemachine.flow.data.CommonFlowDataApi /** diff --git a/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api/AuthGesture.kt b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthGesture.kt similarity index 69% rename from examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api/AuthGesture.kt rename to examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthGesture.kt index a001dfe..6f51886 100644 --- a/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api/AuthGesture.kt +++ b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthGesture.kt @@ -1,4 +1,4 @@ -package com.motorro.statemachine.auth.api +package com.motorro.statemachine.skills.auth.api /** * A base type for authentication user gestures diff --git a/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api/AuthInput.kt b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthInput.kt similarity index 76% rename from examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api/AuthInput.kt rename to examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthInput.kt index f46f3c6..4a8d512 100644 --- a/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api/AuthInput.kt +++ b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthInput.kt @@ -1,4 +1,4 @@ -package com.motorro.statemachine.auth.api +package com.motorro.statemachine.skills.auth.api /** * Initializing data for the feature flow diff --git a/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api/AuthResult.kt b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthResult.kt similarity index 75% rename from examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api/AuthResult.kt rename to examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthResult.kt index a1fffd3..7023117 100644 --- a/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api/AuthResult.kt +++ b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthResult.kt @@ -1,4 +1,4 @@ -package com.motorro.statemachine.auth.api +package com.motorro.statemachine.skills.auth.api /** * Feature flow result diff --git a/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api/AuthUiApi.kt b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthUiApi.kt similarity index 82% rename from examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api/AuthUiApi.kt rename to examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthUiApi.kt index 762d13e..8b13094 100644 --- a/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api/AuthUiApi.kt +++ b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthUiApi.kt @@ -1,4 +1,4 @@ -package com.motorro.statemachine.auth.api +package com.motorro.statemachine.skills.auth.api import com.motorro.commonstatemachine.flow.compose.CommonFlowUiApi /** diff --git a/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api/AuthUiState.kt b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthUiState.kt similarity index 81% rename from examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api/AuthUiState.kt rename to examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthUiState.kt index 5d8aded..d2fbef4 100644 --- a/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api/AuthUiState.kt +++ b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthUiState.kt @@ -1,4 +1,4 @@ -package com.motorro.statemachine.auth.api +package com.motorro.statemachine.skills.auth.api /** * A base type for authentication UI state diff --git a/examples/skills/auth/implementation/build.gradle.kts b/examples/skills/auth/implementation/build.gradle.kts index ea6d163..29d729c 100644 --- a/examples/skills/auth/implementation/build.gradle.kts +++ b/examples/skills/auth/implementation/build.gradle.kts @@ -34,7 +34,7 @@ kotlin { jvmToolchain(21) android { - namespace = "com.motorro.statemachine.auth.implementation" + namespace = "com.motorro.statemachine.skills.auth.implementation" compileSdk = androidCompileSdkVersion minSdk = androidMinSdkVersion @@ -53,6 +53,7 @@ kotlin { implementation(project(":coroutines")) implementation(project(":commonflow:commonflow-data")) implementation(project(":commonflow:commonflow-compose")) + implementation(project(":examples:skills:domain")) implementation(project(":examples:skills:auth:api")) implementation(libs.kotlin.coroutines.core) implementation(libs.composeMultiplatform.runtime) diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/AuthState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/AuthState.kt deleted file mode 100644 index 0589199..0000000 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/AuthState.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.motorro.statemachine.auth.implementation.state - -import com.motorro.commonstatemachine.CommonMachineState -import com.motorro.statemachine.auth.implementation.data.AuthGestureImpl -import com.motorro.statemachine.auth.implementation.data.AuthUiStateImpl - -/** - * Convenient type-alias - bounds gesture and ui state system - */ -internal typealias AuthState = CommonMachineState \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/data/AuthGestureImpl.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthGestureImpl.kt similarity index 83% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/data/AuthGestureImpl.kt rename to examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthGestureImpl.kt index e1b4425..2422c7a 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/data/AuthGestureImpl.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthGestureImpl.kt @@ -1,6 +1,6 @@ -package com.motorro.statemachine.auth.implementation.data +package com.motorro.statemachine.skills.auth.implementation.data -import com.motorro.statemachine.auth.api.AuthGesture +import com.motorro.statemachine.skills.auth.api.AuthGesture /** * All gestures available in this feature diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/data/AuthStateData.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthStateData.kt similarity index 77% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/data/AuthStateData.kt rename to examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthStateData.kt index 249a29e..33a6436 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/data/AuthStateData.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthStateData.kt @@ -1,6 +1,7 @@ -package com.motorro.statemachine.auth.implementation.data +package com.motorro.statemachine.skills.auth.implementation.data -import com.motorro.statemachine.auth.api.AuthInput +import com.motorro.commonstatemachine.skills.domain.entity.PasswordRequirements +import com.motorro.statemachine.skills.auth.api.AuthInput /** * Interstate data to pass between the machine states diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/data/AuthUiStateImpl.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthUiStateImpl.kt similarity index 85% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/data/AuthUiStateImpl.kt rename to examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthUiStateImpl.kt index c9a986e..1f7be37 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/data/AuthUiStateImpl.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthUiStateImpl.kt @@ -1,6 +1,6 @@ -package com.motorro.statemachine.auth.implementation.data +package com.motorro.statemachine.skills.auth.implementation.data -import com.motorro.statemachine.auth.api.AuthUiState +import com.motorro.statemachine.skills.auth.api.AuthUiState /** * All gestures available in this feature diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/AuthContext.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthContext.kt similarity index 63% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/AuthContext.kt rename to examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthContext.kt index c98b66c..5043041 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/AuthContext.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthContext.kt @@ -1,8 +1,8 @@ -package com.motorro.statemachine.auth.implementation.state +package com.motorro.statemachine.skills.auth.implementation.state import com.motorro.commonstatemachine.flow.data.CommonFlowHost -import com.motorro.statemachine.auth.api.AuthResult -import com.motorro.statemachine.auth.implementation.ui.AuthUiRenderer +import com.motorro.statemachine.skills.auth.api.AuthResult +import com.motorro.statemachine.skills.auth.implementation.ui.AuthUiRenderer /** * Common tools used by states diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthState.kt new file mode 100644 index 0000000..bc218c6 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthState.kt @@ -0,0 +1,10 @@ +package com.motorro.statemachine.skills.auth.implementation.state + +import com.motorro.commonstatemachine.CommonMachineState +import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.statemachine.skills.auth.implementation.data.AuthUiStateImpl + +/** + * Convenient type-alias - bounds gesture and ui state system + */ +internal typealias AuthState = CommonMachineState \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/AuthStateFactory.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthStateFactory.kt similarity index 73% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/AuthStateFactory.kt rename to examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthStateFactory.kt index 07b571d..a58bb83 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/AuthStateFactory.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthStateFactory.kt @@ -1,8 +1,9 @@ -package com.motorro.statemachine.auth.implementation.state +package com.motorro.statemachine.skills.auth.implementation.state -import com.motorro.statemachine.auth.api.AuthInput -import com.motorro.statemachine.auth.api.AuthResult -import com.motorro.statemachine.auth.implementation.data.AuthStateData +import com.motorro.commonstatemachine.skills.domain.exception.AppException +import com.motorro.statemachine.skills.auth.api.AuthInput +import com.motorro.statemachine.skills.auth.api.AuthResult +import com.motorro.statemachine.skills.auth.implementation.data.AuthStateData /** * State-factory interface @@ -22,7 +23,7 @@ internal interface AuthStateFactory { * @param input Initialization params * @param error Error that occurred during the initialization */ - fun preloadError(input: AuthInput, error: Throwable): AuthState + fun preloadError(input: AuthInput, error: AppException): AuthState /** * Creates authentication form state. diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/AuthStateFactoryImpl.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthStateFactoryImpl.kt similarity index 75% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/AuthStateFactoryImpl.kt rename to examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthStateFactoryImpl.kt index 189b474..1dd5480 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/AuthStateFactoryImpl.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthStateFactoryImpl.kt @@ -1,10 +1,11 @@ -package com.motorro.statemachine.auth.implementation.state +package com.motorro.statemachine.skills.auth.implementation.state import com.motorro.commonstatemachine.flow.data.CommonFlowHost -import com.motorro.statemachine.auth.api.AuthInput -import com.motorro.statemachine.auth.api.AuthResult -import com.motorro.statemachine.auth.implementation.data.AuthStateData -import com.motorro.statemachine.auth.implementation.ui.AuthUiRenderer +import com.motorro.commonstatemachine.skills.domain.exception.AppException +import com.motorro.statemachine.skills.auth.api.AuthInput +import com.motorro.statemachine.skills.auth.api.AuthResult +import com.motorro.statemachine.skills.auth.implementation.data.AuthStateData +import com.motorro.statemachine.skills.auth.implementation.ui.AuthUiRenderer import io.github.aakira.napier.Napier /** @@ -36,7 +37,7 @@ internal class AuthStateFactoryImpl( /** * Creates preload error state directly */ - override fun preloadError(input: AuthInput, error: Throwable) = PreloadingErrorState( + override fun preloadError(input: AuthInput, error: AppException) = PreloadingErrorState( context, input, error diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/BaseAuthState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/BaseAuthState.kt similarity index 74% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/BaseAuthState.kt rename to examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/BaseAuthState.kt index 8658cd9..337fc47 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/BaseAuthState.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/BaseAuthState.kt @@ -1,9 +1,9 @@ -package com.motorro.statemachine.auth.implementation.state +package com.motorro.statemachine.skills.auth.implementation.state import com.motorro.commonstatemachine.coroutines.CoroutineState -import com.motorro.statemachine.auth.implementation.data.AuthGestureImpl -import com.motorro.statemachine.auth.implementation.data.AuthUiStateImpl -import com.motorro.statemachine.auth.implementation.ui.AuthUiRenderer +import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.statemachine.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.statemachine.skills.auth.implementation.ui.AuthUiRenderer import io.github.aakira.napier.Napier /** diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/FormState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/FormState.kt similarity index 80% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/FormState.kt rename to examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/FormState.kt index c4691ed..9575504 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/FormState.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/FormState.kt @@ -1,9 +1,9 @@ -package com.motorro.statemachine.auth.implementation.state +package com.motorro.statemachine.skills.auth.implementation.state -import com.motorro.statemachine.auth.api.AuthResult -import com.motorro.statemachine.auth.implementation.data.AuthGestureImpl -import com.motorro.statemachine.auth.implementation.data.AuthStateData -import com.motorro.statemachine.auth.implementation.data.isValidToAuthenticate +import com.motorro.statemachine.skills.auth.api.AuthResult +import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.statemachine.skills.auth.implementation.data.AuthStateData +import com.motorro.statemachine.skills.auth.implementation.data.isValidToAuthenticate import io.github.aakira.napier.Napier import org.jetbrains.annotations.VisibleForTesting @@ -18,7 +18,7 @@ import org.jetbrains.annotations.VisibleForTesting */ internal class FormState( context: AuthContext, - @VisibleForTesting var data: AuthStateData + @VisibleForTesting var data: AuthStateData, ) : BaseAuthState(context) { /** diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/PreloadingErrorState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingErrorState.kt similarity index 64% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/PreloadingErrorState.kt rename to examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingErrorState.kt index b165471..975dd6f 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/PreloadingErrorState.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingErrorState.kt @@ -1,8 +1,10 @@ -package com.motorro.statemachine.auth.implementation.state +package com.motorro.statemachine.skills.auth.implementation.state -import com.motorro.statemachine.auth.api.AuthInput -import com.motorro.statemachine.auth.api.AuthResult -import com.motorro.statemachine.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.skills.domain.exception.AppException +import com.motorro.commonstatemachine.skills.domain.exception.IOException +import com.motorro.statemachine.skills.auth.api.AuthInput +import com.motorro.statemachine.skills.auth.api.AuthResult +import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl import io.github.aakira.napier.Napier /** @@ -14,15 +16,17 @@ import io.github.aakira.napier.Napier internal class PreloadingErrorState( context: AuthContext, private val input: AuthInput, - private val error: Throwable + private val error: AppException ) : BaseAuthState(context) { override fun doStart() { - renderer.renderFullScreenError(error, canRetry()) + setUiState { + renderFullScreenError(error.message, canRetry()) + } } - private fun canRetry(): Boolean { - // TODO: check the error and decide if it can be retried - return false + private fun canRetry(): Boolean = when(error) { + is IOException -> true + else -> false } override fun doProcess(gesture: AuthGestureImpl) { diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/PreloadingState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingState.kt similarity index 77% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/PreloadingState.kt rename to examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingState.kt index 36ed436..a85d4e4 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/state/PreloadingState.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingState.kt @@ -1,10 +1,11 @@ -package com.motorro.statemachine.auth.implementation.state +package com.motorro.statemachine.skills.auth.implementation.state -import com.motorro.statemachine.auth.api.AuthInput -import com.motorro.statemachine.auth.api.AuthResult -import com.motorro.statemachine.auth.implementation.data.AuthGestureImpl -import com.motorro.statemachine.auth.implementation.data.AuthStateData -import com.motorro.statemachine.auth.implementation.usecase.GetPasswordRequirements +import com.motorro.commonstatemachine.skills.domain.exception.toAppException +import com.motorro.commonstatemachine.skills.domain.usecase.GetPasswordRequirements +import com.motorro.statemachine.skills.auth.api.AuthInput +import com.motorro.statemachine.skills.auth.api.AuthResult +import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.statemachine.skills.auth.implementation.data.AuthStateData import io.github.aakira.napier.Napier import kotlinx.coroutines.ensureActive import kotlinx.coroutines.launch @@ -56,7 +57,7 @@ internal class PreloadingState( Napier.w(e) { "Error during initialization" } // Advance to the error state setMachineState { - preloadError(input, e) + preloadError(input, e.toAppException()) } } } diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/ui/AuthUiRenderer.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthUiRenderer.kt similarity index 71% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/ui/AuthUiRenderer.kt rename to examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthUiRenderer.kt index 4b94cbe..e366100 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/ui/AuthUiRenderer.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthUiRenderer.kt @@ -1,7 +1,7 @@ -package com.motorro.statemachine.auth.implementation.ui +package com.motorro.statemachine.skills.auth.implementation.ui -import com.motorro.statemachine.auth.implementation.data.AuthStateData -import com.motorro.statemachine.auth.implementation.data.AuthUiStateImpl +import com.motorro.statemachine.skills.auth.implementation.data.AuthStateData +import com.motorro.statemachine.skills.auth.implementation.data.AuthUiStateImpl /** * Transforms the data state to the UI state @@ -28,5 +28,5 @@ internal interface AuthUiRenderer { * @param error Error to present to the user * @param canRetry Error could be retried */ - fun renderFullScreenError(error: Throwable, canRetry: Boolean): AuthUiStateImpl + fun renderFullScreenError(error: String, canRetry: Boolean): AuthUiStateImpl } \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation/data/dataStates.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation/data/dataStates.kt deleted file mode 100644 index cbbc75e..0000000 --- a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation/data/dataStates.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.motorro.statemachine.auth.implementation.data - -import com.motorro.statemachine.auth.api.AuthInput -import com.motorro.statemachine.auth.api.AuthResult - -/** - * Input fixture - */ -internal val INPUT = AuthInput("Please login!") - -/** - * Result fixture - */ -internal val RESULT = AuthResult(authenticated = true) - -internal val PASSWORD_REQUIREMENTS = PasswordRequirements( - regex = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,}\$".toRegex(), - description = "Minimum eight characters, at least one letter and one number" -) - -/** - * Initial state - */ -internal val EMPTY_STATE = AuthStateData(INPUT, PASSWORD_REQUIREMENTS) - -/** - * Valid form state - */ -internal val VALID_FORM_STATE = EMPTY_STATE.copy( - username = "user", - password = "password" -) \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation/data/uiStates.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation/data/uiStates.kt deleted file mode 100644 index 36333ed..0000000 --- a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation/data/uiStates.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.motorro.statemachine.auth.implementation.data - -/** - * Some UI-state to return from [com.motorro.statemachine.auth.implementation.ui.AuthUiRenderer] mock - */ -internal val UI_STATE = AuthUiStateImpl.Loading \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthStateDataTest.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthStateDataTest.kt new file mode 100644 index 0000000..2d0550e --- /dev/null +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthStateDataTest.kt @@ -0,0 +1,51 @@ +package com.motorro.statemachine.skills.auth.implementation.data + +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +internal class AuthStateDataTest { + @Test + fun returnsTrueIfValid() { + val data = AuthStateData( + input = INPUT, + passwordRequirements = PASSWORD_REQUIREMENTS, + username = "user", + password = "password", + ) + assertTrue(data.isValidToAuthenticate()) + } + + @Test + fun returnsFalseIfUsernameEmpty() { + val data = AuthStateData( + input = INPUT, + passwordRequirements = PASSWORD_REQUIREMENTS, + username = "", + password = "password", + ) + assertFalse(data.isValidToAuthenticate()) + } + + @Test + fun returnsFalseIfUsernameBlank() { + val data = AuthStateData( + input = INPUT, + passwordRequirements = PASSWORD_REQUIREMENTS, + username = " ", + password = "password", + ) + assertFalse(data.isValidToAuthenticate()) + } + + @Test + fun returnsFalseIfPasswordInvalid() { + val data = AuthStateData( + input = INPUT, + passwordRequirements = PASSWORD_REQUIREMENTS, + username = "user", + password = "short", // PASSWORD_REQUIREMENTS regex is ^.{8,}$ + ) + assertFalse(data.isValidToAuthenticate()) + } +} diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation/data/authGestures.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/authGesturesFixtures.kt similarity index 77% rename from examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation/data/authGestures.kt rename to examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/authGesturesFixtures.kt index d131f1a..9de6c57 100644 --- a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation/data/authGestures.kt +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/authGesturesFixtures.kt @@ -1,4 +1,4 @@ -package com.motorro.statemachine.auth.implementation.data +package com.motorro.statemachine.skills.auth.implementation.data /** * Username input changed diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/dataStatesFixtures.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/dataStatesFixtures.kt new file mode 100644 index 0000000..cf85e8f --- /dev/null +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/dataStatesFixtures.kt @@ -0,0 +1,45 @@ +package com.motorro.statemachine.skills.auth.implementation.data + +import com.motorro.commonstatemachine.skills.domain.entity.PasswordRequirements +import com.motorro.commonstatemachine.skills.domain.exception.IOException +import com.motorro.commonstatemachine.skills.domain.exception.UnknownException +import com.motorro.statemachine.skills.auth.api.AuthInput +import com.motorro.statemachine.skills.auth.api.AuthResult + +/** + * Input fixture + */ +internal val INPUT = AuthInput("Please login!") + +/** + * Result fixture + */ +internal val RESULT = AuthResult(authenticated = true) + +internal val PASSWORD_REQUIREMENTS = PasswordRequirements( + regex = "^.{8,}$".toRegex(), + description = "Minimum eight characters" +) + +/** + * Initial state + */ +internal val EMPTY_STATE = AuthStateData(INPUT, PASSWORD_REQUIREMENTS) + +/** + * Valid form state + */ +internal val VALID_FORM_STATE = EMPTY_STATE.copy( + username = "user", + password = "password" +) + +/** + * Fatal error fixture + */ +internal val FATAL_ERROR = UnknownException("Something went wrong") + +/** + * Non-fatal error fixture + */ +internal val NON_FATAL_ERROR = IOException("Internet is down") \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/uiStatesFixtures.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/uiStatesFixtures.kt new file mode 100644 index 0000000..4172d5c --- /dev/null +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/uiStatesFixtures.kt @@ -0,0 +1,6 @@ +package com.motorro.statemachine.skills.auth.implementation.data + +/** + * Some UI-state to return from [com.motorro.statemachine.skills.auth.implementation.ui.AuthUiRenderer] mock + */ +internal val UI_STATE = AuthUiStateImpl.Loading \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation/state/BaseStateTest.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/BaseStateTest.kt similarity index 84% rename from examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation/state/BaseStateTest.kt rename to examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/BaseStateTest.kt index 92e38be..4f3f61a 100644 --- a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation/state/BaseStateTest.kt +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/BaseStateTest.kt @@ -1,11 +1,11 @@ -package com.motorro.statemachine.auth.implementation.state +package com.motorro.statemachine.skills.auth.implementation.state import com.motorro.commonstatemachine.CommonStateMachine import com.motorro.commonstatemachine.flow.data.CommonFlowHost -import com.motorro.statemachine.auth.api.AuthResult -import com.motorro.statemachine.auth.implementation.data.AuthGestureImpl -import com.motorro.statemachine.auth.implementation.data.AuthUiStateImpl -import com.motorro.statemachine.auth.implementation.ui.AuthUiRenderer +import com.motorro.statemachine.skills.auth.api.AuthResult +import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.statemachine.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.statemachine.skills.auth.implementation.ui.AuthUiRenderer import dev.mokkery.MockMode import dev.mokkery.mock import kotlinx.coroutines.Dispatchers diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/FormStateTest.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/FormStateTest.kt new file mode 100644 index 0000000..f5ea717 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/FormStateTest.kt @@ -0,0 +1,99 @@ +package com.motorro.statemachine.skills.auth.implementation.state + +import com.motorro.statemachine.skills.auth.api.AuthResult +import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.statemachine.skills.auth.implementation.data.EMPTY_STATE +import com.motorro.statemachine.skills.auth.implementation.data.PASSWORD_CHANGED +import com.motorro.statemachine.skills.auth.implementation.data.UI_STATE +import com.motorro.statemachine.skills.auth.implementation.data.USERNAME_CHANGED +import com.motorro.statemachine.skills.auth.implementation.data.VALID_FORM_STATE +import dev.mokkery.answering.returns +import dev.mokkery.every +import dev.mokkery.matcher.any +import dev.mokkery.verify +import dev.mokkery.verify.VerifyMode +import kotlin.test.Test +import kotlin.test.assertEquals + +internal class FormStateTest : BaseStateTest() { + private lateinit var state: FormState + + override fun doInit() { + state = FormState(context, EMPTY_STATE) + every { renderer.renderForm(any(), any()) } returns UI_STATE + } + + @Test + fun rendersFormOnStart() = test { + state.start(stateMachine) + verify { + renderer.renderForm(EMPTY_STATE, isValid = false) + stateMachine.setUiState(UI_STATE) + } + } + + @Test + fun processesUsernameChanged() = test { + state.start(stateMachine) + state.process(USERNAME_CHANGED) + + val expectedData = EMPTY_STATE.copy(username = USERNAME_CHANGED.value) + verify(mode = VerifyMode.order) { + renderer.renderForm(EMPTY_STATE, isValid = false) + renderer.renderForm(expectedData, isValid = false) + } + assertEquals(USERNAME_CHANGED.value, state.data.username) + } + + @Test + fun processesPasswordChanged() = test { + state.start(stateMachine) + state.process(PASSWORD_CHANGED) + + val expectedData = EMPTY_STATE.copy(password = PASSWORD_CHANGED.value) + verify(mode = VerifyMode.order) { + renderer.renderForm(EMPTY_STATE, isValid = false) + renderer.renderForm(expectedData, isValid = false) + } + assertEquals(PASSWORD_CHANGED.value, state.data.password) + } + + @Test + fun processesAction() = test { + every { stateFactory.authenticating(any()) } returns nextState + state.data = VALID_FORM_STATE + + state.start(stateMachine) + state.process(AuthGestureImpl.Action) + + verify { + renderer.renderForm(VALID_FORM_STATE, isValid = true) + stateFactory.authenticating(VALID_FORM_STATE) + stateMachine.setMachineState(nextState) + } + } + + @Test + fun doesNotProcessActionIfInvalid() = test { + state.start(stateMachine) + state.process(AuthGestureImpl.Action) + + verify(VerifyMode.exactly(0)) { + stateFactory.authenticating(any()) + stateMachine.setMachineState(any()) + } + } + + @Test + fun processesBack() = test { + every { stateFactory.terminated(any()) } returns nextState + + state.start(stateMachine) + state.process(AuthGestureImpl.Back) + + verify { + stateFactory.terminated(AuthResult(false)) + stateMachine.setMachineState(nextState) + } + } +} diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingErrorStateTest.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingErrorStateTest.kt new file mode 100644 index 0000000..3b15050 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingErrorStateTest.kt @@ -0,0 +1,91 @@ +package com.motorro.statemachine.skills.auth.implementation.state + +import com.motorro.commonstatemachine.skills.domain.exception.AppException +import com.motorro.statemachine.skills.auth.api.AuthResult +import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.statemachine.skills.auth.implementation.data.FATAL_ERROR +import com.motorro.statemachine.skills.auth.implementation.data.INPUT +import com.motorro.statemachine.skills.auth.implementation.data.NON_FATAL_ERROR +import com.motorro.statemachine.skills.auth.implementation.data.UI_STATE +import dev.mokkery.answering.returns +import dev.mokkery.every +import dev.mokkery.matcher.any +import dev.mokkery.verify +import kotlin.test.Test + +internal class PreloadingErrorStateTest : BaseStateTest() { + override fun doInit() { + every { renderer.renderFullScreenError(any(), any()) } returns UI_STATE + } + + private fun createState(error: AppException) = PreloadingErrorState( + context, + INPUT, + error + ) + + @Test + fun rendersFatalErrorOnStart() = test { + val state = createState(FATAL_ERROR) + state.start(stateMachine) + verify { + renderer.renderFullScreenError(FATAL_ERROR.message, canRetry = false) + stateMachine.setUiState(UI_STATE) + } + } + + @Test + fun rendersNonFatalErrorOnStart() = test { + val state = createState(NON_FATAL_ERROR) + state.start(stateMachine) + verify { + renderer.renderFullScreenError(NON_FATAL_ERROR.message, canRetry = true) + stateMachine.setUiState(UI_STATE) + } + } + + @Test + fun terminatesOnBack() = test { + val state = createState(NON_FATAL_ERROR) + + every { stateFactory.terminated(any()) } returns nextState + + state.start(stateMachine) + state.process(AuthGestureImpl.Back) + + verify { + stateFactory.terminated(AuthResult(authenticated = false)) + stateMachine.setMachineState(nextState) + } + } + + @Test + fun terminatesOnActionIfCannotRetry() = test { + val state = createState(FATAL_ERROR) + + every { stateFactory.terminated(any()) } returns nextState + + state.start(stateMachine) + state.process(AuthGestureImpl.Action) + + verify { + stateFactory.terminated(AuthResult(authenticated = false)) + stateMachine.setMachineState(nextState) + } + } + + @Test + fun reloadsOnActionIfCanRetry() = test { + val state = createState(NON_FATAL_ERROR) + + every { stateFactory.preloading(any()) } returns nextState + + state.start(stateMachine) + state.process(AuthGestureImpl.Action) + + verify { + stateFactory.preloading(INPUT) + stateMachine.setMachineState(nextState) + } + } +} diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingStateTest.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingStateTest.kt new file mode 100644 index 0000000..d25b6e2 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingStateTest.kt @@ -0,0 +1,82 @@ +package com.motorro.statemachine.skills.auth.implementation.state + +import com.motorro.commonstatemachine.skills.domain.usecase.GetPasswordRequirements +import com.motorro.statemachine.skills.auth.api.AuthResult +import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.statemachine.skills.auth.implementation.data.FATAL_ERROR +import com.motorro.statemachine.skills.auth.implementation.data.INPUT +import com.motorro.statemachine.skills.auth.implementation.data.PASSWORD_REQUIREMENTS +import com.motorro.statemachine.skills.auth.implementation.data.UI_STATE +import dev.mokkery.answering.calls +import dev.mokkery.answering.returns +import dev.mokkery.answering.throws +import dev.mokkery.every +import dev.mokkery.everySuspend +import dev.mokkery.matcher.any +import dev.mokkery.mock +import dev.mokkery.verify +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.test.Test + +internal class PreloadingStateTest : BaseStateTest() { + private lateinit var requirementsMock: GetPasswordRequirements + private lateinit var state: PreloadingState + + override fun doInit() { + requirementsMock = mock() + state = PreloadingState(context, INPUT, requirementsMock) + every { renderer.renderLoading() } returns UI_STATE + } + + @Test + fun rendersLoadingOnStart() = test { + everySuspend { requirementsMock() } calls { suspendCancellableCoroutine { } } + + state.start(stateMachine) + + verify { + renderer.renderLoading() + stateMachine.setUiState(UI_STATE) + } + } + + @Test + fun transfersToFormWhenLoaded() = test { + everySuspend { requirementsMock() } returns PASSWORD_REQUIREMENTS + every { stateFactory.form(any()) } returns nextState + + state.start(stateMachine) + + verify { + stateFactory.form(any()) + stateMachine.setMachineState(nextState) + } + } + + @Test + fun transfersToErrorWhenFailed() = test { + everySuspend { requirementsMock() } throws FATAL_ERROR + every { stateFactory.preloadError(any(), any()) } returns nextState + + state.start(stateMachine) + + verify { + stateFactory.preloadError(INPUT, any()) + stateMachine.setMachineState(nextState) + } + } + + @Test + fun terminatesOnBackWhileLoading() = test { + everySuspend { requirementsMock() } calls { suspendCancellableCoroutine { } } + every { stateFactory.terminated(any()) } returns nextState + + state.start(stateMachine) + state.process(AuthGestureImpl.Back) + + verify { + stateFactory.terminated(AuthResult(authenticated = false)) + stateMachine.setMachineState(nextState) + } + } +} diff --git a/examples/skills/domain/build.gradle.kts b/examples/skills/domain/build.gradle.kts new file mode 100644 index 0000000..f258a2a --- /dev/null +++ b/examples/skills/domain/build.gradle.kts @@ -0,0 +1,49 @@ +@file:Suppress("unused") + +/* +* Copyright 2023 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. +*/ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.kotlin.multiplatform.library) +} + +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 = "com.motorro" +version = versionName + +kotlin { + jvmToolchain(21) + + android { + namespace = "com.motorro.statemachine.skills.domain" + compileSdk = androidCompileSdkVersion + minSdk = androidMinSdkVersion + + compilerOptions { + jvmTarget.set(JvmTarget.JVM_21) + } + } + + sourceSets { + commonMain.dependencies { + implementation(libs.kotlin.coroutines.core) + } + } +} + diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/data/PasswordRequirements.kt b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/entity/PasswordRequirements.kt similarity index 80% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/data/PasswordRequirements.kt rename to examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/entity/PasswordRequirements.kt index c806319..8c24bf2 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/data/PasswordRequirements.kt +++ b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/entity/PasswordRequirements.kt @@ -1,4 +1,4 @@ -package com.motorro.statemachine.auth.implementation.data +package com.motorro.commonstatemachine.skills.domain.entity /** * Password requirements diff --git a/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/exception/AppException.kt b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/exception/AppException.kt new file mode 100644 index 0000000..b1684b0 --- /dev/null +++ b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/exception/AppException.kt @@ -0,0 +1,35 @@ +package com.motorro.commonstatemachine.skills.domain.exception + +/** + * Base domain exception + * @property message Error message + * @property cause Error cause if any + */ +abstract class AppException(final override val message: String, final override val cause: Throwable? = null) : RuntimeException() { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AppException + + if (message != other.message) return false + if (cause != other.cause) return false + + return true + } + + override fun hashCode(): Int { + var result = message.hashCode() + result = 31 * result + (cause?.hashCode() ?: 0) + return result + } +} + +/** + * Converts exception to domain + */ +fun Throwable.toAppException(): AppException = when(this) { + is AppException -> this + else -> UnknownException(message ?: "UNKNOWN exception", this) +} \ No newline at end of file diff --git a/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/exception/IOException.kt b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/exception/IOException.kt new file mode 100644 index 0000000..ab5320d --- /dev/null +++ b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/exception/IOException.kt @@ -0,0 +1,6 @@ +package com.motorro.commonstatemachine.skills.domain.exception + +/** + * IO exception + */ +class IOException(message: String, cause: Throwable? = null) : AppException(message, cause) \ No newline at end of file diff --git a/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/exception/UnknownException.kt b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/exception/UnknownException.kt new file mode 100644 index 0000000..2259626 --- /dev/null +++ b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/exception/UnknownException.kt @@ -0,0 +1,6 @@ +package com.motorro.commonstatemachine.skills.domain.exception + +/** + * Unknown exception + */ +class UnknownException(message: String, cause: Throwable? = null) : AppException(message, cause) \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/usecase/GetPasswordRequirements.kt b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/usecase/GetPasswordRequirements.kt similarity index 56% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/usecase/GetPasswordRequirements.kt rename to examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/usecase/GetPasswordRequirements.kt index 03c7e83..beafa49 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation/usecase/GetPasswordRequirements.kt +++ b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/usecase/GetPasswordRequirements.kt @@ -1,6 +1,6 @@ -package com.motorro.statemachine.auth.implementation.usecase +package com.motorro.commonstatemachine.skills.domain.usecase -import com.motorro.statemachine.auth.implementation.data.PasswordRequirements +import com.motorro.commonstatemachine.skills.domain.entity.PasswordRequirements /** * Loads password requirements diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7e3fb9a..9e9c3a4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,24 +1,24 @@ [versions] androidGradlePlugin = "9.2.1" -androidxAppcompat = "1.7.1" +androidxAppcompat = "1.8.0" androidxCore = "1.19.0" androidxActivity = "1.13.0" -androidxFragment = "1.8.9" +androidxFragment = "1.9.0" androidxHilt = "1.4.0" -compose = "2026.06.01" -composeMultiplatform = "1.11.1" +compose = "2026.09.00" +composeMultiplatform = "1.12.0" viewmodelMultiplatform = "2.11.0" lificyclelMultiplatform = "2.11.0" composeActivity = "1.13.0" composeViewmodel = "2.11.0" coroutines = "1.11.0" -immutable = "0.5.1" +immutable = "0.5.2" desugaring = "2.1.5" dokka = "2.2.0" git = "5.3.3" hilt = "2.60.1" ksp = "2.3.4" -kotlin = "2.4.10" +kotlin = "2.4.20" kotlinDatetime = "0.8.0" lifecycle = "2.11.0" nexusPublish = "2.0.0" @@ -32,7 +32,7 @@ testJunit = "4.13.2" testMockk = "1.14.11" testMockkCommon = "1.13.5" robolectric = "4.16.1" -mokkery = "3.3.0" +mokkery = "3.5.0" [plugins] compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 02f6636..b858c34 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -73,6 +73,7 @@ include( ":examples:books:book", ":examples:books:book:demo", ":examples:books:app", + ":examples:skills:domain", ":examples:skills:auth:api", ":examples:skills:auth:implementation" ) diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md b/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md index 5ae64e4..3dbabff 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md @@ -95,7 +95,7 @@ Then follow these guides for the module implementation: 5. [Create a base abstract unit-test class](references/renderer-factory-context-interface-implementation.md). ## Step 4. Create the machine states -For the user to kick-off faster, we will create some states: +For the user to kick-off faster, we will create some machine-states: - If the user gave you the instructions about what would be the state-flow, or what states does he need - create the states for him. Implement the required functionality or place TODOs in place of the code if it is not clear what to do exactly. @@ -107,4 +107,5 @@ For the user to kick-off faster, we will create some states: Refer to [this manual](references/state-implementation.md) to get the idea of the state mechanics. Repeat this step for every state you need to create. +Create a unit-test for every created machine-state according to [this manual](references/state-test-implementation.md) diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api deleted file mode 100644 index 4fb48e8..0000000 --- a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api +++ /dev/null @@ -1 +0,0 @@ -../../../../../../examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/auth/api \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api new file mode 120000 index 0000000..00c8125 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api @@ -0,0 +1 @@ +../../../../../../examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/ \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation deleted file mode 100644 index e6da7f0..0000000 --- a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation +++ /dev/null @@ -1 +0,0 @@ -../../../../../../examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/auth/implementation \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation new file mode 120000 index 0000000..7b474d2 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation @@ -0,0 +1 @@ +../../../../../../examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-test b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-test deleted file mode 100644 index e352e97..0000000 --- a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-test +++ /dev/null @@ -1 +0,0 @@ -../../../../../../examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/auth/implementation \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests new file mode 120000 index 0000000..719f810 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests @@ -0,0 +1 @@ +../../../../../../examples/skills/auth/implementation/src/commonTest//kotlin/com/motorro/statemachine/skills/auth/implementation/ \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/base-test-class-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/base-test-class-implementation.md index d92dd85..3a4d367 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/base-test-class-implementation.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/base-test-class-implementation.md @@ -41,4 +41,4 @@ You will need to initialize all the mocks listed in the previous state and mock 12. Create the common coroutine test function wrapping `runTest` and using the test dispatcher from the second step. Subclasses will use this function to run the tests. -Take a look at the [example](../assets/example/implementation-test/state/BaseStateTest.kt) of such a class. +Take a look at the [example](../assets/example/implementation-tests/state/BaseStateTest.kt) of such a class. diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/state-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/state-implementation.md index c6e775e..b8351a5 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/state-implementation.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/state-implementation.md @@ -1,5 +1,5 @@ # Module implementation: state implementation -Use this reference to implement the machine-state for the `CommonStateMachine` framework. +Use this reference to implement the machine-state(s) for the `CommonStateMachine` framework. Take a look at the [class diagram](../assets/common-child-flow.puml) to get the idea about components purpose. Take a look at the [example](../assets/example/implementation/state/PreloadingState.kt). diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/state-test-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/state-test-implementation.md new file mode 100644 index 0000000..ca98672 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/state-test-implementation.md @@ -0,0 +1,72 @@ +# Module implementation: state tests +Use this reference to create tests for the [created machine states](state-implementation.md). +`CommonMachineState` tests ensure that state transitions, UI state mapping, and side effects are correctly handled. +Typical task would be the following: + +1. If the [base-test class](base-test-class-implementation.md) is not already available at the same module, add it. +2. Create a test class and extend the base-test class. +3. Define the mocks needed for the test: use-cases, apis, etc. as the lateinit vars. +4. Override the `doInit` function and initialize the mocks. +5. Create test functions using the `test` function defined in the base class. + +## Note: Mocking use-cases and suspend functions that should never complete +Some states call supplied use-cases to load some data and update the machine state accordingly. +Usually the use-case contain some suspend function to do that. +You may want this call to never end. For example - to check the `Back` gesture terminates the flow +while the user awaits for a long loading operation. +In those case use the empty `suspendCancellableCoroutine` to emulate the operation: +```kotlin +// Mock creation +private val requirementsMock: GetPasswordRequirements = mock() + +// Suspending the load operation +everySuspend { requirementsMock.invoke() } calls { suspendCancellableCoroutine { } } +``` +Example: the [test](../assets/example/implementation-tests/state/PreloadingErrorStateTest.kt) for the [PreloadingState](../assets/example/implementation/state/PreloadingState.kt): + +- The state asynchronously loads some data with the use-case at startup. +- You need to check the `Back` pressed while loading terminates. + +## Step 1. Testing the initial rendering +If the state updates the UI, check the UI is updated on state startup: + +- If the UI-renderer is defined in the module - mock the corresponding method, check the calling arguments and a call to `CommonStateMachine::setUiState`. +- If there's no UI-renderer and the state updates the ui directly, evaluate the `CommonStateMachine::setUiState` argument directly. + +Example: the [test](../assets/example/implementation-tests/state/PreloadingErrorStateTest.kt) for the [PreloadingState](../assets/example/implementation/state/PreloadingState.kt): + +- The state asynchronously loads some data with the use-case at startup. +- You need to check the state displays `Loading`. + +# Step 2. Testing the asynchronous outcomes +If the state uses any use-cases or asynchronous operations run at startup, check their outcome: + +- UI updates if any. +- state changes if any. + + +Example: the [test](../assets/example/implementation-tests/state/PreloadingErrorStateTest.kt) for the [PreloadingState](../assets/example/implementation/state/PreloadingState.kt): + +- The state asynchronously loads some data with the use-case at startup. +- You need to check the success: state updates the interstate data and proceeds. +- You need to check the failure: state switches to the error state. + +## Step 3. Testing gestures +If the state processes any gestures in `doProcess` callback, check each gesture path: + +- use [fixtures](test-fixtures-implementation.md) if there are any for gestures. +- check the UI changes if any. +- check interstate data changes if any. +- check machine state changes if any. + +Example 1: the [test](../assets/example/implementation-tests/state/FormStateTest.kt) for the [FormState](../assets/example/implementation/state/FormState.kt): + +- checks the gestures change the interstate data. +- checks the UI updates. +- checks the data validation and state transfer. + +Example 2: the [test](../assets/example/implementation-tests/state/PreloadingErrorStateTest.kt) for the [PreloadingErrorState](../assets/example/implementation/state/PreloadingErrorState.kt): + +- checks the `Back` gesture changes the machine state. +- checks `Action` gesture conditionally changes the machine-state depending on the error. +- checks the data validation and state transfer. diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/test-fixtures-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/test-fixtures-implementation.md index 1bd0cfc..cb9bb44 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/test-fixtures-implementation.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/test-fixtures-implementation.md @@ -10,6 +10,6 @@ For the fixture data, scan the project for already defined fixtures available an your own fixtures. For example, look for the modules like `domain` or `entity` and use their fixtures. Check existing feature modules for the clue how to create fixtures and where to get the common fixture module. -Take a look at the [example](../assets/example/implementation-test/data/authGestures.kt) gesture fixtures. -Take a look at the [example](../assets/example/implementation-test/data/uiStates.kt) ui-state fixtures. -Take a look at the [example](../assets/example/implementation-test/data/dataStates.kt) interstate. +Take a look at the [example](../assets/example/implementation-tests/data/authGesturesFixtures.kt) gesture fixtures. +Take a look at the [example](../assets/example/implementation-tests/data/uiStatesFixtures.kt) ui-state fixtures. +Take a look at the [example](../assets/example/implementation-tests/data/dataStatesFixtures.kt) interstate. From 795b1498729692ad8c3f3f408d59b6a5921c72b7 Mon Sep 17 00:00:00 2001 From: motorro Date: Tue, 15 Sep 2026 20:01:17 +0200 Subject: [PATCH 05/32] Interstate data testing Refs: #14 --- .../boilerplate/references/data-and-ui-implementation.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-implementation.md index 36927fd..f111efd 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-implementation.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-implementation.md @@ -59,6 +59,9 @@ Follow these common rules: Take a look at the [example](../assets/example/implementation/data/AuthStateData.kt) - interstate data for password authentication. +Important: If you create any utility function based on the interstate data (like `isValidToAuthenticate` in the example above), +add a [unit test](../assets/example/implementation-tests/data/AuthStateDataTest.kt) for it. + ## Step 4. Test fixtures Create some fixtures and test data for the created data classes: From 7ac7a4f7d8e91edbabdcbf7cb2765874c1ec7405 Mon Sep 17 00:00:00 2001 From: motorro Date: Thu, 17 Sep 2026 11:27:31 +0200 Subject: [PATCH 06/32] Library updates 2026-09 --- build.gradle.kts | 8 +++++++- commonflow/viewmodel/build.gradle.kts | 2 ++ gradle/libs.versions.toml | 4 ++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 8928362..44ef1f3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -139,6 +139,11 @@ tasks.register("runBooksExampleUnitTests") { description = "Run unit tests for books app." } +tasks.register("runSkillsExampleUnitTests") { + dependsOn(":examples:skills:auth:implementation:allTests") + description = "Run unit tests for skills." +} + tasks.register("displayVersion") { description = "Display application version name" doLast { @@ -156,7 +161,8 @@ tasks.register("runUnitTests") { "runWelcomeExampleUnitTests", "runTimerExampleUnitTests", "runDiExampleUnitTests", - "runBooksExampleUnitTests" + "runBooksExampleUnitTests", + "runSkillsExampleUnitTests" ) group = "verification" description = "Run unit tests for all modules." diff --git a/commonflow/viewmodel/build.gradle.kts b/commonflow/viewmodel/build.gradle.kts index 08d7dc3..bf1c7f8 100644 --- a/commonflow/viewmodel/build.gradle.kts +++ b/commonflow/viewmodel/build.gradle.kts @@ -62,6 +62,7 @@ kotlin { js { binaries.library() + binaries.executable() useCommonJs() browser { testTask(Action { @@ -74,6 +75,7 @@ kotlin { wasmJs { binaries.library() + binaries.executable() useCommonJs() browser { testTask(Action { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9e9c3a4..dda0296 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ desugaring = "2.1.5" dokka = "2.2.0" git = "5.3.3" hilt = "2.60.1" -ksp = "2.3.4" +ksp = "2.3.12" kotlin = "2.4.20" kotlinDatetime = "0.8.0" lifecycle = "2.11.0" @@ -31,7 +31,7 @@ testAndroidxExtJunit = "1.3.0" testJunit = "4.13.2" testMockk = "1.14.11" testMockkCommon = "1.13.5" -robolectric = "4.16.1" +robolectric = "4.17" mokkery = "3.5.0" [plugins] From ca7a1e92719c22e9909b9a9ae8c9913eeec66095 Mon Sep 17 00:00:00 2001 From: motorro Date: Sat, 19 Sep 2026 09:03:55 +0200 Subject: [PATCH 07/32] Creating UI Refs: #14 --- examples/skills/appcore/build.gradle.kts | 75 +++++++++++ .../composeResources/values/strings.xml | 3 + .../auth/appcore/ui/design/SkillsAppBar.kt | 99 +++++++++++++++ .../ui/preview/SkillsPreviewComposition.kt | 28 +++++ .../auth/appcore/ui/theme/Color.kt | 11 ++ .../auth/appcore/ui/theme/Dimensions.kt | 34 +++++ .../auth/appcore/ui/theme/SkillsTheme.kt | 37 ++++++ .../auth/appcore/ui/theme/Type.kt | 18 +++ .../statemachine/skills/auth/api/AuthInput.kt | 4 +- .../skills/auth/api/AuthUiState.kt | 7 +- .../auth/implementation/build.gradle.kts | 16 +++ .../composeResources/values/strings.xml | 3 + .../implementation/data/AuthGestureImpl.kt | 5 + .../implementation/data/AuthUiStateImpl.kt | 22 ++-- .../implementation/state/AuthStateFactory.kt | 7 ++ .../state/AuthStateFactoryImpl.kt | 19 ++- .../state/AuthenticatingState.kt | 78 ++++++++++++ .../state/AuthenticationErrorState.kt | 53 ++++++++ .../auth/implementation/state/FormState.kt | 6 + .../state/PreloadingErrorState.kt | 2 +- .../auth/implementation/ui/AuthErrorScreen.kt | 69 +++++++++++ .../auth/implementation/ui/AuthFormScreen.kt | 116 ++++++++++++++++++ .../implementation/ui/AuthLoadingScreen.kt | 25 ++++ .../auth/implementation/ui/AuthScreen.kt | 89 ++++++++++++++ .../auth/implementation/ui/AuthUiRenderer.kt | 3 +- .../implementation/ui/AuthUiRendererImpl.kt | 26 ++++ .../implementation/data/dataStatesFixtures.kt | 2 +- .../state/AuthenticatingStateTest.kt | 82 +++++++++++++ .../state/AuthenticationErrorStateTest.kt | 87 +++++++++++++ .../implementation/state/FormStateTest.kt | 28 ++++- .../state/PreloadingErrorStateTest.kt | 4 +- .../ui/AuthUiRendererImplTest.kt | 63 ++++++++++ .../exception/AuthenticationException.kt | 6 + .../usecase/AuthenticateWithPassword.kt | 13 ++ gradle/libs.versions.toml | 10 ++ settings.gradle.kts | 1 + .../common-child-flow/boilerplate/SKILL.md | 7 ++ .../references/ui-renderer-implementation.md | 22 ++++ .../references/ui-view-implementation.md | 42 +++++++ 39 files changed, 1195 insertions(+), 27 deletions(-) create mode 100644 examples/skills/appcore/build.gradle.kts create mode 100644 examples/skills/appcore/src/commonMain/composeResources/values/strings.xml create mode 100644 examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/design/SkillsAppBar.kt create mode 100644 examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/preview/SkillsPreviewComposition.kt create mode 100644 examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/theme/Color.kt create mode 100644 examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/theme/Dimensions.kt create mode 100644 examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/theme/SkillsTheme.kt create mode 100644 examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/theme/Type.kt create mode 100644 examples/skills/auth/implementation/src/commonMain/composeResources/values/strings.xml create mode 100644 examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticatingState.kt create mode 100644 examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticationErrorState.kt create mode 100644 examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthErrorScreen.kt create mode 100644 examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthFormScreen.kt create mode 100644 examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthLoadingScreen.kt create mode 100644 examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthScreen.kt create mode 100644 examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthUiRendererImpl.kt create mode 100644 examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticatingStateTest.kt create mode 100644 examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticationErrorStateTest.kt create mode 100644 examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthUiRendererImplTest.kt create mode 100644 examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/exception/AuthenticationException.kt create mode 100644 examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/usecase/AuthenticateWithPassword.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/references/ui-renderer-implementation.md create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/references/ui-view-implementation.md diff --git a/examples/skills/appcore/build.gradle.kts b/examples/skills/appcore/build.gradle.kts new file mode 100644 index 0000000..e9675bd --- /dev/null +++ b/examples/skills/appcore/build.gradle.kts @@ -0,0 +1,75 @@ +@file:Suppress("unused") + +/* +* Copyright 2023 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. +*/ +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) +} + +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 = "com.motorro" +version = versionName + +kotlin { + jvmToolchain(21) + + android { + namespace = "com.motorro.statemachine.skills.appcore" + compileSdk = androidCompileSdkVersion + minSdk = androidMinSdkVersion + + withHostTest { + isIncludeAndroidResources = true + } + + compilerOptions { + jvmTarget.set(JvmTarget.JVM_21) + } + + androidResources.enable = true + } + + sourceSets { + commonMain.dependencies { + api(libs.composeMultiplatform.runtime) + api(libs.composeMultiplatform.foundation) + api(libs.composeMultiplatform.ui) + api(libs.composeMultiplatform.resources) + api(libs.composeMultiplatform.material3) + api(libs.composeMultiplatform.icons) + implementation(libs.composeMultiplatform.navigationevent) + implementation(libs.composeMultiplatform.preview) + } + } +} + +dependencies { + androidRuntimeClasspath(libs.composeMultiplatform.tooling) +} + +compose.resources { + publicResClass = false + packageOfResClass = "com.motorro.statemachine.skills.appcore" + generateResClass = always +} + + diff --git a/examples/skills/appcore/src/commonMain/composeResources/values/strings.xml b/examples/skills/appcore/src/commonMain/composeResources/values/strings.xml new file mode 100644 index 0000000..182a3b9 --- /dev/null +++ b/examples/skills/appcore/src/commonMain/composeResources/values/strings.xml @@ -0,0 +1,3 @@ + + Back + diff --git a/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/design/SkillsAppBar.kt b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/design/SkillsAppBar.kt new file mode 100644 index 0000000..e09124c --- /dev/null +++ b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/design/SkillsAppBar.kt @@ -0,0 +1,99 @@ +package com.motorro.statemachine.auth.appcore.ui.design + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.rounded.AddCircle +import androidx.compose.material.icons.rounded.Call +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.navigationevent.NavigationEventInfo +import androidx.navigationevent.compose.NavigationBackHandler +import androidx.navigationevent.compose.rememberNavigationEventState +import com.motorro.statemachine.auth.appcore.ui.preview.SkillsPreviewComposition +import com.motorro.statemachine.skills.appcore.Res +import com.motorro.statemachine.skills.appcore.btn_back +import org.jetbrains.compose.resources.stringResource + +/** + * Common top bar sample + */ +@Composable +@OptIn(ExperimentalMaterial3Api::class) +fun SkillsAppBar( + title: String, + modifier: Modifier = Modifier, + topLevel: Boolean = true, + actions: @Composable RowScope.() -> Unit = { }, + onBack: () -> Unit = { } +) { + NavigationBackHandler( + state = rememberNavigationEventState( + currentInfo = NavigationEventInfo.None + ), + isBackEnabled = topLevel.not(), + onBackCompleted = onBack + ) + + TopAppBar( + title = { Text(title) }, + modifier = modifier, + navigationIcon = { + if (topLevel.not()) { + IconButton(onClick = onBack) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(Res.string.btn_back) + ) + } + } + }, + actions = actions + ) +} + + +private data class AppBarData(val title: String, val topLevel: Boolean = false, val actions: List = emptyList()) + +private class AppBarDataProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + AppBarData("Top Level", topLevel = true), + AppBarData("Top Level Actions", topLevel = true, listOf( + Icons.Rounded.AddCircle, + Icons.Rounded.Call + )), + AppBarData("Inner", topLevel = false), + AppBarData("Inner Actions", topLevel = false, listOf( + Icons.Rounded.AddCircle, + Icons.Rounded.Call + )) + ) +} + +@Preview +@Composable +private fun SkillsScreenPreview(@PreviewParameter(AppBarDataProvider::class) data: AppBarData?) { + if (null == data) return + SkillsPreviewComposition { + SkillsAppBar( + title = data.title, + topLevel = data.topLevel, + actions = { + data.actions.forEach { + IconButton(onClick = {}) { + Icon(it, null) + } + } + } + ) + } +} diff --git a/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/preview/SkillsPreviewComposition.kt b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/preview/SkillsPreviewComposition.kt new file mode 100644 index 0000000..c1f4d34 --- /dev/null +++ b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/preview/SkillsPreviewComposition.kt @@ -0,0 +1,28 @@ +package com.motorro.statemachine.auth.appcore.ui.preview + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.navigationevent.NavigationEventDispatcher +import androidx.navigationevent.NavigationEventDispatcherOwner +import androidx.navigationevent.compose.LocalNavigationEventDispatcherOwner +import com.motorro.statemachine.auth.appcore.ui.theme.SkillsTheme + +/** + * App preview with all the required locals and theming + */ +@Composable +fun SkillsPreviewComposition(content: @Composable () -> Unit) { + + val navEventDispatcherOwner = remember { + object : NavigationEventDispatcherOwner { + override val navigationEventDispatcher: NavigationEventDispatcher = NavigationEventDispatcher() + } + } + + SkillsTheme { + CompositionLocalProvider(LocalNavigationEventDispatcherOwner provides navEventDispatcherOwner) { + content() + } + } +} \ No newline at end of file diff --git a/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/theme/Color.kt b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/theme/Color.kt new file mode 100644 index 0000000..6df85d4 --- /dev/null +++ b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.motorro.statemachine.auth.appcore.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/theme/Dimensions.kt b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/theme/Dimensions.kt new file mode 100644 index 0000000..81aea4f --- /dev/null +++ b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/theme/Dimensions.kt @@ -0,0 +1,34 @@ +package com.motorro.statemachine.auth.appcore.ui.theme + +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * Common dimensions for the application + */ +data object Dimensions { + /** + * Extra small padding: 4dp + */ + val extraSmall: Dp = 4.dp + + /** + * Small padding: 8dp + */ + val small: Dp = 8.dp + + /** + * Medium padding: 16dp + */ + val medium: Dp = 16.dp + + /** + * Large padding: 24dp + */ + val large: Dp = 24.dp + + /** + * Extra large padding: 32dp + */ + val extraLarge: Dp = 32.dp +} diff --git a/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/theme/SkillsTheme.kt b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/theme/SkillsTheme.kt new file mode 100644 index 0000000..5be08ff --- /dev/null +++ b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/theme/SkillsTheme.kt @@ -0,0 +1,37 @@ +package com.motorro.statemachine.auth.appcore.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Typography +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 +) + +@Composable +fun SkillsTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + val colorScheme = when { + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography(), + content = content + ) +} diff --git a/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/theme/Type.kt b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/theme/Type.kt new file mode 100644 index 0000000..41a907d --- /dev/null +++ b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/theme/Type.kt @@ -0,0 +1,18 @@ +package com.motorro.statemachine.common.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) +) \ No newline at end of file diff --git a/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthInput.kt b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthInput.kt index 4a8d512..9631802 100644 --- a/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthInput.kt +++ b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthInput.kt @@ -2,6 +2,6 @@ package com.motorro.statemachine.skills.auth.api /** * Initializing data for the feature flow - * @param message Example of a property passed to authentication flow + * @property skippable If true, allows user to skip authentication */ -data class AuthInput(val message: String) \ No newline at end of file +data class AuthInput(val skippable: Boolean) \ No newline at end of file diff --git a/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthUiState.kt b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthUiState.kt index d2fbef4..edcb662 100644 --- a/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthUiState.kt +++ b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthUiState.kt @@ -4,9 +4,4 @@ package com.motorro.statemachine.skills.auth.api * A base type for authentication UI state * Available to a feature consumer */ -interface AuthUiState { - /** - * Some common property available to a feature consumer - */ - val interactive: Boolean -} \ No newline at end of file +interface AuthUiState \ No newline at end of file diff --git a/examples/skills/auth/implementation/build.gradle.kts b/examples/skills/auth/implementation/build.gradle.kts index 29d729c..8caf323 100644 --- a/examples/skills/auth/implementation/build.gradle.kts +++ b/examples/skills/auth/implementation/build.gradle.kts @@ -45,6 +45,8 @@ kotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_21) } + + androidResources.enable = true } sourceSets { @@ -54,11 +56,14 @@ kotlin { implementation(project(":commonflow:commonflow-data")) implementation(project(":commonflow:commonflow-compose")) implementation(project(":examples:skills:domain")) + implementation(project(":examples:skills:appcore")) implementation(project(":examples:skills:auth:api")) implementation(libs.kotlin.coroutines.core) implementation(libs.composeMultiplatform.runtime) implementation(libs.composeMultiplatform.foundation) implementation(libs.napier) + implementation(libs.composeMultiplatform.resources) + implementation(libs.composeMultiplatform.preview) } commonTest.dependencies { implementation(libs.test.kotlin) @@ -67,3 +72,14 @@ kotlin { } } +dependencies { + androidRuntimeClasspath(libs.composeMultiplatform.tooling) +} + +compose.resources { + publicResClass = false + packageOfResClass = "com.motorro.statemachine.skills.auth.implementation" + generateResClass = always +} + + diff --git a/examples/skills/auth/implementation/src/commonMain/composeResources/values/strings.xml b/examples/skills/auth/implementation/src/commonMain/composeResources/values/strings.xml new file mode 100644 index 0000000..ac3e2ab --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/composeResources/values/strings.xml @@ -0,0 +1,3 @@ + + Authentication + diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthGestureImpl.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthGestureImpl.kt index 2422c7a..ad78fe6 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthGestureImpl.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthGestureImpl.kt @@ -29,5 +29,10 @@ internal sealed class AuthGestureImpl : AuthGesture { * Password input changed */ data class PasswordChanged(val value: String) : Form() + + /** + * Skips the authentication + */ + data object Skip : Form() } } \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthUiStateImpl.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthUiStateImpl.kt index 1f7be37..3555158 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthUiStateImpl.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthUiStateImpl.kt @@ -6,11 +6,6 @@ import com.motorro.statemachine.skills.auth.api.AuthUiState * All gestures available in this feature */ internal sealed class AuthUiStateImpl : AuthUiState { - /** - * Some common property implementation - */ - override val interactive: Boolean = true - /** * Loading data */ @@ -20,13 +15,22 @@ internal sealed class AuthUiStateImpl : AuthUiState { * Login form * @property username Username * @property password Password - * @property loginEnabled If true, the form is complete and the user can log-in + * @property passwordRequirements Password requirements description + * @property loginEnabled If true, the form is complete and the user can log in + * @property canSkip If true, the user can skip the authentication */ - data class Form(val username: String, val password: String, val loginEnabled: Boolean) : AuthUiStateImpl() + data class Form( + val username: String, + val password: String, + val passwordRequirements: String, + val loginEnabled: Boolean, + val canSkip: Boolean + ) : AuthUiStateImpl() /** * Displays error - * @param message Error message to display + * @property message Error message to display + * @property canRetry If true, the error is recoverable and can be retried */ - data class Error(val message: String) : AuthUiStateImpl() + data class Error(val message: String, val canRetry: Boolean) : AuthUiStateImpl() } \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthStateFactory.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthStateFactory.kt index a58bb83..ab0cdcb 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthStateFactory.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthStateFactory.kt @@ -38,6 +38,13 @@ internal interface AuthStateFactory { */ fun authenticating(data: AuthStateData): AuthState + /** + * Creates an authentication error state. + * @param data Interstate data + * @param error Error that occurred during the initialization + */ + fun authenticationError(data: AuthStateData, error: AppException): AuthState + /** * Creates the termination state that finishes the flow. * All interstate data is lost as not needed anymore. diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthStateFactoryImpl.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthStateFactoryImpl.kt index 1dd5480..c7d4fbd 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthStateFactoryImpl.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthStateFactoryImpl.kt @@ -13,6 +13,7 @@ import io.github.aakira.napier.Napier */ internal class AuthStateFactoryImpl( private val preloadingStateFactory: PreloadingState.Factory, + private val authenticatingStateFactory: AuthenticatingState.Factory, flowHost: CommonFlowHost, renderer: AuthUiRenderer ) : AuthStateFactory { @@ -52,11 +53,21 @@ internal class AuthStateFactoryImpl( ) /** - * Left for the future + * Creates authenticating state */ - override fun authenticating(data: AuthStateData): AuthState { - TODO("Implement the authenticating state") - } + override fun authenticating(data: AuthStateData): AuthState = authenticatingStateFactory.create( + context, + data + ) + + /** + * Creates authentication error state + */ + override fun authenticationError(data: AuthStateData, error: AppException) = AuthenticationErrorState( + context, + data, + error + ) /** * Ad-hoc terminated state implementation diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticatingState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticatingState.kt new file mode 100644 index 0000000..03d9f55 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticatingState.kt @@ -0,0 +1,78 @@ +package com.motorro.statemachine.skills.auth.implementation.state + +import com.motorro.commonstatemachine.skills.domain.exception.toAppException +import com.motorro.commonstatemachine.skills.domain.usecase.AuthenticateWithPassword +import com.motorro.statemachine.skills.auth.api.AuthResult +import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.statemachine.skills.auth.implementation.data.AuthStateData +import io.github.aakira.napier.Napier +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch + +/** + * Authenticates the user + * @param context Auth context + * @param data Interstate data + * @param authenticate Authenticate usecase + */ +internal class AuthenticatingState( + context: AuthContext, + private val data: AuthStateData, + private val authenticate: AuthenticateWithPassword +) : BaseAuthState(context) { + + /** + * State start-up + */ + override fun doStart() { + setUiState { + renderLoading() + } + doAuthenticate() + } + + /** + * Gesture processing + */ + override fun doProcess(gesture: AuthGestureImpl) { + when(gesture) { + is AuthGestureImpl.Back -> setMachineState { + Napier.d { "Back gesture. Returning to form..." } + form(data) + } + else -> super.doProcess(gesture) + } + } + + /** + * Authentication function + */ + private fun doAuthenticate() = stateScope.launch { + try { + authenticate(data.username, data.password) + // Advance to the next state + setMachineState { + terminated(AuthResult(authenticated = true)) + } + } catch (e: Throwable) { + ensureActive() + Napier.w(e) { "Error during authentication" } + // Advance to the error state + setMachineState { + authenticationError(data, e.toAppException()) + } + } + } + + /** + * State factory + * State dependencies are injected here + */ + class Factory(private val authenticate: AuthenticateWithPassword) { + fun create(context: AuthContext, data: AuthStateData): AuthState = AuthenticatingState( + context = context, + data = data, + authenticate = authenticate + ) + } +} diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticationErrorState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticationErrorState.kt new file mode 100644 index 0000000..3313d1b --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticationErrorState.kt @@ -0,0 +1,53 @@ +package com.motorro.statemachine.skills.auth.implementation.state + +import com.motorro.commonstatemachine.skills.domain.exception.AppException +import com.motorro.commonstatemachine.skills.domain.exception.IOException +import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.statemachine.skills.auth.implementation.data.AuthStateData +import io.github.aakira.napier.Napier + +/** + * Handles authentication error + * If the error is recoverable (IOException), allows to transfer back to authenticating to retry. + * Otherwise returns to form. + * @param context Auth context + * @param data Interstate data + * @param error Error to handle + */ +internal class AuthenticationErrorState( + context: AuthContext, + private val data: AuthStateData, + private val error: AppException +) : BaseAuthState(context) { + override fun doStart() { + setUiState { + renderFullScreenError(error, canRetry()) + } + } + + private fun canRetry(): Boolean = when(error) { + is IOException -> true + else -> false + } + + override fun doProcess(gesture: AuthGestureImpl) { + when(gesture) { + is AuthGestureImpl.Back -> setMachineState { + Napier.d { "Back gesture. Returning to form..." } + form(data) + } + is AuthGestureImpl.Action -> if (canRetry()) { + Napier.d { "Action gesture. Retrying authentication..." } + setMachineState { + authenticating(data) + } + } else { + Napier.d { "Action gesture. Returning to form..." } + setMachineState { + form(data) + } + } + else -> super.doProcess(gesture) + } + } +} diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/FormState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/FormState.kt index 9575504..bab742b 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/FormState.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/FormState.kt @@ -64,6 +64,12 @@ internal class FormState( is AuthGestureImpl.Form.PasswordChanged -> updateData { copy(password = gesture.value) } + is AuthGestureImpl.Form.Skip -> if (data.input.skippable) { + Napier.d { "Skipping authentication..." } + setMachineState { + terminated(AuthResult(false)) + } + } } } } \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingErrorState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingErrorState.kt index 975dd6f..8c9907b 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingErrorState.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingErrorState.kt @@ -20,7 +20,7 @@ internal class PreloadingErrorState( ) : BaseAuthState(context) { override fun doStart() { setUiState { - renderFullScreenError(error.message, canRetry()) + renderFullScreenError(error, canRetry()) } } diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthErrorScreen.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthErrorScreen.kt new file mode 100644 index 0000000..2dd2bce --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthErrorScreen.kt @@ -0,0 +1,69 @@ +package com.motorro.statemachine.skills.auth.implementation.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.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.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.motorro.statemachine.auth.appcore.ui.preview.SkillsPreviewComposition +import com.motorro.statemachine.auth.appcore.ui.theme.Dimensions +import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.statemachine.skills.auth.implementation.data.AuthUiStateImpl + +@Composable +internal fun AuthErrorScreen( + state: AuthUiStateImpl.Error, + modifier: Modifier = Modifier, + onGesture: (AuthGestureImpl) -> Unit +) { + Column( + modifier = modifier.fillMaxSize().padding(Dimensions.medium), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = state.message, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodyLarge + ) + Spacer(Modifier.height(Dimensions.medium)) + Button(onClick = { onGesture(AuthGestureImpl.Action) }) { + Text(if (state.canRetry) "Retry" else "Close") + } + } +} + +private class AuthErrorDataProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + AuthUiStateImpl.Error( + message = "A recoverable error occurred", + canRetry = true + ), + AuthUiStateImpl.Error( + message = "A non-recoverable error occurred", + canRetry = false + ) + ) +} + +@Preview +@Composable +private fun AuthErrorPreview(@PreviewParameter(AuthErrorDataProvider::class) state: AuthUiStateImpl.Error?) { + if (null == state) return + SkillsPreviewComposition { + AuthErrorScreen( + state = state, + modifier = Modifier.fillMaxSize() + ) {} + } +} diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthFormScreen.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthFormScreen.kt new file mode 100644 index 0000000..a450b94 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthFormScreen.kt @@ -0,0 +1,116 @@ +package com.motorro.statemachine.skills.auth.implementation.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.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +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.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.motorro.statemachine.auth.appcore.ui.preview.SkillsPreviewComposition +import com.motorro.statemachine.auth.appcore.ui.theme.Dimensions +import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.statemachine.skills.auth.implementation.data.AuthUiStateImpl + +@Composable +internal fun AuthFormScreen( + state: AuthUiStateImpl.Form, + modifier: Modifier = Modifier, + onGesture: (AuthGestureImpl) -> Unit, +) { + Column( + modifier = modifier.fillMaxSize().padding(Dimensions.medium), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + TextField( + value = state.username, + onValueChange = { onGesture(AuthGestureImpl.Form.UsernameChanged(it)) }, + label = { Text("Username") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true + ) + Spacer(Modifier.height(Dimensions.medium)) + TextField( + value = state.password, + onValueChange = { onGesture(AuthGestureImpl.Form.PasswordChanged(it)) }, + label = { Text("Password") }, + modifier = Modifier.fillMaxWidth(), + visualTransformation = PasswordVisualTransformation(), + singleLine = true + ) + Text( + text = state.passwordRequirements, + modifier = Modifier.fillMaxWidth().padding(top = Dimensions.small), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(Modifier.height(Dimensions.medium)) + Button( + onClick = { onGesture(AuthGestureImpl.Action) }, + enabled = state.loginEnabled, + modifier = Modifier.fillMaxWidth() + ) { + Text("Login") + } + if (state.canSkip) { + Spacer(Modifier.height(Dimensions.medium)) + OutlinedButton( + onClick = { onGesture(AuthGestureImpl.Form.Skip) }, + modifier = Modifier.fillMaxWidth() + ) { + Text("Skip") + } + } + } +} + +private class AuthFormDataProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + AuthUiStateImpl.Form( + username = "", + password = "", + passwordRequirements = "At least 8 characters", + loginEnabled = false, + canSkip = true + ), + AuthUiStateImpl.Form( + username = "user", + password = "password", + passwordRequirements = "At least 8 characters", + loginEnabled = true, + canSkip = true + ), + AuthUiStateImpl.Form( + username = "user", + password = "password", + passwordRequirements = "At least 8 characters", + loginEnabled = true, + canSkip = false + ) + ) +} + +@Preview +@Composable +private fun AuthFormPreview(@PreviewParameter(AuthFormDataProvider::class) state: AuthUiStateImpl.Form?) { + if (null == state) return + SkillsPreviewComposition { + AuthFormScreen( + state = state, + modifier = Modifier.fillMaxSize() + ) {} + } +} diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthLoadingScreen.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthLoadingScreen.kt new file mode 100644 index 0000000..2923ffe --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthLoadingScreen.kt @@ -0,0 +1,25 @@ +package com.motorro.statemachine.skills.auth.implementation.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.motorro.statemachine.auth.appcore.ui.preview.SkillsPreviewComposition + +@Composable +internal fun AuthLoadingScreen(modifier: Modifier = Modifier) { + Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } +} + +@Preview +@Composable +private fun AuthLoadingPreview() { + SkillsPreviewComposition { + AuthLoadingScreen(Modifier.fillMaxSize()) + } +} diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthScreen.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthScreen.kt new file mode 100644 index 0000000..b50929d --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthScreen.kt @@ -0,0 +1,89 @@ +package com.motorro.statemachine.skills.auth.implementation.ui + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.motorro.statemachine.auth.appcore.ui.design.SkillsAppBar +import com.motorro.statemachine.auth.appcore.ui.preview.SkillsPreviewComposition +import com.motorro.statemachine.skills.auth.implementation.Res +import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.statemachine.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.statemachine.skills.auth.implementation.title +import org.jetbrains.compose.resources.stringResource + +/** + * Authentication screen + * @param state UI state + * @param onGesture Gesture handler + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun AuthScreen( + state: AuthUiStateImpl, + onGesture: (AuthGestureImpl) -> Unit, +) { + val onBack = remember { + { onGesture(AuthGestureImpl.Back) } + } + Scaffold( + topBar = { + SkillsAppBar( + title = stringResource(Res.string.title), + modifier = Modifier.fillMaxWidth(), + topLevel = false, + onBack = onBack + ) + } + ) { paddingValues -> + val modifier = Modifier.fillMaxSize().padding(paddingValues) + when(state) { + AuthUiStateImpl.Loading -> AuthLoadingScreen(modifier) + is AuthUiStateImpl.Error -> AuthErrorScreen( + state = state, + modifier = modifier, + onGesture = onGesture + ) + is AuthUiStateImpl.Form -> AuthFormScreen( + state = state, + modifier = modifier, + onGesture = onGesture + ) + } + } +} + +private class AuthScreenDataProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + AuthUiStateImpl.Loading, + AuthUiStateImpl.Form( + username = "user", + password = "password", + passwordRequirements = "At least 8 characters", + loginEnabled = true, + canSkip = true + ), + AuthUiStateImpl.Error( + message = "A recoverable error occurred", + canRetry = true + ) + ) +} + +@Preview +@Composable +private fun SkillsScreenPreview(@PreviewParameter(AuthScreenDataProvider::class) state: AuthUiStateImpl?) { + if (null == state) return + SkillsPreviewComposition { + AuthScreen( + state = state + ) {} + } +} diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthUiRenderer.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthUiRenderer.kt index e366100..743c579 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthUiRenderer.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthUiRenderer.kt @@ -1,5 +1,6 @@ package com.motorro.statemachine.skills.auth.implementation.ui +import com.motorro.commonstatemachine.skills.domain.exception.AppException import com.motorro.statemachine.skills.auth.implementation.data.AuthStateData import com.motorro.statemachine.skills.auth.implementation.data.AuthUiStateImpl @@ -28,5 +29,5 @@ internal interface AuthUiRenderer { * @param error Error to present to the user * @param canRetry Error could be retried */ - fun renderFullScreenError(error: String, canRetry: Boolean): AuthUiStateImpl + fun renderFullScreenError(error: AppException, canRetry: Boolean): AuthUiStateImpl } \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthUiRendererImpl.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthUiRendererImpl.kt new file mode 100644 index 0000000..635dc09 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthUiRendererImpl.kt @@ -0,0 +1,26 @@ +package com.motorro.statemachine.skills.auth.implementation.ui + +import com.motorro.commonstatemachine.skills.domain.exception.AppException +import com.motorro.statemachine.skills.auth.implementation.data.AuthStateData +import com.motorro.statemachine.skills.auth.implementation.data.AuthUiStateImpl + +/** + * UI renderer implementation + */ +internal class AuthUiRendererImpl : AuthUiRenderer { + + override fun renderLoading(): AuthUiStateImpl = AuthUiStateImpl.Loading + + override fun renderForm(data: AuthStateData, isValid: Boolean) = AuthUiStateImpl.Form( + username = data.username, + password = data.password, + passwordRequirements = data.passwordRequirements.description, + loginEnabled = isValid, + canSkip = data.input.skippable + ) + + override fun renderFullScreenError(error: AppException, canRetry: Boolean) = AuthUiStateImpl.Error( + message = error.message, + canRetry = canRetry + ) +} \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/dataStatesFixtures.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/dataStatesFixtures.kt index cf85e8f..f479a7b 100644 --- a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/dataStatesFixtures.kt +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/dataStatesFixtures.kt @@ -9,7 +9,7 @@ import com.motorro.statemachine.skills.auth.api.AuthResult /** * Input fixture */ -internal val INPUT = AuthInput("Please login!") +internal val INPUT = AuthInput(true) /** * Result fixture diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticatingStateTest.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticatingStateTest.kt new file mode 100644 index 0000000..98ee331 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticatingStateTest.kt @@ -0,0 +1,82 @@ +package com.motorro.statemachine.skills.auth.implementation.state + +import com.motorro.commonstatemachine.skills.domain.usecase.AuthenticateWithPassword +import com.motorro.statemachine.skills.auth.api.AuthResult +import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.statemachine.skills.auth.implementation.data.FATAL_ERROR +import com.motorro.statemachine.skills.auth.implementation.data.UI_STATE +import com.motorro.statemachine.skills.auth.implementation.data.VALID_FORM_STATE +import dev.mokkery.MockMode +import dev.mokkery.answering.calls +import dev.mokkery.answering.returns +import dev.mokkery.answering.throws +import dev.mokkery.every +import dev.mokkery.everySuspend +import dev.mokkery.matcher.any +import dev.mokkery.mock +import dev.mokkery.verify +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.test.Test + +internal class AuthenticatingStateTest : BaseStateTest() { + private lateinit var authenticateMock: AuthenticateWithPassword + private lateinit var state: AuthenticatingState + + override fun doInit() { + authenticateMock = mock(MockMode.strict) + state = AuthenticatingState(context, VALID_FORM_STATE, authenticateMock) + every { renderer.renderLoading() } returns UI_STATE + } + + @Test + fun rendersLoadingOnStart() = test { + everySuspend { authenticateMock(any(), any()) } calls { suspendCancellableCoroutine { } } + + state.start(stateMachine) + + verify { + renderer.renderLoading() + stateMachine.setUiState(UI_STATE) + } + } + + @Test + fun terminatesWhenAuthenticated() = test { + everySuspend { authenticateMock(any(), any()) } returns Unit + every { stateFactory.terminated(any()) } returns nextState + + state.start(stateMachine) + + verify { + stateFactory.terminated(AuthResult(authenticated = true)) + stateMachine.setMachineState(nextState) + } + } + + @Test + fun transfersToErrorWhenFailed() = test { + everySuspend { authenticateMock(any(), any()) } throws FATAL_ERROR + every { stateFactory.authenticationError(any(), any()) } returns nextState + + state.start(stateMachine) + + verify { + stateFactory.authenticationError(VALID_FORM_STATE, any()) + stateMachine.setMachineState(nextState) + } + } + + @Test + fun returnsToFormOnBackWhileAuthenticating() = test { + everySuspend { authenticateMock(any(), any()) } calls { suspendCancellableCoroutine { } } + every { stateFactory.form(any()) } returns nextState + + state.start(stateMachine) + state.process(AuthGestureImpl.Back) + + verify { + stateFactory.form(VALID_FORM_STATE) + stateMachine.setMachineState(nextState) + } + } +} diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticationErrorStateTest.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticationErrorStateTest.kt new file mode 100644 index 0000000..3348b37 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticationErrorStateTest.kt @@ -0,0 +1,87 @@ +package com.motorro.statemachine.skills.auth.implementation.state + +import com.motorro.commonstatemachine.skills.domain.exception.AppException +import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.statemachine.skills.auth.implementation.data.FATAL_ERROR +import com.motorro.statemachine.skills.auth.implementation.data.NON_FATAL_ERROR +import com.motorro.statemachine.skills.auth.implementation.data.UI_STATE +import com.motorro.statemachine.skills.auth.implementation.data.VALID_FORM_STATE +import dev.mokkery.answering.returns +import dev.mokkery.every +import dev.mokkery.matcher.any +import dev.mokkery.verify +import kotlin.test.Test + +internal class AuthenticationErrorStateTest : BaseStateTest() { + override fun doInit() { + every { renderer.renderFullScreenError(any(), any()) } returns UI_STATE + } + + private fun createState(error: AppException) = AuthenticationErrorState( + context, + VALID_FORM_STATE, + error + ) + + @Test + fun rendersFatalErrorOnStart() = test { + val state = createState(FATAL_ERROR) + state.start(stateMachine) + verify { + renderer.renderFullScreenError(FATAL_ERROR, canRetry = false) + stateMachine.setUiState(UI_STATE) + } + } + + @Test + fun rendersNonFatalErrorOnStart() = test { + val state = createState(NON_FATAL_ERROR) + state.start(stateMachine) + verify { + renderer.renderFullScreenError(NON_FATAL_ERROR, canRetry = true) + stateMachine.setUiState(UI_STATE) + } + } + + @Test + fun returnsToFormOnBack() = test { + val state = createState(NON_FATAL_ERROR) + every { stateFactory.form(any()) } returns nextState + + state.start(stateMachine) + state.process(AuthGestureImpl.Back) + + verify { + stateFactory.form(VALID_FORM_STATE) + stateMachine.setMachineState(nextState) + } + } + + @Test + fun retriesOnActionIfRetryable() = test { + val state = createState(NON_FATAL_ERROR) + every { stateFactory.authenticating(any()) } returns nextState + + state.start(stateMachine) + state.process(AuthGestureImpl.Action) + + verify { + stateFactory.authenticating(VALID_FORM_STATE) + stateMachine.setMachineState(nextState) + } + } + + @Test + fun returnsToFormOnActionIfNotRetryable() = test { + val state = createState(FATAL_ERROR) + every { stateFactory.form(any()) } returns nextState + + state.start(stateMachine) + state.process(AuthGestureImpl.Action) + + verify { + stateFactory.form(VALID_FORM_STATE) + stateMachine.setMachineState(nextState) + } + } +} diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/FormStateTest.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/FormStateTest.kt index f5ea717..fedaee8 100644 --- a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/FormStateTest.kt +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/FormStateTest.kt @@ -92,8 +92,34 @@ internal class FormStateTest : BaseStateTest() { state.process(AuthGestureImpl.Back) verify { - stateFactory.terminated(AuthResult(false)) + stateFactory.terminated(AuthResult(authenticated = false)) stateMachine.setMachineState(nextState) } } + + @Test + fun processesSkip() = test { + every { stateFactory.terminated(any()) } returns nextState + + state.start(stateMachine) + state.process(AuthGestureImpl.Form.Skip) + + verify { + stateFactory.terminated(AuthResult(authenticated = false)) + stateMachine.setMachineState(nextState) + } + } + + @Test + fun doesNotProcessSkipIfNotSkippable() = test { + state.data = EMPTY_STATE.copy(input = EMPTY_STATE.input.copy(skippable = false)) + + state.start(stateMachine) + state.process(AuthGestureImpl.Form.Skip) + + verify(VerifyMode.exactly(0)) { + stateFactory.terminated(any()) + stateMachine.setMachineState(any()) + } + } } diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingErrorStateTest.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingErrorStateTest.kt index 3b15050..f0e63be 100644 --- a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingErrorStateTest.kt +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingErrorStateTest.kt @@ -29,7 +29,7 @@ internal class PreloadingErrorStateTest : BaseStateTest() { val state = createState(FATAL_ERROR) state.start(stateMachine) verify { - renderer.renderFullScreenError(FATAL_ERROR.message, canRetry = false) + renderer.renderFullScreenError(FATAL_ERROR, canRetry = false) stateMachine.setUiState(UI_STATE) } } @@ -39,7 +39,7 @@ internal class PreloadingErrorStateTest : BaseStateTest() { val state = createState(NON_FATAL_ERROR) state.start(stateMachine) verify { - renderer.renderFullScreenError(NON_FATAL_ERROR.message, canRetry = true) + renderer.renderFullScreenError(NON_FATAL_ERROR, canRetry = true) stateMachine.setUiState(UI_STATE) } } diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthUiRendererImplTest.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthUiRendererImplTest.kt new file mode 100644 index 0000000..7ce0472 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthUiRendererImplTest.kt @@ -0,0 +1,63 @@ +package com.motorro.statemachine.skills.auth.implementation.ui + +import com.motorro.statemachine.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.statemachine.skills.auth.implementation.data.EMPTY_STATE +import com.motorro.statemachine.skills.auth.implementation.data.FATAL_ERROR +import com.motorro.statemachine.skills.auth.implementation.data.VALID_FORM_STATE +import kotlin.test.Test +import kotlin.test.assertEquals + +internal class AuthUiRendererImplTest { + private val renderer = AuthUiRendererImpl() + + @Test + fun rendersLoading() { + assertEquals(AuthUiStateImpl.Loading, renderer.renderLoading()) + } + + @Test + fun rendersForm() { + val uiState = renderer.renderForm(VALID_FORM_STATE, isValid = true) + val expected = AuthUiStateImpl.Form( + username = VALID_FORM_STATE.username, + password = VALID_FORM_STATE.password, + passwordRequirements = VALID_FORM_STATE.passwordRequirements.description, + loginEnabled = true, + canSkip = true + ) + assertEquals(expected, uiState) + } + + @Test + fun rendersFormInvalid() { + val uiState = renderer.renderForm(EMPTY_STATE, isValid = false) + val expected = AuthUiStateImpl.Form( + username = EMPTY_STATE.username, + password = EMPTY_STATE.password, + passwordRequirements = EMPTY_STATE.passwordRequirements.description, + loginEnabled = false, + canSkip = true + ) + assertEquals(expected, uiState) + } + + @Test + fun rendersFullScreenError() { + val uiState = renderer.renderFullScreenError(FATAL_ERROR, canRetry = false) + val expected = AuthUiStateImpl.Error( + message = FATAL_ERROR.message, + canRetry = false + ) + assertEquals(expected, uiState) + } + + @Test + fun rendersFullScreenErrorWithRetry() { + val uiState = renderer.renderFullScreenError(FATAL_ERROR, canRetry = true) + val expected = AuthUiStateImpl.Error( + message = FATAL_ERROR.message, + canRetry = true + ) + assertEquals(expected, uiState) + } +} diff --git a/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/exception/AuthenticationException.kt b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/exception/AuthenticationException.kt new file mode 100644 index 0000000..6bd8c7f --- /dev/null +++ b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/exception/AuthenticationException.kt @@ -0,0 +1,6 @@ +package com.motorro.commonstatemachine.skills.domain.exception + +/** + * Authentication exception + */ +class AuthenticationException(message: String, cause: Throwable? = null) : AppException(message, cause) \ No newline at end of file diff --git a/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/usecase/AuthenticateWithPassword.kt b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/usecase/AuthenticateWithPassword.kt new file mode 100644 index 0000000..9edeae0 --- /dev/null +++ b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/usecase/AuthenticateWithPassword.kt @@ -0,0 +1,13 @@ +package com.motorro.commonstatemachine.skills.domain.usecase + +/** + * Authenticates with a password + */ +interface AuthenticateWithPassword { + /** + * Authenticates with [username] and [password] + * @param username Username + * @param password Password + */ + suspend operator fun invoke(username: String, password: String) +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dda0296..fb40285 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,8 +7,11 @@ androidxFragment = "1.9.0" androidxHilt = "1.4.0" compose = "2026.09.00" composeMultiplatform = "1.12.0" +composeMultiplatformMaterial = "1.9.0" +composeMultiplatformMaterialIcons = "1.7.3" viewmodelMultiplatform = "2.11.0" lificyclelMultiplatform = "2.11.0" +navigationMultiplatform = "1.1.0" composeActivity = "1.13.0" composeViewmodel = "2.11.0" coroutines = "1.11.0" @@ -83,8 +86,15 @@ compose-foundation-layouts = { module = "androidx.compose.foundation:foundation- composeMultiplatform-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "composeMultiplatform" } composeMultiplatform-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "composeMultiplatform" } +composeMultiplatform-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "composeMultiplatform" } +composeMultiplatform-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "composeMultiplatformMaterial" } +composeMultiplatform-icons = { module = "org.jetbrains.compose.material:material-icons-extended", version.ref = "composeMultiplatformMaterialIcons" } +composeMultiplatform-resources = { module = "org.jetbrains.compose.components:components-resources", 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" } +composeMultiplatform-navigationevent = { module = "org.jetbrains.androidx.navigationevent:navigationevent-compose", version.ref = "navigationMultiplatform" } +composeMultiplatform-tooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "composeMultiplatform" } +composeMultiplatform-preview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "composeMultiplatform" } hilt_android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } hilt_compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } diff --git a/settings.gradle.kts b/settings.gradle.kts index b858c34..10a00e1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -74,6 +74,7 @@ include( ":examples:books:book:demo", ":examples:books:app", ":examples:skills:domain", + ":examples:skills:appcore", ":examples:skills:auth:api", ":examples:skills:auth:implementation" ) diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md b/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md index 3dbabff..541107d 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md @@ -109,3 +109,10 @@ Refer to [this manual](references/state-implementation.md) to get the idea of th Repeat this step for every state you need to create. Create a unit-test for every created machine-state according to [this manual](references/state-test-implementation.md) +## Step 5. Create basic Compose views +To make the module complete, create some views for the created UI-states if user didn't tell you +otherwise or has given you some special instructions, screenshots, etc. +Follow these steps to implement the views: + +1. [Implement UI renderer if used](references/ui-renderer-implementation.md) +2. [Implement sample composables](references/ui-view-implementation.md) \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/ui-renderer-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/ui-renderer-implementation.md new file mode 100644 index 0000000..7a53cff --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/ui-renderer-implementation.md @@ -0,0 +1,22 @@ +# Module implementation: renderer implementation +Use this reference to implement the [UI-renderer](renderer-factory-context-interface-implementation.md#step-4-ui-renderer-interface) (if used in the module). + +## Step 1. Implement renderer +Follow these steps: + +- Create the implementation class in the `ui` package of the implementation module and add the UI-renderer interface to the list of implementations. +- Enumerate all [UI-states](data-and-ui-implementation.md#step-2-ui-states) and check the source UI-renderer interface. + Find the UI-states corresponding to renderer methods. +- Implement the interface methods transforming the passed parameters to the respectful states. +- Renderer may require other dependencies - resource mappers, etc. Make the class injectable using the DI framework used in a project. + +Take a look at [example](../assets/example/implementation/ui/AuthUiRendererImpl.kt) for our Auth application. + +## Step 2. Create a renderer test +Sometimes UI has a complex model that requires a lot to render. +Secure the logic by writing a unit-test for the renderer. + +- Check every interface method +- Use [fixtures](test-fixtures-implementation.md) if available, modify them by copy-constructors if needed, or add custom. + +Take a look at the [example](../assets/example/implementation-tests/ui/AuthUiRendererImplTest.kt). \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/ui-view-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/ui-view-implementation.md new file mode 100644 index 0000000..04476cc --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/ui-view-implementation.md @@ -0,0 +1,42 @@ +# Module implementation: basic views +Use this reference to implement the basic views for the implementation. + +## Step 1. Implement scaffolding screen +Create the scaffolding composable that will draw all the [UI States](data-and-ui-implementation.md#step-2-ui-states): + +- Follow common design system and approach in a project if available (scaffolds, appbars, themes, dimensions, etc.). +- If no design system found use the following approach: + - Use `Scaffold` as a top-level element + - Create top app bar with a module title +- The function should accept the following parameters: + - Required: `uiState` - one of the [UI State implementation](data-and-ui-implementation.md#step-2-ui-states) will be passed. + - Required: `onGesture` - a callback accepting [Gesture implementation](data-and-ui-implementation.md#step-1-ui-gestures). Make this parameter the last one. + - Optional: `modifier` with a default value in case this is a common approach in current project. +- Place `when` operator in the content section and implement all branches of the UI states. +- All functions should be internal at most + +See the [example](../assets/example/implementation/ui/AuthScreen.kt) for our sample authentication project. + +## Step 2. Implement child views +Create a separate file for child views. Create a file per each [UI State](data-and-ui-implementation.md#step-2-ui-states): + +- Follow common design system and approach in a project if available (scaffolds, appbars, themes, dimensions, etc.). +- The function should accept the following parameters: + - Required: `uiState` - the concrete [UI State implementation](data-and-ui-implementation.md#step-2-ui-states) will be passed. + - Required: `onGesture` - a callback accepting [Gesture implementation](data-and-ui-implementation.md#step-1-ui-gestures). Make this parameter the last one. + - Required: `modifier` with a default value. +- If no design system found use the following approach: + - Each screen should take `fillMaxSize()` to occupy the available space. + - Center content both horizontally (`Alignment.CenterHorizontally`) and vertically (`Arrangement.Center` for `Column` or `Alignment.Center` for `Box`). + - Use common project padding if defined in the design system (e.g., `Dimensions.medium`) for the main container. + - Use design system theme or `MaterialTheme` for colors and typography to ensure consistency. +- Evaluate each UI state and create corresponding widgets +- Use `onGesture` to pass user gestures. Consider moving the lambas in `onChange` callbacks to `remember` to optimize recompositions if you find it effective. +- Generate previews. If you think multiple previews are needed - use data providers. +- Put a child view to the `when` branches of a scaffolding screen. + +See the examples: + +- [Loading view](../assets/example/implementation/ui/AuthLoadingScreen.kt) +- [Form view](../assets/example/implementation/ui/AuthFormScreen.kt) +- [Error view](../assets/example/implementation/ui/AuthErrorScreen.kt) From d2be3402cf2a17927e973682fbc3772daac127fa Mon Sep 17 00:00:00 2001 From: motorro Date: Sun, 20 Sep 2026 12:28:45 +0200 Subject: [PATCH 08/32] DI and API implementation Refs: #14 --- .github/workflows/skills.yaml | 2 +- build.gradle.kts | 1 + examples/skills/auth/demo/.gitignore | 1 + examples/skills/auth/demo/build.gradle.kts | 127 ++++++++++++ examples/skills/auth/demo/proguard-rules.pro | 21 ++ .../auth/demo/src/main/AndroidManifest.xml | 26 +++ .../statemachine/skills/auth/demo/App.kt | 93 +++++++++ .../skills/auth/demo/MainActivity.kt | 49 +++++ .../skills/auth/demo/MainViewModel.kt | 15 ++ .../drawable-v24/ic_launcher_foreground.xml | 43 ++++ .../res/drawable/ic_launcher_background.xml | 183 ++++++++++++++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 18 ++ .../mipmap-anydpi-v26/ic_launcher_round.xml | 18 ++ .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes .../auth/demo/src/main/res/values/strings.xml | 3 + .../demo/src/main/res/xml/backup_rules.xml | 13 ++ .../main/res/xml/data_extraction_rules.xml | 19 ++ .../main/res/xml/network_security_config.xml | 13 ++ .../auth/implementation/build.gradle.kts | 4 + .../auth/implementation/AuthDataApiImpl.kt | 35 ++++ .../skills/auth/implementation/AuthModule.kt | 8 + .../auth/implementation/AuthUiApiImpl.kt | 26 +++ .../state/AuthStateFactoryImpl.kt | 9 +- .../state/AuthenticatingState.kt | 4 +- .../implementation/state/PreloadingState.kt | 4 +- .../implementation/ui/AuthUiRendererImpl.kt | 2 + gradle/libs.versions.toml | 14 ++ settings.gradle.kts | 3 +- build_skills.sh => skills/build_skills.sh | 7 +- .../.shared/ARCHITECTURE.md | 32 +++ .../.shared/architecture.puml | 51 +++++ .../.shared/COMMON-CHILD-FLOW.md | 39 ++++ .../assets => .shared}/common-child-flow.puml | 4 +- .../common-child-flow/boilerplate/SKILL.md | 21 +- .../boilerplate/references/ARCHITECTURE.md | 1 + .../references/COMMON-CHILD-FLOW.md | 1 + .../boilerplate/references/architecture.puml | 1 + .../references/base-state-implementation.md | 2 +- .../references/common-child-flow.puml | 1 + .../boilerplate/references/data-and-ui-api.md | 4 +- .../references/data-and-ui-implementation.md | 2 +- .../references/di-implementation.md | 44 +++++ .../references/gesture-and-ui-state-api.md | 4 +- .../references/input-and-result-api.md | 4 +- ...actory-context-interface-implementation.md | 2 +- .../references/state-implementation.md | 2 +- 55 files changed, 948 insertions(+), 28 deletions(-) create mode 100644 examples/skills/auth/demo/.gitignore create mode 100644 examples/skills/auth/demo/build.gradle.kts create mode 100644 examples/skills/auth/demo/proguard-rules.pro create mode 100644 examples/skills/auth/demo/src/main/AndroidManifest.xml create mode 100644 examples/skills/auth/demo/src/main/kotlin/com/motorro/statemachine/skills/auth/demo/App.kt create mode 100644 examples/skills/auth/demo/src/main/kotlin/com/motorro/statemachine/skills/auth/demo/MainActivity.kt create mode 100644 examples/skills/auth/demo/src/main/kotlin/com/motorro/statemachine/skills/auth/demo/MainViewModel.kt create mode 100644 examples/skills/auth/demo/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 examples/skills/auth/demo/src/main/res/drawable/ic_launcher_background.xml create mode 100644 examples/skills/auth/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 examples/skills/auth/demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 examples/skills/auth/demo/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 examples/skills/auth/demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 examples/skills/auth/demo/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 examples/skills/auth/demo/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 examples/skills/auth/demo/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 examples/skills/auth/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 examples/skills/auth/demo/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 examples/skills/auth/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 examples/skills/auth/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 examples/skills/auth/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 examples/skills/auth/demo/src/main/res/values/strings.xml create mode 100644 examples/skills/auth/demo/src/main/res/xml/backup_rules.xml create mode 100644 examples/skills/auth/demo/src/main/res/xml/data_extraction_rules.xml create mode 100644 examples/skills/auth/demo/src/main/res/xml/network_security_config.xml create mode 100644 examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/AuthDataApiImpl.kt create mode 100644 examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/AuthModule.kt create mode 100644 examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/AuthUiApiImpl.kt rename build_skills.sh => skills/build_skills.sh (79%) create mode 100644 skills/commonstatemachine/.shared/ARCHITECTURE.md create mode 100644 skills/commonstatemachine/.shared/architecture.puml create mode 100644 skills/commonstatemachine/common-child-flow/.shared/COMMON-CHILD-FLOW.md rename skills/commonstatemachine/common-child-flow/{boilerplate/assets => .shared}/common-child-flow.puml (98%) create mode 120000 skills/commonstatemachine/common-child-flow/boilerplate/references/ARCHITECTURE.md create mode 120000 skills/commonstatemachine/common-child-flow/boilerplate/references/COMMON-CHILD-FLOW.md create mode 120000 skills/commonstatemachine/common-child-flow/boilerplate/references/architecture.puml create mode 120000 skills/commonstatemachine/common-child-flow/boilerplate/references/common-child-flow.puml create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/references/di-implementation.md diff --git a/.github/workflows/skills.yaml b/.github/workflows/skills.yaml index f962665..af85fda 100644 --- a/.github/workflows/skills.yaml +++ b/.github/workflows/skills.yaml @@ -17,7 +17,7 @@ jobs: fetch-depth: 0 ref: refs/remotes/origin/master - name: Build skills - run: ./build_skills.sh artifacts + run: ./skills/build_skills.sh artifacts - name: Upload skills to release uses: softprops/action-gh-release@v2 with: diff --git a/build.gradle.kts b/build.gradle.kts index 44ef1f3..3625efd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -28,6 +28,7 @@ plugins { alias(libs.plugins.compose) apply false alias(libs.plugins.kotlin.dokka) apply false alias(libs.plugins.hilt) apply false + alias(libs.plugins.koin) apply false alias(libs.plugins.nexus.publish) alias(libs.plugins.git) } diff --git a/examples/skills/auth/demo/.gitignore b/examples/skills/auth/demo/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/examples/skills/auth/demo/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/examples/skills/auth/demo/build.gradle.kts b/examples/skills/auth/demo/build.gradle.kts new file mode 100644 index 0000000..9be272f --- /dev/null +++ b/examples/skills/auth/demo/build.gradle.kts @@ -0,0 +1,127 @@ +/* + * 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 { + alias(libs.plugins.android.app) + alias(libs.plugins.compose) + alias(libs.plugins.koin) +} + +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.skills.auth.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_21 + targetCompatibility = JavaVersion.VERSION_21 + } + kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_21) + } + } + buildFeatures { + compose = true + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } + namespace = "com.motorro.statemachine.skills.auth.demo" +} + +dependencies { + implementation(project(":examples:skills:appcore")) + implementation(project(":examples:skills:domain")) + implementation(project(":examples:skills:auth:api")) + implementation(project(":examples:skills:auth:implementation")) + implementation(project(":commonstatemachine")) + implementation(project(":coroutines")) + implementation(project(":commonflow:commonflow-data")) + implementation(project(":commonflow:commonflow-compose")) + implementation(project(":commonflow:commonflow-viewmodel")) + + coreLibraryDesugaring(libs.desugaring) + + implementation(libs.napier) + implementation(libs.androidx.core) + implementation(libs.androidx.activity) + 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) + + debugImplementation(libs.compose.tooling) + + implementation(platform(libs.koin.bom)) + implementation(libs.koin.core) + implementation(libs.koin.annotations) + implementation(libs.koin.core.viewmodel) + implementation(libs.koin.android) + implementation(libs.koin.compose) + implementation(libs.koin.compose.viewmodel) +} diff --git a/examples/skills/auth/demo/proguard-rules.pro b/examples/skills/auth/demo/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/examples/skills/auth/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/skills/auth/demo/src/main/AndroidManifest.xml b/examples/skills/auth/demo/src/main/AndroidManifest.xml new file mode 100644 index 0000000..e347350 --- /dev/null +++ b/examples/skills/auth/demo/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/skills/auth/demo/src/main/kotlin/com/motorro/statemachine/skills/auth/demo/App.kt b/examples/skills/auth/demo/src/main/kotlin/com/motorro/statemachine/skills/auth/demo/App.kt new file mode 100644 index 0000000..d986fbe --- /dev/null +++ b/examples/skills/auth/demo/src/main/kotlin/com/motorro/statemachine/skills/auth/demo/App.kt @@ -0,0 +1,93 @@ +package com.motorro.statemachine.skills.auth.demo + +import android.app.Application +import com.motorro.commonstatemachine.skills.domain.entity.PasswordRequirements +import com.motorro.commonstatemachine.skills.domain.exception.AuthenticationException +import com.motorro.commonstatemachine.skills.domain.exception.IOException +import com.motorro.commonstatemachine.skills.domain.usecase.AuthenticateWithPassword +import com.motorro.commonstatemachine.skills.domain.usecase.GetPasswordRequirements +import com.motorro.statemachine.skills.auth.implementation.AuthModule +import io.github.aakira.napier.DebugAntilog +import io.github.aakira.napier.Napier +import kotlinx.coroutines.delay +import org.koin.android.ext.koin.androidContext +import org.koin.android.ext.koin.androidLogger +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.KoinApplication +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single +import org.koin.plugin.module.dsl.startKoin +import kotlin.time.Duration.Companion.seconds + +@KoinApplication(modules = [AppModule::class]) +class App : Application() { + override fun onCreate() { + super.onCreate() + + Napier.base(DebugAntilog()) + + startKoin { + allowOverride(false) + androidLogger() + androidContext(this@App) + } + } +} + +@Module(includes = [AuthModule::class]) +@ComponentScan("com.motorro.statemachine.skills.auth.demo") +class AppModule { + + @Single + fun getPasswordRequirements(): GetPasswordRequirements { + var attempt = 1 + val tag = GetPasswordRequirements::class.simpleName!! + return object : GetPasswordRequirements { + override suspend fun invoke(): PasswordRequirements { + delay(2.seconds) + when (attempt) { + 1 -> { + Napier.d(tag = tag) { "Emulating non-fatal error" } + attempt = 2 + throw IOException("Network error") + } + else -> { + Napier.d(tag = tag) { "Emulating success" } + attempt = 1 + return PasswordRequirements( + regex = "^.{8,}$".toRegex(), + description = "Minimum eight characters" + ) + } + } + } + } + } + + @Single + fun authenticateWithPassword(): AuthenticateWithPassword { + var attempt = 1 + val tag = AuthenticateWithPassword::class.simpleName!! + return object : AuthenticateWithPassword { + override suspend fun invoke(username: String, password: String) { + delay(2.seconds) + when (attempt) { + 1 -> { + Napier.d(tag = tag) { "Emulating credentials error" } + attempt = 2 + throw AuthenticationException("Invalid credentials") + } + 2 -> { + Napier.d(tag = tag) { "Emulating non-fatal error" } + attempt = 3 + throw IOException("Network error") + } + else -> { + Napier.d(tag = tag) { "Emulating success" } + attempt = 1 + } + } + } + } + } +} diff --git a/examples/skills/auth/demo/src/main/kotlin/com/motorro/statemachine/skills/auth/demo/MainActivity.kt b/examples/skills/auth/demo/src/main/kotlin/com/motorro/statemachine/skills/auth/demo/MainActivity.kt new file mode 100644 index 0000000..84efcd9 --- /dev/null +++ b/examples/skills/auth/demo/src/main/kotlin/com/motorro/statemachine/skills/auth/demo/MainActivity.kt @@ -0,0 +1,49 @@ +package com.motorro.statemachine.skills.auth.demo + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.BackHandler +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.runtime.Composable +import com.motorro.commonstatemachine.flow.viewmodel.CommonFlowComposition +import com.motorro.statemachine.auth.appcore.ui.theme.SkillsTheme +import com.motorro.statemachine.skills.auth.api.AuthUiApi +import io.github.aakira.napier.Napier +import org.koin.compose.koinInject +import org.koin.compose.viewmodel.koinViewModel + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + SkillsTheme { + MainScreen { + finish() + } + } + } + } +} + +@Composable +fun MainScreen(onComplete: () -> Unit) { + val viewModel: MainViewModel = koinViewModel() + val uiApi: AuthUiApi = koinInject() + + CommonFlowComposition( + viewModel = viewModel, + navigationBackHandler = { enabled, onBack -> + BackHandler(enabled, onBack) + }, + content = { state, onGesture -> + uiApi.Screen(state, onGesture) + }, + finish = { + Napier.i { "Finished with: $it" } + onComplete() + } + ) +} + diff --git a/examples/skills/auth/demo/src/main/kotlin/com/motorro/statemachine/skills/auth/demo/MainViewModel.kt b/examples/skills/auth/demo/src/main/kotlin/com/motorro/statemachine/skills/auth/demo/MainViewModel.kt new file mode 100644 index 0000000..ad16260 --- /dev/null +++ b/examples/skills/auth/demo/src/main/kotlin/com/motorro/statemachine/skills/auth/demo/MainViewModel.kt @@ -0,0 +1,15 @@ +package com.motorro.statemachine.skills.auth.demo + +import com.motorro.commonstatemachine.flow.viewmodel.CommonFlowViewModel +import com.motorro.statemachine.skills.auth.api.AuthDataApi +import com.motorro.statemachine.skills.auth.api.AuthGesture +import com.motorro.statemachine.skills.auth.api.AuthInput +import com.motorro.statemachine.skills.auth.api.AuthResult +import com.motorro.statemachine.skills.auth.api.AuthUiState +import org.koin.core.annotation.KoinViewModel + +@KoinViewModel +class MainViewModel (api: AuthDataApi) : CommonFlowViewModel( + api = api, + init = AuthInput(skippable = true) +) \ No newline at end of file diff --git a/examples/skills/auth/demo/src/main/res/drawable-v24/ic_launcher_foreground.xml b/examples/skills/auth/demo/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..40ce517 --- /dev/null +++ b/examples/skills/auth/demo/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/skills/auth/demo/src/main/res/drawable/ic_launcher_background.xml b/examples/skills/auth/demo/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..997ef0b --- /dev/null +++ b/examples/skills/auth/demo/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/skills/auth/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/examples/skills/auth/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..0147ca8 --- /dev/null +++ b/examples/skills/auth/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/examples/skills/auth/demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/examples/skills/auth/demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..0147ca8 --- /dev/null +++ b/examples/skills/auth/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/skills/auth/demo/src/main/res/mipmap-hdpi/ic_launcher.webp b/examples/skills/auth/demo/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..c209e78ecd372343283f4157dcfd918ec5165bb3 GIT binary patch literal 1404 zcmV-?1%vuhNk&F=1pok7MM6+kP&il$0000G0000-002h-06|PpNX!5L00Dqw+t%{r zzW2vH!KF=w&cMnnN@{whkTw+#mAh0SV?YL=)3MimFYCWp#fpdtz~8$hD5VPuQgtcN zXl<@<#Cme5f5yr2h%@8TWh?)bSK`O z^Z@d={gn7J{iyxL_y_%J|L>ep{dUxUP8a{byupH&!UNR*OutO~0{*T4q5R6@ApLF! z5{w?Z150gC7#>(VHFJZ-^6O@PYp{t!jH(_Z*nzTK4 zkc{fLE4Q3|mA2`CWQ3{8;gxGizgM!zccbdQoOLZc8hThi-IhN90RFT|zlxh3Ty&VG z?Fe{#9RrRnxzsu|Lg2ddugg7k%>0JeD+{XZ7>Z~{=|M+sh1MF7~ zz>To~`~LVQe1nNoR-gEzkpe{Ak^7{{ZBk2i_<+`Bq<^GB!RYG+z)h;Y3+<{zlMUYd zrd*W4w&jZ0%kBuDZ1EW&KLpyR7r2=}fF2%0VwHM4pUs}ZI2egi#DRMYZPek*^H9YK zay4Iy3WXFG(F14xYsoDA|KXgGc5%2DhmQ1gFCkrgHBm!lXG8I5h*uf{rn48Z!_@ z4Bk6TJAB2CKYqPjiX&mWoW>OPFGd$wqroa($ne7EUK;#3VYkXaew%Kh^3OrMhtjYN?XEoY`tRPQsAkH-DSL^QqyN0>^ zmC>{#F14jz4GeW{pJoRpLFa_*GI{?T93^rX7SPQgT@LbLqpNA}<@2wH;q493)G=1Y z#-sCiRNX~qf3KgiFzB3I>4Z%AfS(3$`-aMIBU+6?gbgDb!)L~A)je+;fR0jWLL-Fu z4)P{c7{B4Hp91&%??2$v9iRSFnuckHUm}or9seH6 z>%NbT+5*@L5(I9j@06@(!{ZI?U0=pKn8uwIg&L{JV14+8s2hnvbRrU|hZCd}IJu7*;;ECgO%8_*W Kmw_-CKmY()leWbG literal 0 HcmV?d00001 diff --git a/examples/skills/auth/demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/examples/skills/auth/demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..b2dfe3d1ba5cf3ee31b3ecc1ced89044a1f3b7a9 GIT binary patch literal 2898 zcmV-Y3$650Nk&FW3jhFDMM6+kP&il$0000G0000-002h-06|PpNWB9900E$G+qN-D z+81ABX7q?;bwx%xBg?kcwr$(C-Tex-ZCkHUw(Y9#+`E5-zuONG5fgw~E2WDng@Bc@ z24xy+R1n%~6xI#u9vJ8zREI)sb<&Il(016}Z~V1n^PU3-_H17A*Bf^o)&{_uBv}Py zulRfeE8g(g6HFhk_?o_;0@tz?1I+l+Y#Q*;RVC?(ud`_cU-~n|AX-b`JHrOIqn(-t&rOg-o`#C zh0LPxmbOAEb;zHTu!R3LDh1QO zZTf-|lJNUxi-PpcbRjw3n~n-pG;$+dIF6eqM5+L();B2O2tQ~|p{PlpNcvDbd1l%c zLtXn%lu(3!aNK!V#+HNn_D3lp z2%l+hK-nsj|Bi9;V*WIcQRTt5j90A<=am+cc`J zTYIN|PsYAhJ|=&h*4wI4ebv-C=Be#u>}%m;a{IGmJDU`0snWS&$9zdrT(z8#{OZ_Y zxwJx!ZClUi%YJjD6Xz@OP8{ieyJB=tn?>zaI-4JN;rr`JQbb%y5h2O-?_V@7pG_+y z(lqAsqYr!NyVb0C^|uclHaeecG)Sz;WV?rtoqOdAAN{j%?Uo%owya(F&qps@Id|Of zo@~Y-(YmfB+chv^%*3g4k3R0WqvuYUIA+8^SGJ{2Bl$X&X&v02>+0$4?di(34{pt* zG=f#yMs@Y|b&=HyH3k4yP&goF2LJ#tBLJNNDo6lG06r}ghC-pC4Q*=x3;|+W04zte zAl>l4kzUBQFYF(E`KJy?ZXd1tnfbH+Z~SMmA21KokJNs#eqcXWKUIC>{TuoKe^vhF z);H)o`t9j~`$h1D`#bxe@E`oE`cM9w(@)5Bp8BNukIwM>wZHfd0S;5bcXA*5KT3bj zc&_~`&{z7u{Et!Z_k78H75gXf4g8<_ul!H$eVspPeU3j&&Au=2R*Zp#M9$9s;fqwgzfiX=E_?BwVcfx3tG9Q-+<5fw z%Hs64z)@Q*%s3_Xd5>S4dg$s>@rN^ixeVj*tqu3ZV)biDcFf&l?lGwsa zWj3rvK}?43c{IruV2L`hUU0t^MemAn3U~x3$4mFDxj=Byowu^Q+#wKRPrWywLjIAp z9*n}eQ9-gZmnd9Y0WHtwi2sn6n~?i#n9VN1B*074_VbZZ=WrpkMYr{RsI ztM_8X1)J*DZejxkjOTRJ&a*lrvMKBQURNP#K)a5wIitfu(CFYV4FT?LUB$jVwJSZz zNBFTWg->Yk0j&h3e*a5>B=-xM7dE`IuOQna!u$OoxLlE;WdrNlN)1 z7**de7-hZ!(%_ZllHBLg`Ir#|t>2$*xVOZ-ADZKTN?{(NUeLU9GbuG-+Axf*AZ-P1 z0ZZ*fx+ck4{XtFsbcc%GRStht@q!m*ImssGwuK+P@%gEK!f5dHymg<9nSCXsB6 zQ*{<`%^bxB($Z@5286^-A(tR;r+p7B%^%$N5h%lb*Vlz-?DL9x;!j<5>~kmXP$E}m zQV|7uv4SwFs0jUervsxVUm>&9Y3DBIzc1XW|CUZrUdb<&{@D5yuLe%Xniw^x&{A2s z0q1+owDSfc3Gs?ht;3jw49c#mmrViUfX-yvc_B*wY|Lo7; zGh!t2R#BHx{1wFXReX*~`NS-LpSX z#TV*miO^~B9PF%O0huw!1Zv>^d0G3$^8dsC6VI!$oKDKiXdJt{mGkyA`+Gwd4D-^1qtNTUK)`N*=NTG-6}=5k6suNfdLt*dt8D| z%H#$k)z#ZRcf|zDWB|pn<3+7Nz>?WW9WdkO5(a^m+D4WRJ9{wc>Y}IN)2Kbgn;_O? zGqdr&9~|$Y0tP=N(k7^Eu;iO*w+f%W`20BNo)=Xa@M_)+o$4LXJyiw{F?a633SC{B zl~9FH%?^Rm*LVz`lkULs)%idDX^O)SxQol(3jDRyBVR!7d`;ar+D7do)jQ}m`g$TevUD5@?*P8)voa?kEe@_hl{_h8j&5eB-5FrYW&*FHVt$ z$kRF9Nstj%KRzpjdd_9wO=4zO8ritN*NPk_9avYrsF(!4))tm{Ga#OY z(r{0buexOzu7+rw8E08Gxd`LTOID{*AC1m*6Nw@osfB%0oBF5sf<~wH1kL;sd zo)k6^VyRFU`)dt*iX^9&QtWbo6yE8XXH?`ztvpiOLgI3R+=MOBQ9=rMVgi<*CU%+d1PQQ0a1U=&b0vkF207%xU0ssI2 literal 0 HcmV?d00001 diff --git a/examples/skills/auth/demo/src/main/res/mipmap-mdpi/ic_launcher.webp b/examples/skills/auth/demo/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..4f0f1d64e58ba64d180ce43ee13bf9a17835fbca GIT binary patch literal 982 zcmV;{11bDcNk&G_0{{S5MM6+kP&il$0000G0000l001ul06|PpNU8t;00Dqo+t#w^ z^1csucXz7-Qrhzl9HuHB%l>&>1tG2^vb*E&k^T3$FG1eQZ51g$uv4V+kI`0<^1Z@N zk?Jjh$olyC%l>)Xq;7!>{iBj&BjJ`P&$fsCfpve_epJOBkTF?nu-B7D!hO=2ZR}

C%4 zc_9eOXvPbC4kzU8YowIA8cW~Uv|eB&yYwAObSwL2vY~UYI7NXPvf3b+c^?wcs~_t{ ze_m66-0)^{JdOMKPwjpQ@Sna!*?$wTZ~su*tNv7o!gXT!GRgivP}ec?5>l1!7<(rT zds|8x(qGc673zrvYIz;J23FG{9nHMnAuP}NpAED^laz3mAN1sy+NXK)!6v1FxQ;lh zOBLA>$~P3r4b*NcqR;y6pwyhZ3_PiDb|%n1gGjl3ZU}ujInlP{eks-#oA6>rh&g+!f`hv#_%JrgYPu z(U^&XLW^QX7F9Z*SRPpQl{B%x)_AMp^}_v~?j7 zapvHMKxSf*Mtyx8I}-<*UGn3)oHd(nn=)BZ`d$lDBwq_GL($_TPaS{UeevT(AJ`p0 z9%+hQb6z)U9qjbuXjg|dExCLjpS8$VKQ55VsIC%@{N5t{NsW)=hNGI`J=x97_kbz@ E0Of=7!TQj4N+cqN`nQhxvX7dAV-`K|Ub$-q+H-5I?Tx0g9jWxd@A|?POE8`3b8fO$T))xP* z(X?&brZw({`)WU&rdAs1iTa0x6F@PIxJ&&L|dpySV!ID|iUhjCcKz(@mE z!x@~W#3H<)4Ae(4eQJRk`Iz3<1)6^m)0b_4_TRZ+cz#eD3f8V;2r-1fE!F}W zEi0MEkTTx}8i1{`l_6vo0(Vuh0HD$I4SjZ=?^?k82R51bC)2D_{y8mi_?X^=U?2|F{Vr7s!k(AZC$O#ZMyavHhlQ7 zUR~QXuH~#o#>(b$u4?s~HLF*3IcF7023AlwAYudn0FV~|odGH^05AYPEfR)8p`i{n zwg3zPVp{+wOsxKc>)(pMupKF!Y2HoUqQ3|Yu|8lwR=?5zZuhG6J?H`bSNk_wPoM{u zSL{c@pY7+c2kck>`^q1^^gR0QB7Y?KUD{vz-uVX~;V-rW)PDcI)$_UjgVV?S?=oLR zf4}zz{#*R_{LkiJ#0RdQLNC^2Vp%JPEUvG9ra2BVZ92(p9h7Ka@!yf9(lj#}>+|u* z;^_?KWdzkM`6gqPo9;;r6&JEa)}R3X{(CWv?NvgLeOTq$cZXqf7|sPImi-7cS8DCN zGf;DVt3Am`>hH3{4-WzH43Ftx)SofNe^-#|0HdCo<+8Qs!}TZP{HH8~z5n`ExcHuT zDL1m&|DVpIy=xsLO>8k92HcmfSKhflQ0H~9=^-{#!I1g(;+44xw~=* zxvNz35vfsQE)@)Zsp*6_GjYD};Squ83<_?^SbALb{a`j<0Gn%6JY!zhp=Fg}Ga2|8 z52e1WU%^L1}15Ex0fF$e@eCT(()_P zvV?CA%#Sy08_U6VPt4EtmVQraWJX` zh=N|WQ>LgrvF~R&qOfB$!%D3cGv?;Xh_z$z7k&s4N)$WYf*k=|*jCEkO19{h_(%W4 zPuOqbCw`SeAX*R}UUsbVsgtuG?xs(#Ikx9`JZoQFz0n*7ZG@Fv@kZk`gzO$HoA9kN z8U5{-yY zvV{`&WKU2$mZeoBmiJrEdzUZAv1sRxpePdg1)F*X^Y)zp^Y*R;;z~vOv-z&)&G)JQ{m!C9cmziu1^nHA z`#`0c>@PnQ9CJKgC5NjJD8HM3|KC(g5nnCq$n0Gsu_DXk36@ql%npEye|?%RmG)

FJ$wK}0tWNB{uH;AM~i literal 0 HcmV?d00001 diff --git a/examples/skills/auth/demo/src/main/res/mipmap-xhdpi/ic_launcher.webp b/examples/skills/auth/demo/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..948a3070fe34c611c42c0d3ad3013a0dce358be0 GIT binary patch literal 1900 zcmV-y2b1_xNk&Fw2LJ$9MM6+kP&il$0000G0001A003VA06|PpNH75a00DqwTbm-~ zullQTcXxO9ki!OCRx^i?oR|n!<8G0=kI^!JSjFi-LL*`V;ET0H2IXfU0*i>o6o6Gy zRq6Ap5(_{XLdXcL-MzlN`ugSdZY_`jXhcENAu)N_0?GhF))9R;E`!bo9p?g?SRgw_ zEXHhFG$0{qYOqhdX<(wE4N@es3VIo$%il%6xP9gjiBri+2pI6aY4 zJbgh-Ud|V%3O!IcHKQx1FQH(_*TK;1>FQWbt^$K1zNn^cczkBs=QHCYZ8b&l!UV{K z{L0$KCf_&KR^}&2Fe|L&?1I7~pBENnCtCuH3sjcx6$c zwqkNkru);ie``q+_QI;IYLD9OV0ZxkuyBz|5<$1BH|vtey$> z5oto4=l-R-Aaq`Dk0}o9N0VrkqW_#;!u{!bJLDq%0092{Ghe=F;(kn} z+sQ@1=UlX30+2nWjkL$B^b!H2^QYO@iFc0{(-~yXj2TWz?VG{v`Jg zg}WyYnwGgn>{HFaG7E~pt=)sOO}*yd(UU-D(E&x{xKEl6OcU?pl)K%#U$dn1mDF19 zSw@l8G!GNFB3c3VVK0?uyqN&utT-D5%NM4g-3@Sii9tSXKtwce~uF zS&Jn746EW^wV~8zdQ1XC28~kXu8+Yo9p!<8h&(Q({J*4DBglPdpe4M_mD8AguZFn~ ztiuO~{6Bx?SfO~_ZV(GIboeR9~hAym{{fV|VM=77MxDrbW6`ujX z<3HF(>Zr;#*uCvC*bpoSr~C$h?_%nXps@A)=l_;({Fo#6Y1+Zv`!T5HB+)#^-Ud_; zBwftPN=d8Vx)*O1Mj+0oO=mZ+NVH*ptNDC-&zZ7Hwho6UQ#l-yNvc0Cm+2$$6YUk2D2t#vdZX-u3>-Be1u9gtTBiMB^xwWQ_rgvGpZ6(C@e23c!^K=>ai-Rqu zhqT`ZQof;9Bu!AD(i^PCbYV%yha9zuoKMp`U^z;3!+&d@Hud&_iy!O-$b9ZLcSRh? z)R|826w}TU!J#X6P%@Zh=La$I6zXa#h!B;{qfug}O%z@K{EZECu6zl)7CiNi%xti0 zB{OKfAj83~iJvmpTU|&q1^?^cIMn2RQ?jeSB95l}{DrEPTW{_gmU_pqTc)h@4T>~& zluq3)GM=xa(#^VU5}@FNqpc$?#SbVsX!~RH*5p0p@w z;~v{QMX0^bFT1!cXGM8K9FP+=9~-d~#TK#ZE{4umGT=;dfvWi?rYj;^l_Zxywze`W z^Cr{55U@*BalS}K%Czii_80e0#0#Zkhlij4-~I@}`-JFJ7$5{>LnoJSs??J8kWVl6|8A}RCGAu9^rAsfCE=2}tHwl93t0C?#+jMpvr7O3`2=tr{Hg$=HlnjVG^ewm|Js0J*kfPa6*GhtB>`fN!m#9J(sU!?(OSfzY*zS(FJ<-Vb zfAIg+`U)YaXv#sY(c--|X zEB+TVyZ%Ie4L$gi#Fc++`h6%vzsS$pjz9aLt+ZL(g;n$Dzy5=m=_TV(3H8^C{r0xd zp#a%}ht55dOq?yhwYPrtp-m1xXp;4X;)NhxxUpgP%XTLmO zcjaFva^}dP3$&sfFTIR_jC=2pHh9kpI@2(6V*GQo7Ws)`j)hd+tr@P~gR*2gO@+1? zG<`_tB+LJuF|SZ9tIec;h%}}6WClT`L>HSW?E{Hp1h^+mlbf_$9zA>!ug>NALJsO{ mU%z=YwVD?}XMya)Bp;vlyE5&E_6!fzx9pwrdz474!~g(M6R?N? literal 0 HcmV?d00001 diff --git a/examples/skills/auth/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/examples/skills/auth/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..1b9a6956b3acdc11f40ce2bb3f6efbd845cc243f GIT binary patch literal 3918 zcmV-U53%r4Nk&FS4*&pHMM6+kP&il$0000G0001A003VA06|PpNSy@$00HoY|G(*G z+qV7x14$dSO^Re!iqt-AAIE9iwr$(CZQJL$blA4B`>;C3fBY6Q8_YSjb2%a=fc}4E zrSzssacq<^nmW|Rs93PJni30R<8w<(bK_$LO4L?!_OxLl$}K$MUEllnMK|rg=f3;y z*?;3j|Nh>)p0JQ3A~rf(MibH2r+)3cyV1qF&;8m{w-S*y+0mM){KTK^M5}ksc`qX3 zy>rf^b>~l>SSHds8(I@hz3&PD@LmEs4&prkT=BjsBCXTMhN$_)+kvnl0bLKW5rEsj z*d#KXGDB4P&>etx0X+`R19yC=LS)j!mgs5M0L~+o-T~Jl!p!AJxnGAhV%~rhYUL4hlWhgES3Kb5oA&X z{}?3OBSS-{!v$nCIGj->(-TAG)8LR{htr41^gxsT8yqt2@DEG6Yl`Uma3Nd4;YUoW zTbkYl3CMU5ypMF3EIkYmWL|*BknM`0+Kq6CpvO(y$#j94e+q{vI{Zp8cV_6RK!`&C zob$*5Q|$IZ09dW=L!V zw@#2wviu|<#3lgGE8GEhcx+zBt`} zOwP8j9X%^f7i_bth4PiJ$LYtFJSCN$3xwDN;8mr*B;CJwBP2G0TMq0uNt7S^DO_wE zepk!Wrn#Z#03j{`c*Rf~y3o7?J}w?tEELRUR2cgxB*Y{LzA#pxHgf}q?u5idu>077 zd^=p)`nA}6e`|@`p?u}YU66PP_MA}Zqqe!c{nK&z%Jwq1N4e_q<#4g^xaz=ao;u|6 zwpRcW2Lax=ZGbx=Q*HhlJ`Ns#Y*r0*%!T?P*TTiX;rb)$CGLz=rSUum$)3Qyv{BL2 zO*=OI2|%(Yz~`pNEOnLp>+?T@glq-DujlIp?hdJeZ7ctP4_OKx|5@EOps3rr(pWzg zK4d3&oN-X2qN(d_MkfwB4I)_)!I_6nj2iA9u^pQ{;GckGLxBGrJUM2Wdda!k)Y>lq zmjws>dVQ*vW9lvEMkiN3wE-__6OWD0txS&Qn0n22cyj4Q*8(nG4!G{6OOwNvsrPIL zCl-$W9UwkEUVuLwyD%|inbOF*xMODZ4VMEVAq_zUxZ+K#Gdqf!DW$5f)?7UNOFMz! zrB~tuu=6X2FE(p^iqgxr+?ZK;=yz`e;C$#_@D9Lj-+TDVOrva>(#*PVbaHO>A)mhl z07OJWCqYC60518$!&c`eNBcBW%GnfaQ*$eazV^2_AW?j)h;J1nUjN(I9=0+!RVx~% z3@Tf!P0TE+98jA?WceK-}A1% zW!K)lyKcGqy#M~})315-A#2NXQ`?6NR#Apo=S!oF=JfpX>iR*49ec{7AN$xxpK{D$ z2d%Fz&rdfSqourN$~Y^NFIMV1CZ?J*bMx~H3k&meGtH@q9ra2vZxmA$S(#jaaj-g4 ztJmxG+DLV<*q<|sDXPp$X>E)#S}Vm&sRaO5P&goh2><}FEdZSXDqsL$06sAkh(e+v zAsBhKSRexgwg6tIy~GFJzaTxXD(}|+0eOwFDA%rn`X;MVwDHT9=4=g%OaJ9s%3b9>9EUTnnp0t;2Zpa{*>mk~hZqItE_!dQ zOtC>8`$l|mV43Jbudf0N6&&X;{=z}Zi}d1`2qmJ}i|0*GsulD3>GgQXHN)pkR6sf1 z?5ZU%&xtL}oH;YiAA)d*^Ndw2T$+Mjuzyzz@-SM`9df7LqTxLuIwC~S0092~+=qYv z@*ja;?Wt!T!{U?c*Z0YtGe)XbI&y-?B&G2$`JDM)(dIV9G`Sc#6?sI60de6kv+)Qb zUW~2|WjvJq3TA8`0+sWA3zRhY9a~ow)O~&StBkG2{*{TGiY~S8ep{V&Vo2l<6LWsu z^#p0-v*t2?3&aA1)ozu|%efSR=XnpX$lvTeRdKlvM!@|pM5p2w3u-6 zU>}t2xiYLS+{|%C65AzX+23Mtlq?BS&YdYcYsVjoiE&rT>;Necn6l^K)T^lmE`5u{ zm1i+-a-gc;Z&v-{;8r)z6NYfBUv+=_L}ef}qa9FX01)+Aaf+;xj(mL6|JUzGJR1|fnanb%?BPPIp>SCjP|8qE5qJ{=n5ZGw?81z3(k;pzH%1CtlX50{E7h)$h{qGKfzC`e2o`*IqA#tjA z`Fz&^%$b9F*N`)U-#6>a)Z`55`$Dd0cfcs0$d13^ONrdCu9xcv_=n#WQo8stcz3jP9|2EvdI-RhJM3%Q%oM&!OlShM|0 z?gz?wHZSnm45njLtsz8PVT1S&jAlbKg5kVam$p16=EK@Sj4EP0OtH zmJDmdc^v)x>56Qg_wmYHz6h)>kl_h$>0@J!ypv%APmjZTAQVLy6Fu50RGY&JAVNhx zrF_qG6`x9MkT;1SFWo$)l{M$;3qUDn9JwE}z zRl#E_bDRJFii61kPgBybIgp8dNW!Cc1b*^YYk-#oWLJvtM_v^hQx~9?8LD4VFFxBF z3MlrsSC%f9Oupn*ctPL0U1fwfX?`tRhPD{PSLFPQOmIt$mDy0SgpNVvHS+f#Do>h1Gn?LZU9(KaN>Q_=Y*_T zvtD7%_u^^+{g`0VGzg(VZrpVQ6Ub5M=tI_p7T93R8@3Zulu3|#{iNcu!oiHxZ4Rf*( zfmiN$$ru(*_Zqn=`Gq#OuHRTSwp7uH_SokR&|)RuW5yo=Z|_4?qU-JU+tpt>!B&Is z@N(=SG;bpVc;AO@zbmMM zScqq1)b-ZQIrs={oD}|?6y{$HNB1U0^LsBh8JI&3!GBZxOXI<}&5-$lgkAaYqhOTb z?2vEnZ$-kk;*M_17(upJF3%+iH*s0-r{vttXVB2OUwI1s^+G(Ft(U8gYFXC}#P&E^ z>T@C^tS`Z7{6HT4_nF~n>JlZtk5&qDBl6r|^kzQYe`wq!C)n@$c>WOPA61NDFj<<6 zGW71NMMhwAl!U-yqrq2xrSFqRCI8acw7?}3j;ynxo*-b7Co;g5r%^j=H@9({PXXBf z@r>U>>N;E)81wx`B4f%{PB~MHka_);%kBCb(d|Jy5!MqJ%2p`t&@L)4$T2j&-WHvG zv3(uyA_gwqNu(k?jQTtv3dgPKRZoH8prxe7>pQBW5L&dpumS&5Ld2?(sCpJjvc4L5 zEnh&?91WVm)ZdTj=fjJ$pPDdgAttLXuke+?KdKxu*;kTC(r!tQk6;gxj4h%FdHAt(^M3YvYj(!tOeN)+Hvj6+< zzyJRG?^lZfWuR#t!tUKP&(?%3v&Zd$R2YN>lB(Lq`OInY48%4%yTv2 zYe1{G`3)(PDEio5Y@-I5tUf`c%%OCJMtSW56g3iEg%3`$7XSJJHyA z<|7&N)5Xrlgv~%BO24eFd;Hd;uiK%D`EdK|quUeRZDqbh9l)%j%J#0lfrZumvA<_w zu&=AVvdChf6}eqh(bUz`(`Ue*p01{fBAcTgKyDYLs_I+YyJEk+rM@avU~>fB$n)HS zM7pfJydu`i%gfS<{PF94kZDv$t>06sAkheDzu40NJ$5CMW%n^Lls?8^p^QGWURbKu3ZduZQZ((s2? zzE`}<{;Zt7<$C|9R8A~DJ~@%x>TfP zF>TX8)@v|t)q4GjRt<}5s6hLHwRel7>V@&r-O|Av(yh;Q1A{E>Ir>p+%dHD|=l+lT zpr(Dg&>#Nu=!)6bCLr-ZS%|;h)Ij$+e@r8_{qO19QvDe=&1tmpY*0lcA^Cc-#{9fQ z<~$*<&P$Q<_jy#<$40PMofM7aQ}C=jphI`4kLg}Z7CIN#26D{-4v-_CA-LiE@(%{y!BzsU%gG`Q?sjLUf%qFSl0y)2#ae*+EI>s|i`d^V$Dn)qmzqRq6VJRY|{4ujsIU%#bnqU6MR&-1I_43=|5(6Jr;Jvert) zE?S|Tmn}Tv<-??sxV5@9t}3D=>YZ0JrQe$CO~|EY=Lj9RM&4svQHPQL6%pV5fPFiH zfXDx;l@~et{*{U*#c#Dvzu)|znDO7$#CRx)Z&yp-}SrD{&|(MQtfUz~n35@RLfUy=aqrhCX0M}J_r5QsK~NmRCR|Nm&L z41UdsLjWxSUlL41r^0K&nCCK>fdR-!MYjFg(z9_mF^C|#ZQw?`)f6uVzF^`bRnVY& zo}@M06J&_+>w9@jpaO4snmU;0t-(zYW1qVBHtuD!d?%?AtN7Plp><-1Y8Rqb20ZaP zTCgn*-Sri4Q8Xn>=gNaWQ57%!D35UkA@ksOlPB*Dvw}t02ENAqw|kFhn%ZyyW%+t{ zNdM!uqEM^;2}f+tECHbwLmH*!nZVrb$-az%t50Y2pg(HqhvY-^-lb}>^6l{$jOI6} zo_kBzj%8aX|6H5M0Y<)7pzz_wLkIpRm!;PzY)9+24wk2&TT{w--phDGDCOz{cN_ca zpnm7`$oDy=HX%0i-`769*0M6(e5j-?(?24%)<)&46y0e&6@HCDZAm9W6Ib#Y#BF6- z=30crHGg+RRTe%VBC>T00OV6F+gQDAK38Ne3N9bm|62tPccBJi)5{B z4zc^Db72XiBd}v$CF|yU{Z=M|DZ%-(XarYNclODlb1Kz1_EKLy(NSLCN`eUl(rBCL zT*jx@wNvze0|TSqgE(QArOZU)_?qH(sj#TwzElLs9q)(0u!_P|R%Cy_0JFQxgGV>1 zz4?_uq<8_gM0`c*Hh|;UMz~vrg1gQXp{ufg`hM_qU;U>+zmvc5blCLSq@PrEBSGR# z&8=2Z4uXN`F3p73ueD1l{s{k$WipAvSh5W7ABe?4)t;r@V?y`bNB5FvBuE|0VRTb< zM1Hn^?DSsJY+sX@T5xW=#>T9VEV|?<(=6|ge$X6Sb05!LFdjDcoq*gM(Zq=t;_)Le&jyt(&9jzR73noru`a# zN*<`KwGa^gZU3-)MSLF0aFag#f0<>E(bYTeHmtdbns#|I)-$)mJ`q9ctQ8g0=ET?| zdO}eZ*b_p>ygRTtR^5Ggdam=Zb5wmd{}np+Jn1d_=M`~P=M67jj})fH4ztb5yQqQW z^C|C&^LHAK-u+ooIK)yM)QM?t;|<{P;;{`p=BclzAN#JzL4jCwXkQB1Dy{=^KR`=~ zTrr)y7eiYBzSNs_DvO=4A6#EgGS-zY%Vi)N*Yb`U;6o}KR}dq{r9pT5wqZ@3NOE8- z9-(}D|Nc5732CSYQbL)!gPQ#RbD8BhK3dl{sUuPvei0tkvnJBxDEAYTesU8H$)g(Plra{VH(v3u^CO1~(+ zU0O7#)jaS4{NcwA+LuSm&VBcX2#Im3xg)W}ySNw%->orn1taZ&+d)}8gJTqA!u|5P z{yv?zol_3|(1(%M(EVU=cp?L`{Pi|ixk{U)*guFML3P!OSlz;zGA#T+E@8@cgQ_mv1o7RSU=Zo_82F?&&2r;WE z@wk}JHYEZ9nYUc(Vv~iTCa3u8e4q(yq<29VoNbKk|`mq%I6u)My=gPIDuUb&lzf4`MEA9^g8u z)vp8|$$HE9m_BTV?lOosIGa4jud=jIbw)O2eCMfyw2*S8?hjWw^nqws$O*M$3I1)x zR0PWFb3$ySOcGTe1dz%N0l;RPc`x%05FtT^f^j{YCP}*Q=lvp4$ZXrTZQHhO+w%wJn3c8j%+5C3UAFD&%8dBl_qi9D5g8fry}6Ev z2_Q~)5^N$!IU`BPh1O|=BxQ#*C5*}`lluC515$lxc-vNC)IgW=K|=z7o%cWFpndn= zX}f{`!VK02_kU+Q5a3m37J;c} zTzbxteE{GNf?yLt5X=Bzc-mio^Up0nunMCgp*ZJ;%MJvPM3QK)BryP(_v@ei4UvHr z6+sbCifQaOkL6-;5fL8$W($zZ_;CZp305C;~$hhRquZr-r)jjd1z z31%ZK{-(`P#|Um_Sivn@p$-vz46uqT>QG0B1w9znfS9A8PB2LaHdzA|_)yjXVR*l{ zkcu3@vEf7bxH0nkh`q?8FmoO_Ucui*>_a~P?qQrlZ9@+D7%MTpSnztpylXrt5!-k8_QPB?YL8Kx_On8WD zgT+111d(Op$^$&KLAN5+@?>f7F4~wFi(8TL8+szgVmcMDTp5l&k6~=rA{Dt}!gb^r zSWY<)M7D|Z2P0cEodj6E42PV>&>DFmQpgt)E-|#sSUU@uKed+F680H@<;-x{p|nuH4!_mn85rx>wz;0mPi2ZkL#k6;sznu?cXh!T0S>{w6 zL^gvR05NY64l*<+_L>On$rjx9!US;l;LX6@z}yi#2XHh)F@Oo+l)h%fq$v}DNmF2> zfs^_t0)3N-W<9-N?uedVv{)-J0W5mh#29QM5R5h&KuiRM=0Zvnf#lF=K#WlCgc#9c zS;qvh(P$!_a8JwyhI^ZJV2k+B6Z^64?w|1?5gyo6y{}923CRZfYVe1#?F% z7h2SUiNO3;T#JUOyovSs@@C1GtwipycA=*x5{BpIZ_#GCMuV8XK=x;qCNy{d7?wA~ zC+=vjls;ci&zW=6$H~4^K%v{p}Ab?U%C6Z4p%eC<3ExqU$XR<}LLF67A$Sr20DR_pJ3yeBa~ z^sw{V0FI5;UpwXsScYuhbqGQ`YQ25;6p6W^+tgL&;Ml;>S3CGpSZ>VrTn0m1$y$HU z&65)I!c?oREz};c=nLCliriqQX->4uivHTgd${GqeAlf*!P^B|jkU|*IdNP(&6C>4 zqOW$)Nw9nvjy^&`?E|gotDV{JmJ9Q~vuhy<`^C4XIUDt|j4o6rK^e8_(=YqC zuaR6TRVf@tUFHB079o4MBIh{M~4>WwnGgesQH*3?w(RA%hCZ*7)b!aNV=yOQ%o_Y=Lt0Sl*(9^jfRnC210Om$=y>*o|3z} zAR&vAdrB#mWoaB0fJSw9xw|Am$fzK>rx-~R#7IFSAwdu_EI|SRfB*yl0w8oX09H^q zAjl2?0I)v*odGJ40FVGaF&2qJq9Gv`>V>2r0|c`GX8h>CX8eHcOy>S0@<;M3<_6UM z7yCEpug5NZL!H_0>Hg_HasQGxR`rY&Z{geOy?N92Z z{lER^um|$*?*G63*njwc(R?NT)Bei*3jVzR>FWUDb^gKhtL4A=kE_1p-%Fo2`!8M} z(0AjuCiS;G{?*^1tB-uY%=)SRx&D)pK4u@>f6@KPe3}2j_har$>HqzH;UCR^ssFD0 z7h+VLO4o@_Yt>>AeaZKUxqyvxWCAjKB>qjQ30UA)#w z&=RmdwlT`7a8J8Yae=7*c8XL|{@%wA8uvCqfsNX^?UZsS>wX}QD{K}ad4y~iO*p%4 z_cS{u7Ek%?WV6em2(U9#d8(&JDirb^u~7wK4+xP$iiI6IlD|a&S)6o=kG;59N|>K1 zn(0mUqbG3YIY7dQd+*4~)`!S9m7H6HP6YcKHhBc#b%1L}VIisp%;TckEkcu0>lo@u995$<*Em;XNodjTiCdC%R+TX|_ZR#|1`RR|`^@Teh zl#w@8fI1FTx2Dy+{blUT{`^kY*V-AZUd?ZZqCS4gW(kY5?retkLbF=>p=59Nl|=sf zo1Pc|{{N4>5nt#627ylGF`3n>X%`w%bw-Y~zWM_{Si$dc82|=YhISal{N7OY?O`C4 zD|qb}6nLWJ`hUyL+E>-;ricg9J@ZNYP(x(Sct&OI$Y!QWr*=^VN;G3#i>^1n4e#Je zOVhbFbLpXVu*16enDM+ic;97@R~u&kh__kgP#!R`*rQEnA+_dLkNP~L`0alC|J;c; zeiK=s8;BsLE)KbG3BD&Br@(Ha@SBT&$?xX`=$;eeel=|R_dIr6-Ro?=HEjnsJ_b`1 zK6Yg^-6;^2aW!xeTK)A~3Rm|L^FCHB_I>jIju7ZGo&N_1*QHkxH2!!%@o4iZ?vntS;&zJdPe1dH#04YD93A44o-MpfD zP{rn_aq>U%RDvC2+bp;xPlsOzauIi3*Lf42`jVKKZCRuKdYhi>FDuL2l=v{$BCN#Q6796s%r-AG$Q^t(3c@ zD?w0UhYr11@feiyl9kY_@H8~|xlmO<8PfQmj1!$@WieW@VxR@Psxfe-v9WCi1+f>F4VL?0O~K7T?m4-u|pSkBpUJZZe*16_wAp zSYZ@;k`3;W3UHKUWc8QeI}0jH5Ly=cGWQPw(Kr2fm=-5L(d`lcXofy8tJY3@Tuadz zYWXR{mW7XT!RF#RVCe%}=tM*O6!AD3^(!8un~opNI%Uko7$5t@<8+?; zTxDys(MyyGsUjtSu9$+|_-t!U3fVb1dkK?l`17<+jfl=hrBHnDSV>^R1=TnQeyqbW z>ov#l%!1|S!1>8UUxIdhQq`_klcHVx0{?#>K3#$4GlXncwldt!g17TcvKq-jo_996 z>oA=tH9CqRl6Yw?Uc`am!V?lHJbizOJaVaScf1UP5e7Dbgabq=b!B~T&_F6?ooU>w%x0A zH~&MHJ=q`fCH{U<7MDXE4SD32cDZA)WJeWkllJ`UspWaS#eDe^kg^oU_A14UE9zG-a^g{xaXf$})Wik>gT zl#dkzGr(;h0JZDuFn(+k8wNq?PZ5grQ<+sM?wBGt@JnH6v0#or-5wBQWKU~(S_> zkE!tc*ZJ1Y&*p(xX84POb3cClRMd!^qJ#CAZfIepEj-<`VURS_yCz0(?*Ixcj4 z-!zV1_QZhpm=0<;*(nm+F>T=)o?ep@CK5I%g^VAA+RB25ab?7)A~z~egru=I1S|@v zH7tXV!0wmGS^qj#e+MY;C5eUjEAp$Y?LDkS^QPZ}8WN85?r$u<-Epi;yZ1|J2J`se z$D6DpH~2F=eI0B&=UFAUnJvZAmClJlK)sutJ?M>xpZiWV&0=G4MZP+x+p>EX=HbCz zxls%Mw?*u^;LbHWIWCyq+yi)`GmFn9J112CZda_u@YIP%i;srFg_paU02Ifij*7}l z&CF-(3|>*a|+vbNR`^RP=9G?ymEJ0Z~)d&c*UE$UMepZ zcITr{0WqhxkjUnM15js_gW=e3Uh|y6ZReaXHIz-=p`x5VvB&rH9y>Amv@^WmXFEw) zQXYrk3feir=a{jMQ+wDIkkFnZ$k{sJakHn*?u za%4b!00ev8NVLM1TY=cl?KB&55BY_MU-sg?c>=Dbz_W{(Z~c?HJi*XpYL)C6Bd8WH zt+v-#0&o~@t4qESi*)+eW%@VD0|o^yF)n0hME$UtXF$*Lvh}7sso{`|pn*JDIy5^Fm3s$5*zEE=?u5<=l8FJc3r%+H} zdfoNl2J0^~!-*mOL5o-x32|e0Im*E!yY7F7E5N)W3>+v_LBydlEx?4$RL5f2oYRD# zaR0wv(-p~wO0eLDl3K=%`{5+0Gd$ktO=W)gWlGZJ0`K z$_RNA=ckrfa;H0KA~dR^p�(p-{x$&=IACIfoAR!za)F-^da-t3#0Dycnp zwO~NVXwXCl;jE<}>%@xz|=8fIJAB?>+E{7)|4l${4ngA3G|=r z2Dyv;VVWSgZx9Wj>qUjleGl3Ei9K4>h!(lPS%8VOG>Xu0%6VDz^O=bjJmuP7>DeUv zrbI}MlHB^^d?{zv6d=@_ZD2lg1&G7UjnVN{1}9WkaM3H~btX0GtSzB+tZ^qRgWo4m z!GmimlG$=wgXCnr6j@m<1gAL46#T~5Bnm=2{^@>|t&`9mkEPddj zAvG~@Tv~TAm2i%VW}R-g(Z0)z-Y|szHr@rk>4MAyG*Ma*7Yh#H7(!-5>DZ@8r;_dx z{prSe<>~099F8vsYd2xff7uAS%7{S)f(|@me3t2$iy&NEc7OUEchp@9A|X;;IA>8!oX+y(BKJ$EzV* znR$z;!L$s7uy@{OT~nG#B!NRraT8(X##Ho!0r_o@gg0CA-9H^;-uE&?$2$nHv_00o z%cbuUc-tCx$Uh&EZ4Nf4Zgqv)Y6>usG3>GeQnxx_Z6+PcbX-+ysbt1hQ`K1LDpOE? zrAhIZhSN9yVIAOa22gn577tbc&i3|3V8NWy&!tw##`}9*x}gtI^h1DzZRA>UuaJG) zaZ7j)dq!O}{?#8Y7~7i6fHh4{`pL?>-18|p!S75Y#^DM>-S3)vuZG+Q7l@ek zQP~#cBpWgg#mApc_sPYjpw8odQuRokmTkzcNl`^CcKB7e&;zViV;{Y{o^Y$%7i0m# z62%#1Lq!RC?}lK>%mp}T!3Xv;L*0v*>USLm``N%>w>@fwC+#T&Tx2bN4w(20JB}oU zuSa6v^kXi0xPs?pbaOHnyiqq6By1EZY9OZ^^QA>{q-Hsd&m`pbQ%8121aWG-F5xf zlZ%;B{;C>X19|`^_?dVyCq>n+41w7|!tUS!{9rHlbhX=SZO5CQ^;!Du_E7*`GiR^Q w)2!4MKjfSAeNo!9>IaV6aUZ*?W>} zs4%E?srLW`CJh0GCIK@hTkrW7A15Iu%N&?Q^$0+!{Tv&|t^Y@u%!L zglTg&?Q5q#ijZ;&HBQ?FNPp;k3J5!&{^+SGq?AX~SiOM9jJMRpyP?RCr@z38AQyy&WRMaC;n4una$~nJKSp?q|s8F00c9?Q! zY_ovvjTFm+DeQM^LXJ#v0}6HRt3R1%5PT*}W!k8BEM;Jrj8dIceFo2fhzTqaB3KKk zGlCLI)gU25(#u6ch6GeB1k@eHq7l{EHXv0n6xE#ws#ri}08kkCf8hUt{|Ejb`2YW* zvg}0nSSX1m=76s?sZhRY$K=3dpJ+y*eDULGnL2}4>4nvW^7_<~wIM_5fjvwt4h1|g z)g0Z6ZFq9j<~9~b8((~TN{Z?ZQfw|is&Xp~AC61sj;xItKyCHdI|tCMC_LbXF>~vR z=w6V3^H=W4CbAgR4#xw}ETTwu2guW~=Crl@SMXv85jQ=%y!s^?m4PI0My7MWICO;- z175jm%&PcPWh8QdOU(#8bp4!N7ET-+)N}N2zk2)8ch|4Q&lPFNQgT-thu053`r*h3 z_8dI@G;`zn;lH$zX3RzIk`E8~`J=BBdR}qD%n@vVG1834)!pS1Y?zVkJGtsa(sB~y zNfMYKsOJb%5J(0ivK8d+l2D2y&5X!cg3BG!AJ}910|_${nF}sC1QF^nLIhzXk-Y#x z0)&1iK!O;Og0Ky!;`b~v%b$`S4E&fB)1NB4v@8wr( z&+NX4e^&o)ecb=)dd~C!{(1e6t?&9j{l8%U*k4)?`(L3;Qjw z#w7FS+U(94MaJKS!J9O8^$)36_J8;thW#2$y9i{bB{?M{QS_inZIJ!jwqAbfXYVd$ zQ5fC$6Nc9hFi8m^;oI-%C#BS|c8vy+@{jx6hFcf^_;2VRgkoN(0h!_VSGmgNPRsxI z8$rTo0LaYq-H5i&gtj81=&xU?H-Y2==G@uQV7E`@+2E9XQW@{&j`?EOktk|Ho{HU>ZqDzvgjwBmdex z&uZNd2C1h{{}2k6Ys9$*nFP3;K%u!MhW`uZy7Sn`1M1zs@Es&;z*Z>Gsh@-3Fe6pE zQD2@cqF((NrRevgvLsvM_8;;iNyJ5nyPyy?e!kvKjGj`6diRFBEe49Oa7wwkJFV7Z z$YT&DWloYu-H?3<0BKn9L&JYDT-SK~*6c5pi18P26$JESKRYj{T7Zk6KiRJcbvOO*{P56Q6s8msbeI3>|j>K9}Q9UBeq*inXKemCm`-<5|-$ZyN4u$(3 z&HcvqehFD%5Yrmykg-^d`=BSa8(i=>ZoC77^mWY{evp(km@aHqhUECBz76YiR+VYK zY_avFC~V3$=`6C4JhfHAQ@DZtUOwH`L;oYX6zK0-uI^?hS$ALfq}A7evR;ohJHij} zHSZdW?EKv9U1s4oD*<(0oQ*;MaQ6@cvGL zuHCPgm_NhVsgp^sfr*ia^Db}swo1?O(_Q2)y+S$CBm+g=9wCOUPbz(x)_GbaKa@A7 zuI&!ynLiZRT#V%_y_-D`0Z5lT*auoe{(U5NylTzFSJW()W-#F6*&A`LNO1bV#Y;QJ zSbLBnp|B^dtK|KIWC|No>JjWBWE@n7O)x{&^E(WMeMvp57#qA8m* zeTow*U@_86B#Fm*rxyYu5PRWaWHx8y> z*qmHEp(AMDl0v)ij(AY8fnH=~ZwwjVAbu*m5;xPfidh@ov6d8g zfJsi&!QyK53Es%sC39ts;54V68koALD4b|%tNHW0bIkZAJKa=W&FomJSEDT>W1xIX z1x%Z>AvNIsSPLcn3RTcHXb@KB?cuM)=x6fcIx>&(GxqZ8w3p#jJ(GVgc*`c0HG}dv zIop&Qim!K1NFwic%07KcjWgHBPUkq7f~lj;TPqVGTiT#cUeim>;nY`>h@a*S{qQex zQ`z62WK|Mj)Y{tfF{;T4P;c8$Q|KU?Joh zIkA^z%X7z|r>4aTh@|StTi!-r1D!g=zb#3d#{{&K3CqE$Iz-UH<%37c zRfkO`&uM%#AD3PHv`g5t0e^O%nVL0d{Xlx^EjEC3#skF@`zl-7PF^0oxW)1!C!JxR zWvuAHH?)61FKA1QeT*_sY7;_Id#!GmV4n`MO{~sv}VLSK` zXRw=Y=Clz*00B(5y^K;gCZMAzjT5+c3IC=)l(9VIDdatpxj3y89WwI|bH&$!ZEvp` zPR!T@#!(|KfI-w?!&+7$N3F6>tD{YO4Qg$d_`nNEdfVCha9vaPn0jI0`)`@*72hq! zpU5ND^P*RoEkbD5o#az(-g=Y)L>HH>Oc%}$ zT3Rs_ih0;4+Lv4Y;@Iv(;fUbQ=i-G(#>vghec~*j(I#r|5mqFiJBpzi&hzEcD{u$< zRsm0BVYn=pT;0>R(itW|*D&;O%bOc7et9ACaH#J>z3A1A~6fdP>pmbM%xzm4>|;c_?B+%sl;Qs2{t!60$^u zH1t@9^6>;?!FuusnISi$f5CL&;z?EqJN$FBuWDA#D5`cy_UvCFIVvf{c?4N0teh;d zET$7aVbj08KTQS!x?Nd1Is8q8qFzs}a=!@nJ;7FSfCY^T@D-gpw`w<6e#X3+;O}1h z$%I!M)0bg|EKUA04Qjn@+x{Rj8vt6Wn!R|3A92z}^$KfF5(#CWr4y#~re1CN4i4w0 z#GsypBR{xA3Er7sgAi(|}1-W?s~n$7?K|9WL8kpVfw-;#b9 z+mn;=ep!162U5R>_t}fOt~tE?s#m( zO-S$7>Ay6*hHdZ)7_oU915WYYCIX;hFI-U2EWYX!pllONr@Q--2o~`!isi6vTPLJ4@(|o=%NHYjo0_S&q*UQIROw@*N-By@PaQ&;YxFZ0aR zX&}LeOEz);#m~Hwm^VAY8DK}b$F4bo{jMN?d!lxKPhNklzr^Cd`0f4oJr^z=I|l`* zm8AHm*fPV`0=lF3Pnnp}&J0N1X@}-D94YvmUabFrLGSnTz7Mu^21F#O5tN#CuY9Vh zUZBH=ez%h*wkf0hBtXJh1SN3d+IF{gzT7lp)j}n?03lt;XSQRAh7qd&v;RwTYDuQ# zbI2*r<>?x-G0@hM{;%{VBD7nLKt~D`T~-HAt5;h%i0_=Ifs=yHma5dhJ+QMG?Ux(a z|E?1CMy1!~oA`FP!k~iG=t&5#>bVdz=peT8HMB6Y)#7PpETtNryT^+Rv3vpJaF^zP z{H}0-LyV9Fu21ID%wO9f1IKlFr1p4c{o-?03vyB-tr5duk^&L$;m_|f$vs`^Sl{j2 z95}oY{LlY+=ZS%J+tZoXCd0*sSU7w^gjovXn+g7uyra5{cU49@yHf#Z^Jl-$9cIfo z+AJuxH$VLb=#+uBbVmUjnx zxb1pZ@-O9=AIk4@S)m6fJ2?{HrNYwwnL3a45muuNjr;6$O`bGEM0T4A2_S$t=86*- zcO+0mywg*j#A4mU}enR_!cGmIYQ;qwfchWtFEXL)AK%*;=j znYne+hS4EMy3S)C*mZ1KI>!+)0V@9!N6H$Y}~MJ{rYuf zz^KljIWvFi-?#?V@LPR&c6Nn{!=XM z>}-h$S76;$H{E{Y%@^zlmOl^efBwa%UU+jJD9UVukQ3ti_kH-?H*RC0?M1W%FCvMB zM_+v6fk$6X2sx)-p~B3&Kl{nscK}pNLM*qjtpaf9>AU{-iPKQZR8yCg!TY}Qg*(;) z)gdvCcB%kppZc$VdvsK@)3l1{&DG!d_6OHOS`y=ITLEVu`unSKA2E%JD*DVX{LJ}K z9l>hMRDqxQh0lnpGHpVYneX}eA3Pt|2v%=q;rt)``R|#bDyB)OXY&vI_@|*}h}G?^ z@aZ4_!7cQPX`!fW_?{oT1NTwHs#l5L-0`E|y@48<3Q^HFf8=Idi zpJYD%1MkII!~|7I^WGo)IF=?{>ACnjJ_WUi39C}!Q{QnheVJqeKKqq5^o5CBde(g9 zvw$X6^jz_^E2$wSw4!q5*RG(C2_^XO$HBn_55vbl44OnTTRwRaePP0vo{K)U1#99& z<>rq7V&V(<&@I%MFoN5zrY}sz=(*-L&}1QQ*a%`u25h{cFj===17eB_uGuzG&byQ< zrm8BJZl4r_E$3k|Wo6FW0-6M7>qac5uFQsQcmkLWGfeH74S3Z_rJ!jgN++!@i=HW8 zkyjI(oPH-+-N#Qc^-mpNO`bc6r=2-<%&Wy5K1vfFJB(L_IkpS6fY^NmuL8qsgj>MD zn~BHH9WM~32_3vd=W&B)k7F9q%stJx+b_L_X-4zr^LVUMCmyCTA3sWtkvsmME?Xiy z?xOSfB=_$oY06~J-HcCq&)qcW{j;uP;?Dm}=hkq?zh&n!;m((-G-u_t|6x399Q;>A zgNpxoJNj{u|MFDH7Rhq@FCAl0dE|ddnl!oh9{Lq?@JDoR6L;C941IK`ISfdE$4S zE0AUQ8+2|Ncl_q5QkSp#AODp~(^mfP&%Au@@|TBQwoP`UU+V{6u8|)6ZA{~uKmQ*M zmrMTDU8S~8Eqi{^v0Ug&5Upcm#y7Z1(RbgZAG8jB$eRwCspQ)>5;U)oGZ&E5aeR*K z8Yt`Y0$G))Yd(Y3KH}tA4`-_QmNke5hU_|nq=xtyjwW(_o?itz>B>WM&^63bNdQ)k@-IgDHW*RW$Xo9#RzrTrCn7L2H{9Amq|qNg@#eZY=|P zCoI?2s+L)zsM%WX(NbVEY^`C>lFjIBYmJ6@DKJ0ZT4&F&WHW!dwa%QzOG!?jY_2(S zDcEzZbz*2Q!43|z))9yOP9X1Xt%DXzwY(3tl-TR=Qb_MbZYRrooh;dYYmS!U_as1(=YVB?Q_A|tNu5Ut&_q3jbfDM zoFxT^uEuH`nX3*sB%K?GuHUkweYReBwnHqh3P)~`+s3+Tj!rDA1e)8vuBv5J*IsxC zkd^~b(aGzArj08{>cnzOuy04C+C`}gb|Yz-1avxeWzev3NzcHbz_&4W@QCr$z3~w=8Ua- z`;vfG1~BP8CyLb=F7t1am~ph_#|O%$khSJ9%Vtcn)YmpgQxF?xM^_Vb+5fnpB^W0I`f%X8gb9#X{Q-yJG0{Z56aWeI&zPxnf5pdJA38bM`cYnS#x)% z`n1tFf$i)W-hGm(f9mde^=X@NcV_lFb=P`4&CI&H=IArijGwdCk&X@uQ$5xmj!~^? z#$ROCI)V-~t%L%GS#wo@U27ddR`4`3)WoB{R-4snfNrfee|kI8^bu#yDgYqOwas9# zmcb`3!kRJ`Cr=_tq)8aMt{aGtUZsqwVlj6DgCGre>AEt&x8H_in!x@uwgExIh|-mA zjdaC(29~CTVSaaF7HPbql&*9Uo8P@f)>LqCXclr}peS7_1BQ28u9PO8Eq1@`l3q9o zkfKCaO2?T?ZyA6loW<#9_c^O=m<&h}CA!ineAD@=(gbq`vyT|tiJ6#^B1$P;;qax` z55k&Q?wEh#87niLo*+n4L@65J(Nz~=Ya%7^(miLb(E>A3B@|Jjl;FU&D>o|9#7PJH z?|ago!o;WC^h=|T7PVBg(DAB}72cyUS zb(f>Bwbr!F1eTCO5fpj<{PqhY5>143p?~5ZA5H40);=@M#MYvrB6gqHbU_!GSY??i z%s=>-ciA4*zOOZHds0a(kWewZ4h(k8h(ua7HX)Au&mY~H8KY6(_cb$_&fA@QjIW-*heP3%$d!m5^AdnT}`12qA^c@!g3DOwZ5WwE2?)-yU z!)Vx#Mtxt?FzFTwK!77sy7)sMzUd->w4^bxtpM2j!b1pjgyk zGKwWGeb4)^zjy{9Es&PU1}gwg?|J#L$KJB7ett9@4M%-nGtIQr0>Fl@8-yh`-+1ed zS6r}(MeSvgSoFmH*_WPu@i?}!AB~2?;i&IxrkNg~cQ9Som98tcq)k^|eeER|Zl77t za-TVUc;DNvzVXJ%w52+#weN?+;i#{f#!Oc&z?81*N>^e~ltRS%ZI@lR{rs()HmqG! zx*}ZrI-EZ}ckJMiy>A^oofwDfC~IH)z8{VHKGT@#E5I(Ll&+MnMCl>~AV7+>Gi%mF zkU1QlKASdR0B80!YhP<$Ywi0?W2Ux45oPfxv9QolWzJPD^weBfvo4SONxP35106sAmh(e+vAs0GboFD@PvNs)jNPvarhW}0YliZEg{Gazv z+JDIpoojRVPr<*C|BTq<`6ga{5q^8^!|0cxe=rZ!zxH3%f5ZO0cQ*Z<^$Yt2{|Ek0 zyT|*F+CO@K;(owBKtGg!S^xj-Z~rga2m6nxKl9J=fBSuNKW_dLKWhJKeg^-Xe`^1? z`TyJj)8E!#>_3Y?uKrwqq3LJ#SGU>AzUO|6`nR^u&3FNN_jGOc zw)Nw`wr3yIKhgcee6IaN=ws>M{6677%)hPwx&HzC(f&u~&)6@b2kNRzBDQAP0*H73 zq%McOmRk{B3i47qRe=DA*$&odrbEJZ*pV9XXa&p@wlW~@Yfs>V{yiTtplMhgM*-Bz zsSnlq&pG;z0OUN%$~$3=g1UF+G*>+17eRbBf3=y79J}KR8owon@$1Z7MIrvvWWH)34nK2SD)GsrJ{l z1Cl#oVo3A8qY3e=aF)qzms~FG#2$LzT=gs&aVMOj>(%{y<&O0cG!nCiESl~x=^dF{ zKvj8F1K8Ng171wwM5Fh4KoQw`_c6#y$(5cAm7e}~nJ#A*fx+c9;y#&W!#VukR)ugk zKp3=+;Ut+IYn%m+r4d*<`L2h%aDnX5}^!5R|H;(34AoVWjRx(msBZvk;rCI*|~ zdOijqI@9Z{Vu!~jvHW{lBa$rnl4+!s_5sfK3bCGk-B%iDe&@-}+%fOKU|(9?V1 zHE8&@4z)Kx!RAvAs z!Wic9=o#(bg?kc-G68-m(jZ`^=XGUXb)}t(%&~sjFnV^sEX%hSy6UKC4iOhgV=BHV z2w`4g7Y=s#Vu2B_?#VQ|hP39@eArgfX>-0S+dd&^mx0*wp}>)x;c4RUgxz%;oNe?& z-7-lJ@Y^2^C;=qJsxx5|xF)*pTGhch2B&kxtn;f!7=gznk}I3}Dh}(CoMXgA5-p&kS202!l?!fT3t|HG*rIP~mS* z$Wjo}jq3}z$Qq!9yrtd3fM0N629ZM?LU$nv@Tv9b7I;D|;0H2dsA~g7Z7zp1| zB)XmrkMgF6OQr|R)HHD^TE{Y#j!~SR?b`Xt3Qs`B+x<hxexYeAjMUWdZ-*n9%(1)Wb(n2U<><7&9dwGJmrob)4%H? zlQ%z+L-^$dFhhH|@u$%97Qz?*Ynh2VG@q|?8vY&L74&fs&_b&3$x&Oyjl~LQDRRap zJU4U*R+(2Dd!G+lh8!V{pT_UJn+^1Qg6$` zqkNm(a#hWyc6SP+p5=C4HL8-m`pO`5o~`-LI?_h5CsH?F_%?nDodmz&pWR20WTpJE z?N|wSzLjMUK8E)a2tI}Lf;+;*M|h3Y(U#>)g1>zk9|Hd}oZAa2 zLYBWBoSW!Ts!RwXr^8h+U*@{9{zqS^iH)Op<;r`Uw~nc}<^$V~_i%$GFjaG?X1@E|M`h)nekvFKt`Dh-f>@|0-`Xoq)o` zx;JmzDfOV9qCx|EVpogEe0LK~tGS?5$$L_i6P$P6wIsCQaP_;d{{N=iV@+8LI}o#( zvo*Ejy=IIn{rdIQh1&q-{EuohpVOjJ^Q3lD*YTp37$^RRgn8ihpdu5{Ct%5-KO!VL zcNB6dUajXI9jkm-P|i3~GB-A(X`P1Oqqb$tcku)UJw0w3GeUijb__#QT4j%64z%EeB7S?jlWwx_7&+EEvB|6N=kV}DwnyAlX=?j`) zmU#!$*^@NIu#n_d7;WoJV@*Fbv9|yJO4;n|BNF2xy(54RyB>t~8lUOUW$&2%Nwi1y zx6JxW88>U2$#qhl^6KUbtmg9}D0o5vYDT7kWJthLGkpGnN4T>{St^_EU>4;DmLF9o zr|LqsA8_MoNLQ=}w?8u!ziSZ@PC#Y<#9uJFo-ozVo6D;<8j^1$c|qAE3ZTE5i~zmE z$BU5lw6l=EWsg^y^;8>r9qH{xfL|~PZYK#md$zZ0?o11gV<*WSW~cgy2GYGQir%wf zt4iW8D+;s*;RGrmd(-T<@2&j(Cb9xhV*l-x`TpK`xq|7p?5R%5*s!69?2c!cC*VY* z2DE^9pvOPLU!1e}wA8S8opcTJ3`NB>hY=JQnL~QFXR4K8A$BqJnoEB$wn-%u@E6Mh zCfMF4kusv3N!(aHC}4)Xs^xoOwXd%e^6pi5|DZo=Q25j+6HlJ^7FodH6y1bMROR^q zGu6)fopS`h%Sw<;ZH%TEPf+#81-#_v+@8nlR0jLcIDKQtLleOC)6yLZgC!D9X3GgS zohwU{v$jl=quD#Go^hB{`@Qw*a%`(^jyT~=q^bWgGzRj;|12J55HWdCWV}EB|K=%N z3Nq-qxJJ`>^|1MNN+q}zTB&ooE3j==AgK@^UW<^oSbeALa2peF)Th6{@sj0KyMNHZ zksk1+MXN2tv+22A%cQOGpS9)77(uP9mh+!5T5ERLvF@b}$+WvXM45Z?-kCa)fb~f1 znVbTD$Gx-0Zxc`0D@YgHakge6SL0H`-vN_x?AP0>iGH0_EE&=v83hMJgaKAI0jJXm zVxVz;X<$v6WW7}fxROO7vr#YLP;;lij5VrX{;>7kK6TtOH&6|Ar^xo>00%+u$C4@# z>!jOt6*3><171+WxoZnKDTzJtDRw+T030;yI}~uV@9fCnei^I*j>Bp&mzP2d=FPb_ zCM*l_+$LDR3B*a!A$g#>xsrZvw0lckxmMg>0aQd7tPyN=t{dgXb;Ie+T8{fZH=gdu zM7Rg9c(kg(Jg0?ARRRl=AONFKrvFj)lTY$KfT%6^6s`mk*ABGhsce*LsoD>K{z_M2 ziPpnu+lw22PfF!CoId^6n*G4H(Ix+#+N{C(da7t1BYMGEaE#PdpOLxsVD5riQXHp@OX;`S`8VnpM~)I920w~<3|mo0 zf8~Az`*?2?H&gZ&*K&bRkV@qzvMlRHXys8*Ze2+1c?5o!^+$&MHxB@4Ee5cke52R! zmn7AZtY6ST%ixgU5)%$%QcwHj7Es-Qu^kLAPwy%7pGBw_4Q9#da^W2$}axNHr03)_nw z5?yuNmXrI5HgS46)c5&}B)Tts49oU92>3xBLLy}FMUW=84DQbVq^;7_e7|(Sdz|&J z73N+M`rc2rt*oSWu#7S{*s~nH6HRHJS1SmzeXk|;CA)FI4bat3<%}nkB%;;?=F>B7ms9QSxv#@+69;@>QaR?REYX4&)=itG>rM{<{A79Rmk)`5ON#GL`*KX%}Ihk3w(RtM-WLt z?f&FLF}4N^yE!(pZ&Yj&Bc`~K0@4_}*0Om?wN|}4WJ>WL;G^H2*QpgEkGA~OET-Km zkwz|5{6dnz1U<2Pe9DNL>3g5FEIvp1jzP&2K#z~j%g6!7B;^zF+o95?fV{3mnB8*RMhCDNp>Am-3e@jNfMj?jHV$MWjk!DDKP zkAz$Y?Sr)!GUOX}qTQ5aMh|wq1uq}~joWyKl=b_LboM#wi{CMuz5x6BKlA-qy++cM01D3b7`uD z#l6M4pI;JCypO8JZ6?U&wNxR!{4oB_ zlV!x9+-&Qy6{%MQ{~yoZGkKiTSC`YS_j22~G;xUV855g2&C(zm^V!(wpcm@zn{%!g z4}JGo(sGZ1O~to-}le

UmY2RIYtNPVDpE$%vda+HD#3m z&VuXJ{BK&Qe+rBa7eq}Q(bq|tn(RrJAk|ztj2(i{d>nmQnM?;HF2k&9sA6up5tmjl z7lySlzMbifH17-m-Lwa_F&e7nOH?ESi3#ckR3tsM+jsck3`oG!uMS}|eAwVXv>}qxwq?QY%QJ0}r@^;fhuUA9W z*BVl>TGo&N004@xSiwDUXUvp51sVmqO3m)=B55aPwf@0=e}cN+$-BdKxY`YrT_4)0 z_d10#i44Q*rFr8MC>*)v$EJvz``(pb{e&*6k+b zsMz%($|1+8hn8c2?P(l@;Rb&CsZeYoCI3?2!LqjbwPXW3z4G$Qfj=cT5Yb%vY0(AX oeb?AaKtwrnc|$|zzw9vfvn^aJJ!zd)XFXqqy0000001=f@-~a#s literal 0 HcmV?d00001 diff --git a/examples/skills/auth/demo/src/main/res/values/strings.xml b/examples/skills/auth/demo/src/main/res/values/strings.xml new file mode 100644 index 0000000..3e25bc5 --- /dev/null +++ b/examples/skills/auth/demo/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Auth Demo + \ No newline at end of file diff --git a/examples/skills/auth/demo/src/main/res/xml/backup_rules.xml b/examples/skills/auth/demo/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000..fa0f996 --- /dev/null +++ b/examples/skills/auth/demo/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/examples/skills/auth/demo/src/main/res/xml/data_extraction_rules.xml b/examples/skills/auth/demo/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..9ee9997 --- /dev/null +++ b/examples/skills/auth/demo/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/examples/skills/auth/demo/src/main/res/xml/network_security_config.xml b/examples/skills/auth/demo/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..c64cff3 --- /dev/null +++ b/examples/skills/auth/demo/src/main/res/xml/network_security_config.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + 10.0.2.2 + + diff --git a/examples/skills/auth/implementation/build.gradle.kts b/examples/skills/auth/implementation/build.gradle.kts index 8caf323..be4f7ca 100644 --- a/examples/skills/auth/implementation/build.gradle.kts +++ b/examples/skills/auth/implementation/build.gradle.kts @@ -20,6 +20,7 @@ plugins { alias(libs.plugins.compose) alias(libs.plugins.composeMultiplatform) alias(libs.plugins.mockery) + alias(libs.plugins.koin) } val versionName: String by project.extra @@ -64,6 +65,9 @@ kotlin { implementation(libs.napier) implementation(libs.composeMultiplatform.resources) implementation(libs.composeMultiplatform.preview) + implementation(project.dependencies.platform(libs.koin.bom)) + implementation(libs.koin.core) + implementation(libs.koin.annotations) } commonTest.dependencies { implementation(libs.test.kotlin) diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/AuthDataApiImpl.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/AuthDataApiImpl.kt new file mode 100644 index 0000000..ed7cb85 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/AuthDataApiImpl.kt @@ -0,0 +1,35 @@ +package com.motorro.statemachine.skills.auth.implementation + +import com.motorro.commonstatemachine.CommonMachineState +import com.motorro.commonstatemachine.flow.data.CommonFlowHost +import com.motorro.statemachine.skills.auth.api.AuthDataApi +import com.motorro.statemachine.skills.auth.api.AuthGesture +import com.motorro.statemachine.skills.auth.api.AuthInput +import com.motorro.statemachine.skills.auth.api.AuthResult +import com.motorro.statemachine.skills.auth.api.AuthUiState +import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.statemachine.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.statemachine.skills.auth.implementation.state.AuthStateFactory +import org.koin.core.annotation.Factory +import org.koin.core.component.KoinComponent +import org.koin.core.component.get +import org.koin.core.parameter.parametersOf + +/** + * Data API implementation + */ +@Factory +internal class AuthDataApiImpl : AuthDataApi, KoinComponent { + + @Suppress("UNCHECKED_CAST") + override fun init(flowHost: CommonFlowHost, input: AuthInput): CommonMachineState { + val factory = this@AuthDataApiImpl.get { + parametersOf(flowHost) + } + return factory.preloading(input) as CommonMachineState + } + + override fun getDefaultUiState(): AuthUiState = AuthUiStateImpl.Loading + + override fun getBackGesture(): AuthGesture = AuthGestureImpl.Back +} \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/AuthModule.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/AuthModule.kt new file mode 100644 index 0000000..860a47f --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/AuthModule.kt @@ -0,0 +1,8 @@ +package com.motorro.statemachine.skills.auth.implementation + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan("com.motorro.statemachine.skills.auth.implementation") +class AuthModule \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/AuthUiApiImpl.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/AuthUiApiImpl.kt new file mode 100644 index 0000000..01c3898 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/AuthUiApiImpl.kt @@ -0,0 +1,26 @@ +package com.motorro.statemachine.skills.auth.implementation + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.motorro.statemachine.skills.auth.api.AuthGesture +import com.motorro.statemachine.skills.auth.api.AuthUiApi +import com.motorro.statemachine.skills.auth.api.AuthUiState +import com.motorro.statemachine.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.statemachine.skills.auth.implementation.ui.AuthScreen +import org.koin.core.annotation.Factory + +/** + * UI API implementation + */ +@Factory +internal class AuthUiApiImpl : AuthUiApi { + @Composable + override fun Screen( + state: AuthUiState, + onGesture: (AuthGesture) -> Unit, + modifier: Modifier + ) = AuthScreen( + state = state as AuthUiStateImpl, + onGesture = onGesture + ) +} \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthStateFactoryImpl.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthStateFactoryImpl.kt index c7d4fbd..181e191 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthStateFactoryImpl.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthStateFactoryImpl.kt @@ -7,14 +7,17 @@ import com.motorro.statemachine.skills.auth.api.AuthResult import com.motorro.statemachine.skills.auth.implementation.data.AuthStateData import com.motorro.statemachine.skills.auth.implementation.ui.AuthUiRenderer import io.github.aakira.napier.Napier +import org.koin.core.annotation.Factory +import org.koin.core.annotation.InjectedParam /** * Main state-factory implementation */ +@Factory internal class AuthStateFactoryImpl( - private val preloadingStateFactory: PreloadingState.Factory, - private val authenticatingStateFactory: AuthenticatingState.Factory, - flowHost: CommonFlowHost, + private val preloadingStateFactory: PreloadingState.StateFactory, + private val authenticatingStateFactory: AuthenticatingState.StateFactory, + @InjectedParam flowHost: CommonFlowHost, renderer: AuthUiRenderer ) : AuthStateFactory { diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticatingState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticatingState.kt index 03d9f55..1e735e0 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticatingState.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticatingState.kt @@ -8,6 +8,7 @@ import com.motorro.statemachine.skills.auth.implementation.data.AuthStateData import io.github.aakira.napier.Napier import kotlinx.coroutines.ensureActive import kotlinx.coroutines.launch +import org.koin.core.annotation.Factory /** * Authenticates the user @@ -68,7 +69,8 @@ internal class AuthenticatingState( * State factory * State dependencies are injected here */ - class Factory(private val authenticate: AuthenticateWithPassword) { + @Factory + class StateFactory(private val authenticate: AuthenticateWithPassword) { fun create(context: AuthContext, data: AuthStateData): AuthState = AuthenticatingState( context = context, data = data, diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingState.kt index a85d4e4..f27cf9d 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingState.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingState.kt @@ -9,6 +9,7 @@ import com.motorro.statemachine.skills.auth.implementation.data.AuthStateData import io.github.aakira.napier.Napier import kotlinx.coroutines.ensureActive import kotlinx.coroutines.launch +import org.koin.core.annotation.Factory /** * Example of a state that preloads some data with the usecase @@ -66,7 +67,8 @@ internal class PreloadingState( * State factory * State dependencies are injected here */ - class Factory(private val getPasswordRequirements: GetPasswordRequirements) { + @Factory + class StateFactory(private val getPasswordRequirements: GetPasswordRequirements) { fun create(context: AuthContext, input: AuthInput): AuthState = PreloadingState( context = context, input = input, diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthUiRendererImpl.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthUiRendererImpl.kt index 635dc09..c896a6d 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthUiRendererImpl.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthUiRendererImpl.kt @@ -3,10 +3,12 @@ package com.motorro.statemachine.skills.auth.implementation.ui import com.motorro.commonstatemachine.skills.domain.exception.AppException import com.motorro.statemachine.skills.auth.implementation.data.AuthStateData import com.motorro.statemachine.skills.auth.implementation.data.AuthUiStateImpl +import org.koin.core.annotation.Factory /** * UI renderer implementation */ +@Factory internal class AuthUiRendererImpl : AuthUiRenderer { override fun renderLoading(): AuthUiStateImpl = AuthUiStateImpl.Loading diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fb40285..cb26bcd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,6 +20,8 @@ desugaring = "2.1.5" dokka = "2.2.0" git = "5.3.3" hilt = "2.60.1" +koin-plugin = "1.2.1" +koin-bom = "4.2.2" ksp = "2.3.12" kotlin = "2.4.20" kotlinDatetime = "0.8.0" @@ -49,11 +51,15 @@ android_kotlin_multiplatform_library = { id = "com.android.kotlin.multiplatform. kotlin_dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } google-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } +koin = { id = "io.insert-koin.compiler.plugin", version.ref = "koin-plugin" } mockery = { id = "dev.mokkery", version.ref = "mokkery" } git = { id = "org.ajoberstar.grgit", version.ref = "git" } nexus_publish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "nexusPublish" } [libraries] +insert-koin-koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel" } +insert-koin-koin-compose = { module = "io.insert-koin:koin-compose" } +insert-koin-koin-android = { module = "io.insert-koin:koin-android" } 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" } @@ -101,6 +107,14 @@ hilt_compiler = { module = "com.google.dagger:hilt-android-compiler", version.re hilt_compiler_androidx = { module = "androidx.hilt:hilt-compiler", version.ref = "androidxHilt" } hilt_compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "androidxHilt" } +koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin-bom" } +koin-core = { module = "io.insert-koin:koin-core" } +koin-core-viewmodel = { module = "io.insert-koin:koin-core-viewmodel" } +koin-annotations = { module = "io.insert-koin:koin-annotations" } +koin-android = { module = "io.insert-koin:koin-android" } +koin-compose = { module = "io.insert-koin:koin-compose" } +koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel" } + timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } test-kotlin = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 10a00e1..f52b1ad 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -76,7 +76,8 @@ include( ":examples:skills:domain", ":examples:skills:appcore", ":examples:skills:auth:api", - ":examples:skills:auth:implementation" + ":examples:skills:auth:implementation", + ":examples:skills:auth:demo" ) diff --git a/build_skills.sh b/skills/build_skills.sh similarity index 79% rename from build_skills.sh rename to skills/build_skills.sh index e5136fa..28a4b3c 100644 --- a/build_skills.sh +++ b/skills/build_skills.sh @@ -2,7 +2,7 @@ set -e # Directory to store zip files, defaults to "artifacts" if not provided -ARTIFACTS_DIR="${1:-$(pwd)/artifacts}" +ARTIFACTS_DIR="${1:-$(pwd)/../artifacts}" echo "Building skills to $ARTIFACTS_DIR..." mkdir -p "$ARTIFACTS_DIR" @@ -12,10 +12,11 @@ if [[ ! "$ARTIFACTS_DIR" = /* ]]; then ARTIFACTS_DIR="$(pwd)/$ARTIFACTS_DIR" fi -find skills -name "SKILL.md" | while read -r skill_file; do +find . -path '*/.*' -prune -o -name "SKILL.md" -print | while read -r skill_file; do skill_dir=$(dirname "$skill_file") + # Get relative path from skills directory - rel_path=${skill_dir#skills/} + rel_path=${skill_dir#./} # Replace slashes with underscores zip_name="skill_${rel_path//\//_}.zip" echo "Zipping $skill_dir to $zip_name" diff --git a/skills/commonstatemachine/.shared/ARCHITECTURE.md b/skills/commonstatemachine/.shared/ARCHITECTURE.md new file mode 100644 index 0000000..3b5362d --- /dev/null +++ b/skills/commonstatemachine/.shared/ARCHITECTURE.md @@ -0,0 +1,32 @@ +# CommonStateMachine Architecture + +![Architecture Diagram](architecture.puml) + +## Core Mechanics +- **Machine State:** A self-contained logic unit. It handles **Gestures (G)** and produces **UI States (U)**. +- **Lifecycle:** + - `doStart()`: Called when state becomes active. Use for initialization or launching side effects. + - `doProcess(gesture)`: Processes user input. + - `doClear()`: Cleanup when leaving the state. +- **Transitions:** States call `setMachineState(nextState)` to move the machine forward. No external "router" is needed. +- **UI Updates:** States call `setUiState(newState)` to update the View. +- **State Machine:** The container for the current **Machine State**. + - It maintains the state transition logic. + - `FlowStateMachine` (standard implementation) exports the current **UI State** as a `StateFlow`. + - It provides the `process(gesture)` entry point for user interaction. +- **Navigation Inversion:** Navigation is a logical state transition within the state machine (Logic/ViewModel layer). The View is passive and follows the logic. +- **ViewModel Integration:** The `ViewModel` (or equivalent lifecycle host) typically: + - Holds the `StateMachine` instance. + - Delegates user gestures to `stateMachine.process(gesture)`. + - Collects and exposes `stateMachine.uiState`. + - Calls `stateMachine.clear()` in its `onCleared()` lifecycle method to clean up resources. + +## Extra Components-Helpers +- **State Factory:** Decouples state creation from the states themselves. This allows for easy dependency injection and makes unit testing transitions simple + by mocking the factory. +- **UI Renderer:** A pure function or class that transforms the internal **Data State** into a **UI State**. This separates the business logic of "what to do" + from the presentation logic of "what to show." +- **State Context:** A common interface passed to all states in a machine. It provides access to shared tools like the State Factory, UI Renderer, + and Feature Hosts, reducing constructor boilerplate. +- **Data State:** A simple data class that holds the cumulative state of the flow (e.g., entered username, password, validation errors). It is passed between + states to maintain continuity. diff --git a/skills/commonstatemachine/.shared/architecture.puml b/skills/commonstatemachine/.shared/architecture.puml new file mode 100644 index 0000000..4d4db8c --- /dev/null +++ b/skills/commonstatemachine/.shared/architecture.puml @@ -0,0 +1,51 @@ +@startuml +'https://plantuml.com/class-diagram + +hide empty members + +interface CommonStateMachine { + + process(gesture: G) + + clear() +} + +abstract class CommonMachineState { + + {abstract} doStart() + + {abstract} doProcess(gesture: G) + + {abstract} doClear() + # setMachineState(next: CommonMachineState) + # setUiState(ui: U) +} + +class FlowStateMachine implements CommonStateMachine { + + uiState: StateFlow +} + +class ViewModel { + - stateMachine: CommonStateMachine + + uiState: StateFlow + + process(gesture: G) +} + +interface StateFactory { + + createNextState(...): CommonMachineState +} + +interface UiRenderer { + + render(data: D): U +} + +interface StateContext { + + factory: StateFactory + + renderer: UiRenderer +} + +class DataState << (D, #FF7700) DataClass >> + +CommonStateMachine "1" *-- "1" CommonMachineState : current +ViewModel "1" *-- "1" CommonStateMachine +CommonMachineState ..> CommonMachineState : transitions +CommonMachineState -right-> StateContext : uses +StateContext "1" *-- "1" StateFactory +StateContext "1" *-- "1" UiRenderer +UiRenderer ..> DataState : renders +@enduml diff --git a/skills/commonstatemachine/common-child-flow/.shared/COMMON-CHILD-FLOW.md b/skills/commonstatemachine/common-child-flow/.shared/COMMON-CHILD-FLOW.md new file mode 100644 index 0000000..e727d51 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/.shared/COMMON-CHILD-FLOW.md @@ -0,0 +1,39 @@ +# Common Child Flow + +![Common Child Flow Diagram](common-child-flow.puml) + +## Philosophy and Principles +- **Modularity via Proxying:** Features are independent modules. A parent flow hosts a child flow using `ProxyMachineState`, which bridges incompatible Gesture/UI systems. +- **Direct Data Flow:** Transitions occur in memory. Complex data or even running coroutines can be passed between states without serialization or "parceling". +- **KMP Ready:** Logic and Data are isolated from the View, making the core feature logic easily shareable across platforms. +- **Standardized Child Flows:** + - `commonflow-data`: Standardizes flow initialization and result handling. + - `commonflow-compose`: Provides a unified way to inject UI into a composition. + - `commonflow-viewmodel`: Bridges the state machine to the platform's Lifecycle and Navigation frameworks. + +## Data API +Data API is defined in the `commonflow-data` module and consists of two primary interfaces: + +### `CommonFlowDataApi` +This is the **Feature Entry Point**. It is defined in the feature module and provides the host with everything needed to launch the child flow. +- **`init(flowHost: CommonFlowHost, input: I)`**: Initializes the child flow and returns the first `MachineState`. It injects the `flowHost` so + the child can signal when it's done. +- **`getDefaultUiState()`**: Provides the initial state (like "Loading") to be displayed while the child machine is starting up. +- **`getBackGesture()`**: Defines which gesture represents "Back" in the feature, allowing the host to bridge system back-presses to the child logic. + +### `CommonFlowHost` +This is the **Communication Bridge**. It is implemented by the parent's `ProxyMachineState` and passed to the child flow. +- **`onComplete(result: R)`**: The child flow calls this method when it reaches a terminal state. The host implementation then receives the `result` and handles + the transition to the next part of the parent flow. + +## UI API +UI API is defined in the `commonflow-compose` module and consists of the following interface: + +### `CommonFlowUiApi` +This interface provides the **Feature View**. It is implemented in the feature module using `Compose` and is used by the host to render the child flow's UI. + +- **`Screen(state: U, onGesture: (G) -> Unit, modifier: Modifier)`**: A `@Composable` function that takes the current feature **UI State (U)** + and a gesture handler for feature **Gestures (G)**. It encapsulates all the UI logic for the feature, allowing the host to simply plug it into its composition. + +This separation ensures that the host doesn't need to know about the internal UI components of the child flow, only how to provide the state and handle +the gestures, which are bridged by the `ProxyMachineState`. diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/common-child-flow.puml b/skills/commonstatemachine/common-child-flow/.shared/common-child-flow.puml similarity index 98% rename from skills/commonstatemachine/common-child-flow/boilerplate/assets/common-child-flow.puml rename to skills/commonstatemachine/common-child-flow/.shared/common-child-flow.puml index 5d2802d..284a449 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/assets/common-child-flow.puml +++ b/skills/commonstatemachine/common-child-flow/.shared/common-child-flow.puml @@ -175,13 +175,13 @@ package Implementation { Data API implementation Injects factory and initializes the flow by calling `init` end note - AuthUiStateImpl -up-|> AuthDataApi + AuthDataApiImpl -up-|> AuthDataApi class AuthUiApiImpl note bottom UI API implementation Builds the view with Compose components inside the module end note - AuthUiStateImpl -up-|> AuthUiApi + AuthUiApiImpl -up-|> AuthUiApi } @enduml \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md b/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md index 541107d..d94f773 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md @@ -17,22 +17,25 @@ metadata: --- ## Description -Use this skill to add a new feature boilerplate to the app based on the [CommonStateMachine](https://github.com/motorro/CommonStateMachine) architecture. -following the [Common Child Flow](https://github.com/motorro/CommonStateMachine/tree/master#common-child-flow-api) design. This design sets a common approach to use proxy state machines for a feature-flow. +Use this skill to add a new feature boilerplate to the app based on the `CommonStateMachine` architecture. +following the `Common Child Flow` design. This design sets a common approach to use proxy state machines for a feature-flow. + +For a deeper dive into the state machine mechanics and common components, refer to the [Core Architecture](references/ARCHITECTURE.md). +For a deeper dive into the common child flow philosophy and design, refer to the [Common Child Flow Architecture](references/COMMON-CHILD-FLOW.md). ## Example -This instruction uses an example authentication module. All examples, schemes and code samples refer +This instruction uses an [example authentication module](assets/example). All examples, schemes and code samples refer to such an example. Adjust the names and packages to your actual need. ## Strategy -You will create a set of gradle modules that together form a feature: +You will create a set of Gradle modules that together form a feature: - The API module: contains the definition of input and output data types for the feature. Example: authentication API that provides the basic interface to the authentication feature. - One or more implementation modules: contains the implementation of the API. Example: login implementation module - implements API with login/password functionality. -Take a look at the [class diagram](assets/common-child-flow.puml). It contains an overview +Take a look at the [class diagram](references/common-child-flow.puml). It contains an overview between all the interfaces, classes and objects in a setup using the Authentication module example. Follow the following common rules when writing code: @@ -115,4 +118,10 @@ otherwise or has given you some special instructions, screenshots, etc. Follow these steps to implement the views: 1. [Implement UI renderer if used](references/ui-renderer-implementation.md) -2. [Implement sample composables](references/ui-view-implementation.md) \ No newline at end of file +2. [Implement sample composables](references/ui-view-implementation.md) + +## Step 6. Create API implementation and DI modules +To be able to use the created implementation, follow this guide to implement: + +- Data and UI API implementations +- DI Modules (optional) diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/ARCHITECTURE.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/ARCHITECTURE.md new file mode 120000 index 0000000..f1d6e62 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/ARCHITECTURE.md @@ -0,0 +1 @@ +../../../.shared/ARCHITECTURE.md \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/COMMON-CHILD-FLOW.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/COMMON-CHILD-FLOW.md new file mode 120000 index 0000000..c83c444 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/COMMON-CHILD-FLOW.md @@ -0,0 +1 @@ +../../.shared/COMMON-CHILD-FLOW.md \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/architecture.puml b/skills/commonstatemachine/common-child-flow/boilerplate/references/architecture.puml new file mode 120000 index 0000000..df09e56 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/architecture.puml @@ -0,0 +1 @@ +../../../.shared/ARCHITECTURE.puml \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/base-state-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/base-state-implementation.md index 6fbf9f1..8e005a8 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/base-state-implementation.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/base-state-implementation.md @@ -1,6 +1,6 @@ # Module implementation: base state Use this reference to implement a common base state for the flow. -Take a look at the [class diagram](../assets/common-child-flow.puml) to get the idea about components purpose. +Take a look at the [class diagram](common-child-flow.puml) to get the idea about components purpose. Take a look at the [example](../assets/example/implementation/state/BaseAuthState.kt). ## Step 1. Create a base class diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/common-child-flow.puml b/skills/commonstatemachine/common-child-flow/boilerplate/references/common-child-flow.puml new file mode 120000 index 0000000..b135e8b --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/common-child-flow.puml @@ -0,0 +1 @@ +../../.shared/common-child-flow.puml \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-api.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-api.md index 1530109..00929f2 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-api.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-api.md @@ -14,11 +14,11 @@ The [Common Child Flow](https://github.com/motorro/CommonStateMachine/tree/maste ## Step 1. Data flow Take the created [Gestures and UI-states](gesture-and-ui-state-api.md), [input and result](data-and-ui-api.md) and create an interface or a typealias to bind the generics of the `com.motorro.commonstatemachine.flow.data.CommonFlowDataApi`. This API would be available to the flow consumer to start the feature flow. -Take a look at the [class diagram](../assets/common-child-flow.puml) for more details. +Take a look at the [class diagram](common-child-flow.puml) for more details. Take a look at the [example](../assets/example/api/AuthDataApi.kt) - data API definition ## Step 2. UI flow Take the created [Gestures and UI-states](gesture-and-ui-state-api.md) and create an interface or a typealias to bind the generics of the `com.motorro.commonstatemachine.flow.compose.CommonFlowUiApi`. This API would be available to the flow consumer to display the feature. -Take a look at the [class diagram](../assets/common-child-flow.puml) for more details. +Take a look at the [class diagram](common-child-flow.puml) for more details. Take a look at the [example](../assets/example/api/AuthUiApi.kt) - UI API definition diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-implementation.md index f111efd..f822c9d 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-implementation.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-implementation.md @@ -1,6 +1,6 @@ # Module implementation: data classes Use this reference to create data classes for the feature implementation. -Take a look at the [class diagram](../assets/common-child-flow.puml) to get the idea about the data types within the module. +Take a look at the [class diagram](common-child-flow.puml) to get the idea about the data types within the module. ## Step 1. UI gestures Create the sealed class to enumerate all the UI events (gestures) the user could send to the model: diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/di-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/di-implementation.md new file mode 100644 index 0000000..19ae9d8 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/di-implementation.md @@ -0,0 +1,44 @@ +# Module implementation: API implementation and DI +Use this reference to: + +- Create a [Data API](data-and-ui-api.md#step-1-data-flow) implementation. +- Create a [UI API](data-and-ui-api.md#step-2-ui-flow) implementation. +- If the project uses a DI framework to provide dependencies, implement DI modules. + +## Step 1. Implement Data API +Create the Data API implementation: + +- Create an internal class and implement the interface of the Data API. Depending on the DI/locator used, pass the state-factory implementation + to constructor or get a reference to it using the DI framework used. +- Override the `init` function. Create the factory, bind the incoming flow-host, create starting state. + Cast the resulting state to the correct API return type. +- Override the `getDefaultUiState` create a function to return the appropriate default [view-state](data-and-ui-implementation.md#step-2-ui-states): + - Loading + - An empty form + - Any state you consider good as a default +- If the backwards [navigation gesture](data-and-ui-implementation.md#step-1-ui-gestures) is there, override the `getBackGesture` function and return it. + +See the [example](../assets/example/implementation/AuthDataApiImpl.kt) for our sample authentication Data API. + +## Step 2. Implement UI API +Create the UI API implementation: + +- Create an internal class and implement the interface of the UI API. +- Override the `Screen` composable using the [component](ui-view-implementation.md#step-1-implement-scaffolding-screen) function. Cast the type of the + UI state to match the one in the UI API + +See the [example](../assets/example/implementation/AuthUiApiImpl.kt) for our sample authentication UI API. + +## Step 3. Define DI module(s) (optional). +Check the project setup or the user input. If the project uses DI to provide dependencies, implement the required modules. +Hints: + +- Make sure all individual state factories are DI-enabled. See the example [StateFactory](../assets/example/implementation/state/PreloadingState.kt). +- Make sure the main state-factory is DI-enabled. See the example [AuthStateFactoryImpl](../assets/example/implementation/state/AuthStateFactoryImpl.kt) +- Make sure the UI-renderer is DI-enabled. See the example [AuthUiRendererImpl](../assets/example/implementation/ui/AuthUiRendererImpl.kt). +- Make sure all interfaces are bound to the respectful implementations. +- Make sure the API implementations from the previous chapters are bound. +- Pay attention to (if used with frameworks like Hilt): + - Data API needs to be bound to the view-model module or similar. + - UI API needs to be bound to the view module (Activity, Fragment, etc.) +- The resulting modules should be public for the consumer. diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/gesture-and-ui-state-api.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/gesture-and-ui-state-api.md index c665d61..69384b6 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/gesture-and-ui-state-api.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/gesture-and-ui-state-api.md @@ -11,13 +11,13 @@ The framework behind is a Model-View-Intent architecture, so: The implementation is hidden from the module consumer using the internal gesture structures. To use the feature we export the basic marker interface that may only contain some common public properties. This interface would be implemented later in the implementation module. -Take a look at the [class diagram](../assets/common-child-flow.puml) for more details. +Take a look at the [class diagram](common-child-flow.puml) for more details. Take a look at the [example](../assets/example/api/AuthGesture.kt) - common API definition ## Step 2. UI-states The implementation is hidden from the module consumer using the internal ui-state structures. To use the feature we export the basic marker interface that may only contain some common public properties. This interface would be implemented later in the implementation module. -Take a look at the [class diagram](../assets/common-child-flow.puml) for more details. +Take a look at the [class diagram](common-child-flow.puml) for more details. Take a look at the [example](../assets/example/api/AuthUiState.kt) - common API definition diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/input-and-result-api.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/input-and-result-api.md index 1f655b7..44a6ce3 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/input-and-result-api.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/input-and-result-api.md @@ -12,11 +12,11 @@ Example: the main application flow could run a child flow in-place to log the us ## Step 1. Input If the user asks to provide any input - create an immutable type - a data class or a sealed structure. If no input type explicitly required - confirm and use `Unit` -Take a look at the [class diagram](../assets/common-child-flow.puml) for more details. +Take a look at the [class diagram](common-child-flow.puml) for more details. Take a look at the [example](../assets/example/api/AuthInput.kt) - common API definition ## Step 2. Result If the user asks to provide a flow result - create an immutable type - a data class or a sealed structure. If no result type explicitly required - confirm and use `Unit` -Take a look at the [class diagram](../assets/common-child-flow.puml) for more details. +Take a look at the [class diagram](common-child-flow.puml) for more details. Take a look at the [example](../assets/example/api/AuthResult.kt) - common API definition diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/renderer-factory-context-interface-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/renderer-factory-context-interface-implementation.md index ee62c7e..e38dcdc 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/renderer-factory-context-interface-implementation.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/renderer-factory-context-interface-implementation.md @@ -5,7 +5,7 @@ Use this reference to create auxiliary interfaces and classes: - UI-state renderer - Interstate context -Take a look at the [class diagram](../assets/common-child-flow.puml) to get the idea about components purpose. +Take a look at the [class diagram](common-child-flow.puml) to get the idea about components purpose. ## Step 1. CommonMachineState binding The framework main building block is a `com.motorro.commonstatemachine.CommonMachineState` - it is a logical state of a state-machine running the flow. diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/state-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/state-implementation.md index b8351a5..bc992e7 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/state-implementation.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/state-implementation.md @@ -1,6 +1,6 @@ # Module implementation: state implementation Use this reference to implement the machine-state(s) for the `CommonStateMachine` framework. -Take a look at the [class diagram](../assets/common-child-flow.puml) to get the idea about components purpose. +Take a look at the [class diagram](common-child-flow.puml) to get the idea about components purpose. Take a look at the [example](../assets/example/implementation/state/PreloadingState.kt). ## Mechanics From 5ab522497183db7ee1a4e91d8cb87fd7aa27c9e5 Mon Sep 17 00:00:00 2001 From: motorro Date: Sun, 20 Sep 2026 12:31:00 +0200 Subject: [PATCH 09/32] Checks skills example Refs: #14 --- .github/workflows/check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 7c2b62f..570b348 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 :examples:di:app:assembleDebug :examples:book:app:assembleDebug :examples:book:book:demo:assembleDebug checkLegacyAbi --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 :examples:skills:auth:demo:assembleDebug checkLegacyAbi --no-daemon --no-configuration-cache - name: Upload problems report uses: actions/upload-artifact@v4 if: failure() From ff6aca822e01a2ef8312f00f031fa5cb95e0143e Mon Sep 17 00:00:00 2001 From: motorro Date: Sun, 20 Sep 2026 12:50:10 +0200 Subject: [PATCH 10/32] Boilerplate skill fixes Refs: #14 --- .../common-child-flow/boilerplate/SKILL.md | 6 +++--- .../references/base-test-class-implementation.md | 2 +- .../boilerplate/references/data-and-ui-api.md | 2 +- .../boilerplate/references/data-and-ui-implementation.md | 2 +- .../renderer-factory-context-interface-implementation.md | 2 +- .../boilerplate/references/state-test-implementation.md | 2 +- .../boilerplate/references/test-fixtures-implementation.md | 2 +- .../boilerplate/references/ui-renderer-implementation.md | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md b/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md index d94f773..2d4d7dd 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md @@ -94,8 +94,8 @@ Then follow these guides for the module implementation: 1. [Create gestures, ui-states, data-state](references/data-and-ui-implementation.md). 2. [Create common-state binding, ui-renderer, state factory and context interfaces](references/renderer-factory-context-interface-implementation.md). 3. [Create basic test fixtures for the data-classes created in the previous state](references/test-fixtures-implementation.md). -4. [Create an abstract base state](references/renderer-factory-context-interface-implementation.md). -5. [Create a base abstract unit-test class](references/renderer-factory-context-interface-implementation.md). +4. [Create an abstract base state](references/base-state-implementation.md). +5. [Create a base abstract unit-test class](references/base-test-class-implementation.md). ## Step 4. Create the machine states For the user to kick-off faster, we will create some machine-states: @@ -121,7 +121,7 @@ Follow these steps to implement the views: 2. [Implement sample composables](references/ui-view-implementation.md) ## Step 6. Create API implementation and DI modules -To be able to use the created implementation, follow this guide to implement: +To be able to use the created implementation, follow [this guide](references/di-implementation.md) to implement: - Data and UI API implementations - DI Modules (optional) diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/base-test-class-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/base-test-class-implementation.md index 3a4d367..35816f9 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/base-test-class-implementation.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/base-test-class-implementation.md @@ -17,7 +17,7 @@ Create the following protected properties to use in child test-classes: - `stateMachine` - `com.motorro.commonstatemachine.CommonStateMachine` mock bound with [implementation gesture and ui-state types](data-and-ui-implementation.md) - `stateFactory` - [machine state factory mock](renderer-factory-context-interface-implementation.md#step-2-state-factory-interface) -- `renderer` - [ui state renderer mock](renderer-factory-context-interface-implementation.md#step-4-ui-renderer-interface) +- `renderer` - [ui state renderer mock](renderer-factory-context-interface-implementation.md#step-3-ui-renderer-interface) - `flowHost` - `com.motorro.commonstatemachine.flow.data.CommonFlowHost` mock bound with the return type - `context` - [interstate context mock](renderer-factory-context-interface-implementation.md#step-4-state-context) - `nextState` - a bound `com.motorro.commonstatemachine.CommonMachineState` type to use with the state-factory mock. diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-api.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-api.md index 00929f2..5f6498f 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-api.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-api.md @@ -12,7 +12,7 @@ The [Common Child Flow](https://github.com/motorro/CommonStateMachine/tree/maste The API implementation is then created in the implementation module and could be injected into the composition using locals or any other way. ## Step 1. Data flow -Take the created [Gestures and UI-states](gesture-and-ui-state-api.md), [input and result](data-and-ui-api.md) and create an interface or a typealias +Take the created [Gestures and UI-states](gesture-and-ui-state-api.md), [input and result](input-and-result-api.md) and create an interface or a typealias to bind the generics of the `com.motorro.commonstatemachine.flow.data.CommonFlowDataApi`. This API would be available to the flow consumer to start the feature flow. Take a look at the [class diagram](common-child-flow.puml) for more details. Take a look at the [example](../assets/example/api/AuthDataApi.kt) - data API definition diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-implementation.md index f822c9d..44d175e 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-implementation.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-implementation.md @@ -17,7 +17,7 @@ Follow these common rules: - Make the created structure internal. - Create data objects for gestures that don't carry any data. Example: user clicks the Back button. -- Create data classes for gestures that carry some data. Example: user changes an input text and the new value is passed with the event.## UI gestures. +- Create data classes for gestures that carry some data. Example: user changes an input text and the new value is passed with the event. - Always include the data object for the backwards navigation gesture: `Back`. - If user didn't give you any information on the input gestures he wants, `Back` would be the only child object in a structure. - If user gives you a picture of a user interface - inspect it and create gestures for every screen. diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/renderer-factory-context-interface-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/renderer-factory-context-interface-implementation.md index e38dcdc..9fe585a 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/renderer-factory-context-interface-implementation.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/renderer-factory-context-interface-implementation.md @@ -36,7 +36,7 @@ Your task here is to: Take a look at the [example](../assets/example/implementation/state/AuthState.kt). -## Step 4. UI renderer interface +## Step 3. UI renderer interface States are encouraged to use a separate renderer interface that translates the [interstate data](data-and-ui-implementation.md#step-3-interstate-data) to the [UI state](data-and-ui-implementation.md#step-2-ui-states). diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/state-test-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/state-test-implementation.md index ca98672..4bc0b15 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/state-test-implementation.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/state-test-implementation.md @@ -38,7 +38,7 @@ Example: the [test](../assets/example/implementation-tests/state/PreloadingError - The state asynchronously loads some data with the use-case at startup. - You need to check the state displays `Loading`. -# Step 2. Testing the asynchronous outcomes +## Step 2. Testing the asynchronous outcomes If the state uses any use-cases or asynchronous operations run at startup, check their outcome: - UI updates if any. diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/test-fixtures-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/test-fixtures-implementation.md index cb9bb44..ecaf1e6 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/test-fixtures-implementation.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/test-fixtures-implementation.md @@ -3,7 +3,7 @@ Use this reference to create test fixtures for the [data classes of the feature 1. Create a `data` sub-package in the test source set. 2. Create a fixture file for data-class gestures. -3. Create some ui-state fixture to use when mocking the [renderer](renderer-factory-context-interface-implementation.md#step-4-ui-renderer-interface). +3. Create some ui-state fixture to use when mocking the [renderer](renderer-factory-context-interface-implementation.md#step-3-ui-renderer-interface). 4. Create a fixture file for data-class interstate data, input and result data classes. For the fixture data, scan the project for already defined fixtures available and prefer using them instead of creating diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/ui-renderer-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/ui-renderer-implementation.md index 7a53cff..4ef14e6 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/ui-renderer-implementation.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/ui-renderer-implementation.md @@ -1,5 +1,5 @@ # Module implementation: renderer implementation -Use this reference to implement the [UI-renderer](renderer-factory-context-interface-implementation.md#step-4-ui-renderer-interface) (if used in the module). +Use this reference to implement the [UI-renderer](renderer-factory-context-interface-implementation.md#step-3-ui-renderer-interface) (if used in the module). ## Step 1. Implement renderer Follow these steps: From 9df6f2ad4252cc22775e4451002d085eeb288689 Mon Sep 17 00:00:00 2001 From: motorro Date: Sun, 20 Sep 2026 13:28:21 +0200 Subject: [PATCH 11/32] Boilerplate skill fixes (take 2) Refs: #14 --- .../commonstatemachine/common-child-flow/boilerplate/SKILL.md | 4 ++-- .../boilerplate/references/base-state-implementation.md | 2 +- .../boilerplate/references/data-and-ui-implementation.md | 2 +- .../boilerplate/references/state-implementation.md | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md b/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md index 2d4d7dd..e1ef42b 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md @@ -100,9 +100,9 @@ Then follow these guides for the module implementation: ## Step 4. Create the machine states For the user to kick-off faster, we will create some machine-states: -- If the user gave you the instructions about what would be the state-flow, or what states does he need - create the states for him. +- If the user gave you the instructions about what would be the state-flow, or what states they need - create the states for them. Implement the required functionality or place TODOs in place of the code if it is not clear what to do exactly. - Don't hesitate to ask the user to clarify what he needs. + Don't hesitate to ask the user to clarify what they need. - If the user didn't give you any specifics, create a classic LCE example: - create a dummy preloading state - create a dummy content state diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/base-state-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/base-state-implementation.md index 8e005a8..a78ef49 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/base-state-implementation.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/base-state-implementation.md @@ -30,7 +30,7 @@ Take a look at the [example](../assets/example/implementation/state/BaseAuthStat ## Step 3. Basic gesture processing The `com.motorro.commonstatemachine.CommonMachineState` processes the user gestures in the `doProcess` method. -The concrete state usually only processes those gestures he is interested in. +The concrete state usually only processes those gestures it is interested in. Example: the login form state is interested only in gestures that are relevant to login actions, it does not process gestures of the logout screen. If the project uses the logging framework, add a simple `doProcess` override to the base class: if the state gets the diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-implementation.md index 44d175e..a784446 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-implementation.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-implementation.md @@ -19,7 +19,7 @@ Follow these common rules: - Create data objects for gestures that don't carry any data. Example: user clicks the Back button. - Create data classes for gestures that carry some data. Example: user changes an input text and the new value is passed with the event. - Always include the data object for the backwards navigation gesture: `Back`. -- If user didn't give you any information on the input gestures he wants, `Back` would be the only child object in a structure. +- If the user didn't give you any information on the input gestures they want, `Back` would be the only child object in a structure. - If user gives you a picture of a user interface - inspect it and create gestures for every screen. - If the feature includes multiple screens - group the specific gestures by screen. - The types of the properties of the ui-gestures should be immutable! diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/state-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/state-implementation.md index bc992e7..29a8173 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/state-implementation.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/state-implementation.md @@ -90,7 +90,7 @@ The typical error state would: - May switch back to the loading state if the error is considered recoverable. - States of that kind are usually rather simple so you don't need any state-factory. -### Example 3. Form state +## Example 3. Form state For example of a state that handles the user input, validates and prepares the data, take a look at the [FormState](../assets/example/implementation/state/FormState.kt) class. The state does the following when started: From 78819ac36f8cf5951917d8ab0ce0e0134cf80027 Mon Sep 17 00:00:00 2001 From: motorro Date: Tue, 22 Sep 2026 13:44:11 +0200 Subject: [PATCH 12/32] Parent factory implementation reference Refs: #14 --- build.gradle.kts | 14 +- examples/skills/app/.gitignore | 1 + examples/skills/app/build.gradle.kts | 131 +++++++++++++ examples/skills/app/proguard-rules.pro | 21 ++ .../skills/app/src/main/AndroidManifest.xml | 26 +++ .../motorro/statemachine/skills/app/App.kt | 30 +++ .../statemachine/skills/app/MainActivity.kt | 23 +++ .../statemachine/skills/app/MainModule.kt | 8 + .../statemachine/skills/app/MainViewModel.kt | 32 +++ .../skills/app/data/MainGesture.kt | 26 +++ .../skills/app/data/MainUiState.kt | 34 ++++ .../skills/app/state/AuthProxyState.kt | 61 ++++++ .../app/state/CheckingAuthentication.kt | 52 +++++ .../skills/app/state/ContentState.kt | 61 ++++++ .../skills/app/state/MainContext.kt | 37 ++++ .../skills/app/state/MainState.kt | 28 +++ .../skills/app/state/MainStateFactory.kt | 57 ++++++ .../statemachine/skills/app/ui/MainScreen.kt | 130 +++++++++++++ .../skills/app/ui/MainUiRenderer.kt | 23 +++ .../drawable-v24/ic_launcher_foreground.xml | 43 ++++ .../res/drawable/ic_launcher_background.xml | 183 ++++++++++++++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 18 ++ .../mipmap-anydpi-v26/ic_launcher_round.xml | 18 ++ .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/xml/backup_rules.xml | 13 ++ .../main/res/xml/data_extraction_rules.xml | 19 ++ .../main/res/xml/network_security_config.xml | 13 ++ .../skills/app/state/BaseStateTest.kt | 61 ++++++ .../app/state/CheckingAuthenticationTest.kt | 104 ++++++++++ examples/skills/appcore/build.gradle.kts | 7 + .../auth/appcore/ui/design/SkillsLoading.kt | 15 ++ examples/skills/auth/api/build.gradle.kts | 7 + .../skills/auth/api/AuthDataApi.kt | 2 +- .../statemachine/skills/auth/api/AuthUiApi.kt | 2 +- .../statemachine/skills/auth/demo/App.kt | 11 +- .../skills/auth/demo/MainActivity.kt | 22 ++- .../auth/implementation/build.gradle.kts | 9 +- .../skills/auth/implementation/AuthModule.kt | 2 +- .../auth/implementation/AuthUiApiImpl.kt | 1 + .../auth/implementation/data/AuthStateData.kt | 2 +- .../state/AuthenticatingState.kt | 5 +- .../implementation/state/PreloadingState.kt | 2 +- .../implementation/ui/AuthLoadingScreen.kt | 8 +- .../auth/implementation/ui/AuthScreen.kt | 49 ++--- .../implementation/data/dataStatesFixtures.kt | 2 +- .../state/AuthenticatingStateTest.kt | 2 +- .../state/PreloadingStateTest.kt | 2 +- examples/skills/domain/build.gradle.kts | 9 +- .../AuthenticateWithPassword.kt | 6 +- .../GetPasswordRequirements.kt | 4 +- .../data}/PasswordRequirements.kt | 2 +- .../skills/domain/friends/CreateFriend.kt | 10 + .../skills/domain/friends/GetFriendList.kt | 10 + .../skills/domain/friends/data/Friend.kt | 3 + .../skills/domain/friends/data/FriendData.kt | 9 + .../skills/domain/session/SessionManager.kt | 33 ++++ .../skills/domain/session/data/Session.kt | 29 +++ .../skills/domain/session/data/Username.kt | 4 + examples/skills/usecase/build.gradle.kts | 61 ++++++ .../skills/usecase/Fixtures.kt | 21 ++ .../skills/usecase/UsecaseModule.kt | 8 + .../AuthenticateWithPasswordImpl.kt | 26 +++ .../GetPasswordRequirementsImpl.kt | 16 ++ .../usecase/session/SessionManagerImpl.kt | 29 +++ settings.gradle.kts | 2 + .../common-child-flow/boilerplate/SKILL.md | 5 + .../boilerplate/assets/example/app | 1 + .../boilerplate/references/data-and-ui-api.md | 12 +- .../references/proxy-implementation.md | 44 +++++ .../references/state-implementation.md | 2 +- 80 files changed, 1689 insertions(+), 77 deletions(-) create mode 100644 examples/skills/app/.gitignore create mode 100644 examples/skills/app/build.gradle.kts create mode 100644 examples/skills/app/proguard-rules.pro create mode 100644 examples/skills/app/src/main/AndroidManifest.xml create mode 100644 examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/App.kt create mode 100644 examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/MainActivity.kt create mode 100644 examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/MainModule.kt create mode 100644 examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/MainViewModel.kt create mode 100644 examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/data/MainGesture.kt create mode 100644 examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/data/MainUiState.kt create mode 100644 examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/state/AuthProxyState.kt create mode 100644 examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/state/CheckingAuthentication.kt create mode 100644 examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/state/ContentState.kt create mode 100644 examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/state/MainContext.kt create mode 100644 examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/state/MainState.kt create mode 100644 examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/state/MainStateFactory.kt create mode 100644 examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/ui/MainScreen.kt create mode 100644 examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/ui/MainUiRenderer.kt create mode 100644 examples/skills/app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 examples/skills/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 examples/skills/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 examples/skills/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 examples/skills/app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 examples/skills/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 examples/skills/app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 examples/skills/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 examples/skills/app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 examples/skills/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 examples/skills/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 examples/skills/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 examples/skills/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 examples/skills/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 examples/skills/app/src/main/res/values/strings.xml create mode 100644 examples/skills/app/src/main/res/xml/backup_rules.xml create mode 100644 examples/skills/app/src/main/res/xml/data_extraction_rules.xml create mode 100644 examples/skills/app/src/main/res/xml/network_security_config.xml create mode 100644 examples/skills/app/src/test/kotlin/com/motorro/statemachine/skills/app/state/BaseStateTest.kt create mode 100644 examples/skills/app/src/test/kotlin/com/motorro/statemachine/skills/app/state/CheckingAuthenticationTest.kt create mode 100644 examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/design/SkillsLoading.kt rename examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/{usecase => authenticate}/AuthenticateWithPassword.kt (50%) rename examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/{usecase => authenticate}/GetPasswordRequirements.kt (54%) rename examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/{entity => authenticate/data}/PasswordRequirements.kt (77%) create mode 100644 examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/friends/CreateFriend.kt create mode 100644 examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/friends/GetFriendList.kt create mode 100644 examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/friends/data/Friend.kt create mode 100644 examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/friends/data/FriendData.kt create mode 100644 examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/session/SessionManager.kt create mode 100644 examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/session/data/Session.kt create mode 100644 examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/session/data/Username.kt create mode 100644 examples/skills/usecase/build.gradle.kts create mode 100644 examples/skills/usecase/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/usecase/Fixtures.kt create mode 100644 examples/skills/usecase/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/usecase/UsecaseModule.kt create mode 100644 examples/skills/usecase/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/usecase/authenticate/AuthenticateWithPasswordImpl.kt create mode 100644 examples/skills/usecase/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/usecase/authenticate/GetPasswordRequirementsImpl.kt create mode 100644 examples/skills/usecase/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/usecase/session/SessionManagerImpl.kt create mode 120000 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/references/proxy-implementation.md diff --git a/build.gradle.kts b/build.gradle.kts index 3625efd..8769240 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,7 +15,7 @@ import groovy.lang.Closure import org.gradle.api.tasks.testing.logging.TestExceptionFormat -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile plugins { alias(libs.plugins.android.app) apply false @@ -62,13 +62,11 @@ allprojects { val projectScm by extra("https://github.com/motorro/CommonStateMachine.git") val projectUrl by extra("https://github.com/motorro/CommonStateMachine") - tasks.withType().configureEach { - compilerOptions { - freeCompilerArgs.addAll(listOf( - "-opt-in=kotlin.RequiresOptIn", - "-Xexpect-actual-classes" - )) - } + tasks.withType>().configureEach { + compilerOptions.freeCompilerArgs.addAll(listOf( + "-opt-in=kotlin.RequiresOptIn", + "-Xexplicit-backing-fields" + )) } tasks.withType().configureEach { diff --git a/examples/skills/app/.gitignore b/examples/skills/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/examples/skills/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/examples/skills/app/build.gradle.kts b/examples/skills/app/build.gradle.kts new file mode 100644 index 0000000..b68665c --- /dev/null +++ b/examples/skills/app/build.gradle.kts @@ -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. + */ + +@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 { + alias(libs.plugins.android.app) + alias(libs.plugins.compose) + alias(libs.plugins.koin) +} + +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.skills.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_21 + targetCompatibility = JavaVersion.VERSION_21 + } + kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_21) + } + } + buildFeatures { + compose = true + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } + namespace = "com.motorro.statemachine.skills.app" +} + +dependencies { + implementation(project(":examples:skills:appcore")) + implementation(project(":examples:skills:domain")) + implementation(project(":examples:skills:usecase")) + implementation(project(":examples:skills:auth:api")) + implementation(project(":examples:skills:auth:implementation")) + implementation(project(":commonstatemachine")) + implementation(project(":coroutines")) + implementation(project(":commonflow:commonflow-data")) + implementation(project(":commonflow:commonflow-compose")) + implementation(project(":commonflow:commonflow-viewmodel")) + + coreLibraryDesugaring(libs.desugaring) + + implementation(libs.napier) + implementation(libs.androidx.core) + implementation(libs.androidx.activity) + 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) + + debugImplementation(libs.compose.tooling) + + implementation(platform(libs.koin.bom)) + implementation(libs.koin.core) + implementation(libs.koin.annotations) + implementation(libs.koin.core.viewmodel) + implementation(libs.koin.android) + implementation(libs.koin.compose) + implementation(libs.koin.compose.viewmodel) + + testImplementation(libs.bundles.test.core) + testImplementation(libs.test.kotlin.coroutines) +} diff --git a/examples/skills/app/proguard-rules.pro b/examples/skills/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/examples/skills/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/skills/app/src/main/AndroidManifest.xml b/examples/skills/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..e347350 --- /dev/null +++ b/examples/skills/app/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/App.kt b/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/App.kt new file mode 100644 index 0000000..30181cd --- /dev/null +++ b/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/App.kt @@ -0,0 +1,30 @@ +package com.motorro.statemachine.skills.app + +import android.app.Application +import com.motorro.commonstatemachine.skills.usecase.UsecaseModule +import com.motorro.statemachine.skills.auth.implementation.AuthModule +import io.github.aakira.napier.DebugAntilog +import io.github.aakira.napier.Napier +import org.koin.android.ext.koin.androidContext +import org.koin.android.ext.koin.androidLogger +import org.koin.core.annotation.KoinApplication +import org.koin.plugin.module.dsl.startKoin + +@KoinApplication(modules = [ + UsecaseModule::class, + AuthModule::class, + MainModule::class +]) +class App : Application() { + override fun onCreate() { + super.onCreate() + + Napier.base(DebugAntilog()) + + startKoin { + allowOverride(false) + androidLogger() + androidContext(this@App) + } + } +} diff --git a/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/MainActivity.kt b/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/MainActivity.kt new file mode 100644 index 0000000..0a543dc --- /dev/null +++ b/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/MainActivity.kt @@ -0,0 +1,23 @@ +package com.motorro.statemachine.skills.app + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import com.motorro.statemachine.auth.appcore.ui.theme.SkillsTheme +import com.motorro.statemachine.skills.app.ui.MainScreen + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + SkillsTheme { + MainScreen { + finish() + } + } + } + } +} + diff --git a/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/MainModule.kt b/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/MainModule.kt new file mode 100644 index 0000000..2375536 --- /dev/null +++ b/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/MainModule.kt @@ -0,0 +1,8 @@ +package com.motorro.statemachine.skills.app + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan("com.motorro.statemachine.skills.app") +internal class MainModule \ No newline at end of file diff --git a/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/MainViewModel.kt b/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/MainViewModel.kt new file mode 100644 index 0000000..a8a30cb --- /dev/null +++ b/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/MainViewModel.kt @@ -0,0 +1,32 @@ +package com.motorro.statemachine.skills.app + +import com.motorro.commonstatemachine.CommonMachineState +import com.motorro.commonstatemachine.flow.data.CommonFlowDataApi +import com.motorro.commonstatemachine.flow.data.CommonFlowHost +import com.motorro.commonstatemachine.flow.viewmodel.CommonFlowViewModel +import com.motorro.statemachine.skills.app.data.MainGesture +import com.motorro.statemachine.skills.app.data.MainUiState +import com.motorro.statemachine.skills.app.state.MainStateFactory +import org.koin.core.annotation.Factory +import org.koin.core.annotation.KoinViewModel +import org.koin.core.component.KoinComponent +import org.koin.core.component.get +import org.koin.core.parameter.parametersOf + +@KoinViewModel +internal class MainViewModel(api: MainApi) : CommonFlowViewModel( + api = api, + init = Unit +) + +@Factory +internal class MainApi : CommonFlowDataApi, KoinComponent { + override fun init(flowHost: CommonFlowHost, input: Unit): CommonMachineState { + val factory = this@MainApi.get { + parametersOf(flowHost) + } + return factory.auth() + } + override fun getDefaultUiState(): MainUiState = MainUiState.Splash + override fun getBackGesture(): MainGesture = MainGesture.Back +} \ No newline at end of file diff --git a/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/data/MainGesture.kt b/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/data/MainGesture.kt new file mode 100644 index 0000000..8a37172 --- /dev/null +++ b/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/data/MainGesture.kt @@ -0,0 +1,26 @@ +/* + * 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.skills.app.data + +import com.motorro.statemachine.skills.auth.api.AuthGesture + +/** + * Main flow gesture + */ +internal sealed class MainGesture { + + object Back : MainGesture() + + data class Auth(val child: AuthGesture) : MainGesture() +} diff --git a/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/data/MainUiState.kt b/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/data/MainUiState.kt new file mode 100644 index 0000000..5daf6b6 --- /dev/null +++ b/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/data/MainUiState.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.skills.app.data + +import com.motorro.statemachine.skills.auth.api.AuthUiState + + +internal sealed class MainUiState { + /** + * Preloading + */ + data object Splash : MainUiState() + + /** + * Authentication + */ + data class Auth(val child: AuthUiState) : MainUiState() + + /** + * Content placeholder + */ + data object Friends : MainUiState() +} \ No newline at end of file diff --git a/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/state/AuthProxyState.kt b/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/state/AuthProxyState.kt new file mode 100644 index 0000000..203d73e --- /dev/null +++ b/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/state/AuthProxyState.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.skills.app.state + +import com.motorro.commonstatemachine.ProxyMachineState +import com.motorro.commonstatemachine.flow.data.CommonFlowHost +import com.motorro.statemachine.skills.app.data.MainGesture +import com.motorro.statemachine.skills.app.data.MainUiState +import com.motorro.statemachine.skills.auth.api.AuthDataApi +import com.motorro.statemachine.skills.auth.api.AuthGesture +import com.motorro.statemachine.skills.auth.api.AuthInput +import com.motorro.statemachine.skills.auth.api.AuthResult +import com.motorro.statemachine.skills.auth.api.AuthUiState +import io.github.aakira.napier.Napier +import org.koin.core.annotation.Factory + +internal class AuthProxyState( + private val context: MainContext, + private val requireAuthentication: Boolean, + private val api: AuthDataApi +) : ProxyMachineState(api.getDefaultUiState()) { + + private val flowHost = CommonFlowHost { result -> + if (result.authenticated) { + Napier.d { "Authenticated. Checking..." } + setMachineState(context.factory.init()) + } else { + Napier.d { "Authentication cancelled. Terminating..." } + setMachineState(context.factory.terminated()) + } + } + + override fun init() = api.init(flowHost, AuthInput(requireAuthentication.not())) + + override fun mapGesture(parent: MainGesture): AuthGesture? = when (parent) { + is MainGesture.Auth -> parent.child + MainGesture.Back -> api.getBackGesture() + } + + override fun mapUiState(child: AuthUiState): MainUiState = MainUiState.Auth(child) + + @Factory + class StateFactory(private val api: AuthDataApi) { + fun create(context: MainContext) = AuthProxyState( + context = context, + requireAuthentication = true, + api = api + ) + } +} \ No newline at end of file diff --git a/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/state/CheckingAuthentication.kt b/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/state/CheckingAuthentication.kt new file mode 100644 index 0000000..1af7c62 --- /dev/null +++ b/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/state/CheckingAuthentication.kt @@ -0,0 +1,52 @@ +package com.motorro.statemachine.skills.app.state + +import com.motorro.commonstatemachine.skills.domain.session.SessionManager +import com.motorro.commonstatemachine.skills.domain.session.data.Session +import com.motorro.statemachine.skills.app.data.MainGesture +import io.github.aakira.napier.Napier +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import org.koin.core.annotation.Factory + +internal class CheckingAuthentication( + context: MainContext, + private val sessionManager: SessionManager +) : BaseMainState(context) { + override fun doStart() { + setUiState(renderer.renderSplash()) + checkAuthentication() + } + + private fun checkAuthentication() = stateScope.launch { + Napier.d { "Checking current session..." } + when(sessionManager.session.catch { emit(Session.None) }.first()) { + is Session.Active -> { + Napier.d { "Authenticated. Transferring to content..." } + setMachineState(factory.content()) + } + Session.None -> { + Napier.d { "No active user. Requiring authentication..." } + setMachineState(factory.auth()) + } + } + } + + override fun doProcess(gesture: MainGesture) { + when(gesture) { + is MainGesture.Back -> { + Napier.d { "Back requested. Terminating..." } + setMachineState(factory.terminated()) + } + else -> super.doProcess(gesture) + } + } + + @Factory + class StateFactory(private val sessionManager: SessionManager) { + fun create(context: MainContext) = CheckingAuthentication( + context = context, + sessionManager = sessionManager + ) + } +} \ No newline at end of file diff --git a/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/state/ContentState.kt b/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/state/ContentState.kt new file mode 100644 index 0000000..4d738de --- /dev/null +++ b/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/state/ContentState.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.skills.app.state + +import com.motorro.commonstatemachine.skills.domain.session.SessionManager +import com.motorro.commonstatemachine.skills.domain.session.data.Session +import com.motorro.statemachine.skills.app.data.MainGesture +import io.github.aakira.napier.Napier +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import org.koin.core.annotation.Factory + +internal class ContentState(context: MainContext, private val sessionManager: SessionManager) : BaseMainState(context) { + override fun doStart() { + subscribeSession() + } + + private fun subscribeSession() = sessionManager + .session + .catch { emit(Session.None) } + .onEach { + when(it) { + is Session.None -> { + Napier.d("No session. Switching to Auth...") + setMachineState(factory.auth()) + } + is Session.Active -> setUiState(renderer.renderContent()) + } + } + .launchIn(stateScope) + + override fun doProcess(gesture: MainGesture) { + when (gesture) { + MainGesture.Back -> { + Napier.d("Back. Terminating...") + setMachineState(factory.terminated()) + } + else -> super.doProcess(gesture) + } + } + + @Factory + class StateFactory(private val sessionManager: SessionManager) { + fun create(context: MainContext) = ContentState( + context, + sessionManager + ) + } +} \ No newline at end of file diff --git a/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/state/MainContext.kt b/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/state/MainContext.kt new file mode 100644 index 0000000..87cd95a --- /dev/null +++ b/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/state/MainContext.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.statemachine.skills.app.state + +import com.motorro.commonstatemachine.flow.data.CommonFlowHost +import com.motorro.statemachine.skills.app.ui.MainUiRenderer + +/** + * Main flow context + */ +internal interface MainContext { + /** + * Main flow host + */ + val flowHost: CommonFlowHost + + /** + * State factory + */ + val factory: MainStateFactory + + /** + * UI renderer + */ + val renderer: MainUiRenderer +} \ No newline at end of file diff --git a/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/state/MainState.kt b/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/state/MainState.kt new file mode 100644 index 0000000..3941439 --- /dev/null +++ b/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/state/MainState.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.skills.app.state + +import com.motorro.commonstatemachine.CommonMachineState +import com.motorro.commonstatemachine.coroutines.CoroutineState +import com.motorro.statemachine.skills.app.data.MainGesture +import com.motorro.statemachine.skills.app.data.MainUiState +import io.github.aakira.napier.Napier + +internal typealias MainState = CommonMachineState + +internal abstract class BaseMainState(context: MainContext) : CoroutineState(), MainContext by context { + override fun doProcess(gesture: MainGesture) { + Napier.w("Unhandled gesture: $gesture") + } +} \ No newline at end of file diff --git a/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/state/MainStateFactory.kt b/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/state/MainStateFactory.kt new file mode 100644 index 0000000..300519b --- /dev/null +++ b/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/state/MainStateFactory.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.skills.app.state + +import com.motorro.commonstatemachine.flow.data.CommonFlowHost +import com.motorro.statemachine.skills.app.ui.MainUiRenderer +import org.koin.core.annotation.Factory +import org.koin.core.annotation.InjectedParam + +/** + * Main state factory + */ +internal interface MainStateFactory { + fun init(): MainState + fun auth(): MainState + fun content(): MainState + fun terminated(): MainState + + @Factory + class Impl( + @InjectedParam flowHost: CommonFlowHost, + renderer: MainUiRenderer, + private val checkingAuthenticationFactory: CheckingAuthentication.StateFactory, + private val contentFactory: ContentState.StateFactory, + private val authFactory: AuthProxyState.StateFactory + ) : MainStateFactory { + + private val context = object : MainContext { + override val flowHost: CommonFlowHost = flowHost + override val factory: MainStateFactory get() = this@Impl + override val renderer: MainUiRenderer = renderer + } + + override fun init() = checkingAuthenticationFactory.create(context) + + override fun auth() = authFactory.create(context) + + override fun content() = contentFactory.create(context) + + override fun terminated() = object : MainState() { + override fun doStart() { + context.flowHost.onComplete(Unit) + } + } + } +} diff --git a/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/ui/MainScreen.kt b/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/ui/MainScreen.kt new file mode 100644 index 0000000..d316e3e --- /dev/null +++ b/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/ui/MainScreen.kt @@ -0,0 +1,130 @@ +/* + * 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.skills.app.ui + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import com.motorro.commonstatemachine.flow.viewmodel.CommonFlowComposition +import com.motorro.statemachine.auth.appcore.ui.design.SkillsAppBar +import com.motorro.statemachine.auth.appcore.ui.design.SkillsLoading +import com.motorro.statemachine.skills.app.MainViewModel +import com.motorro.statemachine.skills.app.R +import com.motorro.statemachine.skills.app.data.MainGesture +import com.motorro.statemachine.skills.app.data.MainUiState +import com.motorro.statemachine.skills.auth.api.AuthGesture +import com.motorro.statemachine.skills.auth.api.AuthUiApi +import io.github.aakira.napier.Napier +import org.koin.compose.koinInject +import org.koin.compose.viewmodel.koinViewModel + +@Composable +internal fun MainScreen(onTerminated: () -> Unit) { + val viewModel: MainViewModel = koinViewModel() + + CommonFlowComposition( + viewModel = viewModel, + navigationBackHandler = { enabled, onBack -> + BackHandler(enabled, onBack) + }, + finish = { + Napier.i { "Finished with: $it" } + onTerminated() + }, + content = { state, onGesture -> + when(state) { + is MainUiState.Splash -> SplashScreen() + is MainUiState.Auth -> { + val onAuthGesture = remember { + { child: AuthGesture -> onGesture(MainGesture.Auth(child)) } + } + AuthScreen(state = state, onGesture = onAuthGesture) + } + MainUiState.Friends -> ContentScreen() + } + } + ) +} + +@Composable +private fun AuthScreen( + state: MainUiState.Auth, + onGesture: (AuthGesture) -> Unit +) { + val uiApi: AuthUiApi = koinInject() + Scaffold( + topBar = { + SkillsAppBar( + title = stringResource(R.string.app_name), + topLevel = true + ) + }, + content = { paddingValues -> + uiApi.Screen( + state = state.child, + onGesture = onGesture, + modifier = Modifier.padding(paddingValues) + ) + } + ) +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +private fun SplashScreen() { + Scaffold( + topBar = { + SkillsAppBar( + title = stringResource(R.string.app_name), + topLevel = true + ) + }, + content = { paddingValues -> + SkillsLoading(Modifier.padding(paddingValues)) + } + ) +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +private fun ContentScreen() { + Scaffold( + topBar = { + SkillsAppBar( + title = stringResource(R.string.app_name), + topLevel = true + ) + }, + content = { paddingValues -> + Box(modifier = Modifier.padding(paddingValues).fillMaxSize(), contentAlignment = Alignment.Center) { + Text( + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + text = "Friends placeholder" + ) + } + } + ) +} diff --git a/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/ui/MainUiRenderer.kt b/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/ui/MainUiRenderer.kt new file mode 100644 index 0000000..8a90ace --- /dev/null +++ b/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/ui/MainUiRenderer.kt @@ -0,0 +1,23 @@ +package com.motorro.statemachine.skills.app.ui + +import com.motorro.statemachine.skills.app.data.MainUiState +import com.motorro.statemachine.skills.auth.api.AuthUiState +import org.koin.core.annotation.Factory + +/** + * Main UI renderer + */ +internal interface MainUiRenderer { + fun renderSplash(): MainUiState + fun renderAuth(child: AuthUiState): MainUiState + fun renderContent(): MainUiState + + @Factory + class Impl : MainUiRenderer { + override fun renderSplash() = MainUiState.Splash + + override fun renderAuth(child: AuthUiState) = MainUiState.Auth(child) + + override fun renderContent(): MainUiState = MainUiState.Friends + } +} \ No newline at end of file diff --git a/examples/skills/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/examples/skills/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..40ce517 --- /dev/null +++ b/examples/skills/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/skills/app/src/main/res/drawable/ic_launcher_background.xml b/examples/skills/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..997ef0b --- /dev/null +++ b/examples/skills/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/skills/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/examples/skills/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..0147ca8 --- /dev/null +++ b/examples/skills/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/examples/skills/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/examples/skills/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..0147ca8 --- /dev/null +++ b/examples/skills/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/skills/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/examples/skills/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..c209e78ecd372343283f4157dcfd918ec5165bb3 GIT binary patch literal 1404 zcmV-?1%vuhNk&F=1pok7MM6+kP&il$0000G0000-002h-06|PpNX!5L00Dqw+t%{r zzW2vH!KF=w&cMnnN@{whkTw+#mAh0SV?YL=)3MimFYCWp#fpdtz~8$hD5VPuQgtcN zXl<@<#Cme5f5yr2h%@8TWh?)bSK`O z^Z@d={gn7J{iyxL_y_%J|L>ep{dUxUP8a{byupH&!UNR*OutO~0{*T4q5R6@ApLF! z5{w?Z150gC7#>(VHFJZ-^6O@PYp{t!jH(_Z*nzTK4 zkc{fLE4Q3|mA2`CWQ3{8;gxGizgM!zccbdQoOLZc8hThi-IhN90RFT|zlxh3Ty&VG z?Fe{#9RrRnxzsu|Lg2ddugg7k%>0JeD+{XZ7>Z~{=|M+sh1MF7~ zz>To~`~LVQe1nNoR-gEzkpe{Ak^7{{ZBk2i_<+`Bq<^GB!RYG+z)h;Y3+<{zlMUYd zrd*W4w&jZ0%kBuDZ1EW&KLpyR7r2=}fF2%0VwHM4pUs}ZI2egi#DRMYZPek*^H9YK zay4Iy3WXFG(F14xYsoDA|KXgGc5%2DhmQ1gFCkrgHBm!lXG8I5h*uf{rn48Z!_@ z4Bk6TJAB2CKYqPjiX&mWoW>OPFGd$wqroa($ne7EUK;#3VYkXaew%Kh^3OrMhtjYN?XEoY`tRPQsAkH-DSL^QqyN0>^ zmC>{#F14jz4GeW{pJoRpLFa_*GI{?T93^rX7SPQgT@LbLqpNA}<@2wH;q493)G=1Y z#-sCiRNX~qf3KgiFzB3I>4Z%AfS(3$`-aMIBU+6?gbgDb!)L~A)je+;fR0jWLL-Fu z4)P{c7{B4Hp91&%??2$v9iRSFnuckHUm}or9seH6 z>%NbT+5*@L5(I9j@06@(!{ZI?U0=pKn8uwIg&L{JV14+8s2hnvbRrU|hZCd}IJu7*;;ECgO%8_*W Kmw_-CKmY()leWbG literal 0 HcmV?d00001 diff --git a/examples/skills/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/examples/skills/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..b2dfe3d1ba5cf3ee31b3ecc1ced89044a1f3b7a9 GIT binary patch literal 2898 zcmV-Y3$650Nk&FW3jhFDMM6+kP&il$0000G0000-002h-06|PpNWB9900E$G+qN-D z+81ABX7q?;bwx%xBg?kcwr$(C-Tex-ZCkHUw(Y9#+`E5-zuONG5fgw~E2WDng@Bc@ z24xy+R1n%~6xI#u9vJ8zREI)sb<&Il(016}Z~V1n^PU3-_H17A*Bf^o)&{_uBv}Py zulRfeE8g(g6HFhk_?o_;0@tz?1I+l+Y#Q*;RVC?(ud`_cU-~n|AX-b`JHrOIqn(-t&rOg-o`#C zh0LPxmbOAEb;zHTu!R3LDh1QO zZTf-|lJNUxi-PpcbRjw3n~n-pG;$+dIF6eqM5+L();B2O2tQ~|p{PlpNcvDbd1l%c zLtXn%lu(3!aNK!V#+HNn_D3lp z2%l+hK-nsj|Bi9;V*WIcQRTt5j90A<=am+cc`J zTYIN|PsYAhJ|=&h*4wI4ebv-C=Be#u>}%m;a{IGmJDU`0snWS&$9zdrT(z8#{OZ_Y zxwJx!ZClUi%YJjD6Xz@OP8{ieyJB=tn?>zaI-4JN;rr`JQbb%y5h2O-?_V@7pG_+y z(lqAsqYr!NyVb0C^|uclHaeecG)Sz;WV?rtoqOdAAN{j%?Uo%owya(F&qps@Id|Of zo@~Y-(YmfB+chv^%*3g4k3R0WqvuYUIA+8^SGJ{2Bl$X&X&v02>+0$4?di(34{pt* zG=f#yMs@Y|b&=HyH3k4yP&goF2LJ#tBLJNNDo6lG06r}ghC-pC4Q*=x3;|+W04zte zAl>l4kzUBQFYF(E`KJy?ZXd1tnfbH+Z~SMmA21KokJNs#eqcXWKUIC>{TuoKe^vhF z);H)o`t9j~`$h1D`#bxe@E`oE`cM9w(@)5Bp8BNukIwM>wZHfd0S;5bcXA*5KT3bj zc&_~`&{z7u{Et!Z_k78H75gXf4g8<_ul!H$eVspPeU3j&&Au=2R*Zp#M9$9s;fqwgzfiX=E_?BwVcfx3tG9Q-+<5fw z%Hs64z)@Q*%s3_Xd5>S4dg$s>@rN^ixeVj*tqu3ZV)biDcFf&l?lGwsa zWj3rvK}?43c{IruV2L`hUU0t^MemAn3U~x3$4mFDxj=Byowu^Q+#wKRPrWywLjIAp z9*n}eQ9-gZmnd9Y0WHtwi2sn6n~?i#n9VN1B*074_VbZZ=WrpkMYr{RsI ztM_8X1)J*DZejxkjOTRJ&a*lrvMKBQURNP#K)a5wIitfu(CFYV4FT?LUB$jVwJSZz zNBFTWg->Yk0j&h3e*a5>B=-xM7dE`IuOQna!u$OoxLlE;WdrNlN)1 z7**de7-hZ!(%_ZllHBLg`Ir#|t>2$*xVOZ-ADZKTN?{(NUeLU9GbuG-+Axf*AZ-P1 z0ZZ*fx+ck4{XtFsbcc%GRStht@q!m*ImssGwuK+P@%gEK!f5dHymg<9nSCXsB6 zQ*{<`%^bxB($Z@5286^-A(tR;r+p7B%^%$N5h%lb*Vlz-?DL9x;!j<5>~kmXP$E}m zQV|7uv4SwFs0jUervsxVUm>&9Y3DBIzc1XW|CUZrUdb<&{@D5yuLe%Xniw^x&{A2s z0q1+owDSfc3Gs?ht;3jw49c#mmrViUfX-yvc_B*wY|Lo7; zGh!t2R#BHx{1wFXReX*~`NS-LpSX z#TV*miO^~B9PF%O0huw!1Zv>^d0G3$^8dsC6VI!$oKDKiXdJt{mGkyA`+Gwd4D-^1qtNTUK)`N*=NTG-6}=5k6suNfdLt*dt8D| z%H#$k)z#ZRcf|zDWB|pn<3+7Nz>?WW9WdkO5(a^m+D4WRJ9{wc>Y}IN)2Kbgn;_O? zGqdr&9~|$Y0tP=N(k7^Eu;iO*w+f%W`20BNo)=Xa@M_)+o$4LXJyiw{F?a633SC{B zl~9FH%?^Rm*LVz`lkULs)%idDX^O)SxQol(3jDRyBVR!7d`;ar+D7do)jQ}m`g$TevUD5@?*P8)voa?kEe@_hl{_h8j&5eB-5FrYW&*FHVt$ z$kRF9Nstj%KRzpjdd_9wO=4zO8ritN*NPk_9avYrsF(!4))tm{Ga#OY z(r{0buexOzu7+rw8E08Gxd`LTOID{*AC1m*6Nw@osfB%0oBF5sf<~wH1kL;sd zo)k6^VyRFU`)dt*iX^9&QtWbo6yE8XXH?`ztvpiOLgI3R+=MOBQ9=rMVgi<*CU%+d1PQQ0a1U=&b0vkF207%xU0ssI2 literal 0 HcmV?d00001 diff --git a/examples/skills/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/examples/skills/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..4f0f1d64e58ba64d180ce43ee13bf9a17835fbca GIT binary patch literal 982 zcmV;{11bDcNk&G_0{{S5MM6+kP&il$0000G0000l001ul06|PpNU8t;00Dqo+t#w^ z^1csucXz7-Qrhzl9HuHB%l>&>1tG2^vb*E&k^T3$FG1eQZ51g$uv4V+kI`0<^1Z@N zk?Jjh$olyC%l>)Xq;7!>{iBj&BjJ`P&$fsCfpve_epJOBkTF?nu-B7D!hO=2ZR}

C%4 zc_9eOXvPbC4kzU8YowIA8cW~Uv|eB&yYwAObSwL2vY~UYI7NXPvf3b+c^?wcs~_t{ ze_m66-0)^{JdOMKPwjpQ@Sna!*?$wTZ~su*tNv7o!gXT!GRgivP}ec?5>l1!7<(rT zds|8x(qGc673zrvYIz;J23FG{9nHMnAuP}NpAED^laz3mAN1sy+NXK)!6v1FxQ;lh zOBLA>$~P3r4b*NcqR;y6pwyhZ3_PiDb|%n1gGjl3ZU}ujInlP{eks-#oA6>rh&g+!f`hv#_%JrgYPu z(U^&XLW^QX7F9Z*SRPpQl{B%x)_AMp^}_v~?j7 zapvHMKxSf*Mtyx8I}-<*UGn3)oHd(nn=)BZ`d$lDBwq_GL($_TPaS{UeevT(AJ`p0 z9%+hQb6z)U9qjbuXjg|dExCLjpS8$VKQ55VsIC%@{N5t{NsW)=hNGI`J=x97_kbz@ E0Of=7!TQj4N+cqN`nQhxvX7dAV-`K|Ub$-q+H-5I?Tx0g9jWxd@A|?POE8`3b8fO$T))xP* z(X?&brZw({`)WU&rdAs1iTa0x6F@PIxJ&&L|dpySV!ID|iUhjCcKz(@mE z!x@~W#3H<)4Ae(4eQJRk`Iz3<1)6^m)0b_4_TRZ+cz#eD3f8V;2r-1fE!F}W zEi0MEkTTx}8i1{`l_6vo0(Vuh0HD$I4SjZ=?^?k82R51bC)2D_{y8mi_?X^=U?2|F{Vr7s!k(AZC$O#ZMyavHhlQ7 zUR~QXuH~#o#>(b$u4?s~HLF*3IcF7023AlwAYudn0FV~|odGH^05AYPEfR)8p`i{n zwg3zPVp{+wOsxKc>)(pMupKF!Y2HoUqQ3|Yu|8lwR=?5zZuhG6J?H`bSNk_wPoM{u zSL{c@pY7+c2kck>`^q1^^gR0QB7Y?KUD{vz-uVX~;V-rW)PDcI)$_UjgVV?S?=oLR zf4}zz{#*R_{LkiJ#0RdQLNC^2Vp%JPEUvG9ra2BVZ92(p9h7Ka@!yf9(lj#}>+|u* z;^_?KWdzkM`6gqPo9;;r6&JEa)}R3X{(CWv?NvgLeOTq$cZXqf7|sPImi-7cS8DCN zGf;DVt3Am`>hH3{4-WzH43Ftx)SofNe^-#|0HdCo<+8Qs!}TZP{HH8~z5n`ExcHuT zDL1m&|DVpIy=xsLO>8k92HcmfSKhflQ0H~9=^-{#!I1g(;+44xw~=* zxvNz35vfsQE)@)Zsp*6_GjYD};Squ83<_?^SbALb{a`j<0Gn%6JY!zhp=Fg}Ga2|8 z52e1WU%^L1}15Ex0fF$e@eCT(()_P zvV?CA%#Sy08_U6VPt4EtmVQraWJX` zh=N|WQ>LgrvF~R&qOfB$!%D3cGv?;Xh_z$z7k&s4N)$WYf*k=|*jCEkO19{h_(%W4 zPuOqbCw`SeAX*R}UUsbVsgtuG?xs(#Ikx9`JZoQFz0n*7ZG@Fv@kZk`gzO$HoA9kN z8U5{-yY zvV{`&WKU2$mZeoBmiJrEdzUZAv1sRxpePdg1)F*X^Y)zp^Y*R;;z~vOv-z&)&G)JQ{m!C9cmziu1^nHA z`#`0c>@PnQ9CJKgC5NjJD8HM3|KC(g5nnCq$n0Gsu_DXk36@ql%npEye|?%RmG)

FJ$wK}0tWNB{uH;AM~i literal 0 HcmV?d00001 diff --git a/examples/skills/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/examples/skills/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..948a3070fe34c611c42c0d3ad3013a0dce358be0 GIT binary patch literal 1900 zcmV-y2b1_xNk&Fw2LJ$9MM6+kP&il$0000G0001A003VA06|PpNH75a00DqwTbm-~ zullQTcXxO9ki!OCRx^i?oR|n!<8G0=kI^!JSjFi-LL*`V;ET0H2IXfU0*i>o6o6Gy zRq6Ap5(_{XLdXcL-MzlN`ugSdZY_`jXhcENAu)N_0?GhF))9R;E`!bo9p?g?SRgw_ zEXHhFG$0{qYOqhdX<(wE4N@es3VIo$%il%6xP9gjiBri+2pI6aY4 zJbgh-Ud|V%3O!IcHKQx1FQH(_*TK;1>FQWbt^$K1zNn^cczkBs=QHCYZ8b&l!UV{K z{L0$KCf_&KR^}&2Fe|L&?1I7~pBENnCtCuH3sjcx6$c zwqkNkru);ie``q+_QI;IYLD9OV0ZxkuyBz|5<$1BH|vtey$> z5oto4=l-R-Aaq`Dk0}o9N0VrkqW_#;!u{!bJLDq%0092{Ghe=F;(kn} z+sQ@1=UlX30+2nWjkL$B^b!H2^QYO@iFc0{(-~yXj2TWz?VG{v`Jg zg}WyYnwGgn>{HFaG7E~pt=)sOO}*yd(UU-D(E&x{xKEl6OcU?pl)K%#U$dn1mDF19 zSw@l8G!GNFB3c3VVK0?uyqN&utT-D5%NM4g-3@Sii9tSXKtwce~uF zS&Jn746EW^wV~8zdQ1XC28~kXu8+Yo9p!<8h&(Q({J*4DBglPdpe4M_mD8AguZFn~ ztiuO~{6Bx?SfO~_ZV(GIboeR9~hAym{{fV|VM=77MxDrbW6`ujX z<3HF(>Zr;#*uCvC*bpoSr~C$h?_%nXps@A)=l_;({Fo#6Y1+Zv`!T5HB+)#^-Ud_; zBwftPN=d8Vx)*O1Mj+0oO=mZ+NVH*ptNDC-&zZ7Hwho6UQ#l-yNvc0Cm+2$$6YUk2D2t#vdZX-u3>-Be1u9gtTBiMB^xwWQ_rgvGpZ6(C@e23c!^K=>ai-Rqu zhqT`ZQof;9Bu!AD(i^PCbYV%yha9zuoKMp`U^z;3!+&d@Hud&_iy!O-$b9ZLcSRh? z)R|826w}TU!J#X6P%@Zh=La$I6zXa#h!B;{qfug}O%z@K{EZECu6zl)7CiNi%xti0 zB{OKfAj83~iJvmpTU|&q1^?^cIMn2RQ?jeSB95l}{DrEPTW{_gmU_pqTc)h@4T>~& zluq3)GM=xa(#^VU5}@FNqpc$?#SbVsX!~RH*5p0p@w z;~v{QMX0^bFT1!cXGM8K9FP+=9~-d~#TK#ZE{4umGT=;dfvWi?rYj;^l_Zxywze`W z^Cr{55U@*BalS}K%Czii_80e0#0#Zkhlij4-~I@}`-JFJ7$5{>LnoJSs??J8kWVl6|8A}RCGAu9^rAsfCE=2}tHwl93t0C?#+jMpvr7O3`2=tr{Hg$=HlnjVG^ewm|Js0J*kfPa6*GhtB>`fN!m#9J(sU!?(OSfzY*zS(FJ<-Vb zfAIg+`U)YaXv#sY(c--|X zEB+TVyZ%Ie4L$gi#Fc++`h6%vzsS$pjz9aLt+ZL(g;n$Dzy5=m=_TV(3H8^C{r0xd zp#a%}ht55dOq?yhwYPrtp-m1xXp;4X;)NhxxUpgP%XTLmO zcjaFva^}dP3$&sfFTIR_jC=2pHh9kpI@2(6V*GQo7Ws)`j)hd+tr@P~gR*2gO@+1? zG<`_tB+LJuF|SZ9tIec;h%}}6WClT`L>HSW?E{Hp1h^+mlbf_$9zA>!ug>NALJsO{ mU%z=YwVD?}XMya)Bp;vlyE5&E_6!fzx9pwrdz474!~g(M6R?N? literal 0 HcmV?d00001 diff --git a/examples/skills/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/examples/skills/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..1b9a6956b3acdc11f40ce2bb3f6efbd845cc243f GIT binary patch literal 3918 zcmV-U53%r4Nk&FS4*&pHMM6+kP&il$0000G0001A003VA06|PpNSy@$00HoY|G(*G z+qV7x14$dSO^Re!iqt-AAIE9iwr$(CZQJL$blA4B`>;C3fBY6Q8_YSjb2%a=fc}4E zrSzssacq<^nmW|Rs93PJni30R<8w<(bK_$LO4L?!_OxLl$}K$MUEllnMK|rg=f3;y z*?;3j|Nh>)p0JQ3A~rf(MibH2r+)3cyV1qF&;8m{w-S*y+0mM){KTK^M5}ksc`qX3 zy>rf^b>~l>SSHds8(I@hz3&PD@LmEs4&prkT=BjsBCXTMhN$_)+kvnl0bLKW5rEsj z*d#KXGDB4P&>etx0X+`R19yC=LS)j!mgs5M0L~+o-T~Jl!p!AJxnGAhV%~rhYUL4hlWhgES3Kb5oA&X z{}?3OBSS-{!v$nCIGj->(-TAG)8LR{htr41^gxsT8yqt2@DEG6Yl`Uma3Nd4;YUoW zTbkYl3CMU5ypMF3EIkYmWL|*BknM`0+Kq6CpvO(y$#j94e+q{vI{Zp8cV_6RK!`&C zob$*5Q|$IZ09dW=L!V zw@#2wviu|<#3lgGE8GEhcx+zBt`} zOwP8j9X%^f7i_bth4PiJ$LYtFJSCN$3xwDN;8mr*B;CJwBP2G0TMq0uNt7S^DO_wE zepk!Wrn#Z#03j{`c*Rf~y3o7?J}w?tEELRUR2cgxB*Y{LzA#pxHgf}q?u5idu>077 zd^=p)`nA}6e`|@`p?u}YU66PP_MA}Zqqe!c{nK&z%Jwq1N4e_q<#4g^xaz=ao;u|6 zwpRcW2Lax=ZGbx=Q*HhlJ`Ns#Y*r0*%!T?P*TTiX;rb)$CGLz=rSUum$)3Qyv{BL2 zO*=OI2|%(Yz~`pNEOnLp>+?T@glq-DujlIp?hdJeZ7ctP4_OKx|5@EOps3rr(pWzg zK4d3&oN-X2qN(d_MkfwB4I)_)!I_6nj2iA9u^pQ{;GckGLxBGrJUM2Wdda!k)Y>lq zmjws>dVQ*vW9lvEMkiN3wE-__6OWD0txS&Qn0n22cyj4Q*8(nG4!G{6OOwNvsrPIL zCl-$W9UwkEUVuLwyD%|inbOF*xMODZ4VMEVAq_zUxZ+K#Gdqf!DW$5f)?7UNOFMz! zrB~tuu=6X2FE(p^iqgxr+?ZK;=yz`e;C$#_@D9Lj-+TDVOrva>(#*PVbaHO>A)mhl z07OJWCqYC60518$!&c`eNBcBW%GnfaQ*$eazV^2_AW?j)h;J1nUjN(I9=0+!RVx~% z3@Tf!P0TE+98jA?WceK-}A1% zW!K)lyKcGqy#M~})315-A#2NXQ`?6NR#Apo=S!oF=JfpX>iR*49ec{7AN$xxpK{D$ z2d%Fz&rdfSqourN$~Y^NFIMV1CZ?J*bMx~H3k&meGtH@q9ra2vZxmA$S(#jaaj-g4 ztJmxG+DLV<*q<|sDXPp$X>E)#S}Vm&sRaO5P&goh2><}FEdZSXDqsL$06sAkh(e+v zAsBhKSRexgwg6tIy~GFJzaTxXD(}|+0eOwFDA%rn`X;MVwDHT9=4=g%OaJ9s%3b9>9EUTnnp0t;2Zpa{*>mk~hZqItE_!dQ zOtC>8`$l|mV43Jbudf0N6&&X;{=z}Zi}d1`2qmJ}i|0*GsulD3>GgQXHN)pkR6sf1 z?5ZU%&xtL}oH;YiAA)d*^Ndw2T$+Mjuzyzz@-SM`9df7LqTxLuIwC~S0092~+=qYv z@*ja;?Wt!T!{U?c*Z0YtGe)XbI&y-?B&G2$`JDM)(dIV9G`Sc#6?sI60de6kv+)Qb zUW~2|WjvJq3TA8`0+sWA3zRhY9a~ow)O~&StBkG2{*{TGiY~S8ep{V&Vo2l<6LWsu z^#p0-v*t2?3&aA1)ozu|%efSR=XnpX$lvTeRdKlvM!@|pM5p2w3u-6 zU>}t2xiYLS+{|%C65AzX+23Mtlq?BS&YdYcYsVjoiE&rT>;Necn6l^K)T^lmE`5u{ zm1i+-a-gc;Z&v-{;8r)z6NYfBUv+=_L}ef}qa9FX01)+Aaf+;xj(mL6|JUzGJR1|fnanb%?BPPIp>SCjP|8qE5qJ{=n5ZGw?81z3(k;pzH%1CtlX50{E7h)$h{qGKfzC`e2o`*IqA#tjA z`Fz&^%$b9F*N`)U-#6>a)Z`55`$Dd0cfcs0$d13^ONrdCu9xcv_=n#WQo8stcz3jP9|2EvdI-RhJM3%Q%oM&!OlShM|0 z?gz?wHZSnm45njLtsz8PVT1S&jAlbKg5kVam$p16=EK@Sj4EP0OtH zmJDmdc^v)x>56Qg_wmYHz6h)>kl_h$>0@J!ypv%APmjZTAQVLy6Fu50RGY&JAVNhx zrF_qG6`x9MkT;1SFWo$)l{M$;3qUDn9JwE}z zRl#E_bDRJFii61kPgBybIgp8dNW!Cc1b*^YYk-#oWLJvtM_v^hQx~9?8LD4VFFxBF z3MlrsSC%f9Oupn*ctPL0U1fwfX?`tRhPD{PSLFPQOmIt$mDy0SgpNVvHS+f#Do>h1Gn?LZU9(KaN>Q_=Y*_T zvtD7%_u^^+{g`0VGzg(VZrpVQ6Ub5M=tI_p7T93R8@3Zulu3|#{iNcu!oiHxZ4Rf*( zfmiN$$ru(*_Zqn=`Gq#OuHRTSwp7uH_SokR&|)RuW5yo=Z|_4?qU-JU+tpt>!B&Is z@N(=SG;bpVc;AO@zbmMM zScqq1)b-ZQIrs={oD}|?6y{$HNB1U0^LsBh8JI&3!GBZxOXI<}&5-$lgkAaYqhOTb z?2vEnZ$-kk;*M_17(upJF3%+iH*s0-r{vttXVB2OUwI1s^+G(Ft(U8gYFXC}#P&E^ z>T@C^tS`Z7{6HT4_nF~n>JlZtk5&qDBl6r|^kzQYe`wq!C)n@$c>WOPA61NDFj<<6 zGW71NMMhwAl!U-yqrq2xrSFqRCI8acw7?}3j;ynxo*-b7Co;g5r%^j=H@9({PXXBf z@r>U>>N;E)81wx`B4f%{PB~MHka_);%kBCb(d|Jy5!MqJ%2p`t&@L)4$T2j&-WHvG zv3(uyA_gwqNu(k?jQTtv3dgPKRZoH8prxe7>pQBW5L&dpumS&5Ld2?(sCpJjvc4L5 zEnh&?91WVm)ZdTj=fjJ$pPDdgAttLXuke+?KdKxu*;kTC(r!tQk6;gxj4h%FdHAt(^M3YvYj(!tOeN)+Hvj6+< zzyJRG?^lZfWuR#t!tUKP&(?%3v&Zd$R2YN>lB(Lq`OInY48%4%yTv2 zYe1{G`3)(PDEio5Y@-I5tUf`c%%OCJMtSW56g3iEg%3`$7XSJJHyA z<|7&N)5Xrlgv~%BO24eFd;Hd;uiK%D`EdK|quUeRZDqbh9l)%j%J#0lfrZumvA<_w zu&=AVvdChf6}eqh(bUz`(`Ue*p01{fBAcTgKyDYLs_I+YyJEk+rM@avU~>fB$n)HS zM7pfJydu`i%gfS<{PF94kZDv$t>06sAkheDzu40NJ$5CMW%n^Lls?8^p^QGWURbKu3ZduZQZ((s2? zzE`}<{;Zt7<$C|9R8A~DJ~@%x>TfP zF>TX8)@v|t)q4GjRt<}5s6hLHwRel7>V@&r-O|Av(yh;Q1A{E>Ir>p+%dHD|=l+lT zpr(Dg&>#Nu=!)6bCLr-ZS%|;h)Ij$+e@r8_{qO19QvDe=&1tmpY*0lcA^Cc-#{9fQ z<~$*<&P$Q<_jy#<$40PMofM7aQ}C=jphI`4kLg}Z7CIN#26D{-4v-_CA-LiE@(%{y!BzsU%gG`Q?sjLUf%qFSl0y)2#ae*+EI>s|i`d^V$Dn)qmzqRq6VJRY|{4ujsIU%#bnqU6MR&-1I_43=|5(6Jr;Jvert) zE?S|Tmn}Tv<-??sxV5@9t}3D=>YZ0JrQe$CO~|EY=Lj9RM&4svQHPQL6%pV5fPFiH zfXDx;l@~et{*{U*#c#Dvzu)|znDO7$#CRx)Z&yp-}SrD{&|(MQtfUz~n35@RLfUy=aqrhCX0M}J_r5QsK~NmRCR|Nm&L z41UdsLjWxSUlL41r^0K&nCCK>fdR-!MYjFg(z9_mF^C|#ZQw?`)f6uVzF^`bRnVY& zo}@M06J&_+>w9@jpaO4snmU;0t-(zYW1qVBHtuD!d?%?AtN7Plp><-1Y8Rqb20ZaP zTCgn*-Sri4Q8Xn>=gNaWQ57%!D35UkA@ksOlPB*Dvw}t02ENAqw|kFhn%ZyyW%+t{ zNdM!uqEM^;2}f+tECHbwLmH*!nZVrb$-az%t50Y2pg(HqhvY-^-lb}>^6l{$jOI6} zo_kBzj%8aX|6H5M0Y<)7pzz_wLkIpRm!;PzY)9+24wk2&TT{w--phDGDCOz{cN_ca zpnm7`$oDy=HX%0i-`769*0M6(e5j-?(?24%)<)&46y0e&6@HCDZAm9W6Ib#Y#BF6- z=30crHGg+RRTe%VBC>T00OV6F+gQDAK38Ne3N9bm|62tPccBJi)5{B z4zc^Db72XiBd}v$CF|yU{Z=M|DZ%-(XarYNclODlb1Kz1_EKLy(NSLCN`eUl(rBCL zT*jx@wNvze0|TSqgE(QArOZU)_?qH(sj#TwzElLs9q)(0u!_P|R%Cy_0JFQxgGV>1 zz4?_uq<8_gM0`c*Hh|;UMz~vrg1gQXp{ufg`hM_qU;U>+zmvc5blCLSq@PrEBSGR# z&8=2Z4uXN`F3p73ueD1l{s{k$WipAvSh5W7ABe?4)t;r@V?y`bNB5FvBuE|0VRTb< zM1Hn^?DSsJY+sX@T5xW=#>T9VEV|?<(=6|ge$X6Sb05!LFdjDcoq*gM(Zq=t;_)Le&jyt(&9jzR73noru`a# zN*<`KwGa^gZU3-)MSLF0aFag#f0<>E(bYTeHmtdbns#|I)-$)mJ`q9ctQ8g0=ET?| zdO}eZ*b_p>ygRTtR^5Ggdam=Zb5wmd{}np+Jn1d_=M`~P=M67jj})fH4ztb5yQqQW z^C|C&^LHAK-u+ooIK)yM)QM?t;|<{P;;{`p=BclzAN#JzL4jCwXkQB1Dy{=^KR`=~ zTrr)y7eiYBzSNs_DvO=4A6#EgGS-zY%Vi)N*Yb`U;6o}KR}dq{r9pT5wqZ@3NOE8- z9-(}D|Nc5732CSYQbL)!gPQ#RbD8BhK3dl{sUuPvei0tkvnJBxDEAYTesU8H$)g(Plra{VH(v3u^CO1~(+ zU0O7#)jaS4{NcwA+LuSm&VBcX2#Im3xg)W}ySNw%->orn1taZ&+d)}8gJTqA!u|5P z{yv?zol_3|(1(%M(EVU=cp?L`{Pi|ixk{U)*guFML3P!OSlz;zGA#T+E@8@cgQ_mv1o7RSU=Zo_82F?&&2r;WE z@wk}JHYEZ9nYUc(Vv~iTCa3u8e4q(yq<29VoNbKk|`mq%I6u)My=gPIDuUb&lzf4`MEA9^g8u z)vp8|$$HE9m_BTV?lOosIGa4jud=jIbw)O2eCMfyw2*S8?hjWw^nqws$O*M$3I1)x zR0PWFb3$ySOcGTe1dz%N0l;RPc`x%05FtT^f^j{YCP}*Q=lvp4$ZXrTZQHhO+w%wJn3c8j%+5C3UAFD&%8dBl_qi9D5g8fry}6Ev z2_Q~)5^N$!IU`BPh1O|=BxQ#*C5*}`lluC515$lxc-vNC)IgW=K|=z7o%cWFpndn= zX}f{`!VK02_kU+Q5a3m37J;c} zTzbxteE{GNf?yLt5X=Bzc-mio^Up0nunMCgp*ZJ;%MJvPM3QK)BryP(_v@ei4UvHr z6+sbCifQaOkL6-;5fL8$W($zZ_;CZp305C;~$hhRquZr-r)jjd1z z31%ZK{-(`P#|Um_Sivn@p$-vz46uqT>QG0B1w9znfS9A8PB2LaHdzA|_)yjXVR*l{ zkcu3@vEf7bxH0nkh`q?8FmoO_Ucui*>_a~P?qQrlZ9@+D7%MTpSnztpylXrt5!-k8_QPB?YL8Kx_On8WD zgT+111d(Op$^$&KLAN5+@?>f7F4~wFi(8TL8+szgVmcMDTp5l&k6~=rA{Dt}!gb^r zSWY<)M7D|Z2P0cEodj6E42PV>&>DFmQpgt)E-|#sSUU@uKed+F680H@<;-x{p|nuH4!_mn85rx>wz;0mPi2ZkL#k6;sznu?cXh!T0S>{w6 zL^gvR05NY64l*<+_L>On$rjx9!US;l;LX6@z}yi#2XHh)F@Oo+l)h%fq$v}DNmF2> zfs^_t0)3N-W<9-N?uedVv{)-J0W5mh#29QM5R5h&KuiRM=0Zvnf#lF=K#WlCgc#9c zS;qvh(P$!_a8JwyhI^ZJV2k+B6Z^64?w|1?5gyo6y{}923CRZfYVe1#?F% z7h2SUiNO3;T#JUOyovSs@@C1GtwipycA=*x5{BpIZ_#GCMuV8XK=x;qCNy{d7?wA~ zC+=vjls;ci&zW=6$H~4^K%v{p}Ab?U%C6Z4p%eC<3ExqU$XR<}LLF67A$Sr20DR_pJ3yeBa~ z^sw{V0FI5;UpwXsScYuhbqGQ`YQ25;6p6W^+tgL&;Ml;>S3CGpSZ>VrTn0m1$y$HU z&65)I!c?oREz};c=nLCliriqQX->4uivHTgd${GqeAlf*!P^B|jkU|*IdNP(&6C>4 zqOW$)Nw9nvjy^&`?E|gotDV{JmJ9Q~vuhy<`^C4XIUDt|j4o6rK^e8_(=YqC zuaR6TRVf@tUFHB079o4MBIh{M~4>WwnGgesQH*3?w(RA%hCZ*7)b!aNV=yOQ%o_Y=Lt0Sl*(9^jfRnC210Om$=y>*o|3z} zAR&vAdrB#mWoaB0fJSw9xw|Am$fzK>rx-~R#7IFSAwdu_EI|SRfB*yl0w8oX09H^q zAjl2?0I)v*odGJ40FVGaF&2qJq9Gv`>V>2r0|c`GX8h>CX8eHcOy>S0@<;M3<_6UM z7yCEpug5NZL!H_0>Hg_HasQGxR`rY&Z{geOy?N92Z z{lER^um|$*?*G63*njwc(R?NT)Bei*3jVzR>FWUDb^gKhtL4A=kE_1p-%Fo2`!8M} z(0AjuCiS;G{?*^1tB-uY%=)SRx&D)pK4u@>f6@KPe3}2j_har$>HqzH;UCR^ssFD0 z7h+VLO4o@_Yt>>AeaZKUxqyvxWCAjKB>qjQ30UA)#w z&=RmdwlT`7a8J8Yae=7*c8XL|{@%wA8uvCqfsNX^?UZsS>wX}QD{K}ad4y~iO*p%4 z_cS{u7Ek%?WV6em2(U9#d8(&JDirb^u~7wK4+xP$iiI6IlD|a&S)6o=kG;59N|>K1 zn(0mUqbG3YIY7dQd+*4~)`!S9m7H6HP6YcKHhBc#b%1L}VIisp%;TckEkcu0>lo@u995$<*Em;XNodjTiCdC%R+TX|_ZR#|1`RR|`^@Teh zl#w@8fI1FTx2Dy+{blUT{`^kY*V-AZUd?ZZqCS4gW(kY5?retkLbF=>p=59Nl|=sf zo1Pc|{{N4>5nt#627ylGF`3n>X%`w%bw-Y~zWM_{Si$dc82|=YhISal{N7OY?O`C4 zD|qb}6nLWJ`hUyL+E>-;ricg9J@ZNYP(x(Sct&OI$Y!QWr*=^VN;G3#i>^1n4e#Je zOVhbFbLpXVu*16enDM+ic;97@R~u&kh__kgP#!R`*rQEnA+_dLkNP~L`0alC|J;c; zeiK=s8;BsLE)KbG3BD&Br@(Ha@SBT&$?xX`=$;eeel=|R_dIr6-Ro?=HEjnsJ_b`1 zK6Yg^-6;^2aW!xeTK)A~3Rm|L^FCHB_I>jIju7ZGo&N_1*QHkxH2!!%@o4iZ?vntS;&zJdPe1dH#04YD93A44o-MpfD zP{rn_aq>U%RDvC2+bp;xPlsOzauIi3*Lf42`jVKKZCRuKdYhi>FDuL2l=v{$BCN#Q6796s%r-AG$Q^t(3c@ zD?w0UhYr11@feiyl9kY_@H8~|xlmO<8PfQmj1!$@WieW@VxR@Psxfe-v9WCi1+f>F4VL?0O~K7T?m4-u|pSkBpUJZZe*16_wAp zSYZ@;k`3;W3UHKUWc8QeI}0jH5Ly=cGWQPw(Kr2fm=-5L(d`lcXofy8tJY3@Tuadz zYWXR{mW7XT!RF#RVCe%}=tM*O6!AD3^(!8un~opNI%Uko7$5t@<8+?; zTxDys(MyyGsUjtSu9$+|_-t!U3fVb1dkK?l`17<+jfl=hrBHnDSV>^R1=TnQeyqbW z>ov#l%!1|S!1>8UUxIdhQq`_klcHVx0{?#>K3#$4GlXncwldt!g17TcvKq-jo_996 z>oA=tH9CqRl6Yw?Uc`am!V?lHJbizOJaVaScf1UP5e7Dbgabq=b!B~T&_F6?ooU>w%x0A zH~&MHJ=q`fCH{U<7MDXE4SD32cDZA)WJeWkllJ`UspWaS#eDe^kg^oU_A14UE9zG-a^g{xaXf$})Wik>gT zl#dkzGr(;h0JZDuFn(+k8wNq?PZ5grQ<+sM?wBGt@JnH6v0#or-5wBQWKU~(S_> zkE!tc*ZJ1Y&*p(xX84POb3cClRMd!^qJ#CAZfIepEj-<`VURS_yCz0(?*Ixcj4 z-!zV1_QZhpm=0<;*(nm+F>T=)o?ep@CK5I%g^VAA+RB25ab?7)A~z~egru=I1S|@v zH7tXV!0wmGS^qj#e+MY;C5eUjEAp$Y?LDkS^QPZ}8WN85?r$u<-Epi;yZ1|J2J`se z$D6DpH~2F=eI0B&=UFAUnJvZAmClJlK)sutJ?M>xpZiWV&0=G4MZP+x+p>EX=HbCz zxls%Mw?*u^;LbHWIWCyq+yi)`GmFn9J112CZda_u@YIP%i;srFg_paU02Ifij*7}l z&CF-(3|>*a|+vbNR`^RP=9G?ymEJ0Z~)d&c*UE$UMepZ zcITr{0WqhxkjUnM15js_gW=e3Uh|y6ZReaXHIz-=p`x5VvB&rH9y>Amv@^WmXFEw) zQXYrk3feir=a{jMQ+wDIkkFnZ$k{sJakHn*?u za%4b!00ev8NVLM1TY=cl?KB&55BY_MU-sg?c>=Dbz_W{(Z~c?HJi*XpYL)C6Bd8WH zt+v-#0&o~@t4qESi*)+eW%@VD0|o^yF)n0hME$UtXF$*Lvh}7sso{`|pn*JDIy5^Fm3s$5*zEE=?u5<=l8FJc3r%+H} zdfoNl2J0^~!-*mOL5o-x32|e0Im*E!yY7F7E5N)W3>+v_LBydlEx?4$RL5f2oYRD# zaR0wv(-p~wO0eLDl3K=%`{5+0Gd$ktO=W)gWlGZJ0`K z$_RNA=ckrfa;H0KA~dR^p�(p-{x$&=IACIfoAR!za)F-^da-t3#0Dycnp zwO~NVXwXCl;jE<}>%@xz|=8fIJAB?>+E{7)|4l${4ngA3G|=r z2Dyv;VVWSgZx9Wj>qUjleGl3Ei9K4>h!(lPS%8VOG>Xu0%6VDz^O=bjJmuP7>DeUv zrbI}MlHB^^d?{zv6d=@_ZD2lg1&G7UjnVN{1}9WkaM3H~btX0GtSzB+tZ^qRgWo4m z!GmimlG$=wgXCnr6j@m<1gAL46#T~5Bnm=2{^@>|t&`9mkEPddj zAvG~@Tv~TAm2i%VW}R-g(Z0)z-Y|szHr@rk>4MAyG*Ma*7Yh#H7(!-5>DZ@8r;_dx z{prSe<>~099F8vsYd2xff7uAS%7{S)f(|@me3t2$iy&NEc7OUEchp@9A|X;;IA>8!oX+y(BKJ$EzV* znR$z;!L$s7uy@{OT~nG#B!NRraT8(X##Ho!0r_o@gg0CA-9H^;-uE&?$2$nHv_00o z%cbuUc-tCx$Uh&EZ4Nf4Zgqv)Y6>usG3>GeQnxx_Z6+PcbX-+ysbt1hQ`K1LDpOE? zrAhIZhSN9yVIAOa22gn577tbc&i3|3V8NWy&!tw##`}9*x}gtI^h1DzZRA>UuaJG) zaZ7j)dq!O}{?#8Y7~7i6fHh4{`pL?>-18|p!S75Y#^DM>-S3)vuZG+Q7l@ek zQP~#cBpWgg#mApc_sPYjpw8odQuRokmTkzcNl`^CcKB7e&;zViV;{Y{o^Y$%7i0m# z62%#1Lq!RC?}lK>%mp}T!3Xv;L*0v*>USLm``N%>w>@fwC+#T&Tx2bN4w(20JB}oU zuSa6v^kXi0xPs?pbaOHnyiqq6By1EZY9OZ^^QA>{q-Hsd&m`pbQ%8121aWG-F5xf zlZ%;B{;C>X19|`^_?dVyCq>n+41w7|!tUS!{9rHlbhX=SZO5CQ^;!Du_E7*`GiR^Q w)2!4MKjfSAeNo!9>IaV6aUZ*?W>} zs4%E?srLW`CJh0GCIK@hTkrW7A15Iu%N&?Q^$0+!{Tv&|t^Y@u%!L zglTg&?Q5q#ijZ;&HBQ?FNPp;k3J5!&{^+SGq?AX~SiOM9jJMRpyP?RCr@z38AQyy&WRMaC;n4una$~nJKSp?q|s8F00c9?Q! zY_ovvjTFm+DeQM^LXJ#v0}6HRt3R1%5PT*}W!k8BEM;Jrj8dIceFo2fhzTqaB3KKk zGlCLI)gU25(#u6ch6GeB1k@eHq7l{EHXv0n6xE#ws#ri}08kkCf8hUt{|Ejb`2YW* zvg}0nSSX1m=76s?sZhRY$K=3dpJ+y*eDULGnL2}4>4nvW^7_<~wIM_5fjvwt4h1|g z)g0Z6ZFq9j<~9~b8((~TN{Z?ZQfw|is&Xp~AC61sj;xItKyCHdI|tCMC_LbXF>~vR z=w6V3^H=W4CbAgR4#xw}ETTwu2guW~=Crl@SMXv85jQ=%y!s^?m4PI0My7MWICO;- z175jm%&PcPWh8QdOU(#8bp4!N7ET-+)N}N2zk2)8ch|4Q&lPFNQgT-thu053`r*h3 z_8dI@G;`zn;lH$zX3RzIk`E8~`J=BBdR}qD%n@vVG1834)!pS1Y?zVkJGtsa(sB~y zNfMYKsOJb%5J(0ivK8d+l2D2y&5X!cg3BG!AJ}910|_${nF}sC1QF^nLIhzXk-Y#x z0)&1iK!O;Og0Ky!;`b~v%b$`S4E&fB)1NB4v@8wr( z&+NX4e^&o)ecb=)dd~C!{(1e6t?&9j{l8%U*k4)?`(L3;Qjw z#w7FS+U(94MaJKS!J9O8^$)36_J8;thW#2$y9i{bB{?M{QS_inZIJ!jwqAbfXYVd$ zQ5fC$6Nc9hFi8m^;oI-%C#BS|c8vy+@{jx6hFcf^_;2VRgkoN(0h!_VSGmgNPRsxI z8$rTo0LaYq-H5i&gtj81=&xU?H-Y2==G@uQV7E`@+2E9XQW@{&j`?EOktk|Ho{HU>ZqDzvgjwBmdex z&uZNd2C1h{{}2k6Ys9$*nFP3;K%u!MhW`uZy7Sn`1M1zs@Es&;z*Z>Gsh@-3Fe6pE zQD2@cqF((NrRevgvLsvM_8;;iNyJ5nyPyy?e!kvKjGj`6diRFBEe49Oa7wwkJFV7Z z$YT&DWloYu-H?3<0BKn9L&JYDT-SK~*6c5pi18P26$JESKRYj{T7Zk6KiRJcbvOO*{P56Q6s8msbeI3>|j>K9}Q9UBeq*inXKemCm`-<5|-$ZyN4u$(3 z&HcvqehFD%5Yrmykg-^d`=BSa8(i=>ZoC77^mWY{evp(km@aHqhUECBz76YiR+VYK zY_avFC~V3$=`6C4JhfHAQ@DZtUOwH`L;oYX6zK0-uI^?hS$ALfq}A7evR;ohJHij} zHSZdW?EKv9U1s4oD*<(0oQ*;MaQ6@cvGL zuHCPgm_NhVsgp^sfr*ia^Db}swo1?O(_Q2)y+S$CBm+g=9wCOUPbz(x)_GbaKa@A7 zuI&!ynLiZRT#V%_y_-D`0Z5lT*auoe{(U5NylTzFSJW()W-#F6*&A`LNO1bV#Y;QJ zSbLBnp|B^dtK|KIWC|No>JjWBWE@n7O)x{&^E(WMeMvp57#qA8m* zeTow*U@_86B#Fm*rxyYu5PRWaWHx8y> z*qmHEp(AMDl0v)ij(AY8fnH=~ZwwjVAbu*m5;xPfidh@ov6d8g zfJsi&!QyK53Es%sC39ts;54V68koALD4b|%tNHW0bIkZAJKa=W&FomJSEDT>W1xIX z1x%Z>AvNIsSPLcn3RTcHXb@KB?cuM)=x6fcIx>&(GxqZ8w3p#jJ(GVgc*`c0HG}dv zIop&Qim!K1NFwic%07KcjWgHBPUkq7f~lj;TPqVGTiT#cUeim>;nY`>h@a*S{qQex zQ`z62WK|Mj)Y{tfF{;T4P;c8$Q|KU?Joh zIkA^z%X7z|r>4aTh@|StTi!-r1D!g=zb#3d#{{&K3CqE$Iz-UH<%37c zRfkO`&uM%#AD3PHv`g5t0e^O%nVL0d{Xlx^EjEC3#skF@`zl-7PF^0oxW)1!C!JxR zWvuAHH?)61FKA1QeT*_sY7;_Id#!GmV4n`MO{~sv}VLSK` zXRw=Y=Clz*00B(5y^K;gCZMAzjT5+c3IC=)l(9VIDdatpxj3y89WwI|bH&$!ZEvp` zPR!T@#!(|KfI-w?!&+7$N3F6>tD{YO4Qg$d_`nNEdfVCha9vaPn0jI0`)`@*72hq! zpU5ND^P*RoEkbD5o#az(-g=Y)L>HH>Oc%}$ zT3Rs_ih0;4+Lv4Y;@Iv(;fUbQ=i-G(#>vghec~*j(I#r|5mqFiJBpzi&hzEcD{u$< zRsm0BVYn=pT;0>R(itW|*D&;O%bOc7et9ACaH#J>z3A1A~6fdP>pmbM%xzm4>|;c_?B+%sl;Qs2{t!60$^u zH1t@9^6>;?!FuusnISi$f5CL&;z?EqJN$FBuWDA#D5`cy_UvCFIVvf{c?4N0teh;d zET$7aVbj08KTQS!x?Nd1Is8q8qFzs}a=!@nJ;7FSfCY^T@D-gpw`w<6e#X3+;O}1h z$%I!M)0bg|EKUA04Qjn@+x{Rj8vt6Wn!R|3A92z}^$KfF5(#CWr4y#~re1CN4i4w0 z#GsypBR{xA3Er7sgAi(|}1-W?s~n$7?K|9WL8kpVfw-;#b9 z+mn;=ep!162U5R>_t}fOt~tE?s#m( zO-S$7>Ay6*hHdZ)7_oU915WYYCIX;hFI-U2EWYX!pllONr@Q--2o~`!isi6vTPLJ4@(|o=%NHYjo0_S&q*UQIROw@*N-By@PaQ&;YxFZ0aR zX&}LeOEz);#m~Hwm^VAY8DK}b$F4bo{jMN?d!lxKPhNklzr^Cd`0f4oJr^z=I|l`* zm8AHm*fPV`0=lF3Pnnp}&J0N1X@}-D94YvmUabFrLGSnTz7Mu^21F#O5tN#CuY9Vh zUZBH=ez%h*wkf0hBtXJh1SN3d+IF{gzT7lp)j}n?03lt;XSQRAh7qd&v;RwTYDuQ# zbI2*r<>?x-G0@hM{;%{VBD7nLKt~D`T~-HAt5;h%i0_=Ifs=yHma5dhJ+QMG?Ux(a z|E?1CMy1!~oA`FP!k~iG=t&5#>bVdz=peT8HMB6Y)#7PpETtNryT^+Rv3vpJaF^zP z{H}0-LyV9Fu21ID%wO9f1IKlFr1p4c{o-?03vyB-tr5duk^&L$;m_|f$vs`^Sl{j2 z95}oY{LlY+=ZS%J+tZoXCd0*sSU7w^gjovXn+g7uyra5{cU49@yHf#Z^Jl-$9cIfo z+AJuxH$VLb=#+uBbVmUjnx zxb1pZ@-O9=AIk4@S)m6fJ2?{HrNYwwnL3a45muuNjr;6$O`bGEM0T4A2_S$t=86*- zcO+0mywg*j#A4mU}enR_!cGmIYQ;qwfchWtFEXL)AK%*;=j znYne+hS4EMy3S)C*mZ1KI>!+)0V@9!N6H$Y}~MJ{rYuf zz^KljIWvFi-?#?V@LPR&c6Nn{!=XM z>}-h$S76;$H{E{Y%@^zlmOl^efBwa%UU+jJD9UVukQ3ti_kH-?H*RC0?M1W%FCvMB zM_+v6fk$6X2sx)-p~B3&Kl{nscK}pNLM*qjtpaf9>AU{-iPKQZR8yCg!TY}Qg*(;) z)gdvCcB%kppZc$VdvsK@)3l1{&DG!d_6OHOS`y=ITLEVu`unSKA2E%JD*DVX{LJ}K z9l>hMRDqxQh0lnpGHpVYneX}eA3Pt|2v%=q;rt)``R|#bDyB)OXY&vI_@|*}h}G?^ z@aZ4_!7cQPX`!fW_?{oT1NTwHs#l5L-0`E|y@48<3Q^HFf8=Idi zpJYD%1MkII!~|7I^WGo)IF=?{>ACnjJ_WUi39C}!Q{QnheVJqeKKqq5^o5CBde(g9 zvw$X6^jz_^E2$wSw4!q5*RG(C2_^XO$HBn_55vbl44OnTTRwRaePP0vo{K)U1#99& z<>rq7V&V(<&@I%MFoN5zrY}sz=(*-L&}1QQ*a%`u25h{cFj===17eB_uGuzG&byQ< zrm8BJZl4r_E$3k|Wo6FW0-6M7>qac5uFQsQcmkLWGfeH74S3Z_rJ!jgN++!@i=HW8 zkyjI(oPH-+-N#Qc^-mpNO`bc6r=2-<%&Wy5K1vfFJB(L_IkpS6fY^NmuL8qsgj>MD zn~BHH9WM~32_3vd=W&B)k7F9q%stJx+b_L_X-4zr^LVUMCmyCTA3sWtkvsmME?Xiy z?xOSfB=_$oY06~J-HcCq&)qcW{j;uP;?Dm}=hkq?zh&n!;m((-G-u_t|6x399Q;>A zgNpxoJNj{u|MFDH7Rhq@FCAl0dE|ddnl!oh9{Lq?@JDoR6L;C941IK`ISfdE$4S zE0AUQ8+2|Ncl_q5QkSp#AODp~(^mfP&%Au@@|TBQwoP`UU+V{6u8|)6ZA{~uKmQ*M zmrMTDU8S~8Eqi{^v0Ug&5Upcm#y7Z1(RbgZAG8jB$eRwCspQ)>5;U)oGZ&E5aeR*K z8Yt`Y0$G))Yd(Y3KH}tA4`-_QmNke5hU_|nq=xtyjwW(_o?itz>B>WM&^63bNdQ)k@-IgDHW*RW$Xo9#RzrTrCn7L2H{9Amq|qNg@#eZY=|P zCoI?2s+L)zsM%WX(NbVEY^`C>lFjIBYmJ6@DKJ0ZT4&F&WHW!dwa%QzOG!?jY_2(S zDcEzZbz*2Q!43|z))9yOP9X1Xt%DXzwY(3tl-TR=Qb_MbZYRrooh;dYYmS!U_as1(=YVB?Q_A|tNu5Ut&_q3jbfDM zoFxT^uEuH`nX3*sB%K?GuHUkweYReBwnHqh3P)~`+s3+Tj!rDA1e)8vuBv5J*IsxC zkd^~b(aGzArj08{>cnzOuy04C+C`}gb|Yz-1avxeWzev3NzcHbz_&4W@QCr$z3~w=8Ua- z`;vfG1~BP8CyLb=F7t1am~ph_#|O%$khSJ9%Vtcn)YmpgQxF?xM^_Vb+5fnpB^W0I`f%X8gb9#X{Q-yJG0{Z56aWeI&zPxnf5pdJA38bM`cYnS#x)% z`n1tFf$i)W-hGm(f9mde^=X@NcV_lFb=P`4&CI&H=IArijGwdCk&X@uQ$5xmj!~^? z#$ROCI)V-~t%L%GS#wo@U27ddR`4`3)WoB{R-4snfNrfee|kI8^bu#yDgYqOwas9# zmcb`3!kRJ`Cr=_tq)8aMt{aGtUZsqwVlj6DgCGre>AEt&x8H_in!x@uwgExIh|-mA zjdaC(29~CTVSaaF7HPbql&*9Uo8P@f)>LqCXclr}peS7_1BQ28u9PO8Eq1@`l3q9o zkfKCaO2?T?ZyA6loW<#9_c^O=m<&h}CA!ineAD@=(gbq`vyT|tiJ6#^B1$P;;qax` z55k&Q?wEh#87niLo*+n4L@65J(Nz~=Ya%7^(miLb(E>A3B@|Jjl;FU&D>o|9#7PJH z?|ago!o;WC^h=|T7PVBg(DAB}72cyUS zb(f>Bwbr!F1eTCO5fpj<{PqhY5>143p?~5ZA5H40);=@M#MYvrB6gqHbU_!GSY??i z%s=>-ciA4*zOOZHds0a(kWewZ4h(k8h(ua7HX)Au&mY~H8KY6(_cb$_&fA@QjIW-*heP3%$d!m5^AdnT}`12qA^c@!g3DOwZ5WwE2?)-yU z!)Vx#Mtxt?FzFTwK!77sy7)sMzUd->w4^bxtpM2j!b1pjgyk zGKwWGeb4)^zjy{9Es&PU1}gwg?|J#L$KJB7ett9@4M%-nGtIQr0>Fl@8-yh`-+1ed zS6r}(MeSvgSoFmH*_WPu@i?}!AB~2?;i&IxrkNg~cQ9Som98tcq)k^|eeER|Zl77t za-TVUc;DNvzVXJ%w52+#weN?+;i#{f#!Oc&z?81*N>^e~ltRS%ZI@lR{rs()HmqG! zx*}ZrI-EZ}ckJMiy>A^oofwDfC~IH)z8{VHKGT@#E5I(Ll&+MnMCl>~AV7+>Gi%mF zkU1QlKASdR0B80!YhP<$Ywi0?W2Ux45oPfxv9QolWzJPD^weBfvo4SONxP35106sAmh(e+vAs0GboFD@PvNs)jNPvarhW}0YliZEg{Gazv z+JDIpoojRVPr<*C|BTq<`6ga{5q^8^!|0cxe=rZ!zxH3%f5ZO0cQ*Z<^$Yt2{|Ek0 zyT|*F+CO@K;(owBKtGg!S^xj-Z~rga2m6nxKl9J=fBSuNKW_dLKWhJKeg^-Xe`^1? z`TyJj)8E!#>_3Y?uKrwqq3LJ#SGU>AzUO|6`nR^u&3FNN_jGOc zw)Nw`wr3yIKhgcee6IaN=ws>M{6677%)hPwx&HzC(f&u~&)6@b2kNRzBDQAP0*H73 zq%McOmRk{B3i47qRe=DA*$&odrbEJZ*pV9XXa&p@wlW~@Yfs>V{yiTtplMhgM*-Bz zsSnlq&pG;z0OUN%$~$3=g1UF+G*>+17eRbBf3=y79J}KR8owon@$1Z7MIrvvWWH)34nK2SD)GsrJ{l z1Cl#oVo3A8qY3e=aF)qzms~FG#2$LzT=gs&aVMOj>(%{y<&O0cG!nCiESl~x=^dF{ zKvj8F1K8Ng171wwM5Fh4KoQw`_c6#y$(5cAm7e}~nJ#A*fx+c9;y#&W!#VukR)ugk zKp3=+;Ut+IYn%m+r4d*<`L2h%aDnX5}^!5R|H;(34AoVWjRx(msBZvk;rCI*|~ zdOijqI@9Z{Vu!~jvHW{lBa$rnl4+!s_5sfK3bCGk-B%iDe&@-}+%fOKU|(9?V1 zHE8&@4z)Kx!RAvAs z!Wic9=o#(bg?kc-G68-m(jZ`^=XGUXb)}t(%&~sjFnV^sEX%hSy6UKC4iOhgV=BHV z2w`4g7Y=s#Vu2B_?#VQ|hP39@eArgfX>-0S+dd&^mx0*wp}>)x;c4RUgxz%;oNe?& z-7-lJ@Y^2^C;=qJsxx5|xF)*pTGhch2B&kxtn;f!7=gznk}I3}Dh}(CoMXgA5-p&kS202!l?!fT3t|HG*rIP~mS* z$Wjo}jq3}z$Qq!9yrtd3fM0N629ZM?LU$nv@Tv9b7I;D|;0H2dsA~g7Z7zp1| zB)XmrkMgF6OQr|R)HHD^TE{Y#j!~SR?b`Xt3Qs`B+x<hxexYeAjMUWdZ-*n9%(1)Wb(n2U<><7&9dwGJmrob)4%H? zlQ%z+L-^$dFhhH|@u$%97Qz?*Ynh2VG@q|?8vY&L74&fs&_b&3$x&Oyjl~LQDRRap zJU4U*R+(2Dd!G+lh8!V{pT_UJn+^1Qg6$` zqkNm(a#hWyc6SP+p5=C4HL8-m`pO`5o~`-LI?_h5CsH?F_%?nDodmz&pWR20WTpJE z?N|wSzLjMUK8E)a2tI}Lf;+;*M|h3Y(U#>)g1>zk9|Hd}oZAa2 zLYBWBoSW!Ts!RwXr^8h+U*@{9{zqS^iH)Op<;r`Uw~nc}<^$V~_i%$GFjaG?X1@E|M`h)nekvFKt`Dh-f>@|0-`Xoq)o` zx;JmzDfOV9qCx|EVpogEe0LK~tGS?5$$L_i6P$P6wIsCQaP_;d{{N=iV@+8LI}o#( zvo*Ejy=IIn{rdIQh1&q-{EuohpVOjJ^Q3lD*YTp37$^RRgn8ihpdu5{Ct%5-KO!VL zcNB6dUajXI9jkm-P|i3~GB-A(X`P1Oqqb$tcku)UJw0w3GeUijb__#QT4j%64z%EeB7S?jlWwx_7&+EEvB|6N=kV}DwnyAlX=?j`) zmU#!$*^@NIu#n_d7;WoJV@*Fbv9|yJO4;n|BNF2xy(54RyB>t~8lUOUW$&2%Nwi1y zx6JxW88>U2$#qhl^6KUbtmg9}D0o5vYDT7kWJthLGkpGnN4T>{St^_EU>4;DmLF9o zr|LqsA8_MoNLQ=}w?8u!ziSZ@PC#Y<#9uJFo-ozVo6D;<8j^1$c|qAE3ZTE5i~zmE z$BU5lw6l=EWsg^y^;8>r9qH{xfL|~PZYK#md$zZ0?o11gV<*WSW~cgy2GYGQir%wf zt4iW8D+;s*;RGrmd(-T<@2&j(Cb9xhV*l-x`TpK`xq|7p?5R%5*s!69?2c!cC*VY* z2DE^9pvOPLU!1e}wA8S8opcTJ3`NB>hY=JQnL~QFXR4K8A$BqJnoEB$wn-%u@E6Mh zCfMF4kusv3N!(aHC}4)Xs^xoOwXd%e^6pi5|DZo=Q25j+6HlJ^7FodH6y1bMROR^q zGu6)fopS`h%Sw<;ZH%TEPf+#81-#_v+@8nlR0jLcIDKQtLleOC)6yLZgC!D9X3GgS zohwU{v$jl=quD#Go^hB{`@Qw*a%`(^jyT~=q^bWgGzRj;|12J55HWdCWV}EB|K=%N z3Nq-qxJJ`>^|1MNN+q}zTB&ooE3j==AgK@^UW<^oSbeALa2peF)Th6{@sj0KyMNHZ zksk1+MXN2tv+22A%cQOGpS9)77(uP9mh+!5T5ERLvF@b}$+WvXM45Z?-kCa)fb~f1 znVbTD$Gx-0Zxc`0D@YgHakge6SL0H`-vN_x?AP0>iGH0_EE&=v83hMJgaKAI0jJXm zVxVz;X<$v6WW7}fxROO7vr#YLP;;lij5VrX{;>7kK6TtOH&6|Ar^xo>00%+u$C4@# z>!jOt6*3><171+WxoZnKDTzJtDRw+T030;yI}~uV@9fCnei^I*j>Bp&mzP2d=FPb_ zCM*l_+$LDR3B*a!A$g#>xsrZvw0lckxmMg>0aQd7tPyN=t{dgXb;Ie+T8{fZH=gdu zM7Rg9c(kg(Jg0?ARRRl=AONFKrvFj)lTY$KfT%6^6s`mk*ABGhsce*LsoD>K{z_M2 ziPpnu+lw22PfF!CoId^6n*G4H(Ix+#+N{C(da7t1BYMGEaE#PdpOLxsVD5riQXHp@OX;`S`8VnpM~)I920w~<3|mo0 zf8~Az`*?2?H&gZ&*K&bRkV@qzvMlRHXys8*Ze2+1c?5o!^+$&MHxB@4Ee5cke52R! zmn7AZtY6ST%ixgU5)%$%QcwHj7Es-Qu^kLAPwy%7pGBw_4Q9#da^W2$}axNHr03)_nw z5?yuNmXrI5HgS46)c5&}B)Tts49oU92>3xBLLy}FMUW=84DQbVq^;7_e7|(Sdz|&J z73N+M`rc2rt*oSWu#7S{*s~nH6HRHJS1SmzeXk|;CA)FI4bat3<%}nkB%;;?=F>B7ms9QSxv#@+69;@>QaR?REYX4&)=itG>rM{<{A79Rmk)`5ON#GL`*KX%}Ihk3w(RtM-WLt z?f&FLF}4N^yE!(pZ&Yj&Bc`~K0@4_}*0Om?wN|}4WJ>WL;G^H2*QpgEkGA~OET-Km zkwz|5{6dnz1U<2Pe9DNL>3g5FEIvp1jzP&2K#z~j%g6!7B;^zF+o95?fV{3mnB8*RMhCDNp>Am-3e@jNfMj?jHV$MWjk!DDKP zkAz$Y?Sr)!GUOX}qTQ5aMh|wq1uq}~joWyKl=b_LboM#wi{CMuz5x6BKlA-qy++cM01D3b7`uD z#l6M4pI;JCypO8JZ6?U&wNxR!{4oB_ zlV!x9+-&Qy6{%MQ{~yoZGkKiTSC`YS_j22~G;xUV855g2&C(zm^V!(wpcm@zn{%!g z4}JGo(sGZ1O~to-}le

UmY2RIYtNPVDpE$%vda+HD#3m z&VuXJ{BK&Qe+rBa7eq}Q(bq|tn(RrJAk|ztj2(i{d>nmQnM?;HF2k&9sA6up5tmjl z7lySlzMbifH17-m-Lwa_F&e7nOH?ESi3#ckR3tsM+jsck3`oG!uMS}|eAwVXv>}qxwq?QY%QJ0}r@^;fhuUA9W z*BVl>TGo&N004@xSiwDUXUvp51sVmqO3m)=B55aPwf@0=e}cN+$-BdKxY`YrT_4)0 z_d10#i44Q*rFr8MC>*)v$EJvz``(pb{e&*6k+b zsMz%($|1+8hn8c2?P(l@;Rb&CsZeYoCI3?2!LqjbwPXW3z4G$Qfj=cT5Yb%vY0(AX oeb?AaKtwrnc|$|zzw9vfvn^aJJ!zd)XFXqqy0000001=f@-~a#s literal 0 HcmV?d00001 diff --git a/examples/skills/app/src/main/res/values/strings.xml b/examples/skills/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..d945175 --- /dev/null +++ b/examples/skills/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Skills app + \ No newline at end of file diff --git a/examples/skills/app/src/main/res/xml/backup_rules.xml b/examples/skills/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000..fa0f996 --- /dev/null +++ b/examples/skills/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/examples/skills/app/src/main/res/xml/data_extraction_rules.xml b/examples/skills/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..9ee9997 --- /dev/null +++ b/examples/skills/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/examples/skills/app/src/main/res/xml/network_security_config.xml b/examples/skills/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..c64cff3 --- /dev/null +++ b/examples/skills/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + 10.0.2.2 + + diff --git a/examples/skills/app/src/test/kotlin/com/motorro/statemachine/skills/app/state/BaseStateTest.kt b/examples/skills/app/src/test/kotlin/com/motorro/statemachine/skills/app/state/BaseStateTest.kt new file mode 100644 index 0000000..8896302 --- /dev/null +++ b/examples/skills/app/src/test/kotlin/com/motorro/statemachine/skills/app/state/BaseStateTest.kt @@ -0,0 +1,61 @@ +package com.motorro.statemachine.skills.app.state + +import com.motorro.commonstatemachine.CommonStateMachine +import com.motorro.commonstatemachine.flow.data.CommonFlowHost +import com.motorro.statemachine.skills.app.data.MainGesture +import com.motorro.statemachine.skills.app.data.MainUiState +import com.motorro.statemachine.skills.app.ui.MainUiRenderer +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 stateFactory: MainStateFactory + protected lateinit var renderer: MainUiRenderer + protected lateinit var context: MainContext + protected lateinit var flowHost: CommonFlowHost + protected lateinit var nextState: MainState + protected lateinit var dispatcher: TestDispatcher + + @Before + fun init() { + dispatcher = UnconfinedTestDispatcher() + Dispatchers.setMain(dispatcher) + + stateMachine = mockk(relaxed = true) + stateFactory = mockk() + flowHost = mockk(relaxed = true) + renderer = mockk() + nextState = MainState() + + context = object : MainContext { + override val factory: MainStateFactory = this@BaseStateTest.stateFactory + override val flowHost: CommonFlowHost = this@BaseStateTest.flowHost + override val renderer: MainUiRenderer = this@BaseStateTest.renderer + } + + doInit() + } + + @After + fun deinit() { + Dispatchers.resetMain() + } + + protected fun test(block: TestScope.() -> Unit) = runTest( + dispatcher, + testBody = block + ) + + protected open fun doInit() = Unit +} \ No newline at end of file diff --git a/examples/skills/app/src/test/kotlin/com/motorro/statemachine/skills/app/state/CheckingAuthenticationTest.kt b/examples/skills/app/src/test/kotlin/com/motorro/statemachine/skills/app/state/CheckingAuthenticationTest.kt new file mode 100644 index 0000000..f27355d --- /dev/null +++ b/examples/skills/app/src/test/kotlin/com/motorro/statemachine/skills/app/state/CheckingAuthenticationTest.kt @@ -0,0 +1,104 @@ +package com.motorro.statemachine.skills.app.state + +import com.motorro.commonstatemachine.skills.domain.session.SessionManager +import com.motorro.commonstatemachine.skills.domain.session.data.Session +import com.motorro.commonstatemachine.skills.domain.session.data.Username +import com.motorro.statemachine.skills.app.data.MainGesture +import com.motorro.statemachine.skills.app.data.MainUiState +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.suspendCancellableCoroutine +import org.junit.Test + +internal class CheckingAuthenticationTest : BaseStateTest() { + private lateinit var sessionManager: SessionManager + private lateinit var state: CheckingAuthentication + + private fun createState(sessionFlow: Flow) { + sessionManager = mockk { + every { session } returns sessionFlow + } + state = CheckingAuthentication(context, sessionManager) + } + + @Test + fun displaysSplashOnStart() = test { + createState(flowOf(Session.None)) + every { renderer.renderSplash() } returns MainUiState.Splash + every { stateFactory.auth() } returns nextState + + state.start(stateMachine) + + verify { + renderer.renderSplash() + stateMachine.setUiState(MainUiState.Splash) + } + } + + @Test + fun transfersToContentWhenAuthenticated() = test { + createState(flowOf(Session.Active(Username("user")))) + every { renderer.renderSplash() } returns MainUiState.Splash + every { stateFactory.content() } returns nextState + + state.start(stateMachine) + + verify { + stateFactory.content() + stateMachine.setMachineState(nextState) + } + } + + @Test + fun transfersToAuthWhenNotAuthenticated() = test { + createState(flowOf(Session.None)) + every { renderer.renderSplash() } returns MainUiState.Splash + every { stateFactory.auth() } returns nextState + + state.start(stateMachine) + + verify { + stateFactory.auth() + stateMachine.setMachineState(nextState) + } + } + + @Test + fun transfersToAuthOnSessionError() = test { + createState(flow { throw RuntimeException("Error") }) + every { renderer.renderSplash() } returns MainUiState.Splash + every { stateFactory.auth() } returns nextState + + state.start(stateMachine) + + verify { + stateFactory.auth() + stateMachine.setMachineState(nextState) + } + } + + @Test + fun terminatesOnBack() = test { + createState( + flow { + suspendCancellableCoroutine { + // No-op + } + } + ) + every { renderer.renderSplash() } returns MainUiState.Splash + every { stateFactory.terminated() } returns nextState + + state.start(stateMachine) + state.process(MainGesture.Back) + + verify { + stateFactory.terminated() + stateMachine.setMachineState(nextState) + } + } +} diff --git a/examples/skills/appcore/build.gradle.kts b/examples/skills/appcore/build.gradle.kts index e9675bd..eb6d154 100644 --- a/examples/skills/appcore/build.gradle.kts +++ b/examples/skills/appcore/build.gradle.kts @@ -32,6 +32,13 @@ version = versionName kotlin { jvmToolchain(21) + compilerOptions.freeCompilerArgs.addAll(listOf( + "-opt-in=kotlin.RequiresOptIn", + "-Xexpect-actual-classes", + "-Xexplicit-backing-fields", + "-Xcontext-parameters" + )) + android { namespace = "com.motorro.statemachine.skills.appcore" compileSdk = androidCompileSdkVersion diff --git a/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/design/SkillsLoading.kt b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/design/SkillsLoading.kt new file mode 100644 index 0000000..41187e2 --- /dev/null +++ b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/design/SkillsLoading.kt @@ -0,0 +1,15 @@ +package com.motorro.statemachine.auth.appcore.ui.design + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun SkillsLoading(modifier: Modifier = Modifier) { + Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } +} diff --git a/examples/skills/auth/api/build.gradle.kts b/examples/skills/auth/api/build.gradle.kts index ae9e74f..6f87376 100644 --- a/examples/skills/auth/api/build.gradle.kts +++ b/examples/skills/auth/api/build.gradle.kts @@ -32,6 +32,13 @@ version = versionName kotlin { jvmToolchain(21) + compilerOptions.freeCompilerArgs.addAll(listOf( + "-opt-in=kotlin.RequiresOptIn", + "-Xexpect-actual-classes", + "-Xexplicit-backing-fields", + "-Xcontext-parameters" + )) + android { namespace = "com.motorro.statemachine.skills.auth.api" compileSdk = androidCompileSdkVersion diff --git a/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthDataApi.kt b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthDataApi.kt index 123c71d..460e5b6 100644 --- a/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthDataApi.kt +++ b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthDataApi.kt @@ -5,4 +5,4 @@ import com.motorro.commonstatemachine.flow.data.CommonFlowDataApi * Data API used to run a child-flow in a proxy machine * Available to a feature consumer */ -typealias AuthDataApi = CommonFlowDataApi \ No newline at end of file +interface AuthDataApi : CommonFlowDataApi \ No newline at end of file diff --git a/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthUiApi.kt b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthUiApi.kt index 8b13094..c7655f4 100644 --- a/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthUiApi.kt +++ b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthUiApi.kt @@ -5,4 +5,4 @@ import com.motorro.commonstatemachine.flow.compose.CommonFlowUiApi * UI API used display a child flow inside a parent composition * Available to a feature consumer */ -typealias AuthUiApi = CommonFlowUiApi \ No newline at end of file +interface AuthUiApi: CommonFlowUiApi \ No newline at end of file diff --git a/examples/skills/auth/demo/src/main/kotlin/com/motorro/statemachine/skills/auth/demo/App.kt b/examples/skills/auth/demo/src/main/kotlin/com/motorro/statemachine/skills/auth/demo/App.kt index d986fbe..d96b7ed 100644 --- a/examples/skills/auth/demo/src/main/kotlin/com/motorro/statemachine/skills/auth/demo/App.kt +++ b/examples/skills/auth/demo/src/main/kotlin/com/motorro/statemachine/skills/auth/demo/App.kt @@ -1,11 +1,12 @@ package com.motorro.statemachine.skills.auth.demo import android.app.Application -import com.motorro.commonstatemachine.skills.domain.entity.PasswordRequirements +import com.motorro.commonstatemachine.skills.domain.authenticate.AuthenticateWithPassword +import com.motorro.commonstatemachine.skills.domain.authenticate.GetPasswordRequirements +import com.motorro.commonstatemachine.skills.domain.authenticate.data.PasswordRequirements import com.motorro.commonstatemachine.skills.domain.exception.AuthenticationException import com.motorro.commonstatemachine.skills.domain.exception.IOException -import com.motorro.commonstatemachine.skills.domain.usecase.AuthenticateWithPassword -import com.motorro.commonstatemachine.skills.domain.usecase.GetPasswordRequirements +import com.motorro.commonstatemachine.skills.domain.session.data.Username import com.motorro.statemachine.skills.auth.implementation.AuthModule import io.github.aakira.napier.DebugAntilog import io.github.aakira.napier.Napier @@ -35,7 +36,7 @@ class App : Application() { } @Module(includes = [AuthModule::class]) -@ComponentScan("com.motorro.statemachine.skills.auth.demo") +@ComponentScan class AppModule { @Single @@ -69,7 +70,7 @@ class AppModule { var attempt = 1 val tag = AuthenticateWithPassword::class.simpleName!! return object : AuthenticateWithPassword { - override suspend fun invoke(username: String, password: String) { + override suspend fun invoke(username: Username, password: String) { delay(2.seconds) when (attempt) { 1 -> { diff --git a/examples/skills/auth/demo/src/main/kotlin/com/motorro/statemachine/skills/auth/demo/MainActivity.kt b/examples/skills/auth/demo/src/main/kotlin/com/motorro/statemachine/skills/auth/demo/MainActivity.kt index 84efcd9..ba1da2e 100644 --- a/examples/skills/auth/demo/src/main/kotlin/com/motorro/statemachine/skills/auth/demo/MainActivity.kt +++ b/examples/skills/auth/demo/src/main/kotlin/com/motorro/statemachine/skills/auth/demo/MainActivity.kt @@ -5,8 +5,14 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import com.motorro.commonstatemachine.flow.viewmodel.CommonFlowComposition +import com.motorro.statemachine.auth.appcore.ui.design.SkillsAppBar import com.motorro.statemachine.auth.appcore.ui.theme.SkillsTheme import com.motorro.statemachine.skills.auth.api.AuthUiApi import io.github.aakira.napier.Napier @@ -38,7 +44,21 @@ fun MainScreen(onComplete: () -> Unit) { BackHandler(enabled, onBack) }, content = { state, onGesture -> - uiApi.Screen(state, onGesture) + Scaffold( + topBar = { + SkillsAppBar( + title = stringResource(R.string.app_name), + modifier = Modifier.fillMaxWidth(), + topLevel = true, + ) + } + ) { paddingValues -> + uiApi.Screen( + state, + onGesture, + Modifier.padding(paddingValues) + ) + } }, finish = { Napier.i { "Finished with: $it" } diff --git a/examples/skills/auth/implementation/build.gradle.kts b/examples/skills/auth/implementation/build.gradle.kts index be4f7ca..df827c3 100644 --- a/examples/skills/auth/implementation/build.gradle.kts +++ b/examples/skills/auth/implementation/build.gradle.kts @@ -34,6 +34,13 @@ version = versionName kotlin { jvmToolchain(21) + compilerOptions.freeCompilerArgs.addAll(listOf( + "-opt-in=kotlin.RequiresOptIn", + "-Xexpect-actual-classes", + "-Xexplicit-backing-fields", + "-Xcontext-parameters" + )) + android { namespace = "com.motorro.statemachine.skills.auth.implementation" compileSdk = androidCompileSdkVersion @@ -62,12 +69,12 @@ kotlin { implementation(libs.kotlin.coroutines.core) implementation(libs.composeMultiplatform.runtime) implementation(libs.composeMultiplatform.foundation) - implementation(libs.napier) implementation(libs.composeMultiplatform.resources) implementation(libs.composeMultiplatform.preview) implementation(project.dependencies.platform(libs.koin.bom)) implementation(libs.koin.core) implementation(libs.koin.annotations) + implementation(libs.napier) } commonTest.dependencies { implementation(libs.test.kotlin) diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/AuthModule.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/AuthModule.kt index 860a47f..e9bc9a2 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/AuthModule.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/AuthModule.kt @@ -4,5 +4,5 @@ import org.koin.core.annotation.ComponentScan import org.koin.core.annotation.Module @Module -@ComponentScan("com.motorro.statemachine.skills.auth.implementation") +@ComponentScan class AuthModule \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/AuthUiApiImpl.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/AuthUiApiImpl.kt index 01c3898..edd1a19 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/AuthUiApiImpl.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/AuthUiApiImpl.kt @@ -21,6 +21,7 @@ internal class AuthUiApiImpl : AuthUiApi { modifier: Modifier ) = AuthScreen( state = state as AuthUiStateImpl, + modifier = modifier, onGesture = onGesture ) } \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthStateData.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthStateData.kt index 33a6436..20420b8 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthStateData.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthStateData.kt @@ -1,6 +1,6 @@ package com.motorro.statemachine.skills.auth.implementation.data -import com.motorro.commonstatemachine.skills.domain.entity.PasswordRequirements +import com.motorro.commonstatemachine.skills.domain.authenticate.data.PasswordRequirements import com.motorro.statemachine.skills.auth.api.AuthInput /** diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticatingState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticatingState.kt index 1e735e0..c917eb5 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticatingState.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticatingState.kt @@ -1,7 +1,8 @@ package com.motorro.statemachine.skills.auth.implementation.state +import com.motorro.commonstatemachine.skills.domain.authenticate.AuthenticateWithPassword import com.motorro.commonstatemachine.skills.domain.exception.toAppException -import com.motorro.commonstatemachine.skills.domain.usecase.AuthenticateWithPassword +import com.motorro.commonstatemachine.skills.domain.session.data.Username import com.motorro.statemachine.skills.auth.api.AuthResult import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl import com.motorro.statemachine.skills.auth.implementation.data.AuthStateData @@ -50,7 +51,7 @@ internal class AuthenticatingState( */ private fun doAuthenticate() = stateScope.launch { try { - authenticate(data.username, data.password) + authenticate(Username(data.username), data.password) // Advance to the next state setMachineState { terminated(AuthResult(authenticated = true)) diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingState.kt index f27cf9d..cd5bfb4 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingState.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingState.kt @@ -1,7 +1,7 @@ package com.motorro.statemachine.skills.auth.implementation.state +import com.motorro.commonstatemachine.skills.domain.authenticate.GetPasswordRequirements import com.motorro.commonstatemachine.skills.domain.exception.toAppException -import com.motorro.commonstatemachine.skills.domain.usecase.GetPasswordRequirements import com.motorro.statemachine.skills.auth.api.AuthInput import com.motorro.statemachine.skills.auth.api.AuthResult import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthLoadingScreen.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthLoadingScreen.kt index 2923ffe..490839c 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthLoadingScreen.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthLoadingScreen.kt @@ -1,19 +1,15 @@ package com.motorro.statemachine.skills.auth.implementation.ui -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import com.motorro.statemachine.auth.appcore.ui.design.SkillsLoading import com.motorro.statemachine.auth.appcore.ui.preview.SkillsPreviewComposition @Composable internal fun AuthLoadingScreen(modifier: Modifier = Modifier) { - Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - CircularProgressIndicator() - } + SkillsLoading(modifier) } @Preview diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthScreen.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthScreen.kt index b50929d..57757b5 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthScreen.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthScreen.kt @@ -1,23 +1,15 @@ package com.motorro.statemachine.skills.auth.implementation.ui import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import com.motorro.statemachine.auth.appcore.ui.design.SkillsAppBar import com.motorro.statemachine.auth.appcore.ui.preview.SkillsPreviewComposition -import com.motorro.statemachine.skills.auth.implementation.Res import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl import com.motorro.statemachine.skills.auth.implementation.data.AuthUiStateImpl -import com.motorro.statemachine.skills.auth.implementation.title -import org.jetbrains.compose.resources.stringResource /** * Authentication screen @@ -28,35 +20,22 @@ import org.jetbrains.compose.resources.stringResource @Composable internal fun AuthScreen( state: AuthUiStateImpl, + modifier: Modifier = Modifier, onGesture: (AuthGestureImpl) -> Unit, ) { - val onBack = remember { - { onGesture(AuthGestureImpl.Back) } - } - Scaffold( - topBar = { - SkillsAppBar( - title = stringResource(Res.string.title), - modifier = Modifier.fillMaxWidth(), - topLevel = false, - onBack = onBack - ) - } - ) { paddingValues -> - val modifier = Modifier.fillMaxSize().padding(paddingValues) - when(state) { - AuthUiStateImpl.Loading -> AuthLoadingScreen(modifier) - is AuthUiStateImpl.Error -> AuthErrorScreen( - state = state, - modifier = modifier, - onGesture = onGesture - ) - is AuthUiStateImpl.Form -> AuthFormScreen( - state = state, - modifier = modifier, - onGesture = onGesture - ) - } + val modifier = modifier.fillMaxSize() + when(state) { + AuthUiStateImpl.Loading -> AuthLoadingScreen(modifier) + is AuthUiStateImpl.Error -> AuthErrorScreen( + state = state, + modifier = modifier, + onGesture = onGesture + ) + is AuthUiStateImpl.Form -> AuthFormScreen( + state = state, + modifier = modifier, + onGesture = onGesture + ) } } diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/dataStatesFixtures.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/dataStatesFixtures.kt index f479a7b..3345a14 100644 --- a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/dataStatesFixtures.kt +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/dataStatesFixtures.kt @@ -1,6 +1,6 @@ package com.motorro.statemachine.skills.auth.implementation.data -import com.motorro.commonstatemachine.skills.domain.entity.PasswordRequirements +import com.motorro.commonstatemachine.skills.domain.authenticate.data.PasswordRequirements import com.motorro.commonstatemachine.skills.domain.exception.IOException import com.motorro.commonstatemachine.skills.domain.exception.UnknownException import com.motorro.statemachine.skills.auth.api.AuthInput diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticatingStateTest.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticatingStateTest.kt index 98ee331..5113873 100644 --- a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticatingStateTest.kt +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticatingStateTest.kt @@ -1,6 +1,6 @@ package com.motorro.statemachine.skills.auth.implementation.state -import com.motorro.commonstatemachine.skills.domain.usecase.AuthenticateWithPassword +import com.motorro.commonstatemachine.skills.domain.authenticate.AuthenticateWithPassword import com.motorro.statemachine.skills.auth.api.AuthResult import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl import com.motorro.statemachine.skills.auth.implementation.data.FATAL_ERROR diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingStateTest.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingStateTest.kt index d25b6e2..e7b0d73 100644 --- a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingStateTest.kt +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingStateTest.kt @@ -1,6 +1,6 @@ package com.motorro.statemachine.skills.auth.implementation.state -import com.motorro.commonstatemachine.skills.domain.usecase.GetPasswordRequirements +import com.motorro.commonstatemachine.skills.domain.authenticate.GetPasswordRequirements import com.motorro.statemachine.skills.auth.api.AuthResult import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl import com.motorro.statemachine.skills.auth.implementation.data.FATAL_ERROR diff --git a/examples/skills/domain/build.gradle.kts b/examples/skills/domain/build.gradle.kts index f258a2a..7e4c6be 100644 --- a/examples/skills/domain/build.gradle.kts +++ b/examples/skills/domain/build.gradle.kts @@ -30,6 +30,13 @@ version = versionName kotlin { jvmToolchain(21) + compilerOptions.freeCompilerArgs.addAll(listOf( + "-opt-in=kotlin.RequiresOptIn", + "-Xexpect-actual-classes", + "-Xexplicit-backing-fields", + "-Xcontext-parameters" + )) + android { namespace = "com.motorro.statemachine.skills.domain" compileSdk = androidCompileSdkVersion @@ -42,7 +49,7 @@ kotlin { sourceSets { commonMain.dependencies { - implementation(libs.kotlin.coroutines.core) + api(libs.kotlin.coroutines.core) } } } diff --git a/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/usecase/AuthenticateWithPassword.kt b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/authenticate/AuthenticateWithPassword.kt similarity index 50% rename from examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/usecase/AuthenticateWithPassword.kt rename to examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/authenticate/AuthenticateWithPassword.kt index 9edeae0..1a58d49 100644 --- a/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/usecase/AuthenticateWithPassword.kt +++ b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/authenticate/AuthenticateWithPassword.kt @@ -1,4 +1,6 @@ -package com.motorro.commonstatemachine.skills.domain.usecase +package com.motorro.commonstatemachine.skills.domain.authenticate + +import com.motorro.commonstatemachine.skills.domain.session.data.Username /** * Authenticates with a password @@ -9,5 +11,5 @@ interface AuthenticateWithPassword { * @param username Username * @param password Password */ - suspend operator fun invoke(username: String, password: String) + suspend operator fun invoke(username: Username, password: String) } \ No newline at end of file diff --git a/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/usecase/GetPasswordRequirements.kt b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/authenticate/GetPasswordRequirements.kt similarity index 54% rename from examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/usecase/GetPasswordRequirements.kt rename to examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/authenticate/GetPasswordRequirements.kt index beafa49..cfc66ff 100644 --- a/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/usecase/GetPasswordRequirements.kt +++ b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/authenticate/GetPasswordRequirements.kt @@ -1,6 +1,6 @@ -package com.motorro.commonstatemachine.skills.domain.usecase +package com.motorro.commonstatemachine.skills.domain.authenticate -import com.motorro.commonstatemachine.skills.domain.entity.PasswordRequirements +import com.motorro.commonstatemachine.skills.domain.authenticate.data.PasswordRequirements /** * Loads password requirements diff --git a/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/entity/PasswordRequirements.kt b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/authenticate/data/PasswordRequirements.kt similarity index 77% rename from examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/entity/PasswordRequirements.kt rename to examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/authenticate/data/PasswordRequirements.kt index 8c24bf2..01bb73a 100644 --- a/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/entity/PasswordRequirements.kt +++ b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/authenticate/data/PasswordRequirements.kt @@ -1,4 +1,4 @@ -package com.motorro.commonstatemachine.skills.domain.entity +package com.motorro.commonstatemachine.skills.domain.authenticate.data /** * Password requirements diff --git a/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/friends/CreateFriend.kt b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/friends/CreateFriend.kt new file mode 100644 index 0000000..f8bbe80 --- /dev/null +++ b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/friends/CreateFriend.kt @@ -0,0 +1,10 @@ +package com.motorro.commonstatemachine.skills.domain.friends + +import com.motorro.commonstatemachine.skills.domain.friends.data.FriendData + +/** + * Creates a new friend + */ +interface CreateFriend { + suspend operator fun invoke(data: FriendData) +} \ No newline at end of file diff --git a/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/friends/GetFriendList.kt b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/friends/GetFriendList.kt new file mode 100644 index 0000000..700c56a --- /dev/null +++ b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/friends/GetFriendList.kt @@ -0,0 +1,10 @@ +package com.motorro.commonstatemachine.skills.domain.friends + +import com.motorro.commonstatemachine.skills.domain.friends.data.Friend + +/** + * Retrieves a friend list + */ +interface GetFriendList { + suspend operator fun invoke(): List +} \ No newline at end of file diff --git a/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/friends/data/Friend.kt b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/friends/data/Friend.kt new file mode 100644 index 0000000..dae8b90 --- /dev/null +++ b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/friends/data/Friend.kt @@ -0,0 +1,3 @@ +package com.motorro.commonstatemachine.skills.domain.friends.data + +data class Friend(val id: Int, val data: FriendData) \ No newline at end of file diff --git a/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/friends/data/FriendData.kt b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/friends/data/FriendData.kt new file mode 100644 index 0000000..da8cb5d --- /dev/null +++ b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/friends/data/FriendData.kt @@ -0,0 +1,9 @@ +package com.motorro.commonstatemachine.skills.domain.friends.data + +/** + * Friend properties + */ +data class FriendData( + val name: String, + val age: Int +) \ No newline at end of file diff --git a/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/session/SessionManager.kt b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/session/SessionManager.kt new file mode 100644 index 0000000..76f8b7b --- /dev/null +++ b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/session/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.commonstatemachine.skills.domain.session + +import com.motorro.commonstatemachine.skills.domain.session.data.Session +import kotlinx.coroutines.flow.Flow + +/** + * Session manager + */ +interface SessionManager { + /** + * Session state + */ + val session: Flow + + /** + * Updates session state + * @param session New session state + */ + suspend fun update(session: Session) +} \ No newline at end of file diff --git a/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/session/data/Session.kt b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/session/data/Session.kt new file mode 100644 index 0000000..29f49fd --- /dev/null +++ b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/session/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.commonstatemachine.skills.domain.session.data + +/** + * Session state + */ +sealed class Session { + /** + * No session + */ + data object None : Session() + + /** + * Session is active + */ + data class Active(val username: Username) : Session() +} \ No newline at end of file diff --git a/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/session/data/Username.kt b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/session/data/Username.kt new file mode 100644 index 0000000..a3471c3 --- /dev/null +++ b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/session/data/Username.kt @@ -0,0 +1,4 @@ +package com.motorro.commonstatemachine.skills.domain.session.data + +@JvmInline +value class Username(val value: String) \ No newline at end of file diff --git a/examples/skills/usecase/build.gradle.kts b/examples/skills/usecase/build.gradle.kts new file mode 100644 index 0000000..76da18d --- /dev/null +++ b/examples/skills/usecase/build.gradle.kts @@ -0,0 +1,61 @@ +@file:Suppress("unused") + +/* +* Copyright 2023 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. +*/ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.kotlin.multiplatform.library) + alias(libs.plugins.koin) +} + +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 = "com.motorro" +version = versionName + +kotlin { + jvmToolchain(21) + + compilerOptions.freeCompilerArgs.addAll(listOf( + "-opt-in=kotlin.RequiresOptIn", + "-Xexpect-actual-classes", + "-Xexplicit-backing-fields", + "-Xcontext-parameters" + )) + + android { + namespace = "com.motorro.statemachine.skills.usecase" + compileSdk = androidCompileSdkVersion + minSdk = androidMinSdkVersion + + compilerOptions { + jvmTarget.set(JvmTarget.JVM_21) + } + } + + sourceSets { + commonMain.dependencies { + implementation(project(":examples:skills:domain")) + implementation(project.dependencies.platform(libs.koin.bom)) + implementation(libs.koin.core) + implementation(libs.koin.annotations) + implementation(libs.napier) + } + } +} + diff --git a/examples/skills/usecase/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/usecase/Fixtures.kt b/examples/skills/usecase/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/usecase/Fixtures.kt new file mode 100644 index 0000000..1557dae --- /dev/null +++ b/examples/skills/usecase/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/usecase/Fixtures.kt @@ -0,0 +1,21 @@ +package com.motorro.commonstatemachine.skills.usecase + +import com.motorro.commonstatemachine.skills.domain.authenticate.data.PasswordRequirements +import com.motorro.commonstatemachine.skills.domain.session.data.Username +import kotlin.time.Duration.Companion.seconds + +internal object Fixtures { + + val NETWORK_DELAY = 2.seconds + + object Authentication { + val REQUIREMENTS = PasswordRequirements( + regex = "^.{8,}$".toRegex(), + description = "Minimum eight characters" + ) + + val USERNAME = Username("user") + + const val PASSWORD = "password" + } +} diff --git a/examples/skills/usecase/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/usecase/UsecaseModule.kt b/examples/skills/usecase/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/usecase/UsecaseModule.kt new file mode 100644 index 0000000..df461a0 --- /dev/null +++ b/examples/skills/usecase/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/usecase/UsecaseModule.kt @@ -0,0 +1,8 @@ +package com.motorro.commonstatemachine.skills.usecase + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +class UsecaseModule \ No newline at end of file diff --git a/examples/skills/usecase/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/usecase/authenticate/AuthenticateWithPasswordImpl.kt b/examples/skills/usecase/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/usecase/authenticate/AuthenticateWithPasswordImpl.kt new file mode 100644 index 0000000..b91f9a1 --- /dev/null +++ b/examples/skills/usecase/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/usecase/authenticate/AuthenticateWithPasswordImpl.kt @@ -0,0 +1,26 @@ +package com.motorro.commonstatemachine.skills.usecase.authenticate + +import com.motorro.commonstatemachine.skills.domain.authenticate.AuthenticateWithPassword +import com.motorro.commonstatemachine.skills.domain.exception.AuthenticationException +import com.motorro.commonstatemachine.skills.domain.session.SessionManager +import com.motorro.commonstatemachine.skills.domain.session.data.Session +import com.motorro.commonstatemachine.skills.domain.session.data.Username +import com.motorro.commonstatemachine.skills.usecase.Fixtures.Authentication +import com.motorro.commonstatemachine.skills.usecase.Fixtures.NETWORK_DELAY +import io.github.aakira.napier.Napier +import kotlinx.coroutines.delay +import org.koin.core.annotation.Factory + +@Factory +internal class AuthenticateWithPasswordImpl(private val sessionManager: SessionManager) : AuthenticateWithPassword { + override suspend fun invoke(username: Username, password: String) { + Napier.i { "Authenticating: ${username.value}..." } + delay(NETWORK_DELAY) + if (Authentication.USERNAME == username && Authentication.PASSWORD == password) { + sessionManager.update(Session.Active(username)) + } else { + Napier.w { "Authentication failed" } + throw AuthenticationException("Invalid login or password") + } + } +} \ No newline at end of file diff --git a/examples/skills/usecase/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/usecase/authenticate/GetPasswordRequirementsImpl.kt b/examples/skills/usecase/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/usecase/authenticate/GetPasswordRequirementsImpl.kt new file mode 100644 index 0000000..7b82ff6 --- /dev/null +++ b/examples/skills/usecase/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/usecase/authenticate/GetPasswordRequirementsImpl.kt @@ -0,0 +1,16 @@ +package com.motorro.commonstatemachine.skills.usecase.authenticate + +import com.motorro.commonstatemachine.skills.domain.authenticate.GetPasswordRequirements +import com.motorro.commonstatemachine.skills.domain.authenticate.data.PasswordRequirements +import com.motorro.commonstatemachine.skills.usecase.Fixtures +import com.motorro.commonstatemachine.skills.usecase.Fixtures.NETWORK_DELAY +import kotlinx.coroutines.delay +import org.koin.core.annotation.Factory + +@Factory +internal class GetPasswordRequirementsImpl : GetPasswordRequirements { + override suspend fun invoke(): PasswordRequirements { + delay(NETWORK_DELAY) + return Fixtures.Authentication.REQUIREMENTS + } +} \ No newline at end of file diff --git a/examples/skills/usecase/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/usecase/session/SessionManagerImpl.kt b/examples/skills/usecase/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/usecase/session/SessionManagerImpl.kt new file mode 100644 index 0000000..b066ab6 --- /dev/null +++ b/examples/skills/usecase/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/usecase/session/SessionManagerImpl.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.skills.usecase.session + +import com.motorro.commonstatemachine.skills.domain.session.SessionManager +import com.motorro.commonstatemachine.skills.domain.session.data.Session +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import org.koin.core.annotation.Single + +@Single +internal class SessionManagerImpl() : SessionManager { + override val session: Flow field = MutableStateFlow(Session.None) + + override suspend fun update(session: Session) { + this.session.emit(session) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index f52b1ad..066b873 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -73,7 +73,9 @@ include( ":examples:books:book", ":examples:books:book:demo", ":examples:books:app", + ":examples:skills:app", ":examples:skills:domain", + ":examples:skills:usecase", ":examples:skills:appcore", ":examples:skills:auth:api", ":examples:skills:auth:implementation", diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md b/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md index e1ef42b..9c6e754 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md @@ -125,3 +125,8 @@ To be able to use the created implementation, follow [this guide](references/di- - Data and UI API implementations - DI Modules (optional) + +## Step 7. Proxy creation (optional) +If the user wants to integrate the child flow to one or several parent flows, +implement the proxy machine states for the hosting flows. +Read [this guide](references/proxy-implementation.md) for more information. diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app new file mode 120000 index 0000000..209d9c3 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app @@ -0,0 +1 @@ +../../../../../../examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-api.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-api.md index 5f6498f..41148fa 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-api.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-api.md @@ -12,13 +12,17 @@ The [Common Child Flow](https://github.com/motorro/CommonStateMachine/tree/maste The API implementation is then created in the implementation module and could be injected into the composition using locals or any other way. ## Step 1. Data flow -Take the created [Gestures and UI-states](gesture-and-ui-state-api.md), [input and result](input-and-result-api.md) and create an interface or a typealias -to bind the generics of the `com.motorro.commonstatemachine.flow.data.CommonFlowDataApi`. This API would be available to the flow consumer to start the feature flow. +Take the created [Gestures and UI-states](gesture-and-ui-state-api.md), [input and result](input-and-result-api.md) and create an interface to bind the generics +of the `com.motorro.commonstatemachine.flow.data.CommonFlowDataApi`. This API would be available to the flow consumer to start the feature flow. + Take a look at the [class diagram](common-child-flow.puml) for more details. + Take a look at the [example](../assets/example/api/AuthDataApi.kt) - data API definition ## Step 2. UI flow -Take the created [Gestures and UI-states](gesture-and-ui-state-api.md) and create an interface or a typealias -to bind the generics of the `com.motorro.commonstatemachine.flow.compose.CommonFlowUiApi`. This API would be available to the flow consumer to display the feature. +Take the created [Gestures and UI-states](gesture-and-ui-state-api.md) and create an interface to bind the generics +of the `com.motorro.commonstatemachine.flow.compose.CommonFlowUiApi`. This API would be available to the flow consumer to display the feature. + Take a look at the [class diagram](common-child-flow.puml) for more details. + Take a look at the [example](../assets/example/api/AuthUiApi.kt) - UI API definition diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/proxy-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/proxy-implementation.md new file mode 100644 index 0000000..78266ef --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/proxy-implementation.md @@ -0,0 +1,44 @@ +# Module implementation: flow proxy +The common-child-flow module is intended to be used inside a hosting proxy state. +If the user asks you to integrate the feature-flow into a parent/hosting flow (e.g., an application main flow or onboarding flow), you must create a `ProxyMachineState` in the parent module. + +For a deeper dive into the state machine mechanics and common components, refer to the [Core Architecture](ARCHITECTURE.md). +For a deeper dive into the common child flow philosophy and design, refer to the [Common Child Flow Architecture](COMMON-CHILD-FLOW.md). + +## Step 1. Preparing the host flow (Gesture & UI-State Adapters) +To run a child feature flow inside a parent state machine, the parent's Gesture and UI-state systems must be extended to wrap the child's gestures and UI states. + +- Proxy Gesture: In the parent's sealed Gesture class/structure, add a wrapper case containing the child's feature gesture. + Example: `data class Auth(val child: AuthGesture) : MainGesture()`. + See the [example gesture](../assets/example/app/data/MainGesture.kt). +- Proxy UI-State: In the parent's sealed UI-state class/structure, add a wrapper case containing the child's feature UI-state. + Example: `data class Auth(val child: AuthUiState) : MainUiState()`. + See the [example UI state](../assets/example/app/data/MainUiState.kt). + +## Step 2. Implement parent proxy state +Create a `ProxyMachineState` subclass in the parent module that encapsulates the child state machine flow. + +The proxy state must: + +- Extend `ProxyMachineState`. Pass `api.getDefaultUiState()` as the initial UI state to the super constructor. +- Implement `CommonFlowHost`. Create a `flowHost` instance that handles the child flow completion. When `onComplete(result)` is called + by the child, update the parent machine state using `setMachineState(...)` to transition to the next parent state based on the result. +- Override `init()`. Call `api.init(flowHost, input)` to create and return the child state machine's starting state. +- Override `mapGesture(parent: ParentGesture)`. Unwraps the child gesture from the parent proxy gesture wrapper (e.g., `MainGesture.Auth -> parent.child`). + Additionally, map system-level parent gestures (such as `MainGesture.Back`) to the child's back gesture (`api.getBackGesture()`) if appropriate. +- Override `mapUiState(child: ChildUiState)`. Wraps the child's UI state into the parent's UI state wrapper (e.g., `MainUiState.Auth(child)`). + +See the [example proxy state](../assets/example/app/state/AuthProxyState.kt) for a complete implementation. + +## Step 3. Create a parent state-factory method & DI setup +To instantiate the proxy state cleanly within the parent flow: + +- Local Proxy Factory: Create an inner factory class or separate factory class for the proxy state, named `StateFactory` (e.g., `@Factory class StateFactory(private val api: AuthDataApi)`). + This factory should inject the child feature's `DataApi` (e.g., `AuthDataApi`) and instantiate `AuthProxyState(context, ..., api)`. +- Parent State Factory Interface: Add a method in the parent's `StateFactory` interface to produce the proxy state (e.g., `fun auth(): MainState`). +- Parent State Factory Implementation: Inject the proxy state factory into the parent `StateFactory` implementation, and delegate + the state creation call to it. +- DI Registration: Ensure the proxy state factory and parent state factory implementation are annotated + for DI (e.g., `@Factory` for Koin, `@Inject` / `@ViewModelScoped` for Hilt). + +See the [example main state factory](../assets/example/app/state/MainStateFactory.kt) and [proxy state factory](../assets/example/app/state/AuthProxyState.kt). diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/state-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/state-implementation.md index 29a8173..1308d91 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/state-implementation.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/state-implementation.md @@ -53,7 +53,7 @@ own state factories put to the DI graph. This lifts a burden of state creation m the main state factory implementation. Create such a factory for the startup state: -- Implement as a child class with the name `Factory`. +- Implement as a child class with the name `StateFactory`. - If required by the DI framework used in project, add required inject annotations. - Inject external dependencies required by the state to the factory constructor. - Add a single `create` method that will accept the [context](renderer-factory-context-interface-implementation.md#step-4-state-context) and all the required interstate parameters that will come from the main factory. From 90517b595f8cfbc6e8af067522b9d72d49f70055 Mon Sep 17 00:00:00 2001 From: motorro Date: Wed, 23 Sep 2026 13:23:40 +0200 Subject: [PATCH 13/32] Content state stub Refs: #14 --- .../skills/app/state/ContentState.kt | 31 +++++-------------- .../skills/domain/friends/CreateFriend.kt | 10 ------ .../skills/domain/friends/GetFriendList.kt | 3 +- 3 files changed, 9 insertions(+), 35 deletions(-) delete mode 100644 examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/friends/CreateFriend.kt diff --git a/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/state/ContentState.kt b/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/state/ContentState.kt index 4d738de..5fd5ab5 100644 --- a/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/state/ContentState.kt +++ b/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/state/ContentState.kt @@ -13,34 +13,18 @@ package com.motorro.statemachine.skills.app.state -import com.motorro.commonstatemachine.skills.domain.session.SessionManager -import com.motorro.commonstatemachine.skills.domain.session.data.Session import com.motorro.statemachine.skills.app.data.MainGesture import io.github.aakira.napier.Napier -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import org.koin.core.annotation.Factory -internal class ContentState(context: MainContext, private val sessionManager: SessionManager) : BaseMainState(context) { +/** + * Content stub + */ +internal class ContentState(context: MainContext) : BaseMainState(context) { override fun doStart() { - subscribeSession() + setUiState(renderer.renderContent()) } - private fun subscribeSession() = sessionManager - .session - .catch { emit(Session.None) } - .onEach { - when(it) { - is Session.None -> { - Napier.d("No session. Switching to Auth...") - setMachineState(factory.auth()) - } - is Session.Active -> setUiState(renderer.renderContent()) - } - } - .launchIn(stateScope) - override fun doProcess(gesture: MainGesture) { when (gesture) { MainGesture.Back -> { @@ -52,10 +36,9 @@ internal class ContentState(context: MainContext, private val sessionManager: Se } @Factory - class StateFactory(private val sessionManager: SessionManager) { + class StateFactory() { fun create(context: MainContext) = ContentState( - context, - sessionManager + context ) } } \ No newline at end of file diff --git a/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/friends/CreateFriend.kt b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/friends/CreateFriend.kt deleted file mode 100644 index f8bbe80..0000000 --- a/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/friends/CreateFriend.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.motorro.commonstatemachine.skills.domain.friends - -import com.motorro.commonstatemachine.skills.domain.friends.data.FriendData - -/** - * Creates a new friend - */ -interface CreateFriend { - suspend operator fun invoke(data: FriendData) -} \ No newline at end of file diff --git a/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/friends/GetFriendList.kt b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/friends/GetFriendList.kt index 700c56a..1a19efd 100644 --- a/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/friends/GetFriendList.kt +++ b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/friends/GetFriendList.kt @@ -1,10 +1,11 @@ package com.motorro.commonstatemachine.skills.domain.friends import com.motorro.commonstatemachine.skills.domain.friends.data.Friend +import kotlinx.coroutines.flow.Flow /** * Retrieves a friend list */ interface GetFriendList { - suspend operator fun invoke(): List + operator fun invoke(): Flow> } \ No newline at end of file From 839a35d6bf1aa36ed248f3c09b0443c9e9d2354e Mon Sep 17 00:00:00 2001 From: motorro Date: Wed, 23 Sep 2026 13:36:12 +0200 Subject: [PATCH 14/32] Friends stub Refs: #14 --- .../skills/usecase/Fixtures.kt | 11 ++++++++++ .../usecase/friends/GetFriendListImpl.kt | 20 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 examples/skills/usecase/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/usecase/friends/GetFriendListImpl.kt diff --git a/examples/skills/usecase/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/usecase/Fixtures.kt b/examples/skills/usecase/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/usecase/Fixtures.kt index 1557dae..2ed4ece 100644 --- a/examples/skills/usecase/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/usecase/Fixtures.kt +++ b/examples/skills/usecase/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/usecase/Fixtures.kt @@ -1,6 +1,8 @@ package com.motorro.commonstatemachine.skills.usecase import com.motorro.commonstatemachine.skills.domain.authenticate.data.PasswordRequirements +import com.motorro.commonstatemachine.skills.domain.friends.data.Friend +import com.motorro.commonstatemachine.skills.domain.friends.data.FriendData import com.motorro.commonstatemachine.skills.domain.session.data.Username import kotlin.time.Duration.Companion.seconds @@ -18,4 +20,13 @@ internal object Fixtures { const val PASSWORD = "password" } + + val FRIENDS = listOf( + Friend(1, FriendData("Alice", 28)), + Friend(2, FriendData("Bob", 32)), + Friend(3, FriendData("Charlie", 25)), + Friend(4, FriendData("Diana", 30)), + Friend(5, FriendData("Evan", 22)) + ) } + diff --git a/examples/skills/usecase/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/usecase/friends/GetFriendListImpl.kt b/examples/skills/usecase/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/usecase/friends/GetFriendListImpl.kt new file mode 100644 index 0000000..d9ed656 --- /dev/null +++ b/examples/skills/usecase/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/usecase/friends/GetFriendListImpl.kt @@ -0,0 +1,20 @@ +package com.motorro.commonstatemachine.skills.usecase.friends + +import com.motorro.commonstatemachine.skills.domain.friends.GetFriendList +import com.motorro.commonstatemachine.skills.domain.friends.data.Friend +import com.motorro.commonstatemachine.skills.usecase.Fixtures +import com.motorro.commonstatemachine.skills.usecase.Fixtures.NETWORK_DELAY +import io.github.aakira.napier.Napier +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import org.koin.core.annotation.Factory + +@Factory +internal class GetFriendListImpl : GetFriendList { + override fun invoke(): Flow> = flow { + Napier.i { "Loading friend list..." } + delay(NETWORK_DELAY) + emit(Fixtures.FRIENDS) + } +} \ No newline at end of file From 6e77f61ac30c9b4a2e0f049d810fe2cbc22c9d5b Mon Sep 17 00:00:00 2001 From: motorro Date: Wed, 23 Sep 2026 13:49:41 +0200 Subject: [PATCH 15/32] Boilerplate skill example Refs: #14 --- skills/README.md | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/skills/README.md b/skills/README.md index 96ead4c..6f238d6 100644 --- a/skills/README.md +++ b/skills/README.md @@ -1,10 +1,26 @@ # AI skills Some AI skills for your convenience: -- [Create a Common Child Flow module boilerplate](commonstatemachine/common-child-flow/boilerplate/SKILL.md): creates a set of API/implementation - feature module boilerplate following the [Common Child Flow](../README.md#common-child-flow-api). +## Create a Common Child Flow module boilerplate +[Module boilerplate skill](commonstatemachine/common-child-flow/boilerplate/SKILL.md): creates a set of API/implementation +feature module boilerplate following the [Common Child Flow](../README.md#common-child-flow-api). - Defines gesture/ui-states sealed structures - Binds data and UI APIs - Builds a basic tool set for the module implementation - Creates a test structure and mocks for the module implementation - - Builds a DI container for the module implementation \ No newline at end of file + - Builds a DI container for the module implementation + +For example, to try the skill with the [example project](../examples/skills), try creating a module to display a list +of friends as soon as the user is authenticated: + +> Create a `friendlist` feature module that loads and displays a list of friends. +> - Use `com.motorro.commonstatemachine.skills.domain.friends.GetFriendList` as the data source, staying subscribed +> to updates after the first load. +> - Show a loading state until the first emission, then the friend list. +> - On failure, check the error: offer a retry for an I/O error, otherwise terminate the flow. +> - Map errors to the domain at the usecase boundary with `toAppException` from +> `com.motorro.commonstatemachine.skills.domain.exception`. +> - Logical states: `list` (with the subscription) and `error`. +> - Views: list, loading, error — built with the shared theme, dimensions and composables from the `appcore` module. +> - Wire up DI modules for the data and UI APIs, add state tests, and create proxies (gesture, UI-state, module flow) +> to embed the flow in the main application module. \ No newline at end of file From fed643e3ec691a459507107dd82df351d22f3206 Mon Sep 17 00:00:00 2001 From: motorro Date: Wed, 23 Sep 2026 16:57:30 +0200 Subject: [PATCH 16/32] Module conflict fix Refs: #14 --- examples/skills/appcore/build.gradle.kts | 3 --- examples/skills/auth/api/build.gradle.kts | 3 --- examples/skills/auth/implementation/build.gradle.kts | 3 --- examples/skills/domain/build.gradle.kts | 3 --- examples/skills/usecase/build.gradle.kts | 3 --- settings.gradle.kts | 2 ++ 6 files changed, 2 insertions(+), 15 deletions(-) diff --git a/examples/skills/appcore/build.gradle.kts b/examples/skills/appcore/build.gradle.kts index eb6d154..37eb812 100644 --- a/examples/skills/appcore/build.gradle.kts +++ b/examples/skills/appcore/build.gradle.kts @@ -26,9 +26,6 @@ val androidMinSdkVersion: Int by project.extra val androidTargetSdkVersion: Int by project.extra val androidCompileSdkVersion: Int by project.extra -group = "com.motorro" -version = versionName - kotlin { jvmToolchain(21) diff --git a/examples/skills/auth/api/build.gradle.kts b/examples/skills/auth/api/build.gradle.kts index 6f87376..9cbb99c 100644 --- a/examples/skills/auth/api/build.gradle.kts +++ b/examples/skills/auth/api/build.gradle.kts @@ -26,9 +26,6 @@ val androidMinSdkVersion: Int by project.extra val androidTargetSdkVersion: Int by project.extra val androidCompileSdkVersion: Int by project.extra -group = "com.motorro" -version = versionName - kotlin { jvmToolchain(21) diff --git a/examples/skills/auth/implementation/build.gradle.kts b/examples/skills/auth/implementation/build.gradle.kts index df827c3..571e14c 100644 --- a/examples/skills/auth/implementation/build.gradle.kts +++ b/examples/skills/auth/implementation/build.gradle.kts @@ -28,9 +28,6 @@ val androidMinSdkVersion: Int by project.extra val androidTargetSdkVersion: Int by project.extra val androidCompileSdkVersion: Int by project.extra -group = "com.motorro" -version = versionName - kotlin { jvmToolchain(21) diff --git a/examples/skills/domain/build.gradle.kts b/examples/skills/domain/build.gradle.kts index 7e4c6be..a47947e 100644 --- a/examples/skills/domain/build.gradle.kts +++ b/examples/skills/domain/build.gradle.kts @@ -24,9 +24,6 @@ val androidMinSdkVersion: Int by project.extra val androidTargetSdkVersion: Int by project.extra val androidCompileSdkVersion: Int by project.extra -group = "com.motorro" -version = versionName - kotlin { jvmToolchain(21) diff --git a/examples/skills/usecase/build.gradle.kts b/examples/skills/usecase/build.gradle.kts index 76da18d..bb0e57f 100644 --- a/examples/skills/usecase/build.gradle.kts +++ b/examples/skills/usecase/build.gradle.kts @@ -25,9 +25,6 @@ val androidMinSdkVersion: Int by project.extra val androidTargetSdkVersion: Int by project.extra val androidCompileSdkVersion: Int by project.extra -group = "com.motorro" -version = versionName - kotlin { jvmToolchain(21) diff --git a/settings.gradle.kts b/settings.gradle.kts index 066b873..48d5256 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -86,3 +86,5 @@ include( + + From 0776b8fe826603d3e53cfa6ea3a61122c9c8c1f9 Mon Sep 17 00:00:00 2001 From: motorro Date: Wed, 23 Sep 2026 16:57:50 +0200 Subject: [PATCH 17/32] Building skill fixed Refs: #14 --- skills/build_skills.sh | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/skills/build_skills.sh b/skills/build_skills.sh index 28a4b3c..5415266 100644 --- a/skills/build_skills.sh +++ b/skills/build_skills.sh @@ -1,27 +1,43 @@ #!/bin/bash set -e +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + # Directory to store zip files, defaults to "artifacts" if not provided ARTIFACTS_DIR="${1:-$(pwd)/../artifacts}" -echo "Building skills to $ARTIFACTS_DIR..." -mkdir -p "$ARTIFACTS_DIR" - # Ensure ARTIFACTS_DIR is an absolute path if [[ ! "$ARTIFACTS_DIR" = /* ]]; then ARTIFACTS_DIR="$(pwd)/$ARTIFACTS_DIR" fi +echo "Building skills to $ARTIFACTS_DIR..." +mkdir -p "$ARTIFACTS_DIR" + find . -path '*/.*' -prune -o -name "SKILL.md" -print | while read -r skill_file; do skill_dir=$(dirname "$skill_file") + rel_dir="${skill_dir#./}" + dir_name=$(basename "$skill_dir") + parent_dir=$(basename "$(dirname "$skill_dir")") + skill_name="skill-${parent_dir}-${dir_name}" + + zip_name="${skill_name}.zip" + echo "Zipping $rel_dir to $zip_name" + + tmp_stage=$(mktemp -d) + mkdir -p "$tmp_stage/$rel_dir" + if command -v rsync >/dev/null 2>&1; then + rsync -aL "$skill_dir/" "$tmp_stage/$rel_dir/" + else + cp -RL "$skill_dir/." "$tmp_stage/$rel_dir/" + fi + + echo "TEMP: $tmp_stage" - # Get relative path from skills directory - rel_path=${skill_dir#./} - # Replace slashes with underscores - zip_name="skill_${rel_path//\//_}.zip" - echo "Zipping $skill_dir to $zip_name" + (cd "$tmp_stage" && zip -q -r "$ARTIFACTS_DIR/$zip_name" "$rel_dir" -x "*.DS_Store") - (cd "$skill_dir" && zip -r "$ARTIFACTS_DIR/$zip_name" . -x "*.DS_Store") + rm -rf "$tmp_stage" done echo "Skills build complete." From 0a7417fc5a505ff53899e7996cd1b77625e815e2 Mon Sep 17 00:00:00 2001 From: motorro Date: Fri, 25 Sep 2026 16:07:10 +0200 Subject: [PATCH 18/32] Convention plugins Refs: #15 --- .github/workflows/check.yml | 2 +- .run/Check All.run.xml | 39 +++ AGENTS.md | 54 ++++ README.md | 233 +++++++++--------- build-logic/convention/build.gradle.kts | 74 ++++++ .../convention}/consumer-rules.pro | 0 build-logic/convention/proguard-rules.pro | 0 .../main/kotlin/AndroidApplicationPlugin.kt | 95 +++++++ .../kotlin/AndroidComposeLibraryPlugin.kt | 40 +++ .../kotlin/AndroidDynamicFeaturePlugin.kt | 43 ++++ .../src/main/kotlin/AndroidLibraryPlugin.kt | 58 +++++ .../src/main/kotlin/AndroidTestPlugin.kt | 35 +++ .../src/main/kotlin/BaseLibraryPlugin.kt | 21 ++ .../convention/src/main/kotlin/CmpPlugin.kt | 57 +++++ build-logic/convention/src/main/kotlin/Jvm.kt | 8 + .../convention/src/main/kotlin/JvmPlugin.kt | 17 ++ .../convention/src/main/kotlin/KmpPlugin.kt | 98 ++++++++ .../convention/src/main/kotlin/Kotlin.kt | 13 + .../src/main/kotlin/PublishingPlugin.kt | 77 ++++++ .../convention/configureCommonAndroid.kt | 25 ++ .../gradle/convention/configureKotlin.kt | 33 +++ .../configureMultiplatformAndroidLibrary.kt | 9 + .../motorro/gradle/convention/dependencies.kt | 37 +++ .../com/motorro/gradle/convention/library.kt | 18 ++ .../com/motorro/gradle/convention/naming.kt | 23 ++ .../motorro/gradle/convention/properties.kt | 57 +++++ .../src/main/kotlin/publishingProperties.kt | 30 +++ build-logic/settings.gradle.kts | 23 ++ build.gradle.kts | 27 +- buildSrc/build.gradle.kts | 9 + buildSrc/settings.gradle.kts | 14 ++ .../motorro/gradle/convention/Versioning.kt | 134 ++++++++++ .../api/android/commonflow-compose.api | 8 + .../compose/api/commonflow-compose.klib.api | 12 + commonflow/compose/build.gradle.kts | 142 +---------- commonflow/compose/gradle.properties | 16 ++ .../data/api/android/commonflow-data.api | 14 ++ commonflow/data/build.gradle.kts | 137 +--------- commonflow/data/gradle.properties | 16 ++ .../api/android/commonflow-viewmodel.api | 82 ++++++ .../api/commonflow-viewmodel.klib.api | 10 + commonflow/viewmodel/build.gradle.kts | 144 +---------- commonflow/viewmodel/gradle.properties | 16 ++ .../viewmodel/ViewModelComposition_android.kt | 16 +- .../api/android/commonstatemachine.api | 187 ++++++++++++++ .../api/commonstatemachine.klib.api | 2 +- commonstatemachine/build.gradle.kts | 146 +---------- commonstatemachine/gradle.properties | 14 ++ coroutines/api/android/coroutines.api | 24 ++ coroutines/api/coroutines.klib.api | 2 +- coroutines/build.gradle.kts | 138 +---------- coroutines/gradle.properties | 14 ++ examples/androidcore/build.gradle.kts | 79 ------ .../androidcore/src/main/AndroidManifest.xml | 17 -- .../androidcore/ui/theme/Theme.kt | 70 ------ .../statemachine/androidcore/ui/theme/Type.kt | 34 --- examples/books/app/build.gradle.kts | 92 +------ .../examples/books/app}/App.kt | 8 +- .../examples}/books/app/MainActivity.kt | 12 +- .../examples}/books/app/MainViewModel.kt | 8 +- .../examples}/books/app/api/MainApi.kt | 6 +- .../examples}/books/app/api/MainFlowHost.kt | 2 +- .../examples}/books/app/api/MainGesture.kt | 4 +- .../examples}/books/app/api/MainUiState.kt | 6 +- .../examples}/books/app/data/MainDataState.kt | 4 +- .../books/app/di/ApplicationModule.kt | 8 +- .../app/repository/BookRepositoryImpl.kt | 8 +- .../books/app/state/BaseMainState.kt | 8 +- .../books/app/state/ItemListState.kt | 16 +- .../examples}/books/app/state/ItemProxy.kt | 20 +- .../examples}/books/app/state/MainContext.kt | 2 +- .../examples}/books/app/state/MainFactory.kt | 12 +- .../examples}/books/app/ui/BookListView.kt | 6 +- .../examples}/books/app/ui/MainScreen.kt | 16 +- .../examples}/books/app/mock.kt | 4 +- .../books/app/state/BaseStateTest.kt | 6 +- .../books/app/state/ItemListStateTest.kt | 20 +- examples/books/book/build.gradle.kts | 57 +---- examples/books/book/demo/build.gradle.kts | 91 +------ .../examples/books/book/demo}/App.kt | 8 +- .../examples}/books/book/demo/MainActivity.kt | 12 +- .../books/book/demo/MainViewModel.kt | 10 +- .../examples}/books/book/demo/data.kt | 4 +- .../books/book/demo/di/ApplicationModule.kt | 10 +- .../examples}/books/book/api/BookApi.kt | 4 +- .../examples}/books/book/api/BookFlowHost.kt | 2 +- .../examples}/books/book/api/BookGesture.kt | 2 +- .../examples}/books/book/api/BookInput.kt | 2 +- .../examples}/books/book/api/BookUiState.kt | 4 +- .../books/book/data/BookDataState.kt | 6 +- .../books/book/state/BaseBookState.kt | 8 +- .../examples}/books/book/state/BookContext.kt | 2 +- .../examples}/books/book/state/BookFactory.kt | 12 +- .../books/book/state/ContentState.kt | 10 +- .../book/state/DeleteConfirmationState.kt | 10 +- .../books/book/state/DeletingState.kt | 10 +- .../examples}/books/book/state/ErrorState.kt | 10 +- .../books/book/state/LoadingState.kt | 12 +- .../books/book/ui/BookContentScreen.kt | 10 +- .../examples}/books/book/ui/BookScreen.kt | 12 +- .../examples}/books/book/mock.kt | 4 +- .../books/book/state/BaseStateTest.kt | 6 +- .../books/book/state/ContentStateTest.kt | 16 +- .../book/state/DeleteConfirmationStateTest.kt | 18 +- .../books/book/state/DeletingStateTest.kt | 16 +- .../books/book/state/ErrorStateTest.kt | 16 +- .../books/book/state/LoadingStateTest.kt | 22 +- examples/books/domain/build.gradle.kts | 54 +--- .../examples}/books/domain/entity/Book.kt | 2 +- .../examples}/books/domain/entity/ListBook.kt | 2 +- .../books/domain/repository/BookRepository.kt | 6 +- examples/commoncore/build.gradle.kts | 53 +--- .../src/androidMain/AndroidManifest.xml | 15 -- .../statemachine/commoncore/log/Logger.kt | 50 ---- .../coroutines/DispatcherProvider.kt | 2 +- .../examples/commoncore/log/Logger.kt | 55 +++++ .../commoncore/resources/ResourceWrapper.kt | 2 +- .../examples/commoncore/ui}/Error.kt | 7 +- .../examples/commoncore/ui}/Loading.kt | 2 +- .../examples/commoncore}/ui/theme/Color.kt | 2 +- .../commoncore}/ui/theme/Dimensions.kt | 2 +- .../examples/commoncore/ui/theme/Theme.kt | 37 +++ .../examples/commoncore/ui/theme/Type.kt | 18 ++ .../statemachine/commoncore/log/Logger.kt | 32 --- .../statemachine/commoncore/log/Logger.kt | 48 ---- examples/di/api/build.gradle.kts | 51 +--- .../examples}/di/api/AuthApi.kt | 4 +- .../examples}/di/api/AuthFlowHost.kt | 4 +- .../examples}/di/api/SessionManager.kt | 4 +- .../examples}/di/api/data/Session.kt | 2 +- examples/di/app/build.gradle.kts | 93 +------ .../examples}/di/app/App.kt | 8 +- .../examples}/di/app/MainActivity.kt | 10 +- .../examples}/di/app/MainViewModel.kt | 8 +- .../examples}/di/app/data/MainGesture.kt | 4 +- .../examples}/di/app/data/MainUiState.kt | 6 +- .../examples}/di/app/di/ApplicationModule.kt | 6 +- .../examples}/di/app/di/MainModule.kt | 4 +- .../di/app/session/SessionManagerImpl.kt | 6 +- .../examples}/di/app/state/AuthProxy.kt | 16 +- .../examples}/di/app/state/BaseMainState.kt | 8 +- .../examples}/di/app/state/Content.kt | 12 +- .../examples}/di/app/state/MainContext.kt | 2 +- .../examples}/di/app/state/MainFactory.kt | 8 +- .../examples}/di/app/state/UpdatingSession.kt | 10 +- .../examples}/di/app/ui/MainScreen.kt | 12 +- .../examples}/di/app/ui/MainScreenView.kt | 10 +- .../examples}/di/app/mock.kt | 4 +- .../examples}/di/app/state/BaseStateTest.kt | 8 +- .../examples}/di/app/state/ContentTest.kt | 12 +- examples/di/login/build.gradle.kts | 58 +---- .../examples}/di/login/LoginConstants.kt | 2 +- .../examples}/di/login/LoginDataApi.kt | 22 +- .../examples}/di/login/data/LoginDataState.kt | 2 +- .../examples}/di/login/data/LoginGesture.kt | 4 +- .../examples}/di/login/data/LoginUiState.kt | 4 +- .../examples}/di/login/di/LoginFlowModule.kt | 10 +- .../di/login/state/BaseLoginState.kt | 10 +- .../examples}/di/login/state/Error.kt | 10 +- .../examples}/di/login/state/FormState.kt | 10 +- .../examples}/di/login/state/LoggingIn.kt | 14 +- .../examples}/di/login/state/LoginContext.kt | 4 +- .../examples}/di/login/state/LoginFactory.kt | 12 +- .../examples}/di/login/ui/LoginFormView.kt | 8 +- .../examples}/di/login/ui/LoginScreen.kt | 10 +- .../examples}/di/login/state/BaseStateTest.kt | 8 +- .../examples}/di/login/state/FormStateTest.kt | 8 +- examples/di/social/build.gradle.kts | 57 +---- .../examples}/di/social/SocialConstants.kt | 2 +- .../examples}/di/social/SocialDataApi.kt | 20 +- .../examples}/di/social/data/SocialGesture.kt | 4 +- .../examples}/di/social/data/SocialUiState.kt | 4 +- .../di/social/di/SocialFlowModule.kt | 10 +- .../di/social/state/BaseSocialState.kt | 10 +- .../examples}/di/social/state/FormState.kt | 8 +- .../examples}/di/social/state/LoggingIn.kt | 12 +- .../di/social/state/SocialContext.kt | 4 +- .../di/social/state/SocialFactory.kt | 10 +- .../examples}/di/social/ui/SocialFormView.kt | 8 +- .../examples}/di/social/ui/SocialScreen.kt | 8 +- examples/lce/build.gradle.kts | 90 +------ .../examples}/lce/MainActivity.kt | 14 +- .../examples}/lce/data/ItemId.kt | 2 +- .../examples}/lce/data/ItemModel.kt | 4 +- .../examples}/lce/data/LceGesture.kt | 4 +- .../examples}/lce/data/LceUiState.kt | 4 +- .../examples}/lce/model/LceViewModel.kt | 16 +- .../examples}/lce/model/state/ContentState.kt | 13 +- .../examples}/lce/model/state/ErrorState.kt | 19 +- .../lce/model/state/ItemListState.kt | 19 +- .../lce/model/state/LceLogicalState.kt | 11 +- .../examples}/lce/model/state/LoadingState.kt | 22 +- .../lce/model/state/TerminatedState.kt | 9 +- .../examples}/lce/ui/ItemContents.kt | 4 +- .../examples}/lce/ui/ItemList.kt | 8 +- .../examples}/lce/ui/LceScreen.kt | 18 +- .../examples}/lce/ui/LoadError.kt | 6 +- .../examples}/lce/ExampleUnitTest.kt | 2 +- .../lce/model/state/BaseStateTest.kt | 6 +- .../lce/model/state/ItemDetailsStateTest.kt | 6 +- .../lce/model/state/ItemListStateTest.kt | 8 +- .../lce/model/state/ItemLoadingStateTest.kt | 14 +- .../lce/model/state/LoadErrorStateTest.kt | 8 +- .../lce/model/state/TerminatedStateTest.kt | 4 +- examples/lifecycle/build.gradle.kts | 85 +------ .../lifecycle/src/main/AndroidManifest.xml | 4 +- .../examples}/lifecycle/App.kt | 8 +- .../examples}/lifecycle/MainActivity.kt | 8 +- .../lifecycle/model/MainViewModel.kt | 8 +- examples/multi/mixed/build.gradle.kts | 87 +------ .../multi/mixed/src/main/AndroidManifest.xml | 4 +- .../commonstatemachine/examples}/mixed/App.kt | 8 +- .../examples}/mixed/MainActivity.kt | 14 +- .../examples}/mixed/model/MainViewModel.kt | 12 +- .../mixed/model/data/MixedGesture.kt | 4 +- .../mixed/model/data/MixedUiState.kt | 4 +- .../examples}/mixed/model/data/SomeGesture.kt | 2 +- .../examples}/mixed/model/data/SomeUiState.kt | 2 +- .../examples}/mixed/model/state/MixedState.kt | 20 +- .../examples}/mixed/model/state/SomeState.kt | 6 +- examples/multi/navbar/build.gradle.kts | 86 +------ .../multi/navbar/src/main/AndroidManifest.xml | 4 +- .../examples}/navbar/App.kt | 8 +- .../examples}/navbar/MainActivity.kt | 10 +- .../examples}/navbar/model/MainViewModel.kt | 8 +- .../navbar/model/data/NavbarGesture.kt | 6 +- .../navbar/model/data/NavbarUiState.kt | 6 +- .../navbar/model/state/NavbarState.kt | 16 +- examples/multi/parallel/build.gradle.kts | 87 +------ .../parallel/src/main/AndroidManifest.xml | 4 +- .../examples}/parallel/App.kt | 8 +- .../examples}/parallel/MainActivity.kt | 10 +- .../examples}/parallel/model/MainViewModel.kt | 10 +- .../parallel/model/data/ParallelGesture.kt | 4 +- .../parallel/model/data/ParallelUiState.kt | 4 +- .../parallel/model/state/ParallelState.kt | 16 +- examples/skills/app/build.gradle.kts | 93 +------ .../examples}/skills/app/App.kt | 4 +- .../examples}/skills/app/MainActivity.kt | 6 +- .../examples/skills/app/MainModule.kt | 8 + .../examples}/skills/app/MainViewModel.kt | 8 +- .../examples}/skills/app/data/MainGesture.kt | 4 +- .../examples}/skills/app/data/MainUiState.kt | 4 +- .../skills/app/state/AuthProxyState.kt | 16 +- .../app/state/CheckingAuthentication.kt | 4 +- .../skills/app/state/ContentState.kt | 4 +- .../examples}/skills/app/state/MainContext.kt | 4 +- .../examples}/skills/app/state/MainState.kt | 6 +- .../skills/app/state/MainStateFactory.kt | 4 +- .../examples}/skills/app/ui/MainScreen.kt | 18 +- .../examples}/skills/app/ui/MainUiRenderer.kt | 6 +- .../statemachine/skills/app/MainModule.kt | 8 - .../skills/app/state/BaseStateTest.kt | 8 +- .../app/state/CheckingAuthenticationTest.kt | 6 +- examples/skills/appcore/build.gradle.kts | 59 +---- .../auth/appcore/ui/design/SkillsAppBar.kt | 8 +- .../auth/appcore/ui/design/SkillsLoading.kt | 2 +- .../ui/preview/SkillsPreviewComposition.kt | 4 +- .../examples/auth/appcore}/ui/theme/Color.kt | 2 +- .../auth/appcore/ui/theme/Dimensions.kt | 34 +++ .../auth/appcore/ui/theme/SkillsTheme.kt | 2 +- .../examples}/auth/appcore/ui/theme/Type.kt | 2 +- examples/skills/auth/api/build.gradle.kts | 38 +-- .../examples}/skills/auth/api/AuthDataApi.kt | 2 +- .../examples}/skills/auth/api/AuthGesture.kt | 2 +- .../examples}/skills/auth/api/AuthInput.kt | 2 +- .../examples}/skills/auth/api/AuthResult.kt | 2 +- .../examples}/skills/auth/api/AuthUiApi.kt | 2 +- .../examples}/skills/auth/api/AuthUiState.kt | 2 +- examples/skills/auth/demo/build.gradle.kts | 90 +------ .../examples}/skills/auth/demo/App.kt | 4 +- .../skills/auth/demo/MainActivity.kt | 8 +- .../skills/auth/demo/MainViewModel.kt | 15 ++ .../skills/auth/demo/MainViewModel.kt | 15 -- .../auth/implementation/build.gradle.kts | 53 +--- .../composeResources/values/strings.xml | 6 + .../auth/implementation/AuthDataApiImpl.kt | 18 +- .../skills/auth/implementation/AuthModule.kt | 2 +- .../auth/implementation/AuthUiApiImpl.kt | 27 ++ .../implementation/data/AuthGestureImpl.kt | 4 +- .../auth/implementation/data/AuthStateData.kt | 4 +- .../implementation/data/AuthUiStateImpl.kt | 4 +- .../auth/implementation/state/AuthContext.kt | 6 +- .../auth/implementation/state/AuthState.kt | 10 + .../implementation/state/AuthStateFactory.kt | 8 +- .../state/AuthStateFactoryImpl.kt | 10 +- .../state/AuthenticatingState.kt | 8 +- .../state/AuthenticationErrorState.kt | 6 +- .../implementation/state/BaseAuthState.kt | 8 +- .../auth/implementation/state/FormState.kt | 12 +- .../state/PreloadingErrorState.kt | 8 +- .../implementation/state/PreloadingState.kt | 10 +- .../auth/implementation/ui/AuthErrorScreen.kt | 22 +- .../auth/implementation/ui/AuthFormScreen.kt | 24 +- .../implementation/ui/AuthLoadingScreen.kt | 6 +- .../auth/implementation/ui/AuthScreen.kt | 8 +- .../auth/implementation/ui/AuthUiRenderer.kt | 6 +- .../implementation/ui/AuthUiRendererImpl.kt | 6 +- .../auth/implementation/AuthUiApiImpl.kt | 27 -- .../auth/implementation/state/AuthState.kt | 10 - .../implementation/data/AuthStateDataTest.kt | 2 +- .../data/authGesturesFixtures.kt | 2 +- .../implementation/data/dataStatesFixtures.kt | 6 +- .../implementation/data/uiStatesFixtures.kt | 6 + .../state/AuthenticatingStateTest.kt | 12 +- .../state/AuthenticationErrorStateTest.kt | 12 +- .../implementation/state/BaseStateTest.kt | 10 +- .../implementation/state/FormStateTest.kt | 18 +- .../state/PreloadingErrorStateTest.kt | 16 +- .../state/PreloadingStateTest.kt | 14 +- .../ui/AuthUiRendererImplTest.kt | 10 +- .../implementation/data/uiStatesFixtures.kt | 6 - examples/skills/domain/build.gradle.kts | 30 +-- .../skills/domain/exception/AppException.kt | 2 +- .../skills/domain/session/data/Username.kt | 2 + examples/skills/usecase/build.gradle.kts | 29 +-- examples/timer/build.gradle.kts | 42 +--- .../examples}/timer/data/TimerGesture.kt | 2 +- .../examples}/timer/data/TimerKey.kt | 2 +- .../examples}/timer/data/TimerUiState.kt | 2 +- .../examples}/timer/state/TimerState.kt | 8 +- .../examples}/timer/ui/TimerScreen.kt | 6 +- .../examples}/timer/state/RunningTest.kt | 6 +- .../examples}/timer/state/mocks.kt | 2 +- .../{androidcore => welcome/app}/.gitignore | 0 examples/welcome/app/build.gradle.kts | 34 +++ .../app}/proguard-rules.pro | 0 .../src/main/AndroidManifest.xml | 2 +- .../examples/welcome/app/App.kt | 46 ++++ .../examples/welcome/app}/MainActivity.kt | 18 +- .../examples/welcome/app/WelcomeModule.kt | 27 ++ .../welcome/app}/data/WelcomeDataState.kt | 2 +- .../welcome/app}/data/WelcomeGesture.kt | 6 +- .../welcome/app}/data/WelcomeUiState.kt | 6 +- .../welcome/app}/model/WelcomeRenderer.kt | 15 +- .../welcome/app}/model/WelcomeViewModel.kt | 46 +--- .../welcome/app}/model/state/CompleteState.kt | 9 +- .../app}/model/state/EmailCheckState.kt | 25 +- .../app}/model/state/EmailEntryState.kt | 13 +- .../app}/model/state/LoginFlowState.kt | 49 ++-- .../app}/model/state/PreloadingState.kt | 19 +- .../app}/model/state/RegistrationFlowState.kt | 51 ++-- .../app}/model/state/TerminatedState.kt | 9 +- .../model/state/TermsAndConditionsState.kt | 11 +- .../app}/model/state/WelcomeContext.kt | 4 +- .../welcome/app}/model/state/WelcomeState.kt | 15 +- .../app/model/state/WelcomeStateFactory.kt | 72 ++++++ .../model/state/WelcomeStateFactoryImpl.kt | 101 ++++++++ .../welcome/app}/usecase/CheckEmail.kt | 17 +- .../examples/welcome/app}/usecase/Preload.kt | 21 +- .../examples/welcome/app}/view/Complete.kt | 6 +- .../examples/welcome/app}/view/EmailEntry.kt | 6 +- .../examples/welcome/app}/view/Welcome.kt | 6 +- .../welcome/app}/view/WelcomeScreen.kt | 32 ++- .../drawable-v24/ic_launcher_foreground.xml | 0 .../res/drawable/ic_launcher_background.xml | 0 .../res/mipmap-anydpi-v26/ic_launcher.xml | 0 .../mipmap-anydpi-v26/ic_launcher_round.xml | 0 .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin .../res/mipmap-hdpi/ic_launcher_round.webp | Bin .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin .../res/mipmap-mdpi/ic_launcher_round.webp | Bin .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin .../src/main/res/values/colors.xml | 0 .../src/main/res/values/strings.xml | 0 .../src/main/res/values/themes.xml | 0 .../src/main/res/xml/backup_rules.xml | 0 .../main/res/xml/data_extraction_rules.xml | 0 .../welcome/app}/model/state/BaseStateTest.kt | 10 +- .../app}/model/state/CompleteStateTest.kt | 6 +- .../app}/model/state/EmailCheckStateTest.kt | 18 +- .../app}/model/state/EmailEntryStateTest.kt | 6 +- .../app}/model/state/PreloadingStateTest.kt | 12 +- .../state/TermsAndConditionsStateTest.kt | 4 +- examples/welcome/commonapi/build.gradle.kts | 35 +-- .../examples/welcome}/commonapi/constants.kt | 6 +- .../examples/welcome/commonapi}/data/users.kt | 2 +- .../commonapi}/model/state/FlowStarter.kt | 2 +- .../model/state/WelcomeFeatureHost.kt | 2 +- .../welcome/commonregister/build.gradle.kts | 73 ------ .../register/model/getPasswordError.kt | 28 --- .../src/androidMain/res/values/strings.xml | 18 -- .../model/state/RegisterStateFactory.kt | 80 ------ .../register/model/getPasswordError.kt | 27 -- examples/welcome/login/build.gradle.kts | 77 +----- .../login/src/main/AndroidManifest.xml | 17 -- .../statemachine/login/di/LoginComponent.kt | 36 --- .../statemachine/login/di/LoginEntryPoint.kt | 32 --- .../statemachine/login/di/LoginModule.kt | 39 --- .../login/model/state/LoginStateFactory.kt | 96 -------- .../welcome/login/LoginFlowStarter.kt | 10 + .../examples/welcome/login/LoginModule.kt} | 15 +- .../welcome}/login/data/LoginDataState.kt | 2 +- .../welcome}/login/data/LoginGesture.kt | 2 +- .../welcome}/login/data/LoginUiState.kt | 2 +- .../welcome}/login/model/LoginRenderer.kt | 19 +- .../model/state/CredentialsCheckState.kt | 25 +- .../welcome}/login/model/state/ErrorState.kt | 11 +- .../login/model/state/LoginContext.kt | 6 +- .../welcome}/login/model/state/LoginState.kt | 15 +- .../login/model/state/LoginStateFactory.kt | 50 ++++ .../model/state/LoginStateFactoryImpl.kt | 52 ++++ .../login/model/state/PasswordEntryState.kt | 13 +- .../login/usecase/CheckCredentials.kt | 17 +- .../welcome}/login/view/LoginScreen.kt | 8 +- .../welcome}/login/view/PasswordEntry.kt | 6 +- .../welcome}/login/view/PasswordError.kt | 6 +- .../login/model/state/BaseStateTest.kt | 14 +- .../model/state/CredentialsCheckStateTest.kt | 20 +- .../login/model/state/ErrorStateTest.kt | 8 +- .../model/state/PasswordEntryStateTest.kt | 8 +- examples/welcome/register/build.gradle.kts | 91 ++----- .../composeResources/values/strings.xml | 10 + .../welcome/register/RegisterFlowStarter.kt | 10 + .../welcome/register/RegisterModule.kt | 8 + .../register/data/PasswordValidationError.kt | 2 +- .../register/data/RegisterDataState.kt | 2 +- .../welcome}/register/data/RegisterGesture.kt | 2 +- .../welcome}/register/data/RegisterUiState.kt | 2 +- .../register/model/RegistrationRenderer.kt | 21 +- .../model/state/PasswordEntryState.kt | 17 +- .../register/model/state/RegisterContext.kt | 11 +- .../register/model/state/RegisterState.kt | 14 +- .../model/state/RegisterStateFactory.kt | 47 ++++ .../model/state/RegisterStateFactoryImpl.kt | 44 ++++ .../register/model/state/RegistrationState.kt | 18 +- .../welcome}/register/usecase/Registration.kt | 10 +- .../welcome}/register/view/PasswordEntry.kt | 28 ++- .../register/view/RegistrationScreen.kt | 10 +- .../examples/welocme}/register/mocks.kt | 42 ++-- .../register/model/state/BaseStateTest.kt | 12 +- .../model/state/PasswordEntryStateTest.kt | 22 +- .../register/model/state/RegisterStateTest.kt | 20 +- .../register/src/main/AndroidManifest.xml | 17 -- .../register/di/RegisterComponent.kt | 36 --- .../register/di/RegisterEntryPoint.kt | 32 --- .../register/di/RegisterModule.kt | 60 ----- .../statemachine/register/di/RegisterScope.kt | 24 -- .../register/src/main/res/values/strings.xml | 22 -- examples/welcome/welcome/.gitignore | 1 - examples/welcome/welcome/build.gradle.kts | 115 --------- examples/welcome/welcome/proguard-rules.pro | 21 -- .../main/java/com/motorro/statemachine/App.kt | 30 --- .../statemachine/di/ApplicationModule.kt | 49 ---- .../statemachine/welcome/di/WelcomeModule.kt | 29 --- .../model/state/WelcomeStateFactory.kt | 166 ------------- gradle/libs.versions.toml | 164 ++++++------ gradle/maven-publish-config.gradle | 48 ---- gradle/versioning.gradle | 126 ---------- lifecycle/api/android/lifecycle.api | 17 ++ lifecycle/api/jvm/lifecycle.api | 1 - lifecycle/api/lifecycle.klib.api | 4 - lifecycle/build.gradle.kts | 141 +---------- lifecycle/gradle.properties | 14 ++ platformtest/build.gradle.kts | 76 +----- platformtest/gradle.properties | 14 ++ settings.gradle.kts | 6 +- .../common-child-flow/boilerplate/SKILL.md | 8 +- .../boilerplate/assets/example/api | 2 +- .../boilerplate/assets/example/app | 2 +- .../boilerplate/assets/example/implementation | 2 +- .../assets/example/implementation-tests | 2 +- tmap/api/android/tmap.api | 25 ++ tmap/api/tmap.klib.api | 2 +- tmap/build.gradle.kts | 146 +---------- tmap/gradle.properties | 14 ++ 471 files changed, 4045 insertions(+), 5828 deletions(-) create mode 100644 .run/Check All.run.xml create mode 100644 AGENTS.md create mode 100644 build-logic/convention/build.gradle.kts rename {examples/androidcore => build-logic/convention}/consumer-rules.pro (100%) create mode 100644 build-logic/convention/proguard-rules.pro create mode 100644 build-logic/convention/src/main/kotlin/AndroidApplicationPlugin.kt create mode 100644 build-logic/convention/src/main/kotlin/AndroidComposeLibraryPlugin.kt create mode 100644 build-logic/convention/src/main/kotlin/AndroidDynamicFeaturePlugin.kt create mode 100644 build-logic/convention/src/main/kotlin/AndroidLibraryPlugin.kt create mode 100644 build-logic/convention/src/main/kotlin/AndroidTestPlugin.kt create mode 100644 build-logic/convention/src/main/kotlin/BaseLibraryPlugin.kt create mode 100644 build-logic/convention/src/main/kotlin/CmpPlugin.kt create mode 100644 build-logic/convention/src/main/kotlin/Jvm.kt create mode 100644 build-logic/convention/src/main/kotlin/JvmPlugin.kt create mode 100644 build-logic/convention/src/main/kotlin/KmpPlugin.kt create mode 100644 build-logic/convention/src/main/kotlin/Kotlin.kt create mode 100644 build-logic/convention/src/main/kotlin/PublishingPlugin.kt create mode 100644 build-logic/convention/src/main/kotlin/com/motorro/gradle/convention/configureCommonAndroid.kt create mode 100644 build-logic/convention/src/main/kotlin/com/motorro/gradle/convention/configureKotlin.kt create mode 100644 build-logic/convention/src/main/kotlin/com/motorro/gradle/convention/configureMultiplatformAndroidLibrary.kt create mode 100644 build-logic/convention/src/main/kotlin/com/motorro/gradle/convention/dependencies.kt create mode 100644 build-logic/convention/src/main/kotlin/com/motorro/gradle/convention/library.kt create mode 100644 build-logic/convention/src/main/kotlin/com/motorro/gradle/convention/naming.kt create mode 100644 build-logic/convention/src/main/kotlin/com/motorro/gradle/convention/properties.kt create mode 100644 build-logic/convention/src/main/kotlin/publishingProperties.kt create mode 100644 build-logic/settings.gradle.kts create mode 100644 buildSrc/build.gradle.kts create mode 100644 buildSrc/settings.gradle.kts create mode 100644 buildSrc/src/main/kotlin/com/motorro/gradle/convention/Versioning.kt create mode 100644 commonflow/compose/api/android/commonflow-compose.api create mode 100644 commonflow/compose/gradle.properties create mode 100644 commonflow/data/api/android/commonflow-data.api create mode 100644 commonflow/data/gradle.properties create mode 100644 commonflow/viewmodel/api/android/commonflow-viewmodel.api create mode 100644 commonflow/viewmodel/gradle.properties create mode 100644 commonstatemachine/api/android/commonstatemachine.api create mode 100644 commonstatemachine/gradle.properties create mode 100644 coroutines/api/android/coroutines.api create mode 100644 coroutines/gradle.properties delete mode 100644 examples/androidcore/build.gradle.kts delete mode 100644 examples/androidcore/src/main/AndroidManifest.xml delete mode 100644 examples/androidcore/src/main/java/com/motorro/statemachine/androidcore/ui/theme/Theme.kt delete mode 100644 examples/androidcore/src/main/java/com/motorro/statemachine/androidcore/ui/theme/Type.kt rename examples/books/{book/demo/src/main/kotlin/com/motorro/statemachine/books/book/demo => app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app}/App.kt (82%) rename examples/books/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/app/MainActivity.kt (77%) rename examples/books/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/app/MainViewModel.kt (77%) rename examples/books/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/app/api/MainApi.kt (85%) rename examples/books/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/app/api/MainFlowHost.kt (92%) rename examples/books/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/app/api/MainGesture.kt (87%) rename examples/books/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/app/api/MainUiState.kt (84%) rename examples/books/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/app/data/MainDataState.kt (84%) rename examples/books/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/app/di/ApplicationModule.kt (90%) rename examples/books/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/app/repository/BookRepositoryImpl.kt (84%) rename examples/books/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/app/state/BaseMainState.kt (76%) rename examples/books/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/app/state/ItemListState.kt (80%) rename examples/books/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/app/state/ItemProxy.kt (70%) rename examples/books/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/app/state/MainContext.kt (91%) rename examples/books/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/app/state/MainFactory.kt (82%) rename examples/books/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/app/ui/BookListView.kt (91%) rename examples/books/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/app/ui/MainScreen.kt (87%) rename examples/books/app/src/test/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/app/mock.kt (86%) rename examples/books/app/src/test/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/app/state/BaseStateTest.kt (90%) rename examples/books/app/src/test/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/app/state/ItemListStateTest.kt (85%) rename examples/books/{app/src/main/kotlin/com/motorro/statemachine/books/app => book/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/demo}/App.kt (82%) rename examples/books/book/demo/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/book/demo/MainActivity.kt (77%) rename examples/books/book/demo/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/book/demo/MainViewModel.kt (77%) rename examples/books/book/demo/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/book/demo/data.kt (87%) rename examples/books/book/demo/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/book/demo/di/ApplicationModule.kt (79%) rename examples/books/book/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/book/api/BookApi.kt (90%) rename examples/books/book/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/book/api/BookFlowHost.kt (92%) rename examples/books/book/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/book/api/BookGesture.kt (93%) rename examples/books/book/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/book/api/BookInput.kt (91%) rename examples/books/book/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/book/api/BookUiState.kt (88%) rename examples/books/book/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/book/data/BookDataState.kt (77%) rename examples/books/book/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/book/state/BaseBookState.kt (76%) rename examples/books/book/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/book/state/BookContext.kt (91%) rename examples/books/book/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/book/state/BookFactory.kt (86%) rename examples/books/book/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/book/state/ContentState.kt (79%) rename examples/books/book/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/book/state/DeleteConfirmationState.kt (79%) rename examples/books/book/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/book/state/DeletingState.kt (82%) rename examples/books/book/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/book/state/ErrorState.kt (77%) rename examples/books/book/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/book/state/LoadingState.kt (81%) rename examples/books/book/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/book/ui/BookContentScreen.kt (93%) rename examples/books/book/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/book/ui/BookScreen.kt (88%) rename examples/books/book/src/test/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/book/mock.kt (87%) rename examples/books/book/src/test/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/book/state/BaseStateTest.kt (90%) rename examples/books/book/src/test/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/book/state/ContentStateTest.kt (78%) rename examples/books/book/src/test/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/book/state/DeleteConfirmationStateTest.kt (80%) rename examples/books/book/src/test/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/book/state/DeletingStateTest.kt (78%) rename examples/books/book/src/test/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/book/state/ErrorStateTest.kt (78%) rename examples/books/book/src/test/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/book/state/LoadingStateTest.kt (76%) rename examples/books/domain/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/domain/entity/Book.kt (91%) rename examples/books/domain/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/domain/entity/ListBook.kt (91%) rename examples/books/domain/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/books/domain/repository/BookRepository.kt (79%) delete mode 100644 examples/commoncore/src/androidMain/AndroidManifest.xml delete mode 100644 examples/commoncore/src/androidMain/kotlin/com/motorro/statemachine/commoncore/log/Logger.kt rename examples/commoncore/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/commoncore/coroutines/DispatcherProvider.kt (94%) create mode 100644 examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/log/Logger.kt rename examples/commoncore/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/commoncore/resources/ResourceWrapper.kt (91%) rename examples/{androidcore/src/main/java/com/motorro/statemachine/androidcore/compose => commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/ui}/Error.kt (93%) rename examples/{androidcore/src/main/java/com/motorro/statemachine/androidcore/compose => commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/ui}/Loading.kt (95%) rename examples/{skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore => commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore}/ui/theme/Color.kt (78%) rename examples/{skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore => commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore}/ui/theme/Dimensions.kt (88%) create mode 100644 examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/ui/theme/Theme.kt create mode 100644 examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/ui/theme/Type.kt delete mode 100644 examples/commoncore/src/commonMain/kotlin/com/motorro/statemachine/commoncore/log/Logger.kt delete mode 100644 examples/commoncore/src/jsMain/kotlin/com/motorro/statemachine/commoncore/log/Logger.kt rename examples/di/api/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/api/AuthApi.kt (91%) rename examples/di/api/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/api/AuthFlowHost.kt (85%) rename examples/di/api/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/api/SessionManager.kt (87%) rename examples/di/api/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/api/data/Session.kt (93%) rename examples/di/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/app/App.kt (82%) rename examples/di/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/app/MainActivity.kt (85%) rename examples/di/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/app/MainViewModel.kt (81%) rename examples/di/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/app/data/MainGesture.kt (86%) rename examples/di/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/app/data/MainUiState.kt (81%) rename examples/di/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/app/di/ApplicationModule.kt (81%) rename examples/di/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/app/di/MainModule.kt (87%) rename examples/di/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/app/session/SessionManagerImpl.kt (84%) rename examples/di/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/app/state/AuthProxy.kt (77%) rename examples/di/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/app/state/BaseMainState.kt (76%) rename examples/di/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/app/state/Content.kt (83%) rename examples/di/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/app/state/MainContext.kt (91%) rename examples/di/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/app/state/MainFactory.kt (88%) rename examples/di/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/app/state/UpdatingSession.kt (79%) rename examples/di/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/app/ui/MainScreen.kt (88%) rename examples/di/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/app/ui/MainScreenView.kt (90%) rename examples/di/app/src/test/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/app/mock.kt (85%) rename examples/di/app/src/test/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/app/state/BaseStateTest.kt (89%) rename examples/di/app/src/test/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/app/state/ContentTest.kt (87%) rename examples/di/login/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/login/LoginConstants.kt (93%) rename examples/di/login/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/login/LoginDataApi.kt (68%) rename examples/di/login/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/login/data/LoginDataState.kt (91%) rename examples/di/login/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/login/data/LoginGesture.kt (87%) rename examples/di/login/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/login/data/LoginUiState.kt (86%) rename examples/di/login/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/login/di/LoginFlowModule.kt (77%) rename examples/di/login/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/login/state/BaseLoginState.kt (76%) rename examples/di/login/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/login/state/Error.kt (76%) rename examples/di/login/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/login/state/FormState.kt (84%) rename examples/di/login/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/login/state/LoggingIn.kt (79%) rename examples/di/login/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/login/state/LoginContext.kt (84%) rename examples/di/login/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/login/state/LoginFactory.kt (85%) rename examples/di/login/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/login/ui/LoginFormView.kt (89%) rename examples/di/login/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/login/ui/LoginScreen.kt (76%) rename examples/di/login/src/test/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/login/state/BaseStateTest.kt (90%) rename examples/di/login/src/test/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/login/state/FormStateTest.kt (93%) rename examples/di/social/src/main/kotlin/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/social/SocialConstants.kt (91%) rename examples/di/social/src/main/kotlin/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/social/SocialDataApi.kt (68%) rename examples/di/social/src/main/kotlin/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/social/data/SocialGesture.kt (83%) rename examples/di/social/src/main/kotlin/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/social/data/SocialUiState.kt (83%) rename examples/di/social/src/main/kotlin/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/social/di/SocialFlowModule.kt (75%) rename examples/di/social/src/main/kotlin/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/social/state/BaseSocialState.kt (75%) rename examples/di/social/src/main/kotlin/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/social/state/FormState.kt (78%) rename examples/di/social/src/main/kotlin/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/social/state/LoggingIn.kt (76%) rename examples/di/social/src/main/kotlin/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/social/state/SocialContext.kt (83%) rename examples/di/social/src/main/kotlin/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/social/state/SocialFactory.kt (85%) rename examples/di/social/src/main/kotlin/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/social/ui/SocialFormView.kt (87%) rename examples/di/social/src/main/kotlin/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/di/social/ui/SocialScreen.kt (75%) rename examples/lce/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/lce/MainActivity.kt (74%) rename examples/lce/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/lce/data/ItemId.kt (92%) rename examples/lce/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/lce/data/ItemModel.kt (79%) rename examples/lce/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/lce/data/LceGesture.kt (85%) rename examples/lce/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/lce/data/LceUiState.kt (86%) rename examples/lce/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/lce/model/LceViewModel.kt (80%) rename examples/lce/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/lce/model/state/ContentState.kt (78%) rename examples/lce/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/lce/model/state/ErrorState.kt (76%) rename examples/lce/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/lce/model/state/ItemListState.kt (74%) rename examples/lce/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/lce/model/state/LceLogicalState.kt (75%) rename examples/lce/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/lce/model/state/LoadingState.kt (78%) rename examples/lce/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/lce/model/state/TerminatedState.kt (80%) rename examples/lce/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/lce/ui/ItemContents.kt (89%) rename examples/lce/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/lce/ui/ItemList.kt (87%) rename examples/lce/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/lce/ui/LceScreen.kt (83%) rename examples/lce/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/lce/ui/LoadError.kt (90%) rename examples/lce/src/test/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/lce/ExampleUnitTest.kt (94%) rename examples/lce/src/test/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/lce/model/state/BaseStateTest.kt (81%) rename examples/lce/src/test/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/lce/model/state/ItemDetailsStateTest.kt (86%) rename examples/lce/src/test/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/lce/model/state/ItemListStateTest.kt (84%) rename examples/lce/src/test/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/lce/model/state/ItemLoadingStateTest.kt (83%) rename examples/lce/src/test/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/lce/model/state/LoadErrorStateTest.kt (87%) rename examples/lce/src/test/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/lce/model/state/TerminatedStateTest.kt (87%) rename examples/lifecycle/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/lifecycle/App.kt (81%) rename examples/lifecycle/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/lifecycle/MainActivity.kt (87%) rename examples/lifecycle/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/lifecycle/model/MainViewModel.kt (81%) rename examples/multi/mixed/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/mixed/App.kt (81%) rename examples/multi/mixed/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/mixed/MainActivity.kt (88%) rename examples/multi/mixed/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/mixed/model/MainViewModel.kt (72%) rename examples/multi/mixed/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/mixed/model/data/MixedGesture.kt (86%) rename examples/multi/mixed/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/mixed/model/data/MixedUiState.kt (85%) rename examples/multi/mixed/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/mixed/model/data/SomeGesture.kt (91%) rename examples/multi/mixed/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/mixed/model/data/SomeUiState.kt (92%) rename examples/multi/mixed/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/mixed/model/state/MixedState.kt (83%) rename examples/multi/mixed/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/mixed/model/state/SomeState.kt (82%) rename examples/multi/navbar/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/navbar/App.kt (81%) rename examples/multi/navbar/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/navbar/MainActivity.kt (93%) rename examples/multi/navbar/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/navbar/model/MainViewModel.kt (78%) rename examples/multi/navbar/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/navbar/model/data/NavbarGesture.kt (82%) rename examples/multi/navbar/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/navbar/model/data/NavbarUiState.kt (80%) rename examples/multi/navbar/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/navbar/model/state/NavbarState.kt (86%) rename examples/multi/parallel/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/parallel/App.kt (81%) rename examples/multi/parallel/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/parallel/MainActivity.kt (89%) rename examples/multi/parallel/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/parallel/model/MainViewModel.kt (75%) rename examples/multi/parallel/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/parallel/model/data/ParallelGesture.kt (86%) rename examples/multi/parallel/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/parallel/model/data/ParallelUiState.kt (84%) rename examples/multi/parallel/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples}/parallel/model/state/ParallelState.kt (86%) rename examples/skills/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/app/App.kt (83%) rename examples/skills/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/app/MainActivity.kt (69%) create mode 100644 examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/MainModule.kt rename examples/skills/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/app/MainViewModel.kt (79%) rename examples/skills/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/app/data/MainGesture.kt (84%) rename examples/skills/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/app/data/MainUiState.kt (86%) rename examples/skills/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/app/state/AuthProxyState.kt (77%) rename examples/skills/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/app/state/CheckingAuthentication.kt (92%) rename examples/skills/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/app/state/ContentState.kt (90%) rename examples/skills/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/app/state/MainContext.kt (86%) rename examples/skills/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/app/state/MainState.kt (83%) rename examples/skills/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/app/state/MainStateFactory.kt (93%) rename examples/skills/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/app/ui/MainScreen.kt (85%) rename examples/skills/app/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/app/ui/MainUiRenderer.kt (70%) delete mode 100644 examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/MainModule.kt rename examples/skills/app/src/test/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/app/state/BaseStateTest.kt (86%) rename examples/skills/app/src/test/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/app/state/CheckingAuthenticationTest.kt (93%) rename examples/skills/appcore/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/auth/appcore/ui/design/SkillsAppBar.kt (91%) rename examples/skills/appcore/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/auth/appcore/ui/design/SkillsLoading.kt (86%) rename examples/skills/appcore/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/auth/appcore/ui/preview/SkillsPreviewComposition.kt (84%) rename examples/{androidcore/src/main/java/com/motorro/statemachine/androidcore => skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore}/ui/theme/Color.kt (77%) create mode 100644 examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/theme/Dimensions.kt rename examples/skills/appcore/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/auth/appcore/ui/theme/SkillsTheme.kt (92%) rename examples/skills/appcore/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/auth/appcore/ui/theme/Type.kt (88%) rename examples/skills/auth/api/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/api/AuthDataApi.kt (80%) rename examples/skills/auth/api/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/api/AuthGesture.kt (63%) rename examples/skills/auth/api/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/api/AuthInput.kt (71%) rename examples/skills/auth/api/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/api/AuthResult.kt (70%) rename examples/skills/auth/api/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/api/AuthUiApi.kt (78%) rename examples/skills/auth/api/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/api/AuthUiState.kt (62%) rename examples/skills/auth/demo/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/demo/App.kt (95%) rename examples/skills/auth/demo/src/main/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/demo/MainActivity.kt (86%) create mode 100644 examples/skills/auth/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/MainViewModel.kt delete mode 100644 examples/skills/auth/demo/src/main/kotlin/com/motorro/statemachine/skills/auth/demo/MainViewModel.kt rename examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/implementation/AuthDataApiImpl.kt (55%) rename examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/implementation/AuthModule.kt (62%) create mode 100644 examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/AuthUiApiImpl.kt rename examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/implementation/data/AuthGestureImpl.kt (82%) rename examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/implementation/data/AuthStateData.kt (83%) rename examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/implementation/data/AuthUiStateImpl.kt (86%) rename examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/implementation/state/AuthContext.kt (59%) create mode 100644 examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthState.kt rename examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/implementation/state/AuthStateFactory.kt (82%) rename examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/implementation/state/AuthStateFactoryImpl.kt (83%) rename examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/implementation/state/AuthenticatingState.kt (86%) rename examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/implementation/state/AuthenticationErrorState.kt (85%) rename examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/implementation/state/BaseAuthState.kt (71%) rename examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/implementation/state/FormState.kt (80%) rename examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/implementation/state/PreloadingErrorState.kt (82%) rename examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/implementation/state/PreloadingState.kt (83%) rename examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/implementation/ui/AuthErrorScreen.kt (68%) rename examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/implementation/ui/AuthFormScreen.kt (76%) rename examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/implementation/ui/AuthLoadingScreen.kt (62%) rename examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/implementation/ui/AuthScreen.kt (83%) rename examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/implementation/ui/AuthUiRenderer.kt (77%) rename examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/implementation/ui/AuthUiRendererImpl.kt (74%) delete mode 100644 examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/AuthUiApiImpl.kt delete mode 100644 examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthState.kt rename examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/implementation/data/AuthStateDataTest.kt (94%) rename examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/implementation/data/authGesturesFixtures.kt (74%) rename examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/implementation/data/dataStatesFixtures.kt (80%) create mode 100644 examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/uiStatesFixtures.kt rename examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/implementation/state/AuthenticatingStateTest.kt (81%) rename examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/implementation/state/AuthenticationErrorStateTest.kt (80%) rename examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/implementation/state/BaseStateTest.kt (81%) rename examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/implementation/state/FormStateTest.kt (82%) rename examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/implementation/state/PreloadingErrorStateTest.kt (79%) rename examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/implementation/state/PreloadingStateTest.kt (78%) rename examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/skills/auth/implementation/ui/AuthUiRendererImplTest.kt (79%) delete mode 100644 examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/uiStatesFixtures.kt rename examples/timer/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/timer/data/TimerGesture.kt (92%) rename examples/timer/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/timer/data/TimerKey.kt (93%) rename examples/timer/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/timer/data/TimerUiState.kt (94%) rename examples/timer/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/timer/state/TimerState.kt (93%) rename examples/timer/src/{androidMain/kotlin/com/motorro/statemachine => commonMain/kotlin/com/motorro/commonstatemachine/examples}/timer/ui/TimerScreen.kt (91%) rename examples/timer/src/commonTest/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/timer/state/RunningTest.kt (94%) rename examples/timer/src/commonTest/kotlin/com/motorro/{statemachine => commonstatemachine/examples}/timer/state/mocks.kt (96%) rename examples/{androidcore => welcome/app}/.gitignore (100%) create mode 100644 examples/welcome/app/build.gradle.kts rename examples/{androidcore => welcome/app}/proguard-rules.pro (100%) rename examples/welcome/{welcome => app}/src/main/AndroidManifest.xml (94%) create mode 100644 examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/App.kt rename examples/welcome/{welcome/src/main/java/com/motorro/statemachine => app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app}/MainActivity.kt (80%) create mode 100644 examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/WelcomeModule.kt rename examples/welcome/{welcome/src/main/java/com/motorro/statemachine/welcome => app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app}/data/WelcomeDataState.kt (91%) rename examples/welcome/{welcome/src/main/java/com/motorro/statemachine/welcome => app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app}/data/WelcomeGesture.kt (85%) rename examples/welcome/{welcome/src/main/java/com/motorro/statemachine/welcome => app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app}/data/WelcomeUiState.kt (89%) rename examples/welcome/{welcome/src/main/java/com/motorro/statemachine/welcome => app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app}/model/WelcomeRenderer.kt (77%) rename examples/welcome/{welcome/src/main/java/com/motorro/statemachine/welcome => app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app}/model/WelcomeViewModel.kt (52%) rename examples/welcome/{welcome/src/main/java/com/motorro/statemachine/welcome => app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app}/model/state/CompleteState.kt (83%) rename examples/welcome/{welcome/src/main/java/com/motorro/statemachine/welcome => app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app}/model/state/EmailCheckState.kt (74%) rename examples/welcome/{welcome/src/main/java/com/motorro/statemachine/welcome => app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app}/model/state/EmailEntryState.kt (87%) rename examples/welcome/{welcome/src/main/java/com/motorro/statemachine/welcome => app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app}/model/state/LoginFlowState.kt (64%) rename examples/welcome/{welcome/src/main/java/com/motorro/statemachine/welcome => app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app}/model/state/PreloadingState.kt (77%) rename examples/welcome/{welcome/src/main/java/com/motorro/statemachine/welcome => app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app}/model/state/RegistrationFlowState.kt (63%) rename examples/welcome/{welcome/src/main/java/com/motorro/statemachine/welcome => app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app}/model/state/TerminatedState.kt (79%) rename examples/welcome/{welcome/src/main/java/com/motorro/statemachine/welcome => app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app}/model/state/TermsAndConditionsState.kt (86%) rename examples/welcome/{welcome/src/main/java/com/motorro/statemachine/welcome => app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app}/model/state/WelcomeContext.kt (86%) rename examples/welcome/{welcome/src/main/java/com/motorro/statemachine/welcome => app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app}/model/state/WelcomeState.kt (64%) create mode 100644 examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/WelcomeStateFactory.kt create mode 100644 examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/WelcomeStateFactoryImpl.kt rename examples/welcome/{welcome/src/main/java/com/motorro/statemachine/welcome => app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app}/usecase/CheckEmail.kt (73%) rename examples/welcome/{welcome/src/main/java/com/motorro/statemachine/welcome => app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app}/usecase/Preload.kt (69%) rename examples/welcome/{welcome/src/main/java/com/motorro/statemachine/welcome => app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app}/view/Complete.kt (92%) rename examples/welcome/{welcome/src/main/java/com/motorro/statemachine/welcome => app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app}/view/EmailEntry.kt (94%) rename examples/welcome/{welcome/src/main/java/com/motorro/statemachine/welcome => app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app}/view/Welcome.kt (94%) rename examples/welcome/{welcome/src/main/java/com/motorro/statemachine/welcome => app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app}/view/WelcomeScreen.kt (61%) rename examples/welcome/{welcome => app}/src/main/res/drawable-v24/ic_launcher_foreground.xml (100%) rename examples/welcome/{welcome => app}/src/main/res/drawable/ic_launcher_background.xml (100%) rename examples/welcome/{welcome => app}/src/main/res/mipmap-anydpi-v26/ic_launcher.xml (100%) rename examples/welcome/{welcome => app}/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml (100%) rename examples/welcome/{welcome => app}/src/main/res/mipmap-hdpi/ic_launcher.webp (100%) rename examples/welcome/{welcome => app}/src/main/res/mipmap-hdpi/ic_launcher_round.webp (100%) rename examples/welcome/{welcome => app}/src/main/res/mipmap-mdpi/ic_launcher.webp (100%) rename examples/welcome/{welcome => app}/src/main/res/mipmap-mdpi/ic_launcher_round.webp (100%) rename examples/welcome/{welcome => app}/src/main/res/mipmap-xhdpi/ic_launcher.webp (100%) rename examples/welcome/{welcome => app}/src/main/res/mipmap-xhdpi/ic_launcher_round.webp (100%) rename examples/welcome/{welcome => app}/src/main/res/mipmap-xxhdpi/ic_launcher.webp (100%) rename examples/welcome/{welcome => app}/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp (100%) rename examples/welcome/{welcome => app}/src/main/res/mipmap-xxxhdpi/ic_launcher.webp (100%) rename examples/welcome/{welcome => app}/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp (100%) rename examples/welcome/{welcome => app}/src/main/res/values/colors.xml (100%) rename examples/welcome/{welcome => app}/src/main/res/values/strings.xml (100%) rename examples/welcome/{welcome => app}/src/main/res/values/themes.xml (100%) rename examples/welcome/{welcome => app}/src/main/res/xml/backup_rules.xml (100%) rename examples/welcome/{welcome => app}/src/main/res/xml/data_extraction_rules.xml (100%) rename examples/welcome/{welcome/src/test/java/com/motorro/statemachine/welcome => app/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/app}/model/state/BaseStateTest.kt (86%) rename examples/welcome/{welcome/src/test/java/com/motorro/statemachine/welcome => app/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/app}/model/state/CompleteStateTest.kt (88%) rename examples/welcome/{welcome/src/test/java/com/motorro/statemachine/welcome => app/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/app}/model/state/EmailCheckStateTest.kt (84%) rename examples/welcome/{welcome/src/test/java/com/motorro/statemachine/welcome => app/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/app}/model/state/EmailEntryStateTest.kt (94%) rename examples/welcome/{welcome/src/test/java/com/motorro/statemachine/welcome => app/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/app}/model/state/PreloadingStateTest.kt (86%) rename examples/welcome/{welcome/src/test/java/com/motorro/statemachine/welcome => app/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/app}/model/state/TermsAndConditionsStateTest.kt (95%) rename examples/welcome/commonapi/src/commonMain/kotlin/com/motorro/{statemachine => commonstatemachine/examples/welcome}/commonapi/constants.kt (80%) rename examples/welcome/commonapi/src/commonMain/kotlin/com/motorro/{statemachine/commonapi/welcome => commonstatemachine/examples/welcome/commonapi}/data/users.kt (90%) rename examples/welcome/commonapi/src/commonMain/kotlin/com/motorro/{statemachine/commonapi/welcome => commonstatemachine/examples/welcome/commonapi}/model/state/FlowStarter.kt (91%) rename examples/welcome/commonapi/src/commonMain/kotlin/com/motorro/{statemachine/commonapi/welcome => commonstatemachine/examples/welcome/commonapi}/model/state/WelcomeFeatureHost.kt (91%) delete mode 100644 examples/welcome/commonregister/build.gradle.kts delete mode 100644 examples/welcome/commonregister/src/androidMain/kotlin/com/motorro/statemachine/register/model/getPasswordError.kt delete mode 100644 examples/welcome/commonregister/src/androidMain/res/values/strings.xml delete mode 100644 examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/model/state/RegisterStateFactory.kt delete mode 100644 examples/welcome/commonregister/src/jsMain/kotlin/com/motorro/statemachine/register/model/getPasswordError.kt delete mode 100644 examples/welcome/login/src/main/AndroidManifest.xml delete mode 100644 examples/welcome/login/src/main/java/com/motorro/statemachine/login/di/LoginComponent.kt delete mode 100644 examples/welcome/login/src/main/java/com/motorro/statemachine/login/di/LoginEntryPoint.kt delete mode 100644 examples/welcome/login/src/main/java/com/motorro/statemachine/login/di/LoginModule.kt delete mode 100644 examples/welcome/login/src/main/java/com/motorro/statemachine/login/model/state/LoginStateFactory.kt create mode 100644 examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/LoginFlowStarter.kt rename examples/welcome/login/src/main/{java/com/motorro/statemachine/login/di/LoginScope.kt => kotlin/com/motorro/commonstatemachine/examples/welcome/login/LoginModule.kt} (74%) rename examples/welcome/login/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples/welcome}/login/data/LoginDataState.kt (91%) rename examples/welcome/login/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples/welcome}/login/data/LoginGesture.kt (93%) rename examples/welcome/login/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples/welcome}/login/data/LoginUiState.kt (95%) rename examples/welcome/login/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples/welcome}/login/model/LoginRenderer.kt (67%) rename examples/welcome/login/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples/welcome}/login/model/state/CredentialsCheckState.kt (74%) rename examples/welcome/login/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples/welcome}/login/model/state/ErrorState.kt (79%) rename examples/welcome/login/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples/welcome}/login/model/state/LoginContext.kt (77%) rename examples/welcome/login/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples/welcome}/login/model/state/LoginState.kt (64%) create mode 100644 examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/LoginStateFactory.kt create mode 100644 examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/LoginStateFactoryImpl.kt rename examples/welcome/login/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples/welcome}/login/model/state/PasswordEntryState.kt (81%) rename examples/welcome/login/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples/welcome}/login/usecase/CheckCredentials.kt (73%) rename examples/welcome/login/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples/welcome}/login/view/LoginScreen.kt (81%) rename examples/welcome/login/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples/welcome}/login/view/PasswordEntry.kt (95%) rename examples/welcome/login/src/main/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples/welcome}/login/view/PasswordError.kt (91%) rename examples/welcome/login/src/test/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples/welcome}/login/model/state/BaseStateTest.kt (81%) rename examples/welcome/login/src/test/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples/welcome}/login/model/state/CredentialsCheckStateTest.kt (84%) rename examples/welcome/login/src/test/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples/welcome}/login/model/state/ErrorStateTest.kt (85%) rename examples/welcome/login/src/test/{java/com/motorro/statemachine => kotlin/com/motorro/commonstatemachine/examples/welcome}/login/model/state/PasswordEntryStateTest.kt (91%) create mode 100644 examples/welcome/register/src/commonMain/composeResources/values/strings.xml create mode 100644 examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/RegisterFlowStarter.kt create mode 100644 examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/RegisterModule.kt rename examples/welcome/{commonregister/src/commonMain/kotlin/com/motorro/statemachine => register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome}/register/data/PasswordValidationError.kt (90%) rename examples/welcome/{commonregister/src/commonMain/kotlin/com/motorro/statemachine => register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome}/register/data/RegisterDataState.kt (91%) rename examples/welcome/{commonregister/src/commonMain/kotlin/com/motorro/statemachine => register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome}/register/data/RegisterGesture.kt (94%) rename examples/welcome/{commonregister/src/commonMain/kotlin/com/motorro/statemachine => register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome}/register/data/RegisterUiState.kt (94%) rename examples/welcome/{commonregister/src/commonMain/kotlin/com/motorro/statemachine => register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome}/register/model/RegistrationRenderer.kt (65%) rename examples/welcome/{commonregister/src/commonMain/kotlin/com/motorro/statemachine => register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome}/register/model/state/PasswordEntryState.kt (81%) rename examples/welcome/{commonregister/src/commonMain/kotlin/com/motorro/statemachine => register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome}/register/model/state/RegisterContext.kt (73%) rename examples/welcome/{commonregister/src/commonMain/kotlin/com/motorro/statemachine => register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome}/register/model/state/RegisterState.kt (65%) create mode 100644 examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/model/state/RegisterStateFactory.kt create mode 100644 examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/model/state/RegisterStateFactoryImpl.kt rename examples/welcome/{commonregister/src/commonMain/kotlin/com/motorro/statemachine => register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome}/register/model/state/RegistrationState.kt (78%) rename examples/welcome/{commonregister/src/commonMain/kotlin/com/motorro/statemachine => register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome}/register/usecase/Registration.kt (80%) rename examples/welcome/register/src/{main/java/com/motorro/statemachine => commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome}/register/view/PasswordEntry.kt (76%) rename examples/welcome/register/src/{main/java/com/motorro/statemachine => commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome}/register/view/RegistrationScreen.kt (79%) rename examples/welcome/{commonregister/src/commonTest/kotlin/com/motorro/statemachine => register/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/welocme}/register/mocks.kt (70%) rename examples/welcome/{commonregister/src/commonTest/kotlin/com/motorro/statemachine => register/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/welocme}/register/model/state/BaseStateTest.kt (74%) rename examples/welcome/{commonregister/src/commonTest/kotlin/com/motorro/statemachine => register/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/welocme}/register/model/state/PasswordEntryStateTest.kt (89%) rename examples/welcome/{commonregister/src/commonTest/kotlin/com/motorro/statemachine => register/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/welocme}/register/model/state/RegisterStateTest.kt (76%) delete mode 100644 examples/welcome/register/src/main/AndroidManifest.xml delete mode 100644 examples/welcome/register/src/main/java/com/motorro/statemachine/register/di/RegisterComponent.kt delete mode 100644 examples/welcome/register/src/main/java/com/motorro/statemachine/register/di/RegisterEntryPoint.kt delete mode 100644 examples/welcome/register/src/main/java/com/motorro/statemachine/register/di/RegisterModule.kt delete mode 100644 examples/welcome/register/src/main/java/com/motorro/statemachine/register/di/RegisterScope.kt delete mode 100644 examples/welcome/register/src/main/res/values/strings.xml delete mode 100644 examples/welcome/welcome/.gitignore delete mode 100644 examples/welcome/welcome/build.gradle.kts delete mode 100644 examples/welcome/welcome/proguard-rules.pro delete mode 100644 examples/welcome/welcome/src/main/java/com/motorro/statemachine/App.kt delete mode 100644 examples/welcome/welcome/src/main/java/com/motorro/statemachine/di/ApplicationModule.kt delete mode 100644 examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/di/WelcomeModule.kt delete mode 100644 examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/WelcomeStateFactory.kt delete mode 100644 gradle/maven-publish-config.gradle delete mode 100644 gradle/versioning.gradle create mode 100644 lifecycle/api/android/lifecycle.api create mode 100644 lifecycle/gradle.properties create mode 100644 platformtest/gradle.properties create mode 100644 tmap/api/android/tmap.api create mode 100644 tmap/gradle.properties diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 570b348..0d34b1a 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 :examples:di:app:assembleDebug :examples:book:app:assembleDebug :examples:book:book:demo:assembleDebug :examples:skills:auth:demo:assembleDebug checkLegacyAbi --no-daemon --no-configuration-cache + run: ./gradlew runUnitTests :examples:lce:assembleDebug :examples:welcome:app: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 :examples:book:app:assembleDebug :examples:skills:auth:demo:assembleDebug :examples:skills:app:assembleDebug checkLegacyAbi --no-daemon --no-configuration-cache - name: Upload problems report uses: actions/upload-artifact@v4 if: failure() diff --git a/.run/Check All.run.xml b/.run/Check All.run.xml new file mode 100644 index 0000000..349f91f --- /dev/null +++ b/.run/Check All.run.xml @@ -0,0 +1,39 @@ + + + + + + + false + true + false + false + false + false + false + + + \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..1c060ca --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,54 @@ +# AGENTS.md + +## Project Overview +**CommonStateMachine** is a lightweight, non-opinionated Kotlin Multiplatform (KMP) library that implements a finite-state machine pattern for MVI (Model-View-Intent) architectures. + +### Key Architecture Concepts +- **`CommonStateMachine`**: Acts as a bridge between the active logical state and the UI. `FlowStateMachine` exports state updates as a Kotlin `StateFlow`. +- **`CommonMachineState`**: Encapsulates state-bound business logic, processes user gestures (`G`), emits UI states (`U`), and handles transitions to other states. Supports state-scoped coroutines via `CoroutineState`. +- **StateMachine Class diagram**: Take a look at the framework [class diagram](doc/StateMachine.puml). +- **State Separation & DI Abstractions**: + - **Use-cases**: Business logic isolated from view logic. + - **View-state Renderer**: Decouples UI rendering logic from logical state transitions. + - **Context & State Factories**: Common `Context` delegates dependencies to states; dedicated factories (`StateFactory`) create states using inter-state dynamic data. +- **Multi-Module Support (`ProxyMachineState`)**: + - Wraps nested feature flows within a parent state machine. + - Bridges heterogeneous gesture and UI state systems through mapping adapters (`mapGesture`, `mapUiState`). + - Standardized via `commonflow` modules (`commonflow-data`, `commonflow-compose`, `commonflow-viewmodel`). + - Take a look at the [proxy lifecycle diagram](doc/ProxyLifecycle.puml). +- **Parallel Machine Composition (`MultiMachineState`)**: + - Runs multiple state machines concurrently or selectively in parallel using `ProxyMachineContainer`. + - Manages active/dormant machine lifecycles via `MachineLifecycle`. + +--- + +## Build and Verification +If you need to test if everything in the project compiles and unit tests pass: + +1. Take the GitHub action: `.github/workflows/check.yml`. +2. Find and execute the Gradle command from the step: `Check with gradle`. + +--- + +## Examples +Example projects demonstrating various architectures, state machine features, and usage patterns are located in the `examples` folder: +- **`examples/lce`**: Basic Load-Content-Error pattern. +- **`examples/welcome`**: Multi-module on-boarding flow. +- **`examples/multi`**: Parallel state machines (`parallel`), bottom navbar navigation (`navbar`), and mixed gesture/UI state composition (`mixed`). +- **`examples/lifecycle`**: View lifecycle tracking and operation pausing on lifecycle changes. +- **`examples/di`**: Dynamic child flow binding with Hilt. +- **`examples/skills`**: Multi-module example demonstrating custom skills implementation. + +--- + +## Skills +The `skills` folder in the project contains skills that provide instructions and guidance on how to use the library. + +--- + +## Coding conventions +- Use KDoc for documenting classes, interfaces, and public functions. +- Follow standard Kotlin coding conventions and style guidelines. +- Add copyright headers at the top of Kotlin source files. +- When checking for boolean `false`, use the `.not()` extension function instead of `!` for clarity (e.g., `if (condition.not()) { ... }`). +- Use Yoda style for equality comparisons (e.g., `if ("a" == b) { ... }`). diff --git a/README.md b/README.md index f030a5e..1812d07 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # CommonStateMachine [![Check](https://github.com/motorro/CommonStateMachine/actions/workflows/check.yml/badge.svg?branch=master)](https://github.com/motorro/CommonStateMachine/actions/workflows/check.yml) [![Maven Central](https://maven-badges.sml.io/maven-central/com.motorro.commonstatemachine/commonstatemachine/badge.png)](https://repo1.maven.org/maven2/com/motorro/commonstatemachine/commonstatemachine/) -Please checkout the Medium article on pattern/library usage. +Please check out the Medium article on pattern/library usage. - [Part I - basics](https://proandroiddev.com/mvi-architecture-with-a-state-machine-basics-721c5ebed893) - [Part II - tools](https://proandroiddev.com/mvi-architecture-with-a-state-machine-tools-721c5ebed893-47f46413415d) - [Part III - multi-module and multi-platform](https://proandroiddev.com/mvi-architecture-with-a-state-machine-modules-3e242666c7c) @@ -47,7 +47,7 @@ Please checkout the Medium article on pattern/library usage. * [ProxyMachineContainer](#proxymachinecontainer) * [Mapping UI states](#mapping-ui-states) * [Dispatching gestures](#dispatching-gestures) - * [MachineLifecle bonus](#machinelifecle-bonus) + * [MachineLifecle bonus](#machinelifecyle-bonus) - [Conclusion](#conclusion) @@ -76,7 +76,7 @@ app growth: - Too much overkill for simple functions like LCE (Load/Content/Error) display - Too much reducer logic based on if/else of the current data state which plays badly in complex - multi-step scenarios. + multistep scenarios. - Quite a learning curve to grasp the technology The simple pattern presented by this project aims to overcome the above drawbacks and to give you @@ -92,7 +92,7 @@ Key features: - Designed for [Jetpack Compose](https://developer.android.com/jetpack/compose) but it is not a restriction - May (if you like to) work as a navigation library - Explicit `Back` gesture management with the total control of yours -- Get rid of `SingleLiveEvent` for navigation, dialogs and even side-effects like toasts if you +- Get rid of `SingleLiveEvent` for navigation, dialogs and even side effects like toasts if you like to by completely describing the current UI state. ## v3.X Breaking change @@ -103,7 +103,7 @@ the `ProxyMachineState` now require to pass initial UI state in constructors. ## v4.X Breaking change -The view lifecycle [MachineLifecycle](commonstatemachine/src/commonMain/kotlin/com/motorro/commonstatemachine/lifecycle/MachineLifecycle.kt)) +The view lifecycle [MachineLifecycle](commonstatemachine/src/commonMain/kotlin/com/motorro/commonstatemachine/lifecycle/MachineLifecycle.kt) implementation is moved to the separate multiplatform library: ```groovy @@ -115,7 +115,7 @@ dependencies { ## Dependencies -The project has a very simple core to implement yourself but you could also grab the latest core +The project has a very simple core to implement yourself, but you could also grab the latest core version like that: ```groovy @@ -143,7 +143,7 @@ val commonMain by getting { ## Examples - [LCE](examples/lce) - basic example of Load-Content-Error application -- [Welcome](examples/welcome/welcome) - multi-module example of user on-boarding flow +- [Welcome](examples/welcome/app) - multi-module example of user on-boarding flow - [Parallel](examples/multi/parallel) - two machines running in parallel in one proxy state - [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 @@ -161,10 +161,10 @@ Let's break down business requirements... We have four application _logical_ states which correspond to _screen_ states for this application: -- Item list - the list of items to load is displayed. User clicks an item to load it's contents. +- Item list - the list of items to load is displayed. User clicks an item to load its contents. - Loading item - the network operation is running. User waits for operation to complete. - Item content - the loaded item content is displayed. User may return back to item list. -- Item load error - the load operation has failed and we have a choice to retry load or to quit the +- Item load error - the load operation has failed, and we have a choice to retry load or to quit the application. ### States and transitions @@ -186,17 +186,17 @@ Let's take a look at which `Gestures` each logical state processes and how they logical states: -| Logical state | Ui-State | Gesture/Event | Next state | Output | -| -------------------- | --------- | ------------- | ---------- | -------------------------------- | -| ItemList | ItemList | Back | Terminated | Finishes activity | -| | | ItemClicked | Loading | Loads requested item | -| Loading | Loading | Back | Item list | Cancels load and returns to list | -| | | onContent | Content | Displays loaded item | -| | | onError | Error | Displays load error | -| Content | Item | Back | Item list | Returns to the item list | -| Error | Error | Back | Item list | Returns to the item list | -| | | Retry | Loading | Retries load operation | -| | | Exit | Terminated | Finishes activity | +| Logical state | Ui-State | Gesture/Event | Next state | Output | +|---------------|----------|---------------|------------|----------------------------------| +| ItemList | ItemList | Back | Terminated | Finishes activity | +| | | ItemClicked | Loading | Loads requested item | +| Loading | Loading | Back | Item list | Cancels load and returns to list | +| | | onContent | Content | Displays loaded item | +| | | onError | Error | Displays load error | +| Content | Item | Back | Item list | Returns to the item list | +| Error | Error | Back | Item list | Returns to the item list | +| | | Retry | Loading | Retries load operation | +| | | Exit | Terminated | Finishes activity | Each logical state should be able to: @@ -211,7 +211,7 @@ state when logic falls behind what's relevant for this state. ### State machine -First of all we need some kind of a bridge between the current logical state and the outside world. +First of all we need some kind of bridge between the current logical state and the outside world. The [state machine](commonstatemachine/src/commonMain/kotlin/com/motorro/commonstatemachine/CommonStateMachine.kt) should be able to: @@ -219,14 +219,14 @@ should be able to: - Transition between states - Delegate gesture processing to the current state - Propagate UI-state changes to the outside world -- Clean-up all resources on shutdown +- Cleanup all resources on shutdown ![State machine](http://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/motorro/CommonStateMachine/master/doc/StateMachine.puml) Methods: - `process(gesture: G)` - Called by view upon user action. Delegated to current state. -- `clear()` - Called by view/framework to cleanup resources. Like in `onCleared` of `ViewModel`. +- `clear()` - Called by view/framework to clean up resources. Like in `onCleared` of `ViewModel`. - `setMachineState(machineState: CommonMachineState)`- Called by active state to transition to the new one. - `setUiState(uiState: U)` - Called by active state to update view. @@ -281,9 +281,9 @@ and two lifecycle methods: by replacing by the new state or when state-machine is about to be destroyed. The state lives between `doStart` and `doClear` calls. You could safely call interaction methods -and expect gesture processing calls within that period. Make sure to cleanup all your pending +and expect gesture processing calls within that period. Make sure to clean up all your pending operations in `doClear` handler. For example, the [CoroutineState](coroutines/src/commonMain/kotlin/com/motorro/commonstatemachine/coroutines/CoroutineState.kt) -provides you the `stateScope` coroutine scope that is being cancelled in `doClear`: +provides you the `stateScope` coroutine scope that is being canceled in `doClear`: ```kotlin abstract class CoroutineState: CommonMachineState() { @@ -303,7 +303,7 @@ concerns. Will talk about it later. #### Item list state -[ItemListState](examples/lce/src/main/java/com/motorro/statemachine/lce/model/state/ItemListState.kt) is a +[ItemListState](examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/ItemListState.kt) is a starting state for our application. It displays the list of items to load. The list is hardcoded for this example so we just emit a complete view-state when started: @@ -319,7 +319,7 @@ override fun doStart() { ``` Handle relevant gestures by transitioning the state-machine to the newly created states. -The `LoadingState` constructor accepts an id of item to load as an inter-state common data. +The `LoadingState` constructor accepts an id of item to load as an interstate common data. ```kotlin override fun doProcess(gesture: LceGesture) = when(gesture) { @@ -339,7 +339,7 @@ private fun onBack() { #### Item loading state -[LoadingState](examples/lce/src/main/java/com/motorro/statemachine/lce/model/state/LoadingState.kt) emulates +[LoadingState](examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/LoadingState.kt) emulates an asynchronous operation: ```kotlin @@ -360,7 +360,7 @@ private fun load() { ``` Depending on the item we pass the state transitions to either a `ContentState` or an `ErrorState` -passing either the mock content or the error occurred as an inter-state data: +passing either the mock content or the error occurred as an interstate data: ```kotlin private fun toContent() { @@ -374,7 +374,7 @@ private fun toError() { #### Item contents state -[ContentState](examples/lce/src/main/java/com/motorro/statemachine/lce/model/state/ContentState.kt) is very +[ContentState](examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/ContentState.kt) is very simple. It just sets the UI state to display data passed to the constructor and handles a `Back` gesture to return to the item list: @@ -394,10 +394,10 @@ private fun onBack() { ``` #### Error state -The [ErrorState](examples/lce/src/main/java/com/motorro/statemachine/lce/model/state/ErrorState.kt) gives a -user the ability to retry item load or to exit the app. Also it handles `Back` gesture to return to +The [ErrorState](examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/ErrorState.kt) gives a +user the ability to retry item load or to exit the app. Also, it handles `Back` gesture to return to the item list. Handling all user interactions through your state machine gives you a precise control -on what happens next. The item ID passed in the constructor as an inter-state data makes it possible +on what happens next. The item ID passed in the constructor as an interstate data makes it possible to preserve user's selection and to restart loading from scratch. ```kotlin @@ -436,7 +436,7 @@ class ErrorState(private val failed: ItemId, private val error: Throwable) : Lce #### Wiring with the application Now that we have all states in place let's connect them together with a state machine. We need some -place to retain a machine through the application flow so let's wrap it to the [Jetpack ViewModel](examples/lce/src/main/java/com/motorro/statemachine/lce/model/LceViewModel.kt) +place to retain a machine through the application flow so let's wrap it to the [Jetpack ViewModel](examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/model/LceViewModel.kt) which is common now: ```kotlin @@ -479,7 +479,7 @@ All we need to do here is: - to figure out the initial state that machine will start from - to wire ui-state and gesture processing with the outside world -And here is an abstract of the [view](examples/lce/src/main/java/com/motorro/statemachine/lce/ui/LceScreen.kt) +And here is an abstract of the [view](examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/ui/LceScreen.kt) that interacts with the model: ```kotlin @@ -513,17 +513,17 @@ fun LceScreen(onExit: @Composable () -> Unit) { } ``` -Compose library plays greatly here but you could easily adapt a fragment transaction or a +Compose library plays greatly here, but you could easily adapt a fragment transaction or a recycler view architecture as well. ### Result As you can see the state-machine pattern may be a good choice in implementing your MVI architecture. It produces a clean and easy to grasp step-by-step logic with well-separated concerns and easy and -[thorough](examples/lce/src/test/java/com/motorro/statemachine/lce/model/state) testing. The pattern also +[thorough](examples/lce/src/test/kotlin/com/motorro/commonstatemachine/examples/lce/model/state) testing. The pattern also attempts to be as non-opinionated as possible. Each state is a black-box with a defined contract and developers may choose the most suitable tools to implement each one without affecting the other. The -example above is a very basic one. However you could do things a bit more clean by using some of the +example above is a very basic one. However, you could do things a bit more clean by using some of the additional abstractions (see below). ## Tools and Handy abstractions to mix-in @@ -541,9 +541,9 @@ So let's introduce some abstractions that will lift the burden off the state's s By use-case I assume any business logic external to your view logic implemented in a state. Be it some network operation or some other "use-case" - provide it to your state and use them as you like. -There is nothing new here - I'm sure you already use the approach in your flavour of +There is nothing new here - I'm sure you already use the approach in your flavor of Clean Architecture or similar. Example of using an external use-case could be found in -[examples/welcome/welcome example](examples/welcome/login/src/main/java/com/motorro/statemachine/login/model/state/CredentialsCheckState.kt): +[examples/welcome/app example](examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/CredentialsCheckState.kt): ```kotlin class CredentialsCheckState(private val checkCredentials: CheckCredentials) { @@ -570,11 +570,11 @@ creates it's scope with `Dispatchers.Main.immediate`. Preparing the complex view-state from your state data might be a non-trivial task in applications with complex interface. Moving a coupling to the view-state and data structures from your state logic might be a good idea. Testing the exact view-state creation would be much easier if you make it -as more or less a clean function. Also your logic states may share the same rendering logic so +as more or less a clean function. Also, your logic states may share the same rendering logic so externalizing it would play greatly in terms of code reuse. For example the same view-state -rendering is used by [PasswordEntryState](examples/welcome/login/src/main/java/com/motorro/statemachine/login/model/state/PasswordEntryState.kt) -and [ErrorState](examples/welcome/login/src/main/java/com/motorro/statemachine/login/model/state/ErrorState.kt) of -examples/welcome/welcome example. You could inject your renderer in a state factory or get it from common context +rendering is used by [PasswordEntryState](examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/PasswordEntryState.kt) +and [ErrorState](examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/ErrorState.kt) of +examples/welcome/app example. You could inject your renderer in a state factory or get it from common context (see below). ### State factories and dependency provision @@ -585,7 +585,7 @@ is not a good idea in terms of coupling and dependency provision. The machine state, when created, may require three main classes of dependencies: - State-specific dependencies like use-cases state operates. -- Inter-state data e.g. data loaded in a previous state, common data state, etc. +- Interstate data e.g. data loaded in a previous state, common data state, etc. - Common dependencies for all states in machine: renderers, resource providers, factories You are free to choose the way to provide dependencies however let's take a look at the approach @@ -605,8 +605,8 @@ class CredentialsCheckState(private val checkCredentials: CheckCredentials) { /** * Dedicated state factory */ - @LoginScope - class Factory @Inject constructor(private val checkCredentials: CheckCredentials) { + @Factory + class StateFactory(private val checkCredentials: CheckCredentials) { operator fun invoke(): LoginState = CredentialsCheckState( checkCredentials ) @@ -616,7 +616,7 @@ class CredentialsCheckState(private val checkCredentials: CheckCredentials) { #### Inter-state data -By inter-state data I assume any dynamic data that is passed between states. It may be a product of +By interstate data I assume any dynamic data that is passed between states. It may be a product of some calculation, user-generated data, etc. The most obvious way is providing it through the state constructor: @@ -647,7 +647,7 @@ class CredentialsCheckState( Common dependencies may include renderers, state factories, common external interfaces and anything else that is required by all states that make up the state-machine. For convenience and to save the number of constructor params I suggest to bind them to some common interface and provide it as a -whole. Let's name it a common [Context](examples/welcome/login/src/main/java/com/motorro/statemachine/login/model/state/LoginContext.kt): +whole. Let's name it a common [Context](examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/LoginContext.kt): ```kotlin interface LoginContext { @@ -669,7 +669,7 @@ interface LoginContext { ``` Then you could provide it to your state through the constructor parameters. To make things even -easier let's make some [base state](examples/welcome/login/src/main/java/com/motorro/statemachine/login/model/state/LoginState.kt) +easier let's make some [base state](examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/LoginState.kt) for the state-machine assembly and use a delegation to provide each context dependency: ```kotlin @@ -678,13 +678,13 @@ abstract class LoginState( ): CoroutineState(), LoginContext by context { override fun doProcess(gesture: LoginGesture) { - Timber.w("Unsupported gesture: %s", gesture) + Napier.w("Unsupported gesture: ${gesture}") } } ``` -Thus every sub-class of the `LoginState` has any context dependency at hand by getting it from the -corresponding property as if the were provided explicitly: +Thus, every subclass of the `LoginState` has any context dependency at hand by getting it from the +corresponding property as if they were provided explicitly: ```kotlin class CredentialsCheckState( @@ -701,8 +701,8 @@ class CredentialsCheckState( /** * Factory updated to pass common context */ - @LoginScope - class Factory @Inject constructor(private val checkCredentials: CheckCredentials) { + @Factory + class StateFactory(private val checkCredentials: CheckCredentials) { operator fun invoke( context: LoginContext, data: LoginDataState @@ -720,7 +720,7 @@ class CredentialsCheckState( As I've already mentioned, creating new states explicitly to pass them to the state-machine later (like in the basic example) is not a good idea in terms of coupling and dependency provision. -Let's move it away from our machine states by introducing a common [factory interface](examples/welcome/login/src/main/java/com/motorro/statemachine/login/model/state/LoginStateFactory.kt) +Let's move it away from our machine states by introducing a common [factory interface](examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/LoginStateFactory.kt) that will take the responsibility to provide dependencies and abstract our state creation logic: ```kotlin @@ -744,24 +744,24 @@ interface LoginStateFactory { } ``` -Each factory method here will accept **only** the inter-state data providing both context and +Each factory method here will accept **only** the interstate data providing both context and state-specific dependencies implicitly. This will decouple state logic from the concrete -implementations and increase our [testability](examples/welcome/login/src/test/java/com/motorro/statemachine/login/model/state/BaseStateTest.kt) +implementations and increase our [testability](examples/welcome/login/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/BaseStateTest.kt) greatly. The exact factory implementation that binds together all data and dependencies will look like that: ```kotlin -@LoginScope -class LoginStateFactoryImpl @Inject constructor( - host: WelcomeFeatureHost, // External interface +@Factory(binds = [LoginFlowStarter::class]) +class LoginStateFactoryImpl( + @InjectedParam host: WelcomeFeatureHost, // External interface renderer: LoginRenderer, // Renderer - private val createCredentialsCheck: CredentialsCheckState.Factory // Concrete state factory + private val createCredentialsCheck: CredentialsCheckState.StateFactory // Concrete state factory ) : LoginStateFactory { // Dependencies common for each state provided through the context private val context: LoginContext = object : LoginContext { - override val factory: LoginStateFactory = this@Impl + override val factory: LoginStateFactory = this@LoginStateFactoryImpl override val host: WelcomeFeatureHost = host override val renderer: LoginRenderer = renderer } @@ -828,22 +828,21 @@ class CredentialsCheckStateTest { We can also provide the state factory to the `ViewModel` and use it to initialize our state-machine: ```kotlin -@HiltViewModel -class LoginViewModel @Inject constructor(private val factory: LoginStateFactory) : ViewModel() { +@KoinViewModel +class WelcomeViewModel(private val factory: WelcomeStateFactory) : ViewModel() { /** * Creates initializing state */ private fun initializeStateMachine(): CommonMachineState { // Obtain data required to start from a saved-state handle or injection - val commonData: LoginDataState = LoginDataState() - return factory.passwordEntry(commonData) + return factory.preload() } /** * State machine */ - private val stateMachine = FlowStateMachine(::initializeStateMachine) + private val stateMachine = FlowStateMachine(WelcomeUiState.Loading, ::initializeStateMachine) } ``` @@ -893,7 +892,7 @@ class WithIdleViewModel : ViewModel() { ## Multi-module applications -Let's take a more complicated example with a multi-screen flow like the [customer on-boarding](examples/welcome/welcome). +Let's take a more complicated example with a multiscreen flow like the [customer on-boarding](examples/welcome/app). ![Welcome flow](doc/screenshots/welcome/flow.png) The user is required to accept terms and conditions and to enter his email. Then the logic checks if he is already registered or a new customer and runs the appropriate flow to login or to register @@ -903,24 +902,22 @@ the work between teams. The state diagram would be the following: The project uses the following modules: -* **examples/welcome/welcome** - common flow: preloading, email entry, customer check, complete -* **commoncore** - common abstractions to build application: dispatchers, resources, etc. -* **commonapi** - common multi-platform module to connect the main app with modules -* **login** - login flow -* **commonregister** - multi-platform registration logic -* **register** - android view module for registration (separate because I've failed to implemented - it in android source of `commonregister` due to some multiplatform misconfiguration) +* **examples/welcome/app** - common flow: preloading, email entry, customer check, complete +* **examples/commoncore** - common abstractions to build application: dispatchers, resources, etc. +* **examples/welcome/commonapi** - common multi-platform module to connect the main app with modules +* **examples/welcome/login** - login flow +* **examples/welcome/register** - multi-platform registration flow logic and UI ### Common API -As you could see in the diagram above each `login` and `commonregister` start after the email is +As you could see in the diagram above each `login` and `register` start after the email is checked and the answer to user's registration status is obtained. The module flow starts from password entry screen though a bit different. Each module flow returns to the main flow either: - when flow completes succefully - transfers to `Complete` - when user hits `Back` - transfers back to email entry -Let's define the main flow interaction [API](examples/welcome/commonapi/src/commonMain/kotlin/com/motorro/statemachine/commonapi/welcome/model/state/WelcomeFeatureHost.kt) +Let's define the main flow interaction [API](examples/welcome/commonapi/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/commonapi/model/state/WelcomeFeatureHost.kt) then: ```kotlin interface WelcomeFeatureHost { @@ -952,13 +949,13 @@ interface LoginContext { ### Module flow -Each module has it's own sealed system of gesture/view-states: +Each module has its own sealed system of gesture/view-states: -| Module name | Gestures | UI-states | -| -------------- | --------------- | ----------------- | -| welcome | WelcomeGesture | WelcomeUiState | -| login | LoginGesture | LoginUiState | -| commonregister | RegisterGesture | RegisterUiState | +| Module name | Gestures | UI-states | +|----------------|-----------------|-----------------| +| welcome | WelcomeGesture | WelcomeUiState | +| login | LoginGesture | LoginUiState | +| register | RegisterGesture | RegisterUiState | Each module is completely independent in terms of gestures and UI states, and we also have a proprietary set of 'handy abstractions' for each module: renderers, factories, use-cases, etc. @@ -982,10 +979,10 @@ To adopt feature-module gestures there are at least two solutions: view-states. Though simple, the solution is not ideal as we lose the type-safe `when` exhaustive checks when we dispatch gestures in our states. So let's drop it... 2. Make a wrapping adapter that wraps the foreign gesture/view-state and unwrap it later when - passing them to concrete implementation. Thus we don't loose compiler support and type-safety. + passing them to concrete implementation. Thus, we don't lose compiler support and type-safety. Let's follow this route -Gesture [adapter](examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/data/WelcomeGesture.kt): +Gesture [adapter](examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/data/WelcomeGesture.kt): ```kotlin sealed class WelcomeGesture { // Native gestures... @@ -1004,7 +1001,7 @@ sealed class WelcomeGesture { } ``` -UI-state [adapter](examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/data/WelcomeUiState.kt): +UI-state [adapter](examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/data/WelcomeUiState.kt): ```kotlin sealed class WelcomeUiState { /** @@ -1025,7 +1022,7 @@ sealed class WelcomeUiState { Now let's build feature and host composables to take advantage of our adapters. -Feature [master-view](examples/welcome/login/src/main/java/com/motorro/statemachine/login/view/LoginScreen.kt): +Feature [master-view](examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/view/LoginScreen.kt): ```kotlin @Composable fun LoginScreen(state: LoginUiState, onGesture: (LoginGesture) -> Unit) { @@ -1038,13 +1035,14 @@ fun RegistrationScreen(state: RegisterUiState, onGesture: (RegisterGesture) -> U } ``` -Application [master-view](examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/view/WelcomeScreen.kt): +Application [master-view](examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/view/WelcomeScreen.kt): ```kotlin +@Composable fun WelcomeScreen(onTerminate: @Composable () -> Unit) { - val model = hiltViewModel() + val model = koinViewModel() val state = model.state.collectAsState(WelcomeUiState.Loading) - BackHandler(onBack = { model.process(Back) }) + BackHandler(onBack = { model.process(WelcomeGesture.Back) }) when (val uiState = state.value) { @@ -1053,13 +1051,13 @@ fun WelcomeScreen(onTerminate: @Composable () -> Unit) { // Render login screens is WelcomeUiState.Login -> LoginScreen( state = uiState.value, - onGesture = { model.process(Login(it)) } + onGesture = { model.process(WelcomeGesture.Login(it)) } ) // Render registration screens is WelcomeUiState.Register -> RegistrationScreen( state = uiState.value, - onGesture = { model.process(Register(it))} + onGesture = { model.process(WelcomeGesture.Register(it))} ) } } @@ -1101,7 +1099,7 @@ by running a _child_ state-machine inside the host state! Whenever a `ProxyMachineState` is started it launches it's internal instance of a state-machine bound to the feature gesture and view system. It also bridges two incompatible gesture/view systems by wrapping/unwrapping and adopting one system to another. Let's see the example of a login flow -[proxy](examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/LoginFlowState.kt) +[proxy](examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/LoginFlowState.kt) to make things clear: ```kotlin @@ -1119,8 +1117,7 @@ private typealias LoginProxy = ProxyMachineState< class LoginFlowState( private val context: WelcomeContext, private val data: WelcomeDataState, - private val loginComponentBuilder: LoginComponentBuilder -) : LoginProxy(Loading), WelcomeFeatureHost { +) : LoginProxy(LoginUiState.Loading), WelcomeFeatureHost, KoinComponent { /** * Should have valid email at this point @@ -1133,9 +1130,7 @@ class LoginFlowState( * Creates initial child state */ override fun init(): CommonMachineState { - val component = loginComponentBuilder.host(this).build() - val starter = EntryPoints.get(component, LoginEntryPoint::class.java).flowStarter() - + val starter = get { parametersOf(this as WelcomeFeatureHost) } return starter.start(email) } @@ -1174,7 +1169,7 @@ class LoginFlowState( To create a proxy you need to implement three core methods: -- `init()` - creates a starting state for a proxy state-machine. We fetch a [FlowStarter](examples/welcome/commonapi/src/commonMain/kotlin/com/motorro/statemachine/commonapi/welcome/model/state/FlowStarter.kt) +- `init()` - creates a starting state for a proxy state-machine. We fetch a [FlowStarter](examples/welcome/commonapi/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/commonapi/model/state/FlowStarter.kt) interface (which is just a feature state factory segregation) to create a starting state. - `mapGesture(parent: PG)` - maps a gesture from the parent system to the child system. You may unwrap the gesture we have implemented in the previous state, adopt one system to another as with @@ -1190,7 +1185,7 @@ this interface by switching host machine to email or complete states in correspo #### Common child flow API -For your convenience there are couple of ready-made interfaces to adopt child flow. +For your convenience there are a couple of ready-made interfaces to adopt child flow. The interfaces are located in a separate libraries: ```groovy @@ -1217,7 +1212,7 @@ 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 a basic [authentication flow](examples/di/api) 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. @@ -1242,21 +1237,21 @@ Take a look at the [books](examples/books) project to see how it works. ## 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 +In case you want several state-machines to run in parallel producing a single combined UI state, or you want to persist several machines on a single screen (like a page with a bottom navigation) there is an option to do it with the [MultiMachineState](commonstatemachine/src/commonMain/kotlin/com/motorro/commonstatemachine/multi/MultiMachineState.kt) and [ProxyMachineContainer](commonstatemachine/src/commonMain/kotlin/com/motorro/commonstatemachine/multi/ProxyMachineContainer.kt) ### MultiMachineState -This state is a proxy that holds several machines at once. It is in charge for combining the UI state +This state is a proxy that holds several machines at once. It is in charge of combining the UI state whenever the running machine updates and for dispatching gestures from a single parent gesture to proxied machines inside the composition. To distinguish machines and to ensure type-safety each machine in composition is identified with the [MachineKey](commonstatemachine/src/commonMain/kotlin/com/motorro/commonstatemachine/multi/MachineKey.kt) The state has three things to override: - [container](commonstatemachine/src/commonMain/kotlin/com/motorro/commonstatemachine/multi/MultiMachineState.kt#L28): - manages machines lifecycle. More on this follows. + manages machines lifecycle. More on these follows. - [mapUiState](commonstatemachine/src/commonMain/kotlin/com/motorro/commonstatemachine/multi/MultiMachineState.kt#L97): called each time your proxied machine updates UI state or explicitly when calling [updateUi](commonstatemachine/src/commonMain/kotlin/com/motorro/commonstatemachine/multi/MultiMachineState.kt#L69). Here you take a decision on changes and build a common resulting UI state. See the dedicated section below. @@ -1267,7 +1262,7 @@ Now let's see how the things work a bit closer. ### ProxyMachineContainer -Container is in charge for creating and managing the lifecycle of the state machines. So far the +Container is in charge of creating and managing the lifecycle of the state machines. So far the interface has two companion functions: - [allTogether](commonstatemachine/src/commonMain/kotlin/com/motorro/commonstatemachine/multi/ProxyMachineContainer.kt#L44): @@ -1315,7 +1310,7 @@ interface MachineInit { The `init` function is called each time the container needs to create a new machine. The [MachineLifecycle](commonstatemachine/src/commonMain/kotlin/com/motorro/commonstatemachine/lifecycle/MachineLifecycle.kt) interface passed to initialization may be used by your states to determine if the machine is suspended or active. If you use coroutines you could use [asFlow](coroutines/src/commonMain/kotlin/com/motorro/commonstatemachine/coroutines/lifecycle/lifecycleStateFlow.kt) -function to convert it to `Flow`. See [example](examples/timer/src/commonMain/kotlin/com/motorro/statemachine/timer/state/TimerState.kt) on how to start/stop +function to convert it to `Flow`. See [example](examples/timer/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/timer/state/TimerState.kt) on how to start/stop your pending operations that are not needed when your machine is inactive: gps tracking, server messaging, etc. For example: @@ -1354,8 +1349,8 @@ private open class TestState : MultiMachineState Check example states for each case: -- [Parallel](examples/multi/parallel/src/main/java/com/motorro/statemachine/parallel/model/state/ParallelState.kt) - two machines running in parallel in one proxy state -- [Navbar](examples/multi/navbar/src/main/java/com/motorro/statemachine/navbar/model/state/NavbarState.kt) - several machines running in proxy state, one of them active at a time +- [Parallel](examples/multi/parallel/src/main/kotlin/com/motorro/commonstatemachine/examples/parallel/model/state/ParallelState.kt) - two machines running in parallel in one proxy state +- [Navbar](examples/multi/navbar/src/main/kotlin/com/motorro/commonstatemachine/examples/navbar/model/state/NavbarState.kt) - several machines running in proxy state, one of them active at a time ### Mapping UI states @@ -1389,8 +1384,8 @@ private open class TestState : MultiMachineState() { Check example states for use cases: -- [Parallel](examples/multi/parallel/src/main/java/com/motorro/statemachine/parallel/model/state/ParallelState.kt) - two machines running in parallel in one proxy state -- [Navbar](examples/multi/navbar/src/main/java/com/motorro/statemachine/navbar/model/state/NavbarState.kt) - several machines running in proxy state, one of them active at a time +- [Parallel](examples/multi/parallel/src/main/kotlin/com/motorro/commonstatemachine/examples/parallel/model/state/ParallelState.kt) - two machines running in parallel in one proxy state +- [Navbar](examples/multi/navbar/src/main/kotlin/com/motorro/commonstatemachine/examples/navbar/model/state/NavbarState.kt) - several machines running in proxy state, one of them active at a time ### Dispatching gestures @@ -1428,27 +1423,27 @@ private open class TestState : MultiMachineState() { Check example states and test class for use cases: -- [Parallel](examples/multi/parallel/src/main/java/com/motorro/statemachine/parallel/model/state/ParallelState.kt) - two machines running in parallel in one proxy state -- [Navbar](examples/multi/navbar/src/main/java/com/motorro/statemachine/navbar/model/state/NavbarState.kt) - several machines running in proxy state, one of them active at a time +- [Parallel](examples/multi/parallel/src/main/kotlin/com/motorro/commonstatemachine/examples/parallel/model/state/ParallelState.kt) - two machines running in parallel in one proxy state +- [Navbar](examples/multi/navbar/src/main/kotlin/com/motorro/commonstatemachine/examples/navbar/model/state/NavbarState.kt) - several machines running in proxy state, one of them active at a time - [MultiMachineStateTest](commonstatemachine/src/commonTest/kotlin/com/motorro/commonstatemachine/multi/MultiMachineStateTest.kt) - unit test ### MachineLifecyle bonus -The interface used to pass the machine activity to proxied state machine could also be used as an -view lifecycle monitor for your app. Pass [UiMachineLifecycle](commonstatemachine/src/androidMain/kotlin/com/motorro/commonstatemachine/lifecycle/UiMachineLifecycle.kt) -to your model initialization to by able to suspend your machines when app is not in use. +The interface used to pass the machine activity to proxied state machine could also be used as a +view lifecycle monitor for your app. Pass [UiMachineLifecycle](lifecycle/src/commonMain/kotlin/com/motorro/commonstatemachine/lifecycle/UiMachineLifecycle.kt) +to your model initialization to be able to suspend your machines when app is not in use. Similar to [state collection methods](https://medium.com/androiddevelopers/a-safer-way-to-collect-flows-from-android-uis-23080b1f8bda) optimized with lificycle. Check the [example](examples/lifecycle) to get the details. ## Conclusion I hope someone finds the article (and the library if you like to take it as-is) helpful in building -complex multi-screen applications with multi-module ability. This approach aims to give you as much -freedom as possible to implement your flows. Of cause it is not a silver bullet but the flexibility +complex multiscreen applications with multi-module ability. This approach aims to give you as much +freedom as possible to implement your flows. Of course, it is not a silver bullet but the flexibility in structuring your app it promotes plays well in most scenarios. You could combine all your application steps in a single state flow or build separate models and inject them to the parts of your navigation library graph. And you could also use any architecture inside your states - simple coroutines to fetch the data, complex RxJava flows or even another MVI library in more complex cases. The library was created with multi-platform approach in mind as it contains no concrete platform -dependencies and coroutines extentions are optional. So you may create your view logic once and +dependencies and coroutines extensions are optional. So you may create your view logic once and adopt it's output to your platform view components. diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts new file mode 100644 index 0000000..442e236 --- /dev/null +++ b/build-logic/convention/build.gradle.kts @@ -0,0 +1,74 @@ +plugins { + `kotlin-dsl` +} + +group = "com.motorro.gradle.convention" + +dependencies { + compileOnly(libs.android.gradle.plugin) + compileOnly(libs.kotlin.gradle.plugin) + compileOnly(libs.compose.gradle.plugin) + compileOnly(libs.compose.multiplatform.gradle.plugin) +} + +tasks { + validatePlugins { + enableStricterValidation = true + failOnWarning = true + } +} + +tasks.withType().configureEach { + compilerOptions { + freeCompilerArgs.addAll(listOf( + "-opt-in=kotlin.RequiresOptIn", + "-Xinline-classes", + "-Xcontext-parameters" + )) + } +} + +gradlePlugin { + plugins { + register("baseLibrary"){ + id = "motorro.baseLibrary" + implementationClass = "BaseLibraryPlugin" + } + register("jvmLibrary"){ + id = "motorro.jvmLibrary" + implementationClass = "JvmLibraryPlugin" + } + register("androidLibrary"){ + id = "motorro.androidLibrary" + implementationClass = "AndroidLibraryPlugin" + } + register("androidComposeLibrary"){ + id = "motorro.androidComposeLibrary" + implementationClass = "AndroidComposeLibraryPlugin" + } + register("androidApplication"){ + id = "motorro.androidApplication" + implementationClass = "AndroidApplicationPlugin" + } + register("androidTest"){ + id = "motorro.androidTest" + implementationClass = "AndroidTestPlugin" + } + register("androidDynamicFreature"){ + id = "motorro.androidDynamicFeature" + implementationClass = "AndroidDynamicFeaturePlugin" + } + register("kotlinMultiplatform"){ + id = "motorro.kmp" + implementationClass = "KmpPlugin" + } + register("composeMultiplatform"){ + id = "motorro.cmp" + implementationClass = "CmpPlugin" + } + register("publishing"){ + id = "motorro.publishing" + implementationClass = "PublishingPlugin" + } + } +} \ No newline at end of file diff --git a/examples/androidcore/consumer-rules.pro b/build-logic/convention/consumer-rules.pro similarity index 100% rename from examples/androidcore/consumer-rules.pro rename to build-logic/convention/consumer-rules.pro diff --git a/build-logic/convention/proguard-rules.pro b/build-logic/convention/proguard-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationPlugin.kt new file mode 100644 index 0000000..ef40f78 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationPlugin.kt @@ -0,0 +1,95 @@ +import com.android.build.api.dsl.ApplicationExtension +import com.motorro.gradle.convention.addDependency +import com.motorro.gradle.convention.configureCommonAndroid +import com.motorro.gradle.convention.implementation +import com.motorro.gradle.convention.libs +import com.motorro.gradle.convention.testImplementation +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.extra + +class AndroidApplicationPlugin : Plugin { + override fun apply(targetProject: Project) = with(targetProject) { + val vName = rootProject.extra.get("versionName") as String + val vCode = rootProject.extra.get("versionCode") as Int + + println("== Application: **$name**, version: $vName($vCode) ==") + + // Core plugins + pluginManager.apply(libs.findPlugin("android_app").get().get().pluginId) + pluginManager.apply(libs.findPlugin("compose").get().get().pluginId) + + extensions.configure { + + configureCommonAndroid(this) + + defaultConfig { + minSdk = libs.findVersion("androidMinSdkVersion").get().requiredVersion.toInt() + targetSdk = libs.findVersion("androidTargetSdkVersion").get().requiredVersion.toInt() + versionName = vName + versionCode = vCode + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = true + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } + + compileOptions { + sourceCompatibility = Jvm.JAVA_VERSION + targetCompatibility = Jvm.JAVA_VERSION + + isCoreLibraryDesugaringEnabled = true + } + + testOptions { + unitTests { + isIncludeAndroidResources = true + all { + it.systemProperties["robolectric.pixelCopyRenderMode"] = "hardware" + } + } + } + } + + dependencies { + addDependency("coreLibraryDesugaring", "desugaring") + + implementation("androidx_core") + implementation("androidx_activity") + implementation("androidx_lifecycle_runtime") + implementation("androidx_lifecycle_viewmodel") + + implementation("kotlin_coroutines_core") + implementation("kotlin_coroutines_android") + + add("implementation", platform(libs.findLibrary("compose_bom").get())) + implementation("compose_ui") + implementation("compose_tooling_preview") + implementation("compose_material") + implementation("compose_activity") + implementation("compose_viewmodel") + implementation("compose_foundation") + implementation("compose_foundation_layouts") + + implementation("napier") + + addDependency("debugImplementation", "compose_tooling") + addDependency("debugImplementation", "test_compose_manifest") + + testImplementation("compose_test_junit") + } + } +} + diff --git a/build-logic/convention/src/main/kotlin/AndroidComposeLibraryPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidComposeLibraryPlugin.kt new file mode 100644 index 0000000..db0f439 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidComposeLibraryPlugin.kt @@ -0,0 +1,40 @@ +import com.android.build.api.dsl.LibraryExtension +import com.motorro.gradle.convention.addDependency +import com.motorro.gradle.convention.implementation +import com.motorro.gradle.convention.libs +import com.motorro.gradle.convention.testImplementation +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.dependencies + +class AndroidComposeLibraryPlugin : Plugin { + + override fun apply(targetProject: Project) = with(targetProject) { + // Core plugin + pluginManager.apply("motorro.androidLibrary") + pluginManager.apply(libs.findPlugin("compose").get().get().pluginId) + + extensions.configure { + buildFeatures { + compose = true + } + } + + dependencies { + add("implementation", platform(libs.findLibrary("compose_bom").get())) + + implementation("compose_runtime") + implementation("compose_ui") + implementation("compose_tooling_preview") + implementation("compose_foundation") + implementation("compose_foundation_layouts") + implementation("compose_material") + + addDependency("debugImplementation", "compose_tooling") + addDependency("debugImplementation", "test_compose_manifest") + + testImplementation("compose_test_junit") + } + } +} diff --git a/build-logic/convention/src/main/kotlin/AndroidDynamicFeaturePlugin.kt b/build-logic/convention/src/main/kotlin/AndroidDynamicFeaturePlugin.kt new file mode 100644 index 0000000..482a193 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidDynamicFeaturePlugin.kt @@ -0,0 +1,43 @@ +import com.android.build.api.dsl.DynamicFeatureExtension +import com.motorro.gradle.convention.configureCommonAndroid +import com.motorro.gradle.convention.libs +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure + +class AndroidDynamicFeaturePlugin : Plugin { + + override fun apply(targetProject: Project) = with(targetProject) { + // Core plugins + pluginManager.apply(libs.findPlugin("android_dynamic_feature").get().get().pluginId) + + extensions.configure { + + configureCommonAndroid(this) + + defaultConfig { + minSdk = libs.findVersion("androidMinSdkVersion").get().requiredVersion.toInt() + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } + + compileOptions { + sourceCompatibility = Jvm.JAVA_VERSION + targetCompatibility = Jvm.JAVA_VERSION + + isCoreLibraryDesugaringEnabled = true + } + + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } + } + } +} diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryPlugin.kt new file mode 100644 index 0000000..85ef16e --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryPlugin.kt @@ -0,0 +1,58 @@ +import com.android.build.api.dsl.LibraryExtension +import com.motorro.gradle.convention.configureCommonAndroid +import com.motorro.gradle.convention.libs +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure + +class AndroidLibraryPlugin : Plugin { + override fun apply(targetProject: Project) = with(targetProject) { + // First base library. + targetProject.pluginManager.apply("motorro.baseLibrary") + + // Core plugins + pluginManager.apply(libs.findPlugin("android_lib").get().get().pluginId) + + extensions.configure { + + configureCommonAndroid(this) + + androidResources.enable = true + + defaultConfig { + minSdk = libs.findVersion("androidMinSdkVersion").get().requiredVersion.toInt() + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } + + compileOptions { + sourceCompatibility = Jvm.JAVA_VERSION + targetCompatibility = Jvm.JAVA_VERSION + + isCoreLibraryDesugaringEnabled = true + } + + testOptions { + unitTests { + isIncludeAndroidResources = true + all { + it.systemProperties["robolectric.pixelCopyRenderMode"] = "hardware" + } + } + } + } + } +} diff --git a/build-logic/convention/src/main/kotlin/AndroidTestPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidTestPlugin.kt new file mode 100644 index 0000000..f6c282a --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidTestPlugin.kt @@ -0,0 +1,35 @@ +import com.android.build.api.dsl.TestExtension +import com.motorro.gradle.convention.configureKotlin +import com.motorro.gradle.convention.defaultNamespace +import com.motorro.gradle.convention.libs +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure + +class AndroidTestPlugin : Plugin { + override fun apply(targetProject: Project) = with(targetProject) { + // Core plugins + pluginManager.apply(libs.findPlugin("android_test").get().get().pluginId) + + extensions.configure { + // Take from module name + namespace = defaultNamespace() + + compileSdk = libs.findVersion("androidCompileSdkVersion").get().requiredVersion.toInt() + + configureKotlin() + + defaultConfig { + minSdk = libs.findVersion("androidMinTestSdk").get().requiredVersion.toInt() + targetSdk = libs.findVersion("androidTargetSdkVersion").get().requiredVersion.toInt() + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = "EMULATOR" + } + + compileOptions { + sourceCompatibility = Jvm.JAVA_VERSION + targetCompatibility = Jvm.JAVA_VERSION + } + } + } +} diff --git a/build-logic/convention/src/main/kotlin/BaseLibraryPlugin.kt b/build-logic/convention/src/main/kotlin/BaseLibraryPlugin.kt new file mode 100644 index 0000000..e3fcabb --- /dev/null +++ b/build-logic/convention/src/main/kotlin/BaseLibraryPlugin.kt @@ -0,0 +1,21 @@ +import com.motorro.gradle.convention.libDesc +import com.motorro.gradle.convention.libId +import com.motorro.gradle.convention.libName +import org.gradle.api.Plugin +import org.gradle.api.Project + +/** + * Base library plugin + */ +class BaseLibraryPlugin : Plugin { + override fun apply(targetProject: Project) = with(targetProject) { + group = rootProject.group + version = rootProject.version + + println("== Library: **$name**, version: $version ==") + println("===> Lib id: $libId") + println("===> Lib name: $libName") + println("===> Lib desc: ${libDesc ?: "--"}") + } +} + diff --git a/build-logic/convention/src/main/kotlin/CmpPlugin.kt b/build-logic/convention/src/main/kotlin/CmpPlugin.kt new file mode 100644 index 0000000..1101eed --- /dev/null +++ b/build-logic/convention/src/main/kotlin/CmpPlugin.kt @@ -0,0 +1,57 @@ +import com.motorro.gradle.convention.defaultNamespace +import com.motorro.gradle.convention.libs +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.dependencies +import org.jetbrains.compose.ComposeExtension +import org.jetbrains.compose.resources.ResourcesExtension +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension + +class CmpPlugin : Plugin { + + override fun apply(targetProject: Project) = with(targetProject) { + // First base KMP convention. + pluginManager.apply("motorro.kmp") + + // Then Compose side. + pluginManager.apply(libs.findPlugin("compose").get().get().pluginId) + pluginManager.apply(libs.findPlugin("compose_multiplatform").get().get().pluginId) + pluginManager.apply(libs.findPlugin("compose_hot_reload").get().get().pluginId) + + extensions.configure { + + sourceSets.apply { + commonMain.dependencies { + implementation(libs.findLibrary("compose_multiplatform_runtime").get()) + implementation(libs.findLibrary("compose_multiplatform_ui").get()) + implementation(libs.findLibrary("compose_multiplatform_foundation").get()) + implementation(libs.findLibrary("compose_multiplatform_material3").get()) + + // KMP resources (Res.drawable.*) + implementation(libs.findLibrary("compose_multiplatform_resources").get()) + + // Enable KMP @Preview for @Compose functions. + implementation(libs.findLibrary("compose_multiplatform_preview").get()) + } + + // Preview / tooling for only Android. + androidMain.dependencies { + implementation(libs.findLibrary("compose_tooling").get()) + } + } + } + + dependencies { + add("androidRuntimeClasspath", libs.findLibrary("compose_multiplatform_tooling").get()) + } + + extensions.configure { + configure { + publicResClass = false + packageOfResClass = defaultNamespace() + generateResClass = always + } + } + } +} \ No newline at end of file diff --git a/build-logic/convention/src/main/kotlin/Jvm.kt b/build-logic/convention/src/main/kotlin/Jvm.kt new file mode 100644 index 0000000..787c0bf --- /dev/null +++ b/build-logic/convention/src/main/kotlin/Jvm.kt @@ -0,0 +1,8 @@ +import org.gradle.api.JavaVersion +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +internal object Jvm { + const val JDK_VERSION = 21 + val JVM_TARGET = JvmTarget.JVM_21 + val JAVA_VERSION = JavaVersion.VERSION_21 +} \ No newline at end of file diff --git a/build-logic/convention/src/main/kotlin/JvmPlugin.kt b/build-logic/convention/src/main/kotlin/JvmPlugin.kt new file mode 100644 index 0000000..1c5ff83 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/JvmPlugin.kt @@ -0,0 +1,17 @@ +import com.motorro.gradle.convention.configureKotlinJvm +import com.motorro.gradle.convention.libs +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.internal.Actions.with + +class JvmPlugin : Plugin { + override fun apply(targetProject: Project) { + // First base library. + targetProject.pluginManager.apply("motorro.baseLibrary") + + with(targetProject) { + pluginManager.apply(libs.findPlugin("kotlin_jvm").get().get().pluginId) + configureKotlinJvm() + } + } +} \ No newline at end of file diff --git a/build-logic/convention/src/main/kotlin/KmpPlugin.kt b/build-logic/convention/src/main/kotlin/KmpPlugin.kt new file mode 100644 index 0000000..7bfbe43 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/KmpPlugin.kt @@ -0,0 +1,98 @@ +import com.motorro.gradle.convention.androidLibrary +import com.motorro.gradle.convention.defaultNamespace +import com.motorro.gradle.convention.frameworkBaseName +import com.motorro.gradle.convention.libs +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension + +class KmpPlugin : Plugin { + override fun apply(targetProject: Project) = with(targetProject) { + // First base library. + targetProject.pluginManager.apply("motorro.baseLibrary") + + // Core KMP plugins + pluginManager.apply(libs.findPlugin("kotlin_multiplatform").get().get().pluginId) + pluginManager.apply(libs.findPlugin("android_kotlin_multiplatform_library").get().get().pluginId) + + extensions.configure { + jvmToolchain(Jvm.JDK_VERSION) + + compilerOptions { + jvm { + compilerOptions.jvmTarget.set(Jvm.JVM_TARGET) + } + compilerOptions.optIn.addAll(Kotlin.optIn) + compilerOptions.freeCompilerArgs.addAll(Kotlin.compilerArgs) + } + + androidLibrary { + namespace = defaultNamespace() + compileSdk = libs.findVersion("androidCompileSdkVersion").get().requiredVersion.toInt() + minSdk = libs.findVersion("androidMinSdkVersion").get().requiredVersion.toInt() + androidResources.enable = true + compilerOptions.jvmTarget.set(Jvm.JVM_TARGET) + + withHostTest { + isIncludeAndroidResources = true + } + + @Suppress("UnstableApiUsage") + optimization { + consumerKeepRules.apply { + publish = true + file("consumer-rules.pro") + } + } + } + + // iOS targets + listOf( + iosArm64(), + iosSimulatorArm64() + ).forEach { + it.binaries.framework { + baseName = project.frameworkBaseName() + isStatic = true + } + } + + js { + outputModuleName.set(project.frameworkBaseName()) + browser() + binaries.executable() + binaries.library() + generateTypeScriptDefinitions() + compilerOptions { + target.set("es2015") + } + } + + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + browser() + binaries.executable() + binaries.library() + useCommonJs() + } + + sourceSets.apply { + commonMain { + dependencies { + implementation(libs.findLibrary("kotlin_annotations").get()) + } + } + jvmMain.dependencies { + + } + commonTest { + dependencies { + implementation(libs.findLibrary("test_kotlin").get()) + } + } + } + } + } +} diff --git a/build-logic/convention/src/main/kotlin/Kotlin.kt b/build-logic/convention/src/main/kotlin/Kotlin.kt new file mode 100644 index 0000000..9f65d28 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/Kotlin.kt @@ -0,0 +1,13 @@ +/** + * Kotlin options + */ +object Kotlin { + val optIn = listOf( + "kotlin.RequiresOptIn", + "kotlinx.coroutines.ExperimentalCoroutinesApi", + "kotlin.time.ExperimentalTime" + ) + val compilerArgs = listOf( + "-Xexpect-actual-classes" + ) +} \ No newline at end of file diff --git a/build-logic/convention/src/main/kotlin/PublishingPlugin.kt b/build-logic/convention/src/main/kotlin/PublishingPlugin.kt new file mode 100644 index 0000000..be7a83c --- /dev/null +++ b/build-logic/convention/src/main/kotlin/PublishingPlugin.kt @@ -0,0 +1,77 @@ +import com.motorro.gradle.convention.libDesc +import com.motorro.gradle.convention.libId +import com.motorro.gradle.convention.libName +import com.motorro.gradle.convention.libs +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.publish.PublishingExtension +import org.gradle.api.publish.maven.MavenPublication +import org.gradle.api.publish.maven.tasks.AbstractPublishToMaven +import org.gradle.api.tasks.bundling.Jar +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.getByType +import org.gradle.kotlin.dsl.register +import org.gradle.kotlin.dsl.withType +import org.gradle.plugins.signing.Sign +import org.gradle.plugins.signing.SigningExtension + +class PublishingPlugin : Plugin { + override fun apply(targetProject: Project) = with(targetProject) { + // Apply plugins + pluginManager.apply(libs.findPlugin("kotlin_dokka").get().get().pluginId) + pluginManager.apply("maven-publish") + pluginManager.apply("signing") + + val javadocJar = tasks.register("javadocJar") { + val dokkaGenerate = tasks.named("dokkaGenerate") + dependsOn(dokkaGenerate) + group = "documentation" + archiveClassifier.set("javadoc") + from(dokkaGenerate) + } + + extensions.configure { + publications.withType { + artifact(javadocJar) + pom { + name.set(provider { + libName ?: libId ?: name.toString() + }) + description.set(provider { + libDesc ?: libName ?: "" + }) + 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) + } + } + } + } + + extensions.configure { + useInMemoryPgpKeys(signingKey, signingPassword) + val publishing = extensions.getByType() + sign(publishing.publications) + } + + val signingTasks = tasks.withType() + tasks.withType().configureEach { + dependsOn(signingTasks) + } + } +} diff --git a/build-logic/convention/src/main/kotlin/com/motorro/gradle/convention/configureCommonAndroid.kt b/build-logic/convention/src/main/kotlin/com/motorro/gradle/convention/configureCommonAndroid.kt new file mode 100644 index 0000000..091b272 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/com/motorro/gradle/convention/configureCommonAndroid.kt @@ -0,0 +1,25 @@ +package com.motorro.gradle.convention + +import com.android.build.api.dsl.CommonExtension +import org.gradle.api.Project +import org.gradle.kotlin.dsl.dependencies + +internal fun Project.configureCommonAndroid(ext: CommonExtension) = ext.apply { + + // Take from module name + namespace = defaultNamespace() + + compileSdk = libs.findVersion("androidCompileSdkVersion").get().requiredVersion.toInt() + + configureKotlin() + + dependencies { + addDependency("coreLibraryDesugaring", "desugaring") + implementation("kotlin_coroutines_core") + + testImplementation("test_kotlin") + testImplementation("test_kotlin_coroutines") + testImplementation("test_junit") + testImplementation("test_mockk") + } +} \ No newline at end of file diff --git a/build-logic/convention/src/main/kotlin/com/motorro/gradle/convention/configureKotlin.kt b/build-logic/convention/src/main/kotlin/com/motorro/gradle/convention/configureKotlin.kt new file mode 100644 index 0000000..77faa32 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/com/motorro/gradle/convention/configureKotlin.kt @@ -0,0 +1,33 @@ +package com.motorro.gradle.convention + +import Jvm +import Kotlin +import org.gradle.api.Project +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +internal fun Project.configureKotlin() { + tasks.withType().configureEach { + compilerOptions { + jvmTarget.set(Jvm.JVM_TARGET) + compilerOptions.optIn.addAll(Kotlin.optIn) + compilerOptions.freeCompilerArgs.addAll(Kotlin.compilerArgs) + } + } +} + +internal fun Project.configureKotlinJvm() { + extensions.configure { + sourceCompatibility = Jvm.JAVA_VERSION + targetCompatibility = Jvm.JAVA_VERSION + } + + extensions.configure { + jvmToolchain(Jvm.JDK_VERSION) + } + + configureKotlin() +} \ No newline at end of file diff --git a/build-logic/convention/src/main/kotlin/com/motorro/gradle/convention/configureMultiplatformAndroidLibrary.kt b/build-logic/convention/src/main/kotlin/com/motorro/gradle/convention/configureMultiplatformAndroidLibrary.kt new file mode 100644 index 0000000..aa5a488 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/com/motorro/gradle/convention/configureMultiplatformAndroidLibrary.kt @@ -0,0 +1,9 @@ +package com.motorro.gradle.convention + +import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget +import org.gradle.kotlin.dsl.configure +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension + +internal fun KotlinMultiplatformExtension.androidLibrary(block: KotlinMultiplatformAndroidLibraryTarget.() -> Unit) { + configure(block) +} \ No newline at end of file diff --git a/build-logic/convention/src/main/kotlin/com/motorro/gradle/convention/dependencies.kt b/build-logic/convention/src/main/kotlin/com/motorro/gradle/convention/dependencies.kt new file mode 100644 index 0000000..a3b8a45 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/com/motorro/gradle/convention/dependencies.kt @@ -0,0 +1,37 @@ +package com.motorro.gradle.convention + +import org.gradle.api.Project +import org.gradle.api.artifacts.VersionCatalog +import org.gradle.api.artifacts.VersionCatalogsExtension +import org.gradle.kotlin.dsl.DependencyHandlerScope +import org.gradle.kotlin.dsl.getByType + +val Project.libs get(): VersionCatalog = extensions.getByType().named("libs") + +context(project: Project) +fun DependencyHandlerScope.addDependency(configurationName: String, dependencyName: String) { + project.libs.findLibrary(dependencyName).ifPresent { + add(configurationName, it) + } +} + +context(project: Project) +fun DependencyHandlerScope.api(dependencyName: String) { + project.libs.findLibrary(dependencyName).ifPresent { + add("api", it) + } +} + +context(project: Project) +fun DependencyHandlerScope.implementation(dependencyName: String) { + project.libs.findLibrary(dependencyName).ifPresent { + add("implementation", it) + } +} + +context(project: Project) +fun DependencyHandlerScope.testImplementation(dependencyName: String) { + project.libs.findLibrary(dependencyName).ifPresent { + add("testImplementation", it) + } +} \ No newline at end of file diff --git a/build-logic/convention/src/main/kotlin/com/motorro/gradle/convention/library.kt b/build-logic/convention/src/main/kotlin/com/motorro/gradle/convention/library.kt new file mode 100644 index 0000000..da2cc80 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/com/motorro/gradle/convention/library.kt @@ -0,0 +1,18 @@ +package com.motorro.gradle.convention + +import org.gradle.api.Project + +/** + * Library ID + */ +val Project.libId: String get() = findProperty("libId") as? String ?: frameworkBaseName() + +/** + * Library Name + */ +val Project.libName: String get() = findProperty("libName") as? String ?: libId + +/** + * Library Description + */ +val Project.libDesc: String? get() = findProperty("libDesc") as? String diff --git a/build-logic/convention/src/main/kotlin/com/motorro/gradle/convention/naming.kt b/build-logic/convention/src/main/kotlin/com/motorro/gradle/convention/naming.kt new file mode 100644 index 0000000..0f3acca --- /dev/null +++ b/build-logic/convention/src/main/kotlin/com/motorro/gradle/convention/naming.kt @@ -0,0 +1,23 @@ +package com.motorro.gradle.convention + +import org.gradle.api.Project + +internal fun Project.defaultNamespace(): String { + val prefix = "com.motorro.commonstatemachine" + val suffix = path + .trim(':') + .split(':') + .joinToString(".") { it.lowercase().replace("-", "_") } + .ifBlank { name.lowercase().replace("-", "_") } + + return "$prefix.$suffix" +} + +internal fun Project.frameworkBaseName(): String { + val readable = path.trim(':') + .split(':') + .joinToString("_") { it.replace("-", "_") } + .ifBlank { name } + + return readable +} \ No newline at end of file diff --git a/build-logic/convention/src/main/kotlin/com/motorro/gradle/convention/properties.kt b/build-logic/convention/src/main/kotlin/com/motorro/gradle/convention/properties.kt new file mode 100644 index 0000000..af24aca --- /dev/null +++ b/build-logic/convention/src/main/kotlin/com/motorro/gradle/convention/properties.kt @@ -0,0 +1,57 @@ +package com.motorro.gradle.convention + +import org.gradle.api.Project +import org.gradle.api.provider.Property +import org.gradle.kotlin.dsl.extra +import org.gradle.kotlin.dsl.getByType +import java.io.File +import java.util.Properties +import kotlin.reflect.KClass + +/** + * Gets extension property + */ +context(project: Project) +inline fun KClass.extensionProperty(block: E.() -> Property): T { + return project.extensions.getByType().block().get() +} + +/** + * Takes extra property + */ +inline fun Project.extraProperty(name: String): T? = if (extra.has("versionCode")) extra.get("versionCode") as T else null + +/** + * Extra or root project extra property + */ +inline fun Project.findExtraProperty(name: String): T? = extraProperty(name) ?: rootProject.extraProperty(name) + +private fun Project.loadLocalProperties(): Properties { + val props = Properties() + val localPropsFile = File(rootProject.rootDir, "local.properties") + if (localPropsFile.exists()) { + localPropsFile.inputStream().use { props.load(it) } + val signingConfigPath = props.getProperty("SIGNING_CONFIG") + if (!signingConfigPath.isNullOrBlank()) { + val signingConfigFile = File(signingConfigPath) + if (signingConfigFile.exists()) { + signingConfigFile.inputStream().use { props.load(it) } + } + } + } + return props +} + +fun Project.getProjectProperty(propertyName: String, envName: String): String? { + val fromProperty = findProperty(propertyName) as? String + if (!fromProperty.isNullOrBlank()) return fromProperty + + val localProps = loadLocalProperties() + val fromLocalProps = localProps.getProperty(propertyName) + if (!fromLocalProps.isNullOrBlank()) return fromLocalProps + + val fromEnv = System.getenv(envName) + if (!fromEnv.isNullOrBlank()) return fromEnv + + return null +} diff --git a/build-logic/convention/src/main/kotlin/publishingProperties.kt b/build-logic/convention/src/main/kotlin/publishingProperties.kt new file mode 100644 index 0000000..d1188ca --- /dev/null +++ b/build-logic/convention/src/main/kotlin/publishingProperties.kt @@ -0,0 +1,30 @@ +import com.motorro.gradle.convention.getProjectProperty +import org.gradle.api.Project +import org.gradle.kotlin.dsl.extra + +val Project.developerId: String? + get() = (findProperty("developerId") as? String) ?: if (rootProject.extra.has("developerId")) rootProject.extra.get("developerId") as String else null + +val Project.developerName: String? + get() = (findProperty("developerName") as? String) ?: if (rootProject.extra.has("developerName")) rootProject.extra.get("developerName") as String else null + +val Project.developerEmail: String? + get() = (findProperty("developerEmail") as? String) ?: if (rootProject.extra.has("developerEmail")) rootProject.extra.get("developerEmail") as String else null + +val Project.projectScm: String? + get() = (findProperty("projectScm") as? String) ?: if (rootProject.extra.has("projectScm")) rootProject.extra.get("projectScm") as String else null + +val Project.projectUrl: String? + get() = (findProperty("projectUrl") as? String) ?: if (rootProject.extra.has("projectUrl")) rootProject.extra.get("projectUrl") as String else null + +val Project.signingKey: String? + get() = getProjectProperty("signingKey", "SIGNING_KEY") + +val Project.signingPassword: String? + get() = getProjectProperty("signingPassword", "SIGNING_PASSWORD") + +val Project.ossrhUsername: String? + get() = getProjectProperty("ossrhUsername", "OSSRH_USERNAME") + +val Project.ossrhPassword: String? + get() = getProjectProperty("ossrhPassword", "OSSRH_PASSWORD") diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts new file mode 100644 index 0000000..b11df90 --- /dev/null +++ b/build-logic/settings.gradle.kts @@ -0,0 +1,23 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} + +rootProject.name = "build-logic" +include(":convention") diff --git a/build.gradle.kts b/build.gradle.kts index 8769240..865fab4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,9 +13,9 @@ @file:Suppress("unused") -import groovy.lang.Closure +import com.motorro.gradle.convention.buildVersionCode +import com.motorro.gradle.convention.buildVersionName import org.gradle.api.tasks.testing.logging.TestExceptionFormat -import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile plugins { alias(libs.plugins.android.app) apply false @@ -26,6 +26,7 @@ plugins { alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.google.ksp) apply false alias(libs.plugins.compose) apply false + alias(libs.plugins.compose.multiplatform) apply false alias(libs.plugins.kotlin.dokka) apply false alias(libs.plugins.hilt) apply false alias(libs.plugins.koin) apply false @@ -33,14 +34,6 @@ plugins { alias(libs.plugins.git) } -apply { - from("$rootDir/gradle/versioning.gradle") - from("$rootDir/gradle/maven-publish-config.gradle") -} - -private val buildVersionName: Closure by ext -private val buildVersionCode: Closure by ext - version = buildVersionName() group = "com.motorro.commonstatemachine" description = "Multiplatform state machine for mobile applications" @@ -62,13 +55,6 @@ allprojects { val projectScm by extra("https://github.com/motorro/CommonStateMachine.git") val projectUrl by extra("https://github.com/motorro/CommonStateMachine") - tasks.withType>().configureEach { - compilerOptions.freeCompilerArgs.addAll(listOf( - "-opt-in=kotlin.RequiresOptIn", - "-Xexplicit-backing-fields" - )) - } - tasks.withType().configureEach { forkEvery = 100 testLogging { @@ -115,9 +101,9 @@ tasks.register("runLceExampleUnitTests") { } tasks.register("runWelcomeExampleUnitTests") { - dependsOn(":examples:welcome:commonregister:allTests") + dependsOn(":examples:welcome:register:allTests") dependsOn(":examples:welcome:login:testDebugUnitTest") - dependsOn(":examples:welcome:welcome:testDebugUnitTest") + dependsOn(":examples:welcome:app:testDebugUnitTest") description = "Run unit tests for welcome app." } @@ -128,7 +114,7 @@ tasks.register("runTimerExampleUnitTests") { tasks.register("runDiExampleUnitTests") { dependsOn(":examples:di:login:testDebugUnitTest") - dependsOn(":examples:di:social:testDebugUnitTest") + dependsOn(":examples:di:app:testLoginDebugUnitTest") description = "Run unit tests for di app." } @@ -139,6 +125,7 @@ tasks.register("runBooksExampleUnitTests") { } tasks.register("runSkillsExampleUnitTests") { + dependsOn(":examples:skills:app:testDebugUnitTest") dependsOn(":examples:skills:auth:implementation:allTests") description = "Run unit tests for skills." } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 0000000..a1e8f84 --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + `kotlin-dsl` +} + +repositories { + google() + mavenCentral() + gradlePluginPortal() +} diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts new file mode 100644 index 0000000..6060510 --- /dev/null +++ b/buildSrc/settings.gradle.kts @@ -0,0 +1,14 @@ +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} + +rootProject.name = "buildSrc" diff --git a/buildSrc/src/main/kotlin/com/motorro/gradle/convention/Versioning.kt b/buildSrc/src/main/kotlin/com/motorro/gradle/convention/Versioning.kt new file mode 100644 index 0000000..07d8f65 --- /dev/null +++ b/buildSrc/src/main/kotlin/com/motorro/gradle/convention/Versioning.kt @@ -0,0 +1,134 @@ +package com.motorro.gradle.convention + +import org.gradle.api.GradleException +import org.gradle.api.Project + +val platformVersionCodeAdditives: Map = mapOf( + "armeabi" to 1, + "armeabi-v7a" to 2, + "arm64-v8a" to 3, + "x86" to 4, + "x86_64" to 5, + "mips" to 6, + "mips64" to 7 +) + +private fun getGitDescribe(project: Project): String? { + val grgit = project.findProperty("grgit") ?: project.rootProject.findProperty("grgit") + if (grgit != null) { + try { + val describeMethod = grgit.javaClass.getMethod("describe", Map::class.java) + val result = describeMethod.invoke(grgit, mapOf("match" to listOf("v[0-9]*"))) as? String + if (!result.isNullOrBlank()) return result + } catch (e: Exception) { + project.logger.info("Failed to invoke grgit.describe: {}", e.message) + } + } + return try { + val process = ProcessBuilder("git", "describe", "--match", "v[0-9]*") + .directory(project.rootDir) + .redirectError(ProcessBuilder.Redirect.DISCARD) + .start() + val output = process.inputStream.bufferedReader().readText().trim() + if (process.waitFor() == 0 && output.isNotEmpty()) output else null + } catch (e: Exception) { + null + } +} + +private fun getGitBranchName(project: Project): String? { + val grgit = project.findProperty("grgit") ?: project.rootProject.findProperty("grgit") + if (grgit != null) { + try { + val branchProp = grgit.javaClass.getMethod("getBranch").invoke(grgit) + val currentMethod = branchProp?.javaClass?.getMethod("current") + val currentBranch = currentMethod?.invoke(branchProp) + val nameMethod = currentBranch?.javaClass?.getMethod("getName") + val name = nameMethod?.invoke(currentBranch) as? String + if (!name.isNullOrBlank()) return name + } catch (e: Exception) { + project.logger.info("Failed to invoke grgit.branch: {}", e.message) + } + } + return try { + val process = ProcessBuilder("git", "rev-parse", "--abbrev-ref", "HEAD") + .directory(project.rootDir) + .redirectError(ProcessBuilder.Redirect.DISCARD) + .start() + val output = process.inputStream.bufferedReader().readText().trim() + if (process.waitFor() == 0 && output.isNotEmpty() && output != "HEAD") output else null + } catch (e: Exception) { + null + } +} + +/** + * Builds app version using latest repository tag + * Tag should be a semantic version of major/minor at least + * Function takes number of commits since last tag and uses it to calculate patch number + * - If patch is omitted in tag - uses number of commits + * - If patch version set in tag - pluses number of commits to patch number + * @return Semantic version string + */ +fun Project.buildVersionName(): String { + val versionRegexp = Regex("""^v?(\d+)\.(\d+)\.?(\d*)-?(\d*)""") + + val fromEnv = (findProperty("versionName") as? String) ?: System.getenv("VERSION_NAME") + if (!fromEnv.isNullOrBlank() && versionRegexp.containsMatchIn(fromEnv)) { + logger.info("Version from ENV: {}", fromEnv) + return fromEnv + } + + val described = getGitDescribe(this) + val matchResult = if (described != null) versionRegexp.find(described) else null + + val (major, minor, patch, changes) = if (matchResult == null) { + logger.info("Git description does not have any version info. Make sure you have a semver tag (v1.2) or (v1.2.3) in a repository.") + logger.info("Using `v0.0.1` for this build") + listOf(0, 0, 1, 0) + } else { + val groups = matchResult.groupValues + val maj = groups[1].toIntOrNull() ?: 0 + val min = groups[2].toIntOrNull() ?: 0 + val pat = groups[3].takeIf { it.isNotEmpty() }?.toIntOrNull() ?: 0 + val cha = groups[4].takeIf { it.isNotEmpty() }?.toIntOrNull() ?: 0 + listOf(maj, min, pat, cha) + } + + val fromGit = "$major.$minor.${patch + changes}" + logger.info("Version evaluated using Git: {}", fromGit) + return fromGit +} + +/** + * Builds version code using [buildVersionName] + * @return Integer version code + */ +fun Project.buildVersionCode(versionName: String = buildVersionName()): Int { + /* + Max version code (INT32) is 214 74 836 47 + Spaces denote positions of: + - major + - minor + - build + - build variant + */ + val parts = versionName.split('.').mapNotNull { it.toIntOrNull() } + val major = parts.getOrElse(0) { 0 } + val minor = parts.getOrElse(1) { 0 } + val patch = parts.getOrElse(2) { 0 } + + if (major > 214) { + throw GradleException("Major version exhausted: version greater than 214 not supported") + } + if (minor > 99) { + throw GradleException("Minor version exhausted: version greater than 99 not supported") + } + if (patch > 999) { + throw GradleException("Patch version exhausted: version greater than 999 not supported") + } + + val version = major * 10000000 + minor * 100000 + patch * 100 + logger.info("Version code evaluated using Git: {}", version) + return version +} diff --git a/commonflow/compose/api/android/commonflow-compose.api b/commonflow/compose/api/android/commonflow-compose.api new file mode 100644 index 0000000..590424d --- /dev/null +++ b/commonflow/compose/api/android/commonflow-compose.api @@ -0,0 +1,8 @@ +public abstract interface class com/motorro/commonstatemachine/flow/compose/CommonFlowUiApi { + public abstract fun Screen (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;I)V +} + +public final class com/motorro/commonstatemachine/flow/compose/CommonFlowUiApi$ComposeDefaultImpls { + public static final fun Screen$default (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lcom/motorro/commonstatemachine/flow/compose/CommonFlowUiApi;Landroidx/compose/runtime/Composer;II)V +} + diff --git a/commonflow/compose/api/commonflow-compose.klib.api b/commonflow/compose/api/commonflow-compose.klib.api index 1791cb6..65c9247 100644 --- a/commonflow/compose/api/commonflow-compose.klib.api +++ b/commonflow/compose/api/commonflow-compose.klib.api @@ -13,3 +13,15 @@ abstract interface <#A: kotlin/Any, #B: kotlin/Any> com.motorro.commonstatemachi final fun Screen$default(#B, kotlin/Function1<#A, kotlin/Unit>, androidx.compose.ui/Modifier?, com.motorro.commonstatemachine.flow.compose/CommonFlowUiApi<#A, #B>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.motorro.commonstatemachine.flow.compose/CommonFlowUiApi.ComposeDefaultImpls.Screen$default|Screen$default#static(2:1;kotlin.Function1<2:0,kotlin.Unit>;androidx.compose.ui.Modifier?;com.motorro.commonstatemachine.flow.compose.CommonFlowUiApi<2:0,2:1>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] } } + +final val com.motorro.commonstatemachine.commonflow.commonflow_compose/com_motorro_commonstatemachine_commonflow_commonflow_compose_Res_array$stableprop // com.motorro.commonstatemachine.commonflow.commonflow_compose/com_motorro_commonstatemachine_commonflow_commonflow_compose_Res_array$stableprop|#static{}com_motorro_commonstatemachine_commonflow_commonflow_compose_Res_array$stableprop[0] +final val com.motorro.commonstatemachine.commonflow.commonflow_compose/com_motorro_commonstatemachine_commonflow_commonflow_compose_Res_drawable$stableprop // com.motorro.commonstatemachine.commonflow.commonflow_compose/com_motorro_commonstatemachine_commonflow_commonflow_compose_Res_drawable$stableprop|#static{}com_motorro_commonstatemachine_commonflow_commonflow_compose_Res_drawable$stableprop[0] +final val com.motorro.commonstatemachine.commonflow.commonflow_compose/com_motorro_commonstatemachine_commonflow_commonflow_compose_Res_font$stableprop // com.motorro.commonstatemachine.commonflow.commonflow_compose/com_motorro_commonstatemachine_commonflow_commonflow_compose_Res_font$stableprop|#static{}com_motorro_commonstatemachine_commonflow_commonflow_compose_Res_font$stableprop[0] +final val com.motorro.commonstatemachine.commonflow.commonflow_compose/com_motorro_commonstatemachine_commonflow_commonflow_compose_Res_plurals$stableprop // com.motorro.commonstatemachine.commonflow.commonflow_compose/com_motorro_commonstatemachine_commonflow_commonflow_compose_Res_plurals$stableprop|#static{}com_motorro_commonstatemachine_commonflow_commonflow_compose_Res_plurals$stableprop[0] +final val com.motorro.commonstatemachine.commonflow.commonflow_compose/com_motorro_commonstatemachine_commonflow_commonflow_compose_Res_string$stableprop // com.motorro.commonstatemachine.commonflow.commonflow_compose/com_motorro_commonstatemachine_commonflow_commonflow_compose_Res_string$stableprop|#static{}com_motorro_commonstatemachine_commonflow_commonflow_compose_Res_string$stableprop[0] + +final fun com.motorro.commonstatemachine.commonflow.commonflow_compose/com_motorro_commonstatemachine_commonflow_commonflow_compose_Res_array$stableprop_getter(): kotlin/Int // com.motorro.commonstatemachine.commonflow.commonflow_compose/com_motorro_commonstatemachine_commonflow_commonflow_compose_Res_array$stableprop_getter|com_motorro_commonstatemachine_commonflow_commonflow_compose_Res_array$stableprop_getter(){}[0] +final fun com.motorro.commonstatemachine.commonflow.commonflow_compose/com_motorro_commonstatemachine_commonflow_commonflow_compose_Res_drawable$stableprop_getter(): kotlin/Int // com.motorro.commonstatemachine.commonflow.commonflow_compose/com_motorro_commonstatemachine_commonflow_commonflow_compose_Res_drawable$stableprop_getter|com_motorro_commonstatemachine_commonflow_commonflow_compose_Res_drawable$stableprop_getter(){}[0] +final fun com.motorro.commonstatemachine.commonflow.commonflow_compose/com_motorro_commonstatemachine_commonflow_commonflow_compose_Res_font$stableprop_getter(): kotlin/Int // com.motorro.commonstatemachine.commonflow.commonflow_compose/com_motorro_commonstatemachine_commonflow_commonflow_compose_Res_font$stableprop_getter|com_motorro_commonstatemachine_commonflow_commonflow_compose_Res_font$stableprop_getter(){}[0] +final fun com.motorro.commonstatemachine.commonflow.commonflow_compose/com_motorro_commonstatemachine_commonflow_commonflow_compose_Res_plurals$stableprop_getter(): kotlin/Int // com.motorro.commonstatemachine.commonflow.commonflow_compose/com_motorro_commonstatemachine_commonflow_commonflow_compose_Res_plurals$stableprop_getter|com_motorro_commonstatemachine_commonflow_commonflow_compose_Res_plurals$stableprop_getter(){}[0] +final fun com.motorro.commonstatemachine.commonflow.commonflow_compose/com_motorro_commonstatemachine_commonflow_commonflow_compose_Res_string$stableprop_getter(): kotlin/Int // com.motorro.commonstatemachine.commonflow.commonflow_compose/com_motorro_commonstatemachine_commonflow_commonflow_compose_Res_string$stableprop_getter|com_motorro_commonstatemachine_commonflow_commonflow_compose_Res_string$stableprop_getter(){}[0] diff --git a/commonflow/compose/build.gradle.kts b/commonflow/compose/build.gradle.kts index 8f67655..034fada 100644 --- a/commonflow/compose/build.gradle.kts +++ b/commonflow/compose/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation + /* * Copyright 2026 Nikolai Kotchetkov. * Licensed under the Apache License, Version 2.0 (the "License"); @@ -11,153 +13,23 @@ * limitations under the License. */ -@file:Suppress("unused") -@file:OptIn(ExperimentalWasmDsl::class, ExperimentalAbiValidation::class) - -import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation - 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") + alias(libs.plugins.motorro.cmp) + alias(libs.plugins.motorro.publishing) } -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 { + @OptIn(ExperimentalAbiValidation::class) abiValidation { } - jvmToolchain(21) - - jvm() - android { - namespace = "com.motorro.commonstatemachine.commonflow.compose" - compileSdk = androidCompileSdkVersion - minSdk = androidMinSdkVersion - - withHostTest { - isIncludeAndroidResources = true - } - - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - } - - js { - binaries.library() - useCommonJs() - browser { - testTask(Action { - useMocha { - timeout = "10s" - } - }) - } - } - - wasmJs { - binaries.library() - useCommonJs() - browser { - testTask(Action { - useMocha { - timeout = "10s" - } - }) - } - } - - listOf( - iosArm64(), - iosSimulatorArm64() - ).forEach { - it.binaries.framework { - baseName = "commonflow-compose" - isStatic = true - } - } - sourceSets { commonMain.dependencies { api(project(":commonstatemachine")) api(project(":commonflow: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) - } + api(libs.compose.multiplatform.runtime) + api(libs.compose.multiplatform.foundation) } } } - -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/gradle.properties b/commonflow/compose/gradle.properties new file mode 100644 index 0000000..bf00b31 --- /dev/null +++ b/commonflow/compose/gradle.properties @@ -0,0 +1,16 @@ +# +# 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. +# +#Project +libId="commonflow-compose" +libName="commonflow-compose" +libDesc="Common compose flow for modularized state machines" diff --git a/commonflow/data/api/android/commonflow-data.api b/commonflow/data/api/android/commonflow-data.api new file mode 100644 index 0000000..5ff04bb --- /dev/null +++ b/commonflow/data/api/android/commonflow-data.api @@ -0,0 +1,14 @@ +public abstract interface class com/motorro/commonstatemachine/flow/data/CommonFlowDataApi { + public fun getBackGesture ()Ljava/lang/Object; + public abstract fun getDefaultUiState ()Ljava/lang/Object; + public abstract fun init (Lcom/motorro/commonstatemachine/flow/data/CommonFlowHost;Ljava/lang/Object;)Lcom/motorro/commonstatemachine/CommonMachineState; +} + +public final class com/motorro/commonstatemachine/flow/data/CommonFlowDataApi$DefaultImpls { + public static fun getBackGesture (Lcom/motorro/commonstatemachine/flow/data/CommonFlowDataApi;)Ljava/lang/Object; +} + +public abstract interface class com/motorro/commonstatemachine/flow/data/CommonFlowHost { + public abstract fun onComplete (Ljava/lang/Object;)V +} + diff --git a/commonflow/data/build.gradle.kts b/commonflow/data/build.gradle.kts index cdf3d9c..bb764cd 100644 --- a/commonflow/data/build.gradle.kts +++ b/commonflow/data/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation + /* * Copyright 2026 Nikolai Kotchetkov. * Licensed under the Apache License, Version 2.0 (the "License"); @@ -11,149 +13,20 @@ * limitations under the License. */ -@file:Suppress("unused") -@file:OptIn(ExperimentalWasmDsl::class, ExperimentalAbiValidation::class) - -import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation - plugins { - alias(libs.plugins.kotlin.multiplatform) - alias(libs.plugins.android.kotlin.multiplatform.library) - alias(libs.plugins.kotlin.dokka) - id("maven-publish") - id("signing") + alias(libs.plugins.motorro.kmp) + alias(libs.plugins.motorro.publishing) } -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 { + @OptIn(ExperimentalAbiValidation::class) abiValidation { } - jvmToolchain(21) - - jvm() - - android { - namespace = "com.motorro.commonstatemachine.commonflow.data" - compileSdk = androidCompileSdkVersion - minSdk = androidMinSdkVersion - - withHostTest { - isIncludeAndroidResources = true - } - - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - } - - js { - binaries.library() - useCommonJs() - browser { - testTask(Action { - useMocha { - timeout = "10s" - } - }) - } - } - - wasmJs { - binaries.library() - useCommonJs() - browser { - testTask(Action { - useMocha { - timeout = "10s" - } - }) - } - } - - listOf( - 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/gradle.properties b/commonflow/data/gradle.properties new file mode 100644 index 0000000..0255c4f --- /dev/null +++ b/commonflow/data/gradle.properties @@ -0,0 +1,16 @@ +# +# 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. +# +#Project +libId="commonflow-data" +libName="commonflow-data" +libDesc="Common data flow for modularized state machines" diff --git a/commonflow/viewmodel/api/android/commonflow-viewmodel.api b/commonflow/viewmodel/api/android/commonflow-viewmodel.api new file mode 100644 index 0000000..b3a0bda --- /dev/null +++ b/commonflow/viewmodel/api/android/commonflow-viewmodel.api @@ -0,0 +1,82 @@ +public final class com/motorro/commonstatemachine/flow/viewmodel/CommonFlowCompositionKt { + public static final fun CommonFlowComposition (Lcom/motorro/commonstatemachine/flow/viewmodel/CommonFlowViewModel;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V +} + +public class com/motorro/commonstatemachine/flow/viewmodel/CommonFlowViewModel : androidx/lifecycle/ViewModel { + public static final field $stable I + public static final field CLOSABLE_KEY Ljava/lang/String; + public static final field Companion Lcom/motorro/commonstatemachine/flow/viewmodel/CommonFlowViewModel$Companion; + public fun (Lcom/motorro/commonstatemachine/flow/data/CommonFlowDataApi;Ljava/lang/Object;[Ljava/lang/AutoCloseable;)V + public final fun getUiState ()Lkotlinx/coroutines/flow/StateFlow; + public final fun process (Lcom/motorro/commonstatemachine/flow/viewmodel/data/BaseFlowGesture;)V +} + +public final class com/motorro/commonstatemachine/flow/viewmodel/CommonFlowViewModel$Companion { +} + +public final class com/motorro/commonstatemachine/flow/viewmodel/ComposableSingletons$CommonFlowCompositionKt { + public static final field INSTANCE Lcom/motorro/commonstatemachine/flow/viewmodel/ComposableSingletons$CommonFlowCompositionKt; + public fun ()V + public final fun getLambda$1100526547$com_motorro_commonstatemachine_commonflow_viewmodel ()Lkotlin/jvm/functions/Function4; +} + +public final class com/motorro/commonstatemachine/flow/viewmodel/ViewModelComposition_androidKt { + public static final fun DefaultNavigationBackHandler (ZLkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V +} + +public abstract class com/motorro/commonstatemachine/flow/viewmodel/data/BaseFlowGesture { + public static final field $stable I +} + +public final class com/motorro/commonstatemachine/flow/viewmodel/data/BaseFlowGesture$Back : com/motorro/commonstatemachine/flow/viewmodel/data/BaseFlowGesture { + public static final field $stable I + public static final field INSTANCE Lcom/motorro/commonstatemachine/flow/viewmodel/data/BaseFlowGesture$Back; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/motorro/commonstatemachine/flow/viewmodel/data/BaseFlowGesture$Child : com/motorro/commonstatemachine/flow/viewmodel/data/BaseFlowGesture { + public static final field $stable I + public fun (Ljava/lang/Object;)V + public final fun component1 ()Ljava/lang/Object; + public final fun copy (Ljava/lang/Object;)Lcom/motorro/commonstatemachine/flow/viewmodel/data/BaseFlowGesture$Child; + public static synthetic fun copy$default (Lcom/motorro/commonstatemachine/flow/viewmodel/data/BaseFlowGesture$Child;Ljava/lang/Object;ILjava/lang/Object;)Lcom/motorro/commonstatemachine/flow/viewmodel/data/BaseFlowGesture$Child; + public fun equals (Ljava/lang/Object;)Z + public final fun getChild ()Ljava/lang/Object; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public abstract class com/motorro/commonstatemachine/flow/viewmodel/data/BaseFlowUiState { + public static final field $stable I + public abstract fun getBackHandlerEnabled ()Z +} + +public final class com/motorro/commonstatemachine/flow/viewmodel/data/BaseFlowUiState$Child : com/motorro/commonstatemachine/flow/viewmodel/data/BaseFlowUiState { + public static final field $stable I + public fun (Ljava/lang/Object;Z)V + public final fun component1 ()Ljava/lang/Object; + public final fun component2 ()Z + public final fun copy (Ljava/lang/Object;Z)Lcom/motorro/commonstatemachine/flow/viewmodel/data/BaseFlowUiState$Child; + public static synthetic fun copy$default (Lcom/motorro/commonstatemachine/flow/viewmodel/data/BaseFlowUiState$Child;Ljava/lang/Object;ZILjava/lang/Object;)Lcom/motorro/commonstatemachine/flow/viewmodel/data/BaseFlowUiState$Child; + public fun equals (Ljava/lang/Object;)Z + public fun getBackHandlerEnabled ()Z + public final fun getChild ()Ljava/lang/Object; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/motorro/commonstatemachine/flow/viewmodel/data/BaseFlowUiState$Terminated : com/motorro/commonstatemachine/flow/viewmodel/data/BaseFlowUiState { + public static final field $stable I + public fun (Ljava/lang/Object;)V + public final fun component1 ()Ljava/lang/Object; + public final fun copy (Ljava/lang/Object;)Lcom/motorro/commonstatemachine/flow/viewmodel/data/BaseFlowUiState$Terminated; + public static synthetic fun copy$default (Lcom/motorro/commonstatemachine/flow/viewmodel/data/BaseFlowUiState$Terminated;Ljava/lang/Object;ILjava/lang/Object;)Lcom/motorro/commonstatemachine/flow/viewmodel/data/BaseFlowUiState$Terminated; + public fun equals (Ljava/lang/Object;)Z + public fun getBackHandlerEnabled ()Z + public final fun getResult ()Ljava/lang/Object; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + diff --git a/commonflow/viewmodel/api/commonflow-viewmodel.klib.api b/commonflow/viewmodel/api/commonflow-viewmodel.klib.api index 73d8160..80fafd4 100644 --- a/commonflow/viewmodel/api/commonflow-viewmodel.klib.api +++ b/commonflow/viewmodel/api/commonflow-viewmodel.klib.api @@ -77,6 +77,11 @@ sealed class <#A: out kotlin/Any> com.motorro.commonstatemachine.flow.viewmodel. } } +final val com.motorro.commonstatemachine.commonflow.commonflow_viewmodel/com_motorro_commonstatemachine_commonflow_commonflow_viewmodel_Res_array$stableprop // com.motorro.commonstatemachine.commonflow.commonflow_viewmodel/com_motorro_commonstatemachine_commonflow_commonflow_viewmodel_Res_array$stableprop|#static{}com_motorro_commonstatemachine_commonflow_commonflow_viewmodel_Res_array$stableprop[0] +final val com.motorro.commonstatemachine.commonflow.commonflow_viewmodel/com_motorro_commonstatemachine_commonflow_commonflow_viewmodel_Res_drawable$stableprop // com.motorro.commonstatemachine.commonflow.commonflow_viewmodel/com_motorro_commonstatemachine_commonflow_commonflow_viewmodel_Res_drawable$stableprop|#static{}com_motorro_commonstatemachine_commonflow_commonflow_viewmodel_Res_drawable$stableprop[0] +final val com.motorro.commonstatemachine.commonflow.commonflow_viewmodel/com_motorro_commonstatemachine_commonflow_commonflow_viewmodel_Res_font$stableprop // com.motorro.commonstatemachine.commonflow.commonflow_viewmodel/com_motorro_commonstatemachine_commonflow_commonflow_viewmodel_Res_font$stableprop|#static{}com_motorro_commonstatemachine_commonflow_commonflow_viewmodel_Res_font$stableprop[0] +final val com.motorro.commonstatemachine.commonflow.commonflow_viewmodel/com_motorro_commonstatemachine_commonflow_commonflow_viewmodel_Res_plurals$stableprop // com.motorro.commonstatemachine.commonflow.commonflow_viewmodel/com_motorro_commonstatemachine_commonflow_commonflow_viewmodel_Res_plurals$stableprop|#static{}com_motorro_commonstatemachine_commonflow_commonflow_viewmodel_Res_plurals$stableprop[0] +final val com.motorro.commonstatemachine.commonflow.commonflow_viewmodel/com_motorro_commonstatemachine_commonflow_commonflow_viewmodel_Res_string$stableprop // com.motorro.commonstatemachine.commonflow.commonflow_viewmodel/com_motorro_commonstatemachine_commonflow_commonflow_viewmodel_Res_string$stableprop|#static{}com_motorro_commonstatemachine_commonflow_commonflow_viewmodel_Res_string$stableprop[0] final val com.motorro.commonstatemachine.flow.viewmodel.data/com_motorro_commonstatemachine_flow_viewmodel_data_BaseFlowGesture$stableprop // com.motorro.commonstatemachine.flow.viewmodel.data/com_motorro_commonstatemachine_flow_viewmodel_data_BaseFlowGesture$stableprop|#static{}com_motorro_commonstatemachine_flow_viewmodel_data_BaseFlowGesture$stableprop[0] final val com.motorro.commonstatemachine.flow.viewmodel.data/com_motorro_commonstatemachine_flow_viewmodel_data_BaseFlowGesture_Back$stableprop // com.motorro.commonstatemachine.flow.viewmodel.data/com_motorro_commonstatemachine_flow_viewmodel_data_BaseFlowGesture_Back$stableprop|#static{}com_motorro_commonstatemachine_flow_viewmodel_data_BaseFlowGesture_Back$stableprop[0] final val com.motorro.commonstatemachine.flow.viewmodel.data/com_motorro_commonstatemachine_flow_viewmodel_data_BaseFlowGesture_Child$stableprop // com.motorro.commonstatemachine.flow.viewmodel.data/com_motorro_commonstatemachine_flow_viewmodel_data_BaseFlowGesture_Child$stableprop|#static{}com_motorro_commonstatemachine_flow_viewmodel_data_BaseFlowGesture_Child$stableprop[0] @@ -86,6 +91,11 @@ final val com.motorro.commonstatemachine.flow.viewmodel.data/com_motorro_commons final val com.motorro.commonstatemachine.flow.viewmodel/com_motorro_commonstatemachine_flow_viewmodel_CommonFlowViewModel$stableprop // com.motorro.commonstatemachine.flow.viewmodel/com_motorro_commonstatemachine_flow_viewmodel_CommonFlowViewModel$stableprop|#static{}com_motorro_commonstatemachine_flow_viewmodel_CommonFlowViewModel$stableprop[0] final fun <#A: kotlin/Any, #B: kotlin/Any, #C: kotlin/Any?> com.motorro.commonstatemachine.flow.viewmodel/CommonFlowComposition(com.motorro.commonstatemachine.flow.viewmodel/CommonFlowViewModel<#A, #B, *, #C>, kotlin/Function4, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>?, kotlin/Function4<#B, kotlin/Function1<#A, kotlin/Unit>, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, kotlin/Function1<#C?, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.motorro.commonstatemachine.flow.viewmodel/CommonFlowComposition|CommonFlowComposition(com.motorro.commonstatemachine.flow.viewmodel.CommonFlowViewModel<0:0,0:1,*,0:2>;kotlin.Function4,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>?;kotlin.Function4<0:1,kotlin.Function1<0:0,kotlin.Unit>,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;kotlin.Function1<0:2?,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){0§;1§;2§}[0] +final fun com.motorro.commonstatemachine.commonflow.commonflow_viewmodel/com_motorro_commonstatemachine_commonflow_commonflow_viewmodel_Res_array$stableprop_getter(): kotlin/Int // com.motorro.commonstatemachine.commonflow.commonflow_viewmodel/com_motorro_commonstatemachine_commonflow_commonflow_viewmodel_Res_array$stableprop_getter|com_motorro_commonstatemachine_commonflow_commonflow_viewmodel_Res_array$stableprop_getter(){}[0] +final fun com.motorro.commonstatemachine.commonflow.commonflow_viewmodel/com_motorro_commonstatemachine_commonflow_commonflow_viewmodel_Res_drawable$stableprop_getter(): kotlin/Int // com.motorro.commonstatemachine.commonflow.commonflow_viewmodel/com_motorro_commonstatemachine_commonflow_commonflow_viewmodel_Res_drawable$stableprop_getter|com_motorro_commonstatemachine_commonflow_commonflow_viewmodel_Res_drawable$stableprop_getter(){}[0] +final fun com.motorro.commonstatemachine.commonflow.commonflow_viewmodel/com_motorro_commonstatemachine_commonflow_commonflow_viewmodel_Res_font$stableprop_getter(): kotlin/Int // com.motorro.commonstatemachine.commonflow.commonflow_viewmodel/com_motorro_commonstatemachine_commonflow_commonflow_viewmodel_Res_font$stableprop_getter|com_motorro_commonstatemachine_commonflow_commonflow_viewmodel_Res_font$stableprop_getter(){}[0] +final fun com.motorro.commonstatemachine.commonflow.commonflow_viewmodel/com_motorro_commonstatemachine_commonflow_commonflow_viewmodel_Res_plurals$stableprop_getter(): kotlin/Int // com.motorro.commonstatemachine.commonflow.commonflow_viewmodel/com_motorro_commonstatemachine_commonflow_commonflow_viewmodel_Res_plurals$stableprop_getter|com_motorro_commonstatemachine_commonflow_commonflow_viewmodel_Res_plurals$stableprop_getter(){}[0] +final fun com.motorro.commonstatemachine.commonflow.commonflow_viewmodel/com_motorro_commonstatemachine_commonflow_commonflow_viewmodel_Res_string$stableprop_getter(): kotlin/Int // com.motorro.commonstatemachine.commonflow.commonflow_viewmodel/com_motorro_commonstatemachine_commonflow_commonflow_viewmodel_Res_string$stableprop_getter|com_motorro_commonstatemachine_commonflow_commonflow_viewmodel_Res_string$stableprop_getter(){}[0] final fun com.motorro.commonstatemachine.flow.viewmodel.data/com_motorro_commonstatemachine_flow_viewmodel_data_BaseFlowGesture$stableprop_getter(): kotlin/Int // com.motorro.commonstatemachine.flow.viewmodel.data/com_motorro_commonstatemachine_flow_viewmodel_data_BaseFlowGesture$stableprop_getter|com_motorro_commonstatemachine_flow_viewmodel_data_BaseFlowGesture$stableprop_getter(){}[0] final fun com.motorro.commonstatemachine.flow.viewmodel.data/com_motorro_commonstatemachine_flow_viewmodel_data_BaseFlowGesture_Back$stableprop_getter(): kotlin/Int // com.motorro.commonstatemachine.flow.viewmodel.data/com_motorro_commonstatemachine_flow_viewmodel_data_BaseFlowGesture_Back$stableprop_getter|com_motorro_commonstatemachine_flow_viewmodel_data_BaseFlowGesture_Back$stableprop_getter(){}[0] final fun com.motorro.commonstatemachine.flow.viewmodel.data/com_motorro_commonstatemachine_flow_viewmodel_data_BaseFlowGesture_Child$stableprop_getter(): kotlin/Int // com.motorro.commonstatemachine.flow.viewmodel.data/com_motorro_commonstatemachine_flow_viewmodel_data_BaseFlowGesture_Child$stableprop_getter|com_motorro_commonstatemachine_flow_viewmodel_data_BaseFlowGesture_Child$stableprop_getter(){}[0] diff --git a/commonflow/viewmodel/build.gradle.kts b/commonflow/viewmodel/build.gradle.kts index bf1c7f8..7e434ff 100644 --- a/commonflow/viewmodel/build.gradle.kts +++ b/commonflow/viewmodel/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation + /* * Copyright 2026 Nikolai Kotchetkov. * Licensed under the Apache License, Version 2.0 (the "License"); @@ -11,98 +13,24 @@ * limitations under the License. */ -@file:Suppress("unused") -@file:OptIn(ExperimentalWasmDsl::class, ExperimentalAbiValidation::class) - -import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation - 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") + alias(libs.plugins.motorro.cmp) + alias(libs.plugins.motorro.publishing) } -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 { + @OptIn(ExperimentalAbiValidation::class) abiValidation { } - jvmToolchain(21) - - jvm() - android { - namespace = "com.motorro.commonstatemachine.commonflow.viewmodel" - compileSdk = androidCompileSdkVersion - minSdk = androidMinSdkVersion - - withHostTest { - isIncludeAndroidResources = true - } - - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - } - - js { - binaries.library() - binaries.executable() - useCommonJs() - browser { - testTask(Action { - useMocha { - timeout = "10s" - } - }) - } - } - - wasmJs { - binaries.library() - binaries.executable() - useCommonJs() - browser { - testTask(Action { - useMocha { - timeout = "10s" - } - }) - } - } - - listOf( - iosArm64(), - iosSimulatorArm64() - ).forEach { - it.binaries.framework { - baseName = "commonflow-viewmodel" - isStatic = true - } - } - sourceSets { commonMain.dependencies { api(project(":commonstatemachine")) api(project(":coroutines")) api(project(":commonflow:commonflow-compose")) - api(libs.composeMultiplatform.viewmodel) - implementation(libs.composeMultiplatform.lifecycle) + api(libs.compose.multiplatform.viewmodel) + implementation(libs.compose.multiplatform.lifecycle) } commonTest.dependencies { implementation(libs.test.kotlin) @@ -116,61 +44,3 @@ kotlin { } } } -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/gradle.properties b/commonflow/viewmodel/gradle.properties new file mode 100644 index 0000000..3ed4780 --- /dev/null +++ b/commonflow/viewmodel/gradle.properties @@ -0,0 +1,16 @@ +# +# 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. +# +#Project +libId="commonflow-viewmodel" +libName="commonflow-viewmodel" +libDesc="Common view components to wrap standard flow" 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 index 7de8d39..0b39a94 100644 --- 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 @@ -27,6 +27,14 @@ import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider.Factory import androidx.lifecycle.viewmodel.CreationExtras +/** + * Default navigation back handler using [BackHandler]. + */ +@Composable +fun DefaultNavigationBackHandler(enabled: Boolean, onBack: () -> Unit) { + BackHandler(enabled = enabled) { onBack() } +} + /** * Builds state machine composition * Ensure that [AppCompatActivity] has correct view model factory @@ -40,9 +48,7 @@ inline fun > Com 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 navigationBackHandler: @Composable (Boolean, () -> Unit) -> Unit = ::DefaultNavigationBackHandler, noinline content: @Composable (U, (G) -> Unit) -> Unit ) { setContent { @@ -77,9 +83,7 @@ inline fun > Fra 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 navigationBackHandler: @Composable (Boolean, () -> Unit) -> Unit = ::DefaultNavigationBackHandler, noinline content: @Composable (U, (G) -> Unit) -> Unit ): View = ComposeView(requireContext()).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) diff --git a/commonstatemachine/api/android/commonstatemachine.api b/commonstatemachine/api/android/commonstatemachine.api new file mode 100644 index 0000000..b8f8180 --- /dev/null +++ b/commonstatemachine/api/android/commonstatemachine.api @@ -0,0 +1,187 @@ +public class com/motorro/commonstatemachine/CommonMachineState { + public fun ()V + public final fun clear ()V + protected fun doClear ()V + protected fun doProcess (Ljava/lang/Object;)V + protected fun doStart ()V + public final fun process (Ljava/lang/Object;)V + protected final fun setMachineState (Lcom/motorro/commonstatemachine/CommonMachineState;)V + protected final fun setUiState (Ljava/lang/Object;)V + public final fun start (Lcom/motorro/commonstatemachine/MachineOutput;)V +} + +public abstract interface class com/motorro/commonstatemachine/CommonStateMachine : com/motorro/commonstatemachine/MachineInput, com/motorro/commonstatemachine/MachineOutput, com/motorro/commonstatemachine/MachineStatus { + public abstract fun start ()V +} + +public abstract class com/motorro/commonstatemachine/CommonStateMachine$Base : com/motorro/commonstatemachine/CommonStateMachine { + protected field activeState Lcom/motorro/commonstatemachine/CommonMachineState; + public fun (Lkotlin/jvm/functions/Function0;)V + public final fun clear ()V + protected final fun getActiveState ()Lcom/motorro/commonstatemachine/CommonMachineState; + public final fun isStarted ()Z + public final fun process (Ljava/lang/Object;)V + protected final fun setActiveState (Lcom/motorro/commonstatemachine/CommonMachineState;)V + public final fun setMachineState (Lcom/motorro/commonstatemachine/CommonMachineState;)V + public fun start ()V +} + +public abstract interface class com/motorro/commonstatemachine/MachineInput { + public abstract fun clear ()V + public abstract fun process (Ljava/lang/Object;)V +} + +public abstract interface class com/motorro/commonstatemachine/MachineOutput { + public abstract fun setMachineState (Lcom/motorro/commonstatemachine/CommonMachineState;)V + public abstract fun setUiState (Ljava/lang/Object;)V +} + +public abstract interface class com/motorro/commonstatemachine/MachineStatus { + public abstract fun getUiState ()Ljava/lang/Object; + public abstract fun isStarted ()Z +} + +public abstract class com/motorro/commonstatemachine/ProxyMachineState : com/motorro/commonstatemachine/CommonMachineState { + public fun (Ljava/lang/Object;)V + protected fun doClear ()V + protected fun doProcess (Ljava/lang/Object;)V + protected fun doStart ()V + protected final fun getChildMachineState ()Lcom/motorro/commonstatemachine/MachineStatus; + protected abstract fun init ()Lcom/motorro/commonstatemachine/CommonMachineState; + protected abstract fun mapGesture (Ljava/lang/Object;)Ljava/lang/Object; + protected abstract fun mapUiState (Ljava/lang/Object;)Ljava/lang/Object; +} + +public final class com/motorro/commonstatemachine/lifecycle/ActivatedMachineLifecycle : com/motorro/commonstatemachine/lifecycle/MachineLifecycle, com/motorro/commonstatemachine/multi/Activated { + public fun (Lcom/motorro/commonstatemachine/lifecycle/MachineLifecycle$State;)V + public fun activate ()V + public fun addObserver (Lcom/motorro/commonstatemachine/lifecycle/MachineLifecycle$Observer;)V + public fun deactivate ()V + public fun getState ()Lcom/motorro/commonstatemachine/lifecycle/MachineLifecycle$State; + public fun hasObservers ()Z + public fun isActive ()Z + public fun removeObserver (Lcom/motorro/commonstatemachine/lifecycle/MachineLifecycle$Observer;)V +} + +public final class com/motorro/commonstatemachine/lifecycle/BaseMachineLifecycle : com/motorro/commonstatemachine/lifecycle/MachineLifecycle { + public fun (Lcom/motorro/commonstatemachine/lifecycle/MachineLifecycle$State;)V + public fun addObserver (Lcom/motorro/commonstatemachine/lifecycle/MachineLifecycle$Observer;)V + public fun getState ()Lcom/motorro/commonstatemachine/lifecycle/MachineLifecycle$State; + public fun hasObservers ()Z + public fun removeObserver (Lcom/motorro/commonstatemachine/lifecycle/MachineLifecycle$Observer;)V + public final fun setState (Lcom/motorro/commonstatemachine/lifecycle/MachineLifecycle$State;)V +} + +public abstract interface class com/motorro/commonstatemachine/lifecycle/MachineLifecycle { + public abstract fun addObserver (Lcom/motorro/commonstatemachine/lifecycle/MachineLifecycle$Observer;)V + public abstract fun getState ()Lcom/motorro/commonstatemachine/lifecycle/MachineLifecycle$State; + public abstract fun hasObservers ()Z + public abstract fun removeObserver (Lcom/motorro/commonstatemachine/lifecycle/MachineLifecycle$Observer;)V +} + +public abstract interface class com/motorro/commonstatemachine/lifecycle/MachineLifecycle$Observer { + public abstract fun onStateChange (Lcom/motorro/commonstatemachine/lifecycle/MachineLifecycle$State;)V +} + +public final class com/motorro/commonstatemachine/lifecycle/MachineLifecycle$State : java/lang/Enum { + public static final field ACTIVE Lcom/motorro/commonstatemachine/lifecycle/MachineLifecycle$State; + public static final field PAUSED Lcom/motorro/commonstatemachine/lifecycle/MachineLifecycle$State; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lcom/motorro/commonstatemachine/lifecycle/MachineLifecycle$State; + public static fun values ()[Lcom/motorro/commonstatemachine/lifecycle/MachineLifecycle$State; +} + +public final class com/motorro/commonstatemachine/lifecycle/MachineLifecycleKt { + public static final fun combinePaused (Lcom/motorro/commonstatemachine/lifecycle/MachineLifecycle;Lcom/motorro/commonstatemachine/lifecycle/MachineLifecycle;)Lcom/motorro/commonstatemachine/lifecycle/MachineLifecycle; +} + +public abstract interface class com/motorro/commonstatemachine/multi/Activated { + public abstract fun activate ()V + public abstract fun deactivate ()V + public abstract fun isActive ()Z +} + +public abstract interface class com/motorro/commonstatemachine/multi/ActiveMachineContainer : com/motorro/commonstatemachine/multi/ProxyMachineContainer { + public abstract fun dispose (Ljava/util/Set;)V + public fun dispose ([Lcom/motorro/commonstatemachine/multi/MachineKey;)V + public abstract fun disposeInactive ()V + public abstract fun getActive ()Ljava/util/Set; + public abstract fun setActive (Ljava/util/Set;)V + public fun setActive ([Lcom/motorro/commonstatemachine/multi/MachineKey;)V +} + +public final class com/motorro/commonstatemachine/multi/ActiveMachineContainer$DefaultImpls { + public static fun dispose (Lcom/motorro/commonstatemachine/multi/ActiveMachineContainer;[Lcom/motorro/commonstatemachine/multi/MachineKey;)V + public static fun setActive (Lcom/motorro/commonstatemachine/multi/ActiveMachineContainer;[Lcom/motorro/commonstatemachine/multi/MachineKey;)V +} + +public abstract interface class com/motorro/commonstatemachine/multi/GestureProcessor { + public abstract fun process (Lcom/motorro/commonstatemachine/multi/MachineKey;Ljava/lang/Object;)V +} + +public abstract interface class com/motorro/commonstatemachine/multi/MachineAccess { + public abstract fun getKeys ()Ljava/util/Set; + public abstract fun getState (Lcom/motorro/commonstatemachine/multi/MachineKey;)Ljava/lang/Object; + public abstract fun process (Lcom/motorro/commonstatemachine/multi/MachineKey;Ljava/lang/Object;)V +} + +public abstract interface class com/motorro/commonstatemachine/multi/MachineInit { + public abstract fun getInit ()Lkotlin/jvm/functions/Function1; + public abstract fun getInitialUiState ()Ljava/lang/Object; + public abstract fun getKey ()Lcom/motorro/commonstatemachine/multi/MachineKey; +} + +public abstract class com/motorro/commonstatemachine/multi/MachineKey { + public fun ()V + public fun (Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getTag ()Ljava/lang/String; + public fun hashCode ()I +} + +public abstract class com/motorro/commonstatemachine/multi/MultiMachineState : com/motorro/commonstatemachine/CommonMachineState { + public fun ()V + protected fun doClear ()V + protected fun doProcess (Ljava/lang/Object;)V + protected fun doStart ()V + protected abstract fun getContainer ()Lcom/motorro/commonstatemachine/multi/ProxyMachineContainer; + protected abstract fun mapGesture (Ljava/lang/Object;Lcom/motorro/commonstatemachine/multi/GestureProcessor;)V + protected abstract fun mapUiState (Lcom/motorro/commonstatemachine/multi/UiStateProvider;Lcom/motorro/commonstatemachine/multi/MachineKey;)Ljava/lang/Object; + protected final fun updateUi ()V +} + +public abstract interface class com/motorro/commonstatemachine/multi/ProxyMachineContainer { + public static final field Companion Lcom/motorro/commonstatemachine/multi/ProxyMachineContainer$Companion; + public abstract fun clear ()V + public abstract fun getMachineAccess ()Lcom/motorro/commonstatemachine/multi/MachineAccess; + public abstract fun start (Lkotlin/jvm/functions/Function2;)V +} + +public abstract class com/motorro/commonstatemachine/multi/ProxyMachineContainer$Base : com/motorro/commonstatemachine/multi/ProxyMachineContainer { + public fun (Ljava/util/Collection;)V + public fun clear ()V + public abstract fun create (Lcom/motorro/commonstatemachine/multi/MachineInit;Lkotlin/jvm/functions/Function2;)Lcom/motorro/commonstatemachine/CommonStateMachine; + public fun doStart ()V + public fun getMachineAccess ()Lcom/motorro/commonstatemachine/multi/MachineAccess; + protected final fun getMachines ()Ljava/util/Map; + protected final fun setMachines (Ljava/util/Map;)V + public fun start (Lkotlin/jvm/functions/Function2;)V +} + +public final class com/motorro/commonstatemachine/multi/ProxyMachineContainer$Companion { + public final fun allTogether (Ljava/util/Collection;)Lcom/motorro/commonstatemachine/multi/ProxyMachineContainer; + public final fun some (Ljava/util/Collection;Ljava/util/Set;)Lcom/motorro/commonstatemachine/multi/ActiveMachineContainer; + public static synthetic fun some$default (Lcom/motorro/commonstatemachine/multi/ProxyMachineContainer$Companion;Ljava/util/Collection;Ljava/util/Set;ILjava/lang/Object;)Lcom/motorro/commonstatemachine/multi/ActiveMachineContainer; +} + +public abstract interface class com/motorro/commonstatemachine/multi/UiStateProvider { + public abstract fun get (Lcom/motorro/commonstatemachine/multi/MachineKey;)Ljava/lang/Object; + public abstract fun getMachineKeys ()Ljava/util/Set; + public fun getValue (Lcom/motorro/commonstatemachine/multi/MachineKey;)Ljava/lang/Object; +} + +public final class com/motorro/commonstatemachine/multi/UiStateProvider$DefaultImpls { + public static fun getValue (Lcom/motorro/commonstatemachine/multi/UiStateProvider;Lcom/motorro/commonstatemachine/multi/MachineKey;)Ljava/lang/Object; +} + diff --git a/commonstatemachine/api/commonstatemachine.klib.api b/commonstatemachine/api/commonstatemachine.klib.api index 93154c0..9cdf9d4 100644 --- a/commonstatemachine/api/commonstatemachine.klib.api +++ b/commonstatemachine/api/commonstatemachine.klib.api @@ -1,5 +1,5 @@ // Klib ABI Dump -// Targets: [iosArm64, iosSimulatorArm64, iosX64, js, wasmJs] +// Targets: [iosArm64, iosSimulatorArm64, js, wasmJs] // Rendering settings: // - Signature version: 2 // - Show manifest properties: true diff --git a/commonstatemachine/build.gradle.kts b/commonstatemachine/build.gradle.kts index 9d20183..288f9cd 100644 --- a/commonstatemachine/build.gradle.kts +++ b/commonstatemachine/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation + /* * Copyright 2022 Nikolai Kotchetkov. * Licensed under the Apache License, Version 2.0 (the "License"); @@ -11,152 +13,14 @@ * limitations under the License. */ -@file:Suppress("unused") -@file:OptIn(ExperimentalWasmDsl::class, ExperimentalAbiValidation::class) - -import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation - plugins { - alias(libs.plugins.kotlin.multiplatform) - alias(libs.plugins.android.kotlin.multiplatform.library) - alias(libs.plugins.kotlin.dokka) - id("maven-publish") - id("signing") + alias(libs.plugins.motorro.kmp) + alias(libs.plugins.motorro.publishing) } -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 { + @OptIn(ExperimentalAbiValidation::class) abiValidation { } - - jvmToolchain(21) - - jvm() - android { - namespace = "com.motorro.commonstatemachine.commonstatemachine" - compileSdk = androidCompileSdkVersion - minSdk = androidMinSdkVersion - - withHostTest { - isIncludeAndroidResources = true - } - - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - } - - js { - 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 = "commonstatemachine" - isStatic = true - } - } - - sourceSets { - commonMain.dependencies { - api(project(":tmap")) - } - commonTest.dependencies { - implementation(libs.test.kotlin) - } - } -} -val javadocJar by tasks.registering(Jar::class) { - dependsOn(tasks.dokkaGenerate) - group = "documentation" - archiveClassifier.set("javadoc") - from(tasks.dokkaGenerate) } - -val libId = "commonstatemachine" -val libName = "commonstatemachine" -val libDesc = "Multiplatform state machine for mobile applications" -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/commonstatemachine/gradle.properties b/commonstatemachine/gradle.properties new file mode 100644 index 0000000..35cfa73 --- /dev/null +++ b/commonstatemachine/gradle.properties @@ -0,0 +1,14 @@ +# +# 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. +# +#Project +libDesc=Multiplatform state machine for mobile applications diff --git a/coroutines/api/android/coroutines.api b/coroutines/api/android/coroutines.api new file mode 100644 index 0000000..b4fcaa2 --- /dev/null +++ b/coroutines/api/android/coroutines.api @@ -0,0 +1,24 @@ +public abstract class com/motorro/commonstatemachine/coroutines/CoroutineState : com/motorro/commonstatemachine/CommonMachineState { + public fun ()V + protected fun doClear ()V + protected final fun getStateScope ()Lkotlinx/coroutines/CoroutineScope; +} + +public class com/motorro/commonstatemachine/coroutines/FlowStateMachine : com/motorro/commonstatemachine/CommonStateMachine$Base { + public fun (Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)V + public fun getUiState ()Ljava/lang/Object; + public final fun getUiState ()Lkotlinx/coroutines/flow/StateFlow; + public final fun getUiStateSubscriptionCount ()Lkotlinx/coroutines/flow/StateFlow; + public final fun setUiState (Ljava/lang/Object;)V +} + +public final class com/motorro/commonstatemachine/coroutines/FlowStateMachineKt { + public static final fun mapUiSubscriptions (Lcom/motorro/commonstatemachine/coroutines/FlowStateMachine;Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)V + public static synthetic fun mapUiSubscriptions$default (Lcom/motorro/commonstatemachine/coroutines/FlowStateMachine;Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)V +} + +public final class com/motorro/commonstatemachine/coroutines/lifecycle/LifecycleStateFlowKt { + public static final fun asCallback (Lkotlinx/coroutines/flow/StateFlow;Lkotlinx/coroutines/CoroutineScope;)Lcom/motorro/commonstatemachine/lifecycle/MachineLifecycle; + public static final fun asFlow (Lcom/motorro/commonstatemachine/lifecycle/MachineLifecycle;Lkotlinx/coroutines/CoroutineScope;)Lkotlinx/coroutines/flow/StateFlow; +} + diff --git a/coroutines/api/coroutines.klib.api b/coroutines/api/coroutines.klib.api index e0d5fba..8b6f52a 100644 --- a/coroutines/api/coroutines.klib.api +++ b/coroutines/api/coroutines.klib.api @@ -1,5 +1,5 @@ // Klib ABI Dump -// Targets: [iosArm64, iosSimulatorArm64, iosX64, js, wasmJs] +// Targets: [iosArm64, iosSimulatorArm64, js, wasmJs] // Rendering settings: // - Signature version: 2 // - Show manifest properties: true diff --git a/coroutines/build.gradle.kts b/coroutines/build.gradle.kts index f35174c..6cc2568 100644 --- a/coroutines/build.gradle.kts +++ b/coroutines/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation + /* * Copyright 2022 Nikolai Kotchetkov. * Licensed under the Apache License, Version 2.0 (the "License"); @@ -11,155 +13,25 @@ * limitations under the License. */ -@file:Suppress("unused") -@file:OptIn(ExperimentalAbiValidation::class) - -import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation - plugins { - alias(libs.plugins.kotlin.multiplatform) - alias(libs.plugins.android.kotlin.multiplatform.library) - alias(libs.plugins.kotlin.dokka) - id("maven-publish") - id("signing") + alias(libs.plugins.motorro.kmp) + alias(libs.plugins.motorro.publishing) } -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 { + @OptIn(ExperimentalAbiValidation::class) abiValidation { } - jvmToolchain(21) - - jvm() - android { - namespace = "com.motorro.commonstatemachine.coroutines" - compileSdk = androidCompileSdkVersion - minSdk = androidMinSdkVersion - - withHostTest { - isIncludeAndroidResources = true - } - - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - } - js { - binaries.library() - useCommonJs() - browser { - testTask(Action { - useMocha { - timeout = "10s" - } - }) - } - } - - @OptIn(ExperimentalWasmDsl::class) - wasmJs { - binaries.library() - useCommonJs() - browser { - testTask(Action { - useMocha { - timeout = "10s" - } - }) - } - } - - listOf( - iosX64(), - iosArm64(), - iosSimulatorArm64() - ).forEach { - it.binaries.framework { - baseName = "coroutines" - isStatic = true - } - } - sourceSets { commonMain.dependencies { api(project(":commonstatemachine")) api(libs.kotlin.coroutines.core) } commonTest.dependencies { - implementation(libs.test.kotlin) implementation(libs.test.kotlin.coroutines) } } } -val javadocJar by tasks.registering(Jar::class) { - dependsOn(tasks.dokkaGenerate) - group = "documentation" - archiveClassifier.set("javadoc") - from(tasks.dokkaGenerate) -} - -val libId = "coroutines" -val libName = "coroutines" -val libDesc = "Coroutines extension for CommonStateMachine" -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/coroutines/gradle.properties b/coroutines/gradle.properties new file mode 100644 index 0000000..7241a4d --- /dev/null +++ b/coroutines/gradle.properties @@ -0,0 +1,14 @@ +# +# 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. +# +#Project +libDesc=Coroutines extension for CommonStateMachine diff --git a/examples/androidcore/build.gradle.kts b/examples/androidcore/build.gradle.kts deleted file mode 100644 index a89e506..0000000 --- a/examples/androidcore/build.gradle.kts +++ /dev/null @@ -1,79 +0,0 @@ -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 { - 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_21 - targetCompatibility = JavaVersion.VERSION_21 - isCoreLibraryDesugaringEnabled = true - } - kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - } - buildFeatures { - compose = true - } - namespace = "com.motorro.statemachine.androidcore" -} - -dependencies { - api(libs.timber) - - coreLibraryDesugaring(libs.desugaring) - - implementation(libs.kotlin.coroutines.core) - - 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) - - debugImplementation(libs.compose.tooling) - - testImplementation(libs.bundles.test.core) - testImplementation(libs.test.androidx.core) - testImplementation(libs.test.kotlin.coroutines) -} diff --git a/examples/androidcore/src/main/AndroidManifest.xml b/examples/androidcore/src/main/AndroidManifest.xml deleted file mode 100644 index cfc666d..0000000 --- a/examples/androidcore/src/main/AndroidManifest.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/examples/androidcore/src/main/java/com/motorro/statemachine/androidcore/ui/theme/Theme.kt b/examples/androidcore/src/main/java/com/motorro/statemachine/androidcore/ui/theme/Theme.kt deleted file mode 100644 index 373f154..0000000 --- a/examples/androidcore/src/main/java/com/motorro/statemachine/androidcore/ui/theme/Theme.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2023 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.ui.theme - -import android.os.Build -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext - -private val DarkColorScheme = darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80 -) - -private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40 - - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), - onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ -) - -@Composable -fun CommonStateMachineTheme( - darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ - dynamicColor: Boolean = true, - content: @Composable () -> Unit -) { - val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } - - darkTheme -> DarkColorScheme - else -> LightColorScheme - } - - MaterialTheme( - colorScheme = colorScheme, - typography = Typography, - content = content - ) -} \ No newline at end of file diff --git a/examples/androidcore/src/main/java/com/motorro/statemachine/androidcore/ui/theme/Type.kt b/examples/androidcore/src/main/java/com/motorro/statemachine/androidcore/ui/theme/Type.kt deleted file mode 100644 index 6451de1..0000000 --- a/examples/androidcore/src/main/java/com/motorro/statemachine/androidcore/ui/theme/Type.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.motorro.statemachine.androidcore.ui.theme - -import androidx.compose.material3.Typography -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp - -// Set of Material typography styles to start with -val Typography = Typography( - bodyLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp - ) - /* Other default text styles to override - titleLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp - ), - labelSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ) - */ -) \ No newline at end of file diff --git a/examples/books/app/build.gradle.kts b/examples/books/app/build.gradle.kts index 27a53ff..5aa005e 100644 --- a/examples/books/app/build.gradle.kts +++ b/examples/books/app/build.gradle.kts @@ -11,80 +11,12 @@ * 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.motorro.android.app) 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_21 - targetCompatibility = JavaVersion.VERSION_21 - } - kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - } - 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:commonflow-data")) @@ -92,37 +24,15 @@ dependencies { implementation(project(":commonflow: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/book/demo/src/main/kotlin/com/motorro/statemachine/books/book/demo/App.kt b/examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/App.kt similarity index 82% rename from examples/books/book/demo/src/main/kotlin/com/motorro/statemachine/books/book/demo/App.kt rename to examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/App.kt index 5567f41..f96bccd 100644 --- a/examples/books/book/demo/src/main/kotlin/com/motorro/statemachine/books/book/demo/App.kt +++ b/examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/App.kt @@ -11,11 +11,13 @@ * limitations under the License. */ -package com.motorro.statemachine.books.book.demo +package com.motorro.commonstatemachine.examples.books.app import android.app.Application import dagger.hilt.android.HiltAndroidApp -import timber.log.Timber +import io.github.aakira.napier.DebugAntilog +import io.github.aakira.napier.Napier + @HiltAndroidApp class App : Application() { @@ -25,6 +27,6 @@ class App : Application() { } private fun setupLogger() { - Timber.plant(Timber.DebugTree()) + Napier.base(DebugAntilog()) } } \ 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/commonstatemachine/examples/books/app/MainActivity.kt similarity index 77% rename from examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/MainActivity.kt rename to examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/MainActivity.kt index ad1bf33..ed4e8be 100644 --- a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/MainActivity.kt +++ b/examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/MainActivity.kt @@ -11,23 +11,23 @@ * limitations under the License. */ -package com.motorro.statemachine.books.app +package com.motorro.commonstatemachine.examples.books.app import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.enableEdgeToEdge +import com.motorro.commonstatemachine.examples.books.app.api.MainGesture +import com.motorro.commonstatemachine.examples.books.app.api.MainUiState +import com.motorro.commonstatemachine.examples.books.app.ui.MainScreen +import com.motorro.commonstatemachine.examples.commoncore.ui.theme.CommonStateMachineTheme 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) + enableEdgeToEdge() setStateMachineContent { state, onGesture -> CommonStateMachineTheme { 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/commonstatemachine/examples/books/app/MainViewModel.kt similarity index 77% rename from examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/MainViewModel.kt rename to examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/MainViewModel.kt index ed17c66..5672200 100644 --- a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/MainViewModel.kt +++ b/examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/MainViewModel.kt @@ -11,12 +11,12 @@ * limitations under the License. */ -package com.motorro.statemachine.books.app +package com.motorro.commonstatemachine.examples.books.app +import com.motorro.commonstatemachine.examples.books.app.api.MainApi +import com.motorro.commonstatemachine.examples.books.app.api.MainGesture +import com.motorro.commonstatemachine.examples.books.app.api.MainUiState 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 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/commonstatemachine/examples/books/app/api/MainApi.kt similarity index 85% rename from examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/api/MainApi.kt rename to examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/api/MainApi.kt index 5a58725..d573545 100644 --- a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/api/MainApi.kt +++ b/examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/api/MainApi.kt @@ -11,13 +11,13 @@ * limitations under the License. */ -package com.motorro.statemachine.books.app.api +package com.motorro.commonstatemachine.examples.books.app.api import com.motorro.commonstatemachine.CommonMachineState +import com.motorro.commonstatemachine.examples.books.app.data.MainDataState +import com.motorro.commonstatemachine.examples.books.app.state.MainFactory 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 /** 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/commonstatemachine/examples/books/app/api/MainFlowHost.kt similarity index 92% rename from examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/api/MainFlowHost.kt rename to examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/api/MainFlowHost.kt index 32b28fa..a51da36 100644 --- a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/api/MainFlowHost.kt +++ b/examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/api/MainFlowHost.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.books.app.api +package com.motorro.commonstatemachine.examples.books.app.api import com.motorro.commonstatemachine.flow.data.CommonFlowHost 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/commonstatemachine/examples/books/app/api/MainGesture.kt similarity index 87% rename from examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/api/MainGesture.kt rename to examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/api/MainGesture.kt index b2d7fe0..7d2a184 100644 --- a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/api/MainGesture.kt +++ b/examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/api/MainGesture.kt @@ -11,9 +11,9 @@ * limitations under the License. */ -package com.motorro.statemachine.books.app.api +package com.motorro.commonstatemachine.examples.books.app.api -import com.motorro.statemachine.books.book.api.BookGesture +import com.motorro.commonstatemachine.examples.books.book.api.BookGesture /** * Main flow gesture 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/commonstatemachine/examples/books/app/api/MainUiState.kt similarity index 84% rename from examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/api/MainUiState.kt rename to examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/api/MainUiState.kt index 6e245e2..de2d97c 100644 --- a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/api/MainUiState.kt +++ b/examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/api/MainUiState.kt @@ -11,11 +11,11 @@ * limitations under the License. */ -package com.motorro.statemachine.books.app.api +package com.motorro.commonstatemachine.examples.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 com.motorro.commonstatemachine.examples.books.book.api.BookUiState +import com.motorro.commonstatemachine.examples.books.domain.entity.ListBook import kotlinx.collections.immutable.ImmutableList /** 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/commonstatemachine/examples/books/app/data/MainDataState.kt similarity index 84% rename from examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/data/MainDataState.kt rename to examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/data/MainDataState.kt index cc6f6f7..2c8aef3 100644 --- a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/data/MainDataState.kt +++ b/examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/data/MainDataState.kt @@ -11,9 +11,9 @@ * limitations under the License. */ -package com.motorro.statemachine.books.app.data +package com.motorro.commonstatemachine.examples.books.app.data -import com.motorro.statemachine.books.domain.entity.ListBook +import com.motorro.commonstatemachine.examples.books.domain.entity.ListBook import kotlinx.collections.immutable.ImmutableList /** 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/commonstatemachine/examples/books/app/di/ApplicationModule.kt similarity index 90% rename from examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/di/ApplicationModule.kt rename to examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/di/ApplicationModule.kt index 15f36d9..c897abb 100644 --- a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/di/ApplicationModule.kt +++ b/examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/di/ApplicationModule.kt @@ -11,11 +11,11 @@ * limitations under the License. */ -package com.motorro.statemachine.books.app.di +package com.motorro.commonstatemachine.examples.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 com.motorro.commonstatemachine.examples.books.app.repository.BookRepositoryImpl +import com.motorro.commonstatemachine.examples.books.domain.entity.Book +import com.motorro.commonstatemachine.examples.books.domain.repository.BookRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn 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/commonstatemachine/examples/books/app/repository/BookRepositoryImpl.kt similarity index 84% rename from examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/repository/BookRepositoryImpl.kt rename to examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/repository/BookRepositoryImpl.kt index 436860b..64d10fb 100644 --- a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/repository/BookRepositoryImpl.kt +++ b/examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/repository/BookRepositoryImpl.kt @@ -11,11 +11,11 @@ * limitations under the License. */ -package com.motorro.statemachine.books.app.repository +package com.motorro.commonstatemachine.examples.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 com.motorro.commonstatemachine.examples.books.domain.entity.Book +import com.motorro.commonstatemachine.examples.books.domain.entity.ListBook +import com.motorro.commonstatemachine.examples.books.domain.repository.BookRepository import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay 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/commonstatemachine/examples/books/app/state/BaseMainState.kt similarity index 76% rename from examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/state/BaseMainState.kt rename to examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/state/BaseMainState.kt index a7e4831..c9a5fbd 100644 --- a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/state/BaseMainState.kt +++ b/examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/state/BaseMainState.kt @@ -11,12 +11,12 @@ * limitations under the License. */ -package com.motorro.statemachine.books.app.state +package com.motorro.commonstatemachine.examples.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 +import com.motorro.commonstatemachine.examples.books.app.api.MainGesture +import com.motorro.commonstatemachine.examples.books.app.api.MainUiState +import com.motorro.commonstatemachine.examples.commoncore.log.Logger internal abstract class BaseMainState(context: MainContext) : CoroutineState(), MainContext by context { override fun doProcess(gesture: MainGesture) { 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/commonstatemachine/examples/books/app/state/ItemListState.kt similarity index 80% rename from examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/state/ItemListState.kt rename to examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/state/ItemListState.kt index 46903c6..6d333b3 100644 --- a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/state/ItemListState.kt +++ b/examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/state/ItemListState.kt @@ -11,15 +11,15 @@ * limitations under the License. */ -package com.motorro.statemachine.books.app.state +package com.motorro.commonstatemachine.examples.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 com.motorro.commonstatemachine.examples.books.app.api.MainGesture +import com.motorro.commonstatemachine.examples.books.app.api.MainUiState +import com.motorro.commonstatemachine.examples.books.app.data.MainDataState +import com.motorro.commonstatemachine.examples.books.book.api.BookInput +import com.motorro.commonstatemachine.examples.books.domain.entity.ListBook +import com.motorro.commonstatemachine.examples.books.domain.repository.BookRepository +import com.motorro.commonstatemachine.examples.commoncore.log.Logger import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach 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/commonstatemachine/examples/books/app/state/ItemProxy.kt similarity index 70% rename from examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/state/ItemProxy.kt rename to examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/state/ItemProxy.kt index 59e0311..01cc0af 100644 --- a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/state/ItemProxy.kt +++ b/examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/state/ItemProxy.kt @@ -11,18 +11,18 @@ * limitations under the License. */ -package com.motorro.statemachine.books.app.state +package com.motorro.commonstatemachine.examples.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 com.motorro.commonstatemachine.examples.books.app.api.MainGesture +import com.motorro.commonstatemachine.examples.books.app.api.MainUiState +import com.motorro.commonstatemachine.examples.books.app.data.MainDataState +import com.motorro.commonstatemachine.examples.books.book.api.BookApi +import com.motorro.commonstatemachine.examples.books.book.api.BookGesture +import com.motorro.commonstatemachine.examples.books.book.api.BookInput +import com.motorro.commonstatemachine.examples.books.book.api.BookUiState +import com.motorro.commonstatemachine.examples.books.book.api.ItemFlowHost +import com.motorro.commonstatemachine.examples.commoncore.log.Logger import javax.inject.Inject internal class ItemProxy( 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/commonstatemachine/examples/books/app/state/MainContext.kt similarity index 91% rename from examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/state/MainContext.kt rename to examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/state/MainContext.kt index 76543a8..96161a9 100644 --- a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/state/MainContext.kt +++ b/examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/state/MainContext.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.books.app.state +package com.motorro.commonstatemachine.examples.books.app.state /** * Main flow context 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/commonstatemachine/examples/books/app/state/MainFactory.kt similarity index 82% rename from examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/state/MainFactory.kt rename to examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/state/MainFactory.kt index 34d8d2e..75d4523 100644 --- a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/state/MainFactory.kt +++ b/examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/state/MainFactory.kt @@ -11,14 +11,14 @@ * limitations under the License. */ -package com.motorro.statemachine.books.app.state +package com.motorro.commonstatemachine.examples.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 com.motorro.commonstatemachine.examples.books.app.api.MainFlowHost +import com.motorro.commonstatemachine.examples.books.app.api.MainGesture +import com.motorro.commonstatemachine.examples.books.app.api.MainUiState +import com.motorro.commonstatemachine.examples.books.app.data.MainDataState +import com.motorro.commonstatemachine.examples.books.book.api.BookInput import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject 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/commonstatemachine/examples/books/app/ui/BookListView.kt similarity index 91% rename from examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/ui/BookListView.kt rename to examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/ui/BookListView.kt index c332b4f..57762b2 100644 --- a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/ui/BookListView.kt +++ b/examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/ui/BookListView.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.books.app.ui +package com.motorro.commonstatemachine.examples.books.app.ui import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column @@ -27,8 +27,8 @@ 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 com.motorro.commonstatemachine.examples.books.domain.entity.ListBook +import com.motorro.commonstatemachine.examples.commoncore.ui.theme.CommonStateMachineTheme import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList 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/commonstatemachine/examples/books/app/ui/MainScreen.kt similarity index 87% rename from examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/ui/MainScreen.kt rename to examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/ui/MainScreen.kt index 1fc3d31..d8bfc40 100644 --- a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/ui/MainScreen.kt +++ b/examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/ui/MainScreen.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.books.app.ui +package com.motorro.commonstatemachine.examples.books.app.ui import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContentTransitionScope @@ -30,13 +30,13 @@ 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 com.motorro.commonstatemachine.examples.books.app.R +import com.motorro.commonstatemachine.examples.books.app.api.MainGesture +import com.motorro.commonstatemachine.examples.books.app.api.MainGesture.Book +import com.motorro.commonstatemachine.examples.books.app.api.MainGesture.BookSelected +import com.motorro.commonstatemachine.examples.books.app.api.MainUiState +import com.motorro.commonstatemachine.examples.books.book.ui.BookScreen +import com.motorro.commonstatemachine.examples.commoncore.ui.Loading import kotlin.reflect.KClass @Composable 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/commonstatemachine/examples/books/app/mock.kt similarity index 86% rename from examples/books/app/src/test/kotlin/com/motorro/statemachine/books/app/mock.kt rename to examples/books/app/src/test/kotlin/com/motorro/commonstatemachine/examples/books/app/mock.kt index dd62339..7d36ddd 100644 --- a/examples/books/app/src/test/kotlin/com/motorro/statemachine/books/app/mock.kt +++ b/examples/books/app/src/test/kotlin/com/motorro/commonstatemachine/examples/books/app/mock.kt @@ -11,9 +11,9 @@ * limitations under the License. */ -package com.motorro.statemachine.books.app +package com.motorro.commonstatemachine.examples.books.app -import com.motorro.statemachine.books.domain.entity.ListBook +import com.motorro.commonstatemachine.examples.books.domain.entity.ListBook import kotlinx.collections.immutable.persistentListOf val BOOK_1 = ListBook( 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/commonstatemachine/examples/books/app/state/BaseStateTest.kt similarity index 90% rename from examples/books/app/src/test/kotlin/com/motorro/statemachine/books/app/state/BaseStateTest.kt rename to examples/books/app/src/test/kotlin/com/motorro/commonstatemachine/examples/books/app/state/BaseStateTest.kt index 87bbd1d..8fb1d8b 100644 --- a/examples/books/app/src/test/kotlin/com/motorro/statemachine/books/app/state/BaseStateTest.kt +++ b/examples/books/app/src/test/kotlin/com/motorro/commonstatemachine/examples/books/app/state/BaseStateTest.kt @@ -11,11 +11,11 @@ * limitations under the License. */ -package com.motorro.statemachine.books.app.state +package com.motorro.commonstatemachine.examples.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 com.motorro.commonstatemachine.examples.books.app.api.MainGesture +import com.motorro.commonstatemachine.examples.books.app.api.MainUiState import io.mockk.mockk import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi 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/commonstatemachine/examples/books/app/state/ItemListStateTest.kt similarity index 85% rename from examples/books/app/src/test/kotlin/com/motorro/statemachine/books/app/state/ItemListStateTest.kt rename to examples/books/app/src/test/kotlin/com/motorro/commonstatemachine/examples/books/app/state/ItemListStateTest.kt index b4ccfa8..3e503bf 100644 --- a/examples/books/app/src/test/kotlin/com/motorro/statemachine/books/app/state/ItemListStateTest.kt +++ b/examples/books/app/src/test/kotlin/com/motorro/commonstatemachine/examples/books/app/state/ItemListStateTest.kt @@ -11,16 +11,16 @@ * 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 +package com.motorro.commonstatemachine.examples.books.app.state + +import com.motorro.commonstatemachine.examples.books.app.BOOK_1 +import com.motorro.commonstatemachine.examples.books.app.BOOK_2 +import com.motorro.commonstatemachine.examples.books.app.api.MainGesture +import com.motorro.commonstatemachine.examples.books.app.api.MainUiState +import com.motorro.commonstatemachine.examples.books.app.data.MainDataState +import com.motorro.commonstatemachine.examples.books.book.api.BookInput +import com.motorro.commonstatemachine.examples.books.domain.entity.ListBook +import com.motorro.commonstatemachine.examples.books.domain.repository.BookRepository import io.mockk.Ordering import io.mockk.coVerify import io.mockk.every diff --git a/examples/books/book/build.gradle.kts b/examples/books/book/build.gradle.kts index 2916083..acb05a2 100644 --- a/examples/books/book/build.gradle.kts +++ b/examples/books/book/build.gradle.kts @@ -1,5 +1,3 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - /* * Copyright 2026 Nikolai Kotchetkov. * Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,77 +12,24 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget */ plugins { - alias(libs.plugins.android.lib) - alias(libs.plugins.compose) + alias(libs.plugins.motorro.android.lib.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_21 - targetCompatibility = JavaVersion.VERSION_21 - isCoreLibraryDesugaringEnabled = true - } - kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - } - buildFeatures { - compose = true - } - namespace = "com.motorro.statemachine.books.book" -} - dependencies { implementation(project(":commonstatemachine")) implementation(project(":coroutines")) implementation(project(":commonflow: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/demo/build.gradle.kts b/examples/books/book/demo/build.gradle.kts index e28d565..e441b8f 100644 --- a/examples/books/book/demo/build.gradle.kts +++ b/examples/books/book/demo/build.gradle.kts @@ -11,80 +11,12 @@ * 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.motorro.android.app) 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_21 - targetCompatibility = JavaVersion.VERSION_21 - } - kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - } - 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:commonflow-data")) @@ -92,34 +24,13 @@ dependencies { implementation(project(":commonflow: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/app/src/main/kotlin/com/motorro/statemachine/books/app/App.kt b/examples/books/book/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/demo/App.kt similarity index 82% rename from examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/App.kt rename to examples/books/book/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/demo/App.kt index b00c8e2..8c5347f 100644 --- a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/App.kt +++ b/examples/books/book/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/demo/App.kt @@ -11,11 +11,13 @@ * limitations under the License. */ -package com.motorro.statemachine.books.app +package com.motorro.commonstatemachine.examples.books.book.demo import android.app.Application import dagger.hilt.android.HiltAndroidApp -import timber.log.Timber +import io.github.aakira.napier.DebugAntilog +import io.github.aakira.napier.Napier + @HiltAndroidApp class App : Application() { @@ -25,6 +27,6 @@ class App : Application() { } private fun setupLogger() { - Timber.plant(Timber.DebugTree()) + Napier.base(DebugAntilog()) } } \ 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/commonstatemachine/examples/books/book/demo/MainActivity.kt similarity index 77% rename from examples/books/book/demo/src/main/kotlin/com/motorro/statemachine/books/book/demo/MainActivity.kt rename to examples/books/book/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/demo/MainActivity.kt index 47aa33c..10792c9 100644 --- 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/commonstatemachine/examples/books/book/demo/MainActivity.kt @@ -11,17 +11,17 @@ * limitations under the License. */ -package com.motorro.statemachine.books.book.demo +package com.motorro.commonstatemachine.examples.books.book.demo import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.enableEdgeToEdge +import com.motorro.commonstatemachine.examples.books.book.api.BookGesture +import com.motorro.commonstatemachine.examples.books.book.api.BookInput +import com.motorro.commonstatemachine.examples.books.book.api.BookUiState +import com.motorro.commonstatemachine.examples.books.book.ui.BookScreen +import com.motorro.commonstatemachine.examples.commoncore.ui.theme.CommonStateMachineTheme 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 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/commonstatemachine/examples/books/book/demo/MainViewModel.kt similarity index 77% rename from examples/books/book/demo/src/main/kotlin/com/motorro/statemachine/books/book/demo/MainViewModel.kt rename to examples/books/book/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/demo/MainViewModel.kt index 143073a..e8f22f4 100644 --- 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/commonstatemachine/examples/books/book/demo/MainViewModel.kt @@ -11,13 +11,13 @@ * limitations under the License. */ -package com.motorro.statemachine.books.book.demo +package com.motorro.commonstatemachine.examples.books.book.demo +import com.motorro.commonstatemachine.examples.books.book.api.BookApi +import com.motorro.commonstatemachine.examples.books.book.api.BookGesture +import com.motorro.commonstatemachine.examples.books.book.api.BookInput +import com.motorro.commonstatemachine.examples.books.book.api.BookUiState 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 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/commonstatemachine/examples/books/book/demo/data.kt similarity index 87% rename from examples/books/book/demo/src/main/kotlin/com/motorro/statemachine/books/book/demo/data.kt rename to examples/books/book/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/demo/data.kt index bd22232..ebc80d2 100644 --- 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/commonstatemachine/examples/books/book/demo/data.kt @@ -11,9 +11,9 @@ * limitations under the License. */ -package com.motorro.statemachine.books.book.demo +package com.motorro.commonstatemachine.examples.books.book.demo -import com.motorro.statemachine.books.domain.entity.Book +import com.motorro.commonstatemachine.examples.books.domain.entity.Book import kotlinx.collections.immutable.persistentListOf internal val BOOK = Book( 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/commonstatemachine/examples/books/book/demo/di/ApplicationModule.kt similarity index 79% rename from examples/books/book/demo/src/main/kotlin/com/motorro/statemachine/books/book/demo/di/ApplicationModule.kt rename to examples/books/book/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/demo/di/ApplicationModule.kt index 56d4fee..65e5870 100644 --- 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/commonstatemachine/examples/books/book/demo/di/ApplicationModule.kt @@ -11,12 +11,12 @@ * limitations under the License. */ -package com.motorro.statemachine.books.book.demo.di +package com.motorro.commonstatemachine.examples.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 com.motorro.commonstatemachine.examples.books.book.demo.BOOK +import com.motorro.commonstatemachine.examples.books.domain.entity.Book +import com.motorro.commonstatemachine.examples.books.domain.entity.ListBook +import com.motorro.commonstatemachine.examples.books.domain.repository.BookRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn 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/commonstatemachine/examples/books/book/api/BookApi.kt similarity index 90% rename from examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/api/BookApi.kt rename to examples/books/book/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/api/BookApi.kt index fe12321..f0c5006 100644 --- a/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/api/BookApi.kt +++ b/examples/books/book/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/api/BookApi.kt @@ -11,12 +11,12 @@ * limitations under the License. */ -package com.motorro.statemachine.books.book.api +package com.motorro.commonstatemachine.examples.books.book.api import com.motorro.commonstatemachine.CommonMachineState +import com.motorro.commonstatemachine.examples.books.book.state.BookFactory 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 /** 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/commonstatemachine/examples/books/book/api/BookFlowHost.kt similarity index 92% rename from examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/api/BookFlowHost.kt rename to examples/books/book/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/api/BookFlowHost.kt index 877d02d..fcb71d6 100644 --- a/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/api/BookFlowHost.kt +++ b/examples/books/book/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/api/BookFlowHost.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.books.book.api +package com.motorro.commonstatemachine.examples.books.book.api import com.motorro.commonstatemachine.flow.data.CommonFlowHost 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/commonstatemachine/examples/books/book/api/BookGesture.kt similarity index 93% rename from examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/api/BookGesture.kt rename to examples/books/book/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/api/BookGesture.kt index 39b6daa..e78cce6 100644 --- a/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/api/BookGesture.kt +++ b/examples/books/book/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/api/BookGesture.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.books.book.api +package com.motorro.commonstatemachine.examples.books.book.api /** * Item item flow gesture 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/commonstatemachine/examples/books/book/api/BookInput.kt similarity index 91% rename from examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/api/BookInput.kt rename to examples/books/book/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/api/BookInput.kt index 04c4953..606c343 100644 --- a/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/api/BookInput.kt +++ b/examples/books/book/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/api/BookInput.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.books.book.api +package com.motorro.commonstatemachine.examples.books.book.api /** * Flow input 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/commonstatemachine/examples/books/book/api/BookUiState.kt similarity index 88% rename from examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/api/BookUiState.kt rename to examples/books/book/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/api/BookUiState.kt index 89bb7e1..b72f8c5 100644 --- a/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/api/BookUiState.kt +++ b/examples/books/book/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/api/BookUiState.kt @@ -11,9 +11,9 @@ * limitations under the License. */ -package com.motorro.statemachine.books.book.api +package com.motorro.commonstatemachine.examples.books.book.api -import com.motorro.statemachine.books.domain.entity.Book +import com.motorro.commonstatemachine.examples.books.domain.entity.Book /** * Item sub-flow UI state 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/commonstatemachine/examples/books/book/data/BookDataState.kt similarity index 77% rename from examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/data/BookDataState.kt rename to examples/books/book/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/data/BookDataState.kt index 075b7bc..8256338 100644 --- a/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/data/BookDataState.kt +++ b/examples/books/book/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/data/BookDataState.kt @@ -11,10 +11,10 @@ * limitations under the License. */ -package com.motorro.statemachine.books.book.data +package com.motorro.commonstatemachine.examples.books.book.data -import com.motorro.statemachine.books.book.api.BookInput -import com.motorro.statemachine.books.domain.entity.Book +import com.motorro.commonstatemachine.examples.books.book.api.BookInput +import com.motorro.commonstatemachine.examples.books.domain.entity.Book /** * Item sub-flow internal data 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/commonstatemachine/examples/books/book/state/BaseBookState.kt similarity index 76% rename from examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/BaseBookState.kt rename to examples/books/book/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/state/BaseBookState.kt index 90277d3..f6ce151 100644 --- a/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/BaseBookState.kt +++ b/examples/books/book/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/state/BaseBookState.kt @@ -11,12 +11,12 @@ * limitations under the License. */ -package com.motorro.statemachine.books.book.state +package com.motorro.commonstatemachine.examples.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 +import com.motorro.commonstatemachine.examples.books.book.api.BookGesture +import com.motorro.commonstatemachine.examples.books.book.api.BookUiState +import com.motorro.commonstatemachine.examples.commoncore.log.Logger internal abstract class BaseBookState(context: BookContext) : CoroutineState(), BookContext by context { override fun doProcess(gesture: BookGesture) { 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/commonstatemachine/examples/books/book/state/BookContext.kt similarity index 91% rename from examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/BookContext.kt rename to examples/books/book/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/state/BookContext.kt index 6e8ce27..286734b 100644 --- a/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/BookContext.kt +++ b/examples/books/book/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/state/BookContext.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.books.book.state +package com.motorro.commonstatemachine.examples.books.book.state /** * Item sub-flow common components 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/commonstatemachine/examples/books/book/state/BookFactory.kt similarity index 86% rename from examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/BookFactory.kt rename to examples/books/book/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/state/BookFactory.kt index 9abdb71..0c3b2a9 100644 --- a/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/BookFactory.kt +++ b/examples/books/book/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/state/BookFactory.kt @@ -11,14 +11,14 @@ * limitations under the License. */ -package com.motorro.statemachine.books.book.state +package com.motorro.commonstatemachine.examples.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 com.motorro.commonstatemachine.examples.books.book.api.BookGesture +import com.motorro.commonstatemachine.examples.books.book.api.BookInput +import com.motorro.commonstatemachine.examples.books.book.api.BookUiState +import com.motorro.commonstatemachine.examples.books.book.api.ItemFlowHost +import com.motorro.commonstatemachine.examples.books.book.data.BookDataState import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject 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/commonstatemachine/examples/books/book/state/ContentState.kt similarity index 79% rename from examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/ContentState.kt rename to examples/books/book/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/state/ContentState.kt index d8812ae..84b7228 100644 --- a/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/ContentState.kt +++ b/examples/books/book/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/state/ContentState.kt @@ -11,12 +11,12 @@ * limitations under the License. */ -package com.motorro.statemachine.books.book.state +package com.motorro.commonstatemachine.examples.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 +import com.motorro.commonstatemachine.examples.books.book.api.BookGesture +import com.motorro.commonstatemachine.examples.books.book.api.BookUiState +import com.motorro.commonstatemachine.examples.books.book.data.BookDataState +import com.motorro.commonstatemachine.examples.commoncore.log.Logger internal class ContentState( context: BookContext, 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/commonstatemachine/examples/books/book/state/DeleteConfirmationState.kt similarity index 79% rename from examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/DeleteConfirmationState.kt rename to examples/books/book/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/state/DeleteConfirmationState.kt index c03bf53..5ecec28 100644 --- a/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/DeleteConfirmationState.kt +++ b/examples/books/book/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/state/DeleteConfirmationState.kt @@ -11,12 +11,12 @@ * limitations under the License. */ -package com.motorro.statemachine.books.book.state +package com.motorro.commonstatemachine.examples.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 +import com.motorro.commonstatemachine.examples.books.book.api.BookGesture +import com.motorro.commonstatemachine.examples.books.book.api.BookUiState +import com.motorro.commonstatemachine.examples.books.book.data.BookDataState +import com.motorro.commonstatemachine.examples.commoncore.log.Logger internal class DeleteConfirmationState( context: BookContext, 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/commonstatemachine/examples/books/book/state/DeletingState.kt similarity index 82% rename from examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/DeletingState.kt rename to examples/books/book/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/state/DeletingState.kt index c162702..d2db124 100644 --- a/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/DeletingState.kt +++ b/examples/books/book/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/state/DeletingState.kt @@ -11,12 +11,12 @@ * limitations under the License. */ -package com.motorro.statemachine.books.book.state +package com.motorro.commonstatemachine.examples.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 com.motorro.commonstatemachine.examples.books.book.api.BookUiState +import com.motorro.commonstatemachine.examples.books.book.data.BookDataState +import com.motorro.commonstatemachine.examples.books.domain.repository.BookRepository +import com.motorro.commonstatemachine.examples.commoncore.log.Logger import kotlinx.coroutines.launch import javax.inject.Inject 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/commonstatemachine/examples/books/book/state/ErrorState.kt similarity index 77% rename from examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/ErrorState.kt rename to examples/books/book/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/state/ErrorState.kt index 942f150..87178a9 100644 --- a/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/ErrorState.kt +++ b/examples/books/book/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/state/ErrorState.kt @@ -11,12 +11,12 @@ * limitations under the License. */ -package com.motorro.statemachine.books.book.state +package com.motorro.commonstatemachine.examples.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 +import com.motorro.commonstatemachine.examples.books.book.api.BookGesture +import com.motorro.commonstatemachine.examples.books.book.api.BookUiState +import com.motorro.commonstatemachine.examples.books.book.data.BookDataState +import com.motorro.commonstatemachine.examples.commoncore.log.Logger internal class ErrorState( context: BookContext, 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/commonstatemachine/examples/books/book/state/LoadingState.kt similarity index 81% rename from examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/LoadingState.kt rename to examples/books/book/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/state/LoadingState.kt index c8c8c48..b42d66c 100644 --- a/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/state/LoadingState.kt +++ b/examples/books/book/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/state/LoadingState.kt @@ -11,13 +11,13 @@ * limitations under the License. */ -package com.motorro.statemachine.books.book.state +package com.motorro.commonstatemachine.examples.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 com.motorro.commonstatemachine.examples.books.book.api.BookGesture +import com.motorro.commonstatemachine.examples.books.book.api.BookUiState +import com.motorro.commonstatemachine.examples.books.book.data.BookDataState +import com.motorro.commonstatemachine.examples.books.domain.repository.BookRepository +import com.motorro.commonstatemachine.examples.commoncore.log.Logger import kotlinx.coroutines.ensureActive import kotlinx.coroutines.launch import javax.inject.Inject 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/commonstatemachine/examples/books/book/ui/BookContentScreen.kt similarity index 93% rename from examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/ui/BookContentScreen.kt rename to examples/books/book/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/ui/BookContentScreen.kt index c93ae76..860fa6d 100644 --- a/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/ui/BookContentScreen.kt +++ b/examples/books/book/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/ui/BookContentScreen.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.books.book.ui +package com.motorro.commonstatemachine.examples.books.book.ui import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -38,10 +38,10 @@ 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 com.motorro.commonstatemachine.examples.books.book.R +import com.motorro.commonstatemachine.examples.books.book.api.BookGesture +import com.motorro.commonstatemachine.examples.books.book.api.BookUiState +import com.motorro.commonstatemachine.examples.books.domain.entity.Book import kotlinx.collections.immutable.persistentListOf @Composable 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/commonstatemachine/examples/books/book/ui/BookScreen.kt similarity index 88% rename from examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/ui/BookScreen.kt rename to examples/books/book/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/ui/BookScreen.kt index da169e2..ce61ad0 100644 --- a/examples/books/book/src/main/kotlin/com/motorro/statemachine/books/book/ui/BookScreen.kt +++ b/examples/books/book/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/ui/BookScreen.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.books.book.ui +package com.motorro.commonstatemachine.examples.books.book.ui import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons @@ -25,11 +25,11 @@ 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 +import com.motorro.commonstatemachine.examples.books.book.R +import com.motorro.commonstatemachine.examples.books.book.api.BookGesture +import com.motorro.commonstatemachine.examples.books.book.api.BookUiState +import com.motorro.commonstatemachine.examples.commoncore.ui.Error +import com.motorro.commonstatemachine.examples.commoncore.ui.Loading @Composable fun BookScreen(state: BookUiState, onGesture: (BookGesture) -> Unit) { 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/commonstatemachine/examples/books/book/mock.kt similarity index 87% rename from examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/mock.kt rename to examples/books/book/src/test/kotlin/com/motorro/commonstatemachine/examples/books/book/mock.kt index 974ceae..0291623 100644 --- a/examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/mock.kt +++ b/examples/books/book/src/test/kotlin/com/motorro/commonstatemachine/examples/books/book/mock.kt @@ -11,9 +11,9 @@ * limitations under the License. */ -package com.motorro.statemachine.books.book +package com.motorro.commonstatemachine.examples.books.book -import com.motorro.statemachine.books.domain.entity.Book +import com.motorro.commonstatemachine.examples.books.domain.entity.Book import kotlinx.collections.immutable.persistentListOf internal const val BOOK_ID = 10 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/commonstatemachine/examples/books/book/state/BaseStateTest.kt similarity index 90% rename from examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/state/BaseStateTest.kt rename to examples/books/book/src/test/kotlin/com/motorro/commonstatemachine/examples/books/book/state/BaseStateTest.kt index 1533a52..82866db 100644 --- a/examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/state/BaseStateTest.kt +++ b/examples/books/book/src/test/kotlin/com/motorro/commonstatemachine/examples/books/book/state/BaseStateTest.kt @@ -11,11 +11,11 @@ * limitations under the License. */ -package com.motorro.statemachine.books.book.state +package com.motorro.commonstatemachine.examples.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 com.motorro.commonstatemachine.examples.books.book.api.BookGesture +import com.motorro.commonstatemachine.examples.books.book.api.BookUiState import io.mockk.mockk import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi 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/commonstatemachine/examples/books/book/state/ContentStateTest.kt similarity index 78% rename from examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/state/ContentStateTest.kt rename to examples/books/book/src/test/kotlin/com/motorro/commonstatemachine/examples/books/book/state/ContentStateTest.kt index cc42f52..0e6eb19 100644 --- a/examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/state/ContentStateTest.kt +++ b/examples/books/book/src/test/kotlin/com/motorro/commonstatemachine/examples/books/book/state/ContentStateTest.kt @@ -11,15 +11,15 @@ * limitations under the License. */ -package com.motorro.statemachine.books.book.state +package com.motorro.commonstatemachine.examples.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.commonstatemachine.examples.books.book.BOOK_ID +import com.motorro.commonstatemachine.examples.books.book.BOOK_TITLE +import com.motorro.commonstatemachine.examples.books.book.Book +import com.motorro.commonstatemachine.examples.books.book.api.BookGesture +import com.motorro.commonstatemachine.examples.books.book.api.BookInput +import com.motorro.commonstatemachine.examples.books.book.api.BookUiState +import com.motorro.commonstatemachine.examples.books.book.data.BookDataState import io.mockk.every import io.mockk.verify import org.junit.Test 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/commonstatemachine/examples/books/book/state/DeleteConfirmationStateTest.kt similarity index 80% rename from examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/state/DeleteConfirmationStateTest.kt rename to examples/books/book/src/test/kotlin/com/motorro/commonstatemachine/examples/books/book/state/DeleteConfirmationStateTest.kt index 10538fd..01a4be0 100644 --- a/examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/state/DeleteConfirmationStateTest.kt +++ b/examples/books/book/src/test/kotlin/com/motorro/commonstatemachine/examples/books/book/state/DeleteConfirmationStateTest.kt @@ -11,15 +11,15 @@ * 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 +package com.motorro.commonstatemachine.examples.books.book.state + +import com.motorro.commonstatemachine.examples.books.book.BOOK_ID +import com.motorro.commonstatemachine.examples.books.book.BOOK_TITLE +import com.motorro.commonstatemachine.examples.books.book.Book +import com.motorro.commonstatemachine.examples.books.book.api.BookGesture +import com.motorro.commonstatemachine.examples.books.book.api.BookInput +import com.motorro.commonstatemachine.examples.books.book.api.BookUiState +import com.motorro.commonstatemachine.examples.books.book.data.BookDataState import io.mockk.every import io.mockk.verify import org.junit.Test 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/commonstatemachine/examples/books/book/state/DeletingStateTest.kt similarity index 78% rename from examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/state/DeletingStateTest.kt rename to examples/books/book/src/test/kotlin/com/motorro/commonstatemachine/examples/books/book/state/DeletingStateTest.kt index cf550d9..dc80333 100644 --- a/examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/state/DeletingStateTest.kt +++ b/examples/books/book/src/test/kotlin/com/motorro/commonstatemachine/examples/books/book/state/DeletingStateTest.kt @@ -11,15 +11,15 @@ * limitations under the License. */ -package com.motorro.statemachine.books.book.state +package com.motorro.commonstatemachine.examples.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 com.motorro.commonstatemachine.examples.books.book.BOOK_ID +import com.motorro.commonstatemachine.examples.books.book.BOOK_TITLE +import com.motorro.commonstatemachine.examples.books.book.Book +import com.motorro.commonstatemachine.examples.books.book.api.BookInput +import com.motorro.commonstatemachine.examples.books.book.api.BookUiState +import com.motorro.commonstatemachine.examples.books.book.data.BookDataState +import com.motorro.commonstatemachine.examples.books.domain.repository.BookRepository import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every 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/commonstatemachine/examples/books/book/state/ErrorStateTest.kt similarity index 78% rename from examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/state/ErrorStateTest.kt rename to examples/books/book/src/test/kotlin/com/motorro/commonstatemachine/examples/books/book/state/ErrorStateTest.kt index f016776..ee11c15 100644 --- a/examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/state/ErrorStateTest.kt +++ b/examples/books/book/src/test/kotlin/com/motorro/commonstatemachine/examples/books/book/state/ErrorStateTest.kt @@ -11,15 +11,15 @@ * limitations under the License. */ -package com.motorro.statemachine.books.book.state +package com.motorro.commonstatemachine.examples.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.commonstatemachine.examples.books.book.BOOK_ID +import com.motorro.commonstatemachine.examples.books.book.BOOK_TITLE +import com.motorro.commonstatemachine.examples.books.book.Book +import com.motorro.commonstatemachine.examples.books.book.api.BookGesture +import com.motorro.commonstatemachine.examples.books.book.api.BookInput +import com.motorro.commonstatemachine.examples.books.book.api.BookUiState +import com.motorro.commonstatemachine.examples.books.book.data.BookDataState import io.mockk.every import io.mockk.verify import org.junit.Test 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/commonstatemachine/examples/books/book/state/LoadingStateTest.kt similarity index 76% rename from examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/state/LoadingStateTest.kt rename to examples/books/book/src/test/kotlin/com/motorro/commonstatemachine/examples/books/book/state/LoadingStateTest.kt index 7db5a48..8ba687a 100644 --- a/examples/books/book/src/test/kotlin/com/motorro/statemachine/books/book/state/LoadingStateTest.kt +++ b/examples/books/book/src/test/kotlin/com/motorro/commonstatemachine/examples/books/book/state/LoadingStateTest.kt @@ -11,22 +11,22 @@ * limitations under the License. */ -package com.motorro.statemachine.books.book.state +package com.motorro.commonstatemachine.examples.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 com.motorro.commonstatemachine.examples.books.book.BOOK_ID +import com.motorro.commonstatemachine.examples.books.book.BOOK_TITLE +import com.motorro.commonstatemachine.examples.books.book.Book +import com.motorro.commonstatemachine.examples.books.book.api.BookGesture +import com.motorro.commonstatemachine.examples.books.book.api.BookInput +import com.motorro.commonstatemachine.examples.books.book.api.BookUiState +import com.motorro.commonstatemachine.examples.books.book.data.BookDataState +import com.motorro.commonstatemachine.examples.books.domain.repository.BookRepository import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk +import kotlinx.coroutines.suspendCancellableCoroutine import org.junit.Test -import kotlin.coroutines.suspendCoroutine import kotlin.test.assertEquals internal class LoadingStateTest : BaseStateTest() { @@ -75,7 +75,7 @@ internal class LoadingStateTest : BaseStateTest() { @Test fun terminatesOnBack() = test { coEvery { repository.getBook(any()) } coAnswers { - suspendCoroutine { + suspendCancellableCoroutine { // No-op } } diff --git a/examples/books/domain/build.gradle.kts b/examples/books/domain/build.gradle.kts index 5d5ce7d..a93cc3b 100644 --- a/examples/books/domain/build.gradle.kts +++ b/examples/books/domain/build.gradle.kts @@ -1,60 +1,8 @@ -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_21 - targetCompatibility = JavaVersion.VERSION_21 - isCoreLibraryDesugaringEnabled = true - } - kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - } - namespace = "com.motorro.statemachine.books.domain" + alias(libs.plugins.motorro.android.lib) } dependencies { - coreLibraryDesugaring(libs.desugaring) - api(libs.kotlin.immutable) - api(libs.kotlin.coroutines.core) } 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/commonstatemachine/examples/books/domain/entity/Book.kt similarity index 91% rename from examples/books/domain/src/main/kotlin/com/motorro/statemachine/books/domain/entity/Book.kt rename to examples/books/domain/src/main/kotlin/com/motorro/commonstatemachine/examples/books/domain/entity/Book.kt index 5a4da8b..ae2ce2a 100644 --- a/examples/books/domain/src/main/kotlin/com/motorro/statemachine/books/domain/entity/Book.kt +++ b/examples/books/domain/src/main/kotlin/com/motorro/commonstatemachine/examples/books/domain/entity/Book.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.books.domain.entity +package com.motorro.commonstatemachine.examples.books.domain.entity import kotlinx.collections.immutable.ImmutableList 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/commonstatemachine/examples/books/domain/entity/ListBook.kt similarity index 91% rename from examples/books/domain/src/main/kotlin/com/motorro/statemachine/books/domain/entity/ListBook.kt rename to examples/books/domain/src/main/kotlin/com/motorro/commonstatemachine/examples/books/domain/entity/ListBook.kt index 8b83a30..2917953 100644 --- a/examples/books/domain/src/main/kotlin/com/motorro/statemachine/books/domain/entity/ListBook.kt +++ b/examples/books/domain/src/main/kotlin/com/motorro/commonstatemachine/examples/books/domain/entity/ListBook.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.books.domain.entity +package com.motorro.commonstatemachine.examples.books.domain.entity import kotlinx.collections.immutable.ImmutableList 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/commonstatemachine/examples/books/domain/repository/BookRepository.kt similarity index 79% rename from examples/books/domain/src/main/kotlin/com/motorro/statemachine/books/domain/repository/BookRepository.kt rename to examples/books/domain/src/main/kotlin/com/motorro/commonstatemachine/examples/books/domain/repository/BookRepository.kt index 05c6d6a..6d266a4 100644 --- a/examples/books/domain/src/main/kotlin/com/motorro/statemachine/books/domain/repository/BookRepository.kt +++ b/examples/books/domain/src/main/kotlin/com/motorro/commonstatemachine/examples/books/domain/repository/BookRepository.kt @@ -11,10 +11,10 @@ * limitations under the License. */ -package com.motorro.statemachine.books.domain.repository +package com.motorro.commonstatemachine.examples.books.domain.repository -import com.motorro.statemachine.books.domain.entity.Book -import com.motorro.statemachine.books.domain.entity.ListBook +import com.motorro.commonstatemachine.examples.books.domain.entity.Book +import com.motorro.commonstatemachine.examples.books.domain.entity.ListBook import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.Flow diff --git a/examples/commoncore/build.gradle.kts b/examples/commoncore/build.gradle.kts index 5379a8c..8caf29d 100644 --- a/examples/commoncore/build.gradle.kts +++ b/examples/commoncore/build.gradle.kts @@ -10,62 +10,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { - alias(libs.plugins.kotlin.multiplatform) - alias(libs.plugins.android.kotlin.multiplatform.library) + alias(libs.plugins.motorro.cmp) } -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 = "com.motorro" -version = versionName - kotlin { - jvmToolchain(21) - - compilerOptions { - freeCompilerArgs.addAll(listOf( - "-Xexpect-actual-classes" - )) - } - - android { - namespace = "com.motorro.statemachine.commoncore" - compileSdk = androidCompileSdkVersion - minSdk = androidMinSdkVersion - - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - } - - js { - binaries.library() - useCommonJs() - browser { - testTask(Action { - useMocha { - timeout = "10s" - } - }) - } - } - sourceSets { commonMain.dependencies { - implementation(libs.kotlin.coroutines.core) - } - commonTest.dependencies { - implementation(libs.test.kotlin) - implementation(libs.test.kotlin.coroutines) - } - androidMain.dependencies { - implementation(libs.timber) + api(libs.kotlin.coroutines.core) + api(libs.compose.multiplatform.icons) + implementation(libs.napier) } } } \ No newline at end of file diff --git a/examples/commoncore/src/androidMain/AndroidManifest.xml b/examples/commoncore/src/androidMain/AndroidManifest.xml deleted file mode 100644 index a4b25cb..0000000 --- a/examples/commoncore/src/androidMain/AndroidManifest.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - \ No newline at end of file diff --git a/examples/commoncore/src/androidMain/kotlin/com/motorro/statemachine/commoncore/log/Logger.kt b/examples/commoncore/src/androidMain/kotlin/com/motorro/statemachine/commoncore/log/Logger.kt deleted file mode 100644 index eb94b30..0000000 --- a/examples/commoncore/src/androidMain/kotlin/com/motorro/statemachine/commoncore/log/Logger.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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.commoncore.log - -import timber.log.Timber - -actual object Logger { - actual fun d(message: String?, vararg args: Any?) { - Timber.d(message, *args) - } - - actual fun i(message: String?, vararg args: Any?) { - Timber.i(message, *args) - } - - actual fun w(message: String?, vararg args: Any?) { - Timber.w(message, *args) - } - - actual fun w(t: Throwable?, message: String?, vararg args: Any?) { - Timber.w(t, message, *args) - } - - actual fun w(t: Throwable?) { - Timber.w(t) - } - - actual fun e(message: String?, vararg args: Any?) { - Timber.e(message, *args) - } - - actual fun e(t: Throwable?, message: String?, vararg args: Any?) { - Timber.e(t, message, *args) - } - - actual fun e(t: Throwable?) { - Timber.e(t) - } -} \ No newline at end of file diff --git a/examples/commoncore/src/commonMain/kotlin/com/motorro/statemachine/commoncore/coroutines/DispatcherProvider.kt b/examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/coroutines/DispatcherProvider.kt similarity index 94% rename from examples/commoncore/src/commonMain/kotlin/com/motorro/statemachine/commoncore/coroutines/DispatcherProvider.kt rename to examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/coroutines/DispatcherProvider.kt index eaf0173..6ada95e 100644 --- a/examples/commoncore/src/commonMain/kotlin/com/motorro/statemachine/commoncore/coroutines/DispatcherProvider.kt +++ b/examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/coroutines/DispatcherProvider.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.commoncore.coroutines +package com.motorro.commonstatemachine.examples.commoncore.coroutines import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers diff --git a/examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/log/Logger.kt b/examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/log/Logger.kt new file mode 100644 index 0000000..2a02778 --- /dev/null +++ b/examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/log/Logger.kt @@ -0,0 +1,55 @@ +/* + * 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.commonstatemachine.examples.commoncore.log + +import io.github.aakira.napier.LogLevel +import io.github.aakira.napier.Napier + +object Logger { + + private fun napier(logLevel: LogLevel, throwable: Throwable?, message: String?, args: Array?) { + val toLog = buildString { + message?.let { append(it) } + throwable?.let { + if (isNotEmpty()) { + append(" ") + } + append(it) + } + args?.takeIf { it.isNotEmpty() }?.let { + if (isNotEmpty()) { + append(" ") + } + append(it.joinToString(prefix = "(", postfix = ")")) + } + } + Napier.log(logLevel, throwable =throwable, message = toLog) + } + + fun d(message: String?, vararg args: Any?) = napier(LogLevel.DEBUG, null, message, args) + + fun i(message: String?, vararg args: Any?) = napier(LogLevel.INFO, null, message, args) + + fun w(message: String?, vararg args: Any?) = napier(LogLevel.WARNING, null, message, args) + + fun w(t: Throwable?, message: String?, vararg args: Any?) = napier(LogLevel.WARNING, t, message, args) + + fun w(t: Throwable?) = napier(LogLevel.WARNING, t, null, null) + + fun e(message: String?, vararg args: Any?) = napier(LogLevel.ERROR, null, message, args) + + fun e(t: Throwable?, message: String?, vararg args: Any?) = napier(LogLevel.ERROR, t, message, args) + + fun e(t: Throwable?) = napier(LogLevel.ERROR, null, null, null) +} \ No newline at end of file diff --git a/examples/commoncore/src/commonMain/kotlin/com/motorro/statemachine/commoncore/resources/ResourceWrapper.kt b/examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/resources/ResourceWrapper.kt similarity index 91% rename from examples/commoncore/src/commonMain/kotlin/com/motorro/statemachine/commoncore/resources/ResourceWrapper.kt rename to examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/resources/ResourceWrapper.kt index 517a489..7da1f4c 100644 --- a/examples/commoncore/src/commonMain/kotlin/com/motorro/statemachine/commoncore/resources/ResourceWrapper.kt +++ b/examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/resources/ResourceWrapper.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.commoncore.resources +package com.motorro.commonstatemachine.examples.commoncore.resources /** * Wraps resource context diff --git a/examples/androidcore/src/main/java/com/motorro/statemachine/androidcore/compose/Error.kt b/examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/ui/Error.kt similarity index 93% rename from examples/androidcore/src/main/java/com/motorro/statemachine/androidcore/compose/Error.kt rename to examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/ui/Error.kt index 6bed1a0..993acd8 100644 --- a/examples/androidcore/src/main/java/com/motorro/statemachine/androidcore/compose/Error.kt +++ b/examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/ui/Error.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.androidcore.compose +package com.motorro.commonstatemachine.examples.commoncore.ui import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -32,8 +32,7 @@ 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 +import com.motorro.commonstatemachine.examples.commoncore.ui.theme.CommonStateMachineTheme @Composable fun Error(error: Throwable, onDismiss: () -> Unit, modifier: Modifier = Modifier) { @@ -77,7 +76,7 @@ fun Error(error: Throwable, onDismiss: () -> Unit, modifier: Modifier = Modifier fun ErrorPreview() { CommonStateMachineTheme { Error( - error = IOException("Network error"), + error = RuntimeException("Network error"), onDismiss = {} ) } diff --git a/examples/androidcore/src/main/java/com/motorro/statemachine/androidcore/compose/Loading.kt b/examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/ui/Loading.kt similarity index 95% rename from examples/androidcore/src/main/java/com/motorro/statemachine/androidcore/compose/Loading.kt rename to examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/ui/Loading.kt index 387f18b..6d5eec7 100644 --- a/examples/androidcore/src/main/java/com/motorro/statemachine/androidcore/compose/Loading.kt +++ b/examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/ui/Loading.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.androidcore.compose +package com.motorro.commonstatemachine.examples.commoncore.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/theme/Color.kt b/examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/ui/theme/Color.kt similarity index 78% rename from examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/theme/Color.kt rename to examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/ui/theme/Color.kt index 6df85d4..75302e7 100644 --- a/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/theme/Color.kt +++ b/examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/ui/theme/Color.kt @@ -1,4 +1,4 @@ -package com.motorro.statemachine.auth.appcore.ui.theme +package com.motorro.commonstatemachine.examples.commoncore.ui.theme import androidx.compose.ui.graphics.Color diff --git a/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/theme/Dimensions.kt b/examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/ui/theme/Dimensions.kt similarity index 88% rename from examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/theme/Dimensions.kt rename to examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/ui/theme/Dimensions.kt index 81aea4f..88b55c1 100644 --- a/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/theme/Dimensions.kt +++ b/examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/ui/theme/Dimensions.kt @@ -1,4 +1,4 @@ -package com.motorro.statemachine.auth.appcore.ui.theme +package com.motorro.commonstatemachine.examples.commoncore.ui.theme import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp diff --git a/examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/ui/theme/Theme.kt b/examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/ui/theme/Theme.kt new file mode 100644 index 0000000..ed0d476 --- /dev/null +++ b/examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/ui/theme/Theme.kt @@ -0,0 +1,37 @@ +package com.motorro.commonstatemachine.examples.commoncore.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Typography +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 +) + +@Composable +fun CommonStateMachineTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + val colorScheme = when { + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography(), + content = content + ) +} diff --git a/examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/ui/theme/Type.kt b/examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/ui/theme/Type.kt new file mode 100644 index 0000000..92f8a81 --- /dev/null +++ b/examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/ui/theme/Type.kt @@ -0,0 +1,18 @@ +package com.motorro.commonstatemachine.examples.commoncore.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) +) \ No newline at end of file diff --git a/examples/commoncore/src/commonMain/kotlin/com/motorro/statemachine/commoncore/log/Logger.kt b/examples/commoncore/src/commonMain/kotlin/com/motorro/statemachine/commoncore/log/Logger.kt deleted file mode 100644 index ba4383e..0000000 --- a/examples/commoncore/src/commonMain/kotlin/com/motorro/statemachine/commoncore/log/Logger.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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.commoncore.log - -expect object Logger { - fun d(message: String?, vararg args: Any?) - - fun i(message: String?, vararg args: Any?) - - fun w(message: String?, vararg args: Any?) - - fun w(t: Throwable?, message: String?, vararg args: Any?) - - fun w(t: Throwable?) - - fun e(message: String?, vararg args: Any?) - - fun e(t: Throwable?, message: String?, vararg args: Any?) - - fun e(t: Throwable?) -} \ No newline at end of file diff --git a/examples/commoncore/src/jsMain/kotlin/com/motorro/statemachine/commoncore/log/Logger.kt b/examples/commoncore/src/jsMain/kotlin/com/motorro/statemachine/commoncore/log/Logger.kt deleted file mode 100644 index 91b2413..0000000 --- a/examples/commoncore/src/jsMain/kotlin/com/motorro/statemachine/commoncore/log/Logger.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.commoncore.log - -actual object Logger { - actual fun d(message: String?, vararg args: Any?) { - console.log(message, *args) - } - - actual fun i(message: String?, vararg args: Any?) { - console.info(message, *args) - } - - actual fun w(message: String?, vararg args: Any?) { - console.warn(message, *args) - } - - actual fun w(t: Throwable?, message: String?, vararg args: Any?) { - console.warn(message, t, *args) - } - - actual fun w(t: Throwable?) { - console.warn(t) - } - - actual fun e(message: String?, vararg args: Any?) { - console.error(message, *args) - } - - actual fun e(t: Throwable?, message: String?, vararg args: Any?) { - console.error(message, t, *args) - } - - actual fun e(t: Throwable?) { - console.error(t) - } -} \ No newline at end of file diff --git a/examples/di/api/build.gradle.kts b/examples/di/api/build.gradle.kts index 27d9ecc..cf32b4f 100644 --- a/examples/di/api/build.gradle.kts +++ b/examples/di/api/build.gradle.kts @@ -1,5 +1,3 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - /* * Copyright 2026 Nikolai Kotchetkov. * Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,58 +12,11 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget */ 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_21 - targetCompatibility = JavaVersion.VERSION_21 - isCoreLibraryDesugaringEnabled = true - } - kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - } - buildFeatures { - compose = true - } - namespace = "com.motorro.statemachine.di.api" + alias(libs.plugins.motorro.android.lib.compose) } dependencies { api(project(":commonstatemachine")) api(project(":commonflow:commonflow-data")) api(project(":commonflow: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/src/main/kotlin/com/motorro/statemachine/di/api/AuthApi.kt b/examples/di/api/src/main/kotlin/com/motorro/commonstatemachine/examples/di/api/AuthApi.kt similarity index 91% rename from examples/di/api/src/main/kotlin/com/motorro/statemachine/di/api/AuthApi.kt rename to examples/di/api/src/main/kotlin/com/motorro/commonstatemachine/examples/di/api/AuthApi.kt index 0517567..3aee0c6 100644 --- a/examples/di/api/src/main/kotlin/com/motorro/statemachine/di/api/AuthApi.kt +++ b/examples/di/api/src/main/kotlin/com/motorro/commonstatemachine/examples/di/api/AuthApi.kt @@ -11,12 +11,12 @@ * limitations under the License. */ -package com.motorro.statemachine.di.api +package com.motorro.commonstatemachine.examples.di.api import androidx.compose.runtime.staticCompositionLocalOf +import com.motorro.commonstatemachine.examples.di.api.data.Session 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 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/commonstatemachine/examples/di/api/AuthFlowHost.kt similarity index 85% rename from examples/di/api/src/main/kotlin/com/motorro/statemachine/di/api/AuthFlowHost.kt rename to examples/di/api/src/main/kotlin/com/motorro/commonstatemachine/examples/di/api/AuthFlowHost.kt index cdb1b0c..fee1f50 100644 --- a/examples/di/api/src/main/kotlin/com/motorro/statemachine/di/api/AuthFlowHost.kt +++ b/examples/di/api/src/main/kotlin/com/motorro/commonstatemachine/examples/di/api/AuthFlowHost.kt @@ -11,10 +11,10 @@ * limitations under the License. */ -package com.motorro.statemachine.di.api +package com.motorro.commonstatemachine.examples.di.api +import com.motorro.commonstatemachine.examples.di.api.data.Session import com.motorro.commonstatemachine.flow.data.CommonFlowHost -import com.motorro.statemachine.di.api.data.Session /** * Handles authentication 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/commonstatemachine/examples/di/api/SessionManager.kt similarity index 87% rename from examples/di/api/src/main/kotlin/com/motorro/statemachine/di/api/SessionManager.kt rename to examples/di/api/src/main/kotlin/com/motorro/commonstatemachine/examples/di/api/SessionManager.kt index 4e74da4..eaebbac 100644 --- a/examples/di/api/src/main/kotlin/com/motorro/statemachine/di/api/SessionManager.kt +++ b/examples/di/api/src/main/kotlin/com/motorro/commonstatemachine/examples/di/api/SessionManager.kt @@ -11,9 +11,9 @@ * limitations under the License. */ -package com.motorro.statemachine.di.api +package com.motorro.commonstatemachine.examples.di.api -import com.motorro.statemachine.di.api.data.Session +import com.motorro.commonstatemachine.examples.di.api.data.Session import kotlinx.coroutines.flow.StateFlow /** 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/commonstatemachine/examples/di/api/data/Session.kt similarity index 93% rename from examples/di/api/src/main/kotlin/com/motorro/statemachine/di/api/data/Session.kt rename to examples/di/api/src/main/kotlin/com/motorro/commonstatemachine/examples/di/api/data/Session.kt index ae26484..53536b8 100644 --- a/examples/di/api/src/main/kotlin/com/motorro/statemachine/di/api/data/Session.kt +++ b/examples/di/api/src/main/kotlin/com/motorro/commonstatemachine/examples/di/api/data/Session.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.di.api.data +package com.motorro.commonstatemachine.examples.di.api.data /** * Session state diff --git a/examples/di/app/build.gradle.kts b/examples/di/app/build.gradle.kts index ebf3168..f36523e 100644 --- a/examples/di/app/build.gradle.kts +++ b/examples/di/app/build.gradle.kts @@ -11,79 +11,13 @@ * 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.motorro.android.app) 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_21 - targetCompatibility = JavaVersion.VERSION_21 - } - kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - } - buildFeatures { - compose = true - } - packaging { - resources { - excludes += "/META-INF/{AL2.0,LGPL2.1}" - } - } - namespace = "com.motorro.statemachine.di.app" - flavorDimensions += "auth" productFlavors { create("login") { @@ -101,40 +35,15 @@ 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/src/main/kotlin/com/motorro/statemachine/di/app/App.kt b/examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/App.kt similarity index 82% rename from examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/App.kt rename to examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/App.kt index ccc579b..b894336 100644 --- a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/App.kt +++ b/examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/App.kt @@ -11,11 +11,13 @@ * limitations under the License. */ -package com.motorro.statemachine.di.app +package com.motorro.commonstatemachine.examples.di.app import android.app.Application import dagger.hilt.android.HiltAndroidApp -import timber.log.Timber +import io.github.aakira.napier.DebugAntilog +import io.github.aakira.napier.Napier + @HiltAndroidApp class App : Application() { @@ -25,6 +27,6 @@ class App : Application() { } private fun setupLogger() { - Timber.plant(Timber.DebugTree()) + Napier.base(DebugAntilog()) } } \ 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/commonstatemachine/examples/di/app/MainActivity.kt similarity index 85% rename from examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/MainActivity.kt rename to examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/MainActivity.kt index ab05030..0392ee2 100644 --- a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/MainActivity.kt +++ b/examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/MainActivity.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.di.app +package com.motorro.commonstatemachine.examples.di.app import android.os.Bundle import androidx.activity.ComponentActivity @@ -21,10 +21,10 @@ 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 com.motorro.commonstatemachine.examples.commoncore.ui.theme.CommonStateMachineTheme +import com.motorro.commonstatemachine.examples.di.api.AuthUiApi +import com.motorro.commonstatemachine.examples.di.api.LocalAuth +import com.motorro.commonstatemachine.examples.di.app.ui.MainScreen import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject 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/commonstatemachine/examples/di/app/MainViewModel.kt similarity index 81% rename from examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/MainViewModel.kt rename to examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/MainViewModel.kt index 174bad7..fd12757 100644 --- a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/MainViewModel.kt +++ b/examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/MainViewModel.kt @@ -11,13 +11,13 @@ * limitations under the License. */ -package com.motorro.statemachine.di.app +package com.motorro.commonstatemachine.examples.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 com.motorro.commonstatemachine.examples.di.app.data.MainGesture +import com.motorro.commonstatemachine.examples.di.app.data.MainUiState +import com.motorro.commonstatemachine.examples.di.app.state.MainFactory import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.StateFlow import javax.inject.Inject 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/commonstatemachine/examples/di/app/data/MainGesture.kt similarity index 86% rename from examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/data/MainGesture.kt rename to examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/data/MainGesture.kt index 9b297cc..def0cf5 100644 --- a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/data/MainGesture.kt +++ b/examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/data/MainGesture.kt @@ -11,9 +11,9 @@ * limitations under the License. */ -package com.motorro.statemachine.di.app.data +package com.motorro.commonstatemachine.examples.di.app.data -import com.motorro.statemachine.di.api.AuthGesture +import com.motorro.commonstatemachine.examples.di.api.AuthGesture /** * Main flow gesture 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/commonstatemachine/examples/di/app/data/MainUiState.kt similarity index 81% rename from examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/data/MainUiState.kt rename to examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/data/MainUiState.kt index 763cf86..290e671 100644 --- a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/data/MainUiState.kt +++ b/examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/data/MainUiState.kt @@ -11,10 +11,10 @@ * limitations under the License. */ -package com.motorro.statemachine.di.app.data +package com.motorro.commonstatemachine.examples.di.app.data -import com.motorro.statemachine.di.api.AuthUiState -import com.motorro.statemachine.di.api.data.Session +import com.motorro.commonstatemachine.examples.di.api.AuthUiState +import com.motorro.commonstatemachine.examples.di.api.data.Session internal sealed class MainUiState { data object Loading : MainUiState() 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/commonstatemachine/examples/di/app/di/ApplicationModule.kt similarity index 81% rename from examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/di/ApplicationModule.kt rename to examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/di/ApplicationModule.kt index 91baaf3..5a61709 100644 --- a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/di/ApplicationModule.kt +++ b/examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/di/ApplicationModule.kt @@ -11,10 +11,10 @@ * limitations under the License. */ -package com.motorro.statemachine.di.app.di +package com.motorro.commonstatemachine.examples.di.app.di -import com.motorro.statemachine.di.api.SessionManager -import com.motorro.statemachine.di.app.session.SessionManagerImpl +import com.motorro.commonstatemachine.examples.di.api.SessionManager +import com.motorro.commonstatemachine.examples.di.app.session.SessionManagerImpl import dagger.Binds import dagger.Module import dagger.hilt.InstallIn 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/commonstatemachine/examples/di/app/di/MainModule.kt similarity index 87% rename from examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/di/MainModule.kt rename to examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/di/MainModule.kt index 09c2e3b..5161f41 100644 --- a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/di/MainModule.kt +++ b/examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/di/MainModule.kt @@ -11,9 +11,9 @@ * limitations under the License. */ -package com.motorro.statemachine.di.app.di +package com.motorro.commonstatemachine.examples.di.app.di -import com.motorro.statemachine.di.app.state.MainFactory +import com.motorro.commonstatemachine.examples.di.app.state.MainFactory import dagger.Binds import dagger.Module import dagger.hilt.InstallIn 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/commonstatemachine/examples/di/app/session/SessionManagerImpl.kt similarity index 84% rename from examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/session/SessionManagerImpl.kt rename to examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/session/SessionManagerImpl.kt index 114a267..68647f3 100644 --- a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/session/SessionManagerImpl.kt +++ b/examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/session/SessionManagerImpl.kt @@ -11,10 +11,10 @@ * limitations under the License. */ -package com.motorro.statemachine.di.app.session +package com.motorro.commonstatemachine.examples.di.app.session -import com.motorro.statemachine.di.api.SessionManager -import com.motorro.statemachine.di.api.data.Session +import com.motorro.commonstatemachine.examples.di.api.SessionManager +import com.motorro.commonstatemachine.examples.di.api.data.Session import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow 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/commonstatemachine/examples/di/app/state/AuthProxy.kt similarity index 77% rename from examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/state/AuthProxy.kt rename to examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/state/AuthProxy.kt index ebb65ad..6730bb7 100644 --- a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/state/AuthProxy.kt +++ b/examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/state/AuthProxy.kt @@ -11,16 +11,16 @@ * limitations under the License. */ -package com.motorro.statemachine.di.app.state +package com.motorro.commonstatemachine.examples.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 com.motorro.commonstatemachine.examples.commoncore.log.Logger +import com.motorro.commonstatemachine.examples.di.api.AuthDataApi +import com.motorro.commonstatemachine.examples.di.api.AuthFlowHost +import com.motorro.commonstatemachine.examples.di.api.AuthGesture +import com.motorro.commonstatemachine.examples.di.api.AuthUiState +import com.motorro.commonstatemachine.examples.di.app.data.MainGesture +import com.motorro.commonstatemachine.examples.di.app.data.MainUiState import javax.inject.Inject internal class AuthProxy( 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/commonstatemachine/examples/di/app/state/BaseMainState.kt similarity index 76% rename from examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/state/BaseMainState.kt rename to examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/state/BaseMainState.kt index b8e9329..ae28d2a 100644 --- a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/state/BaseMainState.kt +++ b/examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/state/BaseMainState.kt @@ -11,12 +11,12 @@ * limitations under the License. */ -package com.motorro.statemachine.di.app.state +package com.motorro.commonstatemachine.examples.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 +import com.motorro.commonstatemachine.examples.commoncore.log.Logger +import com.motorro.commonstatemachine.examples.di.app.data.MainGesture +import com.motorro.commonstatemachine.examples.di.app.data.MainUiState internal abstract class BaseMainState(context: MainContext) : CoroutineState(), MainContext by context { override fun doProcess(gesture: MainGesture) { 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/commonstatemachine/examples/di/app/state/Content.kt similarity index 83% rename from examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/state/Content.kt rename to examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/state/Content.kt index 8a090bc..4abbeae 100644 --- a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/state/Content.kt +++ b/examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/state/Content.kt @@ -11,13 +11,13 @@ * limitations under the License. */ -package com.motorro.statemachine.di.app.state +package com.motorro.commonstatemachine.examples.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 com.motorro.commonstatemachine.examples.commoncore.log.Logger +import com.motorro.commonstatemachine.examples.di.api.SessionManager +import com.motorro.commonstatemachine.examples.di.api.data.Session +import com.motorro.commonstatemachine.examples.di.app.data.MainGesture +import com.motorro.commonstatemachine.examples.di.app.data.MainUiState import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import javax.inject.Inject 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/commonstatemachine/examples/di/app/state/MainContext.kt similarity index 91% rename from examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/state/MainContext.kt rename to examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/state/MainContext.kt index 34d5742..b213c06 100644 --- a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/state/MainContext.kt +++ b/examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/state/MainContext.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.di.app.state +package com.motorro.commonstatemachine.examples.di.app.state /** * Main flow context 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/commonstatemachine/examples/di/app/state/MainFactory.kt similarity index 88% rename from examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/state/MainFactory.kt rename to examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/state/MainFactory.kt index a752123..70f0deb 100644 --- a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/state/MainFactory.kt +++ b/examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/state/MainFactory.kt @@ -11,12 +11,12 @@ * limitations under the License. */ -package com.motorro.statemachine.di.app.state +package com.motorro.commonstatemachine.examples.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 com.motorro.commonstatemachine.examples.di.api.data.Session +import com.motorro.commonstatemachine.examples.di.app.data.MainGesture +import com.motorro.commonstatemachine.examples.di.app.data.MainUiState import javax.inject.Inject import javax.inject.Provider 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/commonstatemachine/examples/di/app/state/UpdatingSession.kt similarity index 79% rename from examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/state/UpdatingSession.kt rename to examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/state/UpdatingSession.kt index bce2095..a8ca521 100644 --- a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/state/UpdatingSession.kt +++ b/examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/state/UpdatingSession.kt @@ -11,12 +11,12 @@ * limitations under the License. */ -package com.motorro.statemachine.di.app.state +package com.motorro.commonstatemachine.examples.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 com.motorro.commonstatemachine.examples.commoncore.log.Logger +import com.motorro.commonstatemachine.examples.di.api.SessionManager +import com.motorro.commonstatemachine.examples.di.api.data.Session +import com.motorro.commonstatemachine.examples.di.app.data.MainUiState import kotlinx.coroutines.launch import javax.inject.Inject 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/commonstatemachine/examples/di/app/ui/MainScreen.kt similarity index 88% rename from examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/ui/MainScreen.kt rename to examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/ui/MainScreen.kt index 6cf88a7..cc429ce 100644 --- a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/ui/MainScreen.kt +++ b/examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/ui/MainScreen.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.di.app.ui +package com.motorro.commonstatemachine.examples.di.app.ui import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.fillMaxSize @@ -30,11 +30,11 @@ 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 +import com.motorro.commonstatemachine.examples.commoncore.ui.Loading +import com.motorro.commonstatemachine.examples.di.api.LocalAuth +import com.motorro.commonstatemachine.examples.di.app.R +import com.motorro.commonstatemachine.examples.di.app.data.MainGesture +import com.motorro.commonstatemachine.examples.di.app.data.MainUiState @Composable @OptIn(ExperimentalMaterial3Api::class) 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/commonstatemachine/examples/di/app/ui/MainScreenView.kt similarity index 90% rename from examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/ui/MainScreenView.kt rename to examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/ui/MainScreenView.kt index 5dc5ae5..04673a3 100644 --- a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/app/ui/MainScreenView.kt +++ b/examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/ui/MainScreenView.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.di.app.ui +package com.motorro.commonstatemachine.examples.di.app.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -30,10 +30,10 @@ 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 +import com.motorro.commonstatemachine.examples.di.api.data.Session +import com.motorro.commonstatemachine.examples.di.app.R +import com.motorro.commonstatemachine.examples.di.app.data.MainGesture +import com.motorro.commonstatemachine.examples.di.app.data.MainUiState @Composable internal fun MainScreenView(state: MainUiState.Content, onGesture: (MainGesture) -> Unit, modifier: Modifier = Modifier) { 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/commonstatemachine/examples/di/app/mock.kt similarity index 85% rename from examples/di/app/src/test/kotlin/com/motorro/statemachine/di/app/mock.kt rename to examples/di/app/src/test/kotlin/com/motorro/commonstatemachine/examples/di/app/mock.kt index 8c145d0..9e082e5 100644 --- a/examples/di/app/src/test/kotlin/com/motorro/statemachine/di/app/mock.kt +++ b/examples/di/app/src/test/kotlin/com/motorro/commonstatemachine/examples/di/app/mock.kt @@ -11,9 +11,9 @@ * limitations under the License. */ -package com.motorro.statemachine.di.app +package com.motorro.commonstatemachine.examples.di.app -import com.motorro.statemachine.di.api.data.Session +import com.motorro.commonstatemachine.examples.di.api.data.Session internal val ACTIVE_SESSION = Session.Active( accessToken = "access", 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/commonstatemachine/examples/di/app/state/BaseStateTest.kt similarity index 89% rename from examples/di/app/src/test/kotlin/com/motorro/statemachine/di/app/state/BaseStateTest.kt rename to examples/di/app/src/test/kotlin/com/motorro/commonstatemachine/examples/di/app/state/BaseStateTest.kt index d8804ad..ee58480 100644 --- a/examples/di/app/src/test/kotlin/com/motorro/statemachine/di/app/state/BaseStateTest.kt +++ b/examples/di/app/src/test/kotlin/com/motorro/commonstatemachine/examples/di/app/state/BaseStateTest.kt @@ -11,12 +11,12 @@ * limitations under the License. */ -package com.motorro.statemachine.di.app.state +package com.motorro.commonstatemachine.examples.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 com.motorro.commonstatemachine.examples.di.api.AuthFlowHost +import com.motorro.commonstatemachine.examples.di.app.data.MainGesture +import com.motorro.commonstatemachine.examples.di.app.data.MainUiState import io.mockk.Runs import io.mockk.every import io.mockk.just 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/commonstatemachine/examples/di/app/state/ContentTest.kt similarity index 87% rename from examples/di/app/src/test/kotlin/com/motorro/statemachine/di/app/state/ContentTest.kt rename to examples/di/app/src/test/kotlin/com/motorro/commonstatemachine/examples/di/app/state/ContentTest.kt index 7b4a617..5899e95 100644 --- a/examples/di/app/src/test/kotlin/com/motorro/statemachine/di/app/state/ContentTest.kt +++ b/examples/di/app/src/test/kotlin/com/motorro/commonstatemachine/examples/di/app/state/ContentTest.kt @@ -11,13 +11,13 @@ * limitations under the License. */ -package com.motorro.statemachine.di.app.state +package com.motorro.commonstatemachine.examples.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 com.motorro.commonstatemachine.examples.di.api.SessionManager +import com.motorro.commonstatemachine.examples.di.api.data.Session +import com.motorro.commonstatemachine.examples.di.app.ACTIVE_SESSION +import com.motorro.commonstatemachine.examples.di.app.data.MainGesture +import com.motorro.commonstatemachine.examples.di.app.data.MainUiState import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every diff --git a/examples/di/login/build.gradle.kts b/examples/di/login/build.gradle.kts index 4b7ed4b..dab2d46 100644 --- a/examples/di/login/build.gradle.kts +++ b/examples/di/login/build.gradle.kts @@ -1,5 +1,3 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - /* * Copyright 2026 Nikolai Kotchetkov. * Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,72 +12,18 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget */ plugins { - alias(libs.plugins.android.lib) - alias(libs.plugins.compose) + alias(libs.plugins.motorro.android.lib.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_21 - targetCompatibility = JavaVersion.VERSION_21 - isCoreLibraryDesugaringEnabled = true - } - kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - } - 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/src/main/kotlin/com/motorro/statemachine/di/login/LoginConstants.kt b/examples/di/login/src/main/kotlin/com/motorro/commonstatemachine/examples/di/login/LoginConstants.kt similarity index 93% rename from examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/LoginConstants.kt rename to examples/di/login/src/main/kotlin/com/motorro/commonstatemachine/examples/di/login/LoginConstants.kt index 3fbdfe0..83e9814 100644 --- a/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/LoginConstants.kt +++ b/examples/di/login/src/main/kotlin/com/motorro/commonstatemachine/examples/di/login/LoginConstants.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.di.login +package com.motorro.commonstatemachine.examples.di.login /** * Mock data 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/commonstatemachine/examples/di/login/LoginDataApi.kt similarity index 68% rename from examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/LoginDataApi.kt rename to examples/di/login/src/main/kotlin/com/motorro/commonstatemachine/examples/di/login/LoginDataApi.kt index dc7809f..3585e48 100644 --- a/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/LoginDataApi.kt +++ b/examples/di/login/src/main/kotlin/com/motorro/commonstatemachine/examples/di/login/LoginDataApi.kt @@ -11,20 +11,20 @@ * limitations under the License. */ -package com.motorro.statemachine.di.login +package com.motorro.commonstatemachine.examples.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 com.motorro.commonstatemachine.examples.di.api.AuthDataApi +import com.motorro.commonstatemachine.examples.di.api.AuthFlowHost +import com.motorro.commonstatemachine.examples.di.api.AuthGesture +import com.motorro.commonstatemachine.examples.di.api.AuthUiApi +import com.motorro.commonstatemachine.examples.di.api.AuthUiState +import com.motorro.commonstatemachine.examples.di.login.data.LoginDataState +import com.motorro.commonstatemachine.examples.di.login.data.LoginGesture +import com.motorro.commonstatemachine.examples.di.login.data.LoginUiState +import com.motorro.commonstatemachine.examples.di.login.state.LoginFactory +import com.motorro.commonstatemachine.examples.di.login.ui.LoginScreen import jakarta.inject.Inject /** 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/commonstatemachine/examples/di/login/data/LoginDataState.kt similarity index 91% rename from examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/data/LoginDataState.kt rename to examples/di/login/src/main/kotlin/com/motorro/commonstatemachine/examples/di/login/data/LoginDataState.kt index 1943f8e..d6f5010 100644 --- a/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/data/LoginDataState.kt +++ b/examples/di/login/src/main/kotlin/com/motorro/commonstatemachine/examples/di/login/data/LoginDataState.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.di.login.data +package com.motorro.commonstatemachine.examples.di.login.data /** * Data state 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/commonstatemachine/examples/di/login/data/LoginGesture.kt similarity index 87% rename from examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/data/LoginGesture.kt rename to examples/di/login/src/main/kotlin/com/motorro/commonstatemachine/examples/di/login/data/LoginGesture.kt index 24d3a2a..51efa22 100644 --- a/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/data/LoginGesture.kt +++ b/examples/di/login/src/main/kotlin/com/motorro/commonstatemachine/examples/di/login/data/LoginGesture.kt @@ -11,9 +11,9 @@ * limitations under the License. */ -package com.motorro.statemachine.di.login.data +package com.motorro.commonstatemachine.examples.di.login.data -import com.motorro.statemachine.di.api.AuthGesture +import com.motorro.commonstatemachine.examples.di.api.AuthGesture internal sealed class LoginGesture : AuthGesture { data object Back : 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/commonstatemachine/examples/di/login/data/LoginUiState.kt similarity index 86% rename from examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/data/LoginUiState.kt rename to examples/di/login/src/main/kotlin/com/motorro/commonstatemachine/examples/di/login/data/LoginUiState.kt index 7857d44..50a0427 100644 --- a/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/data/LoginUiState.kt +++ b/examples/di/login/src/main/kotlin/com/motorro/commonstatemachine/examples/di/login/data/LoginUiState.kt @@ -11,9 +11,9 @@ * limitations under the License. */ -package com.motorro.statemachine.di.login.data +package com.motorro.commonstatemachine.examples.di.login.data -import com.motorro.statemachine.di.api.AuthUiState +import com.motorro.commonstatemachine.examples.di.api.AuthUiState internal sealed class LoginUiState: AuthUiState { data object Loading : LoginUiState() 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/commonstatemachine/examples/di/login/di/LoginFlowModule.kt similarity index 77% rename from examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/di/LoginFlowModule.kt rename to examples/di/login/src/main/kotlin/com/motorro/commonstatemachine/examples/di/login/di/LoginFlowModule.kt index 18395f2..8153be7 100644 --- a/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/di/LoginFlowModule.kt +++ b/examples/di/login/src/main/kotlin/com/motorro/commonstatemachine/examples/di/login/di/LoginFlowModule.kt @@ -11,12 +11,12 @@ * limitations under the License. */ -package com.motorro.statemachine.di.login.di +package com.motorro.commonstatemachine.examples.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 com.motorro.commonstatemachine.examples.di.api.AuthDataApi +import com.motorro.commonstatemachine.examples.di.api.AuthUiApi +import com.motorro.commonstatemachine.examples.di.login.LoginDataApi +import com.motorro.commonstatemachine.examples.di.login.LoginUiApi import dagger.Binds import dagger.Module import dagger.hilt.InstallIn 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/commonstatemachine/examples/di/login/state/BaseLoginState.kt similarity index 76% rename from examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/state/BaseLoginState.kt rename to examples/di/login/src/main/kotlin/com/motorro/commonstatemachine/examples/di/login/state/BaseLoginState.kt index 9736270..2ad8f77 100644 --- a/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/state/BaseLoginState.kt +++ b/examples/di/login/src/main/kotlin/com/motorro/commonstatemachine/examples/di/login/state/BaseLoginState.kt @@ -11,13 +11,13 @@ * limitations under the License. */ -package com.motorro.statemachine.di.login.state +package com.motorro.commonstatemachine.examples.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 +import com.motorro.commonstatemachine.examples.commoncore.log.Logger +import com.motorro.commonstatemachine.examples.di.api.AuthGesture +import com.motorro.commonstatemachine.examples.di.api.AuthUiState +import com.motorro.commonstatemachine.examples.di.login.data.LoginGesture internal abstract class BaseLoginState(context: LoginContext) : CoroutineState(), LoginContext by context { 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/commonstatemachine/examples/di/login/state/Error.kt similarity index 76% rename from examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/state/Error.kt rename to examples/di/login/src/main/kotlin/com/motorro/commonstatemachine/examples/di/login/state/Error.kt index f5ae0a2..64e600f 100644 --- a/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/state/Error.kt +++ b/examples/di/login/src/main/kotlin/com/motorro/commonstatemachine/examples/di/login/state/Error.kt @@ -11,12 +11,12 @@ * limitations under the License. */ -package com.motorro.statemachine.di.login.state +package com.motorro.commonstatemachine.examples.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 com.motorro.commonstatemachine.examples.commoncore.log.Logger +import com.motorro.commonstatemachine.examples.di.login.data.LoginDataState +import com.motorro.commonstatemachine.examples.di.login.data.LoginGesture +import com.motorro.commonstatemachine.examples.di.login.data.LoginUiState /** * Displays error 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/commonstatemachine/examples/di/login/state/FormState.kt similarity index 84% rename from examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/state/FormState.kt rename to examples/di/login/src/main/kotlin/com/motorro/commonstatemachine/examples/di/login/state/FormState.kt index 9ca534e..7bef7bd 100644 --- a/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/state/FormState.kt +++ b/examples/di/login/src/main/kotlin/com/motorro/commonstatemachine/examples/di/login/state/FormState.kt @@ -11,12 +11,12 @@ * limitations under the License. */ -package com.motorro.statemachine.di.login.state +package com.motorro.commonstatemachine.examples.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 com.motorro.commonstatemachine.examples.commoncore.log.Logger +import com.motorro.commonstatemachine.examples.di.login.data.LoginDataState +import com.motorro.commonstatemachine.examples.di.login.data.LoginGesture +import com.motorro.commonstatemachine.examples.di.login.data.LoginUiState import kotlin.properties.Delegates internal class FormState(context: LoginContext, data: LoginDataState) : BaseLoginState(context) { 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/commonstatemachine/examples/di/login/state/LoggingIn.kt similarity index 79% rename from examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/state/LoggingIn.kt rename to examples/di/login/src/main/kotlin/com/motorro/commonstatemachine/examples/di/login/state/LoggingIn.kt index 5025f2c..a8a2c0c 100644 --- a/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/state/LoggingIn.kt +++ b/examples/di/login/src/main/kotlin/com/motorro/commonstatemachine/examples/di/login/state/LoggingIn.kt @@ -11,14 +11,14 @@ * limitations under the License. */ -package com.motorro.statemachine.di.login.state +package com.motorro.commonstatemachine.examples.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 com.motorro.commonstatemachine.examples.commoncore.log.Logger +import com.motorro.commonstatemachine.examples.di.api.data.Session +import com.motorro.commonstatemachine.examples.di.login.LoginConstants +import com.motorro.commonstatemachine.examples.di.login.data.LoginDataState +import com.motorro.commonstatemachine.examples.di.login.data.LoginGesture +import com.motorro.commonstatemachine.examples.di.login.data.LoginUiState import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlin.time.Duration.Companion.seconds 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/commonstatemachine/examples/di/login/state/LoginContext.kt similarity index 84% rename from examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/state/LoginContext.kt rename to examples/di/login/src/main/kotlin/com/motorro/commonstatemachine/examples/di/login/state/LoginContext.kt index 253887e..0d342d8 100644 --- a/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/state/LoginContext.kt +++ b/examples/di/login/src/main/kotlin/com/motorro/commonstatemachine/examples/di/login/state/LoginContext.kt @@ -11,9 +11,9 @@ * limitations under the License. */ -package com.motorro.statemachine.di.login.state +package com.motorro.commonstatemachine.examples.di.login.state -import com.motorro.statemachine.di.api.AuthFlowHost +import com.motorro.commonstatemachine.examples.di.api.AuthFlowHost /** * Common inter-state context 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/commonstatemachine/examples/di/login/state/LoginFactory.kt similarity index 85% rename from examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/state/LoginFactory.kt rename to examples/di/login/src/main/kotlin/com/motorro/commonstatemachine/examples/di/login/state/LoginFactory.kt index 46bc515..a0df7c0 100644 --- a/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/state/LoginFactory.kt +++ b/examples/di/login/src/main/kotlin/com/motorro/commonstatemachine/examples/di/login/state/LoginFactory.kt @@ -11,14 +11,14 @@ * limitations under the License. */ -package com.motorro.statemachine.di.login.state +package com.motorro.commonstatemachine.examples.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 com.motorro.commonstatemachine.examples.di.api.AuthFlowHost +import com.motorro.commonstatemachine.examples.di.api.AuthGesture +import com.motorro.commonstatemachine.examples.di.api.AuthUiState +import com.motorro.commonstatemachine.examples.di.api.data.Session +import com.motorro.commonstatemachine.examples.di.login.data.LoginDataState import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject 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/commonstatemachine/examples/di/login/ui/LoginFormView.kt similarity index 89% rename from examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/ui/LoginFormView.kt rename to examples/di/login/src/main/kotlin/com/motorro/commonstatemachine/examples/di/login/ui/LoginFormView.kt index 37c9634..077d28e 100644 --- a/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/ui/LoginFormView.kt +++ b/examples/di/login/src/main/kotlin/com/motorro/commonstatemachine/examples/di/login/ui/LoginFormView.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.di.login.ui +package com.motorro.commonstatemachine.examples.di.login.ui import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -26,9 +26,9 @@ 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 +import com.motorro.commonstatemachine.examples.commoncore.ui.theme.CommonStateMachineTheme +import com.motorro.commonstatemachine.examples.di.login.data.LoginGesture +import com.motorro.commonstatemachine.examples.di.login.data.LoginUiState @Composable internal fun LoginFormView(state: LoginUiState.Form, onGesture: (LoginGesture) -> Unit, modifier: Modifier = Modifier) { 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/commonstatemachine/examples/di/login/ui/LoginScreen.kt similarity index 76% rename from examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/ui/LoginScreen.kt rename to examples/di/login/src/main/kotlin/com/motorro/commonstatemachine/examples/di/login/ui/LoginScreen.kt index 95734ac..56ea3e5 100644 --- a/examples/di/login/src/main/kotlin/com/motorro/statemachine/di/login/ui/LoginScreen.kt +++ b/examples/di/login/src/main/kotlin/com/motorro/commonstatemachine/examples/di/login/ui/LoginScreen.kt @@ -11,14 +11,14 @@ * limitations under the License. */ -package com.motorro.statemachine.di.login.ui +package com.motorro.commonstatemachine.examples.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 +import com.motorro.commonstatemachine.examples.commoncore.ui.Error +import com.motorro.commonstatemachine.examples.commoncore.ui.Loading +import com.motorro.commonstatemachine.examples.di.login.data.LoginGesture +import com.motorro.commonstatemachine.examples.di.login.data.LoginUiState @Composable internal fun LoginScreen(state: LoginUiState, onGesture: (LoginGesture) -> Unit, modifier: Modifier = Modifier) { 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/commonstatemachine/examples/di/login/state/BaseStateTest.kt similarity index 90% rename from examples/di/login/src/test/kotlin/com/motorro/statemachine/di/login/state/BaseStateTest.kt rename to examples/di/login/src/test/kotlin/com/motorro/commonstatemachine/examples/di/login/state/BaseStateTest.kt index a5cc5e7..36caa32 100644 --- a/examples/di/login/src/test/kotlin/com/motorro/statemachine/di/login/state/BaseStateTest.kt +++ b/examples/di/login/src/test/kotlin/com/motorro/commonstatemachine/examples/di/login/state/BaseStateTest.kt @@ -11,12 +11,12 @@ * limitations under the License. */ -package com.motorro.statemachine.di.login.state +package com.motorro.commonstatemachine.examples.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 com.motorro.commonstatemachine.examples.di.api.AuthFlowHost +import com.motorro.commonstatemachine.examples.di.api.AuthGesture +import com.motorro.commonstatemachine.examples.di.api.AuthUiState import io.mockk.Runs import io.mockk.every import io.mockk.just 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/commonstatemachine/examples/di/login/state/FormStateTest.kt similarity index 93% rename from examples/di/login/src/test/kotlin/com/motorro/statemachine/di/login/state/FormStateTest.kt rename to examples/di/login/src/test/kotlin/com/motorro/commonstatemachine/examples/di/login/state/FormStateTest.kt index 8f4cbc7..65dc624 100644 --- a/examples/di/login/src/test/kotlin/com/motorro/statemachine/di/login/state/FormStateTest.kt +++ b/examples/di/login/src/test/kotlin/com/motorro/commonstatemachine/examples/di/login/state/FormStateTest.kt @@ -11,11 +11,11 @@ * limitations under the License. */ -package com.motorro.statemachine.di.login.state +package com.motorro.commonstatemachine.examples.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 com.motorro.commonstatemachine.examples.di.login.data.LoginDataState +import com.motorro.commonstatemachine.examples.di.login.data.LoginGesture +import com.motorro.commonstatemachine.examples.di.login.data.LoginUiState import io.mockk.Ordering import io.mockk.coVerify import io.mockk.every diff --git a/examples/di/social/build.gradle.kts b/examples/di/social/build.gradle.kts index efd51ee..66a87ef 100644 --- a/examples/di/social/build.gradle.kts +++ b/examples/di/social/build.gradle.kts @@ -1,5 +1,3 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - /* * Copyright 2026 Nikolai Kotchetkov. * Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,73 +12,20 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget */ plugins { - alias(libs.plugins.android.lib) - alias(libs.plugins.compose) + alias(libs.plugins.motorro.android.lib.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_21 - targetCompatibility = JavaVersion.VERSION_21 - isCoreLibraryDesugaringEnabled = true - } - kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - } - 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/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/SocialConstants.kt b/examples/di/social/src/main/kotlin/main/kotlin/com/motorro/commonstatemachine/examples/di/social/SocialConstants.kt similarity index 91% rename from examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/SocialConstants.kt rename to examples/di/social/src/main/kotlin/main/kotlin/com/motorro/commonstatemachine/examples/di/social/SocialConstants.kt index 8c4ad86..6706a69 100644 --- 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/commonstatemachine/examples/di/social/SocialConstants.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package main.kotlin.com.motorro.statemachine.di.social +package main.kotlin.com.motorro.commonstatemachine.examples.di.social /** * Mock data 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/commonstatemachine/examples/di/social/SocialDataApi.kt similarity index 68% rename from examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/SocialDataApi.kt rename to examples/di/social/src/main/kotlin/main/kotlin/com/motorro/commonstatemachine/examples/di/social/SocialDataApi.kt index 5f4b003..bf88504 100644 --- 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/commonstatemachine/examples/di/social/SocialDataApi.kt @@ -11,20 +11,20 @@ * limitations under the License. */ -package main.kotlin.com.motorro.statemachine.di.social +package main.kotlin.com.motorro.commonstatemachine.examples.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 com.motorro.commonstatemachine.examples.di.api.AuthDataApi +import com.motorro.commonstatemachine.examples.di.api.AuthFlowHost +import com.motorro.commonstatemachine.examples.di.api.AuthGesture +import com.motorro.commonstatemachine.examples.di.api.AuthUiApi +import com.motorro.commonstatemachine.examples.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 +import main.kotlin.com.motorro.commonstatemachine.examples.di.social.data.SocialGesture +import main.kotlin.com.motorro.commonstatemachine.examples.di.social.data.SocialUiState +import main.kotlin.com.motorro.commonstatemachine.examples.di.social.state.SocialFactory +import main.kotlin.com.motorro.commonstatemachine.examples.di.social.ui.SocialScreen /** * Auth data API implementation for Social flow 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/commonstatemachine/examples/di/social/data/SocialGesture.kt similarity index 83% rename from examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/data/SocialGesture.kt rename to examples/di/social/src/main/kotlin/main/kotlin/com/motorro/commonstatemachine/examples/di/social/data/SocialGesture.kt index 89bfcb0..a29aa93 100644 --- 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/commonstatemachine/examples/di/social/data/SocialGesture.kt @@ -11,9 +11,9 @@ * limitations under the License. */ -package main.kotlin.com.motorro.statemachine.di.social.data +package main.kotlin.com.motorro.commonstatemachine.examples.di.social.data -import com.motorro.statemachine.di.api.AuthGesture +import com.motorro.commonstatemachine.examples.di.api.AuthGesture internal sealed class SocialGesture : AuthGesture { data object Back : 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/commonstatemachine/examples/di/social/data/SocialUiState.kt similarity index 83% rename from examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/data/SocialUiState.kt rename to examples/di/social/src/main/kotlin/main/kotlin/com/motorro/commonstatemachine/examples/di/social/data/SocialUiState.kt index cdc63d7..403e6e5 100644 --- 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/commonstatemachine/examples/di/social/data/SocialUiState.kt @@ -11,9 +11,9 @@ * limitations under the License. */ -package main.kotlin.com.motorro.statemachine.di.social.data +package main.kotlin.com.motorro.commonstatemachine.examples.di.social.data -import com.motorro.statemachine.di.api.AuthUiState +import com.motorro.commonstatemachine.examples.di.api.AuthUiState internal sealed class SocialUiState: AuthUiState { data object Loading : SocialUiState() 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/commonstatemachine/examples/di/social/di/SocialFlowModule.kt similarity index 75% rename from examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/di/SocialFlowModule.kt rename to examples/di/social/src/main/kotlin/main/kotlin/com/motorro/commonstatemachine/examples/di/social/di/SocialFlowModule.kt index 8c8c582..3d960d7 100644 --- 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/commonstatemachine/examples/di/social/di/SocialFlowModule.kt @@ -11,17 +11,17 @@ * limitations under the License. */ -package main.kotlin.com.motorro.statemachine.di.social.di +package main.kotlin.com.motorro.commonstatemachine.examples.di.social.di -import com.motorro.statemachine.di.api.AuthDataApi -import com.motorro.statemachine.di.api.AuthUiApi +import com.motorro.commonstatemachine.examples.di.api.AuthDataApi +import com.motorro.commonstatemachine.examples.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 +import main.kotlin.com.motorro.commonstatemachine.examples.di.social.SocialDataApi +import main.kotlin.com.motorro.commonstatemachine.examples.di.social.SocialUiApi @Module @InstallIn(ViewModelComponent::class) 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/commonstatemachine/examples/di/social/state/BaseSocialState.kt similarity index 75% rename from examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/state/BaseSocialState.kt rename to examples/di/social/src/main/kotlin/main/kotlin/com/motorro/commonstatemachine/examples/di/social/state/BaseSocialState.kt index 4db3fb2..0ac6dbb 100644 --- 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/commonstatemachine/examples/di/social/state/BaseSocialState.kt @@ -11,13 +11,13 @@ * limitations under the License. */ -package main.kotlin.com.motorro.statemachine.di.social.state +package main.kotlin.com.motorro.commonstatemachine.examples.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 +import com.motorro.commonstatemachine.examples.commoncore.log.Logger +import com.motorro.commonstatemachine.examples.di.api.AuthGesture +import com.motorro.commonstatemachine.examples.di.api.AuthUiState +import main.kotlin.com.motorro.commonstatemachine.examples.di.social.data.SocialGesture internal abstract class BaseSocialState(context: SocialContext) : CoroutineState(), SocialContext by context { 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/commonstatemachine/examples/di/social/state/FormState.kt similarity index 78% rename from examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/state/FormState.kt rename to examples/di/social/src/main/kotlin/main/kotlin/com/motorro/commonstatemachine/examples/di/social/state/FormState.kt index 3427c6f..c987639 100644 --- 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/commonstatemachine/examples/di/social/state/FormState.kt @@ -11,11 +11,11 @@ * limitations under the License. */ -package main.kotlin.com.motorro.statemachine.di.social.state +package main.kotlin.com.motorro.commonstatemachine.examples.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 +import com.motorro.commonstatemachine.examples.commoncore.log.Logger +import main.kotlin.com.motorro.commonstatemachine.examples.di.social.data.SocialGesture +import main.kotlin.com.motorro.commonstatemachine.examples.di.social.data.SocialUiState internal class FormState(context: SocialContext) : BaseSocialState(context) { 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/commonstatemachine/examples/di/social/state/LoggingIn.kt similarity index 76% rename from examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/state/LoggingIn.kt rename to examples/di/social/src/main/kotlin/main/kotlin/com/motorro/commonstatemachine/examples/di/social/state/LoggingIn.kt index c30c1f7..fdbfe6b 100644 --- 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/commonstatemachine/examples/di/social/state/LoggingIn.kt @@ -11,15 +11,15 @@ * limitations under the License. */ -package main.kotlin.com.motorro.statemachine.di.social.state +package main.kotlin.com.motorro.commonstatemachine.examples.di.social.state -import com.motorro.statemachine.commoncore.log.Logger -import com.motorro.statemachine.di.api.data.Session +import com.motorro.commonstatemachine.examples.commoncore.log.Logger +import com.motorro.commonstatemachine.examples.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 main.kotlin.com.motorro.commonstatemachine.examples.di.social.SocialConstants +import main.kotlin.com.motorro.commonstatemachine.examples.di.social.data.SocialGesture +import main.kotlin.com.motorro.commonstatemachine.examples.di.social.data.SocialUiState import kotlin.time.Duration.Companion.seconds /** 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/commonstatemachine/examples/di/social/state/SocialContext.kt similarity index 83% rename from examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/state/SocialContext.kt rename to examples/di/social/src/main/kotlin/main/kotlin/com/motorro/commonstatemachine/examples/di/social/state/SocialContext.kt index 56b89c6..47e3129 100644 --- 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/commonstatemachine/examples/di/social/state/SocialContext.kt @@ -11,9 +11,9 @@ * limitations under the License. */ -package main.kotlin.com.motorro.statemachine.di.social.state +package main.kotlin.com.motorro.commonstatemachine.examples.di.social.state -import com.motorro.statemachine.di.api.AuthFlowHost +import com.motorro.commonstatemachine.examples.di.api.AuthFlowHost /** * Common inter-state context 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/commonstatemachine/examples/di/social/state/SocialFactory.kt similarity index 85% rename from examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/state/SocialFactory.kt rename to examples/di/social/src/main/kotlin/main/kotlin/com/motorro/commonstatemachine/examples/di/social/state/SocialFactory.kt index 2492310..0a173ca 100644 --- 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/commonstatemachine/examples/di/social/state/SocialFactory.kt @@ -11,13 +11,13 @@ * limitations under the License. */ -package main.kotlin.com.motorro.statemachine.di.social.state +package main.kotlin.com.motorro.commonstatemachine.examples.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 com.motorro.commonstatemachine.examples.di.api.AuthFlowHost +import com.motorro.commonstatemachine.examples.di.api.AuthGesture +import com.motorro.commonstatemachine.examples.di.api.AuthUiState +import com.motorro.commonstatemachine.examples.di.api.data.Session import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject 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/commonstatemachine/examples/di/social/ui/SocialFormView.kt similarity index 87% rename from examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/ui/SocialFormView.kt rename to examples/di/social/src/main/kotlin/main/kotlin/com/motorro/commonstatemachine/examples/di/social/ui/SocialFormView.kt index 8fe1585..37db52f 100644 --- 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/commonstatemachine/examples/di/social/ui/SocialFormView.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package main.kotlin.com.motorro.statemachine.di.social.ui +package main.kotlin.com.motorro.commonstatemachine.examples.di.social.ui import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column @@ -31,9 +31,9 @@ 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 +import com.motorro.commonstatemachine.examples.commoncore.ui.theme.CommonStateMachineTheme +import main.kotlin.com.motorro.commonstatemachine.examples.di.social.data.SocialGesture +import main.kotlin.com.motorro.commonstatemachine.examples.di.social.data.SocialUiState @Composable internal fun SocialFormView(state: SocialUiState.Form, onGesture: (SocialGesture) -> Unit, modifier: Modifier = Modifier) { 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/commonstatemachine/examples/di/social/ui/SocialScreen.kt similarity index 75% rename from examples/di/social/src/main/kotlin/main/kotlin/com/motorro/statemachine/di/social/ui/SocialScreen.kt rename to examples/di/social/src/main/kotlin/main/kotlin/com/motorro/commonstatemachine/examples/di/social/ui/SocialScreen.kt index 1841ca3..1067ce1 100644 --- 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/commonstatemachine/examples/di/social/ui/SocialScreen.kt @@ -11,13 +11,13 @@ * limitations under the License. */ -package main.kotlin.com.motorro.statemachine.di.social.ui +package main.kotlin.com.motorro.commonstatemachine.examples.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 +import com.motorro.commonstatemachine.examples.commoncore.ui.Loading +import main.kotlin.com.motorro.commonstatemachine.examples.di.social.data.SocialGesture +import main.kotlin.com.motorro.commonstatemachine.examples.di.social.data.SocialUiState @Composable internal fun SocialScreen(state: SocialUiState, onGesture: (SocialGesture) -> Unit, modifier: Modifier = Modifier) { diff --git a/examples/lce/build.gradle.kts b/examples/lce/build.gradle.kts index 2880944..2847d26 100644 --- a/examples/lce/build.gradle.kts +++ b/examples/lce/build.gradle.kts @@ -1,7 +1,3 @@ -@file:Suppress("unused") - -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - /* * Copyright 2022 Nikolai Kotchetkov. * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,94 +12,12 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget */ plugins { - id("com.android.application") - id("org.jetbrains.kotlin.plugin.compose") -} - -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 // Assuming androidCompileSdkVersion is defined elsewhere (e.g., gradle.properties or root build.gradle) - - defaultConfig { - applicationId = "com.motorro.statemachine.lce" - minSdk = androidMinSdkVersion // Assuming androidMinSdkVersion is defined elsewhere - targetSdk = androidTargetSdkVersion // Assuming androidTargetSdkVersion is defined elsewhere - versionCode = versionCode // Assuming versionCode is defined elsewhere - versionName = versionName // Assuming versionName is defined elsewhere - - 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_21 - targetCompatibility = JavaVersion.VERSION_21 - } - kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - } - buildFeatures { - compose = true - } - packaging { - resources { - excludes += "/META-INF/{AL2.0,LGPL2.1}" - } - } - namespace = "com.motorro.statemachine.lce" + alias(libs.plugins.motorro.android.app) } dependencies { implementation(project(":commonstatemachine")) implementation(project(":coroutines")) implementation(project(":examples:commoncore")) - implementation(project(":examples:androidcore")) - - api(project(":commonstatemachine")) - api(project(":commonflow:commonflow-data")) - api(project(":commonflow:commonflow-compose")) - - 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) - debugImplementation(libs.compose.material) - debugImplementation(libs.compose.material.icons) - - debugImplementation(libs.compose.tooling) - - testImplementation(libs.bundles.test.core) - testImplementation(libs.test.androidx.arch) - testImplementation(libs.test.kotlin.coroutines) + implementation(libs.compose.material.icons) } diff --git a/examples/lce/src/main/java/com/motorro/statemachine/lce/MainActivity.kt b/examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/MainActivity.kt similarity index 74% rename from examples/lce/src/main/java/com/motorro/statemachine/lce/MainActivity.kt rename to examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/MainActivity.kt index bd3676e..275ba9b 100644 --- a/examples/lce/src/main/java/com/motorro/statemachine/lce/MainActivity.kt +++ b/examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/MainActivity.kt @@ -11,21 +11,25 @@ * limitations under the License. */ -package com.motorro.statemachine.lce +package com.motorro.commonstatemachine.examples.lce import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge import androidx.compose.runtime.LaunchedEffect -import com.motorro.statemachine.androidcore.ui.theme.CommonStateMachineTheme -import com.motorro.statemachine.lce.ui.LceScreen -import timber.log.Timber +import com.motorro.commonstatemachine.examples.commoncore.ui.theme.CommonStateMachineTheme +import com.motorro.commonstatemachine.examples.lce.ui.LceScreen +import io.github.aakira.napier.DebugAntilog +import io.github.aakira.napier.Napier + class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + enableEdgeToEdge() - Timber.plant(Timber.DebugTree()) + Napier.base(DebugAntilog()) setContent { CommonStateMachineTheme { diff --git a/examples/lce/src/main/java/com/motorro/statemachine/lce/data/ItemId.kt b/examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/data/ItemId.kt similarity index 92% rename from examples/lce/src/main/java/com/motorro/statemachine/lce/data/ItemId.kt rename to examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/data/ItemId.kt index 317cd31..8b87720 100644 --- a/examples/lce/src/main/java/com/motorro/statemachine/lce/data/ItemId.kt +++ b/examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/data/ItemId.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.lce.data +package com.motorro.commonstatemachine.examples.lce.data enum class ItemId { LOADS_CONTENT, diff --git a/examples/lce/src/main/java/com/motorro/statemachine/lce/data/ItemModel.kt b/examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/data/ItemModel.kt similarity index 79% rename from examples/lce/src/main/java/com/motorro/statemachine/lce/data/ItemModel.kt rename to examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/data/ItemModel.kt index 74faf6c..2e9481d 100644 --- a/examples/lce/src/main/java/com/motorro/statemachine/lce/data/ItemModel.kt +++ b/examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/data/ItemModel.kt @@ -11,9 +11,9 @@ * limitations under the License. */ -package com.motorro.statemachine.lce.data +package com.motorro.commonstatemachine.examples.lce.data /** * Item UI model */ -data class ItemModel(val id: ItemId, val name: String) \ No newline at end of file +data class ItemModel(val id: com.motorro.commonstatemachine.examples.lce.data.ItemId, val name: String) \ No newline at end of file diff --git a/examples/lce/src/main/java/com/motorro/statemachine/lce/data/LceGesture.kt b/examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/data/LceGesture.kt similarity index 85% rename from examples/lce/src/main/java/com/motorro/statemachine/lce/data/LceGesture.kt rename to examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/data/LceGesture.kt index 8664176..23dfcd6 100644 --- a/examples/lce/src/main/java/com/motorro/statemachine/lce/data/LceGesture.kt +++ b/examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/data/LceGesture.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.lce.data +package com.motorro.commonstatemachine.examples.lce.data /** * Gestures for application flow @@ -21,7 +21,7 @@ sealed class LceGesture { * Item to load clicked * @property id Item ID to load */ - data class ItemClicked(val id: ItemId) : LceGesture() + data class ItemClicked(val id: com.motorro.commonstatemachine.examples.lce.data.ItemId) : LceGesture() /** * Retry operation clicked diff --git a/examples/lce/src/main/java/com/motorro/statemachine/lce/data/LceUiState.kt b/examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/data/LceUiState.kt similarity index 86% rename from examples/lce/src/main/java/com/motorro/statemachine/lce/data/LceUiState.kt rename to examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/data/LceUiState.kt index 07e2cc0..fabaf52 100644 --- a/examples/lce/src/main/java/com/motorro/statemachine/lce/data/LceUiState.kt +++ b/examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/data/LceUiState.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.lce.data +package com.motorro.commonstatemachine.examples.lce.data import androidx.compose.runtime.Immutable @@ -23,7 +23,7 @@ sealed class LceUiState { * Item list view */ @Immutable - data class ItemList(val items: List) : LceUiState() + data class ItemList(val items: List) : LceUiState() /** * Loading spinner diff --git a/examples/lce/src/main/java/com/motorro/statemachine/lce/model/LceViewModel.kt b/examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/model/LceViewModel.kt similarity index 80% rename from examples/lce/src/main/java/com/motorro/statemachine/lce/model/LceViewModel.kt rename to examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/model/LceViewModel.kt index 60ba0e7..573a695 100644 --- a/examples/lce/src/main/java/com/motorro/statemachine/lce/model/LceViewModel.kt +++ b/examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/model/LceViewModel.kt @@ -11,17 +11,18 @@ * limitations under the License. */ -package com.motorro.statemachine.lce.model +package com.motorro.commonstatemachine.examples.lce.model import androidx.lifecycle.ViewModel import com.motorro.commonstatemachine.CommonMachineState import com.motorro.commonstatemachine.coroutines.FlowStateMachine -import com.motorro.statemachine.lce.data.LceGesture -import com.motorro.statemachine.lce.data.LceUiState -import com.motorro.statemachine.lce.model.state.ItemListState +import com.motorro.commonstatemachine.examples.lce.data.LceGesture +import com.motorro.commonstatemachine.examples.lce.data.LceUiState +import com.motorro.commonstatemachine.examples.lce.model.state.ItemListState +import io.github.aakira.napier.Napier import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import timber.log.Timber + /** * Wraps state-machine with view-model @@ -31,7 +32,8 @@ class LceViewModel : ViewModel() { /** * Creates initial state for state-machine */ - private fun initStateMachine(): CommonMachineState = ItemListState() + private fun initStateMachine(): CommonMachineState = + ItemListState() /** * State-machine instance @@ -48,7 +50,7 @@ class LceViewModel : ViewModel() { * @param gesture UI gesture to proceed */ fun process(gesture: LceGesture) { - Timber.d("Gesture: %s", gesture) + Napier.d("Gesture: ${gesture}") stateMachine.process(gesture) } diff --git a/examples/lce/src/main/java/com/motorro/statemachine/lce/model/state/ContentState.kt b/examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/ContentState.kt similarity index 78% rename from examples/lce/src/main/java/com/motorro/statemachine/lce/model/state/ContentState.kt rename to examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/ContentState.kt index 92f9e4a..7a119f9 100644 --- a/examples/lce/src/main/java/com/motorro/statemachine/lce/model/state/ContentState.kt +++ b/examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/ContentState.kt @@ -11,11 +11,12 @@ * limitations under the License. */ -package com.motorro.statemachine.lce.model.state +package com.motorro.commonstatemachine.examples.lce.model.state + +import com.motorro.commonstatemachine.examples.lce.data.LceGesture +import com.motorro.commonstatemachine.examples.lce.data.LceUiState +import io.github.aakira.napier.Napier -import com.motorro.statemachine.lce.data.LceGesture -import com.motorro.statemachine.lce.data.LceUiState -import timber.log.Timber /** * Displays item details @@ -27,7 +28,7 @@ class ContentState(private val contents: String) : LceLogicalState() { * A part of [start] template to initialize state */ override fun doStart() { - Timber.d("Displaying content...") + Napier.d("Displaying content...") setUiState(LceUiState.Item(contents)) } @@ -40,7 +41,7 @@ class ContentState(private val contents: String) : LceLogicalState() { } private fun onBack() { - Timber.d("Back: returning to item list view...") + Napier.d("Back: returning to item list view...") setMachineState(ItemListState()) } } \ No newline at end of file diff --git a/examples/lce/src/main/java/com/motorro/statemachine/lce/model/state/ErrorState.kt b/examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/ErrorState.kt similarity index 76% rename from examples/lce/src/main/java/com/motorro/statemachine/lce/model/state/ErrorState.kt rename to examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/ErrorState.kt index d2173d1..4fe064b 100644 --- a/examples/lce/src/main/java/com/motorro/statemachine/lce/model/state/ErrorState.kt +++ b/examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/ErrorState.kt @@ -11,12 +11,13 @@ * limitations under the License. */ -package com.motorro.statemachine.lce.model.state +package com.motorro.commonstatemachine.examples.lce.model.state + +import com.motorro.commonstatemachine.examples.lce.data.ItemId +import com.motorro.commonstatemachine.examples.lce.data.LceGesture +import com.motorro.commonstatemachine.examples.lce.data.LceUiState +import io.github.aakira.napier.Napier -import com.motorro.statemachine.lce.data.ItemId -import com.motorro.statemachine.lce.data.LceGesture -import com.motorro.statemachine.lce.data.LceUiState -import timber.log.Timber /** * Item load error state @@ -30,7 +31,7 @@ class ErrorState(private val failed: ItemId, private val error: Throwable) : Lce * A part of [start] template to initialize state */ override fun doStart() { - Timber.d("Displaying error...") + Napier.d("Displaying error...") setUiState(LceUiState.Error(error)) } @@ -45,17 +46,17 @@ class ErrorState(private val failed: ItemId, private val error: Throwable) : Lce } private fun onRetry() { - Timber.d("Retry: retrying load...") + Napier.d("Retry: retrying load...") setMachineState(LoadingState(failed)) } private fun onBack() { - Timber.d("Back: returning to item list view...") + Napier.d("Back: returning to item list view...") setMachineState(ItemListState()) } private fun onExit() { - Timber.d("Exit: Terminating flow...") + Napier.d("Exit: Terminating flow...") setMachineState(TerminatedState()) } } \ No newline at end of file diff --git a/examples/lce/src/main/java/com/motorro/statemachine/lce/model/state/ItemListState.kt b/examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/ItemListState.kt similarity index 74% rename from examples/lce/src/main/java/com/motorro/statemachine/lce/model/state/ItemListState.kt rename to examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/ItemListState.kt index e4063ac..b4f3125 100644 --- a/examples/lce/src/main/java/com/motorro/statemachine/lce/model/state/ItemListState.kt +++ b/examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/ItemListState.kt @@ -11,13 +11,14 @@ * limitations under the License. */ -package com.motorro.statemachine.lce.model.state +package com.motorro.commonstatemachine.examples.lce.model.state + +import com.motorro.commonstatemachine.examples.lce.data.ItemId +import com.motorro.commonstatemachine.examples.lce.data.ItemModel +import com.motorro.commonstatemachine.examples.lce.data.LceGesture +import com.motorro.commonstatemachine.examples.lce.data.LceUiState +import io.github.aakira.napier.Napier -import com.motorro.statemachine.lce.data.ItemId -import com.motorro.statemachine.lce.data.ItemModel -import com.motorro.statemachine.lce.data.LceGesture -import com.motorro.statemachine.lce.data.LceUiState -import timber.log.Timber /** * Item list @@ -34,7 +35,7 @@ class ItemListState : LceLogicalState() { * A part of [start] template to initialize state */ override fun doStart() { - Timber.d("Displaying item list") + Napier.d("Displaying item list") setUiState(LceUiState.ItemList(items.map { ItemModel(it.first, it.second) })) } @@ -48,12 +49,12 @@ class ItemListState : LceLogicalState() { } private fun onItemClicked(id: ItemId) { - Timber.d("Item clicked: transferring to loading...") + Napier.d("Item clicked: transferring to loading...") setMachineState(LoadingState(id)) } private fun onBack() { - Timber.d("Back: Terminating flow...") + Napier.d("Back: Terminating flow...") setMachineState(TerminatedState()) } } \ No newline at end of file diff --git a/examples/lce/src/main/java/com/motorro/statemachine/lce/model/state/LceLogicalState.kt b/examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/LceLogicalState.kt similarity index 75% rename from examples/lce/src/main/java/com/motorro/statemachine/lce/model/state/LceLogicalState.kt rename to examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/LceLogicalState.kt index 710d22d..f2a886a 100644 --- a/examples/lce/src/main/java/com/motorro/statemachine/lce/model/state/LceLogicalState.kt +++ b/examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/LceLogicalState.kt @@ -11,12 +11,13 @@ * limitations under the License. */ -package com.motorro.statemachine.lce.model.state +package com.motorro.commonstatemachine.examples.lce.model.state import com.motorro.commonstatemachine.CommonMachineState -import com.motorro.statemachine.lce.data.LceGesture -import com.motorro.statemachine.lce.data.LceUiState -import timber.log.Timber +import com.motorro.commonstatemachine.examples.lce.data.LceGesture +import com.motorro.commonstatemachine.examples.lce.data.LceUiState +import io.github.aakira.napier.Napier + /** * Basic flow state @@ -26,6 +27,6 @@ abstract class LceLogicalState : CommonMachineState() { * A part of [process] template to process UI gesture */ override fun doProcess(gesture: LceGesture) { - Timber.d("Unhandled gesture: %s", gesture) + Napier.d("Unhandled gesture: ${gesture}") } } \ No newline at end of file diff --git a/examples/lce/src/main/java/com/motorro/statemachine/lce/model/state/LoadingState.kt b/examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/LoadingState.kt similarity index 78% rename from examples/lce/src/main/java/com/motorro/statemachine/lce/model/state/LoadingState.kt rename to examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/LoadingState.kt index 1202ff5..62f8e1b 100644 --- a/examples/lce/src/main/java/com/motorro/statemachine/lce/model/state/LoadingState.kt +++ b/examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/LoadingState.kt @@ -11,19 +11,19 @@ * limitations under the License. */ -package com.motorro.statemachine.lce.model.state +package com.motorro.commonstatemachine.examples.lce.model.state import androidx.annotation.VisibleForTesting import com.motorro.commonstatemachine.coroutines.CoroutineState -import com.motorro.statemachine.lce.data.ItemId -import com.motorro.statemachine.lce.data.LceGesture -import com.motorro.statemachine.lce.data.LceUiState +import com.motorro.commonstatemachine.examples.lce.data.ItemId +import com.motorro.commonstatemachine.examples.lce.data.LceGesture +import com.motorro.commonstatemachine.examples.lce.data.LceUiState +import io.github.aakira.napier.Napier import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import timber.log.Timber import java.io.IOException /** @@ -41,14 +41,14 @@ class LoadingState( * A part of [start] template to initialize state */ override fun doStart() { - Timber.d("Displaying spinner...") + Napier.d("Displaying spinner...") setUiState(LceUiState.Loading) load() } private fun load() { stateScope.launch(defaultDispatcher) { - Timber.d("Loading data...") + Napier.d("Loading data...") delay(1000L) withContext(Dispatchers.Main) { when (id) { @@ -60,12 +60,12 @@ class LoadingState( } private fun toContent() { - Timber.d("Data loaded: transferring to content...") + Napier.d("Data loaded: transferring to content...") setMachineState(ContentState("Some item data...")) } private fun toError() { - Timber.d("Data load error: transferring to error...") + Napier.d("Data load error: transferring to error...") setMachineState(ErrorState(id, IOException("Failed to load item"))) } @@ -74,11 +74,11 @@ class LoadingState( */ override fun doProcess(gesture: LceGesture) = when(gesture) { LceGesture.Back -> onBack() - else -> Timber.d("Unhandled gesture: %s", gesture) + else -> Napier.d("Unhandled gesture: ${gesture}") } private fun onBack() { - Timber.d("Back: returning to item view") + Napier.d("Back: returning to item view") setMachineState(ItemListState()) } } \ No newline at end of file diff --git a/examples/lce/src/main/java/com/motorro/statemachine/lce/model/state/TerminatedState.kt b/examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/TerminatedState.kt similarity index 80% rename from examples/lce/src/main/java/com/motorro/statemachine/lce/model/state/TerminatedState.kt rename to examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/TerminatedState.kt index 6f98d6e..44805f4 100644 --- a/examples/lce/src/main/java/com/motorro/statemachine/lce/model/state/TerminatedState.kt +++ b/examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/TerminatedState.kt @@ -11,10 +11,11 @@ * limitations under the License. */ -package com.motorro.statemachine.lce.model.state +package com.motorro.commonstatemachine.examples.lce.model.state + +import com.motorro.commonstatemachine.examples.lce.data.LceUiState +import io.github.aakira.napier.Napier -import com.motorro.statemachine.lce.data.LceUiState -import timber.log.Timber /** * Terminates activity @@ -24,7 +25,7 @@ class TerminatedState : LceLogicalState() { * A part of [start] template to initialize state */ override fun doStart() { - Timber.d("Terminated") + Napier.d("Terminated") setUiState(LceUiState.Terminated) } } \ No newline at end of file diff --git a/examples/lce/src/main/java/com/motorro/statemachine/lce/ui/ItemContents.kt b/examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/ui/ItemContents.kt similarity index 89% rename from examples/lce/src/main/java/com/motorro/statemachine/lce/ui/ItemContents.kt rename to examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/ui/ItemContents.kt index 8e2a1f0..391c586 100644 --- a/examples/lce/src/main/java/com/motorro/statemachine/lce/ui/ItemContents.kt +++ b/examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/ui/ItemContents.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.lce.ui +package com.motorro.commonstatemachine.examples.lce.ui import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -19,7 +19,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import com.motorro.statemachine.lce.data.LceUiState +import com.motorro.commonstatemachine.examples.lce.data.LceUiState @Composable fun ItemDetails(state: LceUiState.Item) { diff --git a/examples/lce/src/main/java/com/motorro/statemachine/lce/ui/ItemList.kt b/examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/ui/ItemList.kt similarity index 87% rename from examples/lce/src/main/java/com/motorro/statemachine/lce/ui/ItemList.kt rename to examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/ui/ItemList.kt index 473756b..8fed86f 100644 --- a/examples/lce/src/main/java/com/motorro/statemachine/lce/ui/ItemList.kt +++ b/examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/ui/ItemList.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.lce.ui +package com.motorro.commonstatemachine.examples.lce.ui import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -26,9 +26,9 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.motorro.statemachine.lce.R -import com.motorro.statemachine.lce.data.ItemId -import com.motorro.statemachine.lce.data.LceUiState +import com.motorro.commonstatemachine.examples.lce.R +import com.motorro.commonstatemachine.examples.lce.data.ItemId +import com.motorro.commonstatemachine.examples.lce.data.LceUiState @Composable fun ItemList(state: LceUiState.ItemList, onItemClicked: (ItemId) -> Unit) { diff --git a/examples/lce/src/main/java/com/motorro/statemachine/lce/ui/LceScreen.kt b/examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/ui/LceScreen.kt similarity index 83% rename from examples/lce/src/main/java/com/motorro/statemachine/lce/ui/LceScreen.kt rename to examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/ui/LceScreen.kt index a60d99c..77bc5d1 100644 --- a/examples/lce/src/main/java/com/motorro/statemachine/lce/ui/LceScreen.kt +++ b/examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/ui/LceScreen.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.lce.ui +package com.motorro.commonstatemachine.examples.lce.ui import android.annotation.SuppressLint import androidx.activity.compose.BackHandler @@ -32,14 +32,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.lifecycle.viewmodel.compose.viewModel -import com.motorro.statemachine.androidcore.compose.Loading -import com.motorro.statemachine.lce.R -import com.motorro.statemachine.lce.data.LceGesture.Back -import com.motorro.statemachine.lce.data.LceGesture.Exit -import com.motorro.statemachine.lce.data.LceGesture.ItemClicked -import com.motorro.statemachine.lce.data.LceGesture.Retry -import com.motorro.statemachine.lce.data.LceUiState -import com.motorro.statemachine.lce.model.LceViewModel +import com.motorro.commonstatemachine.examples.commoncore.ui.Loading +import com.motorro.commonstatemachine.examples.lce.R +import com.motorro.commonstatemachine.examples.lce.data.LceGesture.Back +import com.motorro.commonstatemachine.examples.lce.data.LceGesture.Exit +import com.motorro.commonstatemachine.examples.lce.data.LceGesture.ItemClicked +import com.motorro.commonstatemachine.examples.lce.data.LceGesture.Retry +import com.motorro.commonstatemachine.examples.lce.data.LceUiState +import com.motorro.commonstatemachine.examples.lce.model.LceViewModel @Composable @OptIn(ExperimentalMaterial3Api::class) diff --git a/examples/lce/src/main/java/com/motorro/statemachine/lce/ui/LoadError.kt b/examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/ui/LoadError.kt similarity index 90% rename from examples/lce/src/main/java/com/motorro/statemachine/lce/ui/LoadError.kt rename to examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/ui/LoadError.kt index f31bcc1..4a0dcfe 100644 --- a/examples/lce/src/main/java/com/motorro/statemachine/lce/ui/LoadError.kt +++ b/examples/lce/src/main/kotlin/com/motorro/commonstatemachine/examples/lce/ui/LoadError.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.lce.ui +package com.motorro.commonstatemachine.examples.lce.ui import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.AlertDialog @@ -21,8 +21,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign -import com.motorro.statemachine.lce.R -import com.motorro.statemachine.lce.data.LceUiState +import com.motorro.commonstatemachine.examples.lce.R +import com.motorro.commonstatemachine.examples.lce.data.LceUiState @Composable fun LoadError(state: LceUiState.Error, onRetry: () -> Unit, onBack: () -> Unit, onExit: () -> Unit) { diff --git a/examples/lce/src/test/java/com/motorro/statemachine/lce/ExampleUnitTest.kt b/examples/lce/src/test/kotlin/com/motorro/commonstatemachine/examples/lce/ExampleUnitTest.kt similarity index 94% rename from examples/lce/src/test/java/com/motorro/statemachine/lce/ExampleUnitTest.kt rename to examples/lce/src/test/kotlin/com/motorro/commonstatemachine/examples/lce/ExampleUnitTest.kt index e653870..32019c4 100644 --- a/examples/lce/src/test/java/com/motorro/statemachine/lce/ExampleUnitTest.kt +++ b/examples/lce/src/test/kotlin/com/motorro/commonstatemachine/examples/lce/ExampleUnitTest.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.lce +package com.motorro.commonstatemachine.examples.lce import org.junit.Assert.assertEquals import org.junit.Test diff --git a/examples/lce/src/test/java/com/motorro/statemachine/lce/model/state/BaseStateTest.kt b/examples/lce/src/test/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/BaseStateTest.kt similarity index 81% rename from examples/lce/src/test/java/com/motorro/statemachine/lce/model/state/BaseStateTest.kt rename to examples/lce/src/test/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/BaseStateTest.kt index 11d191d..aca32f2 100644 --- a/examples/lce/src/test/java/com/motorro/statemachine/lce/model/state/BaseStateTest.kt +++ b/examples/lce/src/test/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/BaseStateTest.kt @@ -11,11 +11,11 @@ * limitations under the License. */ -package com.motorro.statemachine.lce.model.state +package com.motorro.commonstatemachine.examples.lce.model.state import com.motorro.commonstatemachine.CommonStateMachine -import com.motorro.statemachine.lce.data.LceGesture -import com.motorro.statemachine.lce.data.LceUiState +import com.motorro.commonstatemachine.examples.lce.data.LceGesture +import com.motorro.commonstatemachine.examples.lce.data.LceUiState import io.mockk.mockk open class BaseStateTest { diff --git a/examples/lce/src/test/java/com/motorro/statemachine/lce/model/state/ItemDetailsStateTest.kt b/examples/lce/src/test/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/ItemDetailsStateTest.kt similarity index 86% rename from examples/lce/src/test/java/com/motorro/statemachine/lce/model/state/ItemDetailsStateTest.kt rename to examples/lce/src/test/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/ItemDetailsStateTest.kt index 30a0ccc..02ea4db 100644 --- a/examples/lce/src/test/java/com/motorro/statemachine/lce/model/state/ItemDetailsStateTest.kt +++ b/examples/lce/src/test/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/ItemDetailsStateTest.kt @@ -11,10 +11,10 @@ * limitations under the License. */ -package com.motorro.statemachine.lce.model.state +package com.motorro.commonstatemachine.examples.lce.model.state -import com.motorro.statemachine.lce.data.LceGesture -import com.motorro.statemachine.lce.data.LceUiState +import com.motorro.commonstatemachine.examples.lce.data.LceGesture +import com.motorro.commonstatemachine.examples.lce.data.LceUiState import io.mockk.verify import org.junit.Test import kotlin.test.assertIs diff --git a/examples/lce/src/test/java/com/motorro/statemachine/lce/model/state/ItemListStateTest.kt b/examples/lce/src/test/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/ItemListStateTest.kt similarity index 84% rename from examples/lce/src/test/java/com/motorro/statemachine/lce/model/state/ItemListStateTest.kt rename to examples/lce/src/test/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/ItemListStateTest.kt index 0fc31ad..6bf1664 100644 --- a/examples/lce/src/test/java/com/motorro/statemachine/lce/model/state/ItemListStateTest.kt +++ b/examples/lce/src/test/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/ItemListStateTest.kt @@ -11,11 +11,11 @@ * limitations under the License. */ -package com.motorro.statemachine.lce.model.state +package com.motorro.commonstatemachine.examples.lce.model.state -import com.motorro.statemachine.lce.data.ItemId -import com.motorro.statemachine.lce.data.LceGesture -import com.motorro.statemachine.lce.data.LceUiState +import com.motorro.commonstatemachine.examples.lce.data.ItemId +import com.motorro.commonstatemachine.examples.lce.data.LceGesture +import com.motorro.commonstatemachine.examples.lce.data.LceUiState import io.mockk.verify import org.junit.Test import kotlin.test.assertIs diff --git a/examples/lce/src/test/java/com/motorro/statemachine/lce/model/state/ItemLoadingStateTest.kt b/examples/lce/src/test/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/ItemLoadingStateTest.kt similarity index 83% rename from examples/lce/src/test/java/com/motorro/statemachine/lce/model/state/ItemLoadingStateTest.kt rename to examples/lce/src/test/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/ItemLoadingStateTest.kt index d04829e..6472ae0 100644 --- a/examples/lce/src/test/java/com/motorro/statemachine/lce/model/state/ItemLoadingStateTest.kt +++ b/examples/lce/src/test/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/ItemLoadingStateTest.kt @@ -13,15 +13,19 @@ @file:OptIn(ExperimentalCoroutinesApi::class) -package com.motorro.statemachine.lce.model.state +package com.motorro.commonstatemachine.examples.lce.model.state -import com.motorro.statemachine.lce.data.ItemId -import com.motorro.statemachine.lce.data.LceGesture -import com.motorro.statemachine.lce.data.LceUiState +import com.motorro.commonstatemachine.examples.lce.data.ItemId +import com.motorro.commonstatemachine.examples.lce.data.LceGesture +import com.motorro.commonstatemachine.examples.lce.data.LceUiState import io.mockk.verify import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.* +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Before import org.junit.Test diff --git a/examples/lce/src/test/java/com/motorro/statemachine/lce/model/state/LoadErrorStateTest.kt b/examples/lce/src/test/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/LoadErrorStateTest.kt similarity index 87% rename from examples/lce/src/test/java/com/motorro/statemachine/lce/model/state/LoadErrorStateTest.kt rename to examples/lce/src/test/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/LoadErrorStateTest.kt index db69418..dac172c 100644 --- a/examples/lce/src/test/java/com/motorro/statemachine/lce/model/state/LoadErrorStateTest.kt +++ b/examples/lce/src/test/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/LoadErrorStateTest.kt @@ -11,11 +11,11 @@ * limitations under the License. */ -package com.motorro.statemachine.lce.model.state +package com.motorro.commonstatemachine.examples.lce.model.state -import com.motorro.statemachine.lce.data.ItemId -import com.motorro.statemachine.lce.data.LceGesture -import com.motorro.statemachine.lce.data.LceUiState +import com.motorro.commonstatemachine.examples.lce.data.ItemId +import com.motorro.commonstatemachine.examples.lce.data.LceGesture +import com.motorro.commonstatemachine.examples.lce.data.LceUiState import io.mockk.verify import org.junit.Test import java.io.IOException diff --git a/examples/lce/src/test/java/com/motorro/statemachine/lce/model/state/TerminatedStateTest.kt b/examples/lce/src/test/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/TerminatedStateTest.kt similarity index 87% rename from examples/lce/src/test/java/com/motorro/statemachine/lce/model/state/TerminatedStateTest.kt rename to examples/lce/src/test/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/TerminatedStateTest.kt index 85ef3cf..0df0ad8 100644 --- a/examples/lce/src/test/java/com/motorro/statemachine/lce/model/state/TerminatedStateTest.kt +++ b/examples/lce/src/test/kotlin/com/motorro/commonstatemachine/examples/lce/model/state/TerminatedStateTest.kt @@ -11,9 +11,9 @@ * limitations under the License. */ -package com.motorro.statemachine.lce.model.state +package com.motorro.commonstatemachine.examples.lce.model.state -import com.motorro.statemachine.lce.data.LceUiState +import com.motorro.commonstatemachine.examples.lce.data.LceUiState import io.mockk.verify import org.junit.Test diff --git a/examples/lifecycle/build.gradle.kts b/examples/lifecycle/build.gradle.kts index 38a0b36..312fd51 100644 --- a/examples/lifecycle/build.gradle.kts +++ b/examples/lifecycle/build.gradle.kts @@ -1,8 +1,3 @@ -@file:Suppress("unused") - -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - - /* * Copyright 2026 Nikolai Kotchetkov. * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,60 +12,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget */ plugins { - alias(libs.plugins.android.app) - alias(libs.plugins.compose) -} - -val versionCode: String by project.extra -val versionName: String by project.extra -val androidMinSdkVersion: Int by project.extra -val androidTargetSdkVersion: Int by project.extra -val androidCompileSdkVersion: Int by project.extra - -android { - namespace = "com.motorro.statemachine.lifecycle" - compileSdk = androidCompileSdkVersion - - defaultConfig { - applicationId = "com.motorro.statemachine.lifecycle" - 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_21 - targetCompatibility = JavaVersion.VERSION_21 - } - kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - } - buildFeatures { - compose = true - } - packaging { - resources { - excludes += "/META-INF/{AL2.0,LGPL2.1}" - } - } + alias(libs.plugins.motorro.android.app) } dependencies { @@ -78,32 +20,7 @@ dependencies { implementation(project(":coroutines")) implementation(project(":lifecycle")) implementation(project(":examples:commoncore")) - implementation(project(":examples:androidcore")) implementation(project(":examples:timer")) - 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.coroutines.core) - implementation(libs.kotlin.coroutines.android) - - val composeBom = platform(libs.compose.bom) - implementation(composeBom) - androidTestImplementation(composeBom) - - implementation(libs.bundles.compose.core) - implementation(libs.compose.activity) - implementation(libs.compose.viewmodel) - implementation(libs.compose.foundation) - implementation(libs.compose.foundation.layouts) - - debugImplementation(libs.compose.tooling) - - testImplementation(libs.bundles.test.core) testImplementation(libs.test.androidx.arch) - testImplementation(libs.test.kotlin.coroutines) } \ No newline at end of file diff --git a/examples/lifecycle/src/main/AndroidManifest.xml b/examples/lifecycle/src/main/AndroidManifest.xml index 5e83929..96b177b 100644 --- a/examples/lifecycle/src/main/AndroidManifest.xml +++ b/examples/lifecycle/src/main/AndroidManifest.xml @@ -20,10 +20,10 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:name="com.motorro.statemachine.lifecycle.App" + android:name="com.motorro.commonstatemachine.examples.lifecycle.App" android:theme="@style/Theme.CommonStateMachine"> diff --git a/examples/lifecycle/src/main/java/com/motorro/statemachine/lifecycle/App.kt b/examples/lifecycle/src/main/kotlin/com/motorro/commonstatemachine/examples/lifecycle/App.kt similarity index 81% rename from examples/lifecycle/src/main/java/com/motorro/statemachine/lifecycle/App.kt rename to examples/lifecycle/src/main/kotlin/com/motorro/commonstatemachine/examples/lifecycle/App.kt index 5624198..74c38ff 100644 --- a/examples/lifecycle/src/main/java/com/motorro/statemachine/lifecycle/App.kt +++ b/examples/lifecycle/src/main/kotlin/com/motorro/commonstatemachine/examples/lifecycle/App.kt @@ -11,10 +11,12 @@ * limitations under the License. */ -package com.motorro.statemachine.lifecycle +package com.motorro.commonstatemachine.examples.lifecycle import android.app.Application -import timber.log.Timber +import io.github.aakira.napier.DebugAntilog +import io.github.aakira.napier.Napier + class App : Application() { override fun onCreate() { @@ -23,6 +25,6 @@ class App : Application() { } private fun setupLogger() { - Timber.plant(Timber.DebugTree()) + Napier.base(DebugAntilog()) } } \ No newline at end of file diff --git a/examples/lifecycle/src/main/java/com/motorro/statemachine/lifecycle/MainActivity.kt b/examples/lifecycle/src/main/kotlin/com/motorro/commonstatemachine/examples/lifecycle/MainActivity.kt similarity index 87% rename from examples/lifecycle/src/main/java/com/motorro/statemachine/lifecycle/MainActivity.kt rename to examples/lifecycle/src/main/kotlin/com/motorro/commonstatemachine/examples/lifecycle/MainActivity.kt index d8f6c51..c4934b6 100644 --- a/examples/lifecycle/src/main/java/com/motorro/statemachine/lifecycle/MainActivity.kt +++ b/examples/lifecycle/src/main/kotlin/com/motorro/commonstatemachine/examples/lifecycle/MainActivity.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.lifecycle +package com.motorro.commonstatemachine.examples.lifecycle import android.os.Bundle import androidx.activity.ComponentActivity @@ -24,9 +24,9 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.viewmodel.compose.viewModel -import com.motorro.statemachine.androidcore.ui.theme.CommonStateMachineTheme -import com.motorro.statemachine.lifecycle.model.MainViewModel -import com.motorro.statemachine.timer.ui.TimerScreen +import com.motorro.commonstatemachine.examples.commoncore.ui.theme.CommonStateMachineTheme +import com.motorro.commonstatemachine.examples.lifecycle.model.MainViewModel +import com.motorro.commonstatemachine.examples.timer.ui.TimerScreen class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { diff --git a/examples/lifecycle/src/main/java/com/motorro/statemachine/lifecycle/model/MainViewModel.kt b/examples/lifecycle/src/main/kotlin/com/motorro/commonstatemachine/examples/lifecycle/model/MainViewModel.kt similarity index 81% rename from examples/lifecycle/src/main/java/com/motorro/statemachine/lifecycle/model/MainViewModel.kt rename to examples/lifecycle/src/main/kotlin/com/motorro/commonstatemachine/examples/lifecycle/model/MainViewModel.kt index 3003d34..70c0bd9 100644 --- a/examples/lifecycle/src/main/java/com/motorro/statemachine/lifecycle/model/MainViewModel.kt +++ b/examples/lifecycle/src/main/kotlin/com/motorro/commonstatemachine/examples/lifecycle/model/MainViewModel.kt @@ -1,13 +1,13 @@ -package com.motorro.statemachine.lifecycle.model +package com.motorro.commonstatemachine.examples.lifecycle.model import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModel import com.motorro.commonstatemachine.coroutines.FlowStateMachine +import com.motorro.commonstatemachine.examples.timer.data.TimerGesture +import com.motorro.commonstatemachine.examples.timer.data.TimerUiState +import com.motorro.commonstatemachine.examples.timer.state.TimerState import com.motorro.commonstatemachine.lifecycle.UiMachineLifecycle import com.motorro.commonstatemachine.lifecycle.UiMachineLifecycle.Companion.bindLifecycle -import com.motorro.statemachine.timer.data.TimerGesture -import com.motorro.statemachine.timer.data.TimerUiState -import com.motorro.statemachine.timer.state.TimerState import kotlinx.coroutines.flow.StateFlow import kotlin.time.Duration diff --git a/examples/multi/mixed/build.gradle.kts b/examples/multi/mixed/build.gradle.kts index 712270d..e3295b6 100644 --- a/examples/multi/mixed/build.gradle.kts +++ b/examples/multi/mixed/build.gradle.kts @@ -1,8 +1,3 @@ -@file:Suppress("unused") - -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - - /* * Copyright 2023 Nikolai Kotchetkov. * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,92 +12,12 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget */ plugins { - alias(libs.plugins.android.app) - alias(libs.plugins.compose) -} - -val versionCode: String by project.extra -val versionName: String by project.extra -val androidMinSdkVersion: Int by project.extra -val androidTargetSdkVersion: Int by project.extra -val androidCompileSdkVersion: Int by project.extra - -android { - namespace = "com.motorro.statemachine.multi.mixed" - compileSdk = androidCompileSdkVersion - - defaultConfig { - applicationId = "com.motorro.statemachine.multi.mixed" - 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_21 - targetCompatibility = JavaVersion.VERSION_21 - } - kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - } - buildFeatures { - compose = true - } - packaging { - resources { - excludes += "/META-INF/{AL2.0,LGPL2.1}" - } - } + alias(libs.plugins.motorro.android.app) } dependencies { implementation(project(":commonstatemachine")) implementation(project(":coroutines")) implementation(project(":examples:commoncore")) - implementation(project(":examples:androidcore")) implementation(project(":examples:timer")) - - 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.coroutines.core) - implementation(libs.kotlin.coroutines.android) - - val composeBom = platform(libs.compose.bom) - implementation(composeBom) - androidTestImplementation(composeBom) - - implementation(libs.bundles.compose.core) - implementation(libs.compose.activity) - implementation(libs.compose.viewmodel) - implementation(libs.compose.foundation) - implementation(libs.compose.foundation.layouts) - - debugImplementation(libs.compose.tooling) - - testImplementation(libs.bundles.test.core) - testImplementation(libs.test.androidx.arch) - testImplementation(libs.test.kotlin.coroutines) } \ No newline at end of file diff --git a/examples/multi/mixed/src/main/AndroidManifest.xml b/examples/multi/mixed/src/main/AndroidManifest.xml index dff16cc..9986708 100644 --- a/examples/multi/mixed/src/main/AndroidManifest.xml +++ b/examples/multi/mixed/src/main/AndroidManifest.xml @@ -21,10 +21,10 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:name="com.motorro.statemachine.mixed.App" + android:name="com.motorro.commonstatemachine.examples.mixed.App" android:theme="@style/Theme.CommonStateMachine"> diff --git a/examples/multi/navbar/src/main/java/com/motorro/statemachine/navbar/App.kt b/examples/multi/navbar/src/main/kotlin/com/motorro/commonstatemachine/examples/navbar/App.kt similarity index 81% rename from examples/multi/navbar/src/main/java/com/motorro/statemachine/navbar/App.kt rename to examples/multi/navbar/src/main/kotlin/com/motorro/commonstatemachine/examples/navbar/App.kt index d0a9593..b3646ea 100644 --- a/examples/multi/navbar/src/main/java/com/motorro/statemachine/navbar/App.kt +++ b/examples/multi/navbar/src/main/kotlin/com/motorro/commonstatemachine/examples/navbar/App.kt @@ -11,10 +11,12 @@ * limitations under the License. */ -package com.motorro.statemachine.navbar +package com.motorro.commonstatemachine.examples.navbar import android.app.Application -import timber.log.Timber +import io.github.aakira.napier.DebugAntilog +import io.github.aakira.napier.Napier + class App : Application() { override fun onCreate() { @@ -23,6 +25,6 @@ class App : Application() { } private fun setupLogger() { - Timber.plant(Timber.DebugTree()) + Napier.base(DebugAntilog()) } } \ No newline at end of file diff --git a/examples/multi/navbar/src/main/java/com/motorro/statemachine/navbar/MainActivity.kt b/examples/multi/navbar/src/main/kotlin/com/motorro/commonstatemachine/examples/navbar/MainActivity.kt similarity index 93% rename from examples/multi/navbar/src/main/java/com/motorro/statemachine/navbar/MainActivity.kt rename to examples/multi/navbar/src/main/kotlin/com/motorro/commonstatemachine/examples/navbar/MainActivity.kt index 8adcbed..160de42 100644 --- a/examples/multi/navbar/src/main/java/com/motorro/statemachine/navbar/MainActivity.kt +++ b/examples/multi/navbar/src/main/kotlin/com/motorro/commonstatemachine/examples/navbar/MainActivity.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.navbar +package com.motorro.commonstatemachine.examples.navbar import android.content.ComponentCallbacks2 import android.os.Bundle @@ -45,10 +45,10 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import com.motorro.statemachine.androidcore.ui.theme.CommonStateMachineTheme -import com.motorro.statemachine.navbar.model.MainViewModel -import com.motorro.statemachine.navbar.model.data.NavbarGesture -import com.motorro.statemachine.timer.ui.TimerScreen +import com.motorro.commonstatemachine.examples.commoncore.ui.theme.CommonStateMachineTheme +import com.motorro.commonstatemachine.examples.navbar.model.MainViewModel +import com.motorro.commonstatemachine.examples.navbar.model.data.NavbarGesture +import com.motorro.commonstatemachine.examples.timer.ui.TimerScreen import kotlinx.coroutines.launch class MainActivity : ComponentActivity(), ComponentCallbacks2 { diff --git a/examples/multi/navbar/src/main/java/com/motorro/statemachine/navbar/model/MainViewModel.kt b/examples/multi/navbar/src/main/kotlin/com/motorro/commonstatemachine/examples/navbar/model/MainViewModel.kt similarity index 78% rename from examples/multi/navbar/src/main/java/com/motorro/statemachine/navbar/model/MainViewModel.kt rename to examples/multi/navbar/src/main/kotlin/com/motorro/commonstatemachine/examples/navbar/model/MainViewModel.kt index 03754c4..a021ad6 100644 --- a/examples/multi/navbar/src/main/java/com/motorro/statemachine/navbar/model/MainViewModel.kt +++ b/examples/multi/navbar/src/main/kotlin/com/motorro/commonstatemachine/examples/navbar/model/MainViewModel.kt @@ -11,13 +11,13 @@ * limitations under the License. */ -package com.motorro.statemachine.navbar.model +package com.motorro.commonstatemachine.examples.navbar.model import androidx.lifecycle.ViewModel import com.motorro.commonstatemachine.coroutines.FlowStateMachine -import com.motorro.statemachine.navbar.model.data.NavbarGesture -import com.motorro.statemachine.navbar.model.data.NavbarUiState -import com.motorro.statemachine.navbar.model.state.NavbarState +import com.motorro.commonstatemachine.examples.navbar.model.data.NavbarGesture +import com.motorro.commonstatemachine.examples.navbar.model.data.NavbarUiState +import com.motorro.commonstatemachine.examples.navbar.model.state.NavbarState import kotlinx.coroutines.flow.StateFlow class MainViewModel : ViewModel() { diff --git a/examples/multi/navbar/src/main/java/com/motorro/statemachine/navbar/model/data/NavbarGesture.kt b/examples/multi/navbar/src/main/kotlin/com/motorro/commonstatemachine/examples/navbar/model/data/NavbarGesture.kt similarity index 82% rename from examples/multi/navbar/src/main/java/com/motorro/statemachine/navbar/model/data/NavbarGesture.kt rename to examples/multi/navbar/src/main/kotlin/com/motorro/commonstatemachine/examples/navbar/model/data/NavbarGesture.kt index b501208..23f2776 100644 --- a/examples/multi/navbar/src/main/java/com/motorro/statemachine/navbar/model/data/NavbarGesture.kt +++ b/examples/multi/navbar/src/main/kotlin/com/motorro/commonstatemachine/examples/navbar/model/data/NavbarGesture.kt @@ -11,10 +11,10 @@ * limitations under the License. */ -package com.motorro.statemachine.navbar.model.data +package com.motorro.commonstatemachine.examples.navbar.model.data -import com.motorro.statemachine.timer.data.TimerGesture -import com.motorro.statemachine.timer.data.TimerKey +import com.motorro.commonstatemachine.examples.timer.data.TimerGesture +import com.motorro.commonstatemachine.examples.timer.data.TimerKey /** * Application gestures diff --git a/examples/multi/navbar/src/main/java/com/motorro/statemachine/navbar/model/data/NavbarUiState.kt b/examples/multi/navbar/src/main/kotlin/com/motorro/commonstatemachine/examples/navbar/model/data/NavbarUiState.kt similarity index 80% rename from examples/multi/navbar/src/main/java/com/motorro/statemachine/navbar/model/data/NavbarUiState.kt rename to examples/multi/navbar/src/main/kotlin/com/motorro/commonstatemachine/examples/navbar/model/data/NavbarUiState.kt index dcc91ce..09c4f21 100644 --- a/examples/multi/navbar/src/main/java/com/motorro/statemachine/navbar/model/data/NavbarUiState.kt +++ b/examples/multi/navbar/src/main/kotlin/com/motorro/commonstatemachine/examples/navbar/model/data/NavbarUiState.kt @@ -11,12 +11,12 @@ * limitations under the License. */ -package com.motorro.statemachine.navbar.model.data +package com.motorro.commonstatemachine.examples.navbar.model.data import androidx.compose.runtime.Immutable +import com.motorro.commonstatemachine.examples.timer.data.TimerKey +import com.motorro.commonstatemachine.examples.timer.data.TimerUiState import com.motorro.commonstatemachine.multi.MachineKey -import com.motorro.statemachine.timer.data.TimerKey -import com.motorro.statemachine.timer.data.TimerUiState /** * Common state for all active machines diff --git a/examples/multi/navbar/src/main/java/com/motorro/statemachine/navbar/model/state/NavbarState.kt b/examples/multi/navbar/src/main/kotlin/com/motorro/commonstatemachine/examples/navbar/model/state/NavbarState.kt similarity index 86% rename from examples/multi/navbar/src/main/java/com/motorro/statemachine/navbar/model/state/NavbarState.kt rename to examples/multi/navbar/src/main/kotlin/com/motorro/commonstatemachine/examples/navbar/model/state/NavbarState.kt index 3e27426..38c631e 100644 --- a/examples/multi/navbar/src/main/java/com/motorro/statemachine/navbar/model/state/NavbarState.kt +++ b/examples/multi/navbar/src/main/kotlin/com/motorro/commonstatemachine/examples/navbar/model/state/NavbarState.kt @@ -11,9 +11,16 @@ * limitations under the License. */ -package com.motorro.statemachine.navbar.model.state +package com.motorro.commonstatemachine.examples.navbar.model.state import com.motorro.commonstatemachine.CommonMachineState +import com.motorro.commonstatemachine.examples.commoncore.log.Logger +import com.motorro.commonstatemachine.examples.navbar.model.data.NavbarGesture +import com.motorro.commonstatemachine.examples.navbar.model.data.NavbarUiState +import com.motorro.commonstatemachine.examples.timer.data.TimerGesture +import com.motorro.commonstatemachine.examples.timer.data.TimerKey +import com.motorro.commonstatemachine.examples.timer.data.TimerUiState +import com.motorro.commonstatemachine.examples.timer.state.TimerState import com.motorro.commonstatemachine.lifecycle.MachineLifecycle import com.motorro.commonstatemachine.multi.ActiveMachineContainer import com.motorro.commonstatemachine.multi.GestureProcessor @@ -22,13 +29,6 @@ import com.motorro.commonstatemachine.multi.MachineKey import com.motorro.commonstatemachine.multi.MultiMachineState import com.motorro.commonstatemachine.multi.ProxyMachineContainer import com.motorro.commonstatemachine.multi.UiStateProvider -import com.motorro.statemachine.commoncore.log.Logger -import com.motorro.statemachine.navbar.model.data.NavbarGesture -import com.motorro.statemachine.navbar.model.data.NavbarUiState -import com.motorro.statemachine.timer.data.TimerGesture -import com.motorro.statemachine.timer.data.TimerKey -import com.motorro.statemachine.timer.data.TimerUiState -import com.motorro.statemachine.timer.state.TimerState import kotlin.time.Duration /** diff --git a/examples/multi/parallel/build.gradle.kts b/examples/multi/parallel/build.gradle.kts index 9754e7d..e3295b6 100644 --- a/examples/multi/parallel/build.gradle.kts +++ b/examples/multi/parallel/build.gradle.kts @@ -1,8 +1,3 @@ -@file:Suppress("unused") - -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - - /* * Copyright 2023 Nikolai Kotchetkov. * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,92 +12,12 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget */ plugins { - alias(libs.plugins.android.app) - alias(libs.plugins.compose) -} - -val versionCode: String by project.extra -val versionName: String by project.extra -val androidMinSdkVersion: Int by project.extra -val androidTargetSdkVersion: Int by project.extra -val androidCompileSdkVersion: Int by project.extra - -android { - namespace = "com.motorro.statemachine.multi.parallel" - compileSdk = androidCompileSdkVersion - - defaultConfig { - applicationId = "com.motorro.statemachine.multi.parallel" - 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_21 - targetCompatibility = JavaVersion.VERSION_21 - } - kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - } - buildFeatures { - compose = true - } - packaging { - resources { - excludes += "/META-INF/{AL2.0,LGPL2.1}" - } - } + alias(libs.plugins.motorro.android.app) } dependencies { implementation(project(":commonstatemachine")) implementation(project(":coroutines")) implementation(project(":examples:commoncore")) - implementation(project(":examples:androidcore")) implementation(project(":examples:timer")) - - 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.coroutines.core) - implementation(libs.kotlin.coroutines.android) - - val composeBom = platform(libs.compose.bom) - implementation(composeBom) - androidTestImplementation(composeBom) - - implementation(libs.bundles.compose.core) - implementation(libs.compose.activity) - implementation(libs.compose.viewmodel) - implementation(libs.compose.foundation) - implementation(libs.compose.foundation.layouts) - - debugImplementation(libs.compose.tooling) - - testImplementation(libs.bundles.test.core) - testImplementation(libs.test.androidx.arch) - testImplementation(libs.test.kotlin.coroutines) } \ No newline at end of file diff --git a/examples/multi/parallel/src/main/AndroidManifest.xml b/examples/multi/parallel/src/main/AndroidManifest.xml index a87bdae..d3ff5fc 100644 --- a/examples/multi/parallel/src/main/AndroidManifest.xml +++ b/examples/multi/parallel/src/main/AndroidManifest.xml @@ -21,10 +21,10 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:name="com.motorro.statemachine.parallel.App" + android:name="com.motorro.commonstatemachine.examples.parallel.App" android:theme="@style/Theme.CommonStateMachine"> diff --git a/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/state/MainStateFactory.kt b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/state/MainStateFactory.kt similarity index 93% rename from examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/state/MainStateFactory.kt rename to examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/state/MainStateFactory.kt index 300519b..5b5d987 100644 --- a/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/state/MainStateFactory.kt +++ b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/state/MainStateFactory.kt @@ -11,10 +11,10 @@ * limitations under the License. */ -package com.motorro.statemachine.skills.app.state +package com.motorro.commonstatemachine.examples.skills.app.state +import com.motorro.commonstatemachine.examples.skills.app.ui.MainUiRenderer import com.motorro.commonstatemachine.flow.data.CommonFlowHost -import com.motorro.statemachine.skills.app.ui.MainUiRenderer import org.koin.core.annotation.Factory import org.koin.core.annotation.InjectedParam diff --git a/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/ui/MainScreen.kt b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/ui/MainScreen.kt similarity index 85% rename from examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/ui/MainScreen.kt rename to examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/ui/MainScreen.kt index d316e3e..566c749 100644 --- a/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/ui/MainScreen.kt +++ b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/ui/MainScreen.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.skills.app.ui +package com.motorro.commonstatemachine.examples.skills.app.ui import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Box @@ -27,15 +27,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import com.motorro.commonstatemachine.examples.auth.appcore.ui.design.SkillsAppBar +import com.motorro.commonstatemachine.examples.auth.appcore.ui.design.SkillsLoading +import com.motorro.commonstatemachine.examples.skills.app.MainViewModel +import com.motorro.commonstatemachine.examples.skills.app.R +import com.motorro.commonstatemachine.examples.skills.app.data.MainGesture +import com.motorro.commonstatemachine.examples.skills.app.data.MainUiState +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthGesture +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthUiApi import com.motorro.commonstatemachine.flow.viewmodel.CommonFlowComposition -import com.motorro.statemachine.auth.appcore.ui.design.SkillsAppBar -import com.motorro.statemachine.auth.appcore.ui.design.SkillsLoading -import com.motorro.statemachine.skills.app.MainViewModel -import com.motorro.statemachine.skills.app.R -import com.motorro.statemachine.skills.app.data.MainGesture -import com.motorro.statemachine.skills.app.data.MainUiState -import com.motorro.statemachine.skills.auth.api.AuthGesture -import com.motorro.statemachine.skills.auth.api.AuthUiApi import io.github.aakira.napier.Napier import org.koin.compose.koinInject import org.koin.compose.viewmodel.koinViewModel diff --git a/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/ui/MainUiRenderer.kt b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/ui/MainUiRenderer.kt similarity index 70% rename from examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/ui/MainUiRenderer.kt rename to examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/ui/MainUiRenderer.kt index 8a90ace..97c4bf4 100644 --- a/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/ui/MainUiRenderer.kt +++ b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/ui/MainUiRenderer.kt @@ -1,7 +1,7 @@ -package com.motorro.statemachine.skills.app.ui +package com.motorro.commonstatemachine.examples.skills.app.ui -import com.motorro.statemachine.skills.app.data.MainUiState -import com.motorro.statemachine.skills.auth.api.AuthUiState +import com.motorro.commonstatemachine.examples.skills.app.data.MainUiState +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthUiState import org.koin.core.annotation.Factory /** diff --git a/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/MainModule.kt b/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/MainModule.kt deleted file mode 100644 index 2375536..0000000 --- a/examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app/MainModule.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.motorro.statemachine.skills.app - -import org.koin.core.annotation.ComponentScan -import org.koin.core.annotation.Module - -@Module -@ComponentScan("com.motorro.statemachine.skills.app") -internal class MainModule \ No newline at end of file diff --git a/examples/skills/app/src/test/kotlin/com/motorro/statemachine/skills/app/state/BaseStateTest.kt b/examples/skills/app/src/test/kotlin/com/motorro/commonstatemachine/examples/skills/app/state/BaseStateTest.kt similarity index 86% rename from examples/skills/app/src/test/kotlin/com/motorro/statemachine/skills/app/state/BaseStateTest.kt rename to examples/skills/app/src/test/kotlin/com/motorro/commonstatemachine/examples/skills/app/state/BaseStateTest.kt index 8896302..7f686a3 100644 --- a/examples/skills/app/src/test/kotlin/com/motorro/statemachine/skills/app/state/BaseStateTest.kt +++ b/examples/skills/app/src/test/kotlin/com/motorro/commonstatemachine/examples/skills/app/state/BaseStateTest.kt @@ -1,10 +1,10 @@ -package com.motorro.statemachine.skills.app.state +package com.motorro.commonstatemachine.examples.skills.app.state import com.motorro.commonstatemachine.CommonStateMachine +import com.motorro.commonstatemachine.examples.skills.app.data.MainGesture +import com.motorro.commonstatemachine.examples.skills.app.data.MainUiState +import com.motorro.commonstatemachine.examples.skills.app.ui.MainUiRenderer import com.motorro.commonstatemachine.flow.data.CommonFlowHost -import com.motorro.statemachine.skills.app.data.MainGesture -import com.motorro.statemachine.skills.app.data.MainUiState -import com.motorro.statemachine.skills.app.ui.MainUiRenderer import io.mockk.mockk import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi diff --git a/examples/skills/app/src/test/kotlin/com/motorro/statemachine/skills/app/state/CheckingAuthenticationTest.kt b/examples/skills/app/src/test/kotlin/com/motorro/commonstatemachine/examples/skills/app/state/CheckingAuthenticationTest.kt similarity index 93% rename from examples/skills/app/src/test/kotlin/com/motorro/statemachine/skills/app/state/CheckingAuthenticationTest.kt rename to examples/skills/app/src/test/kotlin/com/motorro/commonstatemachine/examples/skills/app/state/CheckingAuthenticationTest.kt index f27355d..8aae00c 100644 --- a/examples/skills/app/src/test/kotlin/com/motorro/statemachine/skills/app/state/CheckingAuthenticationTest.kt +++ b/examples/skills/app/src/test/kotlin/com/motorro/commonstatemachine/examples/skills/app/state/CheckingAuthenticationTest.kt @@ -1,10 +1,10 @@ -package com.motorro.statemachine.skills.app.state +package com.motorro.commonstatemachine.examples.skills.app.state +import com.motorro.commonstatemachine.examples.skills.app.data.MainGesture +import com.motorro.commonstatemachine.examples.skills.app.data.MainUiState import com.motorro.commonstatemachine.skills.domain.session.SessionManager import com.motorro.commonstatemachine.skills.domain.session.data.Session import com.motorro.commonstatemachine.skills.domain.session.data.Username -import com.motorro.statemachine.skills.app.data.MainGesture -import com.motorro.statemachine.skills.app.data.MainUiState import io.mockk.every import io.mockk.mockk import io.mockk.verify diff --git a/examples/skills/appcore/build.gradle.kts b/examples/skills/appcore/build.gradle.kts index 37eb812..f509912 100644 --- a/examples/skills/appcore/build.gradle.kts +++ b/examples/skills/appcore/build.gradle.kts @@ -1,5 +1,3 @@ -@file:Suppress("unused") - /* * Copyright 2023 Nikolai Kotchetkov. * Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,68 +10,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -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.motorro.cmp) } -val versionName: String by project.extra -val androidMinSdkVersion: Int by project.extra -val androidTargetSdkVersion: Int by project.extra -val androidCompileSdkVersion: Int by project.extra - kotlin { - jvmToolchain(21) - - compilerOptions.freeCompilerArgs.addAll(listOf( - "-opt-in=kotlin.RequiresOptIn", - "-Xexpect-actual-classes", - "-Xexplicit-backing-fields", - "-Xcontext-parameters" - )) - - android { - namespace = "com.motorro.statemachine.skills.appcore" - compileSdk = androidCompileSdkVersion - minSdk = androidMinSdkVersion - - withHostTest { - isIncludeAndroidResources = true - } - - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - - androidResources.enable = true - } - sourceSets { commonMain.dependencies { - api(libs.composeMultiplatform.runtime) - api(libs.composeMultiplatform.foundation) - api(libs.composeMultiplatform.ui) - api(libs.composeMultiplatform.resources) - api(libs.composeMultiplatform.material3) - api(libs.composeMultiplatform.icons) - implementation(libs.composeMultiplatform.navigationevent) - implementation(libs.composeMultiplatform.preview) + api(libs.compose.multiplatform.icons) + implementation(libs.compose.multiplatform.navigationevent) + implementation(libs.compose.multiplatform.preview) } } } -dependencies { - androidRuntimeClasspath(libs.composeMultiplatform.tooling) -} - -compose.resources { - publicResClass = false - packageOfResClass = "com.motorro.statemachine.skills.appcore" - generateResClass = always -} - diff --git a/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/design/SkillsAppBar.kt b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/design/SkillsAppBar.kt similarity index 91% rename from examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/design/SkillsAppBar.kt rename to examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/design/SkillsAppBar.kt index e09124c..e2223e4 100644 --- a/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/design/SkillsAppBar.kt +++ b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/design/SkillsAppBar.kt @@ -1,4 +1,4 @@ -package com.motorro.statemachine.auth.appcore.ui.design +package com.motorro.commonstatemachine.examples.auth.appcore.ui.design import androidx.compose.foundation.layout.RowScope import androidx.compose.material.icons.Icons @@ -19,9 +19,9 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.navigationevent.NavigationEventInfo import androidx.navigationevent.compose.NavigationBackHandler import androidx.navigationevent.compose.rememberNavigationEventState -import com.motorro.statemachine.auth.appcore.ui.preview.SkillsPreviewComposition -import com.motorro.statemachine.skills.appcore.Res -import com.motorro.statemachine.skills.appcore.btn_back +import com.motorro.commonstatemachine.examples.auth.appcore.ui.preview.SkillsPreviewComposition +import com.motorro.commonstatemachine.examples.skills.appcore.Res +import com.motorro.commonstatemachine.examples.skills.appcore.btn_back import org.jetbrains.compose.resources.stringResource /** diff --git a/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/design/SkillsLoading.kt b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/design/SkillsLoading.kt similarity index 86% rename from examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/design/SkillsLoading.kt rename to examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/design/SkillsLoading.kt index 41187e2..fe1010f 100644 --- a/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/design/SkillsLoading.kt +++ b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/design/SkillsLoading.kt @@ -1,4 +1,4 @@ -package com.motorro.statemachine.auth.appcore.ui.design +package com.motorro.commonstatemachine.examples.auth.appcore.ui.design import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize diff --git a/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/preview/SkillsPreviewComposition.kt b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/preview/SkillsPreviewComposition.kt similarity index 84% rename from examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/preview/SkillsPreviewComposition.kt rename to examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/preview/SkillsPreviewComposition.kt index c1f4d34..9f2a203 100644 --- a/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/preview/SkillsPreviewComposition.kt +++ b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/preview/SkillsPreviewComposition.kt @@ -1,4 +1,4 @@ -package com.motorro.statemachine.auth.appcore.ui.preview +package com.motorro.commonstatemachine.examples.auth.appcore.ui.preview import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -6,7 +6,7 @@ import androidx.compose.runtime.remember import androidx.navigationevent.NavigationEventDispatcher import androidx.navigationevent.NavigationEventDispatcherOwner import androidx.navigationevent.compose.LocalNavigationEventDispatcherOwner -import com.motorro.statemachine.auth.appcore.ui.theme.SkillsTheme +import com.motorro.commonstatemachine.examples.auth.appcore.ui.theme.SkillsTheme /** * App preview with all the required locals and theming diff --git a/examples/androidcore/src/main/java/com/motorro/statemachine/androidcore/ui/theme/Color.kt b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/theme/Color.kt similarity index 77% rename from examples/androidcore/src/main/java/com/motorro/statemachine/androidcore/ui/theme/Color.kt rename to examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/theme/Color.kt index 6e5361e..421d7fc 100644 --- a/examples/androidcore/src/main/java/com/motorro/statemachine/androidcore/ui/theme/Color.kt +++ b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/theme/Color.kt @@ -1,4 +1,4 @@ -package com.motorro.statemachine.androidcore.ui.theme +package com.motorro.commonstatemachine.examples.auth.appcore.ui.theme import androidx.compose.ui.graphics.Color diff --git a/examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/theme/Dimensions.kt b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/theme/Dimensions.kt new file mode 100644 index 0000000..6e8ceaf --- /dev/null +++ b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/theme/Dimensions.kt @@ -0,0 +1,34 @@ +package com.motorro.commonstatemachine.examples.auth.appcore.ui.theme + +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * Common dimensions for the application + */ +data object Dimensions { + /** + * Extra small padding: 4dp + */ + val extraSmall: Dp = 4.dp + + /** + * Small padding: 8dp + */ + val small: Dp = 8.dp + + /** + * Medium padding: 16dp + */ + val medium: Dp = 16.dp + + /** + * Large padding: 24dp + */ + val large: Dp = 24.dp + + /** + * Extra large padding: 32dp + */ + val extraLarge: Dp = 32.dp +} diff --git a/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/theme/SkillsTheme.kt b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/theme/SkillsTheme.kt similarity index 92% rename from examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/theme/SkillsTheme.kt rename to examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/theme/SkillsTheme.kt index 5be08ff..3631d0e 100644 --- a/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/theme/SkillsTheme.kt +++ b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/theme/SkillsTheme.kt @@ -1,4 +1,4 @@ -package com.motorro.statemachine.auth.appcore.ui.theme +package com.motorro.commonstatemachine.examples.auth.appcore.ui.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme diff --git a/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/theme/Type.kt b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/theme/Type.kt similarity index 88% rename from examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/theme/Type.kt rename to examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/theme/Type.kt index 41a907d..2ac1237 100644 --- a/examples/skills/appcore/src/commonMain/kotlin/com/motorro/statemachine/auth/appcore/ui/theme/Type.kt +++ b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/theme/Type.kt @@ -1,4 +1,4 @@ -package com.motorro.statemachine.common.ui.theme +package com.motorro.commonstatemachine.examples.common.ui.theme import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle diff --git a/examples/skills/auth/api/build.gradle.kts b/examples/skills/auth/api/build.gradle.kts index 9cbb99c..be1f8cc 100644 --- a/examples/skills/auth/api/build.gradle.kts +++ b/examples/skills/auth/api/build.gradle.kts @@ -1,5 +1,3 @@ -@file:Suppress("unused") - /* * Copyright 2023 Nikolai Kotchetkov. * Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,44 +10,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -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.motorro.cmp) } -val versionName: String by project.extra -val androidMinSdkVersion: Int by project.extra -val androidTargetSdkVersion: Int by project.extra -val androidCompileSdkVersion: Int by project.extra - kotlin { - jvmToolchain(21) - - compilerOptions.freeCompilerArgs.addAll(listOf( - "-opt-in=kotlin.RequiresOptIn", - "-Xexpect-actual-classes", - "-Xexplicit-backing-fields", - "-Xcontext-parameters" - )) - - android { - namespace = "com.motorro.statemachine.skills.auth.api" - compileSdk = androidCompileSdkVersion - minSdk = androidMinSdkVersion - - withHostTest { - isIncludeAndroidResources = true - } - - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - } - sourceSets { commonMain.dependencies { implementation(project(":commonstatemachine")) @@ -57,8 +23,6 @@ kotlin { implementation(project(":commonflow:commonflow-data")) implementation(project(":commonflow:commonflow-compose")) implementation(libs.kotlin.coroutines.core) - implementation(libs.composeMultiplatform.runtime) - implementation(libs.composeMultiplatform.foundation) } } } diff --git a/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthDataApi.kt b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api/AuthDataApi.kt similarity index 80% rename from examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthDataApi.kt rename to examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api/AuthDataApi.kt index 460e5b6..e03a962 100644 --- a/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthDataApi.kt +++ b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api/AuthDataApi.kt @@ -1,4 +1,4 @@ -package com.motorro.statemachine.skills.auth.api +package com.motorro.commonstatemachine.examples.skills.auth.api import com.motorro.commonstatemachine.flow.data.CommonFlowDataApi /** diff --git a/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthGesture.kt b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api/AuthGesture.kt similarity index 63% rename from examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthGesture.kt rename to examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api/AuthGesture.kt index 6f51886..b93d81e 100644 --- a/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthGesture.kt +++ b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api/AuthGesture.kt @@ -1,4 +1,4 @@ -package com.motorro.statemachine.skills.auth.api +package com.motorro.commonstatemachine.examples.skills.auth.api /** * A base type for authentication user gestures diff --git a/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthInput.kt b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api/AuthInput.kt similarity index 71% rename from examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthInput.kt rename to examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api/AuthInput.kt index 9631802..90aba9d 100644 --- a/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthInput.kt +++ b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api/AuthInput.kt @@ -1,4 +1,4 @@ -package com.motorro.statemachine.skills.auth.api +package com.motorro.commonstatemachine.examples.skills.auth.api /** * Initializing data for the feature flow diff --git a/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthResult.kt b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api/AuthResult.kt similarity index 70% rename from examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthResult.kt rename to examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api/AuthResult.kt index 7023117..e57a379 100644 --- a/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthResult.kt +++ b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api/AuthResult.kt @@ -1,4 +1,4 @@ -package com.motorro.statemachine.skills.auth.api +package com.motorro.commonstatemachine.examples.skills.auth.api /** * Feature flow result diff --git a/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthUiApi.kt b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api/AuthUiApi.kt similarity index 78% rename from examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthUiApi.kt rename to examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api/AuthUiApi.kt index c7655f4..1663448 100644 --- a/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthUiApi.kt +++ b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api/AuthUiApi.kt @@ -1,4 +1,4 @@ -package com.motorro.statemachine.skills.auth.api +package com.motorro.commonstatemachine.examples.skills.auth.api import com.motorro.commonstatemachine.flow.compose.CommonFlowUiApi /** diff --git a/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthUiState.kt b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api/AuthUiState.kt similarity index 62% rename from examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthUiState.kt rename to examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api/AuthUiState.kt index edcb662..b16324d 100644 --- a/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/AuthUiState.kt +++ b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api/AuthUiState.kt @@ -1,4 +1,4 @@ -package com.motorro.statemachine.skills.auth.api +package com.motorro.commonstatemachine.examples.skills.auth.api /** * A base type for authentication UI state diff --git a/examples/skills/auth/demo/build.gradle.kts b/examples/skills/auth/demo/build.gradle.kts index 9be272f..0173a50 100644 --- a/examples/skills/auth/demo/build.gradle.kts +++ b/examples/skills/auth/demo/build.gradle.kts @@ -11,79 +11,11 @@ * 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 { - alias(libs.plugins.android.app) - alias(libs.plugins.compose) + alias(libs.plugins.motorro.android.app) alias(libs.plugins.koin) } -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.skills.auth.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_21 - targetCompatibility = JavaVersion.VERSION_21 - } - kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - } - buildFeatures { - compose = true - } - packaging { - resources { - excludes += "/META-INF/{AL2.0,LGPL2.1}" - } - } - namespace = "com.motorro.statemachine.skills.auth.demo" -} - dependencies { implementation(project(":examples:skills:appcore")) implementation(project(":examples:skills:domain")) @@ -95,27 +27,7 @@ dependencies { implementation(project(":commonflow:commonflow-compose")) implementation(project(":commonflow:commonflow-viewmodel")) - coreLibraryDesugaring(libs.desugaring) - implementation(libs.napier) - implementation(libs.androidx.core) - implementation(libs.androidx.activity) - 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) - - debugImplementation(libs.compose.tooling) implementation(platform(libs.koin.bom)) implementation(libs.koin.core) diff --git a/examples/skills/auth/demo/src/main/kotlin/com/motorro/statemachine/skills/auth/demo/App.kt b/examples/skills/auth/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/App.kt similarity index 95% rename from examples/skills/auth/demo/src/main/kotlin/com/motorro/statemachine/skills/auth/demo/App.kt rename to examples/skills/auth/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/App.kt index d96b7ed..a79d35e 100644 --- a/examples/skills/auth/demo/src/main/kotlin/com/motorro/statemachine/skills/auth/demo/App.kt +++ b/examples/skills/auth/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/App.kt @@ -1,13 +1,13 @@ -package com.motorro.statemachine.skills.auth.demo +package com.motorro.commonstatemachine.examples.skills.auth.demo import android.app.Application +import com.motorro.commonstatemachine.examples.skills.auth.implementation.AuthModule import com.motorro.commonstatemachine.skills.domain.authenticate.AuthenticateWithPassword import com.motorro.commonstatemachine.skills.domain.authenticate.GetPasswordRequirements import com.motorro.commonstatemachine.skills.domain.authenticate.data.PasswordRequirements import com.motorro.commonstatemachine.skills.domain.exception.AuthenticationException import com.motorro.commonstatemachine.skills.domain.exception.IOException import com.motorro.commonstatemachine.skills.domain.session.data.Username -import com.motorro.statemachine.skills.auth.implementation.AuthModule import io.github.aakira.napier.DebugAntilog import io.github.aakira.napier.Napier import kotlinx.coroutines.delay diff --git a/examples/skills/auth/demo/src/main/kotlin/com/motorro/statemachine/skills/auth/demo/MainActivity.kt b/examples/skills/auth/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/MainActivity.kt similarity index 86% rename from examples/skills/auth/demo/src/main/kotlin/com/motorro/statemachine/skills/auth/demo/MainActivity.kt rename to examples/skills/auth/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/MainActivity.kt index ba1da2e..9f986c2 100644 --- a/examples/skills/auth/demo/src/main/kotlin/com/motorro/statemachine/skills/auth/demo/MainActivity.kt +++ b/examples/skills/auth/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/MainActivity.kt @@ -1,4 +1,4 @@ -package com.motorro.statemachine.skills.auth.demo +package com.motorro.commonstatemachine.examples.skills.auth.demo import android.os.Bundle import androidx.activity.ComponentActivity @@ -11,10 +11,10 @@ import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import com.motorro.commonstatemachine.examples.auth.appcore.ui.design.SkillsAppBar +import com.motorro.commonstatemachine.examples.auth.appcore.ui.theme.SkillsTheme +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthUiApi import com.motorro.commonstatemachine.flow.viewmodel.CommonFlowComposition -import com.motorro.statemachine.auth.appcore.ui.design.SkillsAppBar -import com.motorro.statemachine.auth.appcore.ui.theme.SkillsTheme -import com.motorro.statemachine.skills.auth.api.AuthUiApi import io.github.aakira.napier.Napier import org.koin.compose.koinInject import org.koin.compose.viewmodel.koinViewModel diff --git a/examples/skills/auth/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/MainViewModel.kt b/examples/skills/auth/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/MainViewModel.kt new file mode 100644 index 0000000..88d2fcd --- /dev/null +++ b/examples/skills/auth/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/MainViewModel.kt @@ -0,0 +1,15 @@ +package com.motorro.commonstatemachine.examples.skills.auth.demo + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthDataApi +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthGesture +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthInput +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthUiState +import com.motorro.commonstatemachine.flow.viewmodel.CommonFlowViewModel +import org.koin.core.annotation.KoinViewModel + +@KoinViewModel +class MainViewModel (api: AuthDataApi) : CommonFlowViewModel( + api = api, + init = AuthInput(skippable = true) +) \ No newline at end of file diff --git a/examples/skills/auth/demo/src/main/kotlin/com/motorro/statemachine/skills/auth/demo/MainViewModel.kt b/examples/skills/auth/demo/src/main/kotlin/com/motorro/statemachine/skills/auth/demo/MainViewModel.kt deleted file mode 100644 index ad16260..0000000 --- a/examples/skills/auth/demo/src/main/kotlin/com/motorro/statemachine/skills/auth/demo/MainViewModel.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.motorro.statemachine.skills.auth.demo - -import com.motorro.commonstatemachine.flow.viewmodel.CommonFlowViewModel -import com.motorro.statemachine.skills.auth.api.AuthDataApi -import com.motorro.statemachine.skills.auth.api.AuthGesture -import com.motorro.statemachine.skills.auth.api.AuthInput -import com.motorro.statemachine.skills.auth.api.AuthResult -import com.motorro.statemachine.skills.auth.api.AuthUiState -import org.koin.core.annotation.KoinViewModel - -@KoinViewModel -class MainViewModel (api: AuthDataApi) : CommonFlowViewModel( - api = api, - init = AuthInput(skippable = true) -) \ No newline at end of file diff --git a/examples/skills/auth/implementation/build.gradle.kts b/examples/skills/auth/implementation/build.gradle.kts index 571e14c..ec5590b 100644 --- a/examples/skills/auth/implementation/build.gradle.kts +++ b/examples/skills/auth/implementation/build.gradle.kts @@ -1,5 +1,3 @@ -@file:Suppress("unused") - /* * Copyright 2023 Nikolai Kotchetkov. * Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,48 +10,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -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.motorro.cmp) alias(libs.plugins.mockery) alias(libs.plugins.koin) } -val versionName: String by project.extra -val androidMinSdkVersion: Int by project.extra -val androidTargetSdkVersion: Int by project.extra -val androidCompileSdkVersion: Int by project.extra - kotlin { - jvmToolchain(21) - - compilerOptions.freeCompilerArgs.addAll(listOf( - "-opt-in=kotlin.RequiresOptIn", - "-Xexpect-actual-classes", - "-Xexplicit-backing-fields", - "-Xcontext-parameters" - )) - - android { - namespace = "com.motorro.statemachine.skills.auth.implementation" - compileSdk = androidCompileSdkVersion - minSdk = androidMinSdkVersion - - withHostTest { - isIncludeAndroidResources = true - } - - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - - androidResources.enable = true - } - sourceSets { commonMain.dependencies { implementation(project(":commonstatemachine")) @@ -64,30 +28,15 @@ kotlin { implementation(project(":examples:skills:appcore")) implementation(project(":examples:skills:auth:api")) implementation(libs.kotlin.coroutines.core) - implementation(libs.composeMultiplatform.runtime) - implementation(libs.composeMultiplatform.foundation) - implementation(libs.composeMultiplatform.resources) - implementation(libs.composeMultiplatform.preview) implementation(project.dependencies.platform(libs.koin.bom)) implementation(libs.koin.core) implementation(libs.koin.annotations) implementation(libs.napier) } commonTest.dependencies { - implementation(libs.test.kotlin) implementation(libs.test.kotlin.coroutines) } } } -dependencies { - androidRuntimeClasspath(libs.composeMultiplatform.tooling) -} - -compose.resources { - publicResClass = false - packageOfResClass = "com.motorro.statemachine.skills.auth.implementation" - generateResClass = always -} - diff --git a/examples/skills/auth/implementation/src/commonMain/composeResources/values/strings.xml b/examples/skills/auth/implementation/src/commonMain/composeResources/values/strings.xml index ac3e2ab..f6e8ff6 100644 --- a/examples/skills/auth/implementation/src/commonMain/composeResources/values/strings.xml +++ b/examples/skills/auth/implementation/src/commonMain/composeResources/values/strings.xml @@ -1,3 +1,9 @@ Authentication + Username + Username + Login + Skip + Retry + Close diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/AuthDataApiImpl.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/AuthDataApiImpl.kt similarity index 55% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/AuthDataApiImpl.kt rename to examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/AuthDataApiImpl.kt index ed7cb85..aa90a5e 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/AuthDataApiImpl.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/AuthDataApiImpl.kt @@ -1,15 +1,15 @@ -package com.motorro.statemachine.skills.auth.implementation +package com.motorro.commonstatemachine.examples.skills.auth.implementation import com.motorro.commonstatemachine.CommonMachineState +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthDataApi +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthGesture +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthInput +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthUiState +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.state.AuthStateFactory import com.motorro.commonstatemachine.flow.data.CommonFlowHost -import com.motorro.statemachine.skills.auth.api.AuthDataApi -import com.motorro.statemachine.skills.auth.api.AuthGesture -import com.motorro.statemachine.skills.auth.api.AuthInput -import com.motorro.statemachine.skills.auth.api.AuthResult -import com.motorro.statemachine.skills.auth.api.AuthUiState -import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl -import com.motorro.statemachine.skills.auth.implementation.data.AuthUiStateImpl -import com.motorro.statemachine.skills.auth.implementation.state.AuthStateFactory import org.koin.core.annotation.Factory import org.koin.core.component.KoinComponent import org.koin.core.component.get diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/AuthModule.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/AuthModule.kt similarity index 62% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/AuthModule.kt rename to examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/AuthModule.kt index e9bc9a2..b646f31 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/AuthModule.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/AuthModule.kt @@ -1,4 +1,4 @@ -package com.motorro.statemachine.skills.auth.implementation +package com.motorro.commonstatemachine.examples.skills.auth.implementation import org.koin.core.annotation.ComponentScan import org.koin.core.annotation.Module diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/AuthUiApiImpl.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/AuthUiApiImpl.kt new file mode 100644 index 0000000..a0ccad4 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/AuthUiApiImpl.kt @@ -0,0 +1,27 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthGesture +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthUiApi +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthUiState +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.ui.AuthScreen +import org.koin.core.annotation.Factory + +/** + * UI API implementation + */ +@Factory +internal class AuthUiApiImpl : AuthUiApi { + @Composable + override fun Screen( + state: AuthUiState, + onGesture: (AuthGesture) -> Unit, + modifier: Modifier + ) = AuthScreen( + state = state as AuthUiStateImpl, + modifier = modifier, + onGesture = onGesture + ) +} \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthGestureImpl.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/AuthGestureImpl.kt similarity index 82% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthGestureImpl.kt rename to examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/AuthGestureImpl.kt index ad78fe6..ba1df8e 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthGestureImpl.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/AuthGestureImpl.kt @@ -1,6 +1,6 @@ -package com.motorro.statemachine.skills.auth.implementation.data +package com.motorro.commonstatemachine.examples.skills.auth.implementation.data -import com.motorro.statemachine.skills.auth.api.AuthGesture +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthGesture /** * All gestures available in this feature diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthStateData.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/AuthStateData.kt similarity index 83% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthStateData.kt rename to examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/AuthStateData.kt index 20420b8..d8cf559 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthStateData.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/AuthStateData.kt @@ -1,7 +1,7 @@ -package com.motorro.statemachine.skills.auth.implementation.data +package com.motorro.commonstatemachine.examples.skills.auth.implementation.data +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthInput import com.motorro.commonstatemachine.skills.domain.authenticate.data.PasswordRequirements -import com.motorro.statemachine.skills.auth.api.AuthInput /** * Interstate data to pass between the machine states diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthUiStateImpl.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/AuthUiStateImpl.kt similarity index 86% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthUiStateImpl.kt rename to examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/AuthUiStateImpl.kt index 3555158..5476939 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthUiStateImpl.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/AuthUiStateImpl.kt @@ -1,6 +1,6 @@ -package com.motorro.statemachine.skills.auth.implementation.data +package com.motorro.commonstatemachine.examples.skills.auth.implementation.data -import com.motorro.statemachine.skills.auth.api.AuthUiState +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthUiState /** * All gestures available in this feature diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthContext.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthContext.kt similarity index 59% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthContext.kt rename to examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthContext.kt index 5043041..21bc639 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthContext.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthContext.kt @@ -1,8 +1,8 @@ -package com.motorro.statemachine.skills.auth.implementation.state +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.ui.AuthUiRenderer import com.motorro.commonstatemachine.flow.data.CommonFlowHost -import com.motorro.statemachine.skills.auth.api.AuthResult -import com.motorro.statemachine.skills.auth.implementation.ui.AuthUiRenderer /** * Common tools used by states diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthState.kt new file mode 100644 index 0000000..bcf46b6 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthState.kt @@ -0,0 +1,10 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.CommonMachineState +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl + +/** + * Convenient type-alias - bounds gesture and ui state system + */ +internal typealias AuthState = CommonMachineState \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthStateFactory.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthStateFactory.kt similarity index 82% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthStateFactory.kt rename to examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthStateFactory.kt index ab0cdcb..5013652 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthStateFactory.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthStateFactory.kt @@ -1,9 +1,9 @@ -package com.motorro.statemachine.skills.auth.implementation.state +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthInput +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthStateData import com.motorro.commonstatemachine.skills.domain.exception.AppException -import com.motorro.statemachine.skills.auth.api.AuthInput -import com.motorro.statemachine.skills.auth.api.AuthResult -import com.motorro.statemachine.skills.auth.implementation.data.AuthStateData /** * State-factory interface diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthStateFactoryImpl.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthStateFactoryImpl.kt similarity index 83% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthStateFactoryImpl.kt rename to examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthStateFactoryImpl.kt index 181e191..35be8ff 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthStateFactoryImpl.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthStateFactoryImpl.kt @@ -1,11 +1,11 @@ -package com.motorro.statemachine.skills.auth.implementation.state +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthInput +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthStateData +import com.motorro.commonstatemachine.examples.skills.auth.implementation.ui.AuthUiRenderer import com.motorro.commonstatemachine.flow.data.CommonFlowHost import com.motorro.commonstatemachine.skills.domain.exception.AppException -import com.motorro.statemachine.skills.auth.api.AuthInput -import com.motorro.statemachine.skills.auth.api.AuthResult -import com.motorro.statemachine.skills.auth.implementation.data.AuthStateData -import com.motorro.statemachine.skills.auth.implementation.ui.AuthUiRenderer import io.github.aakira.napier.Napier import org.koin.core.annotation.Factory import org.koin.core.annotation.InjectedParam diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticatingState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthenticatingState.kt similarity index 86% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticatingState.kt rename to examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthenticatingState.kt index c917eb5..f337087 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticatingState.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthenticatingState.kt @@ -1,11 +1,11 @@ -package com.motorro.statemachine.skills.auth.implementation.state +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthStateData import com.motorro.commonstatemachine.skills.domain.authenticate.AuthenticateWithPassword import com.motorro.commonstatemachine.skills.domain.exception.toAppException import com.motorro.commonstatemachine.skills.domain.session.data.Username -import com.motorro.statemachine.skills.auth.api.AuthResult -import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl -import com.motorro.statemachine.skills.auth.implementation.data.AuthStateData import io.github.aakira.napier.Napier import kotlinx.coroutines.ensureActive import kotlinx.coroutines.launch diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticationErrorState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthenticationErrorState.kt similarity index 85% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticationErrorState.kt rename to examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthenticationErrorState.kt index 3313d1b..772a972 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticationErrorState.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthenticationErrorState.kt @@ -1,9 +1,9 @@ -package com.motorro.statemachine.skills.auth.implementation.state +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthStateData import com.motorro.commonstatemachine.skills.domain.exception.AppException import com.motorro.commonstatemachine.skills.domain.exception.IOException -import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl -import com.motorro.statemachine.skills.auth.implementation.data.AuthStateData import io.github.aakira.napier.Napier /** diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/BaseAuthState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/BaseAuthState.kt similarity index 71% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/BaseAuthState.kt rename to examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/BaseAuthState.kt index 337fc47..267cf74 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/BaseAuthState.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/BaseAuthState.kt @@ -1,9 +1,9 @@ -package com.motorro.statemachine.skills.auth.implementation.state +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state import com.motorro.commonstatemachine.coroutines.CoroutineState -import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl -import com.motorro.statemachine.skills.auth.implementation.data.AuthUiStateImpl -import com.motorro.statemachine.skills.auth.implementation.ui.AuthUiRenderer +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.ui.AuthUiRenderer import io.github.aakira.napier.Napier /** diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/FormState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/FormState.kt similarity index 80% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/FormState.kt rename to examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/FormState.kt index bab742b..c52effb 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/FormState.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/FormState.kt @@ -1,9 +1,9 @@ -package com.motorro.statemachine.skills.auth.implementation.state +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state -import com.motorro.statemachine.skills.auth.api.AuthResult -import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl -import com.motorro.statemachine.skills.auth.implementation.data.AuthStateData -import com.motorro.statemachine.skills.auth.implementation.data.isValidToAuthenticate +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthStateData +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.isValidToAuthenticate import io.github.aakira.napier.Napier import org.jetbrains.annotations.VisibleForTesting @@ -18,7 +18,7 @@ import org.jetbrains.annotations.VisibleForTesting */ internal class FormState( context: AuthContext, - @VisibleForTesting var data: AuthStateData, + @get:VisibleForTesting var data: AuthStateData, ) : BaseAuthState(context) { /** diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingErrorState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/PreloadingErrorState.kt similarity index 82% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingErrorState.kt rename to examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/PreloadingErrorState.kt index 8c9907b..61db5fc 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingErrorState.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/PreloadingErrorState.kt @@ -1,10 +1,10 @@ -package com.motorro.statemachine.skills.auth.implementation.state +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthInput +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl import com.motorro.commonstatemachine.skills.domain.exception.AppException import com.motorro.commonstatemachine.skills.domain.exception.IOException -import com.motorro.statemachine.skills.auth.api.AuthInput -import com.motorro.statemachine.skills.auth.api.AuthResult -import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl import io.github.aakira.napier.Napier /** diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/PreloadingState.kt similarity index 83% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingState.kt rename to examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/PreloadingState.kt index cd5bfb4..63dd138 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingState.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/PreloadingState.kt @@ -1,11 +1,11 @@ -package com.motorro.statemachine.skills.auth.implementation.state +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthInput +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthStateData import com.motorro.commonstatemachine.skills.domain.authenticate.GetPasswordRequirements import com.motorro.commonstatemachine.skills.domain.exception.toAppException -import com.motorro.statemachine.skills.auth.api.AuthInput -import com.motorro.statemachine.skills.auth.api.AuthResult -import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl -import com.motorro.statemachine.skills.auth.implementation.data.AuthStateData import io.github.aakira.napier.Napier import kotlinx.coroutines.ensureActive import kotlinx.coroutines.launch diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthErrorScreen.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthErrorScreen.kt similarity index 68% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthErrorScreen.kt rename to examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthErrorScreen.kt index 2dd2bce..fd8c8a1 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthErrorScreen.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthErrorScreen.kt @@ -1,4 +1,4 @@ -package com.motorro.statemachine.skills.auth.implementation.ui +package com.motorro.commonstatemachine.examples.skills.auth.implementation.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -15,10 +15,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import com.motorro.statemachine.auth.appcore.ui.preview.SkillsPreviewComposition -import com.motorro.statemachine.auth.appcore.ui.theme.Dimensions -import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl -import com.motorro.statemachine.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.commonstatemachine.examples.auth.appcore.ui.preview.SkillsPreviewComposition +import com.motorro.commonstatemachine.examples.auth.appcore.ui.theme.Dimensions +import com.motorro.commonstatemachine.examples.skills.auth.implementation.Res +import com.motorro.commonstatemachine.examples.skills.auth.implementation.btn_close +import com.motorro.commonstatemachine.examples.skills.auth.implementation.btn_retry +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl +import org.jetbrains.compose.resources.stringResource @Composable internal fun AuthErrorScreen( @@ -38,7 +42,13 @@ internal fun AuthErrorScreen( ) Spacer(Modifier.height(Dimensions.medium)) Button(onClick = { onGesture(AuthGestureImpl.Action) }) { - Text(if (state.canRetry) "Retry" else "Close") + Text( + if (state.canRetry) { + stringResource(Res.string.btn_retry) + } else { + stringResource(Res.string.btn_close) + } + ) } } } diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthFormScreen.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthFormScreen.kt similarity index 76% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthFormScreen.kt rename to examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthFormScreen.kt index a450b94..715a6e5 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthFormScreen.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthFormScreen.kt @@ -1,4 +1,4 @@ -package com.motorro.statemachine.skills.auth.implementation.ui +package com.motorro.commonstatemachine.examples.skills.auth.implementation.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -19,10 +19,16 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import com.motorro.statemachine.auth.appcore.ui.preview.SkillsPreviewComposition -import com.motorro.statemachine.auth.appcore.ui.theme.Dimensions -import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl -import com.motorro.statemachine.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.commonstatemachine.examples.auth.appcore.ui.preview.SkillsPreviewComposition +import com.motorro.commonstatemachine.examples.auth.appcore.ui.theme.Dimensions +import com.motorro.commonstatemachine.examples.skills.auth.implementation.Res +import com.motorro.commonstatemachine.examples.skills.auth.implementation.btn_login +import com.motorro.commonstatemachine.examples.skills.auth.implementation.btn_skip +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.input_password +import com.motorro.commonstatemachine.examples.skills.auth.implementation.input_username +import org.jetbrains.compose.resources.stringResource @Composable internal fun AuthFormScreen( @@ -38,7 +44,7 @@ internal fun AuthFormScreen( TextField( value = state.username, onValueChange = { onGesture(AuthGestureImpl.Form.UsernameChanged(it)) }, - label = { Text("Username") }, + label = { Text(stringResource(Res.string.input_username)) }, modifier = Modifier.fillMaxWidth(), singleLine = true ) @@ -46,7 +52,7 @@ internal fun AuthFormScreen( TextField( value = state.password, onValueChange = { onGesture(AuthGestureImpl.Form.PasswordChanged(it)) }, - label = { Text("Password") }, + label = { Text(stringResource(Res.string.input_password)) }, modifier = Modifier.fillMaxWidth(), visualTransformation = PasswordVisualTransformation(), singleLine = true @@ -63,7 +69,7 @@ internal fun AuthFormScreen( enabled = state.loginEnabled, modifier = Modifier.fillMaxWidth() ) { - Text("Login") + Text(stringResource(Res.string.btn_login)) } if (state.canSkip) { Spacer(Modifier.height(Dimensions.medium)) @@ -71,7 +77,7 @@ internal fun AuthFormScreen( onClick = { onGesture(AuthGestureImpl.Form.Skip) }, modifier = Modifier.fillMaxWidth() ) { - Text("Skip") + Text(stringResource(Res.string.btn_skip)) } } } diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthLoadingScreen.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthLoadingScreen.kt similarity index 62% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthLoadingScreen.kt rename to examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthLoadingScreen.kt index 490839c..90875c9 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthLoadingScreen.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthLoadingScreen.kt @@ -1,11 +1,11 @@ -package com.motorro.statemachine.skills.auth.implementation.ui +package com.motorro.commonstatemachine.examples.skills.auth.implementation.ui import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview -import com.motorro.statemachine.auth.appcore.ui.design.SkillsLoading -import com.motorro.statemachine.auth.appcore.ui.preview.SkillsPreviewComposition +import com.motorro.commonstatemachine.examples.auth.appcore.ui.design.SkillsLoading +import com.motorro.commonstatemachine.examples.auth.appcore.ui.preview.SkillsPreviewComposition @Composable internal fun AuthLoadingScreen(modifier: Modifier = Modifier) { diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthScreen.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthScreen.kt similarity index 83% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthScreen.kt rename to examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthScreen.kt index 57757b5..f2fbfd9 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthScreen.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthScreen.kt @@ -1,4 +1,4 @@ -package com.motorro.statemachine.skills.auth.implementation.ui +package com.motorro.commonstatemachine.examples.skills.auth.implementation.ui import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.ExperimentalMaterial3Api @@ -7,9 +7,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import com.motorro.statemachine.auth.appcore.ui.preview.SkillsPreviewComposition -import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl -import com.motorro.statemachine.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.commonstatemachine.examples.auth.appcore.ui.preview.SkillsPreviewComposition +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl /** * Authentication screen diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthUiRenderer.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthUiRenderer.kt similarity index 77% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthUiRenderer.kt rename to examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthUiRenderer.kt index 743c579..f117aba 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthUiRenderer.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthUiRenderer.kt @@ -1,8 +1,8 @@ -package com.motorro.statemachine.skills.auth.implementation.ui +package com.motorro.commonstatemachine.examples.skills.auth.implementation.ui +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthStateData +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl import com.motorro.commonstatemachine.skills.domain.exception.AppException -import com.motorro.statemachine.skills.auth.implementation.data.AuthStateData -import com.motorro.statemachine.skills.auth.implementation.data.AuthUiStateImpl /** * Transforms the data state to the UI state diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthUiRendererImpl.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthUiRendererImpl.kt similarity index 74% rename from examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthUiRendererImpl.kt rename to examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthUiRendererImpl.kt index c896a6d..8f4e0b7 100644 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthUiRendererImpl.kt +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthUiRendererImpl.kt @@ -1,8 +1,8 @@ -package com.motorro.statemachine.skills.auth.implementation.ui +package com.motorro.commonstatemachine.examples.skills.auth.implementation.ui +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthStateData +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl import com.motorro.commonstatemachine.skills.domain.exception.AppException -import com.motorro.statemachine.skills.auth.implementation.data.AuthStateData -import com.motorro.statemachine.skills.auth.implementation.data.AuthUiStateImpl import org.koin.core.annotation.Factory /** diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/AuthUiApiImpl.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/AuthUiApiImpl.kt deleted file mode 100644 index edd1a19..0000000 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/AuthUiApiImpl.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.motorro.statemachine.skills.auth.implementation - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.motorro.statemachine.skills.auth.api.AuthGesture -import com.motorro.statemachine.skills.auth.api.AuthUiApi -import com.motorro.statemachine.skills.auth.api.AuthUiState -import com.motorro.statemachine.skills.auth.implementation.data.AuthUiStateImpl -import com.motorro.statemachine.skills.auth.implementation.ui.AuthScreen -import org.koin.core.annotation.Factory - -/** - * UI API implementation - */ -@Factory -internal class AuthUiApiImpl : AuthUiApi { - @Composable - override fun Screen( - state: AuthUiState, - onGesture: (AuthGesture) -> Unit, - modifier: Modifier - ) = AuthScreen( - state = state as AuthUiStateImpl, - modifier = modifier, - onGesture = onGesture - ) -} \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthState.kt deleted file mode 100644 index bc218c6..0000000 --- a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthState.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.motorro.statemachine.skills.auth.implementation.state - -import com.motorro.commonstatemachine.CommonMachineState -import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl -import com.motorro.statemachine.skills.auth.implementation.data.AuthUiStateImpl - -/** - * Convenient type-alias - bounds gesture and ui state system - */ -internal typealias AuthState = CommonMachineState \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthStateDataTest.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/AuthStateDataTest.kt similarity index 94% rename from examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthStateDataTest.kt rename to examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/AuthStateDataTest.kt index 2d0550e..a27db2f 100644 --- a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/AuthStateDataTest.kt +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/AuthStateDataTest.kt @@ -1,4 +1,4 @@ -package com.motorro.statemachine.skills.auth.implementation.data +package com.motorro.commonstatemachine.examples.skills.auth.implementation.data import kotlin.test.Test import kotlin.test.assertFalse diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/authGesturesFixtures.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/authGesturesFixtures.kt similarity index 74% rename from examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/authGesturesFixtures.kt rename to examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/authGesturesFixtures.kt index 9de6c57..45c6ec5 100644 --- a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/authGesturesFixtures.kt +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/authGesturesFixtures.kt @@ -1,4 +1,4 @@ -package com.motorro.statemachine.skills.auth.implementation.data +package com.motorro.commonstatemachine.examples.skills.auth.implementation.data /** * Username input changed diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/dataStatesFixtures.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/dataStatesFixtures.kt similarity index 80% rename from examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/dataStatesFixtures.kt rename to examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/dataStatesFixtures.kt index 3345a14..7b04bbf 100644 --- a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/dataStatesFixtures.kt +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/dataStatesFixtures.kt @@ -1,10 +1,10 @@ -package com.motorro.statemachine.skills.auth.implementation.data +package com.motorro.commonstatemachine.examples.skills.auth.implementation.data +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthInput +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult import com.motorro.commonstatemachine.skills.domain.authenticate.data.PasswordRequirements import com.motorro.commonstatemachine.skills.domain.exception.IOException import com.motorro.commonstatemachine.skills.domain.exception.UnknownException -import com.motorro.statemachine.skills.auth.api.AuthInput -import com.motorro.statemachine.skills.auth.api.AuthResult /** * Input fixture diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/uiStatesFixtures.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/uiStatesFixtures.kt new file mode 100644 index 0000000..14b7733 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/uiStatesFixtures.kt @@ -0,0 +1,6 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.data + +/** + * Some UI-state to return from [com.motorro.commonstatemachine.examples.skills.auth.implementation.ui.AuthUiRenderer] mock + */ +internal val UI_STATE = AuthUiStateImpl.Loading \ No newline at end of file diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticatingStateTest.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthenticatingStateTest.kt similarity index 81% rename from examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticatingStateTest.kt rename to examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthenticatingStateTest.kt index 5113873..5d5f135 100644 --- a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticatingStateTest.kt +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthenticatingStateTest.kt @@ -1,11 +1,11 @@ -package com.motorro.statemachine.skills.auth.implementation.state +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.FATAL_ERROR +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.UI_STATE +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.VALID_FORM_STATE import com.motorro.commonstatemachine.skills.domain.authenticate.AuthenticateWithPassword -import com.motorro.statemachine.skills.auth.api.AuthResult -import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl -import com.motorro.statemachine.skills.auth.implementation.data.FATAL_ERROR -import com.motorro.statemachine.skills.auth.implementation.data.UI_STATE -import com.motorro.statemachine.skills.auth.implementation.data.VALID_FORM_STATE import dev.mokkery.MockMode import dev.mokkery.answering.calls import dev.mokkery.answering.returns diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticationErrorStateTest.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthenticationErrorStateTest.kt similarity index 80% rename from examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticationErrorStateTest.kt rename to examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthenticationErrorStateTest.kt index 3348b37..163d205 100644 --- a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/AuthenticationErrorStateTest.kt +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthenticationErrorStateTest.kt @@ -1,11 +1,11 @@ -package com.motorro.statemachine.skills.auth.implementation.state +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.FATAL_ERROR +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.NON_FATAL_ERROR +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.UI_STATE +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.VALID_FORM_STATE import com.motorro.commonstatemachine.skills.domain.exception.AppException -import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl -import com.motorro.statemachine.skills.auth.implementation.data.FATAL_ERROR -import com.motorro.statemachine.skills.auth.implementation.data.NON_FATAL_ERROR -import com.motorro.statemachine.skills.auth.implementation.data.UI_STATE -import com.motorro.statemachine.skills.auth.implementation.data.VALID_FORM_STATE import dev.mokkery.answering.returns import dev.mokkery.every import dev.mokkery.matcher.any diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/BaseStateTest.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/BaseStateTest.kt similarity index 81% rename from examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/BaseStateTest.kt rename to examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/BaseStateTest.kt index 4f3f61a..1d4ca54 100644 --- a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/BaseStateTest.kt +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/BaseStateTest.kt @@ -1,11 +1,11 @@ -package com.motorro.statemachine.skills.auth.implementation.state +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state import com.motorro.commonstatemachine.CommonStateMachine +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.ui.AuthUiRenderer import com.motorro.commonstatemachine.flow.data.CommonFlowHost -import com.motorro.statemachine.skills.auth.api.AuthResult -import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl -import com.motorro.statemachine.skills.auth.implementation.data.AuthUiStateImpl -import com.motorro.statemachine.skills.auth.implementation.ui.AuthUiRenderer import dev.mokkery.MockMode import dev.mokkery.mock import kotlinx.coroutines.Dispatchers diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/FormStateTest.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/FormStateTest.kt similarity index 82% rename from examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/FormStateTest.kt rename to examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/FormStateTest.kt index fedaee8..80c6e7a 100644 --- a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/FormStateTest.kt +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/FormStateTest.kt @@ -1,12 +1,12 @@ -package com.motorro.statemachine.skills.auth.implementation.state - -import com.motorro.statemachine.skills.auth.api.AuthResult -import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl -import com.motorro.statemachine.skills.auth.implementation.data.EMPTY_STATE -import com.motorro.statemachine.skills.auth.implementation.data.PASSWORD_CHANGED -import com.motorro.statemachine.skills.auth.implementation.data.UI_STATE -import com.motorro.statemachine.skills.auth.implementation.data.USERNAME_CHANGED -import com.motorro.statemachine.skills.auth.implementation.data.VALID_FORM_STATE +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.EMPTY_STATE +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.PASSWORD_CHANGED +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.UI_STATE +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.USERNAME_CHANGED +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.VALID_FORM_STATE import dev.mokkery.answering.returns import dev.mokkery.every import dev.mokkery.matcher.any diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingErrorStateTest.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/PreloadingErrorStateTest.kt similarity index 79% rename from examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingErrorStateTest.kt rename to examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/PreloadingErrorStateTest.kt index f0e63be..cc51dc9 100644 --- a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingErrorStateTest.kt +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/PreloadingErrorStateTest.kt @@ -1,12 +1,12 @@ -package com.motorro.statemachine.skills.auth.implementation.state - +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.FATAL_ERROR +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.INPUT +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.NON_FATAL_ERROR +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.UI_STATE import com.motorro.commonstatemachine.skills.domain.exception.AppException -import com.motorro.statemachine.skills.auth.api.AuthResult -import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl -import com.motorro.statemachine.skills.auth.implementation.data.FATAL_ERROR -import com.motorro.statemachine.skills.auth.implementation.data.INPUT -import com.motorro.statemachine.skills.auth.implementation.data.NON_FATAL_ERROR -import com.motorro.statemachine.skills.auth.implementation.data.UI_STATE import dev.mokkery.answering.returns import dev.mokkery.every import dev.mokkery.matcher.any diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingStateTest.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/PreloadingStateTest.kt similarity index 78% rename from examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingStateTest.kt rename to examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/PreloadingStateTest.kt index e7b0d73..3017c63 100644 --- a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/state/PreloadingStateTest.kt +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/PreloadingStateTest.kt @@ -1,12 +1,12 @@ -package com.motorro.statemachine.skills.auth.implementation.state +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.FATAL_ERROR +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.INPUT +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.PASSWORD_REQUIREMENTS +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.UI_STATE import com.motorro.commonstatemachine.skills.domain.authenticate.GetPasswordRequirements -import com.motorro.statemachine.skills.auth.api.AuthResult -import com.motorro.statemachine.skills.auth.implementation.data.AuthGestureImpl -import com.motorro.statemachine.skills.auth.implementation.data.FATAL_ERROR -import com.motorro.statemachine.skills.auth.implementation.data.INPUT -import com.motorro.statemachine.skills.auth.implementation.data.PASSWORD_REQUIREMENTS -import com.motorro.statemachine.skills.auth.implementation.data.UI_STATE import dev.mokkery.answering.calls import dev.mokkery.answering.returns import dev.mokkery.answering.throws diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthUiRendererImplTest.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthUiRendererImplTest.kt similarity index 79% rename from examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthUiRendererImplTest.kt rename to examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthUiRendererImplTest.kt index 7ce0472..7e59579 100644 --- a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/ui/AuthUiRendererImplTest.kt +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthUiRendererImplTest.kt @@ -1,9 +1,9 @@ -package com.motorro.statemachine.skills.auth.implementation.ui +package com.motorro.commonstatemachine.examples.skills.auth.implementation.ui -import com.motorro.statemachine.skills.auth.implementation.data.AuthUiStateImpl -import com.motorro.statemachine.skills.auth.implementation.data.EMPTY_STATE -import com.motorro.statemachine.skills.auth.implementation.data.FATAL_ERROR -import com.motorro.statemachine.skills.auth.implementation.data.VALID_FORM_STATE +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.EMPTY_STATE +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.FATAL_ERROR +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.VALID_FORM_STATE import kotlin.test.Test import kotlin.test.assertEquals diff --git a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/uiStatesFixtures.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/uiStatesFixtures.kt deleted file mode 100644 index 4172d5c..0000000 --- a/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/statemachine/skills/auth/implementation/data/uiStatesFixtures.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.motorro.statemachine.skills.auth.implementation.data - -/** - * Some UI-state to return from [com.motorro.statemachine.skills.auth.implementation.ui.AuthUiRenderer] mock - */ -internal val UI_STATE = AuthUiStateImpl.Loading \ No newline at end of file diff --git a/examples/skills/domain/build.gradle.kts b/examples/skills/domain/build.gradle.kts index a47947e..d1b4748 100644 --- a/examples/skills/domain/build.gradle.kts +++ b/examples/skills/domain/build.gradle.kts @@ -1,5 +1,3 @@ -@file:Suppress("unused") - /* * Copyright 2023 Nikolai Kotchetkov. * Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,38 +10,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { - alias(libs.plugins.kotlin.multiplatform) - alias(libs.plugins.android.kotlin.multiplatform.library) + alias(libs.plugins.motorro.kmp) } -val versionName: String by project.extra -val androidMinSdkVersion: Int by project.extra -val androidTargetSdkVersion: Int by project.extra -val androidCompileSdkVersion: Int by project.extra - kotlin { - jvmToolchain(21) - - compilerOptions.freeCompilerArgs.addAll(listOf( - "-opt-in=kotlin.RequiresOptIn", - "-Xexpect-actual-classes", - "-Xexplicit-backing-fields", - "-Xcontext-parameters" - )) - - android { - namespace = "com.motorro.statemachine.skills.domain" - compileSdk = androidCompileSdkVersion - minSdk = androidMinSdkVersion - - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - } - sourceSets { commonMain.dependencies { api(libs.kotlin.coroutines.core) diff --git a/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/exception/AppException.kt b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/exception/AppException.kt index b1684b0..4812838 100644 --- a/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/exception/AppException.kt +++ b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/exception/AppException.kt @@ -9,7 +9,7 @@ abstract class AppException(final override val message: String, final override v override fun equals(other: Any?): Boolean { if (this === other) return true - if (javaClass != other?.javaClass) return false + if (other == null || this::class != other::class) return false other as AppException diff --git a/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/session/data/Username.kt b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/session/data/Username.kt index a3471c3..bcd923c 100644 --- a/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/session/data/Username.kt +++ b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/session/data/Username.kt @@ -1,4 +1,6 @@ package com.motorro.commonstatemachine.skills.domain.session.data +import kotlin.jvm.JvmInline + @JvmInline value class Username(val value: String) \ No newline at end of file diff --git a/examples/skills/usecase/build.gradle.kts b/examples/skills/usecase/build.gradle.kts index bb0e57f..5eaed0a 100644 --- a/examples/skills/usecase/build.gradle.kts +++ b/examples/skills/usecase/build.gradle.kts @@ -12,39 +12,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { - alias(libs.plugins.kotlin.multiplatform) - alias(libs.plugins.android.kotlin.multiplatform.library) + alias(libs.plugins.motorro.cmp) alias(libs.plugins.koin) } - -val versionName: String by project.extra -val androidMinSdkVersion: Int by project.extra -val androidTargetSdkVersion: Int by project.extra -val androidCompileSdkVersion: Int by project.extra - kotlin { - jvmToolchain(21) - - compilerOptions.freeCompilerArgs.addAll(listOf( - "-opt-in=kotlin.RequiresOptIn", - "-Xexpect-actual-classes", - "-Xexplicit-backing-fields", - "-Xcontext-parameters" - )) - - android { - namespace = "com.motorro.statemachine.skills.usecase" - compileSdk = androidCompileSdkVersion - minSdk = androidMinSdkVersion - - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - } - sourceSets { commonMain.dependencies { implementation(project(":examples:skills:domain")) diff --git a/examples/timer/build.gradle.kts b/examples/timer/build.gradle.kts index e4f7989..922a670 100644 --- a/examples/timer/build.gradle.kts +++ b/examples/timer/build.gradle.kts @@ -12,39 +12,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -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.motorro.cmp) } -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 = "com.motorro" -version = versionName - kotlin { - jvmToolchain(21) - - android { - namespace = "com.motorro.statemachine.timer" - compileSdk = androidCompileSdkVersion - minSdk = androidMinSdkVersion - - withHostTest { - isIncludeAndroidResources = true - } - - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - } - sourceSets { commonMain.dependencies { implementation(project(":commonstatemachine")) @@ -54,21 +27,8 @@ kotlin { implementation(libs.kotlin.datetime) } commonTest.dependencies { - implementation(libs.test.kotlin) implementation(libs.test.kotlin.coroutines) } - androidMain.dependencies { - implementation(libs.timber) - implementation(libs.kotlin.coroutines.android) - - implementation(project.dependencies.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) - } } } diff --git a/examples/timer/src/commonMain/kotlin/com/motorro/statemachine/timer/data/TimerGesture.kt b/examples/timer/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/timer/data/TimerGesture.kt similarity index 92% rename from examples/timer/src/commonMain/kotlin/com/motorro/statemachine/timer/data/TimerGesture.kt rename to examples/timer/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/timer/data/TimerGesture.kt index 7c0bd68..47adf0f 100644 --- a/examples/timer/src/commonMain/kotlin/com/motorro/statemachine/timer/data/TimerGesture.kt +++ b/examples/timer/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/timer/data/TimerGesture.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.timer.data +package com.motorro.commonstatemachine.examples.timer.data /** * Timer gesture diff --git a/examples/timer/src/commonMain/kotlin/com/motorro/statemachine/timer/data/TimerKey.kt b/examples/timer/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/timer/data/TimerKey.kt similarity index 93% rename from examples/timer/src/commonMain/kotlin/com/motorro/statemachine/timer/data/TimerKey.kt rename to examples/timer/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/timer/data/TimerKey.kt index 2ccbd91..9adbe00 100644 --- a/examples/timer/src/commonMain/kotlin/com/motorro/statemachine/timer/data/TimerKey.kt +++ b/examples/timer/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/timer/data/TimerKey.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.timer.data +package com.motorro.commonstatemachine.examples.timer.data import com.motorro.commonstatemachine.multi.MachineKey diff --git a/examples/timer/src/commonMain/kotlin/com/motorro/statemachine/timer/data/TimerUiState.kt b/examples/timer/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/timer/data/TimerUiState.kt similarity index 94% rename from examples/timer/src/commonMain/kotlin/com/motorro/statemachine/timer/data/TimerUiState.kt rename to examples/timer/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/timer/data/TimerUiState.kt index a1ff1d4..1bc6bd4 100644 --- a/examples/timer/src/commonMain/kotlin/com/motorro/statemachine/timer/data/TimerUiState.kt +++ b/examples/timer/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/timer/data/TimerUiState.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.timer.data +package com.motorro.commonstatemachine.examples.timer.data import kotlin.time.Duration diff --git a/examples/timer/src/commonMain/kotlin/com/motorro/statemachine/timer/state/TimerState.kt b/examples/timer/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/timer/state/TimerState.kt similarity index 93% rename from examples/timer/src/commonMain/kotlin/com/motorro/statemachine/timer/state/TimerState.kt rename to examples/timer/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/timer/state/TimerState.kt index 5e7cd4b..672b2a3 100644 --- a/examples/timer/src/commonMain/kotlin/com/motorro/statemachine/timer/state/TimerState.kt +++ b/examples/timer/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/timer/state/TimerState.kt @@ -11,14 +11,14 @@ * limitations under the License. */ -package com.motorro.statemachine.timer.state +package com.motorro.commonstatemachine.examples.timer.state import com.motorro.commonstatemachine.coroutines.CoroutineState import com.motorro.commonstatemachine.coroutines.lifecycle.asFlow +import com.motorro.commonstatemachine.examples.commoncore.log.Logger +import com.motorro.commonstatemachine.examples.timer.data.TimerGesture +import com.motorro.commonstatemachine.examples.timer.data.TimerUiState import com.motorro.commonstatemachine.lifecycle.MachineLifecycle -import com.motorro.statemachine.commoncore.log.Logger -import com.motorro.statemachine.timer.data.TimerGesture -import com.motorro.statemachine.timer.data.TimerUiState import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay diff --git a/examples/timer/src/androidMain/kotlin/com/motorro/statemachine/timer/ui/TimerScreen.kt b/examples/timer/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/timer/ui/TimerScreen.kt similarity index 91% rename from examples/timer/src/androidMain/kotlin/com/motorro/statemachine/timer/ui/TimerScreen.kt rename to examples/timer/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/timer/ui/TimerScreen.kt index 5416064..fd5e943 100644 --- a/examples/timer/src/androidMain/kotlin/com/motorro/statemachine/timer/ui/TimerScreen.kt +++ b/examples/timer/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/timer/ui/TimerScreen.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.timer.ui +package com.motorro.commonstatemachine.examples.timer.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -23,8 +23,8 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import com.motorro.statemachine.timer.data.TimerGesture -import com.motorro.statemachine.timer.data.TimerUiState +import com.motorro.commonstatemachine.examples.timer.data.TimerGesture +import com.motorro.commonstatemachine.examples.timer.data.TimerUiState import kotlin.time.Duration @Composable diff --git a/examples/timer/src/commonTest/kotlin/com/motorro/statemachine/timer/state/RunningTest.kt b/examples/timer/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/timer/state/RunningTest.kt similarity index 94% rename from examples/timer/src/commonTest/kotlin/com/motorro/statemachine/timer/state/RunningTest.kt rename to examples/timer/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/timer/state/RunningTest.kt index 90fdc1a..a269250 100644 --- a/examples/timer/src/commonTest/kotlin/com/motorro/statemachine/timer/state/RunningTest.kt +++ b/examples/timer/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/timer/state/RunningTest.kt @@ -11,12 +11,12 @@ * limitations under the License. */ -package com.motorro.statemachine.timer.state +package com.motorro.commonstatemachine.examples.timer.state +import com.motorro.commonstatemachine.examples.timer.data.TimerGesture +import com.motorro.commonstatemachine.examples.timer.data.TimerUiState import com.motorro.commonstatemachine.lifecycle.ActivatedMachineLifecycle import com.motorro.commonstatemachine.lifecycle.MachineLifecycle -import com.motorro.statemachine.timer.data.TimerGesture -import com.motorro.statemachine.timer.data.TimerUiState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher diff --git a/examples/timer/src/commonTest/kotlin/com/motorro/statemachine/timer/state/mocks.kt b/examples/timer/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/timer/state/mocks.kt similarity index 96% rename from examples/timer/src/commonTest/kotlin/com/motorro/statemachine/timer/state/mocks.kt rename to examples/timer/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/timer/state/mocks.kt index 9a68d43..fe6876c 100644 --- a/examples/timer/src/commonTest/kotlin/com/motorro/statemachine/timer/state/mocks.kt +++ b/examples/timer/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/timer/state/mocks.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.timer.state +package com.motorro.commonstatemachine.examples.timer.state import com.motorro.commonstatemachine.CommonMachineState import com.motorro.commonstatemachine.CommonStateMachine diff --git a/examples/androidcore/.gitignore b/examples/welcome/app/.gitignore similarity index 100% rename from examples/androidcore/.gitignore rename to examples/welcome/app/.gitignore diff --git a/examples/welcome/app/build.gradle.kts b/examples/welcome/app/build.gradle.kts new file mode 100644 index 0000000..601e2d5 --- /dev/null +++ b/examples/welcome/app/build.gradle.kts @@ -0,0 +1,34 @@ +/* + * 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 { + alias(libs.plugins.motorro.android.app) + alias(libs.plugins.koin) +} + +dependencies { + implementation(project(":commonstatemachine")) + implementation(project(":coroutines")) + implementation(project(":examples:welcome:commonapi")) + implementation(project(":examples:commoncore")) + implementation(project(":examples:welcome:login")) + implementation(project(":examples:welcome:register")) + + implementation(platform(libs.koin.bom)) + implementation(libs.koin.android) + implementation(libs.koin.compose) + implementation(libs.koin.compose.viewmodel) + implementation(libs.koin.annotations) + + testImplementation(libs.test.androidx.arch) +} diff --git a/examples/androidcore/proguard-rules.pro b/examples/welcome/app/proguard-rules.pro similarity index 100% rename from examples/androidcore/proguard-rules.pro rename to examples/welcome/app/proguard-rules.pro diff --git a/examples/welcome/welcome/src/main/AndroidManifest.xml b/examples/welcome/app/src/main/AndroidManifest.xml similarity index 94% rename from examples/welcome/welcome/src/main/AndroidManifest.xml rename to examples/welcome/app/src/main/AndroidManifest.xml index 3ddcc43..8ae827e 100644 --- a/examples/welcome/welcome/src/main/AndroidManifest.xml +++ b/examples/welcome/app/src/main/AndroidManifest.xml @@ -27,7 +27,7 @@ android:theme="@style/Theme.StateMachine" tools:targetApi="31"> diff --git a/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/App.kt b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/App.kt new file mode 100644 index 0000000..cd8ba1f --- /dev/null +++ b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/App.kt @@ -0,0 +1,46 @@ +/* + * 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.commonstatemachine.examples.welcome.app + +import android.app.Application +import com.motorro.commonstatemachine.examples.welcome.login.LoginModule +import com.motorro.commonstatemachine.examples.welcome.register.RegisterModule +import io.github.aakira.napier.DebugAntilog +import io.github.aakira.napier.Napier +import org.koin.android.ext.koin.androidContext +import org.koin.android.ext.koin.androidLogger +import org.koin.core.annotation.KoinApplication +import org.koin.plugin.module.dsl.startKoin + +@KoinApplication(modules = [ + RegisterModule::class, + LoginModule::class, + WelcomeModule::class, +]) +class App : Application() { + override fun onCreate() { + super.onCreate() + setupLogger() + startKoin { + allowOverride(false) + androidLogger() + androidContext(this@App) + } + Napier.d { "Started" } + } + + private fun setupLogger() { + Napier.base(DebugAntilog()) + } +} diff --git a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/MainActivity.kt b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/MainActivity.kt similarity index 80% rename from examples/welcome/welcome/src/main/java/com/motorro/statemachine/MainActivity.kt rename to examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/MainActivity.kt index ad608a6..8399b1b 100644 --- a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/MainActivity.kt +++ b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/MainActivity.kt @@ -11,26 +11,26 @@ * limitations under the License. */ -package com.motorro.statemachine +package com.motorro.commonstatemachine.examples.welcome.app import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Surface import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier -import androidx.core.view.WindowCompat -import com.motorro.statemachine.androidcore.ui.theme.CommonStateMachineTheme -import com.motorro.statemachine.welcome.view.WelcomeScreen -import dagger.hilt.android.AndroidEntryPoint -import timber.log.Timber +import com.motorro.commonstatemachine.examples.commoncore.ui.theme.CommonStateMachineTheme +import com.motorro.commonstatemachine.examples.welcome.app.view.WelcomeScreen +import io.github.aakira.napier.Napier + -@AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - WindowCompat.setDecorFitsSystemWindows(window, false) + enableEdgeToEdge() + setContent { CommonStateMachineTheme { Surface( @@ -50,7 +50,7 @@ class MainActivity : ComponentActivity() { } override fun finish() { - Timber.d("Activity finished") + Napier.d("Activity finished") super.finish() } } diff --git a/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/WelcomeModule.kt b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/WelcomeModule.kt new file mode 100644 index 0000000..64bd249 --- /dev/null +++ b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/WelcomeModule.kt @@ -0,0 +1,27 @@ +package com.motorro.commonstatemachine.examples.welcome.app + +import android.content.Context +import com.motorro.commonstatemachine.examples.commoncore.coroutines.DispatcherProvider +import com.motorro.commonstatemachine.examples.commoncore.resources.ResourceWrapper +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Factory +import org.koin.core.annotation.Module + +@Module +@ComponentScan +class WelcomeModule { + + @Factory + fun resourceWrapper(context: Context): ResourceWrapper = object : ResourceWrapper { + override fun getString(resId: Int, vararg args: Any): String = context.getString(resId, *args) + } + + @Factory + fun dispatchers(): DispatcherProvider = object : DispatcherProvider { + override val default: CoroutineDispatcher = Dispatchers.Default + override val main: CoroutineDispatcher = Dispatchers.Main + override val io: CoroutineDispatcher = Dispatchers.IO + } +} \ No newline at end of file diff --git a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/data/WelcomeDataState.kt b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/data/WelcomeDataState.kt similarity index 91% rename from examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/data/WelcomeDataState.kt rename to examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/data/WelcomeDataState.kt index 4866fb9..c3747e6 100644 --- a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/data/WelcomeDataState.kt +++ b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/data/WelcomeDataState.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.welcome.data +package com.motorro.commonstatemachine.examples.welcome.app.data /** * Welcome data state diff --git a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/data/WelcomeGesture.kt b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/data/WelcomeGesture.kt similarity index 85% rename from examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/data/WelcomeGesture.kt rename to examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/data/WelcomeGesture.kt index f08ba30..6b49154 100644 --- a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/data/WelcomeGesture.kt +++ b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/data/WelcomeGesture.kt @@ -11,10 +11,10 @@ * limitations under the License. */ -package com.motorro.statemachine.welcome.data +package com.motorro.commonstatemachine.examples.welcome.app.data -import com.motorro.statemachine.login.data.LoginGesture -import com.motorro.statemachine.register.data.RegisterGesture +import com.motorro.commonstatemachine.examples.welcome.login.data.LoginGesture +import com.motorro.commonstatemachine.examples.welcome.register.data.RegisterGesture /** * Welcome flow UI gestures diff --git a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/data/WelcomeUiState.kt b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/data/WelcomeUiState.kt similarity index 89% rename from examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/data/WelcomeUiState.kt rename to examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/data/WelcomeUiState.kt index 6569e8e..b4682ca 100644 --- a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/data/WelcomeUiState.kt +++ b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/data/WelcomeUiState.kt @@ -11,10 +11,10 @@ * limitations under the License. */ -package com.motorro.statemachine.welcome.data +package com.motorro.commonstatemachine.examples.welcome.app.data -import com.motorro.statemachine.login.data.LoginUiState -import com.motorro.statemachine.register.data.RegisterUiState +import com.motorro.commonstatemachine.examples.welcome.login.data.LoginUiState +import com.motorro.commonstatemachine.examples.welcome.register.data.RegisterUiState /** * Welcome flow UI state diff --git a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/WelcomeRenderer.kt b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/WelcomeRenderer.kt similarity index 77% rename from examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/WelcomeRenderer.kt rename to examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/WelcomeRenderer.kt index fb098b1..259086c 100644 --- a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/WelcomeRenderer.kt +++ b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/WelcomeRenderer.kt @@ -11,19 +11,18 @@ * limitations under the License. */ -package com.motorro.statemachine.welcome.model +package com.motorro.commonstatemachine.examples.welcome.app.model -import com.motorro.statemachine.commoncore.resources.ResourceWrapper -import com.motorro.statemachine.welcome.data.WelcomeDataState -import com.motorro.statemachine.welcome.data.WelcomeUiState -import dagger.hilt.android.scopes.ViewModelScoped -import javax.inject.Inject +import com.motorro.commonstatemachine.examples.commoncore.resources.ResourceWrapper +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeDataState +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeUiState +import org.koin.core.annotation.Factory /** * Welcome ui-state renderer */ -@ViewModelScoped -class WelcomeRenderer @Inject constructor(resourceWrapper: ResourceWrapper): ResourceWrapper by resourceWrapper { +@Factory +class WelcomeRenderer(resourceWrapper: ResourceWrapper): ResourceWrapper by resourceWrapper { /** * Renders preloading */ diff --git a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/WelcomeViewModel.kt b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/WelcomeViewModel.kt similarity index 52% rename from examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/WelcomeViewModel.kt rename to examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/WelcomeViewModel.kt index 8824fd6..d6744da 100644 --- a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/WelcomeViewModel.kt +++ b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/WelcomeViewModel.kt @@ -11,58 +11,36 @@ * limitations under the License. */ -package com.motorro.statemachine.welcome.model +package com.motorro.commonstatemachine.examples.welcome.app.model import androidx.lifecycle.ViewModel import com.motorro.commonstatemachine.CommonMachineState import com.motorro.commonstatemachine.coroutines.FlowStateMachine -import com.motorro.statemachine.welcome.data.WelcomeGesture -import com.motorro.statemachine.welcome.data.WelcomeUiState -import com.motorro.statemachine.welcome.model.state.WelcomeStateFactory -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.ExperimentalCoroutinesApi +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeGesture +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeUiState +import com.motorro.commonstatemachine.examples.welcome.app.model.state.WelcomeStateFactory +import io.github.aakira.napier.Napier import kotlinx.coroutines.flow.Flow -import timber.log.Timber -import javax.inject.Inject +import org.koin.core.annotation.KoinViewModel + +@KoinViewModel +class WelcomeViewModel(private val factory: WelcomeStateFactory) : ViewModel() { -@HiltViewModel -@OptIn(ExperimentalCoroutinesApi::class) -class WelcomeViewModel @Inject constructor(private val factory: WelcomeStateFactory) : ViewModel() { - /** - * State machine - */ private val stateMachine = FlowStateMachine(WelcomeUiState.Loading, ::initializeStateMachine) - /** - * Creates initializing state - */ private fun initializeStateMachine(): CommonMachineState { - Timber.d("Initializing state machine...") + Napier.d("Initializing state machine...") return factory.preload() } - /** - * UI State - */ val state: Flow = stateMachine.uiState - /** - * Updates state with UI gesture - * @param gesture UI gesture to proceed - */ fun process(gesture: WelcomeGesture) { - Timber.d("Gesture: %s", gesture) + Napier.d("Gesture: $gesture") stateMachine.process(gesture) } - /** - * This method will be called when this ViewModel is no longer used and will be destroyed. - * - * - * It is useful when ViewModel observes some data and you need to clear this subscription to - * prevent a leak of this ViewModel. - */ override fun onCleared() { stateMachine.clear() } -} \ No newline at end of file +} diff --git a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/CompleteState.kt b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/CompleteState.kt similarity index 83% rename from examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/CompleteState.kt rename to examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/CompleteState.kt index a06b0d6..9faf5fe 100644 --- a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/CompleteState.kt +++ b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/CompleteState.kt @@ -11,10 +11,11 @@ * limitations under the License. */ -package com.motorro.statemachine.welcome.model.state +package com.motorro.commonstatemachine.examples.welcome.app.model.state + +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeGesture +import io.github.aakira.napier.Napier -import com.motorro.statemachine.welcome.data.WelcomeGesture -import timber.log.Timber class CompleteState( context: WelcomeContext, @@ -36,7 +37,7 @@ class CompleteState( } private fun onBack() { - Timber.d("Back. Terminating...") + Napier.d("Back. Terminating...") setMachineState(factory.terminate()) } } \ No newline at end of file diff --git a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/EmailCheckState.kt b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/EmailCheckState.kt similarity index 74% rename from examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/EmailCheckState.kt rename to examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/EmailCheckState.kt index 1b5903c..c35d52e 100644 --- a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/EmailCheckState.kt +++ b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/EmailCheckState.kt @@ -11,15 +11,14 @@ * limitations under the License. */ -package com.motorro.statemachine.welcome.model.state +package com.motorro.commonstatemachine.examples.welcome.app.model.state -import com.motorro.statemachine.welcome.data.WelcomeDataState -import com.motorro.statemachine.welcome.data.WelcomeGesture -import com.motorro.statemachine.welcome.usecase.CheckEmail -import dagger.hilt.android.scopes.ViewModelScoped +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeDataState +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeGesture +import com.motorro.commonstatemachine.examples.welcome.app.usecase.CheckEmail +import io.github.aakira.napier.Napier import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject +import org.koin.core.annotation.Factory /** * Switches login/registration flow based on hardcoded addresses @@ -42,14 +41,14 @@ class EmailCheckState( */ override fun doStart() { setUiState(renderer.renderChecking(data)) - Timber.d("Checking if user exists...") + Napier.d("Checking if user exists...") stateScope.launch { val exists = checkEmail(email) if (exists) { - Timber.d("Existing user. Transferring to login flow...") + Napier.d("Existing user. Transferring to login flow...") setMachineState(factory.loginFlow(data)) } else { - Timber.d("New user. Transferring to registration flow...") + Napier.d("New user. Transferring to registration flow...") setMachineState(factory.registrationFlow(data)) } } @@ -67,12 +66,12 @@ class EmailCheckState( * Exits app */ private fun onBack() { - Timber.d("Going back to email entry...") + Napier.d("Going back to email entry...") setMachineState(factory.emailEntry(data)) } - @ViewModelScoped - class Factory @Inject constructor(private val checkEmail: CheckEmail) { + @Factory + class StateFactory(private val checkEmail: CheckEmail) { operator fun invoke( context: WelcomeContext, data: WelcomeDataState diff --git a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/EmailEntryState.kt b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/EmailEntryState.kt similarity index 87% rename from examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/EmailEntryState.kt rename to examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/EmailEntryState.kt index 8b5619c..7387f2e 100644 --- a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/EmailEntryState.kt +++ b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/EmailEntryState.kt @@ -11,12 +11,13 @@ * limitations under the License. */ -package com.motorro.statemachine.welcome.model.state +package com.motorro.commonstatemachine.examples.welcome.app.model.state import androidx.annotation.VisibleForTesting -import com.motorro.statemachine.welcome.data.WelcomeDataState -import com.motorro.statemachine.welcome.data.WelcomeGesture -import timber.log.Timber +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeDataState +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeGesture +import io.github.aakira.napier.Napier + /** * Email entry state @@ -61,13 +62,13 @@ class EmailEntryState( private fun onAction() { if (data.isValid()) { - Timber.d("Moving to checking email...") + Napier.d("Moving to checking email...") setMachineState(factory.checkEmail(data)) } } private fun onBack() { - Timber.d("Terminating...") + Napier.d("Terminating...") setMachineState(factory.terminate()) } diff --git a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/LoginFlowState.kt b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/LoginFlowState.kt similarity index 64% rename from examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/LoginFlowState.kt rename to examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/LoginFlowState.kt index e620a57..9877039 100644 --- a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/LoginFlowState.kt +++ b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/LoginFlowState.kt @@ -11,22 +11,22 @@ * limitations under the License. */ -package com.motorro.statemachine.welcome.model.state +package com.motorro.commonstatemachine.examples.welcome.app.model.state import com.motorro.commonstatemachine.CommonMachineState import com.motorro.commonstatemachine.ProxyMachineState -import com.motorro.statemachine.commonapi.welcome.model.state.WelcomeFeatureHost -import com.motorro.statemachine.login.data.LoginGesture -import com.motorro.statemachine.login.data.LoginUiState -import com.motorro.statemachine.login.di.LoginComponentBuilder -import com.motorro.statemachine.login.di.LoginEntryPoint -import com.motorro.statemachine.welcome.data.WelcomeDataState -import com.motorro.statemachine.welcome.data.WelcomeGesture -import com.motorro.statemachine.welcome.data.WelcomeUiState -import dagger.hilt.EntryPoints -import dagger.hilt.android.scopes.ViewModelScoped -import timber.log.Timber -import javax.inject.Inject +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeDataState +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeGesture +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeUiState +import com.motorro.commonstatemachine.examples.welcome.commonapi.model.state.WelcomeFeatureHost +import com.motorro.commonstatemachine.examples.welcome.login.LoginFlowStarter +import com.motorro.commonstatemachine.examples.welcome.login.data.LoginGesture +import com.motorro.commonstatemachine.examples.welcome.login.data.LoginUiState +import io.github.aakira.napier.Napier +import org.koin.core.annotation.Factory +import org.koin.core.component.KoinComponent +import org.koin.core.component.get +import org.koin.core.parameter.parametersOf /** * Proxy for login flow @@ -35,7 +35,7 @@ import javax.inject.Inject class LoginFlowState( private val context: WelcomeContext, private val data: WelcomeDataState, - private val loginComponentBuilder: LoginComponentBuilder + private val startLogin: (WelcomeFeatureHost, String) -> CommonMachineState ) : LoginProxy(LoginUiState.Loading), WelcomeFeatureHost { /** @@ -48,12 +48,7 @@ class LoginFlowState( /** * Creates initial child state */ - override fun init(): CommonMachineState { - val component = loginComponentBuilder.host(this).build() - val starter = EntryPoints.get(component, LoginEntryPoint::class.java).flowStarter() - - return starter.start(email) - } + override fun init(): CommonMachineState = startLogin(this, email) /** * Maps child UI state to parent if relevant @@ -76,7 +71,7 @@ class LoginFlowState( * Returns user to email entry screen */ override fun backToEmailEntry() { - Timber.d("Transferring to e-mail entry...") + Napier.d("Transferring to e-mail entry...") setMachineState(context.factory.emailEntry(data)) } @@ -84,19 +79,19 @@ class LoginFlowState( * Authentication complete */ override fun complete() { - Timber.d("Transferring to complete screen...") + Napier.d("Transferring to complete screen...") setMachineState(context.factory.complete(email)) } - @ViewModelScoped - class Factory @Inject constructor(private val loginComponentBuilder: LoginComponentBuilder) { + @Factory + class StateFactory : KoinComponent { operator fun invoke( context: WelcomeContext, data: WelcomeDataState ): CommonMachineState = LoginFlowState( - context, - data, - loginComponentBuilder + context = context, + data = data, + startLogin = { host, email -> get { parametersOf(host) }.start(email) } ) } } diff --git a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/PreloadingState.kt b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/PreloadingState.kt similarity index 77% rename from examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/PreloadingState.kt rename to examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/PreloadingState.kt index 8d97cca..d9ff9c4 100644 --- a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/PreloadingState.kt +++ b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/PreloadingState.kt @@ -11,14 +11,13 @@ * limitations under the License. */ -package com.motorro.statemachine.welcome.model.state +package com.motorro.commonstatemachine.examples.welcome.app.model.state -import com.motorro.statemachine.welcome.data.WelcomeGesture -import com.motorro.statemachine.welcome.usecase.Preload -import dagger.hilt.android.scopes.ViewModelScoped +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeGesture +import com.motorro.commonstatemachine.examples.welcome.app.usecase.Preload +import io.github.aakira.napier.Napier import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject +import org.koin.core.annotation.Factory /** * Preloads 'data' to use in [TermsAndConditionsState] @@ -33,7 +32,7 @@ class PreloadingState(context: WelcomeContext, private val preload: Preload) : W setUiState(renderer.renderPreloading()) stateScope.launch { val greeting = preload() - Timber.d("Transferring to welcome screen...") + Napier.d("Transferring to welcome screen...") setMachineState(factory.welcome(greeting)) } } @@ -50,12 +49,12 @@ class PreloadingState(context: WelcomeContext, private val preload: Preload) : W * Exits app */ private fun onBack() { - Timber.d("Terminating...") + Napier.d("Terminating...") setMachineState(factory.terminate()) } - @ViewModelScoped - class Factory @Inject constructor(private val preload: Preload) { + @Factory + class StateFactory(private val preload: Preload) { operator fun invoke(context: WelcomeContext) = PreloadingState(context, preload) } } \ No newline at end of file diff --git a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/RegistrationFlowState.kt b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/RegistrationFlowState.kt similarity index 63% rename from examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/RegistrationFlowState.kt rename to examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/RegistrationFlowState.kt index 8a2821f..e46a637 100644 --- a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/RegistrationFlowState.kt +++ b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/RegistrationFlowState.kt @@ -11,31 +11,31 @@ * limitations under the License. */ -package com.motorro.statemachine.welcome.model.state +package com.motorro.commonstatemachine.examples.welcome.app.model.state import com.motorro.commonstatemachine.CommonMachineState import com.motorro.commonstatemachine.ProxyMachineState -import com.motorro.statemachine.commonapi.welcome.model.state.WelcomeFeatureHost -import com.motorro.statemachine.register.data.RegisterGesture -import com.motorro.statemachine.register.data.RegisterUiState -import com.motorro.statemachine.register.di.RegisterComponentBuilder -import com.motorro.statemachine.register.di.RegisterEntryPoint -import com.motorro.statemachine.welcome.data.WelcomeDataState -import com.motorro.statemachine.welcome.data.WelcomeGesture -import com.motorro.statemachine.welcome.data.WelcomeUiState -import dagger.hilt.EntryPoints -import dagger.hilt.android.scopes.ViewModelScoped -import timber.log.Timber -import javax.inject.Inject +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeDataState +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeGesture +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeUiState +import com.motorro.commonstatemachine.examples.welcome.commonapi.model.state.WelcomeFeatureHost +import com.motorro.commonstatemachine.examples.welcome.register.RegisterFlowStarter +import com.motorro.commonstatemachine.examples.welcome.register.data.RegisterGesture +import com.motorro.commonstatemachine.examples.welcome.register.data.RegisterUiState +import io.github.aakira.napier.Napier +import org.koin.core.annotation.Factory +import org.koin.core.component.KoinComponent +import org.koin.core.component.get +import org.koin.core.parameter.parametersOf /** * Proxy for registration flow - * Adapts login flow sub-graph to registration flow + * Adapts registration flow sub-graph to welcome flow */ class RegistrationFlowState( private val context: WelcomeContext, private val data: WelcomeDataState, - private val registerComponentBuilder: RegisterComponentBuilder + private val startRegister: (WelcomeFeatureHost, String) -> CommonMachineState ) : RegistrationProxy(RegisterUiState.Loading), WelcomeFeatureHost { /** @@ -48,12 +48,7 @@ class RegistrationFlowState( /** * Creates initial child state */ - override fun init(): CommonMachineState { - val component = registerComponentBuilder.host(this).build() - val starter = EntryPoints.get(component, RegisterEntryPoint::class.java).flowStarter() - - return starter.start(email) - } + override fun init(): CommonMachineState = startRegister(this, email) /** * Maps child UI state to parent if relevant @@ -76,7 +71,7 @@ class RegistrationFlowState( * Returns user to email entry screen */ override fun backToEmailEntry() { - Timber.d("Transferring to e-mail entry...") + Napier.d("Transferring to e-mail entry...") setMachineState(context.factory.emailEntry(data)) } @@ -84,19 +79,19 @@ class RegistrationFlowState( * Authentication complete */ override fun complete() { - Timber.d("Transferring to complete screen...") + Napier.d("Transferring to complete screen...") setMachineState(context.factory.complete(email)) } - @ViewModelScoped - class Factory @Inject constructor(private val registerComponentBuilder: RegisterComponentBuilder) { + @Factory + class StateFactory : KoinComponent { operator fun invoke( context: WelcomeContext, data: WelcomeDataState ): CommonMachineState = RegistrationFlowState( - context, - data, - registerComponentBuilder + context = context, + data = data, + startRegister = { host, email -> get { parametersOf(host) }.start(email) } ) } } diff --git a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/TerminatedState.kt b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/TerminatedState.kt similarity index 79% rename from examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/TerminatedState.kt rename to examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/TerminatedState.kt index 670b020..d079882 100644 --- a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/TerminatedState.kt +++ b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/TerminatedState.kt @@ -11,10 +11,11 @@ * limitations under the License. */ -package com.motorro.statemachine.welcome.model.state +package com.motorro.commonstatemachine.examples.welcome.app.model.state + +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeUiState +import io.github.aakira.napier.Napier -import com.motorro.statemachine.welcome.data.WelcomeUiState -import timber.log.Timber /** * Terminates wizard @@ -24,7 +25,7 @@ class TerminatedState(context: WelcomeContext) : WelcomeState(context) { * A part of [start] template to initialize state */ override fun doStart() { - Timber.d("Terminated") + Napier.d("Terminated") setUiState(WelcomeUiState.Terminated) } } \ No newline at end of file diff --git a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/TermsAndConditionsState.kt b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/TermsAndConditionsState.kt similarity index 86% rename from examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/TermsAndConditionsState.kt rename to examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/TermsAndConditionsState.kt index ea80ec5..a2ade44 100644 --- a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/TermsAndConditionsState.kt +++ b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/TermsAndConditionsState.kt @@ -11,10 +11,11 @@ * limitations under the License. */ -package com.motorro.statemachine.welcome.model.state +package com.motorro.commonstatemachine.examples.welcome.app.model.state + +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeGesture +import io.github.aakira.napier.Napier -import com.motorro.statemachine.welcome.data.WelcomeGesture -import timber.log.Timber /** * Terms and conditions screen @@ -55,13 +56,13 @@ class TermsAndConditionsState( private fun onAction() { if (termsAccepted) { - Timber.d("Action. Going to email entry...") + Napier.d("Action. Going to email entry...") setMachineState(factory.emailEntry()) } } private fun onBack() { - Timber.d("Back. Terminating...") + Napier.d("Back. Terminating...") setMachineState(factory.terminate()) } diff --git a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/WelcomeContext.kt b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/WelcomeContext.kt similarity index 86% rename from examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/WelcomeContext.kt rename to examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/WelcomeContext.kt index ee66128..38fa596 100644 --- a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/WelcomeContext.kt +++ b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/WelcomeContext.kt @@ -11,10 +11,10 @@ * limitations under the License. */ -package com.motorro.statemachine.welcome.model.state +package com.motorro.commonstatemachine.examples.welcome.app.model.state import androidx.lifecycle.SavedStateHandle -import com.motorro.statemachine.welcome.model.WelcomeRenderer +import com.motorro.commonstatemachine.examples.welcome.app.model.WelcomeRenderer /** * Common state passed from state to state diff --git a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/WelcomeState.kt b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/WelcomeState.kt similarity index 64% rename from examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/WelcomeState.kt rename to examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/WelcomeState.kt index e24b95c..31005f8 100644 --- a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/WelcomeState.kt +++ b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/WelcomeState.kt @@ -11,23 +11,22 @@ * limitations under the License. */ -package com.motorro.statemachine.welcome.model.state +package com.motorro.commonstatemachine.examples.welcome.app.model.state import com.motorro.commonstatemachine.coroutines.CoroutineState -import com.motorro.statemachine.welcome.data.WelcomeGesture -import com.motorro.statemachine.welcome.data.WelcomeUiState -import timber.log.Timber +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeGesture +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeUiState +import io.github.aakira.napier.Napier + /** * Base class for welcome flow state */ -abstract class WelcomeState( - context: WelcomeContext -): CoroutineState(), WelcomeContext by context { +abstract class WelcomeState(context: WelcomeContext): CoroutineState(), WelcomeContext by context { /** * A part of [process] template to process UI gesture */ override fun doProcess(gesture: WelcomeGesture) { - Timber.w("Unsupported gesture: %s", gesture) + Napier.w("Unsupported gesture: ${gesture}") } } \ No newline at end of file diff --git a/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/WelcomeStateFactory.kt b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/WelcomeStateFactory.kt new file mode 100644 index 0000000..3797f64 --- /dev/null +++ b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/WelcomeStateFactory.kt @@ -0,0 +1,72 @@ +/* + * 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.commonstatemachine.examples.welcome.app.model.state + +import com.motorro.commonstatemachine.CommonMachineState +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeDataState +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeGesture +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeUiState + +/** + * Welcome state factory + */ +interface WelcomeStateFactory { + /** + * Preloads 'data' for registration + */ + fun preload(): CommonMachineState + + /** + * Creates welcome screen state + * @param welcomeGreeting Message to display on welcome screen + */ + fun welcome(welcomeGreeting: String): CommonMachineState + + /** + * Creates email-entry state + * @param data Data state + */ + fun emailEntry(data: WelcomeDataState? = null): CommonMachineState + + /** + * Checks if email is registered + * @param data Data state + */ + fun checkEmail(data: WelcomeDataState): CommonMachineState + + /** + * Enter existing user password + * @param data Data state + */ + fun loginFlow(data: WelcomeDataState): CommonMachineState + + /** + * Enter registration user password + * @param data Data state + */ + fun registrationFlow(data: WelcomeDataState): CommonMachineState + + /** + * Registration complete state + * @param email Registered user's email + */ + fun complete(email: String) : CommonMachineState + + /** + * Terminates registration flow + */ + fun terminate() : CommonMachineState + +} + diff --git a/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/WelcomeStateFactoryImpl.kt b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/WelcomeStateFactoryImpl.kt new file mode 100644 index 0000000..d83be1d --- /dev/null +++ b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/WelcomeStateFactoryImpl.kt @@ -0,0 +1,101 @@ +package com.motorro.commonstatemachine.examples.welcome.app.model.state + +import androidx.lifecycle.SavedStateHandle +import com.motorro.commonstatemachine.CommonMachineState +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeDataState +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeGesture +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeUiState +import com.motorro.commonstatemachine.examples.welcome.app.model.WelcomeRenderer +import io.github.aakira.napier.Napier +import org.koin.core.annotation.Factory + +/** + * [WelcomeStateFactory] implementation + */ +@Factory +class WelcomeStateFactoryImpl( + savedStateHandle: SavedStateHandle, + renderer: WelcomeRenderer, + private val createPreloading: PreloadingState.StateFactory, + private val createLogin: LoginFlowState.StateFactory, + private val createRegister: RegistrationFlowState.StateFactory, + private val createEmailCheck: EmailCheckState.StateFactory +) : WelcomeStateFactory { + + private val context: WelcomeContext = object : WelcomeContext { + override val factory = this@WelcomeStateFactoryImpl + override val savedStateHandle = savedStateHandle + override val renderer: WelcomeRenderer = renderer + } + + /** + * Preloads 'data' for registration + */ + override fun preload(): CommonMachineState { + Napier.d("Creating 'Preloading'...") + return createPreloading(context) + } + + /** + * Creates welcome screen state + * @param welcomeGreeting Message to display on welcome screen + */ + override fun welcome(welcomeGreeting: String): CommonMachineState { + Napier.d("Creating 'Welcome'...") + return TermsAndConditionsState(context, welcomeGreeting) + } + + /** + * Creates email-entry state + * @param data Data state + */ + override fun emailEntry(data: WelcomeDataState?): CommonMachineState { + Napier.d("Creating 'Email entry'...") + return EmailEntryState(context, data) + } + + /** + * Checks if email is registered + * @param data Data state + */ + override fun checkEmail(data: WelcomeDataState): CommonMachineState { + Napier.d("Creating 'Check e-mail'...") + return createEmailCheck(context, data) + } + + /** + * Enter existing user password + * @param data Data state + */ + override fun loginFlow(data: WelcomeDataState): CommonMachineState { + Napier.d("Creating 'Login flow'...") + return createLogin(context, data) + } + + /** + * Enter registration user password + * @param data Data state + */ + override fun registrationFlow(data: WelcomeDataState): CommonMachineState { + Napier.d("Creating 'Registration flow'...") + return createRegister(context, data) + } + + /** + * Registration complete state + * @param email Registered user's email + */ + override fun complete(email: String): CommonMachineState { + Napier.d("Creating 'Complete'...") + return CompleteState(context, email) + } + + /** + * Terminates registration flow + */ + override fun terminate(): CommonMachineState { + Napier.d("Creating 'Terminated'...") + return TerminatedState(context) + } + +} \ No newline at end of file diff --git a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/usecase/CheckEmail.kt b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/usecase/CheckEmail.kt similarity index 73% rename from examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/usecase/CheckEmail.kt rename to examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/usecase/CheckEmail.kt index b1b94cb..8378f59 100644 --- a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/usecase/CheckEmail.kt +++ b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/usecase/CheckEmail.kt @@ -11,22 +11,21 @@ * limitations under the License. */ -package com.motorro.statemachine.welcome.usecase +package com.motorro.commonstatemachine.examples.welcome.app.usecase -import com.motorro.statemachine.commonapi.NETWORK_DELAY -import com.motorro.statemachine.commonapi.welcome.data.BAD -import com.motorro.statemachine.commonapi.welcome.data.GOOD -import com.motorro.statemachine.commoncore.coroutines.DispatcherProvider -import dagger.hilt.android.scopes.ViewModelScoped +import com.motorro.commonstatemachine.examples.commoncore.coroutines.DispatcherProvider +import com.motorro.commonstatemachine.examples.welcome.commonapi.NETWORK_DELAY +import com.motorro.commonstatemachine.examples.welcome.commonapi.data.BAD +import com.motorro.commonstatemachine.examples.welcome.commonapi.data.GOOD import kotlinx.coroutines.delay import kotlinx.coroutines.withContext -import javax.inject.Inject +import org.koin.core.annotation.Factory /** * Checks if user is registered */ -@ViewModelScoped -class CheckEmail @Inject constructor(private val dispatchers: DispatcherProvider) { +@Factory +class CheckEmail(private val dispatchers: DispatcherProvider) { /** * Checks if user is registered * @param email User to check diff --git a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/usecase/Preload.kt b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/usecase/Preload.kt similarity index 69% rename from examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/usecase/Preload.kt rename to examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/usecase/Preload.kt index 8130081..63fb23f 100644 --- a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/usecase/Preload.kt +++ b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/usecase/Preload.kt @@ -11,23 +11,22 @@ * limitations under the License. */ -package com.motorro.statemachine.welcome.usecase +package com.motorro.commonstatemachine.examples.welcome.app.usecase -import com.motorro.statemachine.R -import com.motorro.statemachine.commonapi.NETWORK_DELAY -import com.motorro.statemachine.commoncore.coroutines.DispatcherProvider -import com.motorro.statemachine.commoncore.resources.ResourceWrapper -import dagger.hilt.android.scopes.ViewModelScoped +import com.motorro.commonstatemachine.examples.commoncore.coroutines.DispatcherProvider +import com.motorro.commonstatemachine.examples.commoncore.resources.ResourceWrapper +import com.motorro.commonstatemachine.examples.welcome.app.R +import com.motorro.commonstatemachine.examples.welcome.commonapi.NETWORK_DELAY +import io.github.aakira.napier.Napier import kotlinx.coroutines.delay import kotlinx.coroutines.withContext -import timber.log.Timber -import javax.inject.Inject +import org.koin.core.annotation.Factory /** * Emulates data preload */ -@ViewModelScoped -class Preload @Inject constructor( +@Factory +class Preload( resourceWrapper: ResourceWrapper, private val dispatchers: DispatcherProvider, ): ResourceWrapper by resourceWrapper { @@ -41,7 +40,7 @@ class Preload @Inject constructor( private suspend fun loadGreeting(): String { delay(NETWORK_DELAY) - Timber.d("Loading complete") + Napier.d("Loading complete") return getString(R.string.welcome_terms) } } \ No newline at end of file diff --git a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/view/Complete.kt b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/view/Complete.kt similarity index 92% rename from examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/view/Complete.kt rename to examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/view/Complete.kt index 0e0668e..3a620fe 100644 --- a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/view/Complete.kt +++ b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/view/Complete.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.welcome.view +package com.motorro.commonstatemachine.examples.welcome.app.view import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -32,8 +32,8 @@ 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.R -import com.motorro.statemachine.welcome.data.WelcomeUiState +import com.motorro.commonstatemachine.examples.welcome.app.R +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeUiState @Composable fun Complete(state: WelcomeUiState.Complete, onAction: () -> Unit) { diff --git a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/view/EmailEntry.kt b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/view/EmailEntry.kt similarity index 94% rename from examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/view/EmailEntry.kt rename to examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/view/EmailEntry.kt index 6726075..786c2f5 100644 --- a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/view/EmailEntry.kt +++ b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/view/EmailEntry.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.welcome.view +package com.motorro.commonstatemachine.examples.welcome.app.view import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -35,8 +35,8 @@ 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.R -import com.motorro.statemachine.welcome.data.WelcomeUiState +import com.motorro.commonstatemachine.examples.welcome.app.R +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeUiState @Composable fun EmailEntry(state: WelcomeUiState.EmailEntry, onEmailChanged: (String) -> Unit, onNext: () -> Unit) { diff --git a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/view/Welcome.kt b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/view/Welcome.kt similarity index 94% rename from examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/view/Welcome.kt rename to examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/view/Welcome.kt index de58729..1845453 100644 --- a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/view/Welcome.kt +++ b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/view/Welcome.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.welcome.view +package com.motorro.commonstatemachine.examples.welcome.app.view import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -34,8 +34,8 @@ 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.R -import com.motorro.statemachine.welcome.data.WelcomeUiState +import com.motorro.commonstatemachine.examples.welcome.app.R +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeUiState @Composable fun Welcome(state: WelcomeUiState.Welcome, onTermsToggled: () -> Unit, onNext: () -> Unit) { diff --git a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/view/WelcomeScreen.kt b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/view/WelcomeScreen.kt similarity index 61% rename from examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/view/WelcomeScreen.kt rename to examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/view/WelcomeScreen.kt index e3c4e6c..8c73983 100644 --- a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/view/WelcomeScreen.kt +++ b/examples/welcome/app/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/app/view/WelcomeScreen.kt @@ -11,30 +11,28 @@ * limitations under the License. */ -package com.motorro.statemachine.welcome.view +package com.motorro.commonstatemachine.examples.welcome.app.view import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import com.motorro.statemachine.androidcore.compose.Loading -import com.motorro.statemachine.login.view.LoginScreen -import com.motorro.statemachine.register.view.RegistrationScreen -import com.motorro.statemachine.welcome.data.WelcomeGesture.Action -import com.motorro.statemachine.welcome.data.WelcomeGesture.Back -import com.motorro.statemachine.welcome.data.WelcomeGesture.EmailChanged -import com.motorro.statemachine.welcome.data.WelcomeGesture.Login -import com.motorro.statemachine.welcome.data.WelcomeGesture.Register -import com.motorro.statemachine.welcome.data.WelcomeGesture.TermsAndConditionsToggled -import com.motorro.statemachine.welcome.data.WelcomeUiState -import com.motorro.statemachine.welcome.model.WelcomeViewModel +import com.motorro.commonstatemachine.examples.commoncore.ui.Loading +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeGesture +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeGesture.Action +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeGesture.EmailChanged +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeGesture.TermsAndConditionsToggled +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeUiState +import com.motorro.commonstatemachine.examples.welcome.app.model.WelcomeViewModel +import com.motorro.commonstatemachine.examples.welcome.login.view.LoginScreen +import com.motorro.commonstatemachine.examples.welcome.register.view.RegistrationScreen +import org.koin.compose.viewmodel.koinViewModel @Composable fun WelcomeScreen(onTerminate: @Composable () -> Unit) { - val model = hiltViewModel() + val model = koinViewModel() val state = model.state.collectAsState(WelcomeUiState.Loading) - BackHandler(onBack = { model.process(Back) }) + BackHandler(onBack = { model.process(WelcomeGesture.Back) }) when (val uiState = state.value) { WelcomeUiState.Loading -> Loading() @@ -50,11 +48,11 @@ fun WelcomeScreen(onTerminate: @Composable () -> Unit) { ) is WelcomeUiState.Login -> LoginScreen( state = uiState.value, - onGesture = { model.process(Login(it)) } + onGesture = { model.process(WelcomeGesture.Login(it)) } ) is WelcomeUiState.Register -> RegistrationScreen( state = uiState.value, - onGesture = { model.process(Register(it))} + onGesture = { model.process(WelcomeGesture.Register(it))} ) is WelcomeUiState.Complete -> Complete( state = uiState, diff --git a/examples/welcome/welcome/src/main/res/drawable-v24/ic_launcher_foreground.xml b/examples/welcome/app/src/main/res/drawable-v24/ic_launcher_foreground.xml similarity index 100% rename from examples/welcome/welcome/src/main/res/drawable-v24/ic_launcher_foreground.xml rename to examples/welcome/app/src/main/res/drawable-v24/ic_launcher_foreground.xml diff --git a/examples/welcome/welcome/src/main/res/drawable/ic_launcher_background.xml b/examples/welcome/app/src/main/res/drawable/ic_launcher_background.xml similarity index 100% rename from examples/welcome/welcome/src/main/res/drawable/ic_launcher_background.xml rename to examples/welcome/app/src/main/res/drawable/ic_launcher_background.xml diff --git a/examples/welcome/welcome/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/examples/welcome/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 100% rename from examples/welcome/welcome/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to examples/welcome/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml diff --git a/examples/welcome/welcome/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/examples/welcome/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml similarity index 100% rename from examples/welcome/welcome/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml rename to examples/welcome/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml diff --git a/examples/welcome/welcome/src/main/res/mipmap-hdpi/ic_launcher.webp b/examples/welcome/app/src/main/res/mipmap-hdpi/ic_launcher.webp similarity index 100% rename from examples/welcome/welcome/src/main/res/mipmap-hdpi/ic_launcher.webp rename to examples/welcome/app/src/main/res/mipmap-hdpi/ic_launcher.webp diff --git a/examples/welcome/welcome/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/examples/welcome/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp similarity index 100% rename from examples/welcome/welcome/src/main/res/mipmap-hdpi/ic_launcher_round.webp rename to examples/welcome/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp diff --git a/examples/welcome/welcome/src/main/res/mipmap-mdpi/ic_launcher.webp b/examples/welcome/app/src/main/res/mipmap-mdpi/ic_launcher.webp similarity index 100% rename from examples/welcome/welcome/src/main/res/mipmap-mdpi/ic_launcher.webp rename to examples/welcome/app/src/main/res/mipmap-mdpi/ic_launcher.webp diff --git a/examples/welcome/welcome/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/examples/welcome/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp similarity index 100% rename from examples/welcome/welcome/src/main/res/mipmap-mdpi/ic_launcher_round.webp rename to examples/welcome/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp diff --git a/examples/welcome/welcome/src/main/res/mipmap-xhdpi/ic_launcher.webp b/examples/welcome/app/src/main/res/mipmap-xhdpi/ic_launcher.webp similarity index 100% rename from examples/welcome/welcome/src/main/res/mipmap-xhdpi/ic_launcher.webp rename to examples/welcome/app/src/main/res/mipmap-xhdpi/ic_launcher.webp diff --git a/examples/welcome/welcome/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/examples/welcome/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp similarity index 100% rename from examples/welcome/welcome/src/main/res/mipmap-xhdpi/ic_launcher_round.webp rename to examples/welcome/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp diff --git a/examples/welcome/welcome/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/examples/welcome/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp similarity index 100% rename from examples/welcome/welcome/src/main/res/mipmap-xxhdpi/ic_launcher.webp rename to examples/welcome/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp diff --git a/examples/welcome/welcome/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/examples/welcome/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp similarity index 100% rename from examples/welcome/welcome/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp rename to examples/welcome/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp diff --git a/examples/welcome/welcome/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/examples/welcome/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp similarity index 100% rename from examples/welcome/welcome/src/main/res/mipmap-xxxhdpi/ic_launcher.webp rename to examples/welcome/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp diff --git a/examples/welcome/welcome/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/examples/welcome/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp similarity index 100% rename from examples/welcome/welcome/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp rename to examples/welcome/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp diff --git a/examples/welcome/welcome/src/main/res/values/colors.xml b/examples/welcome/app/src/main/res/values/colors.xml similarity index 100% rename from examples/welcome/welcome/src/main/res/values/colors.xml rename to examples/welcome/app/src/main/res/values/colors.xml diff --git a/examples/welcome/welcome/src/main/res/values/strings.xml b/examples/welcome/app/src/main/res/values/strings.xml similarity index 100% rename from examples/welcome/welcome/src/main/res/values/strings.xml rename to examples/welcome/app/src/main/res/values/strings.xml diff --git a/examples/welcome/welcome/src/main/res/values/themes.xml b/examples/welcome/app/src/main/res/values/themes.xml similarity index 100% rename from examples/welcome/welcome/src/main/res/values/themes.xml rename to examples/welcome/app/src/main/res/values/themes.xml diff --git a/examples/welcome/welcome/src/main/res/xml/backup_rules.xml b/examples/welcome/app/src/main/res/xml/backup_rules.xml similarity index 100% rename from examples/welcome/welcome/src/main/res/xml/backup_rules.xml rename to examples/welcome/app/src/main/res/xml/backup_rules.xml diff --git a/examples/welcome/welcome/src/main/res/xml/data_extraction_rules.xml b/examples/welcome/app/src/main/res/xml/data_extraction_rules.xml similarity index 100% rename from examples/welcome/welcome/src/main/res/xml/data_extraction_rules.xml rename to examples/welcome/app/src/main/res/xml/data_extraction_rules.xml diff --git a/examples/welcome/welcome/src/test/java/com/motorro/statemachine/welcome/model/state/BaseStateTest.kt b/examples/welcome/app/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/BaseStateTest.kt similarity index 86% rename from examples/welcome/welcome/src/test/java/com/motorro/statemachine/welcome/model/state/BaseStateTest.kt rename to examples/welcome/app/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/BaseStateTest.kt index 8095d69..cde1bee 100644 --- a/examples/welcome/welcome/src/test/java/com/motorro/statemachine/welcome/model/state/BaseStateTest.kt +++ b/examples/welcome/app/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/BaseStateTest.kt @@ -11,15 +11,15 @@ * limitations under the License. */ -package com.motorro.statemachine.welcome.model.state +package com.motorro.commonstatemachine.examples.welcome.app.model.state import androidx.annotation.CallSuper import androidx.lifecycle.SavedStateHandle import com.motorro.commonstatemachine.CommonStateMachine -import com.motorro.statemachine.commoncore.resources.ResourceWrapper -import com.motorro.statemachine.welcome.data.WelcomeGesture -import com.motorro.statemachine.welcome.data.WelcomeUiState -import com.motorro.statemachine.welcome.model.WelcomeRenderer +import com.motorro.commonstatemachine.examples.commoncore.resources.ResourceWrapper +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeGesture +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeUiState +import com.motorro.commonstatemachine.examples.welcome.app.model.WelcomeRenderer import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.Dispatchers diff --git a/examples/welcome/welcome/src/test/java/com/motorro/statemachine/welcome/model/state/CompleteStateTest.kt b/examples/welcome/app/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/CompleteStateTest.kt similarity index 88% rename from examples/welcome/welcome/src/test/java/com/motorro/statemachine/welcome/model/state/CompleteStateTest.kt rename to examples/welcome/app/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/CompleteStateTest.kt index 03e17b8..f03e175 100644 --- a/examples/welcome/welcome/src/test/java/com/motorro/statemachine/welcome/model/state/CompleteStateTest.kt +++ b/examples/welcome/app/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/CompleteStateTest.kt @@ -11,10 +11,10 @@ * limitations under the License. */ -package com.motorro.statemachine.welcome.model.state +package com.motorro.commonstatemachine.examples.welcome.app.model.state -import com.motorro.statemachine.commonapi.welcome.data.GOOD -import com.motorro.statemachine.welcome.data.WelcomeGesture +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeGesture +import com.motorro.commonstatemachine.examples.welcome.commonapi.data.GOOD import io.mockk.every import io.mockk.mockk import io.mockk.verify diff --git a/examples/welcome/welcome/src/test/java/com/motorro/statemachine/welcome/model/state/EmailCheckStateTest.kt b/examples/welcome/app/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/EmailCheckStateTest.kt similarity index 84% rename from examples/welcome/welcome/src/test/java/com/motorro/statemachine/welcome/model/state/EmailCheckStateTest.kt rename to examples/welcome/app/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/EmailCheckStateTest.kt index 1fa13a5..778283a 100644 --- a/examples/welcome/welcome/src/test/java/com/motorro/statemachine/welcome/model/state/EmailCheckStateTest.kt +++ b/examples/welcome/app/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/EmailCheckStateTest.kt @@ -13,13 +13,17 @@ @file:OptIn(ExperimentalCoroutinesApi::class) -package com.motorro.statemachine.welcome.model.state - -import com.motorro.statemachine.commonapi.welcome.data.GOOD -import com.motorro.statemachine.welcome.data.WelcomeDataState -import com.motorro.statemachine.welcome.data.WelcomeGesture -import com.motorro.statemachine.welcome.usecase.CheckEmail -import io.mockk.* +package com.motorro.commonstatemachine.examples.welcome.app.model.state + +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeDataState +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeGesture +import com.motorro.commonstatemachine.examples.welcome.app.usecase.CheckEmail +import com.motorro.commonstatemachine.examples.welcome.commonapi.data.GOOD +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest diff --git a/examples/welcome/welcome/src/test/java/com/motorro/statemachine/welcome/model/state/EmailEntryStateTest.kt b/examples/welcome/app/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/EmailEntryStateTest.kt similarity index 94% rename from examples/welcome/welcome/src/test/java/com/motorro/statemachine/welcome/model/state/EmailEntryStateTest.kt rename to examples/welcome/app/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/EmailEntryStateTest.kt index 0663d74..89a2a40 100644 --- a/examples/welcome/welcome/src/test/java/com/motorro/statemachine/welcome/model/state/EmailEntryStateTest.kt +++ b/examples/welcome/app/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/EmailEntryStateTest.kt @@ -11,11 +11,11 @@ * limitations under the License. */ -package com.motorro.statemachine.welcome.model.state +package com.motorro.commonstatemachine.examples.welcome.app.model.state import androidx.lifecycle.SavedStateHandle -import com.motorro.statemachine.welcome.data.WelcomeDataState -import com.motorro.statemachine.welcome.data.WelcomeGesture +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeDataState +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeGesture import io.mockk.every import io.mockk.mockk import io.mockk.verify diff --git a/examples/welcome/welcome/src/test/java/com/motorro/statemachine/welcome/model/state/PreloadingStateTest.kt b/examples/welcome/app/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/PreloadingStateTest.kt similarity index 86% rename from examples/welcome/welcome/src/test/java/com/motorro/statemachine/welcome/model/state/PreloadingStateTest.kt rename to examples/welcome/app/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/PreloadingStateTest.kt index f538092..911a3f7 100644 --- a/examples/welcome/welcome/src/test/java/com/motorro/statemachine/welcome/model/state/PreloadingStateTest.kt +++ b/examples/welcome/app/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/PreloadingStateTest.kt @@ -13,11 +13,15 @@ @file:OptIn(ExperimentalCoroutinesApi::class) -package com.motorro.statemachine.welcome.model.state +package com.motorro.commonstatemachine.examples.welcome.app.model.state -import com.motorro.statemachine.welcome.data.WelcomeGesture -import com.motorro.statemachine.welcome.usecase.Preload -import io.mockk.* +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeGesture +import com.motorro.commonstatemachine.examples.welcome.app.usecase.Preload +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest diff --git a/examples/welcome/welcome/src/test/java/com/motorro/statemachine/welcome/model/state/TermsAndConditionsStateTest.kt b/examples/welcome/app/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/TermsAndConditionsStateTest.kt similarity index 95% rename from examples/welcome/welcome/src/test/java/com/motorro/statemachine/welcome/model/state/TermsAndConditionsStateTest.kt rename to examples/welcome/app/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/TermsAndConditionsStateTest.kt index fad9a48..a991f64 100644 --- a/examples/welcome/welcome/src/test/java/com/motorro/statemachine/welcome/model/state/TermsAndConditionsStateTest.kt +++ b/examples/welcome/app/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/app/model/state/TermsAndConditionsStateTest.kt @@ -11,9 +11,9 @@ * limitations under the License. */ -package com.motorro.statemachine.welcome.model.state +package com.motorro.commonstatemachine.examples.welcome.app.model.state -import com.motorro.statemachine.welcome.data.WelcomeGesture +import com.motorro.commonstatemachine.examples.welcome.app.data.WelcomeGesture import io.mockk.every import io.mockk.mockk import io.mockk.verify diff --git a/examples/welcome/commonapi/build.gradle.kts b/examples/welcome/commonapi/build.gradle.kts index 2c38ecb..caee088 100644 --- a/examples/welcome/commonapi/build.gradle.kts +++ b/examples/welcome/commonapi/build.gradle.kts @@ -10,51 +10,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { - alias(libs.plugins.kotlin.multiplatform) - alias(libs.plugins.android.kotlin.multiplatform.library) + alias(libs.plugins.motorro.cmp) } -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 = "com.motorro" -version = versionName - kotlin { - android { - namespace = "com.motorro.statemachine.commonapi" - compileSdk = androidCompileSdkVersion - minSdk = androidMinSdkVersion - - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - } - - js { - binaries.library() - useCommonJs() - browser { - testTask(Action { - useMocha { - timeout = "10s" - } - }) - } - } - sourceSets { commonMain.dependencies { implementation(project(":commonstatemachine")) implementation(libs.kotlin.coroutines.core) } commonTest.dependencies { - implementation(libs.test.kotlin) implementation(libs.test.kotlin.coroutines) } } diff --git a/examples/welcome/commonapi/src/commonMain/kotlin/com/motorro/statemachine/commonapi/constants.kt b/examples/welcome/commonapi/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/commonapi/constants.kt similarity index 80% rename from examples/welcome/commonapi/src/commonMain/kotlin/com/motorro/statemachine/commonapi/constants.kt rename to examples/welcome/commonapi/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/commonapi/constants.kt index 9d2011f..1cc3ac0 100644 --- a/examples/welcome/commonapi/src/commonMain/kotlin/com/motorro/statemachine/commonapi/constants.kt +++ b/examples/welcome/commonapi/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/commonapi/constants.kt @@ -11,9 +11,11 @@ * limitations under the License. */ -package com.motorro.statemachine.commonapi +package com.motorro.commonstatemachine.examples.welcome.commonapi + +import kotlin.time.Duration.Companion.milliseconds /** * Network emulation delay */ -const val NETWORK_DELAY = 2000L \ No newline at end of file +val NETWORK_DELAY = 2000.milliseconds \ No newline at end of file diff --git a/examples/welcome/commonapi/src/commonMain/kotlin/com/motorro/statemachine/commonapi/welcome/data/users.kt b/examples/welcome/commonapi/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/commonapi/data/users.kt similarity index 90% rename from examples/welcome/commonapi/src/commonMain/kotlin/com/motorro/statemachine/commonapi/welcome/data/users.kt rename to examples/welcome/commonapi/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/commonapi/data/users.kt index 6c10b5a..cd4184c 100644 --- a/examples/welcome/commonapi/src/commonMain/kotlin/com/motorro/statemachine/commonapi/welcome/data/users.kt +++ b/examples/welcome/commonapi/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/commonapi/data/users.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.commonapi.welcome.data +package com.motorro.commonstatemachine.examples.welcome.commonapi.data /** * Mock users diff --git a/examples/welcome/commonapi/src/commonMain/kotlin/com/motorro/statemachine/commonapi/welcome/model/state/FlowStarter.kt b/examples/welcome/commonapi/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/commonapi/model/state/FlowStarter.kt similarity index 91% rename from examples/welcome/commonapi/src/commonMain/kotlin/com/motorro/statemachine/commonapi/welcome/model/state/FlowStarter.kt rename to examples/welcome/commonapi/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/commonapi/model/state/FlowStarter.kt index f8e21bc..a8cfe8b 100644 --- a/examples/welcome/commonapi/src/commonMain/kotlin/com/motorro/statemachine/commonapi/welcome/model/state/FlowStarter.kt +++ b/examples/welcome/commonapi/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/commonapi/model/state/FlowStarter.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.commonapi.welcome.model.state +package com.motorro.commonstatemachine.examples.welcome.commonapi.model.state import com.motorro.commonstatemachine.CommonMachineState diff --git a/examples/welcome/commonapi/src/commonMain/kotlin/com/motorro/statemachine/commonapi/welcome/model/state/WelcomeFeatureHost.kt b/examples/welcome/commonapi/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/commonapi/model/state/WelcomeFeatureHost.kt similarity index 91% rename from examples/welcome/commonapi/src/commonMain/kotlin/com/motorro/statemachine/commonapi/welcome/model/state/WelcomeFeatureHost.kt rename to examples/welcome/commonapi/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/commonapi/model/state/WelcomeFeatureHost.kt index d44e7c9..8233f4e 100644 --- a/examples/welcome/commonapi/src/commonMain/kotlin/com/motorro/statemachine/commonapi/welcome/model/state/WelcomeFeatureHost.kt +++ b/examples/welcome/commonapi/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/commonapi/model/state/WelcomeFeatureHost.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.commonapi.welcome.model.state +package com.motorro.commonstatemachine.examples.welcome.commonapi.model.state /** diff --git a/examples/welcome/commonregister/build.gradle.kts b/examples/welcome/commonregister/build.gradle.kts deleted file mode 100644 index b4f07b4..0000000 --- a/examples/welcome/commonregister/build.gradle.kts +++ /dev/null @@ -1,73 +0,0 @@ -@file:Suppress("unused") - -/* -* 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. -*/ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - alias(libs.plugins.kotlin.multiplatform) - alias(libs.plugins.android.kotlin.multiplatform.library) -} - -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 = "com.motorro" -version = versionName - -kotlin { - android { - namespace = "com.motorro.statemachine.commonregister" - compileSdk = androidCompileSdkVersion - minSdk = androidMinSdkVersion - - androidResources { - enable = true - } - - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - } - - js { - binaries.library() - useCommonJs() - browser { - testTask(Action { - useMocha { - timeout = "10s" - } - }) - } - } - - sourceSets { - commonMain.dependencies { - implementation(project(":commonstatemachine")) - implementation(project(":coroutines")) - implementation(project(":examples:commoncore")) - implementation(project(":examples:welcome:commonapi")) - implementation(libs.kotlin.coroutines.core) - } - commonTest.dependencies { - implementation(libs.test.kotlin) - implementation(libs.test.kotlin.coroutines) - } - androidMain.dependencies { - implementation(libs.timber) - } - } -} diff --git a/examples/welcome/commonregister/src/androidMain/kotlin/com/motorro/statemachine/register/model/getPasswordError.kt b/examples/welcome/commonregister/src/androidMain/kotlin/com/motorro/statemachine/register/model/getPasswordError.kt deleted file mode 100644 index 9171b21..0000000 --- a/examples/welcome/commonregister/src/androidMain/kotlin/com/motorro/statemachine/register/model/getPasswordError.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.register.model - -import com.motorro.statemachine.commoncore.resources.ResourceWrapper -import com.motorro.statemachine.commonregister.R -import com.motorro.statemachine.register.data.PasswordValidationError -import com.motorro.statemachine.register.data.PasswordValidationError.PASSWORD_LENGTH -import com.motorro.statemachine.register.data.PasswordValidationError.PASSWORD_MISMATCH - -/** - * Returns password error text - */ -actual fun ResourceWrapper.getPasswordError(error: PasswordValidationError): String = when(error) { - PASSWORD_LENGTH -> getString(R.string.register_error_password_length) - PASSWORD_MISMATCH -> getString(R.string.register_error_password_mismatch) -} diff --git a/examples/welcome/commonregister/src/androidMain/res/values/strings.xml b/examples/welcome/commonregister/src/androidMain/res/values/strings.xml deleted file mode 100644 index 289a66a..0000000 --- a/examples/welcome/commonregister/src/androidMain/res/values/strings.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - Password should be at least 6 chars long - Passwords mismatch - \ No newline at end of file diff --git a/examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/model/state/RegisterStateFactory.kt b/examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/model/state/RegisterStateFactory.kt deleted file mode 100644 index 278e94f..0000000 --- a/examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/model/state/RegisterStateFactory.kt +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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.register.model.state - -import com.motorro.commonstatemachine.CommonMachineState -import com.motorro.statemachine.commonapi.welcome.model.state.FlowStarter -import com.motorro.statemachine.commonapi.welcome.model.state.WelcomeFeatureHost -import com.motorro.statemachine.commoncore.log.Logger -import com.motorro.statemachine.register.data.RegisterDataState -import com.motorro.statemachine.register.data.RegisterGesture -import com.motorro.statemachine.register.data.RegisterUiState -import com.motorro.statemachine.register.model.RegistrationRenderer - -/** - * Login flow state factory - */ -interface RegisterStateFactory : FlowStarter { - /** - * Creates a starting state - * @param email Email to proceed with - */ - override fun start(email: String): CommonMachineState = passwordEntry(RegisterDataState(email)) - - /** - * Password entry screen - * @param data Registration data state - */ - fun passwordEntry(data: RegisterDataState): RegisterState - - /** - * Registers user - * @param data Data state - */ - fun registering(data: RegisterDataState): RegisterState - - /** - * [RegisterStateFactory] implementation - */ - class Impl( - host: WelcomeFeatureHost, - renderer: RegistrationRenderer, - private val createRegistration: RegistrationState.Factory - ) : RegisterStateFactory { - - private val context: RegisterContext = object : RegisterContext { - override val factory: RegisterStateFactory = this@Impl - override val host: WelcomeFeatureHost = host - override val renderer: RegistrationRenderer = renderer - } - - /** - * Password entry screen - * @param data Registration data state - */ - override fun passwordEntry(data: RegisterDataState): RegisterState { - Logger.d("Creating 'Password entry'...") - return PasswordEntryState(context, data) - } - - /** - * Registers user - * @param data Data state - */ - override fun registering(data: RegisterDataState): RegisterState { - Logger.d("Creating 'Credentials check'...") - return createRegistration(context, data) - } - } -} \ No newline at end of file diff --git a/examples/welcome/commonregister/src/jsMain/kotlin/com/motorro/statemachine/register/model/getPasswordError.kt b/examples/welcome/commonregister/src/jsMain/kotlin/com/motorro/statemachine/register/model/getPasswordError.kt deleted file mode 100644 index fee5b2f..0000000 --- a/examples/welcome/commonregister/src/jsMain/kotlin/com/motorro/statemachine/register/model/getPasswordError.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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.register.model - -import com.motorro.statemachine.commoncore.resources.ResourceWrapper -import com.motorro.statemachine.register.data.PasswordValidationError -import com.motorro.statemachine.register.data.PasswordValidationError.PASSWORD_LENGTH -import com.motorro.statemachine.register.data.PasswordValidationError.PASSWORD_MISMATCH - -/** - * Returns password error text - */ -actual fun ResourceWrapper.getPasswordError(error: PasswordValidationError): String = when(error) { - PASSWORD_LENGTH -> "Password should be at least 6 chars long" - PASSWORD_MISMATCH -> "Passwords mismatch" -} diff --git a/examples/welcome/login/build.gradle.kts b/examples/welcome/login/build.gradle.kts index 0d6699b..eb9886b 100644 --- a/examples/welcome/login/build.gradle.kts +++ b/examples/welcome/login/build.gradle.kts @@ -1,7 +1,3 @@ -@file:Suppress("unused") - -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - /* * Copyright 2022 Nikolai Kotchetkov. * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,50 +12,8 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget */ plugins { - alias(libs.plugins.android.lib) - alias(libs.plugins.google.ksp) - alias(libs.plugins.hilt) - alias(libs.plugins.compose) -} - -val versionCode: String by project.extra -val versionName: String by project.extra -val androidMinSdkVersion: Int by project.extra -val androidTargetSdkVersion: Int by project.extra -val androidCompileSdkVersion: Int by project.extra - -android { - // Assuming androidCompileSdkVersion, androidMinSdkVersion, and androidTargetSdkVersion - // are defined in your project's gradle.properties or root build.gradle.kts - 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 { - isCoreLibraryDesugaringEnabled = true - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 - } - kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - } - buildFeatures { - compose = true - } - namespace = "com.motorro.statemachine.login" + alias(libs.plugins.motorro.android.lib.compose) + alias(libs.plugins.koin) } dependencies { @@ -67,28 +21,9 @@ dependencies { implementation(project(":coroutines")) implementation(project(":examples:welcome:commonapi")) implementation(project(":examples:commoncore")) - implementation(project(":examples:androidcore")) - - coreLibraryDesugaring(libs.desugaring) - - 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.foundation) - implementation(libs.compose.foundation.layouts) // Corrected from "foundation.layout" to "foundation.layouts" as per your original file - - implementation(libs.hilt.android) - implementation(libs.hilt.compose) // Assuming this is 'libs.hilt.navigation.compose' or similar based on common usage with Hilt and Compose - ksp(libs.hilt.compiler) - ksp(libs.hilt.compiler.androidx) // Assuming this is 'libs.hilt.android.compiler' or similar - - debugImplementation(libs.compose.tooling) + implementation(libs.napier) - testImplementation(libs.bundles.test.core) - testImplementation(libs.test.androidx.arch) - testImplementation(libs.test.kotlin.coroutines) + implementation(platform(libs.koin.bom)) + implementation(libs.koin.android) + implementation(libs.koin.annotations) } diff --git a/examples/welcome/login/src/main/AndroidManifest.xml b/examples/welcome/login/src/main/AndroidManifest.xml deleted file mode 100644 index cfc666d..0000000 --- a/examples/welcome/login/src/main/AndroidManifest.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/di/LoginComponent.kt b/examples/welcome/login/src/main/java/com/motorro/statemachine/login/di/LoginComponent.kt deleted file mode 100644 index b225aa7..0000000 --- a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/di/LoginComponent.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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.login.di - -import com.motorro.statemachine.commonapi.welcome.model.state.WelcomeFeatureHost -import dagger.BindsInstance -import dagger.hilt.DefineComponent -import dagger.hilt.components.SingletonComponent - -@LoginScope -@DefineComponent(parent = SingletonComponent::class) -interface LoginComponent - -@DefineComponent.Builder -interface LoginComponentBuilder { - /** - * Defines feature host graph - */ - fun host(@BindsInstance value: WelcomeFeatureHost): LoginComponentBuilder - - /** - * Builds component - */ - fun build(): LoginComponent -} diff --git a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/di/LoginEntryPoint.kt b/examples/welcome/login/src/main/java/com/motorro/statemachine/login/di/LoginEntryPoint.kt deleted file mode 100644 index b974fc2..0000000 --- a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/di/LoginEntryPoint.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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.login.di - -import com.motorro.statemachine.commonapi.welcome.model.state.FlowStarter -import com.motorro.statemachine.login.data.LoginGesture -import com.motorro.statemachine.login.data.LoginUiState -import dagger.hilt.EntryPoint -import dagger.hilt.InstallIn - -/** - * Login feature entry point - */ -@EntryPoint -@InstallIn(LoginComponent::class) -interface LoginEntryPoint { - /** - * Flow starter - */ - fun flowStarter(): FlowStarter -} \ No newline at end of file diff --git a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/di/LoginModule.kt b/examples/welcome/login/src/main/java/com/motorro/statemachine/login/di/LoginModule.kt deleted file mode 100644 index 7102833..0000000 --- a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/di/LoginModule.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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.login.di - -import com.motorro.statemachine.commonapi.welcome.model.state.FlowStarter -import com.motorro.statemachine.login.data.LoginGesture -import com.motorro.statemachine.login.data.LoginUiState -import com.motorro.statemachine.login.model.state.LoginStateFactory -import dagger.Binds -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn - -@Module -@InstallIn(LoginComponent::class) -internal class LoginModule { - @Provides - @LoginScope - fun flowStarter(factory: LoginStateFactory): FlowStarter = factory -} - -@Module -@InstallIn(LoginComponent::class) -internal interface LoginBindingModule { - @Binds - @LoginScope - fun factory(impl: LoginStateFactory.Impl): LoginStateFactory -} \ No newline at end of file diff --git a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/model/state/LoginStateFactory.kt b/examples/welcome/login/src/main/java/com/motorro/statemachine/login/model/state/LoginStateFactory.kt deleted file mode 100644 index c53ebd6..0000000 --- a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/model/state/LoginStateFactory.kt +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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.login.model.state - -import com.motorro.commonstatemachine.CommonMachineState -import com.motorro.statemachine.commonapi.welcome.model.state.FlowStarter -import com.motorro.statemachine.commonapi.welcome.model.state.WelcomeFeatureHost -import com.motorro.statemachine.login.data.LoginDataState -import com.motorro.statemachine.login.data.LoginGesture -import com.motorro.statemachine.login.data.LoginUiState -import com.motorro.statemachine.login.di.LoginScope -import com.motorro.statemachine.login.model.LoginRenderer -import timber.log.Timber -import javax.inject.Inject - -/** - * Login flow state factory - */ -internal interface LoginStateFactory : FlowStarter { - /** - * Creates a starting state - * @param email Common data state - */ - override fun start(email: String): CommonMachineState = passwordEntry(LoginDataState(email)) - - /** - * Enter existing user password - * @param data Login data state - */ - fun passwordEntry(data: LoginDataState): LoginState - - /** - * Checks email/password - * @param data Data state - */ - fun checking(data: LoginDataState): LoginState - - /** - * Password error screen - */ - fun error(data: LoginDataState, error: Throwable): LoginState - - /** - * [LoginStateFactory] implementation - */ - @LoginScope - class Impl @Inject constructor( - host: WelcomeFeatureHost, - renderer: LoginRenderer, - private val createCredentialsCheck: CredentialsCheckState.Factory - ) : LoginStateFactory { - - private val context: LoginContext = object : LoginContext { - override val factory: LoginStateFactory = this@Impl - override val host: WelcomeFeatureHost = host - override val renderer: LoginRenderer = renderer - } - - /** - * Enter existing user password - * @param data Common data state - */ - override fun passwordEntry(data: LoginDataState): LoginState { - Timber.d("Creating 'Password entry'...") - return PasswordEntryState(context, data) - } - - /** - * Checks email/password - * @param data Data state - */ - override fun checking(data: LoginDataState): LoginState { - Timber.d("Creating 'Credentials check'...") - return createCredentialsCheck(context, data) - } - - /** - * Password error screen - */ - override fun error(data: LoginDataState, error: Throwable): LoginState { - Timber.d("Creating 'Error'...") - return ErrorState(context, data, error) - } - } -} \ No newline at end of file diff --git a/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/LoginFlowStarter.kt b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/LoginFlowStarter.kt new file mode 100644 index 0000000..04ece08 --- /dev/null +++ b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/LoginFlowStarter.kt @@ -0,0 +1,10 @@ +package com.motorro.commonstatemachine.examples.welcome.login + +import com.motorro.commonstatemachine.examples.welcome.commonapi.model.state.FlowStarter +import com.motorro.commonstatemachine.examples.welcome.login.data.LoginGesture +import com.motorro.commonstatemachine.examples.welcome.login.data.LoginUiState + +/** + * Starts login flow + */ +interface LoginFlowStarter : FlowStarter \ No newline at end of file diff --git a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/di/LoginScope.kt b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/LoginModule.kt similarity index 74% rename from examples/welcome/login/src/main/java/com/motorro/statemachine/login/di/LoginScope.kt rename to examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/LoginModule.kt index 2d48a78..20fbdb5 100644 --- a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/di/LoginScope.kt +++ b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/LoginModule.kt @@ -11,14 +11,11 @@ * limitations under the License. */ -package com.motorro.statemachine.login.di +package com.motorro.commonstatemachine.examples.welcome.login -import javax.inject.Scope +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module -/** - * Login feature scope - */ -@Scope -@MustBeDocumented -@Retention(value = AnnotationRetention.RUNTIME) -annotation class LoginScope \ No newline at end of file +@Module +@ComponentScan +class LoginModule diff --git a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/data/LoginDataState.kt b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/data/LoginDataState.kt similarity index 91% rename from examples/welcome/login/src/main/java/com/motorro/statemachine/login/data/LoginDataState.kt rename to examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/data/LoginDataState.kt index 0912e12..8b8bbad 100644 --- a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/data/LoginDataState.kt +++ b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/data/LoginDataState.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.login.data +package com.motorro.commonstatemachine.examples.welcome.login.data /** diff --git a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/data/LoginGesture.kt b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/data/LoginGesture.kt similarity index 93% rename from examples/welcome/login/src/main/java/com/motorro/statemachine/login/data/LoginGesture.kt rename to examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/data/LoginGesture.kt index 58ed854..5ab9b6d 100644 --- a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/data/LoginGesture.kt +++ b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/data/LoginGesture.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.login.data +package com.motorro.commonstatemachine.examples.welcome.login.data /** * Login flow UI gestures diff --git a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/data/LoginUiState.kt b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/data/LoginUiState.kt similarity index 95% rename from examples/welcome/login/src/main/java/com/motorro/statemachine/login/data/LoginUiState.kt rename to examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/data/LoginUiState.kt index a8fd402..039b836 100644 --- a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/data/LoginUiState.kt +++ b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/data/LoginUiState.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.login.data +package com.motorro.commonstatemachine.examples.welcome.login.data /** * Login flow UI state diff --git a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/model/LoginRenderer.kt b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/LoginRenderer.kt similarity index 67% rename from examples/welcome/login/src/main/java/com/motorro/statemachine/login/model/LoginRenderer.kt rename to examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/LoginRenderer.kt index 0c2b61f..7fd5c98 100644 --- a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/model/LoginRenderer.kt +++ b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/LoginRenderer.kt @@ -11,24 +11,23 @@ * limitations under the License. */ -package com.motorro.statemachine.login.model +package com.motorro.commonstatemachine.examples.welcome.login.model -import com.motorro.statemachine.commoncore.resources.ResourceWrapper -import com.motorro.statemachine.login.R -import com.motorro.statemachine.login.data.LoginDataState -import com.motorro.statemachine.login.data.LoginUiState -import com.motorro.statemachine.login.di.LoginScope -import javax.inject.Inject +import com.motorro.commonstatemachine.examples.commoncore.resources.ResourceWrapper +import com.motorro.commonstatemachine.examples.welcome.login.R +import com.motorro.commonstatemachine.examples.welcome.login.data.LoginDataState +import com.motorro.commonstatemachine.examples.welcome.login.data.LoginUiState +import org.koin.core.annotation.Factory /** * Renders Login flow view states */ -@LoginScope -internal class LoginRenderer @Inject constructor(resourceWrapper: ResourceWrapper): ResourceWrapper by resourceWrapper { +@Factory +internal class LoginRenderer(resourceWrapper: ResourceWrapper): ResourceWrapper by resourceWrapper { /** * Renders password form */ - fun renderPassword(data: LoginDataState, isValid: Boolean): LoginUiState.PasswordEntry = + fun renderPassword(data: com.motorro.commonstatemachine.examples.welcome.login.data.LoginDataState, isValid: Boolean): LoginUiState.PasswordEntry = LoginUiState.PasswordEntry( data.email, data.password.orEmpty(), diff --git a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/model/state/CredentialsCheckState.kt b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/CredentialsCheckState.kt similarity index 74% rename from examples/welcome/login/src/main/java/com/motorro/statemachine/login/model/state/CredentialsCheckState.kt rename to examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/CredentialsCheckState.kt index f2eb1b1..ddd7347 100644 --- a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/model/state/CredentialsCheckState.kt +++ b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/CredentialsCheckState.kt @@ -11,15 +11,14 @@ * limitations under the License. */ -package com.motorro.statemachine.login.model.state +package com.motorro.commonstatemachine.examples.welcome.login.model.state -import com.motorro.statemachine.login.data.LoginDataState -import com.motorro.statemachine.login.data.LoginGesture -import com.motorro.statemachine.login.di.LoginScope -import com.motorro.statemachine.login.usecase.CheckCredentials +import com.motorro.commonstatemachine.examples.welcome.login.data.LoginDataState +import com.motorro.commonstatemachine.examples.welcome.login.data.LoginGesture +import com.motorro.commonstatemachine.examples.welcome.login.usecase.CheckCredentials +import io.github.aakira.napier.Napier import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject +import org.koin.core.annotation.Factory /** * Emulates login operation @@ -42,14 +41,14 @@ internal class CredentialsCheckState( */ override fun doStart() { setUiState(renderer.renderLoading(data)) - Timber.d("Checking for user credentials...") + Napier.d("Checking for user credentials...") stateScope.launch { val valid = checkCredentials(data.email, password) if (valid) { - Timber.d("Correct credentials. Transferring to complete screen...") + Napier.d("Correct credentials. Transferring to complete screen...") host.complete() } else { - Timber.w("Login error. Transferring to error screen...") + Napier.w("Login error. Transferring to error screen...") setMachineState(factory.error(data, IllegalArgumentException("Wrong username or password"))) } } @@ -64,12 +63,12 @@ internal class CredentialsCheckState( } private fun onBack() { - Timber.d("Returning to password entry") + Napier.d("Returning to password entry") setMachineState(factory.passwordEntry(data)) } - @LoginScope - class Factory @Inject constructor(private val checkCredentials: CheckCredentials) { + @Factory + class StateFactory(private val checkCredentials: CheckCredentials) { operator fun invoke( context: LoginContext, data: LoginDataState diff --git a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/model/state/ErrorState.kt b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/ErrorState.kt similarity index 79% rename from examples/welcome/login/src/main/java/com/motorro/statemachine/login/model/state/ErrorState.kt rename to examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/ErrorState.kt index 47e0061..bd3bd64 100644 --- a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/model/state/ErrorState.kt +++ b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/ErrorState.kt @@ -11,11 +11,12 @@ * limitations under the License. */ -package com.motorro.statemachine.login.model.state +package com.motorro.commonstatemachine.examples.welcome.login.model.state + +import com.motorro.commonstatemachine.examples.welcome.login.data.LoginDataState +import com.motorro.commonstatemachine.examples.welcome.login.data.LoginGesture +import io.github.aakira.napier.Napier -import com.motorro.statemachine.login.data.LoginDataState -import com.motorro.statemachine.login.data.LoginGesture -import timber.log.Timber /** * Login error @@ -35,7 +36,7 @@ internal class ErrorState( } private fun onDismiss() { - Timber.d("Returning to password entry") + Napier.d("Returning to password entry") setMachineState(factory.passwordEntry(data)) } diff --git a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/model/state/LoginContext.kt b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/LoginContext.kt similarity index 77% rename from examples/welcome/login/src/main/java/com/motorro/statemachine/login/model/state/LoginContext.kt rename to examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/LoginContext.kt index 4a3fc61..765a09a 100644 --- a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/model/state/LoginContext.kt +++ b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/LoginContext.kt @@ -11,10 +11,10 @@ * limitations under the License. */ -package com.motorro.statemachine.login.model.state +package com.motorro.commonstatemachine.examples.welcome.login.model.state -import com.motorro.statemachine.commonapi.welcome.model.state.WelcomeFeatureHost -import com.motorro.statemachine.login.model.LoginRenderer +import com.motorro.commonstatemachine.examples.welcome.commonapi.model.state.WelcomeFeatureHost +import com.motorro.commonstatemachine.examples.welcome.login.model.LoginRenderer /** diff --git a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/model/state/LoginState.kt b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/LoginState.kt similarity index 64% rename from examples/welcome/login/src/main/java/com/motorro/statemachine/login/model/state/LoginState.kt rename to examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/LoginState.kt index 11f7bab..501d60a 100644 --- a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/model/state/LoginState.kt +++ b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/LoginState.kt @@ -11,23 +11,22 @@ * limitations under the License. */ -package com.motorro.statemachine.login.model.state +package com.motorro.commonstatemachine.examples.welcome.login.model.state import com.motorro.commonstatemachine.coroutines.CoroutineState -import com.motorro.statemachine.login.data.LoginGesture -import com.motorro.statemachine.login.data.LoginUiState -import timber.log.Timber +import com.motorro.commonstatemachine.examples.welcome.login.data.LoginGesture +import com.motorro.commonstatemachine.examples.welcome.login.data.LoginUiState +import io.github.aakira.napier.Napier + /** * Base class for login flow state */ -internal abstract class LoginState( - context: LoginContext -): CoroutineState(), LoginContext by context { +internal abstract class LoginState(context: LoginContext): CoroutineState(), LoginContext by context { /** * A part of [process] template to process UI gesture */ override fun doProcess(gesture: LoginGesture) { - Timber.w("Unsupported gesture: %s", gesture) + Napier.w("Unsupported gesture: ${gesture}") } } \ No newline at end of file diff --git a/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/LoginStateFactory.kt b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/LoginStateFactory.kt new file mode 100644 index 0000000..a7caec2 --- /dev/null +++ b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/LoginStateFactory.kt @@ -0,0 +1,50 @@ +/* + * 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.commonstatemachine.examples.welcome.login.model.state + +import com.motorro.commonstatemachine.CommonMachineState +import com.motorro.commonstatemachine.examples.welcome.login.LoginFlowStarter +import com.motorro.commonstatemachine.examples.welcome.login.data.LoginDataState +import com.motorro.commonstatemachine.examples.welcome.login.data.LoginGesture +import com.motorro.commonstatemachine.examples.welcome.login.data.LoginUiState + +/** + * Login flow state factory + */ +internal interface LoginStateFactory : LoginFlowStarter { + /** + * Creates a starting state + * @param email Common data state + */ + override fun start(email: String): CommonMachineState = passwordEntry(LoginDataState(email)) + + /** + * Enter existing user password + * @param data Login data state + */ + fun passwordEntry(data: LoginDataState): LoginState + + /** + * Checks email/password + * @param data Data state + */ + abstract fun checking(data: LoginDataState): LoginState + + /** + * Password error screen + */ + abstract fun error(data: LoginDataState, error: Throwable): LoginState + +} + diff --git a/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/LoginStateFactoryImpl.kt b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/LoginStateFactoryImpl.kt new file mode 100644 index 0000000..00607e4 --- /dev/null +++ b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/LoginStateFactoryImpl.kt @@ -0,0 +1,52 @@ +package com.motorro.commonstatemachine.examples.welcome.login.model.state + +import com.motorro.commonstatemachine.examples.welcome.commonapi.model.state.WelcomeFeatureHost +import com.motorro.commonstatemachine.examples.welcome.login.LoginFlowStarter +import com.motorro.commonstatemachine.examples.welcome.login.data.LoginDataState +import com.motorro.commonstatemachine.examples.welcome.login.model.LoginRenderer +import io.github.aakira.napier.Napier +import org.koin.core.annotation.Factory +import org.koin.core.annotation.InjectedParam + +/** + * [LoginStateFactory] implementation + */ +@Factory(binds = [LoginFlowStarter::class]) +internal class LoginStateFactoryImpl( + @InjectedParam host: WelcomeFeatureHost, + renderer: LoginRenderer, + private val createCredentialsCheck: CredentialsCheckState.StateFactory +) : LoginStateFactory { + + private val context: LoginContext = object : LoginContext { + override val factory: LoginStateFactory = this@LoginStateFactoryImpl + override val host: WelcomeFeatureHost = host + override val renderer: LoginRenderer = renderer + } + + /** + * Enter existing user password + * @param data Common data state + */ + override fun passwordEntry(data: LoginDataState): LoginState { + Napier.d("Creating 'Password entry'...") + return PasswordEntryState(context, data) + } + + /** + * Checks email/password + * @param data Data state + */ + override fun checking(data: LoginDataState): LoginState { + Napier.d("Creating 'Credentials check'...") + return createCredentialsCheck(context, data) + } + + /** + * Password error screen + */ + override fun error(data: LoginDataState, error: Throwable): LoginState { + Napier.d("Creating 'Error'...") + return ErrorState(context, data, error) + } +} \ No newline at end of file diff --git a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/model/state/PasswordEntryState.kt b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/PasswordEntryState.kt similarity index 81% rename from examples/welcome/login/src/main/java/com/motorro/statemachine/login/model/state/PasswordEntryState.kt rename to examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/PasswordEntryState.kt index 93cd0a8..4b6a331 100644 --- a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/model/state/PasswordEntryState.kt +++ b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/PasswordEntryState.kt @@ -11,11 +11,12 @@ * limitations under the License. */ -package com.motorro.statemachine.login.model.state +package com.motorro.commonstatemachine.examples.welcome.login.model.state + +import com.motorro.commonstatemachine.examples.welcome.login.data.LoginDataState +import com.motorro.commonstatemachine.examples.welcome.login.data.LoginGesture +import io.github.aakira.napier.Napier -import com.motorro.statemachine.login.data.LoginDataState -import com.motorro.statemachine.login.data.LoginGesture -import timber.log.Timber /** * Password entry screen @@ -43,13 +44,13 @@ internal class PasswordEntryState( private fun onAction() { if (isPasswordValid()) { - Timber.d("Valid password. Transferring to credentials check") + Napier.d("Valid password. Transferring to credentials check") setMachineState(factory.checking(data)) } } private fun onBack() { - Timber.d("Returning to e-mail entry...") + Napier.d("Returning to e-mail entry...") host.backToEmailEntry() } diff --git a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/usecase/CheckCredentials.kt b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/usecase/CheckCredentials.kt similarity index 73% rename from examples/welcome/login/src/main/java/com/motorro/statemachine/login/usecase/CheckCredentials.kt rename to examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/usecase/CheckCredentials.kt index 69a0106..fa82f66 100644 --- a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/usecase/CheckCredentials.kt +++ b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/usecase/CheckCredentials.kt @@ -11,22 +11,21 @@ * limitations under the License. */ -package com.motorro.statemachine.login.usecase +package com.motorro.commonstatemachine.examples.welcome.login.usecase -import com.motorro.statemachine.commonapi.NETWORK_DELAY -import com.motorro.statemachine.commonapi.welcome.data.BAD -import com.motorro.statemachine.commonapi.welcome.data.GOOD -import com.motorro.statemachine.commoncore.coroutines.DispatcherProvider -import com.motorro.statemachine.login.di.LoginScope +import com.motorro.commonstatemachine.examples.commoncore.coroutines.DispatcherProvider +import com.motorro.commonstatemachine.examples.welcome.commonapi.NETWORK_DELAY +import com.motorro.commonstatemachine.examples.welcome.commonapi.data.BAD +import com.motorro.commonstatemachine.examples.welcome.commonapi.data.GOOD import kotlinx.coroutines.delay import kotlinx.coroutines.withContext -import javax.inject.Inject +import org.koin.core.annotation.Factory /** * Emulates user's credentials check */ -@LoginScope -internal class CheckCredentials @Inject constructor(private val dispatchers: DispatcherProvider) { +@Factory +internal class CheckCredentials(private val dispatchers: DispatcherProvider) { /** * Tries to log-in user * @param email User to check diff --git a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/view/LoginScreen.kt b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/view/LoginScreen.kt similarity index 81% rename from examples/welcome/login/src/main/java/com/motorro/statemachine/login/view/LoginScreen.kt rename to examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/view/LoginScreen.kt index f6079d8..83ba492 100644 --- a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/view/LoginScreen.kt +++ b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/view/LoginScreen.kt @@ -11,12 +11,12 @@ * limitations under the License. */ -package com.motorro.statemachine.login.view +package com.motorro.commonstatemachine.examples.welcome.login.view import androidx.compose.runtime.Composable -import com.motorro.statemachine.androidcore.compose.Loading -import com.motorro.statemachine.login.data.LoginGesture -import com.motorro.statemachine.login.data.LoginUiState +import com.motorro.commonstatemachine.examples.commoncore.ui.Loading +import com.motorro.commonstatemachine.examples.welcome.login.data.LoginGesture +import com.motorro.commonstatemachine.examples.welcome.login.data.LoginUiState /** * Login screen flow diff --git a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/view/PasswordEntry.kt b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/view/PasswordEntry.kt similarity index 95% rename from examples/welcome/login/src/main/java/com/motorro/statemachine/login/view/PasswordEntry.kt rename to examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/view/PasswordEntry.kt index cf8d1bc..17f13a5 100644 --- a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/view/PasswordEntry.kt +++ b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/view/PasswordEntry.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.login.view +package com.motorro.commonstatemachine.examples.welcome.login.view import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -38,8 +38,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.motorro.statemachine.login.R -import com.motorro.statemachine.login.data.LoginUiState +import com.motorro.commonstatemachine.examples.welcome.login.R +import com.motorro.commonstatemachine.examples.welcome.login.data.LoginUiState @Composable internal fun PasswordEntry( diff --git a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/view/PasswordError.kt b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/view/PasswordError.kt similarity index 91% rename from examples/welcome/login/src/main/java/com/motorro/statemachine/login/view/PasswordError.kt rename to examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/view/PasswordError.kt index 300e170..1251fd8 100644 --- a/examples/welcome/login/src/main/java/com/motorro/statemachine/login/view/PasswordError.kt +++ b/examples/welcome/login/src/main/kotlin/com/motorro/commonstatemachine/examples/welcome/login/view/PasswordError.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.login.view +package com.motorro.commonstatemachine.examples.welcome.login.view import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.AlertDialog @@ -22,8 +22,8 @@ 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 com.motorro.statemachine.login.R -import com.motorro.statemachine.login.data.LoginUiState +import com.motorro.commonstatemachine.examples.welcome.login.R +import com.motorro.commonstatemachine.examples.welcome.login.data.LoginUiState @Composable internal fun PasswordError(state: LoginUiState.LoginError, onDismiss: () -> Unit) { diff --git a/examples/welcome/login/src/test/java/com/motorro/statemachine/login/model/state/BaseStateTest.kt b/examples/welcome/login/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/BaseStateTest.kt similarity index 81% rename from examples/welcome/login/src/test/java/com/motorro/statemachine/login/model/state/BaseStateTest.kt rename to examples/welcome/login/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/BaseStateTest.kt index 7da04a8..bcd0871 100644 --- a/examples/welcome/login/src/test/java/com/motorro/statemachine/login/model/state/BaseStateTest.kt +++ b/examples/welcome/login/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/BaseStateTest.kt @@ -11,16 +11,16 @@ * limitations under the License. */ -package com.motorro.statemachine.login.model.state +package com.motorro.commonstatemachine.examples.welcome.login.model.state import androidx.annotation.CallSuper import com.motorro.commonstatemachine.CommonStateMachine -import com.motorro.statemachine.commonapi.welcome.data.GOOD -import com.motorro.statemachine.commonapi.welcome.model.state.WelcomeFeatureHost -import com.motorro.statemachine.commoncore.resources.ResourceWrapper -import com.motorro.statemachine.login.data.LoginGesture -import com.motorro.statemachine.login.data.LoginUiState -import com.motorro.statemachine.login.model.LoginRenderer +import com.motorro.commonstatemachine.examples.commoncore.resources.ResourceWrapper +import com.motorro.commonstatemachine.examples.welcome.commonapi.data.GOOD +import com.motorro.commonstatemachine.examples.welcome.commonapi.model.state.WelcomeFeatureHost +import com.motorro.commonstatemachine.examples.welcome.login.data.LoginGesture +import com.motorro.commonstatemachine.examples.welcome.login.data.LoginUiState +import com.motorro.commonstatemachine.examples.welcome.login.model.LoginRenderer import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.Dispatchers diff --git a/examples/welcome/login/src/test/java/com/motorro/statemachine/login/model/state/CredentialsCheckStateTest.kt b/examples/welcome/login/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/CredentialsCheckStateTest.kt similarity index 84% rename from examples/welcome/login/src/test/java/com/motorro/statemachine/login/model/state/CredentialsCheckStateTest.kt rename to examples/welcome/login/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/CredentialsCheckStateTest.kt index cd97c62..31bae83 100644 --- a/examples/welcome/login/src/test/java/com/motorro/statemachine/login/model/state/CredentialsCheckStateTest.kt +++ b/examples/welcome/login/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/CredentialsCheckStateTest.kt @@ -13,13 +13,19 @@ @file:OptIn(ExperimentalCoroutinesApi::class) -package com.motorro.statemachine.login.model.state - -import com.motorro.statemachine.commonapi.welcome.data.GOOD -import com.motorro.statemachine.login.data.LoginDataState -import com.motorro.statemachine.login.data.LoginGesture -import com.motorro.statemachine.login.usecase.CheckCredentials -import io.mockk.* +package com.motorro.commonstatemachine.examples.welcome.login.model.state + +import com.motorro.commonstatemachine.examples.welcome.commonapi.data.GOOD +import com.motorro.commonstatemachine.examples.welcome.login.data.LoginDataState +import com.motorro.commonstatemachine.examples.welcome.login.data.LoginGesture +import com.motorro.commonstatemachine.examples.welcome.login.usecase.CheckCredentials +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest diff --git a/examples/welcome/login/src/test/java/com/motorro/statemachine/login/model/state/ErrorStateTest.kt b/examples/welcome/login/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/ErrorStateTest.kt similarity index 85% rename from examples/welcome/login/src/test/java/com/motorro/statemachine/login/model/state/ErrorStateTest.kt rename to examples/welcome/login/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/ErrorStateTest.kt index 36adb2e..c9884af 100644 --- a/examples/welcome/login/src/test/java/com/motorro/statemachine/login/model/state/ErrorStateTest.kt +++ b/examples/welcome/login/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/ErrorStateTest.kt @@ -11,11 +11,11 @@ * limitations under the License. */ -package com.motorro.statemachine.login.model.state +package com.motorro.commonstatemachine.examples.welcome.login.model.state -import com.motorro.statemachine.commonapi.welcome.data.BAD -import com.motorro.statemachine.login.data.LoginDataState -import com.motorro.statemachine.login.data.LoginGesture +import com.motorro.commonstatemachine.examples.welcome.commonapi.data.BAD +import com.motorro.commonstatemachine.examples.welcome.login.data.LoginDataState +import com.motorro.commonstatemachine.examples.welcome.login.data.LoginGesture import io.mockk.every import io.mockk.mockk import io.mockk.verify diff --git a/examples/welcome/login/src/test/java/com/motorro/statemachine/login/model/state/PasswordEntryStateTest.kt b/examples/welcome/login/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/PasswordEntryStateTest.kt similarity index 91% rename from examples/welcome/login/src/test/java/com/motorro/statemachine/login/model/state/PasswordEntryStateTest.kt rename to examples/welcome/login/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/PasswordEntryStateTest.kt index b55e06d..c7a09f0 100644 --- a/examples/welcome/login/src/test/java/com/motorro/statemachine/login/model/state/PasswordEntryStateTest.kt +++ b/examples/welcome/login/src/test/kotlin/com/motorro/commonstatemachine/examples/welcome/login/model/state/PasswordEntryStateTest.kt @@ -11,11 +11,11 @@ * limitations under the License. */ -package com.motorro.statemachine.login.model.state +package com.motorro.commonstatemachine.examples.welcome.login.model.state -import com.motorro.statemachine.commonapi.welcome.data.GOOD -import com.motorro.statemachine.login.data.LoginDataState -import com.motorro.statemachine.login.data.LoginGesture +import com.motorro.commonstatemachine.examples.welcome.commonapi.data.GOOD +import com.motorro.commonstatemachine.examples.welcome.login.data.LoginDataState +import com.motorro.commonstatemachine.examples.welcome.login.data.LoginGesture import io.mockk.Runs import io.mockk.every import io.mockk.just diff --git a/examples/welcome/register/build.gradle.kts b/examples/welcome/register/build.gradle.kts index 5170669..8513ab3 100644 --- a/examples/welcome/register/build.gradle.kts +++ b/examples/welcome/register/build.gradle.kts @@ -1,7 +1,3 @@ -@file:Suppress("unused") - -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - /* * Copyright 2022 Nikolai Kotchetkov. * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,80 +12,25 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget */ plugins { - alias(libs.plugins.android.lib) - alias(libs.plugins.google.ksp) - alias(libs.plugins.hilt) - alias(libs.plugins.compose) + alias(libs.plugins.motorro.cmp) + alias(libs.plugins.koin) } - -val versionCode: String by project.extra -val versionName: String by project.extra -val androidMinSdkVersion: Int by project.extra -val androidCompileSdkVersion: Int by project.extra - - -android { - // Ensure androidCompileSdkVersion, androidMinSdkVersion, and androidTargetSdkVersion - // are defined in your project's gradle.properties or root build.gradle.kts - 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") +kotlin { + sourceSets { + commonMain.dependencies { + implementation(project(":commonstatemachine")) + implementation(project(":coroutines")) + implementation(project(":examples:commoncore")) + implementation(project(":examples:welcome:commonapi")) + implementation(libs.kotlin.coroutines.core) + implementation(libs.napier) + implementation(project.dependencies.platform(libs.koin.bom)) + implementation(libs.koin.core) + implementation(libs.koin.annotations) } - } - compileOptions { - isCoreLibraryDesugaringEnabled = true - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 - } - kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) + commonTest.dependencies { + implementation(libs.test.kotlin.coroutines) } } - buildFeatures { - compose = true - } - namespace = "com.motorro.statemachine.register" -} - -dependencies { - implementation(project(":commonstatemachine")) - implementation(project(":examples:welcome:commonapi")) - implementation(project(":examples:commoncore")) - implementation(project(":examples:androidcore")) - api(project(":examples:welcome:commonregister")) - - coreLibraryDesugaring(libs.desugaring) - - 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.foundation) - implementation(libs.compose.foundation.layouts) // In Groovy, this was 'foundation.layouts'. Ensure this maps correctly in your TOML. - - implementation(libs.hilt.android) - implementation(libs.hilt.compose) // This was 'libs.hilt.compose'. Verify it maps to 'androidx.hilt:hilt-navigation-compose' or your intended Hilt Compose library. - ksp(libs.hilt.compiler) - ksp(libs.hilt.compiler.androidx) // This was 'libs.hilt.compiler.androidx'. Verify it maps to 'com.google.dagger:hilt-android-compiler' or the intended artifact. - - debugImplementation(libs.compose.tooling) - - testImplementation(libs.bundles.test.core) - testImplementation(libs.test.androidx.arch) - testImplementation(libs.test.kotlin.coroutines) } diff --git a/examples/welcome/register/src/commonMain/composeResources/values/strings.xml b/examples/welcome/register/src/commonMain/composeResources/values/strings.xml new file mode 100644 index 0000000..ab51dbb --- /dev/null +++ b/examples/welcome/register/src/commonMain/composeResources/values/strings.xml @@ -0,0 +1,10 @@ + + Enter password for: + Any password at least 6 chars long will do + Password + Repeat password + Previous + Next + Password should be at least 6 chars long + Passwords mismatch + diff --git a/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/RegisterFlowStarter.kt b/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/RegisterFlowStarter.kt new file mode 100644 index 0000000..7c3db63 --- /dev/null +++ b/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/RegisterFlowStarter.kt @@ -0,0 +1,10 @@ +package com.motorro.commonstatemachine.examples.welcome.register + +import com.motorro.commonstatemachine.examples.welcome.commonapi.model.state.FlowStarter +import com.motorro.commonstatemachine.examples.welcome.register.data.RegisterGesture +import com.motorro.commonstatemachine.examples.welcome.register.data.RegisterUiState + +/** + * Starts login flow + */ +interface RegisterFlowStarter : FlowStarter \ No newline at end of file diff --git a/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/RegisterModule.kt b/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/RegisterModule.kt new file mode 100644 index 0000000..7d801b9 --- /dev/null +++ b/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/RegisterModule.kt @@ -0,0 +1,8 @@ +package com.motorro.commonstatemachine.examples.welcome.register + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +class RegisterModule \ No newline at end of file diff --git a/examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/data/PasswordValidationError.kt b/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/data/PasswordValidationError.kt similarity index 90% rename from examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/data/PasswordValidationError.kt rename to examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/data/PasswordValidationError.kt index 5383146..0fc58ca 100644 --- a/examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/data/PasswordValidationError.kt +++ b/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/data/PasswordValidationError.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.register.data +package com.motorro.commonstatemachine.examples.welcome.register.data enum class PasswordValidationError { PASSWORD_LENGTH, diff --git a/examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/data/RegisterDataState.kt b/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/data/RegisterDataState.kt similarity index 91% rename from examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/data/RegisterDataState.kt rename to examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/data/RegisterDataState.kt index e28f5b5..5d30425 100644 --- a/examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/data/RegisterDataState.kt +++ b/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/data/RegisterDataState.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.register.data +package com.motorro.commonstatemachine.examples.welcome.register.data /** * Login flow inter-state data diff --git a/examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/data/RegisterGesture.kt b/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/data/RegisterGesture.kt similarity index 94% rename from examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/data/RegisterGesture.kt rename to examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/data/RegisterGesture.kt index 3e24a6d..a872667 100644 --- a/examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/data/RegisterGesture.kt +++ b/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/data/RegisterGesture.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.register.data +package com.motorro.commonstatemachine.examples.welcome.register.data /** * Login flow UI gestures diff --git a/examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/data/RegisterUiState.kt b/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/data/RegisterUiState.kt similarity index 94% rename from examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/data/RegisterUiState.kt rename to examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/data/RegisterUiState.kt index 6d2c39c..3ed2603 100644 --- a/examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/data/RegisterUiState.kt +++ b/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/data/RegisterUiState.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.register.data +package com.motorro.commonstatemachine.examples.welcome.register.data /** * Login flow UI state diff --git a/examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/model/RegistrationRenderer.kt b/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/model/RegistrationRenderer.kt similarity index 65% rename from examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/model/RegistrationRenderer.kt rename to examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/model/RegistrationRenderer.kt index 7be4540..5c188ff 100644 --- a/examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/model/RegistrationRenderer.kt +++ b/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/model/RegistrationRenderer.kt @@ -11,18 +11,21 @@ * limitations under the License. */ -package com.motorro.statemachine.register.model +package com.motorro.commonstatemachine.examples.welcome.register.model -import com.motorro.statemachine.commoncore.resources.ResourceWrapper -import com.motorro.statemachine.register.data.PasswordValidationError -import com.motorro.statemachine.register.data.RegisterDataState -import com.motorro.statemachine.register.data.RegisterUiState +import com.motorro.commonstatemachine.examples.commoncore.resources.ResourceWrapper +import com.motorro.commonstatemachine.examples.welcome.register.data.PasswordValidationError +import com.motorro.commonstatemachine.examples.welcome.register.data.PasswordValidationError.PASSWORD_LENGTH +import com.motorro.commonstatemachine.examples.welcome.register.data.PasswordValidationError.PASSWORD_MISMATCH +import com.motorro.commonstatemachine.examples.welcome.register.data.RegisterDataState +import com.motorro.commonstatemachine.examples.welcome.register.data.RegisterUiState +import org.koin.core.annotation.Factory /** * Renders data state * Externalizes resource usage */ -interface RegistrationRenderer { +internal interface RegistrationRenderer { /** * Renders password entry */ @@ -37,6 +40,7 @@ interface RegistrationRenderer { */ fun renderRegistration(data: RegisterDataState): RegisterUiState + @Factory class Impl(resourceWrapper: ResourceWrapper): RegistrationRenderer, ResourceWrapper by resourceWrapper { /** * Renders password entry @@ -62,4 +66,7 @@ interface RegistrationRenderer { /** * Returns password error text */ -expect fun ResourceWrapper.getPasswordError(error: PasswordValidationError): String +fun ResourceWrapper.getPasswordError(error: PasswordValidationError): String = when(error) { + PASSWORD_LENGTH -> "Password should be at least 6 chars long" + PASSWORD_MISMATCH -> "Passwords mismatch" +} diff --git a/examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/model/state/PasswordEntryState.kt b/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/model/state/PasswordEntryState.kt similarity index 81% rename from examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/model/state/PasswordEntryState.kt rename to examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/model/state/PasswordEntryState.kt index 92ea7b7..6890a38 100644 --- a/examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/model/state/PasswordEntryState.kt +++ b/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/model/state/PasswordEntryState.kt @@ -11,19 +11,19 @@ * limitations under the License. */ -package com.motorro.statemachine.register.model.state +package com.motorro.commonstatemachine.examples.welcome.register.model.state -import com.motorro.statemachine.commoncore.log.Logger -import com.motorro.statemachine.register.data.PasswordValidationError -import com.motorro.statemachine.register.data.PasswordValidationError.PASSWORD_LENGTH -import com.motorro.statemachine.register.data.PasswordValidationError.PASSWORD_MISMATCH -import com.motorro.statemachine.register.data.RegisterDataState -import com.motorro.statemachine.register.data.RegisterGesture +import com.motorro.commonstatemachine.examples.commoncore.log.Logger +import com.motorro.commonstatemachine.examples.welcome.register.data.PasswordValidationError +import com.motorro.commonstatemachine.examples.welcome.register.data.PasswordValidationError.PASSWORD_LENGTH +import com.motorro.commonstatemachine.examples.welcome.register.data.PasswordValidationError.PASSWORD_MISMATCH +import com.motorro.commonstatemachine.examples.welcome.register.data.RegisterDataState +import com.motorro.commonstatemachine.examples.welcome.register.data.RegisterGesture /** * Password entry screen */ -class PasswordEntryState( +internal class PasswordEntryState( context: RegisterContext, private var data: RegisterDataState ) : RegisterState(context) { @@ -101,4 +101,3 @@ class PasswordEntryState( else -> null } } - diff --git a/examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/model/state/RegisterContext.kt b/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/model/state/RegisterContext.kt similarity index 73% rename from examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/model/state/RegisterContext.kt rename to examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/model/state/RegisterContext.kt index f839264..dec5c69 100644 --- a/examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/model/state/RegisterContext.kt +++ b/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/model/state/RegisterContext.kt @@ -11,16 +11,15 @@ * limitations under the License. */ -package com.motorro.statemachine.register.model.state - -import com.motorro.statemachine.commonapi.welcome.model.state.WelcomeFeatureHost -import com.motorro.statemachine.register.model.RegistrationRenderer +package com.motorro.commonstatemachine.examples.welcome.register.model.state +import com.motorro.commonstatemachine.examples.welcome.commonapi.model.state.WelcomeFeatureHost +import com.motorro.commonstatemachine.examples.welcome.register.model.RegistrationRenderer /** * Context for login flow */ -interface RegisterContext { +internal interface RegisterContext { /** * State factory */ @@ -35,4 +34,4 @@ interface RegisterContext { * Renderer */ val renderer: RegistrationRenderer -} \ No newline at end of file +} diff --git a/examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/model/state/RegisterState.kt b/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/model/state/RegisterState.kt similarity index 65% rename from examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/model/state/RegisterState.kt rename to examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/model/state/RegisterState.kt index 96de414..40b3dc9 100644 --- a/examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/model/state/RegisterState.kt +++ b/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/model/state/RegisterState.kt @@ -11,23 +11,21 @@ * limitations under the License. */ -package com.motorro.statemachine.register.model.state +package com.motorro.commonstatemachine.examples.welcome.register.model.state import com.motorro.commonstatemachine.coroutines.CoroutineState -import com.motorro.statemachine.commoncore.log.Logger -import com.motorro.statemachine.register.data.RegisterGesture -import com.motorro.statemachine.register.data.RegisterUiState +import com.motorro.commonstatemachine.examples.commoncore.log.Logger +import com.motorro.commonstatemachine.examples.welcome.register.data.RegisterGesture +import com.motorro.commonstatemachine.examples.welcome.register.data.RegisterUiState /** * Base class for registration flow state */ -abstract class RegisterState( - context: RegisterContext -): CoroutineState(), RegisterContext by context { +internal abstract class RegisterState(context: RegisterContext): CoroutineState(), RegisterContext by context { /** * A part of [process] template to process UI gesture */ override fun doProcess(gesture: RegisterGesture) { Logger.w("Unsupported gesture: %s", gesture) } -} \ No newline at end of file +} diff --git a/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/model/state/RegisterStateFactory.kt b/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/model/state/RegisterStateFactory.kt new file mode 100644 index 0000000..58ceae3 --- /dev/null +++ b/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/model/state/RegisterStateFactory.kt @@ -0,0 +1,47 @@ +/* + * 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.commonstatemachine.examples.welcome.register.model.state + +import com.motorro.commonstatemachine.CommonMachineState +import com.motorro.commonstatemachine.examples.welcome.register.RegisterFlowStarter +import com.motorro.commonstatemachine.examples.welcome.register.data.RegisterDataState +import com.motorro.commonstatemachine.examples.welcome.register.data.RegisterGesture +import com.motorro.commonstatemachine.examples.welcome.register.data.RegisterUiState + +/** + * Login flow state factory + */ +internal interface RegisterStateFactory : RegisterFlowStarter { + /** + * Creates a starting state + * @param email Email to proceed with + */ + override fun start(email: String): CommonMachineState = passwordEntry( + RegisterDataState(email) + ) + + /** + * Password entry screen + * @param data Registration data state + */ + fun passwordEntry(data: RegisterDataState): RegisterState + + /** + * Registers user + * @param data Data state + */ + fun registering(data: RegisterDataState): RegisterState + +} + diff --git a/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/model/state/RegisterStateFactoryImpl.kt b/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/model/state/RegisterStateFactoryImpl.kt new file mode 100644 index 0000000..c06e4c4 --- /dev/null +++ b/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/model/state/RegisterStateFactoryImpl.kt @@ -0,0 +1,44 @@ +package com.motorro.commonstatemachine.examples.welcome.register.model.state + +import com.motorro.commonstatemachine.examples.commoncore.log.Logger +import com.motorro.commonstatemachine.examples.welcome.commonapi.model.state.WelcomeFeatureHost +import com.motorro.commonstatemachine.examples.welcome.register.RegisterFlowStarter +import com.motorro.commonstatemachine.examples.welcome.register.data.RegisterDataState +import com.motorro.commonstatemachine.examples.welcome.register.model.RegistrationRenderer +import org.koin.core.annotation.Factory +import org.koin.core.annotation.InjectedParam + +/** + * [RegisterStateFactory] implementation + */ +@Factory(binds = [RegisterFlowStarter::class]) +internal class RegisterStateFactoryImpl( + @InjectedParam host: WelcomeFeatureHost, + renderer: RegistrationRenderer, + private val createRegistration: RegistrationState.StateFactory +) : RegisterStateFactory { + + private val context: RegisterContext = object : RegisterContext { + override val factory: RegisterStateFactory = this@RegisterStateFactoryImpl + override val host: WelcomeFeatureHost = host + override val renderer: RegistrationRenderer = renderer + } + + /** + * Password entry screen + * @param data Registration data state + */ + override fun passwordEntry(data: RegisterDataState): RegisterState { + Logger.d("Creating 'Password entry'...") + return PasswordEntryState(context, data) + } + + /** + * Registers user + * @param data Data state + */ + override fun registering(data: RegisterDataState): RegisterState { + Logger.d("Creating 'Credentials check'...") + return createRegistration(context, data) + } +} \ No newline at end of file diff --git a/examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/model/state/RegistrationState.kt b/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/model/state/RegistrationState.kt similarity index 78% rename from examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/model/state/RegistrationState.kt rename to examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/model/state/RegistrationState.kt index a58624d..d7bf40b 100644 --- a/examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/model/state/RegistrationState.kt +++ b/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/model/state/RegistrationState.kt @@ -11,18 +11,19 @@ * limitations under the License. */ -package com.motorro.statemachine.register.model.state +package com.motorro.commonstatemachine.examples.welcome.register.model.state -import com.motorro.statemachine.commoncore.log.Logger -import com.motorro.statemachine.register.data.RegisterDataState -import com.motorro.statemachine.register.data.RegisterGesture -import com.motorro.statemachine.register.usecase.Registration +import com.motorro.commonstatemachine.examples.commoncore.log.Logger +import com.motorro.commonstatemachine.examples.welcome.register.data.RegisterDataState +import com.motorro.commonstatemachine.examples.welcome.register.data.RegisterGesture +import com.motorro.commonstatemachine.examples.welcome.register.usecase.Registration import kotlinx.coroutines.launch +import org.koin.core.annotation.Factory /** * Emulates login operation */ -class RegistrationState( +internal class RegistrationState( context: RegisterContext, private val data: RegisterDataState, private val register: Registration @@ -64,7 +65,8 @@ class RegistrationState( /** * [RegisterState] factory */ - class Factory(private val register: Registration) { + @Factory + class StateFactory(private val register: Registration) { operator fun invoke( context: RegisterContext, data: RegisterDataState @@ -74,4 +76,4 @@ class RegistrationState( register ) } -} \ No newline at end of file +} diff --git a/examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/usecase/Registration.kt b/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/usecase/Registration.kt similarity index 80% rename from examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/usecase/Registration.kt rename to examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/usecase/Registration.kt index 4432e46..758971f 100644 --- a/examples/welcome/commonregister/src/commonMain/kotlin/com/motorro/statemachine/register/usecase/Registration.kt +++ b/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/usecase/Registration.kt @@ -11,17 +11,18 @@ * limitations under the License. */ -package com.motorro.statemachine.register.usecase +package com.motorro.commonstatemachine.examples.welcome.register.usecase -import com.motorro.statemachine.commonapi.NETWORK_DELAY -import com.motorro.statemachine.commoncore.coroutines.DispatcherProvider +import com.motorro.commonstatemachine.examples.commoncore.coroutines.DispatcherProvider +import com.motorro.commonstatemachine.examples.welcome.commonapi.NETWORK_DELAY import kotlinx.coroutines.delay import kotlinx.coroutines.withContext +import org.koin.core.annotation.Factory /** * Emulates user registration */ -interface Registration { +internal interface Registration { /** * Registers user * @param email Email to register @@ -29,6 +30,7 @@ interface Registration { */ suspend operator fun invoke(email: String, password: String): Boolean + @Factory class Impl(private val dispatchers: DispatcherProvider): Registration { /** * Registers user diff --git a/examples/welcome/register/src/main/java/com/motorro/statemachine/register/view/PasswordEntry.kt b/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/view/PasswordEntry.kt similarity index 76% rename from examples/welcome/register/src/main/java/com/motorro/statemachine/register/view/PasswordEntry.kt rename to examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/view/PasswordEntry.kt index 5eb06aa..81cba86 100644 --- a/examples/welcome/register/src/main/java/com/motorro/statemachine/register/view/PasswordEntry.kt +++ b/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/view/PasswordEntry.kt @@ -11,7 +11,7 @@ * limitations under the License. */ -package com.motorro.statemachine.register.view +package com.motorro.commonstatemachine.examples.welcome.register.view import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -34,12 +34,18 @@ 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.input.PasswordVisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.motorro.statemachine.register.R -import com.motorro.statemachine.register.data.RegisterUiState +import com.motorro.commonstatemachine.examples.welcome.register.Res +import com.motorro.commonstatemachine.examples.welcome.register.data.RegisterUiState +import com.motorro.commonstatemachine.examples.welcome.register.register_password_entry_desc +import com.motorro.commonstatemachine.examples.welcome.register.register_password_entry_label +import com.motorro.commonstatemachine.examples.welcome.register.register_password_entry_next +import com.motorro.commonstatemachine.examples.welcome.register.register_password_entry_prev +import com.motorro.commonstatemachine.examples.welcome.register.register_password_entry_title +import com.motorro.commonstatemachine.examples.welcome.register.register_repeat_password_entry_label +import org.jetbrains.compose.resources.stringResource @Composable fun PasswordEntry( @@ -63,7 +69,7 @@ fun PasswordEntry( ) { Column(modifier = Modifier.fillMaxWidth()) { Text( - text = stringResource(id = R.string.register_password_entry_title), + text = stringResource(Res.string.register_password_entry_title), style = MaterialTheme.typography.headlineMedium ) @@ -76,7 +82,7 @@ fun PasswordEntry( Spacer(modifier = Modifier.height(12.dp)) Text( - text = stringResource(id = R.string.register_password_entry_desc), + text = stringResource(Res.string.register_password_entry_desc), style = MaterialTheme.typography.bodyMedium ) @@ -86,7 +92,7 @@ fun PasswordEntry( value = state.password, onValueChange = onPasswordChanged, modifier = Modifier.fillMaxWidth(), - label = { Text(stringResource(id = R.string.register_password_entry_label)) }, + label = { Text(stringResource(Res.string.register_password_entry_label)) }, visualTransformation = PasswordVisualTransformation() ) @@ -96,7 +102,7 @@ fun PasswordEntry( value = state.repeatPassword, onValueChange = onRepeatPasswordChanged, modifier = Modifier.fillMaxWidth(), - label = { Text(stringResource(id = R.string.register_repeat_password_entry_label)) }, + label = { Text(stringResource(Res.string.register_repeat_password_entry_label)) }, visualTransformation = PasswordVisualTransformation() ) @@ -116,13 +122,13 @@ fun PasswordEntry( verticalAlignment = Alignment.CenterVertically ) { Button(onClick = onPrevious) { - Text(text = stringResource(id = R.string.register_password_entry_prev)) + Text(text = stringResource(Res.string.register_password_entry_prev)) } Spacer(modifier = Modifier.width(12.dp)) Button(onClick = onNext) { - Text(text = stringResource(id = R.string.register_password_entry_next)) + Text(text = stringResource(Res.string.register_password_entry_next)) } } } @@ -143,4 +149,4 @@ fun EmailEntryPreview() { onPrevious = {}, onNext = {} ) -} \ No newline at end of file +} diff --git a/examples/welcome/register/src/main/java/com/motorro/statemachine/register/view/RegistrationScreen.kt b/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/view/RegistrationScreen.kt similarity index 79% rename from examples/welcome/register/src/main/java/com/motorro/statemachine/register/view/RegistrationScreen.kt rename to examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/view/RegistrationScreen.kt index 5ccc356..0863f92 100644 --- a/examples/welcome/register/src/main/java/com/motorro/statemachine/register/view/RegistrationScreen.kt +++ b/examples/welcome/register/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/welcome/register/view/RegistrationScreen.kt @@ -11,12 +11,12 @@ * limitations under the License. */ -package com.motorro.statemachine.register.view +package com.motorro.commonstatemachine.examples.welcome.register.view import androidx.compose.runtime.Composable -import com.motorro.statemachine.androidcore.compose.Loading -import com.motorro.statemachine.register.data.RegisterGesture -import com.motorro.statemachine.register.data.RegisterUiState +import com.motorro.commonstatemachine.examples.commoncore.ui.Loading +import com.motorro.commonstatemachine.examples.welcome.register.data.RegisterGesture +import com.motorro.commonstatemachine.examples.welcome.register.data.RegisterUiState /** * Registration screen flow @@ -33,4 +33,4 @@ fun RegistrationScreen(state: RegisterUiState, onGesture: (RegisterGesture) -> U onNext = { onGesture(RegisterGesture.Action) } ) } -} \ No newline at end of file +} diff --git a/examples/welcome/commonregister/src/commonTest/kotlin/com/motorro/statemachine/register/mocks.kt b/examples/welcome/register/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/welocme/register/mocks.kt similarity index 70% rename from examples/welcome/commonregister/src/commonTest/kotlin/com/motorro/statemachine/register/mocks.kt rename to examples/welcome/register/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/welocme/register/mocks.kt index 0eeeb1e..da77bc1 100644 --- a/examples/welcome/commonregister/src/commonTest/kotlin/com/motorro/statemachine/register/mocks.kt +++ b/examples/welcome/register/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/welocme/register/mocks.kt @@ -11,26 +11,26 @@ * limitations under the License. */ -package com.motorro.statemachine.register +package com.motorro.commonstatemachine.examples.welcome.register import com.motorro.commonstatemachine.CommonMachineState import com.motorro.commonstatemachine.CommonStateMachine -import com.motorro.statemachine.commonapi.welcome.model.state.WelcomeFeatureHost -import com.motorro.statemachine.commoncore.resources.ResourceWrapper -import com.motorro.statemachine.register.data.PasswordValidationError -import com.motorro.statemachine.register.data.RegisterDataState -import com.motorro.statemachine.register.data.RegisterGesture -import com.motorro.statemachine.register.data.RegisterUiState -import com.motorro.statemachine.register.model.RegistrationRenderer -import com.motorro.statemachine.register.model.state.RegisterContext -import com.motorro.statemachine.register.model.state.RegisterState -import com.motorro.statemachine.register.model.state.RegisterStateFactory -import com.motorro.statemachine.register.usecase.Registration +import com.motorro.commonstatemachine.examples.commoncore.resources.ResourceWrapper +import com.motorro.commonstatemachine.examples.welcome.commonapi.model.state.WelcomeFeatureHost +import com.motorro.commonstatemachine.examples.welcome.register.data.PasswordValidationError +import com.motorro.commonstatemachine.examples.welcome.register.data.RegisterDataState +import com.motorro.commonstatemachine.examples.welcome.register.data.RegisterGesture +import com.motorro.commonstatemachine.examples.welcome.register.data.RegisterUiState +import com.motorro.commonstatemachine.examples.welcome.register.model.RegistrationRenderer +import com.motorro.commonstatemachine.examples.welcome.register.model.state.RegisterContext +import com.motorro.commonstatemachine.examples.welcome.register.model.state.RegisterState +import com.motorro.commonstatemachine.examples.welcome.register.model.state.RegisterStateFactory +import com.motorro.commonstatemachine.examples.welcome.register.usecase.Registration const val R_STRING = "string" val R_CONTENT = RegisterUiState.Loading -class StateMock(context: RegisterContext) : RegisterState(context) { +internal class StateMock(context: RegisterContext) : RegisterState(context) { var started = false val processed = mutableListOf() var cleared = false @@ -48,7 +48,7 @@ class StateMock(context: RegisterContext) : RegisterState(context) { } } -class MachineMock : CommonStateMachine { +internal class MachineMock : CommonStateMachine { val machineStates = mutableListOf>() val processed = mutableListOf() val uiStates = mutableListOf() @@ -80,13 +80,13 @@ class MachineMock : CommonStateMachine { } } -class MockContext : RegisterContext { +internal class MockContext : RegisterContext { override val factory: MockFactory = MockFactory(this) override val host: MockHost = MockHost() override val renderer: MockRenderer = MockRenderer() } -class MockHost : WelcomeFeatureHost { +internal class MockHost : WelcomeFeatureHost { var backToEmails = 0 var completes = 0 @@ -99,7 +99,7 @@ class MockHost : WelcomeFeatureHost { } } -class MockFactory(private val context: RegisterContext) : RegisterStateFactory { +internal class MockFactory(private val context: RegisterContext) : RegisterStateFactory { val passwordEntries = mutableListOf() val registers = mutableListOf() @@ -114,7 +114,7 @@ class MockFactory(private val context: RegisterContext) : RegisterStateFactory { } } -class MockRenderer : RegistrationRenderer { +internal class MockRenderer : RegistrationRenderer { val passwordEntries = mutableListOf>() val registrations = mutableListOf() @@ -133,7 +133,7 @@ class MockRenderer : RegistrationRenderer { } } -class MockResourceWrapper : ResourceWrapper { +internal class MockResourceWrapper : ResourceWrapper { val getStrings = mutableListOf>>() override fun getString(resId: Int, vararg args: Any): String { @@ -142,11 +142,11 @@ class MockResourceWrapper : ResourceWrapper { } } -class MockRegistration : Registration { +internal class MockRegistration : Registration { val invocations = mutableListOf>() override suspend fun invoke(email: String, password: String): Boolean { invocations.add(email to password) return true } -} \ No newline at end of file +} diff --git a/examples/welcome/commonregister/src/commonTest/kotlin/com/motorro/statemachine/register/model/state/BaseStateTest.kt b/examples/welcome/register/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/welocme/register/model/state/BaseStateTest.kt similarity index 74% rename from examples/welcome/commonregister/src/commonTest/kotlin/com/motorro/statemachine/register/model/state/BaseStateTest.kt rename to examples/welcome/register/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/welocme/register/model/state/BaseStateTest.kt index ac1d8ed..27b5a31 100644 --- a/examples/welcome/commonregister/src/commonTest/kotlin/com/motorro/statemachine/register/model/state/BaseStateTest.kt +++ b/examples/welcome/register/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/welocme/register/model/state/BaseStateTest.kt @@ -11,9 +11,13 @@ * limitations under the License. */ -package com.motorro.statemachine.register.model.state +package com.motorro.commonstatemachine.examples.welcome.register.model.state -import com.motorro.statemachine.register.* +import com.motorro.commonstatemachine.examples.welcome.register.MachineMock +import com.motorro.commonstatemachine.examples.welcome.register.MockContext +import com.motorro.commonstatemachine.examples.welcome.register.MockFactory +import com.motorro.commonstatemachine.examples.welcome.register.MockHost +import com.motorro.commonstatemachine.examples.welcome.register.MockResourceWrapper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher @@ -22,7 +26,7 @@ import kotlinx.coroutines.test.setMain import kotlin.test.AfterTest import kotlin.test.BeforeTest -open class BaseStateTest { +internal open class BaseStateTest { protected val stateMachine: MachineMock = MachineMock() protected val context: MockContext = MockContext() protected val factory: MockFactory = context.factory @@ -40,4 +44,4 @@ open class BaseStateTest { open fun after() { Dispatchers.resetMain() } -} \ No newline at end of file +} diff --git a/examples/welcome/commonregister/src/commonTest/kotlin/com/motorro/statemachine/register/model/state/PasswordEntryStateTest.kt b/examples/welcome/register/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/welocme/register/model/state/PasswordEntryStateTest.kt similarity index 89% rename from examples/welcome/commonregister/src/commonTest/kotlin/com/motorro/statemachine/register/model/state/PasswordEntryStateTest.kt rename to examples/welcome/register/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/welocme/register/model/state/PasswordEntryStateTest.kt index b45089d..1ac9213 100644 --- a/examples/welcome/commonregister/src/commonTest/kotlin/com/motorro/statemachine/register/model/state/PasswordEntryStateTest.kt +++ b/examples/welcome/register/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/welocme/register/model/state/PasswordEntryStateTest.kt @@ -13,24 +13,25 @@ @file:Suppress("RemoveExplicitTypeArguments") -package com.motorro.statemachine.register.model.state - -import com.motorro.statemachine.commonapi.welcome.data.GOOD -import com.motorro.statemachine.register.R_CONTENT -import com.motorro.statemachine.register.data.PasswordValidationError -import com.motorro.statemachine.register.data.RegisterDataState -import com.motorro.statemachine.register.data.RegisterGesture -import com.motorro.statemachine.register.data.RegisterUiState +package com.motorro.commonstatemachine.examples.welcome.register.model.state + +import com.motorro.commonstatemachine.examples.welcome.commonapi.data.GOOD +import com.motorro.commonstatemachine.examples.welcome.register.R_CONTENT +import com.motorro.commonstatemachine.examples.welcome.register.data.PasswordValidationError +import com.motorro.commonstatemachine.examples.welcome.register.data.RegisterDataState +import com.motorro.commonstatemachine.examples.welcome.register.data.RegisterGesture +import com.motorro.commonstatemachine.examples.welcome.register.data.RegisterUiState import kotlin.test.Test import kotlin.test.assertEquals -class PasswordEntryStateTest : BaseStateTest() { +internal class PasswordEntryStateTest : BaseStateTest() { private val password = "password" private val data = RegisterDataState(GOOD) private lateinit var state: PasswordEntryState - private fun createState(data: RegisterDataState = this.data) = PasswordEntryState(context, data) + private fun createState(data: RegisterDataState = this.data) = + PasswordEntryState(context, data) @Test fun displaysValidStateOnEmptyPasswordBeforeAction() { @@ -197,4 +198,3 @@ class PasswordEntryStateTest : BaseStateTest() { ) } } - diff --git a/examples/welcome/commonregister/src/commonTest/kotlin/com/motorro/statemachine/register/model/state/RegisterStateTest.kt b/examples/welcome/register/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/welocme/register/model/state/RegisterStateTest.kt similarity index 76% rename from examples/welcome/commonregister/src/commonTest/kotlin/com/motorro/statemachine/register/model/state/RegisterStateTest.kt rename to examples/welcome/register/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/welocme/register/model/state/RegisterStateTest.kt index 024e486..5cc478a 100644 --- a/examples/welcome/commonregister/src/commonTest/kotlin/com/motorro/statemachine/register/model/state/RegisterStateTest.kt +++ b/examples/welcome/register/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/welocme/register/model/state/RegisterStateTest.kt @@ -13,29 +13,31 @@ @file:Suppress("RemoveExplicitTypeArguments") -package com.motorro.statemachine.register.model.state +package com.motorro.commonstatemachine.examples.welcome.register.model.state -import com.motorro.statemachine.commonapi.welcome.data.GOOD -import com.motorro.statemachine.register.MockRegistration -import com.motorro.statemachine.register.R_CONTENT -import com.motorro.statemachine.register.data.RegisterDataState -import com.motorro.statemachine.register.data.RegisterGesture -import com.motorro.statemachine.register.data.RegisterUiState +import com.motorro.commonstatemachine.examples.welcome.commonapi.data.GOOD +import com.motorro.commonstatemachine.examples.welcome.register.MockRegistration +import com.motorro.commonstatemachine.examples.welcome.register.R_CONTENT +import com.motorro.commonstatemachine.examples.welcome.register.data.RegisterDataState +import com.motorro.commonstatemachine.examples.welcome.register.data.RegisterGesture +import com.motorro.commonstatemachine.examples.welcome.register.data.RegisterUiState import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import kotlin.test.AfterTest +import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals @OptIn(ExperimentalCoroutinesApi::class) -abstract class RegisterStateTest: BaseStateTest() { +internal abstract class RegisterStateTest: BaseStateTest() { private val password = "password" private val data = RegisterDataState(GOOD, password) private val registration = MockRegistration() private lateinit var state: RegistrationState + @BeforeTest override fun before() { super.before() state = RegistrationState(context, data, registration) @@ -87,4 +89,4 @@ abstract class RegisterStateTest: BaseStateTest() { host.backToEmails ) } -} \ No newline at end of file +} diff --git a/examples/welcome/register/src/main/AndroidManifest.xml b/examples/welcome/register/src/main/AndroidManifest.xml deleted file mode 100644 index cfc666d..0000000 --- a/examples/welcome/register/src/main/AndroidManifest.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/examples/welcome/register/src/main/java/com/motorro/statemachine/register/di/RegisterComponent.kt b/examples/welcome/register/src/main/java/com/motorro/statemachine/register/di/RegisterComponent.kt deleted file mode 100644 index 94db8b0..0000000 --- a/examples/welcome/register/src/main/java/com/motorro/statemachine/register/di/RegisterComponent.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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.register.di - -import com.motorro.statemachine.commonapi.welcome.model.state.WelcomeFeatureHost -import dagger.BindsInstance -import dagger.hilt.DefineComponent -import dagger.hilt.components.SingletonComponent - -@RegisterScope -@DefineComponent(parent = SingletonComponent::class) -interface RegisterComponent - -@DefineComponent.Builder -interface RegisterComponentBuilder { - /** - * Defines feature host graph - */ - fun host(@BindsInstance value: WelcomeFeatureHost): RegisterComponentBuilder - - /** - * Builds component - */ - fun build(): RegisterComponent -} diff --git a/examples/welcome/register/src/main/java/com/motorro/statemachine/register/di/RegisterEntryPoint.kt b/examples/welcome/register/src/main/java/com/motorro/statemachine/register/di/RegisterEntryPoint.kt deleted file mode 100644 index 132e35e..0000000 --- a/examples/welcome/register/src/main/java/com/motorro/statemachine/register/di/RegisterEntryPoint.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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.register.di - -import com.motorro.statemachine.commonapi.welcome.model.state.FlowStarter -import com.motorro.statemachine.register.data.RegisterGesture -import com.motorro.statemachine.register.data.RegisterUiState -import dagger.hilt.EntryPoint -import dagger.hilt.InstallIn - -/** - * Register feature entry point - */ -@EntryPoint -@InstallIn(RegisterComponent::class) -interface RegisterEntryPoint { - /** - * Flow starter - */ - fun flowStarter(): FlowStarter -} \ No newline at end of file diff --git a/examples/welcome/register/src/main/java/com/motorro/statemachine/register/di/RegisterModule.kt b/examples/welcome/register/src/main/java/com/motorro/statemachine/register/di/RegisterModule.kt deleted file mode 100644 index 19d4ba2..0000000 --- a/examples/welcome/register/src/main/java/com/motorro/statemachine/register/di/RegisterModule.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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.register.di - -import com.motorro.statemachine.commonapi.welcome.model.state.FlowStarter -import com.motorro.statemachine.commonapi.welcome.model.state.WelcomeFeatureHost -import com.motorro.statemachine.commoncore.coroutines.DispatcherProvider -import com.motorro.statemachine.commoncore.resources.ResourceWrapper -import com.motorro.statemachine.register.data.RegisterGesture -import com.motorro.statemachine.register.data.RegisterUiState -import com.motorro.statemachine.register.model.RegistrationRenderer -import com.motorro.statemachine.register.model.state.RegisterStateFactory -import com.motorro.statemachine.register.model.state.RegistrationState -import com.motorro.statemachine.register.usecase.Registration -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn - -@Module -@InstallIn(RegisterComponent::class) -class RegisterModule { - @Provides - @RegisterScope - fun registration(dispatchers: DispatcherProvider): Registration = Registration.Impl(dispatchers) - - @Provides - @RegisterScope - fun registrationState(registration: Registration): RegistrationState.Factory = RegistrationState.Factory(registration) - - @Provides - @RegisterScope - fun renderer(resourceWrapper: ResourceWrapper): RegistrationRenderer = RegistrationRenderer.Impl(resourceWrapper) - - @Provides - @RegisterScope - fun factory( - host: WelcomeFeatureHost, - renderer: RegistrationRenderer, - createRegistration: RegistrationState.Factory - ): RegisterStateFactory = RegisterStateFactory.Impl( - host, - renderer, - createRegistration - ) - - @Provides - @RegisterScope - fun flowStarter(factory: RegisterStateFactory): FlowStarter = factory -} \ No newline at end of file diff --git a/examples/welcome/register/src/main/java/com/motorro/statemachine/register/di/RegisterScope.kt b/examples/welcome/register/src/main/java/com/motorro/statemachine/register/di/RegisterScope.kt deleted file mode 100644 index 29e1732..0000000 --- a/examples/welcome/register/src/main/java/com/motorro/statemachine/register/di/RegisterScope.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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.register.di - -import javax.inject.Scope - -/** - * Login feature scope - */ -@Scope -@MustBeDocumented -@Retention(value = AnnotationRetention.RUNTIME) -annotation class RegisterScope \ No newline at end of file diff --git a/examples/welcome/register/src/main/res/values/strings.xml b/examples/welcome/register/src/main/res/values/strings.xml deleted file mode 100644 index 07ad1a0..0000000 --- a/examples/welcome/register/src/main/res/values/strings.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - Enter password for: - Any password at least 6 chars long will do - Password - Repeat password - Previous - Next - \ No newline at end of file diff --git a/examples/welcome/welcome/.gitignore b/examples/welcome/welcome/.gitignore deleted file mode 100644 index 42afabf..0000000 --- a/examples/welcome/welcome/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/examples/welcome/welcome/build.gradle.kts b/examples/welcome/welcome/build.gradle.kts deleted file mode 100644 index af80b4c..0000000 --- a/examples/welcome/welcome/build.gradle.kts +++ /dev/null @@ -1,115 +0,0 @@ -@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 { - alias(libs.plugins.android.app) - alias(libs.plugins.google.ksp) - alias(libs.plugins.hilt) - alias(libs.plugins.compose) -} - -val versionCode: String by project.extra -val versionName: String by project.extra -val androidMinSdkVersion: Int by project.extra -val androidTargetSdkVersion: Int by project.extra -val androidCompileSdkVersion: Int by project.extra - -android { - // Ensure these version variables are defined in gradle.properties or root build.gradle.kts - compileSdk = androidCompileSdkVersion - defaultConfig { - applicationId = "com.motorro.statemachine.welcome" - minSdk = androidMinSdkVersion - targetSdk = androidTargetSdkVersion - versionCode = versionCode - versionName = versionName - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - vectorDrawables { - useSupportLibrary = true - } - } - - buildTypes { - release { - isMinifyEnabled = false // Changed from minifyEnabled - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") - } - } - compileOptions { - isCoreLibraryDesugaringEnabled = true - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 - } - kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - } - buildFeatures { - compose = true - } - packaging { - resources { - excludes += "/META-INF/{AL2.0,LGPL2.1}" - } - } - namespace = "com.motorro.statemachine" // Original was com.motorro.statemachine.welcome, but there's also a namespace further down. Ensure this is the correct primary namespace. The one at the end "com.motorro.statemachine" will take precedence. -} - -hilt { - enableAggregatingTask = true -} - -dependencies { - implementation(project(":commonstatemachine")) // Simplified project dependency - implementation(project(":coroutines")) - implementation(project(":examples:welcome:commonapi")) - implementation(project(":examples:commoncore")) - implementation(project(":examples:androidcore")) - implementation(project(":examples:welcome:login")) - implementation(project(":examples:welcome:register")) - - 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.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) // Was foundation.layouts in Groovy - - implementation(libs.hilt.android) - implementation(libs.hilt.compose) // Verify this maps to 'androidx.hilt:hilt-navigation-compose' or your intended Hilt Compose lib - ksp(libs.hilt.compiler) - ksp(libs.hilt.compiler.androidx) // Verify this maps to 'com.google.dagger:hilt-android-compiler' or intended Hilt AndroidX compiler - - debugImplementation(libs.compose.tooling) - - testImplementation(libs.bundles.test.core) - testImplementation(libs.test.androidx.arch) - testImplementation(libs.test.kotlin.coroutines) -} diff --git a/examples/welcome/welcome/proguard-rules.pro b/examples/welcome/welcome/proguard-rules.pro deleted file mode 100644 index 481bb43..0000000 --- a/examples/welcome/welcome/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# 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/welcome/welcome/src/main/java/com/motorro/statemachine/App.kt b/examples/welcome/welcome/src/main/java/com/motorro/statemachine/App.kt deleted file mode 100644 index 889e383..0000000 --- a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/App.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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 - -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/welcome/welcome/src/main/java/com/motorro/statemachine/di/ApplicationModule.kt b/examples/welcome/welcome/src/main/java/com/motorro/statemachine/di/ApplicationModule.kt deleted file mode 100644 index 7913a95..0000000 --- a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/di/ApplicationModule.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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 - -import android.app.Application -import android.content.Context -import com.motorro.statemachine.commoncore.coroutines.DispatcherProvider -import com.motorro.statemachine.commoncore.resources.ResourceWrapper -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -class ApplicationModule { - - @Provides - @Singleton - fun context(application: Application): Context = application - - @Provides - @Singleton - fun resourceWrapper(context: Context): ResourceWrapper = object : ResourceWrapper { - override fun getString(resId: Int, vararg args: Any): String = context.getString(resId, *args) - } - - @Provides - @Singleton - fun dispatchers(): DispatcherProvider = object : DispatcherProvider { - override val default: CoroutineDispatcher = Dispatchers.Default - override val main: CoroutineDispatcher = Dispatchers.Main - override val io: CoroutineDispatcher = Dispatchers.IO - } -} \ No newline at end of file diff --git a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/di/WelcomeModule.kt b/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/di/WelcomeModule.kt deleted file mode 100644 index 370cd33..0000000 --- a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/di/WelcomeModule.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.welcome.di - -import com.motorro.statemachine.welcome.model.state.WelcomeStateFactory -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped - -@Module -@InstallIn(ViewModelComponent::class) -interface WelcomeModule { - @Binds - @ViewModelScoped - fun factory(impl: WelcomeStateFactory.Impl): WelcomeStateFactory -} \ No newline at end of file diff --git a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/WelcomeStateFactory.kt b/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/WelcomeStateFactory.kt deleted file mode 100644 index 039cbf9..0000000 --- a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/welcome/model/state/WelcomeStateFactory.kt +++ /dev/null @@ -1,166 +0,0 @@ -/* - * 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.welcome.model.state - -import androidx.lifecycle.SavedStateHandle -import com.motorro.commonstatemachine.CommonMachineState -import com.motorro.statemachine.welcome.data.WelcomeDataState -import com.motorro.statemachine.welcome.data.WelcomeGesture -import com.motorro.statemachine.welcome.data.WelcomeUiState -import com.motorro.statemachine.welcome.model.WelcomeRenderer -import dagger.hilt.android.scopes.ViewModelScoped -import timber.log.Timber -import javax.inject.Inject - -/** - * Welcome state factory - */ -interface WelcomeStateFactory { - /** - * Preloads 'data' for registration - */ - fun preload(): CommonMachineState - - /** - * Creates welcome screen state - * @param welcomeGreeting Message to display on welcome screen - */ - fun welcome(welcomeGreeting: String): CommonMachineState - - /** - * Creates email-entry state - * @param data Data state - */ - fun emailEntry(data: WelcomeDataState? = null): CommonMachineState - - /** - * Checks if email is registered - * @param data Data state - */ - fun checkEmail(data: WelcomeDataState): CommonMachineState - - /** - * Enter existing user password - * @param data Data state - */ - fun loginFlow(data: WelcomeDataState): CommonMachineState - - /** - * Enter registration user password - * @param data Data state - */ - fun registrationFlow(data: WelcomeDataState): CommonMachineState - - /** - * Registration complete state - * @param email Registered user's email - */ - fun complete(email: String) : CommonMachineState - - /** - * Terminates registration flow - */ - fun terminate() : CommonMachineState - - /** - * [WelcomeStateFactory] implementation - */ - @ViewModelScoped - class Impl @Inject constructor( - savedStateHandle: SavedStateHandle, - renderer: WelcomeRenderer, - private val createPreloading: PreloadingState.Factory, - private val createLogin: LoginFlowState.Factory, - private val createRegister: RegistrationFlowState.Factory, - private val createEmailCheck: EmailCheckState.Factory - ) : WelcomeStateFactory { - - private val context: WelcomeContext = object : WelcomeContext { - override val factory = this@Impl - override val savedStateHandle = savedStateHandle - override val renderer: WelcomeRenderer = renderer - } - - /** - * Preloads 'data' for registration - */ - override fun preload(): CommonMachineState { - Timber.d("Creating 'Preloading'...") - return createPreloading(context) - } - - /** - * Creates welcome screen state - * @param welcomeGreeting Message to display on welcome screen - */ - override fun welcome(welcomeGreeting: String): CommonMachineState { - Timber.d("Creating 'Welcome'...") - return TermsAndConditionsState(context, welcomeGreeting) - } - - /** - * Creates email-entry state - * @param data Data state - */ - override fun emailEntry(data: WelcomeDataState?): CommonMachineState { - Timber.d("Creating 'Email entry'...") - return EmailEntryState(context, data) - } - - /** - * Checks if email is registered - * @param data Data state - */ - override fun checkEmail(data: WelcomeDataState): CommonMachineState { - Timber.d("Creating 'Check e-mail'...") - return createEmailCheck(context, data) - } - - /** - * Enter existing user password - * @param data Data state - */ - override fun loginFlow(data: WelcomeDataState): CommonMachineState { - Timber.d("Creating 'Login flow'...") - return createLogin(context, data) - } - - /** - * Enter registration user password - * @param data Data state - */ - override fun registrationFlow(data: WelcomeDataState): CommonMachineState { - Timber.d("Creating 'Registration flow'...") - return createRegister(context, data) - } - - /** - * Registration complete state - * @param email Registered user's email - */ - override fun complete(email: String): CommonMachineState { - Timber.d("Creating 'Complete'...") - return CompleteState(context, email) - } - - /** - * Terminates registration flow - */ - override fun terminate(): CommonMachineState { - Timber.d("Creating 'Terminated'...") - return TerminatedState(context) - } - - } -} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cb26bcd..2fb392b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,4 +1,8 @@ [versions] +androidMinSdkVersion = "24" +androidTargetSdkVersion = "37" +androidCompileSdkVersion = "37" +androidMinTestSdk = "28" androidGradlePlugin = "9.2.1" androidxAppcompat = "1.8.0" androidxCore = "1.19.0" @@ -9,8 +13,9 @@ compose = "2026.09.00" composeMultiplatform = "1.12.0" composeMultiplatformMaterial = "1.9.0" composeMultiplatformMaterialIcons = "1.7.3" +composeHotReload = "1.1.1" viewmodelMultiplatform = "2.11.0" -lificyclelMultiplatform = "2.11.0" +lifecycleMultiplatform = "2.11.0" navigationMultiplatform = "1.1.0" composeActivity = "1.13.0" composeViewmodel = "2.11.0" @@ -20,11 +25,12 @@ desugaring = "2.1.5" dokka = "2.2.0" git = "5.3.3" hilt = "2.60.1" -koin-plugin = "1.2.1" -koin-bom = "4.2.2" +koinPlugin = "1.2.1" +koinBom = "4.2.2" ksp = "2.3.12" kotlin = "2.4.20" kotlinDatetime = "0.8.0" +kotlinAnnotations = "26.1.0" lifecycle = "2.11.0" nexusPublish = "2.0.0" timber = "5.0.1" @@ -41,100 +47,120 @@ mokkery = "3.5.0" [plugins] compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } -composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" } +compose_multiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" } +compose_hot_reload = { id = "org.jetbrains.compose.hot-reload", version.ref = "composeHotReload" } android_app = { id = "com.android.application", version.ref = "androidGradlePlugin" } android_lib = { id = "com.android.library", version.ref = "androidGradlePlugin" } +android_test = { id = "com.android.test", version.ref = "androidGradlePlugin" } +android_dynamic_feature = { id = "com.android.dynamic-feature", version.ref = "androidGradlePlugin" } kotlin_jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin_android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin_multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } android_kotlin_multiplatform_library = { id = "com.android.kotlin.multiplatform.library", version.ref = "androidGradlePlugin" } kotlin_dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } -google-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +google_ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } -koin = { id = "io.insert-koin.compiler.plugin", version.ref = "koin-plugin" } +koin = { id = "io.insert-koin.compiler.plugin", version.ref = "koinPlugin" } mockery = { id = "dev.mokkery", version.ref = "mokkery" } git = { id = "org.ajoberstar.grgit", version.ref = "git" } nexus_publish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "nexusPublish" } +# Build-logic aliases +motorro_jvm = { id = "motorro.jvmLibrary" } +motorro_android_lib = { id = "motorro.androidLibrary" } +motorro_android_lib_compose = { id = "motorro.androidComposeLibrary" } +motorro_android_app = { id = "motorro.androidApplication" } +motorro_android_test = { id = "motorro.androidTest" } +motorro_android_df = { id = "motorro.androidDynamicFeature" } +motorro_kmp = { id = "motorro.kmp" } +motorro_cmp = { id = "motorro.cmp" } +motorro_publishing = { id = "motorro.publishing" } + [libraries] -insert-koin-koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel" } -insert-koin-koin-compose = { module = "io.insert-koin:koin-compose" } -insert-koin-koin-android = { module = "io.insert-koin:koin-android" } -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" } +# Plugin source +android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref = "androidGradlePlugin" } +kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } +compose_gradle_plugin = { module = "org.jetbrains.kotlin.plugin.compose:org.jetbrains.kotlin.plugin.compose.gradle.plugin", version.ref = "kotlin" } +compose_multiplatform_gradle_plugin = { module = "org.jetbrains.compose:org.jetbrains.compose.gradle.plugin", version.ref = "composeMultiplatform" } + +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_coroutines_swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" } +kotlin_datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinDatetime" } +kotlin_annotations = { module = "org.jetbrains:annotations", version.ref = "kotlinAnnotations" } napier = { module = "io.github.aakira:napier", version.ref = "napier" } 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" } -androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } +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" } +androidx_lifecycle_runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } -compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose"} -compose-ui = { module = "androidx.compose.ui:ui" } -compose-tooling = { module = "androidx.compose.ui:ui-tooling" } -compose-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } -compose-material = { module = "androidx.compose.material3:material3" } -compose-material-icons = { module = "androidx.compose.material:material-icons-extended" } -compose-test-junit = { module = "androidx.compose.ui:ui-test-junit4" } -compose-activity = { module = "androidx.activity:activity-compose", version.ref = "composeActivity" } -compose-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "composeViewmodel" } -compose-foundation = { module = "androidx.compose.foundation:foundation" } -compose-foundation-layouts = { module = "androidx.compose.foundation:foundation-layout" } +compose_bom = { module = "androidx.compose:compose-bom", version.ref = "compose"} +compose_runtime = { module = "androidx.compose.runtime:runtime" } +compose_ui = { module = "androidx.compose.ui:ui" } +compose_tooling = { module = "androidx.compose.ui:ui-tooling" } +compose_tooling_preview = { module = "androidx.compose.ui:ui-tooling-preview" } +compose_material = { module = "androidx.compose.material3:material3" } +compose_material_icons = { module = "androidx.compose.material:material-icons-extended" } +compose_test_junit = { module = "androidx.compose.ui:ui-test-junit4" } +compose_activity = { module = "androidx.activity:activity-compose", version.ref = "composeActivity" } +compose_viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "composeViewmodel" } +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-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "composeMultiplatform" } -composeMultiplatform-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "composeMultiplatformMaterial" } -composeMultiplatform-icons = { module = "org.jetbrains.compose.material:material-icons-extended", version.ref = "composeMultiplatformMaterialIcons" } -composeMultiplatform-resources = { module = "org.jetbrains.compose.components:components-resources", 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" } -composeMultiplatform-navigationevent = { module = "org.jetbrains.androidx.navigationevent:navigationevent-compose", version.ref = "navigationMultiplatform" } -composeMultiplatform-tooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "composeMultiplatform" } -composeMultiplatform-preview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "composeMultiplatform" } +compose_multiplatform_runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "composeMultiplatform" } +compose_multiplatform_foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "composeMultiplatform" } +compose_multiplatform_ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "composeMultiplatform" } +compose_multiplatform_material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "composeMultiplatformMaterial" } +compose_multiplatform_icons = { module = "org.jetbrains.compose.material:material-icons-extended", version.ref = "composeMultiplatformMaterialIcons" } +compose_multiplatform_resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "composeMultiplatform" } +compose_multiplatform_viewmodel = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "viewmodelMultiplatform" } +compose_multiplatform_lifecycle = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycleMultiplatform" } +compose_multiplatform_navigationevent = { module = "org.jetbrains.androidx.navigationevent:navigationevent-compose", version.ref = "navigationMultiplatform" } +compose_multiplatform_tooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "composeMultiplatform" } +compose_multiplatform_preview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "composeMultiplatform" } 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" } hilt_compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "androidxHilt" } -koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin-bom" } -koin-core = { module = "io.insert-koin:koin-core" } -koin-core-viewmodel = { module = "io.insert-koin:koin-core-viewmodel" } -koin-annotations = { module = "io.insert-koin:koin-annotations" } -koin-android = { module = "io.insert-koin:koin-android" } -koin-compose = { module = "io.insert-koin:koin-compose" } -koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel" } +koin_bom = { module = "io.insert-koin:koin-bom", version.ref = "koinBom" } +koin_core = { module = "io.insert-koin:koin-core" } +koin_core_viewmodel = { module = "io.insert-koin:koin-core-viewmodel" } +koin_annotations = { module = "io.insert-koin:koin-annotations" } +koin_android = { module = "io.insert-koin:koin-android" } +koin_compose = { module = "io.insert-koin:koin-compose" } +koin_compose_viewmodel = { module = "io.insert-koin:koin-compose-viewmodel" } timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } -test-kotlin = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } -test-kotlin-common = { module = "org.jetbrains.kotlin:kotlin-test-common", version.ref = "kotlin" } -test-kotlin-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } -test-kotlin-js = { module = "org.jetbrains.kotlin:kotlin-test-js", version.ref = "kotlin" } -test-kotlin-annotations-common = { module = "org.jetbrains.kotlin:kotlin-test-annotations-common", version.ref = "kotlin" } -test-junit = { module = "junit:junit", version.ref = "testJunit" } -test-androidx-core = { module = "androidx.test:core", version.ref = "testAndroidxCore" } -test-androidx-arch = { module = "androidx.arch.core:core-testing", version.ref = "testAndroidxArch" } -test-androidx-lifecycle = { module = "androidx.lifecycle:lifecycle-runtime-testing", version.ref = "lifecycle" } -test-androidx-ext-junit = { module = "androidx.test.ext:junit", version.ref = "testAndroidxExtJunit" } -test-androidx-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "testAndroidxEspresso" } -test-compose-ui = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose" } -test-compose-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "compose" } -test-mockk = { module = "io.mockk:mockk", version.ref = "testMockk" } -test-mockk-common = { module = "io.mockk:mockk-common", version.ref = "testMockkCommon" } -test-kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } -test-robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } +test_kotlin = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +test_kotlin_common = { module = "org.jetbrains.kotlin:kotlin-test-common", version.ref = "kotlin" } +test_kotlin_junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } +test_kotlin_js = { module = "org.jetbrains.kotlin:kotlin-test-js", version.ref = "kotlin" } +test_kotlin_annotations_common = { module = "org.jetbrains.kotlin:kotlin-test-annotations-common", version.ref = "kotlin" } +test_junit = { module = "junit:junit", version.ref = "testJunit" } +test_androidx_core = { module = "androidx.test:core", version.ref = "testAndroidxCore" } +test_androidx_arch = { module = "androidx.arch.core:core-testing", version.ref = "testAndroidxArch" } +test_androidx_lifecycle = { module = "androidx.lifecycle:lifecycle-runtime-testing", version.ref = "lifecycle" } +test_androidx_ext_junit = { module = "androidx.test.ext:junit", version.ref = "testAndroidxExtJunit" } +test_androidx_espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "testAndroidxEspresso" } +test_compose_ui = { module = "androidx.compose.ui:ui-test-junit4" } +test_compose_manifest = { module = "androidx.compose.ui:ui-test-manifest" } +test_mockk = { module = "io.mockk:mockk", version.ref = "testMockk" } +test_mockk_common = { module = "io.mockk:mockk-common", version.ref = "testMockkCommon" } +test_kotlin_coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } +test_robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } [bundles] -compose-core = ["compose-ui", "compose-tooling-preview", "compose-material"] -test-core = ["test-kotlin", "test-junit", "test-mockk"] \ No newline at end of file +compose_core = ["compose_ui", "compose_tooling_preview", "compose_material"] +test_core = ["test_kotlin", "test_junit", "test_mockk"] diff --git a/gradle/maven-publish-config.gradle b/gradle/maven-publish-config.gradle deleted file mode 100644 index c81fdc5..0000000 --- a/gradle/maven-publish-config.gradle +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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. - */ - -// Based on this article: https://getstream.io/blog/publishing-libraries-to-mavencentral-2021/ - -File secretPropsFile = project.rootProject.file('local.properties') -if (secretPropsFile.exists()) { - Properties lp = new Properties() - new FileInputStream(secretPropsFile).withCloseable { is -> - lp.load(is) - } - if (lp.containsKey('SIGNING_CONFIG')) { - File signingConfigFile = file(lp.getProperty('SIGNING_CONFIG')) - if (signingConfigFile.exists()) { - Properties sc = new Properties() - new FileInputStream(signingConfigFile).withCloseable { is -> - sc.load(is) - } - sc.each { name, value -> - ext[name] = value - } - } else { - getFromEnv() - } - } else { - getFromEnv() - } -} else { - getFromEnv() -} - -def getFromEnv() { - ext["signingKey"] = System.getenv('SIGNING_KEY') - ext["signingPassword"] = System.getenv('SIGNING_PASSWORD') - ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME') - ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD') - ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID') -} diff --git a/gradle/versioning.gradle b/gradle/versioning.gradle deleted file mode 100644 index 3cba02f..0000000 --- a/gradle/versioning.gradle +++ /dev/null @@ -1,126 +0,0 @@ -/* - * 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. - */ - -ext { - platformVersionCodeAdditives = [ - "armeabi": 1, - "armeabi-v7a": 2, - "arm64-v8a": 3, - "x86": 4, - "x86_64": 5, - "mips": 6, - "mips64": 7 - ] - - /** - * Builds app version using latest repository tag - * Tag should be a semantic version of major/minor at least - * Function takes number of commits since last tag and uses it to calculate patch number - * - If patch is omitted in tag - uses number of commits - * - If patch version set in tag - pluses number of commits to patch number - * @returns Semantic version string - */ - buildVersionName = { - def versionRegexp = /^v?(\d+)\.(\d+)\.?(\d*)-?(\d*)/ - - String fromEnv = project.findProperty('versionName') ?: System.getenv("VERSION_NAME") - if (null != fromEnv && fromEnv =~ versionRegexp) { - logger.info('Version from ENV', fromEnv) - return fromEnv - } - - def descriptions = grgit.describe(match: ["v[0-9]*"]) =~ versionRegexp - def description = null - if (descriptions.getCount() < 1) { - logger.info('Git description does not have any version info. Make sure you have a semver tag (v1.2) or (v1.2.3) in a repository.') - logger.info('Using `v0.0.1` for this build') - description = ["v0.0.1", "0", "0", "1", "0"] - } else { - description = descriptions[0] - } - - int major = description[1].toInteger() - int minor = description[2].toInteger() - int patch = "" != description[3] ? description[3].toInteger() : 0 - int changes = "" != description[4] ? description[4].toInteger() : 0 - - def fromGit = "$major.$minor.${patch + changes}" - - logger.info('Version evaluated using Git', fromGit) - - fromGit - } - - /** - * Builds version code using [buildVersionName] - * @returns Integer version code - */ - buildVersionCode = { - /* - Max version code (INT32) is 214 74 836 47 - Spaces denote positions of: - - major - - minor - - build - - build variant - */ - def (major, minor, patch) = buildVersionName().tokenize('.').collect { it.toInteger() } - - if (major > 214) { - throw new GradleException('Major version exhausted: version greater than 214 not supported') - } - if (minor > 99) { - throw new GradleException('Minor version exhausted: version greater than 99 not supported') - } - if (patch > 999) { - throw new GradleException('Patch version exhausted: version greater than 999 not supported') - } - - int version = major * 10000000 + minor * 100000 + patch * 100 - - logger.info('Version code evaluated using Git', version) - - version - } - - /** - * Returns git branch name. - */ - getBranchName = { - def branch = grgit.branch.current() - if (null == branch) { - 'DETACHED' - } else { - branch.getName() - } - } - - /** - * Returns JIRA ticket if branch is in a form of PROJECT-123-some-description - * If ticket may not be parsed - returns full branch name - * Made for BITRISE CI - */ - getJiraTicketOrBranchName = { - def branch = System.getenv('BITRISE_GIT_BRANCH') - if (null == branch) { - branch = getBranchName() - } - println("--== Using branch name: ${branch} ==--") - def parsed = branch =~ /^(\w+-\d+)/ - if (parsed.getCount() < 1) { - branch - } else { - parsed[0][1] - } - } -} diff --git a/lifecycle/api/android/lifecycle.api b/lifecycle/api/android/lifecycle.api new file mode 100644 index 0000000..e51e8e8 --- /dev/null +++ b/lifecycle/api/android/lifecycle.api @@ -0,0 +1,17 @@ +public final class com/motorro/commonstatemachine/lifecycle/UiMachineLifecycle : com/motorro/commonstatemachine/lifecycle/MachineLifecycle { + public static final field Companion Lcom/motorro/commonstatemachine/lifecycle/UiMachineLifecycle$Companion; + public fun ()V + public fun addObserver (Lcom/motorro/commonstatemachine/lifecycle/MachineLifecycle$Observer;)V + public fun getState ()Lcom/motorro/commonstatemachine/lifecycle/MachineLifecycle$State; + public fun hasObservers ()Z + public fun removeObserver (Lcom/motorro/commonstatemachine/lifecycle/MachineLifecycle$Observer;)V +} + +public final class com/motorro/commonstatemachine/lifecycle/UiMachineLifecycle$Companion { + public final fun bindLifecycle (Lcom/motorro/commonstatemachine/lifecycle/UiMachineLifecycle;Landroidx/lifecycle/Lifecycle;)Lcom/motorro/commonstatemachine/lifecycle/UiMachineLifecycle$UiObserver; +} + +public abstract interface class com/motorro/commonstatemachine/lifecycle/UiMachineLifecycle$UiObserver : androidx/lifecycle/LifecycleObserver { + public abstract fun dispose ()V +} + diff --git a/lifecycle/api/jvm/lifecycle.api b/lifecycle/api/jvm/lifecycle.api index 32a6b66..e51e8e8 100644 --- a/lifecycle/api/jvm/lifecycle.api +++ b/lifecycle/api/jvm/lifecycle.api @@ -1,5 +1,4 @@ public final class com/motorro/commonstatemachine/lifecycle/UiMachineLifecycle : com/motorro/commonstatemachine/lifecycle/MachineLifecycle { - public static final field $stable I public static final field Companion Lcom/motorro/commonstatemachine/lifecycle/UiMachineLifecycle$Companion; public fun ()V public fun addObserver (Lcom/motorro/commonstatemachine/lifecycle/MachineLifecycle$Observer;)V diff --git a/lifecycle/api/lifecycle.klib.api b/lifecycle/api/lifecycle.klib.api index 91c3c06..0caaade 100644 --- a/lifecycle/api/lifecycle.klib.api +++ b/lifecycle/api/lifecycle.klib.api @@ -22,7 +22,3 @@ final class com.motorro.commonstatemachine.lifecycle/UiMachineLifecycle : com.mo final fun (com.motorro.commonstatemachine.lifecycle/UiMachineLifecycle).bindLifecycle(androidx.lifecycle/Lifecycle): com.motorro.commonstatemachine.lifecycle/UiMachineLifecycle.UiObserver // com.motorro.commonstatemachine.lifecycle/UiMachineLifecycle.Companion.bindLifecycle|bindLifecycle@com.motorro.commonstatemachine.lifecycle.UiMachineLifecycle(androidx.lifecycle.Lifecycle){}[0] } } - -final val com.motorro.commonstatemachine.lifecycle/com_motorro_commonstatemachine_lifecycle_UiMachineLifecycle$stableprop // com.motorro.commonstatemachine.lifecycle/com_motorro_commonstatemachine_lifecycle_UiMachineLifecycle$stableprop|#static{}com_motorro_commonstatemachine_lifecycle_UiMachineLifecycle$stableprop[0] - -final fun com.motorro.commonstatemachine.lifecycle/com_motorro_commonstatemachine_lifecycle_UiMachineLifecycle$stableprop_getter(): kotlin/Int // com.motorro.commonstatemachine.lifecycle/com_motorro_commonstatemachine_lifecycle_UiMachineLifecycle$stableprop_getter|com_motorro_commonstatemachine_lifecycle_UiMachineLifecycle$stableprop_getter(){}[0] diff --git a/lifecycle/build.gradle.kts b/lifecycle/build.gradle.kts index f5fe81b..333083d 100644 --- a/lifecycle/build.gradle.kts +++ b/lifecycle/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation + /* * Copyright 2026 Nikolai Kotchetkov. * Licensed under the Apache License, Version 2.0 (the "License"); @@ -11,157 +13,26 @@ * limitations under the License. */ -@file:Suppress("unused") -@file:OptIn(ExperimentalWasmDsl::class, ExperimentalAbiValidation::class) - -import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation - 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") + alias(libs.plugins.motorro.kmp) + alias(libs.plugins.motorro.publishing) } -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 { + @OptIn(ExperimentalAbiValidation::class) abiValidation { } - jvmToolchain(21) - - jvm() - android { - namespace = "com.motorro.commonstatemachine.lifecycle" - compileSdk = androidCompileSdkVersion - minSdk = androidMinSdkVersion - - withHostTest { - isIncludeAndroidResources = true - } - - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - } - - js { - binaries.library() - useCommonJs() - browser { - testTask(Action { - useMocha { - timeout = "10s" - } - }) - } - } - - wasmJs { - binaries.library() - useCommonJs() - browser { - testTask(Action { - useMocha { - timeout = "10s" - } - }) - } - } - - listOf( - iosArm64(), - iosSimulatorArm64() - ).forEach { - it.binaries.framework { - baseName = "lifecycle" - isStatic = true - } - } - sourceSets { commonMain.dependencies { api(project(":commonstatemachine")) api(libs.kotlin.coroutines.core) - api(libs.composeMultiplatform.lifecycle) + api(libs.compose.multiplatform.lifecycle) } commonTest.dependencies { - implementation(libs.test.kotlin) implementation(libs.test.kotlin.coroutines) implementation(project(":platformtest")) } } } -val javadocJar by tasks.registering(Jar::class) { - dependsOn(tasks.dokkaGenerate) - group = "documentation" - archiveClassifier.set("javadoc") - from(tasks.dokkaGenerate) -} - -val libId = "lifecycle" -val libName = "lifecycle" -val libDesc = "Lifecycle implementation for MachineLifecycle" -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/lifecycle/gradle.properties b/lifecycle/gradle.properties new file mode 100644 index 0000000..3c2fffa --- /dev/null +++ b/lifecycle/gradle.properties @@ -0,0 +1,14 @@ +# +# 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. +# +#Project +libDesc=Lifecycle implementation for MachineLifecycle diff --git a/platformtest/build.gradle.kts b/platformtest/build.gradle.kts index ecf953e..0556224 100644 --- a/platformtest/build.gradle.kts +++ b/platformtest/build.gradle.kts @@ -11,85 +11,11 @@ * 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.motorro.kmp) } -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 - kotlin { - - compilerOptions { - freeCompilerArgs.addAll(listOf( - "-Xexpect-actual-classes" - )) - } - - jvmToolchain(21) - - jvm() - android { - namespace = "com.motorro.commonstatemachine.platformtest" - compileSdk = androidCompileSdkVersion - minSdk = androidMinSdkVersion - - withHostTest { - isIncludeAndroidResources = true - } - - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - } - - js { - 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 = "platformtest" - isStatic = true - } - } - sourceSets { androidMain.dependencies { implementation(libs.test.junit) diff --git a/platformtest/gradle.properties b/platformtest/gradle.properties new file mode 100644 index 0000000..7997934 --- /dev/null +++ b/platformtest/gradle.properties @@ -0,0 +1,14 @@ +# +# 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. +# +#Project +libDesc=A helper library for multiplatform testing diff --git a/settings.gradle.kts b/settings.gradle.kts index 48d5256..460334e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -37,6 +37,8 @@ dependencyResolutionManagement { } rootProject.name = "CommonStateMachine" +includeBuild("build-logic") + // Libraries include(":tmap") include(":commonstatemachine") @@ -53,10 +55,8 @@ project(":commonflow:viewmodel").name = "commonflow-viewmodel" // Examples include( ":examples:commoncore", - ":examples:androidcore", - ":examples:welcome:welcome", + ":examples:welcome:app", ":examples:welcome:login", - ":examples:welcome:commonregister", ":examples:welcome:commonapi", ":examples:welcome:register", ":examples:lce", diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md b/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md index 9c6e754..585d39e 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md @@ -48,11 +48,11 @@ Follow the following common rules when writing code: ## Step 1. Module directory structure creation Create the following module structure if not instructed otherwise: ``` -my-project # Project folder. Common package: com.motorro.statemachine -├── app # Example: an application module, package: com.motorro.statemachine.app +my-project # Project folder. Common package: com.motorro.commonstatemachine.examples +├── app # Example: an application module, package: com.motorro.commonstatemachine.examples.app ├── auth # Required: a base directory for a new feature (authentication) -│ ├── api # Required: an API gradle module that describes the feature, package: com.motorro.statemachine.auth.api -│ └── implementation # Required: an implementation gradle module that implements the API, package: com.motorro.statemachine.auth.implementation +│ ├── api # Required: an API gradle module that describes the feature, package: com.motorro.commonstatemachine.examples.auth.api +│ └── implementation # Required: an implementation gradle module that implements the API, package: com.motorro.commonstatemachine.examples.auth.implementation └── ... ``` diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api index 00c8125..681fb02 120000 --- a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api @@ -1 +1 @@ -../../../../../../examples/skills/auth/api/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/api/ \ No newline at end of file +../../../../../../examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api/ \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app index 209d9c3..700d179 120000 --- a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app @@ -1 +1 @@ -../../../../../../examples/skills/app/src/main/kotlin/com/motorro/statemachine/skills/app \ No newline at end of file +../../../../../../examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation index 7b474d2..fe05eb8 120000 --- a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation @@ -1 +1 @@ -../../../../../../examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/statemachine/skills/auth/implementation/ \ No newline at end of file +../../../../../../examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests index 719f810..ec06e70 120000 --- a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests @@ -1 +1 @@ -../../../../../../examples/skills/auth/implementation/src/commonTest//kotlin/com/motorro/statemachine/skills/auth/implementation/ \ No newline at end of file +../../../../../../examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ \ No newline at end of file diff --git a/tmap/api/android/tmap.api b/tmap/api/android/tmap.api new file mode 100644 index 0000000..cefc248 --- /dev/null +++ b/tmap/api/android/tmap.api @@ -0,0 +1,25 @@ +public abstract interface class com/motorro/commonstatemachine/tmap/TElement { + public abstract fun getKey ()Lcom/motorro/commonstatemachine/tmap/TKey; + public abstract fun getValue ()Ljava/lang/Object; +} + +public abstract interface class com/motorro/commonstatemachine/tmap/TKey { +} + +public abstract interface class com/motorro/commonstatemachine/tmap/TMap : java/util/Collection, kotlin/jvm/internal/markers/KMappedMarker { + public static final field Companion Lcom/motorro/commonstatemachine/tmap/TMap$Companion; + public abstract fun get (Lcom/motorro/commonstatemachine/tmap/TKey;)Ljava/lang/Object; + public abstract fun minusKey (Lcom/motorro/commonstatemachine/tmap/TKey;)Lcom/motorro/commonstatemachine/tmap/TMap; + public abstract fun plus (Lcom/motorro/commonstatemachine/tmap/TElement;)Lcom/motorro/commonstatemachine/tmap/TMap; +} + +public final class com/motorro/commonstatemachine/tmap/TMap$Companion { + public final fun empty ()Lcom/motorro/commonstatemachine/tmap/TMap; + public final fun of (Ljava/lang/Iterable;)Lcom/motorro/commonstatemachine/tmap/TMap; + public final fun of ([Lcom/motorro/commonstatemachine/tmap/TElement;)Lcom/motorro/commonstatemachine/tmap/TMap; +} + +public final class com/motorro/commonstatemachine/tmap/TMapKt { + public static final fun tot (Lcom/motorro/commonstatemachine/tmap/TKey;Ljava/lang/Object;)Lcom/motorro/commonstatemachine/tmap/TElement; +} + diff --git a/tmap/api/tmap.klib.api b/tmap/api/tmap.klib.api index 8b1b6ec..b5fe72a 100644 --- a/tmap/api/tmap.klib.api +++ b/tmap/api/tmap.klib.api @@ -1,5 +1,5 @@ // Klib ABI Dump -// Targets: [iosArm64, iosSimulatorArm64, iosX64, js, wasmJs] +// Targets: [iosArm64, iosSimulatorArm64, js, wasmJs] // Rendering settings: // - Signature version: 2 // - Show manifest properties: true diff --git a/tmap/build.gradle.kts b/tmap/build.gradle.kts index bab1428..019fecf 100644 --- a/tmap/build.gradle.kts +++ b/tmap/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation + /* * Copyright 2023 Nikolai Kotchetkov. * Licensed under the Apache License, Version 2.0 (the "License"); @@ -11,152 +13,14 @@ * limitations under the License. */ -@file:Suppress("unused") -@file:OptIn(ExperimentalAbiValidation::class) - -import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation - plugins { - alias(libs.plugins.kotlin.multiplatform) - alias(libs.plugins.android.kotlin.multiplatform.library) - alias(libs.plugins.kotlin.dokka) - id("maven-publish") - id("signing") + alias(libs.plugins.motorro.kmp) + alias(libs.plugins.motorro.publishing) } -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 { + @OptIn(ExperimentalAbiValidation::class) abiValidation { } - - jvmToolchain(21) - - jvm() - - android { - namespace = "com.motorro.commonstatemachine.tmap" - compileSdk = androidCompileSdkVersion - minSdk = androidMinSdkVersion - - withHostTest { - isIncludeAndroidResources = true - } - - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } - } - - js { - binaries.library() - useCommonJs() - browser { - testTask(Action { - useMocha { - timeout = "10s" - } - }) - } - } - - @OptIn(ExperimentalWasmDsl::class) - wasmJs { - binaries.library() - useCommonJs() - browser { - testTask(Action { - useMocha { - timeout = "10s" - } - }) - } - } - - listOf( - iosX64(), - iosArm64(), - iosSimulatorArm64() - ).forEach { - it.binaries.framework { - baseName = "tmap" - isStatic = true - } - } - - sourceSets { - commonTest.dependencies { - implementation(libs.test.kotlin) - } - } -} - -val javadocJar by tasks.registering(Jar::class) { - dependsOn(tasks.dokkaGenerate) - group = "documentation" - archiveClassifier.set("javadoc") - from(tasks.dokkaGenerate) } - -val libId = "tmap" -val libName = "tmap" -val libDesc = "A map-like storage which bounds key with value type" -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/tmap/gradle.properties b/tmap/gradle.properties new file mode 100644 index 0000000..3d45276 --- /dev/null +++ b/tmap/gradle.properties @@ -0,0 +1,14 @@ +# +# 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. +# +#Project +libDesc=A map-like storage which bounds key with value type From 602a32fc77c9065aad1e85e4893aa6d18c6b54fc Mon Sep 17 00:00:00 2001 From: motorro Date: Mon, 28 Sep 2026 13:50:34 +0200 Subject: [PATCH 19/32] Convention publishing Refs: #15 --- .../src/main/kotlin/PublishingPlugin.kt | 23 ++++++++++++++ .../src/main/kotlin/publishingProperties.kt | 30 ------------------- build.gradle.kts | 12 -------- commonflow/compose/consumer-rules.pro | 0 commonflow/compose/gradle.properties | 2 +- commonflow/data/consumer-rules.pro | 0 commonflow/data/gradle.properties | 2 +- commonflow/viewmodel/consumer-rules.pro | 0 commonstatemachine/consumer-rules.pro | 0 coroutines/consumer-rules.pro | 0 examples/commoncore/consumer-rules.pro | 0 examples/skills/appcore/consumer-rules.pro | 0 examples/skills/auth/api/consumer-rules.pro | 0 .../auth/implementation/consumer-rules.pro | 0 examples/skills/domain/consumer-rules.pro | 0 examples/skills/usecase/consumer-rules.pro | 0 examples/timer/consumer-rules.pro | 0 examples/welcome/commonapi/consumer-rules.pro | 0 lifecycle/consumer-rules.pro | 0 platformtest/consumer-rules.pro | 0 tmap/consumer-rules.pro | 0 21 files changed, 25 insertions(+), 44 deletions(-) delete mode 100644 build-logic/convention/src/main/kotlin/publishingProperties.kt create mode 100644 commonflow/compose/consumer-rules.pro create mode 100644 commonflow/data/consumer-rules.pro create mode 100644 commonflow/viewmodel/consumer-rules.pro create mode 100644 commonstatemachine/consumer-rules.pro create mode 100644 coroutines/consumer-rules.pro create mode 100644 examples/commoncore/consumer-rules.pro create mode 100644 examples/skills/appcore/consumer-rules.pro create mode 100644 examples/skills/auth/api/consumer-rules.pro create mode 100644 examples/skills/auth/implementation/consumer-rules.pro create mode 100644 examples/skills/domain/consumer-rules.pro create mode 100644 examples/skills/usecase/consumer-rules.pro create mode 100644 examples/timer/consumer-rules.pro create mode 100644 examples/welcome/commonapi/consumer-rules.pro create mode 100644 lifecycle/consumer-rules.pro create mode 100644 platformtest/consumer-rules.pro create mode 100644 tmap/consumer-rules.pro diff --git a/build-logic/convention/src/main/kotlin/PublishingPlugin.kt b/build-logic/convention/src/main/kotlin/PublishingPlugin.kt index be7a83c..9c244b9 100644 --- a/build-logic/convention/src/main/kotlin/PublishingPlugin.kt +++ b/build-logic/convention/src/main/kotlin/PublishingPlugin.kt @@ -1,3 +1,4 @@ +import com.motorro.gradle.convention.getProjectProperty import com.motorro.gradle.convention.libDesc import com.motorro.gradle.convention.libId import com.motorro.gradle.convention.libName @@ -9,12 +10,34 @@ import org.gradle.api.publish.maven.MavenPublication import org.gradle.api.publish.maven.tasks.AbstractPublishToMaven import org.gradle.api.tasks.bundling.Jar import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.extra import org.gradle.kotlin.dsl.getByType import org.gradle.kotlin.dsl.register import org.gradle.kotlin.dsl.withType import org.gradle.plugins.signing.Sign import org.gradle.plugins.signing.SigningExtension +val Project.developerId: String? + get() = (findProperty("developerId") as? String) ?: if (rootProject.extra.has("developerId")) rootProject.extra.get("developerId") as String else null + +val Project.developerName: String? + get() = (findProperty("developerName") as? String) ?: if (rootProject.extra.has("developerName")) rootProject.extra.get("developerName") as String else null + +val Project.developerEmail: String? + get() = (findProperty("developerEmail") as? String) ?: if (rootProject.extra.has("developerEmail")) rootProject.extra.get("developerEmail") as String else null + +val Project.projectScm: String? + get() = (findProperty("projectScm") as? String) ?: if (rootProject.extra.has("projectScm")) rootProject.extra.get("projectScm") as String else null + +val Project.projectUrl: String? + get() = (findProperty("projectUrl") as? String) ?: if (rootProject.extra.has("projectUrl")) rootProject.extra.get("projectUrl") as String else null + +val Project.signingKey: String? + get() = getProjectProperty("signingKey", "SIGNING_KEY") + +val Project.signingPassword: String? + get() = getProjectProperty("signingPassword", "SIGNING_PASSWORD") + class PublishingPlugin : Plugin { override fun apply(targetProject: Project) = with(targetProject) { // Apply plugins diff --git a/build-logic/convention/src/main/kotlin/publishingProperties.kt b/build-logic/convention/src/main/kotlin/publishingProperties.kt deleted file mode 100644 index d1188ca..0000000 --- a/build-logic/convention/src/main/kotlin/publishingProperties.kt +++ /dev/null @@ -1,30 +0,0 @@ -import com.motorro.gradle.convention.getProjectProperty -import org.gradle.api.Project -import org.gradle.kotlin.dsl.extra - -val Project.developerId: String? - get() = (findProperty("developerId") as? String) ?: if (rootProject.extra.has("developerId")) rootProject.extra.get("developerId") as String else null - -val Project.developerName: String? - get() = (findProperty("developerName") as? String) ?: if (rootProject.extra.has("developerName")) rootProject.extra.get("developerName") as String else null - -val Project.developerEmail: String? - get() = (findProperty("developerEmail") as? String) ?: if (rootProject.extra.has("developerEmail")) rootProject.extra.get("developerEmail") as String else null - -val Project.projectScm: String? - get() = (findProperty("projectScm") as? String) ?: if (rootProject.extra.has("projectScm")) rootProject.extra.get("projectScm") as String else null - -val Project.projectUrl: String? - get() = (findProperty("projectUrl") as? String) ?: if (rootProject.extra.has("projectUrl")) rootProject.extra.get("projectUrl") as String else null - -val Project.signingKey: String? - get() = getProjectProperty("signingKey", "SIGNING_KEY") - -val Project.signingPassword: String? - get() = getProjectProperty("signingPassword", "SIGNING_PASSWORD") - -val Project.ossrhUsername: String? - get() = getProjectProperty("ossrhUsername", "OSSRH_USERNAME") - -val Project.ossrhPassword: String? - get() = getProjectProperty("ossrhPassword", "OSSRH_PASSWORD") diff --git a/build.gradle.kts b/build.gradle.kts index 865fab4..aa80982 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -43,18 +43,6 @@ allprojects { val versionName by extra(buildVersionName()) val versionCode by extra(buildVersionCode()) - val androidBuildToolsVersion by extra("35.0.0") - val androidMinSdkVersion by extra(24) - val androidTargetSdkVersion by extra(37) - val androidCompileSdkVersion by extra(37) - - val developerId by extra("motorro") - val developerName by extra("Nikolai Kotchetkov") - val developerEmail by extra("motorro@gmail.com") - - val projectScm by extra("https://github.com/motorro/CommonStateMachine.git") - val projectUrl by extra("https://github.com/motorro/CommonStateMachine") - tasks.withType().configureEach { forkEvery = 100 testLogging { diff --git a/commonflow/compose/consumer-rules.pro b/commonflow/compose/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/commonflow/compose/gradle.properties b/commonflow/compose/gradle.properties index bf00b31..f3d63af 100644 --- a/commonflow/compose/gradle.properties +++ b/commonflow/compose/gradle.properties @@ -13,4 +13,4 @@ #Project libId="commonflow-compose" libName="commonflow-compose" -libDesc="Common compose flow for modularized state machines" +libDesc="Common compose flow for modularised state machines" diff --git a/commonflow/data/consumer-rules.pro b/commonflow/data/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/commonflow/data/gradle.properties b/commonflow/data/gradle.properties index 0255c4f..a0125c2 100644 --- a/commonflow/data/gradle.properties +++ b/commonflow/data/gradle.properties @@ -13,4 +13,4 @@ #Project libId="commonflow-data" libName="commonflow-data" -libDesc="Common data flow for modularized state machines" +libDesc="Common data flow for modularised state machines" diff --git a/commonflow/viewmodel/consumer-rules.pro b/commonflow/viewmodel/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/commonstatemachine/consumer-rules.pro b/commonstatemachine/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/coroutines/consumer-rules.pro b/coroutines/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/examples/commoncore/consumer-rules.pro b/examples/commoncore/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/examples/skills/appcore/consumer-rules.pro b/examples/skills/appcore/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/examples/skills/auth/api/consumer-rules.pro b/examples/skills/auth/api/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/examples/skills/auth/implementation/consumer-rules.pro b/examples/skills/auth/implementation/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/examples/skills/domain/consumer-rules.pro b/examples/skills/domain/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/examples/skills/usecase/consumer-rules.pro b/examples/skills/usecase/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/examples/timer/consumer-rules.pro b/examples/timer/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/examples/welcome/commonapi/consumer-rules.pro b/examples/welcome/commonapi/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/lifecycle/consumer-rules.pro b/lifecycle/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/platformtest/consumer-rules.pro b/platformtest/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/tmap/consumer-rules.pro b/tmap/consumer-rules.pro new file mode 100644 index 0000000..e69de29 From bdec092ed42e394735c682e8e78abe47a54d5b7c Mon Sep 17 00:00:00 2001 From: motorro Date: Wed, 30 Sep 2026 19:53:20 +0200 Subject: [PATCH 20/32] Skills repackaged to use with npx Refs: #14 --- .githooks/pre-commit | 18 ++ .github/workflows/skills.yaml | 26 --- Claude outputs/skills.yaml | 41 ++++ README.md | 17 ++ skills-src/README.md | 26 +++ skills-src/build_skills.sh | 27 +++ .../.shared/ARCHITECTURE.md | 0 .../.shared/architecture.puml | 0 .../.shared/COMMON-CHILD-FLOW.md | 0 .../.shared/common-child-flow.puml | 0 .../common-child-flow/boilerplate/SKILL.md | 132 ++++++++++++ .../boilerplate/assets/example/api | 0 .../boilerplate/assets/example/app | 0 .../boilerplate/assets/example/implementation | 0 .../assets/example/implementation-tests | 0 .../boilerplate/references/ARCHITECTURE.md | 1 + .../references/COMMON-CHILD-FLOW.md | 1 + .../boilerplate/references/architecture.puml | 1 + .../references/base-state-implementation.md | 39 ++++ .../base-test-class-implementation.md | 44 ++++ .../references/common-child-flow.puml | 1 + .../boilerplate/references/data-and-ui-api.md | 28 +++ .../references/data-and-ui-implementation.md | 76 +++++++ .../references/di-implementation.md | 44 ++++ .../references/gesture-and-ui-state-api.md | 23 +++ .../references/input-and-result-api.md | 22 ++ .../references/proxy-implementation.md | 44 ++++ ...actory-context-interface-implementation.md | 85 ++++++++ .../references/state-implementation.md | 102 ++++++++++ .../references/state-test-implementation.md | 72 +++++++ .../test-fixtures-implementation.md | 15 ++ .../references/ui-renderer-implementation.md | 22 ++ .../references/ui-view-implementation.md | 42 ++++ skills/build_skills.sh | 43 ---- .../assets/example/api/AuthDataApi.kt | 8 + .../assets/example/api/AuthGesture.kt | 7 + .../assets/example/api/AuthInput.kt | 7 + .../assets/example/api/AuthResult.kt | 7 + .../assets/example/api/AuthUiApi.kt | 8 + .../assets/example/api/AuthUiState.kt | 7 + .../boilerplate/assets/example/app/App.kt | 30 +++ .../assets/example/app/MainActivity.kt | 23 +++ .../assets/example/app/MainModule.kt | 8 + .../assets/example/app/MainViewModel.kt | 32 +++ .../assets/example/app/data/MainGesture.kt | 26 +++ .../assets/example/app/data/MainUiState.kt | 34 ++++ .../example/app/state/AuthProxyState.kt | 61 ++++++ .../app/state/CheckingAuthentication.kt | 52 +++++ .../assets/example/app/state/ContentState.kt | 44 ++++ .../assets/example/app/state/MainContext.kt | 37 ++++ .../assets/example/app/state/MainState.kt | 28 +++ .../example/app/state/MainStateFactory.kt | 57 ++++++ .../assets/example/app/ui/MainScreen.kt | 130 ++++++++++++ .../assets/example/app/ui/MainUiRenderer.kt | 23 +++ .../data/AuthStateDataTest.kt | 51 +++++ .../data/authGesturesFixtures.kt | 11 + .../data/dataStatesFixtures.kt | 45 +++++ .../data/uiStatesFixtures.kt | 6 + .../state/AuthenticatingStateTest.kt | 82 ++++++++ .../state/AuthenticationErrorStateTest.kt | 87 ++++++++ .../state/BaseStateTest.kt | 63 ++++++ .../state/FormStateTest.kt | 125 ++++++++++++ .../state/PreloadingErrorStateTest.kt | 91 +++++++++ .../state/PreloadingStateTest.kt | 82 ++++++++ .../ui/AuthUiRendererImplTest.kt | 63 ++++++ .../example/implementation/AuthDataApiImpl.kt | 35 ++++ .../example/implementation/AuthModule.kt | 8 + .../example/implementation/AuthUiApiImpl.kt | 27 +++ .../implementation/data/AuthGestureImpl.kt | 38 ++++ .../implementation/data/AuthStateData.kt | 23 +++ .../implementation/data/AuthUiStateImpl.kt | 36 ++++ .../implementation/state/AuthContext.kt | 25 +++ .../example/implementation/state/AuthState.kt | 10 + .../implementation/state/AuthStateFactory.kt | 54 +++++ .../state/AuthStateFactoryImpl.kt | 84 ++++++++ .../state/AuthenticatingState.kt | 81 ++++++++ .../state/AuthenticationErrorState.kt | 53 +++++ .../implementation/state/BaseAuthState.kt | 34 ++++ .../example/implementation/state/FormState.kt | 75 +++++++ .../state/PreloadingErrorState.kt | 52 +++++ .../implementation/state/PreloadingState.kt | 78 ++++++++ .../implementation/ui/AuthErrorScreen.kt | 79 ++++++++ .../implementation/ui/AuthFormScreen.kt | 122 ++++++++++++ .../implementation/ui/AuthLoadingScreen.kt | 21 ++ .../example/implementation/ui/AuthScreen.kt | 68 +++++++ .../implementation/ui/AuthUiRenderer.kt | 33 +++ .../implementation/ui/AuthUiRendererImpl.kt | 28 +++ .../boilerplate/references/ARCHITECTURE.md | 33 ++- .../references/COMMON-CHILD-FLOW.md | 40 +++- .../boilerplate/references/architecture.puml | 52 ++++- .../references/common-child-flow.puml | 188 +++++++++++++++++- 91 files changed, 3631 insertions(+), 73 deletions(-) create mode 100644 .githooks/pre-commit delete mode 100644 .github/workflows/skills.yaml create mode 100644 Claude outputs/skills.yaml create mode 100644 skills-src/README.md create mode 100644 skills-src/build_skills.sh rename {skills => skills-src}/commonstatemachine/.shared/ARCHITECTURE.md (100%) rename {skills => skills-src}/commonstatemachine/.shared/architecture.puml (100%) rename {skills => skills-src}/commonstatemachine/common-child-flow/.shared/COMMON-CHILD-FLOW.md (100%) rename {skills => skills-src}/commonstatemachine/common-child-flow/.shared/common-child-flow.puml (100%) create mode 100644 skills-src/commonstatemachine/common-child-flow/boilerplate/SKILL.md rename {skills => skills-src}/commonstatemachine/common-child-flow/boilerplate/assets/example/api (100%) rename {skills => skills-src}/commonstatemachine/common-child-flow/boilerplate/assets/example/app (100%) rename {skills => skills-src}/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation (100%) rename {skills => skills-src}/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests (100%) create mode 120000 skills-src/commonstatemachine/common-child-flow/boilerplate/references/ARCHITECTURE.md create mode 120000 skills-src/commonstatemachine/common-child-flow/boilerplate/references/COMMON-CHILD-FLOW.md create mode 120000 skills-src/commonstatemachine/common-child-flow/boilerplate/references/architecture.puml create mode 100644 skills-src/commonstatemachine/common-child-flow/boilerplate/references/base-state-implementation.md create mode 100644 skills-src/commonstatemachine/common-child-flow/boilerplate/references/base-test-class-implementation.md create mode 120000 skills-src/commonstatemachine/common-child-flow/boilerplate/references/common-child-flow.puml create mode 100644 skills-src/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-api.md create mode 100644 skills-src/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-implementation.md create mode 100644 skills-src/commonstatemachine/common-child-flow/boilerplate/references/di-implementation.md create mode 100644 skills-src/commonstatemachine/common-child-flow/boilerplate/references/gesture-and-ui-state-api.md create mode 100644 skills-src/commonstatemachine/common-child-flow/boilerplate/references/input-and-result-api.md create mode 100644 skills-src/commonstatemachine/common-child-flow/boilerplate/references/proxy-implementation.md create mode 100644 skills-src/commonstatemachine/common-child-flow/boilerplate/references/renderer-factory-context-interface-implementation.md create mode 100644 skills-src/commonstatemachine/common-child-flow/boilerplate/references/state-implementation.md create mode 100644 skills-src/commonstatemachine/common-child-flow/boilerplate/references/state-test-implementation.md create mode 100644 skills-src/commonstatemachine/common-child-flow/boilerplate/references/test-fixtures-implementation.md create mode 100644 skills-src/commonstatemachine/common-child-flow/boilerplate/references/ui-renderer-implementation.md create mode 100644 skills-src/commonstatemachine/common-child-flow/boilerplate/references/ui-view-implementation.md delete mode 100644 skills/build_skills.sh create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthDataApi.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthGesture.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthInput.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthResult.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthUiApi.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthUiState.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/App.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/MainActivity.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/MainModule.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/MainViewModel.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/data/MainGesture.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/data/MainUiState.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/state/AuthProxyState.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/state/CheckingAuthentication.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/state/ContentState.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/state/MainContext.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/state/MainState.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/state/MainStateFactory.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/ui/MainScreen.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/ui/MainUiRenderer.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/data/AuthStateDataTest.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/data/authGesturesFixtures.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/data/dataStatesFixtures.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/data/uiStatesFixtures.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/state/AuthenticatingStateTest.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/state/AuthenticationErrorStateTest.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/state/BaseStateTest.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/state/FormStateTest.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/state/PreloadingErrorStateTest.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/state/PreloadingStateTest.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/ui/AuthUiRendererImplTest.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/AuthDataApiImpl.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/AuthModule.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/AuthUiApiImpl.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/data/AuthGestureImpl.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/data/AuthStateData.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/data/AuthUiStateImpl.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/AuthContext.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/AuthState.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/AuthStateFactory.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/AuthStateFactoryImpl.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/AuthenticatingState.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/AuthenticationErrorState.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/BaseAuthState.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/FormState.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/PreloadingErrorState.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/PreloadingState.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/ui/AuthErrorScreen.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/ui/AuthFormScreen.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/ui/AuthLoadingScreen.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/ui/AuthScreen.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/ui/AuthUiRenderer.kt create mode 100644 skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/ui/AuthUiRendererImpl.kt mode change 120000 => 100644 skills/commonstatemachine/common-child-flow/boilerplate/references/ARCHITECTURE.md mode change 120000 => 100644 skills/commonstatemachine/common-child-flow/boilerplate/references/COMMON-CHILD-FLOW.md mode change 120000 => 100644 skills/commonstatemachine/common-child-flow/boilerplate/references/architecture.puml mode change 120000 => 100644 skills/commonstatemachine/common-child-flow/boilerplate/references/common-child-flow.puml diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100644 index 0000000..8f68637 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,18 @@ +#!/bin/bash +# Rebuilds the packaged skills (skills/) from their sources (skills-src/) and stages +# the result, so any changes are included in the commit being made. +set -e + +repo_root="$(git rev-parse --show-toplevel)" +cd "$repo_root" + +if [ ! -f "skills-src/build_skills.sh" ]; then + exit 0 +fi + +echo "Rebuilding skills/ from skills-src/..." +bash skills-src/build_skills.sh + +git add skills + +exit 0 diff --git a/.github/workflows/skills.yaml b/.github/workflows/skills.yaml deleted file mode 100644 index af85fda..0000000 --- a/.github/workflows/skills.yaml +++ /dev/null @@ -1,26 +0,0 @@ -name: Skills - -on: - release: - types: [published] - -jobs: - publish: - name: Build and upload skills - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: refs/remotes/origin/master - - name: Build skills - run: ./skills/build_skills.sh artifacts - - name: Upload skills to release - uses: softprops/action-gh-release@v2 - with: - files: artifacts/*.zip - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Claude outputs/skills.yaml b/Claude outputs/skills.yaml new file mode 100644 index 0000000..a4a4fcd --- /dev/null +++ b/Claude outputs/skills.yaml @@ -0,0 +1,41 @@ +name: Skills + +on: + release: + types: [published] + +jobs: + publish: + name: Build and upload skills + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: refs/remotes/origin/master + - name: Build skills + run: ./skills-src/build_skills.sh + - name: Archive skills + run: | + mkdir -p artifacts + cd skills + find . -path '*/.*' -prune -o -name "SKILL.md" -print | while read -r skill_file; do + skill_dir=$(dirname "$skill_file") + rel_dir="${skill_dir#./}" + dir_name=$(basename "$skill_dir") + parent_dir=$(basename "$(dirname "$skill_dir")") + skill_name="skill-${parent_dir}-${dir_name}" + + zip_name="${skill_name}.zip" + echo "Zipping $rel_dir to $zip_name" + zip -q -r "../artifacts/$zip_name" "$rel_dir" -x "*.DS_Store" + done + - name: Upload skills to release + uses: softprops/action-gh-release@v2 + with: + files: artifacts/*.zip + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 1812d07..4c0a1b7 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,23 @@ Please check out the Medium article on pattern/library usage. - [Part II - tools](https://proandroiddev.com/mvi-architecture-with-a-state-machine-tools-721c5ebed893-47f46413415d) - [Part III - multi-module and multi-platform](https://proandroiddev.com/mvi-architecture-with-a-state-machine-modules-3e242666c7c) +## AI Skills + +The project ships a set of AI skills that teach coding agents (such as Claude) how to work with +`CommonStateMachine` - for example, scaffolding a new feature-flow module that follows the +[Common Child Flow](#common-child-flow-api) architecture described below. See +[skills/README.md](skills/README.md) for the full list of available skills. + +Skills can be installed straight from this repository with the [`skills` CLI](https://github.com/vercel-labs/skills): + +```bash +# List the skills available in this repository +npx skills add motorro/commonstatemachine --list + +# Install specific skills +npx skills add motorro/commonstatemachine --skill commonstatemachine-commonchildflow-boilerplate +``` + ## Contents diff --git a/skills-src/README.md b/skills-src/README.md new file mode 100644 index 0000000..6f238d6 --- /dev/null +++ b/skills-src/README.md @@ -0,0 +1,26 @@ +# AI skills +Some AI skills for your convenience: + +## Create a Common Child Flow module boilerplate +[Module boilerplate skill](commonstatemachine/common-child-flow/boilerplate/SKILL.md): creates a set of API/implementation +feature module boilerplate following the [Common Child Flow](../README.md#common-child-flow-api). + - Defines gesture/ui-states sealed structures + - Binds data and UI APIs + - Builds a basic tool set for the module implementation + - Creates a test structure and mocks for the module implementation + - Builds a DI container for the module implementation + +For example, to try the skill with the [example project](../examples/skills), try creating a module to display a list +of friends as soon as the user is authenticated: + +> Create a `friendlist` feature module that loads and displays a list of friends. +> - Use `com.motorro.commonstatemachine.skills.domain.friends.GetFriendList` as the data source, staying subscribed +> to updates after the first load. +> - Show a loading state until the first emission, then the friend list. +> - On failure, check the error: offer a retry for an I/O error, otherwise terminate the flow. +> - Map errors to the domain at the usecase boundary with `toAppException` from +> `com.motorro.commonstatemachine.skills.domain.exception`. +> - Logical states: `list` (with the subscription) and `error`. +> - Views: list, loading, error — built with the shared theme, dimensions and composables from the `appcore` module. +> - Wire up DI modules for the data and UI APIs, add state tests, and create proxies (gesture, UI-state, module flow) +> to embed the flow in the main application module. \ No newline at end of file diff --git a/skills-src/build_skills.sh b/skills-src/build_skills.sh new file mode 100644 index 0000000..32240a9 --- /dev/null +++ b/skills-src/build_skills.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SRC_DIR="$SCRIPT_DIR" + +# Directory to copy the self-contained skills to, defaults to "../skills" (repo root) if not provided +DEST_DIR="${1:-$SCRIPT_DIR/../skills}" + +# Ensure DEST_DIR is an absolute path +if [[ ! "$DEST_DIR" = /* ]]; then + DEST_DIR="$(pwd)/$DEST_DIR" +fi + +echo "Copying skills from $SRC_DIR to $DEST_DIR, resolving symlinks..." +rm -rf "$DEST_DIR" +mkdir -p "$DEST_DIR" + +if command -v rsync >/dev/null 2>&1; then + rsync -aL --exclude=".*" --exclude="build_skills.sh" "$SRC_DIR/" "$DEST_DIR/" +else + cp -RL "$SRC_DIR/." "$DEST_DIR/" + find "$DEST_DIR" -mindepth 1 -name ".*" -exec rm -rf {} + + rm -f "$DEST_DIR/build_skills.sh" +fi + +echo "Skills copied to $DEST_DIR." diff --git a/skills/commonstatemachine/.shared/ARCHITECTURE.md b/skills-src/commonstatemachine/.shared/ARCHITECTURE.md similarity index 100% rename from skills/commonstatemachine/.shared/ARCHITECTURE.md rename to skills-src/commonstatemachine/.shared/ARCHITECTURE.md diff --git a/skills/commonstatemachine/.shared/architecture.puml b/skills-src/commonstatemachine/.shared/architecture.puml similarity index 100% rename from skills/commonstatemachine/.shared/architecture.puml rename to skills-src/commonstatemachine/.shared/architecture.puml diff --git a/skills/commonstatemachine/common-child-flow/.shared/COMMON-CHILD-FLOW.md b/skills-src/commonstatemachine/common-child-flow/.shared/COMMON-CHILD-FLOW.md similarity index 100% rename from skills/commonstatemachine/common-child-flow/.shared/COMMON-CHILD-FLOW.md rename to skills-src/commonstatemachine/common-child-flow/.shared/COMMON-CHILD-FLOW.md diff --git a/skills/commonstatemachine/common-child-flow/.shared/common-child-flow.puml b/skills-src/commonstatemachine/common-child-flow/.shared/common-child-flow.puml similarity index 100% rename from skills/commonstatemachine/common-child-flow/.shared/common-child-flow.puml rename to skills-src/commonstatemachine/common-child-flow/.shared/common-child-flow.puml diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/SKILL.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/SKILL.md new file mode 100644 index 0000000..585d39e --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/SKILL.md @@ -0,0 +1,132 @@ +--- +name: commonstatemachine-commonchildflow-boilerplate +description: Use this skill to make a boilerplate for the new feature-flow using + the [CommonStateMachine](https://github.com/motorro/CommonStateMachine) architecture. + If the user asks you something like "Create a new authentication flow feature" - use this skill. +metadata: + author: Motorro + last-updated: '2026-08-12' + keywords: + - android + - architecture + - cross-platform + - kotlin-multiplatform + - mvi + - state-machine + - feature-flow +--- + +## Description +Use this skill to add a new feature boilerplate to the app based on the `CommonStateMachine` architecture. +following the `Common Child Flow` design. This design sets a common approach to use proxy state machines for a feature-flow. + +For a deeper dive into the state machine mechanics and common components, refer to the [Core Architecture](references/ARCHITECTURE.md). +For a deeper dive into the common child flow philosophy and design, refer to the [Common Child Flow Architecture](references/COMMON-CHILD-FLOW.md). + +## Example +This instruction uses an [example authentication module](assets/example). All examples, schemes and code samples refer +to such an example. Adjust the names and packages to your actual need. + +## Strategy +You will create a set of Gradle modules that together form a feature: + +- The API module: contains the definition of input and output data types for the feature. + Example: authentication API that provides the basic interface to the authentication feature. +- One or more implementation modules: contains the implementation of the API. + Example: login implementation module - implements API with login/password functionality. + +Take a look at the [class diagram](references/common-child-flow.puml). It contains an overview +between all the interfaces, classes and objects in a setup using the Authentication module example. + +Follow the following common rules when writing code: + +- Follow the project coding conventions or use Kotlin default +- Follow the project naming conventions or use Kotlin default +- If the project uses KDoc as a coding convention - add it to the created types, methods, properties +- Add TODO comments in dummy classes and functions to point the user to provide the actual implementation + +## Step 1. Module directory structure creation +Create the following module structure if not instructed otherwise: +``` +my-project # Project folder. Common package: com.motorro.commonstatemachine.examples +├── app # Example: an application module, package: com.motorro.commonstatemachine.examples.app +├── auth # Required: a base directory for a new feature (authentication) +│ ├── api # Required: an API gradle module that describes the feature, package: com.motorro.commonstatemachine.examples.auth.api +│ └── implementation # Required: an implementation gradle module that implements the API, package: com.motorro.commonstatemachine.examples.auth.implementation +└── ... +``` + +- Use the Gradle setup common for the other modules in a project +- Include the following dependencies if not provided already: + - Base state-machine components: "com.motorro.commonstatemachine:commonstatemachine:x.x.x" + - Coroutine extensions (if using coroutines): "com.motorro.commonstatemachine:coroutines:x.x.x" + - Common data API (state machine child flow): "com.motorro.commonstatemachine:commonflow-data:x.x.x" + - Common UI API (Compose API if used): "com.motorro.commonstatemachine:commonflow-compose:x.x.x" +- Include test dependencies that are used across the project and coroutines test dependencies if used. +- Include DI dependencies that are used across the project. +- Include Logging dependencies that are used across the project. +- Include Compose native or multiplatform (depending on the project setup or the user choice) dependencies to the implementation modules. +- If the user asks you to create some specific implementation or several implementations of the same API, infer the names + and create several modules. For example, the user may ask you to create two authentication implementations of the same Authentication API: + + - `password` - an implementation for the password authentication + - `social` - and implementation for the social network authentication + + If so - create several implementation modules, use the same API module as a base for them. +- Include `api` as a dependency in the implementation modules. + +## Step 2. Create an API module code +Create a package for the feature API. By default, use the common application package adding the feature name and `api`. +See the module structure above for an example. +Then follow these guides for the API module implementation: + +1. [UI gesture definitions and UI state definitions API](references/gesture-and-ui-state-api.md) +2. [Feature-flow input and result API](references/input-and-result-api.md) +3. [Feature-flow data and UI API](references/data-and-ui-api.md) + +## Step 3. Create implementation module +Here you will create an API implementation. +If several implementations are requested by user (step 1) - repeat for each implementation. +Create a package for the feature implementation. By default, use the common application package adding the feature name and `implementation`. +If several implementations are requested by user (step 1) - name according to the implementation name. +Then follow these guides for the module implementation: + +1. [Create gestures, ui-states, data-state](references/data-and-ui-implementation.md). +2. [Create common-state binding, ui-renderer, state factory and context interfaces](references/renderer-factory-context-interface-implementation.md). +3. [Create basic test fixtures for the data-classes created in the previous state](references/test-fixtures-implementation.md). +4. [Create an abstract base state](references/base-state-implementation.md). +5. [Create a base abstract unit-test class](references/base-test-class-implementation.md). + +## Step 4. Create the machine states +For the user to kick-off faster, we will create some machine-states: + +- If the user gave you the instructions about what would be the state-flow, or what states they need - create the states for them. + Implement the required functionality or place TODOs in place of the code if it is not clear what to do exactly. + Don't hesitate to ask the user to clarify what they need. +- If the user didn't give you any specifics, create a classic LCE example: + - create a dummy preloading state + - create a dummy content state + - create a dummy error state + +Refer to [this manual](references/state-implementation.md) to get the idea of the state mechanics. +Repeat this step for every state you need to create. +Create a unit-test for every created machine-state according to [this manual](references/state-test-implementation.md) + +## Step 5. Create basic Compose views +To make the module complete, create some views for the created UI-states if user didn't tell you +otherwise or has given you some special instructions, screenshots, etc. +Follow these steps to implement the views: + +1. [Implement UI renderer if used](references/ui-renderer-implementation.md) +2. [Implement sample composables](references/ui-view-implementation.md) + +## Step 6. Create API implementation and DI modules +To be able to use the created implementation, follow [this guide](references/di-implementation.md) to implement: + +- Data and UI API implementations +- DI Modules (optional) + +## Step 7. Proxy creation (optional) +If the user wants to integrate the child flow to one or several parent flows, +implement the proxy machine states for the hosting flows. +Read [this guide](references/proxy-implementation.md) for more information. diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api b/skills-src/commonstatemachine/common-child-flow/boilerplate/assets/example/api similarity index 100% rename from skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api rename to skills-src/commonstatemachine/common-child-flow/boilerplate/assets/example/api diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app b/skills-src/commonstatemachine/common-child-flow/boilerplate/assets/example/app similarity index 100% rename from skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app rename to skills-src/commonstatemachine/common-child-flow/boilerplate/assets/example/app diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation b/skills-src/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation similarity index 100% rename from skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation rename to skills-src/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests b/skills-src/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests similarity index 100% rename from skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests rename to skills-src/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/ARCHITECTURE.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/ARCHITECTURE.md new file mode 120000 index 0000000..f1d6e62 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/ARCHITECTURE.md @@ -0,0 +1 @@ +../../../.shared/ARCHITECTURE.md \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/COMMON-CHILD-FLOW.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/COMMON-CHILD-FLOW.md new file mode 120000 index 0000000..c83c444 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/COMMON-CHILD-FLOW.md @@ -0,0 +1 @@ +../../.shared/COMMON-CHILD-FLOW.md \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/architecture.puml b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/architecture.puml new file mode 120000 index 0000000..df09e56 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/architecture.puml @@ -0,0 +1 @@ +../../../.shared/ARCHITECTURE.puml \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/base-state-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/base-state-implementation.md new file mode 100644 index 0000000..a78ef49 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/base-state-implementation.md @@ -0,0 +1,39 @@ +# Module implementation: base state +Use this reference to implement a common base state for the flow. +Take a look at the [class diagram](common-child-flow.puml) to get the idea about components purpose. +Take a look at the [example](../assets/example/implementation/state/BaseAuthState.kt). + +## Step 1. Create a base class +Create a base abstract class that would be used as a common ancestor for the most of the logical states. +Follow the following common rules: + +- Make the class internal to the implementation module. +- The name should start with `Base` prefix. +- The class should be abstract +- The class should extend the `com.motorro.commonstatemachine.coroutines.CoroutineState` +- The class should bind the generic parameters for the [gesture](data-and-ui-implementation.md#step-1-ui-gestures) and [ui-state](data-and-ui-implementation.md#step-2-ui-states) types. +- The class should take the [context](renderer-factory-context-interface-implementation.md#step-4-state-context) parameter and implement the context by delegate. + +## Step 2. Add convenience methods to change the machine and UI states +The `com.motorro.commonstatemachine.CommonMachineState` subclass is a part of a state-machine. +It updates the outside world with the following methods: + +- `setUiState` - updates the current UI state, thus the screen to the user. +- `setMachineState` - switches the logical state in a hosting state-machine. + +To reduce the boilerplate, create two protected inline convenience methods: + +- `setMachineState(block: AuthStateFactory.() -> AuthState)` - runs the block on the factory and changes the machine state. +- `setUiState(block: AuthUiRenderer.() -> AuthUiStateImpl)` - runs the renderer and updates the UI state + +Take a look at the [example](../assets/example/implementation/state/BaseAuthState.kt) for the implementation. + +## Step 3. Basic gesture processing +The `com.motorro.commonstatemachine.CommonMachineState` processes the user gestures in the `doProcess` method. +The concrete state usually only processes those gestures it is interested in. +Example: the login form state is interested only in gestures that are relevant to login actions, it does not process +gestures of the logout screen. +If the project uses the logging framework, add a simple `doProcess` override to the base class: if the state gets the +unexpected gesture - log a warning message using the logging framework. + +Take a look at the [example](../assets/example/implementation/state/BaseAuthState.kt) for the implementation. diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/base-test-class-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/base-test-class-implementation.md new file mode 100644 index 0000000..35816f9 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/base-test-class-implementation.md @@ -0,0 +1,44 @@ +# Module implementation: base test class +Use this reference to get the idea snd create a base abstract uni-test class. +This class will handle common test-mocks and initialization for the most of machine-states unit-tests: + +- state-machine mock +- factory mock +- renderer mock +- context mock + +Most of the machine-state test will subclass this base class. + +## Step 1. Base test class +Create an abstract internal class and call `BaseStateTest`. + +## Step 2. Protected properties +Create the following protected properties to use in child test-classes: + +- `stateMachine` - `com.motorro.commonstatemachine.CommonStateMachine` mock bound with [implementation gesture and ui-state types](data-and-ui-implementation.md) +- `stateFactory` - [machine state factory mock](renderer-factory-context-interface-implementation.md#step-2-state-factory-interface) +- `renderer` - [ui state renderer mock](renderer-factory-context-interface-implementation.md#step-3-ui-renderer-interface) +- `flowHost` - `com.motorro.commonstatemachine.flow.data.CommonFlowHost` mock bound with the return type +- `context` - [interstate context mock](renderer-factory-context-interface-implementation.md#step-4-state-context) +- `nextState` - a bound `com.motorro.commonstatemachine.CommonMachineState` type to use with the state-factory mock. +- `dispatcher` - a coroutine `TestDispatcher` that will be used in test functions. + +## Step 3. Mock initialization +You will need to initialize all the mocks listed in the previous state and mock some of the functions there: + +1. Create the `init` function and mark it with the annotation to run before the test run. +2. Initialize `dispatcher` with the `UnconfinedTestDispatcher` if not instructed otherwise. +3. Initialize coroutines `Main` dispatcher with the test dispatcher from the previous step. +4. Initialize the `stateMachine` with the relaxed mock. +5. Initialize the `stateFactory` with the strict mock. +6. Initialize the `flowHost` with the relaxed mock. +7. Initialize the `renderer` with the strict mock. +8. Initialize the `nextState` with the bound type alias. +9. Initialize the `context` with the object and override the properties with the mocks from the previous steps. +10. Create the empty open fun `doInit` and call it from the `init` function. Subclasses will override `doInit` + and initialize themselves with the mocks already created. +11. Create `deinit` function and call `Dispatchers.resetMain()` there. +12. Create the common coroutine test function wrapping `runTest` and using the test dispatcher from the second step. + Subclasses will use this function to run the tests. + +Take a look at the [example](../assets/example/implementation-tests/state/BaseStateTest.kt) of such a class. diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/common-child-flow.puml b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/common-child-flow.puml new file mode 120000 index 0000000..b135e8b --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/common-child-flow.puml @@ -0,0 +1 @@ +../../.shared/common-child-flow.puml \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-api.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-api.md new file mode 100644 index 0000000..41148fa --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-api.md @@ -0,0 +1,28 @@ +# Module API: data and UI API +Use this reference to create the required data and UI APIs. + +## Mechanics +The [Common Child Flow](https://github.com/motorro/CommonStateMachine/tree/master#common-child-flow-api) consists of two main interfaces: + +- A subtype of [CommonFlowDataApi](https://github.com/motorro/CommonStateMachine/blob/master/commonflow/data/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/data/CommonFlowDataApi.kt). + This interface binds together the gesture-system, ui-system, input and output providing the child flow initialization. + The API implementation is then created in the implementation module and could be injected into the parent flow. +- A subtype of [CommonFlowUiApi](https://github.com/motorro/CommonStateMachine/blob/master/commonflow/compose/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/compose/CommonFlowUiApi.kt). + This interface binds gesture-system and ui-system with the composable responsible for the view rendering. + The API implementation is then created in the implementation module and could be injected into the composition using locals or any other way. + +## Step 1. Data flow +Take the created [Gestures and UI-states](gesture-and-ui-state-api.md), [input and result](input-and-result-api.md) and create an interface to bind the generics +of the `com.motorro.commonstatemachine.flow.data.CommonFlowDataApi`. This API would be available to the flow consumer to start the feature flow. + +Take a look at the [class diagram](common-child-flow.puml) for more details. + +Take a look at the [example](../assets/example/api/AuthDataApi.kt) - data API definition + +## Step 2. UI flow +Take the created [Gestures and UI-states](gesture-and-ui-state-api.md) and create an interface to bind the generics +of the `com.motorro.commonstatemachine.flow.compose.CommonFlowUiApi`. This API would be available to the flow consumer to display the feature. + +Take a look at the [class diagram](common-child-flow.puml) for more details. + +Take a look at the [example](../assets/example/api/AuthUiApi.kt) - UI API definition diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-implementation.md new file mode 100644 index 0000000..a784446 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-implementation.md @@ -0,0 +1,76 @@ +# Module implementation: data classes +Use this reference to create data classes for the feature implementation. +Take a look at the [class diagram](common-child-flow.puml) to get the idea about the data types within the module. + +## Step 1. UI gestures +Create the sealed class to enumerate all the UI events (gestures) the user could send to the model: + +- button presses +- navigation elements clicks +- input changes + +The created sealed class should implement the base Gesture type defined in the API module. +See the [corresponding instruction](gesture-and-ui-state-api.md#step-1-gestures). +Put the created class to the `data` sub-package of the implementation module. + +Follow these common rules: + +- Make the created structure internal. +- Create data objects for gestures that don't carry any data. Example: user clicks the Back button. +- Create data classes for gestures that carry some data. Example: user changes an input text and the new value is passed with the event. +- Always include the data object for the backwards navigation gesture: `Back`. +- If the user didn't give you any information on the input gestures they want, `Back` would be the only child object in a structure. +- If user gives you a picture of a user interface - inspect it and create gestures for every screen. +- If the feature includes multiple screens - group the specific gestures by screen. +- The types of the properties of the ui-gestures should be immutable! + +Take a look at the [example](../assets/example/implementation/data/AuthGestureImpl.kt) - gesture collection. + +## Step 2. UI states +Create the sealed class to enumerate all the UI states the interface could display. + +The created sealed class should implement the base UI-state defined in the API module. +See the [corresponding instruction](gesture-and-ui-state-api.md#step-2-ui-states). +Put the created class to the `data` sub-package of the implementation module. + +Follow these common rules: + +- Make the created structure internal. +- Create data objects for ui-states that don't carry any data. Example: loading screen without any text messages. +- Create data classes for ui-states that carry some data. Example: login/password entry form. +- If user gives you a picture of a user interface - inspect it and create the data state for every dynamic UI element. +- If the feature includes multiple screens - group the specific ui-states by screen. +- The types of the properties of the ui-states should be immutable! Use `@Immutable` annotation for non-stable types to help Compose compiler optimize recompositions. + If immutable collection library is used in a project - use them for the collections in UI-states. + +Take a look at the [example](../assets/example/implementation/data/AuthUiStateImpl.kt) - UI-state collection. + +## Step 3. Interstate data +Create a data class to store the flow data passed between the machine states. +Follow the user's instructions to create a data class properties. +Put the created class to the `data` sub-package of the implementation module. + +Follow these common rules: + +- Make the created data class internal. +- The types of the properties of the data-state should be immutable! +- Include the flow startup input as a property. +- Set default values for the properties if user provides the information or try to guess them. + +Take a look at the [example](../assets/example/implementation/data/AuthStateData.kt) - interstate data for password authentication. + +Important: If you create any utility function based on the interstate data (like `isValidToAuthenticate` in the example above), +add a [unit test](../assets/example/implementation-tests/data/AuthStateDataTest.kt) for it. + +## Step 4. Test fixtures +Create some fixtures and test data for the created data classes: + +1. Create a `data` sub-package in the test source set. +2. Create a fixture file for data-class gestures. +3. Create a fixture file for data-class ui-states. +4. Create a fixture file for data-class interstate data. + For the interstate data, scan the project for already defined fixtures available at the class path and + prefer using them instead of creating your own fixtures + + +Take a look at the [example](../assets/example/implementation/data/AuthStateData.kt) - interstate data for password authentication. diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/di-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/di-implementation.md new file mode 100644 index 0000000..19ae9d8 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/di-implementation.md @@ -0,0 +1,44 @@ +# Module implementation: API implementation and DI +Use this reference to: + +- Create a [Data API](data-and-ui-api.md#step-1-data-flow) implementation. +- Create a [UI API](data-and-ui-api.md#step-2-ui-flow) implementation. +- If the project uses a DI framework to provide dependencies, implement DI modules. + +## Step 1. Implement Data API +Create the Data API implementation: + +- Create an internal class and implement the interface of the Data API. Depending on the DI/locator used, pass the state-factory implementation + to constructor or get a reference to it using the DI framework used. +- Override the `init` function. Create the factory, bind the incoming flow-host, create starting state. + Cast the resulting state to the correct API return type. +- Override the `getDefaultUiState` create a function to return the appropriate default [view-state](data-and-ui-implementation.md#step-2-ui-states): + - Loading + - An empty form + - Any state you consider good as a default +- If the backwards [navigation gesture](data-and-ui-implementation.md#step-1-ui-gestures) is there, override the `getBackGesture` function and return it. + +See the [example](../assets/example/implementation/AuthDataApiImpl.kt) for our sample authentication Data API. + +## Step 2. Implement UI API +Create the UI API implementation: + +- Create an internal class and implement the interface of the UI API. +- Override the `Screen` composable using the [component](ui-view-implementation.md#step-1-implement-scaffolding-screen) function. Cast the type of the + UI state to match the one in the UI API + +See the [example](../assets/example/implementation/AuthUiApiImpl.kt) for our sample authentication UI API. + +## Step 3. Define DI module(s) (optional). +Check the project setup or the user input. If the project uses DI to provide dependencies, implement the required modules. +Hints: + +- Make sure all individual state factories are DI-enabled. See the example [StateFactory](../assets/example/implementation/state/PreloadingState.kt). +- Make sure the main state-factory is DI-enabled. See the example [AuthStateFactoryImpl](../assets/example/implementation/state/AuthStateFactoryImpl.kt) +- Make sure the UI-renderer is DI-enabled. See the example [AuthUiRendererImpl](../assets/example/implementation/ui/AuthUiRendererImpl.kt). +- Make sure all interfaces are bound to the respectful implementations. +- Make sure the API implementations from the previous chapters are bound. +- Pay attention to (if used with frameworks like Hilt): + - Data API needs to be bound to the view-model module or similar. + - UI API needs to be bound to the view module (Activity, Fragment, etc.) +- The resulting modules should be public for the consumer. diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/gesture-and-ui-state-api.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/gesture-and-ui-state-api.md new file mode 100644 index 0000000..69384b6 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/gesture-and-ui-state-api.md @@ -0,0 +1,23 @@ +# Module API: Gesture and UI-state +Use this reference to create the UI gesture and UI state API. + +## Mechanics +The framework behind is a Model-View-Intent architecture, so: + +- The state is updated as a result of user intents (gestures) +- The view-state is fully described by data classes/objects - ui-states + +## Step 1. Gestures +The implementation is hidden from the module consumer using the internal gesture structures. +To use the feature we export the basic marker interface that may only contain some common public properties. +This interface would be implemented later in the implementation module. +Take a look at the [class diagram](common-child-flow.puml) for more details. +Take a look at the [example](../assets/example/api/AuthGesture.kt) - common API definition + +## Step 2. UI-states +The implementation is hidden from the module consumer using the internal ui-state structures. +To use the feature we export the basic marker interface that may only contain some common public properties. +This interface would be implemented later in the implementation module. +Take a look at the [class diagram](common-child-flow.puml) for more details. +Take a look at the [example](../assets/example/api/AuthUiState.kt) - common API definition + diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/input-and-result-api.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/input-and-result-api.md new file mode 100644 index 0000000..44a6ce3 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/input-and-result-api.md @@ -0,0 +1,22 @@ +# Module API: Module initialization data and flow result +Use this reference to create the required input data and define the flow result. + +## Mechanics +The [Common Child Flow](https://github.com/motorro/CommonStateMachine/tree/master#common-child-flow-api) runs a +child state-machine flow in a parent state-machine to get some result, display some UI, etc. +Example: the main application flow could run a child flow in-place to log the user-in. + +- The child flow could be initialized by some data passed from parent. +- The child flow could return some data back to the parent on completion. + +## Step 1. Input +If the user asks to provide any input - create an immutable type - a data class or a sealed structure. +If no input type explicitly required - confirm and use `Unit` +Take a look at the [class diagram](common-child-flow.puml) for more details. +Take a look at the [example](../assets/example/api/AuthInput.kt) - common API definition + +## Step 2. Result +If the user asks to provide a flow result - create an immutable type - a data class or a sealed structure. +If no result type explicitly required - confirm and use `Unit` +Take a look at the [class diagram](common-child-flow.puml) for more details. +Take a look at the [example](../assets/example/api/AuthResult.kt) - common API definition diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/proxy-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/proxy-implementation.md new file mode 100644 index 0000000..78266ef --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/proxy-implementation.md @@ -0,0 +1,44 @@ +# Module implementation: flow proxy +The common-child-flow module is intended to be used inside a hosting proxy state. +If the user asks you to integrate the feature-flow into a parent/hosting flow (e.g., an application main flow or onboarding flow), you must create a `ProxyMachineState` in the parent module. + +For a deeper dive into the state machine mechanics and common components, refer to the [Core Architecture](ARCHITECTURE.md). +For a deeper dive into the common child flow philosophy and design, refer to the [Common Child Flow Architecture](COMMON-CHILD-FLOW.md). + +## Step 1. Preparing the host flow (Gesture & UI-State Adapters) +To run a child feature flow inside a parent state machine, the parent's Gesture and UI-state systems must be extended to wrap the child's gestures and UI states. + +- Proxy Gesture: In the parent's sealed Gesture class/structure, add a wrapper case containing the child's feature gesture. + Example: `data class Auth(val child: AuthGesture) : MainGesture()`. + See the [example gesture](../assets/example/app/data/MainGesture.kt). +- Proxy UI-State: In the parent's sealed UI-state class/structure, add a wrapper case containing the child's feature UI-state. + Example: `data class Auth(val child: AuthUiState) : MainUiState()`. + See the [example UI state](../assets/example/app/data/MainUiState.kt). + +## Step 2. Implement parent proxy state +Create a `ProxyMachineState` subclass in the parent module that encapsulates the child state machine flow. + +The proxy state must: + +- Extend `ProxyMachineState`. Pass `api.getDefaultUiState()` as the initial UI state to the super constructor. +- Implement `CommonFlowHost`. Create a `flowHost` instance that handles the child flow completion. When `onComplete(result)` is called + by the child, update the parent machine state using `setMachineState(...)` to transition to the next parent state based on the result. +- Override `init()`. Call `api.init(flowHost, input)` to create and return the child state machine's starting state. +- Override `mapGesture(parent: ParentGesture)`. Unwraps the child gesture from the parent proxy gesture wrapper (e.g., `MainGesture.Auth -> parent.child`). + Additionally, map system-level parent gestures (such as `MainGesture.Back`) to the child's back gesture (`api.getBackGesture()`) if appropriate. +- Override `mapUiState(child: ChildUiState)`. Wraps the child's UI state into the parent's UI state wrapper (e.g., `MainUiState.Auth(child)`). + +See the [example proxy state](../assets/example/app/state/AuthProxyState.kt) for a complete implementation. + +## Step 3. Create a parent state-factory method & DI setup +To instantiate the proxy state cleanly within the parent flow: + +- Local Proxy Factory: Create an inner factory class or separate factory class for the proxy state, named `StateFactory` (e.g., `@Factory class StateFactory(private val api: AuthDataApi)`). + This factory should inject the child feature's `DataApi` (e.g., `AuthDataApi`) and instantiate `AuthProxyState(context, ..., api)`. +- Parent State Factory Interface: Add a method in the parent's `StateFactory` interface to produce the proxy state (e.g., `fun auth(): MainState`). +- Parent State Factory Implementation: Inject the proxy state factory into the parent `StateFactory` implementation, and delegate + the state creation call to it. +- DI Registration: Ensure the proxy state factory and parent state factory implementation are annotated + for DI (e.g., `@Factory` for Koin, `@Inject` / `@ViewModelScoped` for Hilt). + +See the [example main state factory](../assets/example/app/state/MainStateFactory.kt) and [proxy state factory](../assets/example/app/state/AuthProxyState.kt). diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/renderer-factory-context-interface-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/renderer-factory-context-interface-implementation.md new file mode 100644 index 0000000..9fe585a --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/renderer-factory-context-interface-implementation.md @@ -0,0 +1,85 @@ +# Module implementation: factory interface, ui-renderer interface, context +Use this reference to create auxiliary interfaces and classes: + +- state factory +- UI-state renderer +- Interstate context + +Take a look at the [class diagram](common-child-flow.puml) to get the idea about components purpose. + +## Step 1. CommonMachineState binding +The framework main building block is a `com.motorro.commonstatemachine.CommonMachineState` - it is a logical state of a state-machine running the flow. +The generic class should be bound with two parameters: + +- [Gesture](data-and-ui-implementation.md#step-1-ui-gestures) +- [UI-state](data-and-ui-implementation.md#step-2-ui-states) + +Create an internal type-alias and put it to the `state` sub-package. +Take a look at the [example](../assets/example/implementation/state/AuthState.kt). + +## Step 2. State factory interface +Whenever it is a time to switch to another state, the current state does not create the new state directly - it delegates +the state creation to the state-factory: + +- State-factory foundation is an interface with the methods like: `createLoading`, `createForm`, etc. +- Factory functions may pass some parameters: [interstate data](data-and-ui-implementation.md#step-3-interstate-data), errors, auxiliary data. +- Machine-states get the reference to the state factory and use it to create the new state. +- State-factory interface is internal in the module. + +Your task here is to: + +- Create a state-factory internal interface +- Put the interface `state` sub-package of the implementation module. +- Every factory function should return the `CommonMachineState` binding type created in the previous step. +- Create some startup function to build the initializing state. The method should include the [input](input-and-result-api.md#step-1-input) as a parameter. +- Create `terminated` method with the [result](input-and-result-api.md#step-2-result) as a parameter. + +Take a look at the [example](../assets/example/implementation/state/AuthState.kt). + +## Step 3. UI renderer interface +States are encouraged to use a separate renderer interface that translates the [interstate data](data-and-ui-implementation.md#step-3-interstate-data) +to the [UI state](data-and-ui-implementation.md#step-2-ui-states). + +Follow these common rules: +- Make the created interface internal. +- Put the interface into the `ui` sub-package of the implementation module. +- Create a function for every UI-state in a sealed system. +- Every function should return the sealed class ui-state type. +- Provide an [interstate data](data-and-ui-implementation.md#step-3-interstate-data) parameter if the result state is a data class. +- Provide more parameters if data is required to build the ui-state, and it could not be built using the data-state. + Example: when creating some error view-state, add an error parameter. The calling state will provide it separately. + +Take a look at the [example](../assets/example/implementation/ui/AuthUiRenderer.kt). + +## Step 4. State context +To make the tools created above accessible, create an accessor interface - a context. +The state being created will receive that context in a constructor and use it to access factory, renderer, etc. + +Follow these common rules: +- Make the created interface internal. +- Put the interface into the `state` sub-package of the implementation module. +- Add properties resolving to the following tools: + - state factory + - ui-renderer + - flow host bound with the [result type](input-and-result-api.md#step-2-result) used to interact with the parent state. + +Take a look at the [example](../assets/example/implementation/state/AuthContext.kt). + +## Step 5. State factory implementation +Add an implementation class to implement the factory interface from the previous step. +The implementation will: + +- Receive state-factories in constructor and use them to create the states with complex dependencies (see the `preloading` implementation in the [example](../assets/example/implementation/state/AuthStateFactoryImpl.kt)). +- Create simple states locally (see the `preloadError` implementation in the [example](../assets/example/implementation/state/AuthStateFactoryImpl.kt)) +- For the even simpler states you may want to implement them ad-hoc (see the `terminated` implementation in the [example](../assets/example/implementation/state/AuthStateFactoryImpl.kt)). + +Follow these common rules: + +- Create the class in the implementation module. +- If required by the DI framework used in project, add required inject annotations. +- Inject external factories and dependencies required by the state to the factory constructor. + Try to use lazy provider injection to delay the initialization. +- Pass the flow host to the factory constructor. +- Build the private context object to pass to the child states. + +See the [example](../assets/example/implementation/state/AuthStateFactoryImpl.kt). diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/state-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/state-implementation.md new file mode 100644 index 0000000..1308d91 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/state-implementation.md @@ -0,0 +1,102 @@ +# Module implementation: state implementation +Use this reference to implement the machine-state(s) for the `CommonStateMachine` framework. +Take a look at the [class diagram](common-child-flow.puml) to get the idea about components purpose. +Take a look at the [example](../assets/example/implementation/state/PreloadingState.kt). + +## Mechanics +Here's how the machine-state works: + +- All machine states subclass the basic `com.motorro.commonstatemachine.CommonMachineState` (or its child) bound with the gesture and UI-state types + as described in the basic [interface implementation](renderer-factory-context-interface-implementation.md#step-1-commonmachinestate-binding) + and [base state implementation](base-state-implementation.md) manuals. +- It'd be great if a state has a single responsibility whenever possible and limit the number of gestures it processes. + For example, the document loading could be divided into the following states: + - `LoadingState` - runs the usecase and updates the interstate data. + - `ContentState` - displays loaded data and interacts with a user. + - `ErrorState` - displays an error and if possible, offers to retry by transferring back to `LoadingState`. +- The state has the following callbacks which are called by the hosting state machine: + - `doStart` - called when the state is started. + - `doProcess` - called when the state receives a gesture. + - `doClear` - called when the state is removed from the state-machine. +- The state could call the following functions to interact with the hosting state machine: + - `setMachineState` - called to switch to another state. We tipically use the factory to create the new state. + - `setUiState` - called to update the UI state. We tipically use the renderer to update the UI. +- It is important that `setMachineState` and `setUiState` are called from when state is active (between calls to `doStart` and `doClear`). + If the call is made from the inactive state - it will fail. +- Always postpone the heavy initialization till `doStart` is called. + +## Step 1. Create a class +Put the class to the `state` sub-package of the implementation module. +Follow the following common rules: + +- Make the class internal to the implementation module. +- Subclass the [base state](base-state-implementation.md). +- Pass the [context](renderer-factory-context-interface-implementation.md#step-4-state-context) as a first parameter. +- Pass the [input data](input-and-result-api.md#step-1-input) as a parameter. + +## Step 2. Add a startup implementation (or a dummy) +When the `com.motorro.commonstatemachine.CommonMachineState` is started, the state-machine calls the `doStart` lifecycle method. +Implement the method to update the UI, start loading data, etc. +The `com.motorro.commonstatemachine.coroutines.CoroutineState`, which is an ancestor of the [abstract base state](base-state-implementation.md), has a +bound coroutine scope property: `stateScope`. Use it to run coroutines. The scope would be canceled when the state is removed from the state-machine. + +## Step 3. Add a gesture processing implementation +When the user interface emits a gesture, the state-machine calls the `onGesture` method of a current `com.motorro.commonstatemachine.CommonMachineState`. +Implement the override of a function that: + +- Will process the gestures specific to this state. +- Will call the super-method on every other unexpected gesture which are out of scope of this state. + +## Step 4. Implement a local state factory +To increase the visibility and to decrease the boilerplate, the states that require external dependencies (use-cases, managers, APIs, etc.) have their +own state factories put to the DI graph. This lifts a burden of state creation micromanagement away from +the main state factory implementation. +Create such a factory for the startup state: + +- Implement as a child class with the name `StateFactory`. +- If required by the DI framework used in project, add required inject annotations. +- Inject external dependencies required by the state to the factory constructor. +- Add a single `create` method that will accept the [context](renderer-factory-context-interface-implementation.md#step-4-state-context) and all the required interstate parameters that will come from the main factory. + +## Step 5. Create a method in the main state-factory interface +The states are to be created by the [main state-factory](renderer-factory-context-interface-implementation.md#step-2-state-factory-interface) so the states +do not have hard links to one each-other. +When you create a new state, you need to create a method in the main state-factory for it: + +- Name the function to indicate what happens inside the state. Examples: `preloading`, `error`, etc. +- Pass the common interstate if needed as a data parameter (see the `form` function in the [example](../assets/example/implementation/state/AuthStateFactory.kt)). +- Pass the specific parameters - those needed by this particular state only (see the `preload` function in the [example](../assets/example/implementation/state/AuthStateFactory.kt)). +- Implement the method in the [implementation class](renderer-factory-context-interface-implementation.md#step-5-state-factory-implementation). + +## Example 1. Preloading state +For example of a state that preloads some data with the usecase, take a look at the [PreloadingState](../assets/example/implementation/state/PreloadingState.kt) class. +The state does the following when started: + +- Sets the `Loading` UI state in `doStart` method. +- Calls the `preload` function that: + - calls a usecase to preload data some data or emulates loading if the usecase is not specified, and we are doing a dummy preloading. + - creates the valid [data-state](data-and-ui-implementation.md#step-3-interstate-data). + - switches to the next state by creating it with a factory and calling the `setMachineState` function. +- Implements the `doProcess` function to handle the back gesture or any other gestures that are relevant to this state. +- Does moderate debug-logging in lifecycle methods to track the flow. + +## Example 2. Preloading error state +If any operation fails, you'd better switch the state-machine to an error state. See [PreloadingErrorState](../assets/example/implementation/state/PreloadingErrorState.kt) +state to handle the preloading error. +The typical error state would: + +- Set the `Error` UI state in `doStart` method passing the error to display +- Handle the back gesture that would terminate the flow or return to a previous state if relevant keeping the variables needed. +- May switch back to the loading state if the error is considered recoverable. +- States of that kind are usually rather simple so you don't need any state-factory. + +## Example 3. Form state +For example of a state that handles the user input, validates and prepares the data, take a look at the [FormState](../assets/example/implementation/state/FormState.kt) class. +The state does the following when started: + +- Updates the UI state with the initial data. +- Listens to the gestures, changes the interstate data and updates the screen in the `doProcess` method. +- Changes the machine state when the action gesture is received. +- Validates the data + + diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/state-test-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/state-test-implementation.md new file mode 100644 index 0000000..4bc0b15 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/state-test-implementation.md @@ -0,0 +1,72 @@ +# Module implementation: state tests +Use this reference to create tests for the [created machine states](state-implementation.md). +`CommonMachineState` tests ensure that state transitions, UI state mapping, and side effects are correctly handled. +Typical task would be the following: + +1. If the [base-test class](base-test-class-implementation.md) is not already available at the same module, add it. +2. Create a test class and extend the base-test class. +3. Define the mocks needed for the test: use-cases, apis, etc. as the lateinit vars. +4. Override the `doInit` function and initialize the mocks. +5. Create test functions using the `test` function defined in the base class. + +## Note: Mocking use-cases and suspend functions that should never complete +Some states call supplied use-cases to load some data and update the machine state accordingly. +Usually the use-case contain some suspend function to do that. +You may want this call to never end. For example - to check the `Back` gesture terminates the flow +while the user awaits for a long loading operation. +In those case use the empty `suspendCancellableCoroutine` to emulate the operation: +```kotlin +// Mock creation +private val requirementsMock: GetPasswordRequirements = mock() + +// Suspending the load operation +everySuspend { requirementsMock.invoke() } calls { suspendCancellableCoroutine { } } +``` +Example: the [test](../assets/example/implementation-tests/state/PreloadingErrorStateTest.kt) for the [PreloadingState](../assets/example/implementation/state/PreloadingState.kt): + +- The state asynchronously loads some data with the use-case at startup. +- You need to check the `Back` pressed while loading terminates. + +## Step 1. Testing the initial rendering +If the state updates the UI, check the UI is updated on state startup: + +- If the UI-renderer is defined in the module - mock the corresponding method, check the calling arguments and a call to `CommonStateMachine::setUiState`. +- If there's no UI-renderer and the state updates the ui directly, evaluate the `CommonStateMachine::setUiState` argument directly. + +Example: the [test](../assets/example/implementation-tests/state/PreloadingErrorStateTest.kt) for the [PreloadingState](../assets/example/implementation/state/PreloadingState.kt): + +- The state asynchronously loads some data with the use-case at startup. +- You need to check the state displays `Loading`. + +## Step 2. Testing the asynchronous outcomes +If the state uses any use-cases or asynchronous operations run at startup, check their outcome: + +- UI updates if any. +- state changes if any. + + +Example: the [test](../assets/example/implementation-tests/state/PreloadingErrorStateTest.kt) for the [PreloadingState](../assets/example/implementation/state/PreloadingState.kt): + +- The state asynchronously loads some data with the use-case at startup. +- You need to check the success: state updates the interstate data and proceeds. +- You need to check the failure: state switches to the error state. + +## Step 3. Testing gestures +If the state processes any gestures in `doProcess` callback, check each gesture path: + +- use [fixtures](test-fixtures-implementation.md) if there are any for gestures. +- check the UI changes if any. +- check interstate data changes if any. +- check machine state changes if any. + +Example 1: the [test](../assets/example/implementation-tests/state/FormStateTest.kt) for the [FormState](../assets/example/implementation/state/FormState.kt): + +- checks the gestures change the interstate data. +- checks the UI updates. +- checks the data validation and state transfer. + +Example 2: the [test](../assets/example/implementation-tests/state/PreloadingErrorStateTest.kt) for the [PreloadingErrorState](../assets/example/implementation/state/PreloadingErrorState.kt): + +- checks the `Back` gesture changes the machine state. +- checks `Action` gesture conditionally changes the machine-state depending on the error. +- checks the data validation and state transfer. diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/test-fixtures-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/test-fixtures-implementation.md new file mode 100644 index 0000000..ecaf1e6 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/test-fixtures-implementation.md @@ -0,0 +1,15 @@ +# Module implementation: test fixtures +Use this reference to create test fixtures for the [data classes of the feature implementation](data-and-ui-implementation.md): + +1. Create a `data` sub-package in the test source set. +2. Create a fixture file for data-class gestures. +3. Create some ui-state fixture to use when mocking the [renderer](renderer-factory-context-interface-implementation.md#step-3-ui-renderer-interface). +4. Create a fixture file for data-class interstate data, input and result data classes. + +For the fixture data, scan the project for already defined fixtures available and prefer using them instead of creating +your own fixtures. For example, look for the modules like `domain` or `entity` and use their fixtures. Check existing feature +modules for the clue how to create fixtures and where to get the common fixture module. + +Take a look at the [example](../assets/example/implementation-tests/data/authGesturesFixtures.kt) gesture fixtures. +Take a look at the [example](../assets/example/implementation-tests/data/uiStatesFixtures.kt) ui-state fixtures. +Take a look at the [example](../assets/example/implementation-tests/data/dataStatesFixtures.kt) interstate. diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/ui-renderer-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/ui-renderer-implementation.md new file mode 100644 index 0000000..4ef14e6 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/ui-renderer-implementation.md @@ -0,0 +1,22 @@ +# Module implementation: renderer implementation +Use this reference to implement the [UI-renderer](renderer-factory-context-interface-implementation.md#step-3-ui-renderer-interface) (if used in the module). + +## Step 1. Implement renderer +Follow these steps: + +- Create the implementation class in the `ui` package of the implementation module and add the UI-renderer interface to the list of implementations. +- Enumerate all [UI-states](data-and-ui-implementation.md#step-2-ui-states) and check the source UI-renderer interface. + Find the UI-states corresponding to renderer methods. +- Implement the interface methods transforming the passed parameters to the respectful states. +- Renderer may require other dependencies - resource mappers, etc. Make the class injectable using the DI framework used in a project. + +Take a look at [example](../assets/example/implementation/ui/AuthUiRendererImpl.kt) for our Auth application. + +## Step 2. Create a renderer test +Sometimes UI has a complex model that requires a lot to render. +Secure the logic by writing a unit-test for the renderer. + +- Check every interface method +- Use [fixtures](test-fixtures-implementation.md) if available, modify them by copy-constructors if needed, or add custom. + +Take a look at the [example](../assets/example/implementation-tests/ui/AuthUiRendererImplTest.kt). \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/ui-view-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/ui-view-implementation.md new file mode 100644 index 0000000..04476cc --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/ui-view-implementation.md @@ -0,0 +1,42 @@ +# Module implementation: basic views +Use this reference to implement the basic views for the implementation. + +## Step 1. Implement scaffolding screen +Create the scaffolding composable that will draw all the [UI States](data-and-ui-implementation.md#step-2-ui-states): + +- Follow common design system and approach in a project if available (scaffolds, appbars, themes, dimensions, etc.). +- If no design system found use the following approach: + - Use `Scaffold` as a top-level element + - Create top app bar with a module title +- The function should accept the following parameters: + - Required: `uiState` - one of the [UI State implementation](data-and-ui-implementation.md#step-2-ui-states) will be passed. + - Required: `onGesture` - a callback accepting [Gesture implementation](data-and-ui-implementation.md#step-1-ui-gestures). Make this parameter the last one. + - Optional: `modifier` with a default value in case this is a common approach in current project. +- Place `when` operator in the content section and implement all branches of the UI states. +- All functions should be internal at most + +See the [example](../assets/example/implementation/ui/AuthScreen.kt) for our sample authentication project. + +## Step 2. Implement child views +Create a separate file for child views. Create a file per each [UI State](data-and-ui-implementation.md#step-2-ui-states): + +- Follow common design system and approach in a project if available (scaffolds, appbars, themes, dimensions, etc.). +- The function should accept the following parameters: + - Required: `uiState` - the concrete [UI State implementation](data-and-ui-implementation.md#step-2-ui-states) will be passed. + - Required: `onGesture` - a callback accepting [Gesture implementation](data-and-ui-implementation.md#step-1-ui-gestures). Make this parameter the last one. + - Required: `modifier` with a default value. +- If no design system found use the following approach: + - Each screen should take `fillMaxSize()` to occupy the available space. + - Center content both horizontally (`Alignment.CenterHorizontally`) and vertically (`Arrangement.Center` for `Column` or `Alignment.Center` for `Box`). + - Use common project padding if defined in the design system (e.g., `Dimensions.medium`) for the main container. + - Use design system theme or `MaterialTheme` for colors and typography to ensure consistency. +- Evaluate each UI state and create corresponding widgets +- Use `onGesture` to pass user gestures. Consider moving the lambas in `onChange` callbacks to `remember` to optimize recompositions if you find it effective. +- Generate previews. If you think multiple previews are needed - use data providers. +- Put a child view to the `when` branches of a scaffolding screen. + +See the examples: + +- [Loading view](../assets/example/implementation/ui/AuthLoadingScreen.kt) +- [Form view](../assets/example/implementation/ui/AuthFormScreen.kt) +- [Error view](../assets/example/implementation/ui/AuthErrorScreen.kt) diff --git a/skills/build_skills.sh b/skills/build_skills.sh deleted file mode 100644 index 5415266..0000000 --- a/skills/build_skills.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "$SCRIPT_DIR" - -# Directory to store zip files, defaults to "artifacts" if not provided -ARTIFACTS_DIR="${1:-$(pwd)/../artifacts}" - -# Ensure ARTIFACTS_DIR is an absolute path -if [[ ! "$ARTIFACTS_DIR" = /* ]]; then - ARTIFACTS_DIR="$(pwd)/$ARTIFACTS_DIR" -fi - -echo "Building skills to $ARTIFACTS_DIR..." -mkdir -p "$ARTIFACTS_DIR" - -find . -path '*/.*' -prune -o -name "SKILL.md" -print | while read -r skill_file; do - skill_dir=$(dirname "$skill_file") - rel_dir="${skill_dir#./}" - dir_name=$(basename "$skill_dir") - parent_dir=$(basename "$(dirname "$skill_dir")") - skill_name="skill-${parent_dir}-${dir_name}" - - zip_name="${skill_name}.zip" - echo "Zipping $rel_dir to $zip_name" - - tmp_stage=$(mktemp -d) - mkdir -p "$tmp_stage/$rel_dir" - if command -v rsync >/dev/null 2>&1; then - rsync -aL "$skill_dir/" "$tmp_stage/$rel_dir/" - else - cp -RL "$skill_dir/." "$tmp_stage/$rel_dir/" - fi - - echo "TEMP: $tmp_stage" - - (cd "$tmp_stage" && zip -q -r "$ARTIFACTS_DIR/$zip_name" "$rel_dir" -x "*.DS_Store") - - rm -rf "$tmp_stage" -done - -echo "Skills build complete." diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthDataApi.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthDataApi.kt new file mode 100644 index 0000000..e03a962 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthDataApi.kt @@ -0,0 +1,8 @@ +package com.motorro.commonstatemachine.examples.skills.auth.api +import com.motorro.commonstatemachine.flow.data.CommonFlowDataApi + +/** + * Data API used to run a child-flow in a proxy machine + * Available to a feature consumer + */ +interface AuthDataApi : CommonFlowDataApi \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthGesture.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthGesture.kt new file mode 100644 index 0000000..b93d81e --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthGesture.kt @@ -0,0 +1,7 @@ +package com.motorro.commonstatemachine.examples.skills.auth.api + +/** + * A base type for authentication user gestures + * Available to a feature consumer + */ +interface AuthGesture \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthInput.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthInput.kt new file mode 100644 index 0000000..90aba9d --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthInput.kt @@ -0,0 +1,7 @@ +package com.motorro.commonstatemachine.examples.skills.auth.api + +/** + * Initializing data for the feature flow + * @property skippable If true, allows user to skip authentication + */ +data class AuthInput(val skippable: Boolean) \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthResult.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthResult.kt new file mode 100644 index 0000000..e57a379 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthResult.kt @@ -0,0 +1,7 @@ +package com.motorro.commonstatemachine.examples.skills.auth.api + +/** + * Feature flow result + * @param authenticated Example of a property returned to a parent flow + */ +data class AuthResult(val authenticated: Boolean) \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthUiApi.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthUiApi.kt new file mode 100644 index 0000000..1663448 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthUiApi.kt @@ -0,0 +1,8 @@ +package com.motorro.commonstatemachine.examples.skills.auth.api +import com.motorro.commonstatemachine.flow.compose.CommonFlowUiApi + +/** + * UI API used display a child flow inside a parent composition + * Available to a feature consumer + */ +interface AuthUiApi: CommonFlowUiApi \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthUiState.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthUiState.kt new file mode 100644 index 0000000..b16324d --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/api/AuthUiState.kt @@ -0,0 +1,7 @@ +package com.motorro.commonstatemachine.examples.skills.auth.api + +/** + * A base type for authentication UI state + * Available to a feature consumer + */ +interface AuthUiState \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/App.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/App.kt new file mode 100644 index 0000000..2aea628 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/App.kt @@ -0,0 +1,30 @@ +package com.motorro.commonstatemachine.examples.skills.app + +import android.app.Application +import com.motorro.commonstatemachine.examples.skills.auth.implementation.AuthModule +import com.motorro.commonstatemachine.skills.usecase.UsecaseModule +import io.github.aakira.napier.DebugAntilog +import io.github.aakira.napier.Napier +import org.koin.android.ext.koin.androidContext +import org.koin.android.ext.koin.androidLogger +import org.koin.core.annotation.KoinApplication +import org.koin.plugin.module.dsl.startKoin + +@KoinApplication(modules = [ + UsecaseModule::class, + AuthModule::class, + MainModule::class +]) +class App : Application() { + override fun onCreate() { + super.onCreate() + + Napier.base(DebugAntilog()) + + startKoin { + allowOverride(false) + androidLogger() + androidContext(this@App) + } + } +} diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/MainActivity.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/MainActivity.kt new file mode 100644 index 0000000..d8d6ccf --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/MainActivity.kt @@ -0,0 +1,23 @@ +package com.motorro.commonstatemachine.examples.skills.app + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import com.motorro.commonstatemachine.examples.auth.appcore.ui.theme.SkillsTheme +import com.motorro.commonstatemachine.examples.skills.app.ui.MainScreen + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + SkillsTheme { + MainScreen { + finish() + } + } + } + } +} + diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/MainModule.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/MainModule.kt new file mode 100644 index 0000000..9f15aa7 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/MainModule.kt @@ -0,0 +1,8 @@ +package com.motorro.commonstatemachine.examples.skills.app + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan("com.motorro.commonstatemachine.examples.skills.app") +internal class MainModule \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/MainViewModel.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/MainViewModel.kt new file mode 100644 index 0000000..000ebb7 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/MainViewModel.kt @@ -0,0 +1,32 @@ +package com.motorro.commonstatemachine.examples.skills.app + +import com.motorro.commonstatemachine.CommonMachineState +import com.motorro.commonstatemachine.examples.skills.app.data.MainGesture +import com.motorro.commonstatemachine.examples.skills.app.data.MainUiState +import com.motorro.commonstatemachine.examples.skills.app.state.MainStateFactory +import com.motorro.commonstatemachine.flow.data.CommonFlowDataApi +import com.motorro.commonstatemachine.flow.data.CommonFlowHost +import com.motorro.commonstatemachine.flow.viewmodel.CommonFlowViewModel +import org.koin.core.annotation.Factory +import org.koin.core.annotation.KoinViewModel +import org.koin.core.component.KoinComponent +import org.koin.core.component.get +import org.koin.core.parameter.parametersOf + +@KoinViewModel +internal class MainViewModel(api: MainApi) : CommonFlowViewModel( + api = api, + init = Unit +) + +@Factory +internal class MainApi : CommonFlowDataApi, KoinComponent { + override fun init(flowHost: CommonFlowHost, input: Unit): CommonMachineState { + val factory = this@MainApi.get { + parametersOf(flowHost) + } + return factory.auth() + } + override fun getDefaultUiState(): MainUiState = MainUiState.Splash + override fun getBackGesture(): MainGesture = MainGesture.Back +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/data/MainGesture.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/data/MainGesture.kt new file mode 100644 index 0000000..8126794 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/data/MainGesture.kt @@ -0,0 +1,26 @@ +/* + * 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.examples.skills.app.data + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthGesture + +/** + * Main flow gesture + */ +internal sealed class MainGesture { + + object Back : MainGesture() + + data class Auth(val child: AuthGesture) : MainGesture() +} diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/data/MainUiState.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/data/MainUiState.kt new file mode 100644 index 0000000..c25ca24 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/data/MainUiState.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.commonstatemachine.examples.skills.app.data + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthUiState + + +internal sealed class MainUiState { + /** + * Preloading + */ + data object Splash : MainUiState() + + /** + * Authentication + */ + data class Auth(val child: AuthUiState) : MainUiState() + + /** + * Content placeholder + */ + data object Friends : MainUiState() +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/state/AuthProxyState.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/state/AuthProxyState.kt new file mode 100644 index 0000000..9f1bcd1 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/state/AuthProxyState.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.commonstatemachine.examples.skills.app.state + +import com.motorro.commonstatemachine.ProxyMachineState +import com.motorro.commonstatemachine.examples.skills.app.data.MainGesture +import com.motorro.commonstatemachine.examples.skills.app.data.MainUiState +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthDataApi +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthGesture +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthInput +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthUiState +import com.motorro.commonstatemachine.flow.data.CommonFlowHost +import io.github.aakira.napier.Napier +import org.koin.core.annotation.Factory + +internal class AuthProxyState( + private val context: MainContext, + private val requireAuthentication: Boolean, + private val api: AuthDataApi +) : ProxyMachineState(api.getDefaultUiState()) { + + private val flowHost = CommonFlowHost { result -> + if (result.authenticated) { + Napier.d { "Authenticated. Checking..." } + setMachineState(context.factory.init()) + } else { + Napier.d { "Authentication cancelled. Terminating..." } + setMachineState(context.factory.terminated()) + } + } + + override fun init() = api.init(flowHost, AuthInput(requireAuthentication.not())) + + override fun mapGesture(parent: MainGesture): AuthGesture? = when (parent) { + is MainGesture.Auth -> parent.child + MainGesture.Back -> api.getBackGesture() + } + + override fun mapUiState(child: AuthUiState): MainUiState = MainUiState.Auth(child) + + @Factory + class StateFactory(private val api: AuthDataApi) { + fun create(context: MainContext) = AuthProxyState( + context = context, + requireAuthentication = true, + api = api + ) + } +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/state/CheckingAuthentication.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/state/CheckingAuthentication.kt new file mode 100644 index 0000000..713e591 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/state/CheckingAuthentication.kt @@ -0,0 +1,52 @@ +package com.motorro.commonstatemachine.examples.skills.app.state + +import com.motorro.commonstatemachine.examples.skills.app.data.MainGesture +import com.motorro.commonstatemachine.skills.domain.session.SessionManager +import com.motorro.commonstatemachine.skills.domain.session.data.Session +import io.github.aakira.napier.Napier +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import org.koin.core.annotation.Factory + +internal class CheckingAuthentication( + context: MainContext, + private val sessionManager: SessionManager +) : BaseMainState(context) { + override fun doStart() { + setUiState(renderer.renderSplash()) + checkAuthentication() + } + + private fun checkAuthentication() = stateScope.launch { + Napier.d { "Checking current session..." } + when(sessionManager.session.catch { emit(Session.None) }.first()) { + is Session.Active -> { + Napier.d { "Authenticated. Transferring to content..." } + setMachineState(factory.content()) + } + Session.None -> { + Napier.d { "No active user. Requiring authentication..." } + setMachineState(factory.auth()) + } + } + } + + override fun doProcess(gesture: MainGesture) { + when(gesture) { + is MainGesture.Back -> { + Napier.d { "Back requested. Terminating..." } + setMachineState(factory.terminated()) + } + else -> super.doProcess(gesture) + } + } + + @Factory + class StateFactory(private val sessionManager: SessionManager) { + fun create(context: MainContext) = CheckingAuthentication( + context = context, + sessionManager = sessionManager + ) + } +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/state/ContentState.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/state/ContentState.kt new file mode 100644 index 0000000..6d22d89 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/state/ContentState.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.examples.skills.app.state + +import com.motorro.commonstatemachine.examples.skills.app.data.MainGesture +import io.github.aakira.napier.Napier +import org.koin.core.annotation.Factory + +/** + * Content stub + */ +internal class ContentState(context: MainContext) : BaseMainState(context) { + override fun doStart() { + setUiState(renderer.renderContent()) + } + + override fun doProcess(gesture: MainGesture) { + when (gesture) { + MainGesture.Back -> { + Napier.d("Back. Terminating...") + setMachineState(factory.terminated()) + } + else -> super.doProcess(gesture) + } + } + + @Factory + class StateFactory() { + fun create(context: MainContext) = ContentState( + context + ) + } +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/state/MainContext.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/state/MainContext.kt new file mode 100644 index 0000000..d92d16a --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/state/MainContext.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.examples.skills.app.state + +import com.motorro.commonstatemachine.examples.skills.app.ui.MainUiRenderer +import com.motorro.commonstatemachine.flow.data.CommonFlowHost + +/** + * Main flow context + */ +internal interface MainContext { + /** + * Main flow host + */ + val flowHost: CommonFlowHost + + /** + * State factory + */ + val factory: MainStateFactory + + /** + * UI renderer + */ + val renderer: MainUiRenderer +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/state/MainState.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/state/MainState.kt new file mode 100644 index 0000000..140d336 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/state/MainState.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.commonstatemachine.examples.skills.app.state + +import com.motorro.commonstatemachine.CommonMachineState +import com.motorro.commonstatemachine.coroutines.CoroutineState +import com.motorro.commonstatemachine.examples.skills.app.data.MainGesture +import com.motorro.commonstatemachine.examples.skills.app.data.MainUiState +import io.github.aakira.napier.Napier + +internal typealias MainState = CommonMachineState + +internal abstract class BaseMainState(context: MainContext) : CoroutineState(), MainContext by context { + override fun doProcess(gesture: MainGesture) { + Napier.w("Unhandled gesture: $gesture") + } +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/state/MainStateFactory.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/state/MainStateFactory.kt new file mode 100644 index 0000000..5b5d987 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/state/MainStateFactory.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.commonstatemachine.examples.skills.app.state + +import com.motorro.commonstatemachine.examples.skills.app.ui.MainUiRenderer +import com.motorro.commonstatemachine.flow.data.CommonFlowHost +import org.koin.core.annotation.Factory +import org.koin.core.annotation.InjectedParam + +/** + * Main state factory + */ +internal interface MainStateFactory { + fun init(): MainState + fun auth(): MainState + fun content(): MainState + fun terminated(): MainState + + @Factory + class Impl( + @InjectedParam flowHost: CommonFlowHost, + renderer: MainUiRenderer, + private val checkingAuthenticationFactory: CheckingAuthentication.StateFactory, + private val contentFactory: ContentState.StateFactory, + private val authFactory: AuthProxyState.StateFactory + ) : MainStateFactory { + + private val context = object : MainContext { + override val flowHost: CommonFlowHost = flowHost + override val factory: MainStateFactory get() = this@Impl + override val renderer: MainUiRenderer = renderer + } + + override fun init() = checkingAuthenticationFactory.create(context) + + override fun auth() = authFactory.create(context) + + override fun content() = contentFactory.create(context) + + override fun terminated() = object : MainState() { + override fun doStart() { + context.flowHost.onComplete(Unit) + } + } + } +} diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/ui/MainScreen.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/ui/MainScreen.kt new file mode 100644 index 0000000..566c749 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/ui/MainScreen.kt @@ -0,0 +1,130 @@ +/* + * 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.examples.skills.app.ui + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import com.motorro.commonstatemachine.examples.auth.appcore.ui.design.SkillsAppBar +import com.motorro.commonstatemachine.examples.auth.appcore.ui.design.SkillsLoading +import com.motorro.commonstatemachine.examples.skills.app.MainViewModel +import com.motorro.commonstatemachine.examples.skills.app.R +import com.motorro.commonstatemachine.examples.skills.app.data.MainGesture +import com.motorro.commonstatemachine.examples.skills.app.data.MainUiState +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthGesture +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthUiApi +import com.motorro.commonstatemachine.flow.viewmodel.CommonFlowComposition +import io.github.aakira.napier.Napier +import org.koin.compose.koinInject +import org.koin.compose.viewmodel.koinViewModel + +@Composable +internal fun MainScreen(onTerminated: () -> Unit) { + val viewModel: MainViewModel = koinViewModel() + + CommonFlowComposition( + viewModel = viewModel, + navigationBackHandler = { enabled, onBack -> + BackHandler(enabled, onBack) + }, + finish = { + Napier.i { "Finished with: $it" } + onTerminated() + }, + content = { state, onGesture -> + when(state) { + is MainUiState.Splash -> SplashScreen() + is MainUiState.Auth -> { + val onAuthGesture = remember { + { child: AuthGesture -> onGesture(MainGesture.Auth(child)) } + } + AuthScreen(state = state, onGesture = onAuthGesture) + } + MainUiState.Friends -> ContentScreen() + } + } + ) +} + +@Composable +private fun AuthScreen( + state: MainUiState.Auth, + onGesture: (AuthGesture) -> Unit +) { + val uiApi: AuthUiApi = koinInject() + Scaffold( + topBar = { + SkillsAppBar( + title = stringResource(R.string.app_name), + topLevel = true + ) + }, + content = { paddingValues -> + uiApi.Screen( + state = state.child, + onGesture = onGesture, + modifier = Modifier.padding(paddingValues) + ) + } + ) +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +private fun SplashScreen() { + Scaffold( + topBar = { + SkillsAppBar( + title = stringResource(R.string.app_name), + topLevel = true + ) + }, + content = { paddingValues -> + SkillsLoading(Modifier.padding(paddingValues)) + } + ) +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +private fun ContentScreen() { + Scaffold( + topBar = { + SkillsAppBar( + title = stringResource(R.string.app_name), + topLevel = true + ) + }, + content = { paddingValues -> + Box(modifier = Modifier.padding(paddingValues).fillMaxSize(), contentAlignment = Alignment.Center) { + Text( + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + text = "Friends placeholder" + ) + } + } + ) +} diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/ui/MainUiRenderer.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/ui/MainUiRenderer.kt new file mode 100644 index 0000000..97c4bf4 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/app/ui/MainUiRenderer.kt @@ -0,0 +1,23 @@ +package com.motorro.commonstatemachine.examples.skills.app.ui + +import com.motorro.commonstatemachine.examples.skills.app.data.MainUiState +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthUiState +import org.koin.core.annotation.Factory + +/** + * Main UI renderer + */ +internal interface MainUiRenderer { + fun renderSplash(): MainUiState + fun renderAuth(child: AuthUiState): MainUiState + fun renderContent(): MainUiState + + @Factory + class Impl : MainUiRenderer { + override fun renderSplash() = MainUiState.Splash + + override fun renderAuth(child: AuthUiState) = MainUiState.Auth(child) + + override fun renderContent(): MainUiState = MainUiState.Friends + } +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/data/AuthStateDataTest.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/data/AuthStateDataTest.kt new file mode 100644 index 0000000..a27db2f --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/data/AuthStateDataTest.kt @@ -0,0 +1,51 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.data + +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +internal class AuthStateDataTest { + @Test + fun returnsTrueIfValid() { + val data = AuthStateData( + input = INPUT, + passwordRequirements = PASSWORD_REQUIREMENTS, + username = "user", + password = "password", + ) + assertTrue(data.isValidToAuthenticate()) + } + + @Test + fun returnsFalseIfUsernameEmpty() { + val data = AuthStateData( + input = INPUT, + passwordRequirements = PASSWORD_REQUIREMENTS, + username = "", + password = "password", + ) + assertFalse(data.isValidToAuthenticate()) + } + + @Test + fun returnsFalseIfUsernameBlank() { + val data = AuthStateData( + input = INPUT, + passwordRequirements = PASSWORD_REQUIREMENTS, + username = " ", + password = "password", + ) + assertFalse(data.isValidToAuthenticate()) + } + + @Test + fun returnsFalseIfPasswordInvalid() { + val data = AuthStateData( + input = INPUT, + passwordRequirements = PASSWORD_REQUIREMENTS, + username = "user", + password = "short", // PASSWORD_REQUIREMENTS regex is ^.{8,}$ + ) + assertFalse(data.isValidToAuthenticate()) + } +} diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/data/authGesturesFixtures.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/data/authGesturesFixtures.kt new file mode 100644 index 0000000..45c6ec5 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/data/authGesturesFixtures.kt @@ -0,0 +1,11 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.data + +/** + * Username input changed + */ +internal val USERNAME_CHANGED = AuthGestureImpl.Form.UsernameChanged("user") + +/** + * Password input changed + */ +internal val PASSWORD_CHANGED = AuthGestureImpl.Form.PasswordChanged("password") diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/data/dataStatesFixtures.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/data/dataStatesFixtures.kt new file mode 100644 index 0000000..7b04bbf --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/data/dataStatesFixtures.kt @@ -0,0 +1,45 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.data + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthInput +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.skills.domain.authenticate.data.PasswordRequirements +import com.motorro.commonstatemachine.skills.domain.exception.IOException +import com.motorro.commonstatemachine.skills.domain.exception.UnknownException + +/** + * Input fixture + */ +internal val INPUT = AuthInput(true) + +/** + * Result fixture + */ +internal val RESULT = AuthResult(authenticated = true) + +internal val PASSWORD_REQUIREMENTS = PasswordRequirements( + regex = "^.{8,}$".toRegex(), + description = "Minimum eight characters" +) + +/** + * Initial state + */ +internal val EMPTY_STATE = AuthStateData(INPUT, PASSWORD_REQUIREMENTS) + +/** + * Valid form state + */ +internal val VALID_FORM_STATE = EMPTY_STATE.copy( + username = "user", + password = "password" +) + +/** + * Fatal error fixture + */ +internal val FATAL_ERROR = UnknownException("Something went wrong") + +/** + * Non-fatal error fixture + */ +internal val NON_FATAL_ERROR = IOException("Internet is down") \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/data/uiStatesFixtures.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/data/uiStatesFixtures.kt new file mode 100644 index 0000000..14b7733 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/data/uiStatesFixtures.kt @@ -0,0 +1,6 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.data + +/** + * Some UI-state to return from [com.motorro.commonstatemachine.examples.skills.auth.implementation.ui.AuthUiRenderer] mock + */ +internal val UI_STATE = AuthUiStateImpl.Loading \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/state/AuthenticatingStateTest.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/state/AuthenticatingStateTest.kt new file mode 100644 index 0000000..5d5f135 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/state/AuthenticatingStateTest.kt @@ -0,0 +1,82 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.FATAL_ERROR +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.UI_STATE +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.VALID_FORM_STATE +import com.motorro.commonstatemachine.skills.domain.authenticate.AuthenticateWithPassword +import dev.mokkery.MockMode +import dev.mokkery.answering.calls +import dev.mokkery.answering.returns +import dev.mokkery.answering.throws +import dev.mokkery.every +import dev.mokkery.everySuspend +import dev.mokkery.matcher.any +import dev.mokkery.mock +import dev.mokkery.verify +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.test.Test + +internal class AuthenticatingStateTest : BaseStateTest() { + private lateinit var authenticateMock: AuthenticateWithPassword + private lateinit var state: AuthenticatingState + + override fun doInit() { + authenticateMock = mock(MockMode.strict) + state = AuthenticatingState(context, VALID_FORM_STATE, authenticateMock) + every { renderer.renderLoading() } returns UI_STATE + } + + @Test + fun rendersLoadingOnStart() = test { + everySuspend { authenticateMock(any(), any()) } calls { suspendCancellableCoroutine { } } + + state.start(stateMachine) + + verify { + renderer.renderLoading() + stateMachine.setUiState(UI_STATE) + } + } + + @Test + fun terminatesWhenAuthenticated() = test { + everySuspend { authenticateMock(any(), any()) } returns Unit + every { stateFactory.terminated(any()) } returns nextState + + state.start(stateMachine) + + verify { + stateFactory.terminated(AuthResult(authenticated = true)) + stateMachine.setMachineState(nextState) + } + } + + @Test + fun transfersToErrorWhenFailed() = test { + everySuspend { authenticateMock(any(), any()) } throws FATAL_ERROR + every { stateFactory.authenticationError(any(), any()) } returns nextState + + state.start(stateMachine) + + verify { + stateFactory.authenticationError(VALID_FORM_STATE, any()) + stateMachine.setMachineState(nextState) + } + } + + @Test + fun returnsToFormOnBackWhileAuthenticating() = test { + everySuspend { authenticateMock(any(), any()) } calls { suspendCancellableCoroutine { } } + every { stateFactory.form(any()) } returns nextState + + state.start(stateMachine) + state.process(AuthGestureImpl.Back) + + verify { + stateFactory.form(VALID_FORM_STATE) + stateMachine.setMachineState(nextState) + } + } +} diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/state/AuthenticationErrorStateTest.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/state/AuthenticationErrorStateTest.kt new file mode 100644 index 0000000..163d205 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/state/AuthenticationErrorStateTest.kt @@ -0,0 +1,87 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.FATAL_ERROR +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.NON_FATAL_ERROR +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.UI_STATE +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.VALID_FORM_STATE +import com.motorro.commonstatemachine.skills.domain.exception.AppException +import dev.mokkery.answering.returns +import dev.mokkery.every +import dev.mokkery.matcher.any +import dev.mokkery.verify +import kotlin.test.Test + +internal class AuthenticationErrorStateTest : BaseStateTest() { + override fun doInit() { + every { renderer.renderFullScreenError(any(), any()) } returns UI_STATE + } + + private fun createState(error: AppException) = AuthenticationErrorState( + context, + VALID_FORM_STATE, + error + ) + + @Test + fun rendersFatalErrorOnStart() = test { + val state = createState(FATAL_ERROR) + state.start(stateMachine) + verify { + renderer.renderFullScreenError(FATAL_ERROR, canRetry = false) + stateMachine.setUiState(UI_STATE) + } + } + + @Test + fun rendersNonFatalErrorOnStart() = test { + val state = createState(NON_FATAL_ERROR) + state.start(stateMachine) + verify { + renderer.renderFullScreenError(NON_FATAL_ERROR, canRetry = true) + stateMachine.setUiState(UI_STATE) + } + } + + @Test + fun returnsToFormOnBack() = test { + val state = createState(NON_FATAL_ERROR) + every { stateFactory.form(any()) } returns nextState + + state.start(stateMachine) + state.process(AuthGestureImpl.Back) + + verify { + stateFactory.form(VALID_FORM_STATE) + stateMachine.setMachineState(nextState) + } + } + + @Test + fun retriesOnActionIfRetryable() = test { + val state = createState(NON_FATAL_ERROR) + every { stateFactory.authenticating(any()) } returns nextState + + state.start(stateMachine) + state.process(AuthGestureImpl.Action) + + verify { + stateFactory.authenticating(VALID_FORM_STATE) + stateMachine.setMachineState(nextState) + } + } + + @Test + fun returnsToFormOnActionIfNotRetryable() = test { + val state = createState(FATAL_ERROR) + every { stateFactory.form(any()) } returns nextState + + state.start(stateMachine) + state.process(AuthGestureImpl.Action) + + verify { + stateFactory.form(VALID_FORM_STATE) + stateMachine.setMachineState(nextState) + } + } +} diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/state/BaseStateTest.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/state/BaseStateTest.kt new file mode 100644 index 0000000..1d4ca54 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/state/BaseStateTest.kt @@ -0,0 +1,63 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.CommonStateMachine +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.ui.AuthUiRenderer +import com.motorro.commonstatemachine.flow.data.CommonFlowHost +import dev.mokkery.MockMode +import dev.mokkery.mock +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 + +@OptIn(ExperimentalCoroutinesApi::class) +internal abstract class BaseStateTest { + protected lateinit var stateMachine: CommonStateMachine + protected lateinit var stateFactory: AuthStateFactory + protected lateinit var renderer: AuthUiRenderer + protected lateinit var context: AuthContext + protected lateinit var flowHost: CommonFlowHost + protected lateinit var nextState: AuthState + protected lateinit var dispatcher: TestDispatcher + + @BeforeTest + fun init() { + dispatcher = UnconfinedTestDispatcher() + Dispatchers.setMain(dispatcher) + + stateMachine = mock(mode = MockMode.autoUnit) + stateFactory = mock() + flowHost = mock(mode = MockMode.autoUnit) + renderer = mock(mode = MockMode.strict) + nextState = AuthState() + + context = object : AuthContext { + override val factory: AuthStateFactory = this@BaseStateTest.stateFactory + override val flowHost: CommonFlowHost = this@BaseStateTest.flowHost + override val renderer: AuthUiRenderer = this@BaseStateTest.renderer + } + + doInit() + } + + @AfterTest + fun deinit() { + Dispatchers.resetMain() + } + + protected fun test(block: TestScope.() -> Unit) = runTest( + dispatcher, + testBody = block + ) + + protected open fun doInit() = Unit +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/state/FormStateTest.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/state/FormStateTest.kt new file mode 100644 index 0000000..80c6e7a --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/state/FormStateTest.kt @@ -0,0 +1,125 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.EMPTY_STATE +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.PASSWORD_CHANGED +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.UI_STATE +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.USERNAME_CHANGED +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.VALID_FORM_STATE +import dev.mokkery.answering.returns +import dev.mokkery.every +import dev.mokkery.matcher.any +import dev.mokkery.verify +import dev.mokkery.verify.VerifyMode +import kotlin.test.Test +import kotlin.test.assertEquals + +internal class FormStateTest : BaseStateTest() { + private lateinit var state: FormState + + override fun doInit() { + state = FormState(context, EMPTY_STATE) + every { renderer.renderForm(any(), any()) } returns UI_STATE + } + + @Test + fun rendersFormOnStart() = test { + state.start(stateMachine) + verify { + renderer.renderForm(EMPTY_STATE, isValid = false) + stateMachine.setUiState(UI_STATE) + } + } + + @Test + fun processesUsernameChanged() = test { + state.start(stateMachine) + state.process(USERNAME_CHANGED) + + val expectedData = EMPTY_STATE.copy(username = USERNAME_CHANGED.value) + verify(mode = VerifyMode.order) { + renderer.renderForm(EMPTY_STATE, isValid = false) + renderer.renderForm(expectedData, isValid = false) + } + assertEquals(USERNAME_CHANGED.value, state.data.username) + } + + @Test + fun processesPasswordChanged() = test { + state.start(stateMachine) + state.process(PASSWORD_CHANGED) + + val expectedData = EMPTY_STATE.copy(password = PASSWORD_CHANGED.value) + verify(mode = VerifyMode.order) { + renderer.renderForm(EMPTY_STATE, isValid = false) + renderer.renderForm(expectedData, isValid = false) + } + assertEquals(PASSWORD_CHANGED.value, state.data.password) + } + + @Test + fun processesAction() = test { + every { stateFactory.authenticating(any()) } returns nextState + state.data = VALID_FORM_STATE + + state.start(stateMachine) + state.process(AuthGestureImpl.Action) + + verify { + renderer.renderForm(VALID_FORM_STATE, isValid = true) + stateFactory.authenticating(VALID_FORM_STATE) + stateMachine.setMachineState(nextState) + } + } + + @Test + fun doesNotProcessActionIfInvalid() = test { + state.start(stateMachine) + state.process(AuthGestureImpl.Action) + + verify(VerifyMode.exactly(0)) { + stateFactory.authenticating(any()) + stateMachine.setMachineState(any()) + } + } + + @Test + fun processesBack() = test { + every { stateFactory.terminated(any()) } returns nextState + + state.start(stateMachine) + state.process(AuthGestureImpl.Back) + + verify { + stateFactory.terminated(AuthResult(authenticated = false)) + stateMachine.setMachineState(nextState) + } + } + + @Test + fun processesSkip() = test { + every { stateFactory.terminated(any()) } returns nextState + + state.start(stateMachine) + state.process(AuthGestureImpl.Form.Skip) + + verify { + stateFactory.terminated(AuthResult(authenticated = false)) + stateMachine.setMachineState(nextState) + } + } + + @Test + fun doesNotProcessSkipIfNotSkippable() = test { + state.data = EMPTY_STATE.copy(input = EMPTY_STATE.input.copy(skippable = false)) + + state.start(stateMachine) + state.process(AuthGestureImpl.Form.Skip) + + verify(VerifyMode.exactly(0)) { + stateFactory.terminated(any()) + stateMachine.setMachineState(any()) + } + } +} diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/state/PreloadingErrorStateTest.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/state/PreloadingErrorStateTest.kt new file mode 100644 index 0000000..cc51dc9 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/state/PreloadingErrorStateTest.kt @@ -0,0 +1,91 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.FATAL_ERROR +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.INPUT +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.NON_FATAL_ERROR +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.UI_STATE +import com.motorro.commonstatemachine.skills.domain.exception.AppException +import dev.mokkery.answering.returns +import dev.mokkery.every +import dev.mokkery.matcher.any +import dev.mokkery.verify +import kotlin.test.Test + +internal class PreloadingErrorStateTest : BaseStateTest() { + override fun doInit() { + every { renderer.renderFullScreenError(any(), any()) } returns UI_STATE + } + + private fun createState(error: AppException) = PreloadingErrorState( + context, + INPUT, + error + ) + + @Test + fun rendersFatalErrorOnStart() = test { + val state = createState(FATAL_ERROR) + state.start(stateMachine) + verify { + renderer.renderFullScreenError(FATAL_ERROR, canRetry = false) + stateMachine.setUiState(UI_STATE) + } + } + + @Test + fun rendersNonFatalErrorOnStart() = test { + val state = createState(NON_FATAL_ERROR) + state.start(stateMachine) + verify { + renderer.renderFullScreenError(NON_FATAL_ERROR, canRetry = true) + stateMachine.setUiState(UI_STATE) + } + } + + @Test + fun terminatesOnBack() = test { + val state = createState(NON_FATAL_ERROR) + + every { stateFactory.terminated(any()) } returns nextState + + state.start(stateMachine) + state.process(AuthGestureImpl.Back) + + verify { + stateFactory.terminated(AuthResult(authenticated = false)) + stateMachine.setMachineState(nextState) + } + } + + @Test + fun terminatesOnActionIfCannotRetry() = test { + val state = createState(FATAL_ERROR) + + every { stateFactory.terminated(any()) } returns nextState + + state.start(stateMachine) + state.process(AuthGestureImpl.Action) + + verify { + stateFactory.terminated(AuthResult(authenticated = false)) + stateMachine.setMachineState(nextState) + } + } + + @Test + fun reloadsOnActionIfCanRetry() = test { + val state = createState(NON_FATAL_ERROR) + + every { stateFactory.preloading(any()) } returns nextState + + state.start(stateMachine) + state.process(AuthGestureImpl.Action) + + verify { + stateFactory.preloading(INPUT) + stateMachine.setMachineState(nextState) + } + } +} diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/state/PreloadingStateTest.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/state/PreloadingStateTest.kt new file mode 100644 index 0000000..3017c63 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/state/PreloadingStateTest.kt @@ -0,0 +1,82 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.FATAL_ERROR +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.INPUT +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.PASSWORD_REQUIREMENTS +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.UI_STATE +import com.motorro.commonstatemachine.skills.domain.authenticate.GetPasswordRequirements +import dev.mokkery.answering.calls +import dev.mokkery.answering.returns +import dev.mokkery.answering.throws +import dev.mokkery.every +import dev.mokkery.everySuspend +import dev.mokkery.matcher.any +import dev.mokkery.mock +import dev.mokkery.verify +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.test.Test + +internal class PreloadingStateTest : BaseStateTest() { + private lateinit var requirementsMock: GetPasswordRequirements + private lateinit var state: PreloadingState + + override fun doInit() { + requirementsMock = mock() + state = PreloadingState(context, INPUT, requirementsMock) + every { renderer.renderLoading() } returns UI_STATE + } + + @Test + fun rendersLoadingOnStart() = test { + everySuspend { requirementsMock() } calls { suspendCancellableCoroutine { } } + + state.start(stateMachine) + + verify { + renderer.renderLoading() + stateMachine.setUiState(UI_STATE) + } + } + + @Test + fun transfersToFormWhenLoaded() = test { + everySuspend { requirementsMock() } returns PASSWORD_REQUIREMENTS + every { stateFactory.form(any()) } returns nextState + + state.start(stateMachine) + + verify { + stateFactory.form(any()) + stateMachine.setMachineState(nextState) + } + } + + @Test + fun transfersToErrorWhenFailed() = test { + everySuspend { requirementsMock() } throws FATAL_ERROR + every { stateFactory.preloadError(any(), any()) } returns nextState + + state.start(stateMachine) + + verify { + stateFactory.preloadError(INPUT, any()) + stateMachine.setMachineState(nextState) + } + } + + @Test + fun terminatesOnBackWhileLoading() = test { + everySuspend { requirementsMock() } calls { suspendCancellableCoroutine { } } + every { stateFactory.terminated(any()) } returns nextState + + state.start(stateMachine) + state.process(AuthGestureImpl.Back) + + verify { + stateFactory.terminated(AuthResult(authenticated = false)) + stateMachine.setMachineState(nextState) + } + } +} diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/ui/AuthUiRendererImplTest.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/ui/AuthUiRendererImplTest.kt new file mode 100644 index 0000000..7e59579 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests/ui/AuthUiRendererImplTest.kt @@ -0,0 +1,63 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.ui + +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.EMPTY_STATE +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.FATAL_ERROR +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.VALID_FORM_STATE +import kotlin.test.Test +import kotlin.test.assertEquals + +internal class AuthUiRendererImplTest { + private val renderer = AuthUiRendererImpl() + + @Test + fun rendersLoading() { + assertEquals(AuthUiStateImpl.Loading, renderer.renderLoading()) + } + + @Test + fun rendersForm() { + val uiState = renderer.renderForm(VALID_FORM_STATE, isValid = true) + val expected = AuthUiStateImpl.Form( + username = VALID_FORM_STATE.username, + password = VALID_FORM_STATE.password, + passwordRequirements = VALID_FORM_STATE.passwordRequirements.description, + loginEnabled = true, + canSkip = true + ) + assertEquals(expected, uiState) + } + + @Test + fun rendersFormInvalid() { + val uiState = renderer.renderForm(EMPTY_STATE, isValid = false) + val expected = AuthUiStateImpl.Form( + username = EMPTY_STATE.username, + password = EMPTY_STATE.password, + passwordRequirements = EMPTY_STATE.passwordRequirements.description, + loginEnabled = false, + canSkip = true + ) + assertEquals(expected, uiState) + } + + @Test + fun rendersFullScreenError() { + val uiState = renderer.renderFullScreenError(FATAL_ERROR, canRetry = false) + val expected = AuthUiStateImpl.Error( + message = FATAL_ERROR.message, + canRetry = false + ) + assertEquals(expected, uiState) + } + + @Test + fun rendersFullScreenErrorWithRetry() { + val uiState = renderer.renderFullScreenError(FATAL_ERROR, canRetry = true) + val expected = AuthUiStateImpl.Error( + message = FATAL_ERROR.message, + canRetry = true + ) + assertEquals(expected, uiState) + } +} diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/AuthDataApiImpl.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/AuthDataApiImpl.kt new file mode 100644 index 0000000..aa90a5e --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/AuthDataApiImpl.kt @@ -0,0 +1,35 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation + +import com.motorro.commonstatemachine.CommonMachineState +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthDataApi +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthGesture +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthInput +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthUiState +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.state.AuthStateFactory +import com.motorro.commonstatemachine.flow.data.CommonFlowHost +import org.koin.core.annotation.Factory +import org.koin.core.component.KoinComponent +import org.koin.core.component.get +import org.koin.core.parameter.parametersOf + +/** + * Data API implementation + */ +@Factory +internal class AuthDataApiImpl : AuthDataApi, KoinComponent { + + @Suppress("UNCHECKED_CAST") + override fun init(flowHost: CommonFlowHost, input: AuthInput): CommonMachineState { + val factory = this@AuthDataApiImpl.get { + parametersOf(flowHost) + } + return factory.preloading(input) as CommonMachineState + } + + override fun getDefaultUiState(): AuthUiState = AuthUiStateImpl.Loading + + override fun getBackGesture(): AuthGesture = AuthGestureImpl.Back +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/AuthModule.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/AuthModule.kt new file mode 100644 index 0000000..b646f31 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/AuthModule.kt @@ -0,0 +1,8 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +class AuthModule \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/AuthUiApiImpl.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/AuthUiApiImpl.kt new file mode 100644 index 0000000..a0ccad4 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/AuthUiApiImpl.kt @@ -0,0 +1,27 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthGesture +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthUiApi +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthUiState +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.ui.AuthScreen +import org.koin.core.annotation.Factory + +/** + * UI API implementation + */ +@Factory +internal class AuthUiApiImpl : AuthUiApi { + @Composable + override fun Screen( + state: AuthUiState, + onGesture: (AuthGesture) -> Unit, + modifier: Modifier + ) = AuthScreen( + state = state as AuthUiStateImpl, + modifier = modifier, + onGesture = onGesture + ) +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/data/AuthGestureImpl.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/data/AuthGestureImpl.kt new file mode 100644 index 0000000..ba1df8e --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/data/AuthGestureImpl.kt @@ -0,0 +1,38 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.data + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthGesture + +/** + * All gestures available in this feature + */ +internal sealed class AuthGestureImpl : AuthGesture { + /** + * Backwards navigation element clicked + */ + data object Back : AuthGestureImpl() + + /** + * Default call-to-action clicked + */ + data object Action : AuthGestureImpl() + + /** + * Gestures for the login form + */ + sealed class Form : AuthGestureImpl() { + /** + * Username input changed + */ + data class UsernameChanged(val value: String) : Form() + + /** + * Password input changed + */ + data class PasswordChanged(val value: String) : Form() + + /** + * Skips the authentication + */ + data object Skip : Form() + } +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/data/AuthStateData.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/data/AuthStateData.kt new file mode 100644 index 0000000..d8cf559 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/data/AuthStateData.kt @@ -0,0 +1,23 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.data + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthInput +import com.motorro.commonstatemachine.skills.domain.authenticate.data.PasswordRequirements + +/** + * Interstate data to pass between the machine states + * @property input Flow input from the flow initialization + * @property passwordRequirements Password requirements (preloaded later) + * @property username User input for the username is stored here + * @property password User input for the password is stored here + */ +internal data class AuthStateData( + val input: AuthInput, + val passwordRequirements: PasswordRequirements, + val username: String = "", + val password: String = "" +) + +/** + * Some utility function to check the form state + */ +internal fun AuthStateData.isValidToAuthenticate() = username.isNotBlank() && passwordRequirements.regex.matches(password) diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/data/AuthUiStateImpl.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/data/AuthUiStateImpl.kt new file mode 100644 index 0000000..5476939 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/data/AuthUiStateImpl.kt @@ -0,0 +1,36 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.data + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthUiState + +/** + * All gestures available in this feature + */ +internal sealed class AuthUiStateImpl : AuthUiState { + /** + * Loading data + */ + data object Loading : AuthUiStateImpl() + + /** + * Login form + * @property username Username + * @property password Password + * @property passwordRequirements Password requirements description + * @property loginEnabled If true, the form is complete and the user can log in + * @property canSkip If true, the user can skip the authentication + */ + data class Form( + val username: String, + val password: String, + val passwordRequirements: String, + val loginEnabled: Boolean, + val canSkip: Boolean + ) : AuthUiStateImpl() + + /** + * Displays error + * @property message Error message to display + * @property canRetry If true, the error is recoverable and can be retried + */ + data class Error(val message: String, val canRetry: Boolean) : AuthUiStateImpl() +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/AuthContext.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/AuthContext.kt new file mode 100644 index 0000000..21bc639 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/AuthContext.kt @@ -0,0 +1,25 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.ui.AuthUiRenderer +import com.motorro.commonstatemachine.flow.data.CommonFlowHost + +/** + * Common tools used by states + */ +internal interface AuthContext { + /** + * State-factory + */ + val factory: AuthStateFactory + + /** + * Flow-host + */ + val flowHost: CommonFlowHost + + /** + * UI-renderer + */ + val renderer: AuthUiRenderer +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/AuthState.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/AuthState.kt new file mode 100644 index 0000000..bcf46b6 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/AuthState.kt @@ -0,0 +1,10 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.CommonMachineState +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl + +/** + * Convenient type-alias - bounds gesture and ui state system + */ +internal typealias AuthState = CommonMachineState \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/AuthStateFactory.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/AuthStateFactory.kt new file mode 100644 index 0000000..5013652 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/AuthStateFactory.kt @@ -0,0 +1,54 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthInput +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthStateData +import com.motorro.commonstatemachine.skills.domain.exception.AppException + +/** + * State-factory interface + * Abstracts the machine state from the state creation + */ +internal interface AuthStateFactory { + /** + * Creates a flow preloading state. + * The interstate data will be created by the state. + * @param input Initialization params + */ + fun preloading(input: AuthInput): AuthState + + /** + * Creates a preload error state. + * Again, there's no interstate data here as the preloading failed. + * @param input Initialization params + * @param error Error that occurred during the initialization + */ + fun preloadError(input: AuthInput, error: AppException): AuthState + + /** + * Creates authentication form state. + * Now we have the interstate data, pass it as a parameter. + * @param data Interstate data + */ + fun form(data: AuthStateData): AuthState + + /** + * Creates the authentication state. + * @param data Interstate data + */ + fun authenticating(data: AuthStateData): AuthState + + /** + * Creates an authentication error state. + * @param data Interstate data + * @param error Error that occurred during the initialization + */ + fun authenticationError(data: AuthStateData, error: AppException): AuthState + + /** + * Creates the termination state that finishes the flow. + * All interstate data is lost as not needed anymore. + * @param result Flow result + */ + fun terminated(result: AuthResult): AuthState +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/AuthStateFactoryImpl.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/AuthStateFactoryImpl.kt new file mode 100644 index 0000000..35be8ff --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/AuthStateFactoryImpl.kt @@ -0,0 +1,84 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthInput +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthStateData +import com.motorro.commonstatemachine.examples.skills.auth.implementation.ui.AuthUiRenderer +import com.motorro.commonstatemachine.flow.data.CommonFlowHost +import com.motorro.commonstatemachine.skills.domain.exception.AppException +import io.github.aakira.napier.Napier +import org.koin.core.annotation.Factory +import org.koin.core.annotation.InjectedParam + +/** + * Main state-factory implementation + */ +@Factory +internal class AuthStateFactoryImpl( + private val preloadingStateFactory: PreloadingState.StateFactory, + private val authenticatingStateFactory: AuthenticatingState.StateFactory, + @InjectedParam flowHost: CommonFlowHost, + renderer: AuthUiRenderer +) : AuthStateFactory { + + /** + * Context implementation + */ + private val context = object : AuthContext { + override val factory: AuthStateFactory = this@AuthStateFactoryImpl + override val flowHost: CommonFlowHost = flowHost + override val renderer: AuthUiRenderer = renderer + } + + /** + * Creates preloading state with help of a preloading state factory + */ + override fun preloading(input: AuthInput) = preloadingStateFactory.create( + context, + input + ) + + /** + * Creates preload error state directly + */ + override fun preloadError(input: AuthInput, error: AppException) = PreloadingErrorState( + context, + input, + error + ) + + /** + * Creates authentication form state + */ + override fun form(data: AuthStateData) = FormState( + context, + data + ) + + /** + * Creates authenticating state + */ + override fun authenticating(data: AuthStateData): AuthState = authenticatingStateFactory.create( + context, + data + ) + + /** + * Creates authentication error state + */ + override fun authenticationError(data: AuthStateData, error: AppException) = AuthenticationErrorState( + context, + data, + error + ) + + /** + * Ad-hoc terminated state implementation + */ + override fun terminated(result: AuthResult) = object : AuthState() { + override fun doStart() { + Napier.d { "Terminating flow..." } + context.flowHost.onComplete(result) + } + } +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/AuthenticatingState.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/AuthenticatingState.kt new file mode 100644 index 0000000..f337087 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/AuthenticatingState.kt @@ -0,0 +1,81 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthStateData +import com.motorro.commonstatemachine.skills.domain.authenticate.AuthenticateWithPassword +import com.motorro.commonstatemachine.skills.domain.exception.toAppException +import com.motorro.commonstatemachine.skills.domain.session.data.Username +import io.github.aakira.napier.Napier +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch +import org.koin.core.annotation.Factory + +/** + * Authenticates the user + * @param context Auth context + * @param data Interstate data + * @param authenticate Authenticate usecase + */ +internal class AuthenticatingState( + context: AuthContext, + private val data: AuthStateData, + private val authenticate: AuthenticateWithPassword +) : BaseAuthState(context) { + + /** + * State start-up + */ + override fun doStart() { + setUiState { + renderLoading() + } + doAuthenticate() + } + + /** + * Gesture processing + */ + override fun doProcess(gesture: AuthGestureImpl) { + when(gesture) { + is AuthGestureImpl.Back -> setMachineState { + Napier.d { "Back gesture. Returning to form..." } + form(data) + } + else -> super.doProcess(gesture) + } + } + + /** + * Authentication function + */ + private fun doAuthenticate() = stateScope.launch { + try { + authenticate(Username(data.username), data.password) + // Advance to the next state + setMachineState { + terminated(AuthResult(authenticated = true)) + } + } catch (e: Throwable) { + ensureActive() + Napier.w(e) { "Error during authentication" } + // Advance to the error state + setMachineState { + authenticationError(data, e.toAppException()) + } + } + } + + /** + * State factory + * State dependencies are injected here + */ + @Factory + class StateFactory(private val authenticate: AuthenticateWithPassword) { + fun create(context: AuthContext, data: AuthStateData): AuthState = AuthenticatingState( + context = context, + data = data, + authenticate = authenticate + ) + } +} diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/AuthenticationErrorState.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/AuthenticationErrorState.kt new file mode 100644 index 0000000..772a972 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/AuthenticationErrorState.kt @@ -0,0 +1,53 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthStateData +import com.motorro.commonstatemachine.skills.domain.exception.AppException +import com.motorro.commonstatemachine.skills.domain.exception.IOException +import io.github.aakira.napier.Napier + +/** + * Handles authentication error + * If the error is recoverable (IOException), allows to transfer back to authenticating to retry. + * Otherwise returns to form. + * @param context Auth context + * @param data Interstate data + * @param error Error to handle + */ +internal class AuthenticationErrorState( + context: AuthContext, + private val data: AuthStateData, + private val error: AppException +) : BaseAuthState(context) { + override fun doStart() { + setUiState { + renderFullScreenError(error, canRetry()) + } + } + + private fun canRetry(): Boolean = when(error) { + is IOException -> true + else -> false + } + + override fun doProcess(gesture: AuthGestureImpl) { + when(gesture) { + is AuthGestureImpl.Back -> setMachineState { + Napier.d { "Back gesture. Returning to form..." } + form(data) + } + is AuthGestureImpl.Action -> if (canRetry()) { + Napier.d { "Action gesture. Retrying authentication..." } + setMachineState { + authenticating(data) + } + } else { + Napier.d { "Action gesture. Returning to form..." } + setMachineState { + form(data) + } + } + else -> super.doProcess(gesture) + } + } +} diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/BaseAuthState.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/BaseAuthState.kt new file mode 100644 index 0000000..267cf74 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/BaseAuthState.kt @@ -0,0 +1,34 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.coroutines.CoroutineState +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.ui.AuthUiRenderer +import io.github.aakira.napier.Napier + +/** + * Base class for the most logic states + */ +internal abstract class BaseAuthState(context: AuthContext) : CoroutineState(), AuthContext by context { + + /** + * Common gesture processing (logs a warning for an unexpected gesture) + */ + override fun doProcess(gesture: AuthGestureImpl) { + Napier.w { "Unsupported gesture: $gesture" } + } + + /** + * Convenience function to switch a machine state + */ + protected inline fun setMachineState(block: AuthStateFactory.() -> AuthState) { + setMachineState(block(factory)) + } + + /** + * Convenience function to render UI state + */ + protected inline fun setUiState(block: AuthUiRenderer.() -> AuthUiStateImpl) { + setUiState(renderer.block()) + } +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/FormState.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/FormState.kt new file mode 100644 index 0000000..c52effb --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/FormState.kt @@ -0,0 +1,75 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthStateData +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.isValidToAuthenticate +import io.github.aakira.napier.Napier +import org.jetbrains.annotations.VisibleForTesting + +/** + * Authentication form state - an interactive state that: + * - handles user input + * - updates the UI state + * - validates the input + * - updates the interstate data + * @param context Auth context + * @param data Interstate data. Visible for testing. + */ +internal class FormState( + context: AuthContext, + @get:VisibleForTesting var data: AuthStateData, +) : BaseAuthState(context) { + + /** + * State start-up + */ + override fun doStart() { + setUiState { + renderForm(data, data.isValidToAuthenticate()) + } + } + + /** + * Updates the interstate data and renders the form + */ + private inline fun updateData(block: AuthStateData.() -> AuthStateData) { + val oldData = data + val newData = data.block() + if (oldData != newData) { + data = newData + setUiState { + renderForm(newData, newData.isValidToAuthenticate()) + } + } + } + + /** + * Processes the gesture + */ + override fun doProcess(gesture: AuthGestureImpl) { + when(gesture) { + is AuthGestureImpl.Back -> setMachineState { + terminated(AuthResult(false)) + } + is AuthGestureImpl.Action -> if (data.isValidToAuthenticate()) { + Napier.d { "Action gesture. Authenticating..." } + setMachineState { + authenticating(data) + } + } + is AuthGestureImpl.Form.UsernameChanged -> updateData { + copy(username = gesture.value) + } + is AuthGestureImpl.Form.PasswordChanged -> updateData { + copy(password = gesture.value) + } + is AuthGestureImpl.Form.Skip -> if (data.input.skippable) { + Napier.d { "Skipping authentication..." } + setMachineState { + terminated(AuthResult(false)) + } + } + } + } +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/PreloadingErrorState.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/PreloadingErrorState.kt new file mode 100644 index 0000000..61db5fc --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/PreloadingErrorState.kt @@ -0,0 +1,52 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthInput +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.skills.domain.exception.AppException +import com.motorro.commonstatemachine.skills.domain.exception.IOException +import io.github.aakira.napier.Napier + +/** + * Handles preloading error + * If the error is recoverable, allows to transfer back to loading to retry, otherwise terminates the flow. + * @param context Auth context + * @param error Error to handle + */ +internal class PreloadingErrorState( + context: AuthContext, + private val input: AuthInput, + private val error: AppException +) : BaseAuthState(context) { + override fun doStart() { + setUiState { + renderFullScreenError(error, canRetry()) + } + } + + private fun canRetry(): Boolean = when(error) { + is IOException -> true + else -> false + } + + override fun doProcess(gesture: AuthGestureImpl) { + when(gesture) { + is AuthGestureImpl.Back -> setMachineState { + Napier.d { "Back gesture. Terminating flow..." } + terminated(AuthResult(false)) + } + is AuthGestureImpl.Action -> if (canRetry()) { + Napier.d { "Action gesture. Retrying initialization..." } + setMachineState { + preloading(input) + } + } else { + Napier.d { "Action gesture. Terminating flow..." } + setMachineState { + terminated(AuthResult(false)) + } + } + else -> super.doProcess(gesture) + } + } +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/PreloadingState.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/PreloadingState.kt new file mode 100644 index 0000000..63dd138 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/state/PreloadingState.kt @@ -0,0 +1,78 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthInput +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthStateData +import com.motorro.commonstatemachine.skills.domain.authenticate.GetPasswordRequirements +import com.motorro.commonstatemachine.skills.domain.exception.toAppException +import io.github.aakira.napier.Napier +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch +import org.koin.core.annotation.Factory + +/** + * Example of a state that preloads some data with the usecase + * and creates the valid [data-state](data-and-ui-implementation.md#step-3-interstate-data). + */ +internal class PreloadingState( + context: AuthContext, + private val input: AuthInput, + private val getPasswordRequirements: GetPasswordRequirements +) : BaseAuthState(context) { + + /** + * State start-up + */ + override fun doStart() { + setUiState { + renderLoading() + } + preload() + } + + /** + * Dummy gesture processing + */ + override fun doProcess(gesture: AuthGestureImpl) { + when(gesture) { + is AuthGestureImpl.Back -> setMachineState { + terminated(AuthResult(false)) + } + else -> super.doProcess(gesture) + } + } + + /** + * Dummy preloading function + */ + private fun preload() = stateScope.launch { + try { + val passwordRequirements = getPasswordRequirements() + // Advance to the next state + setMachineState { + form(AuthStateData(input, passwordRequirements)) + } + } catch (e: Throwable) { + ensureActive() + Napier.w(e) { "Error during initialization" } + // Advance to the error state + setMachineState { + preloadError(input, e.toAppException()) + } + } + } + + /** + * State factory + * State dependencies are injected here + */ + @Factory + class StateFactory(private val getPasswordRequirements: GetPasswordRequirements) { + fun create(context: AuthContext, input: AuthInput): AuthState = PreloadingState( + context = context, + input = input, + getPasswordRequirements = getPasswordRequirements + ) + } +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/ui/AuthErrorScreen.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/ui/AuthErrorScreen.kt new file mode 100644 index 0000000..fd8c8a1 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/ui/AuthErrorScreen.kt @@ -0,0 +1,79 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.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.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.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.motorro.commonstatemachine.examples.auth.appcore.ui.preview.SkillsPreviewComposition +import com.motorro.commonstatemachine.examples.auth.appcore.ui.theme.Dimensions +import com.motorro.commonstatemachine.examples.skills.auth.implementation.Res +import com.motorro.commonstatemachine.examples.skills.auth.implementation.btn_close +import com.motorro.commonstatemachine.examples.skills.auth.implementation.btn_retry +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl +import org.jetbrains.compose.resources.stringResource + +@Composable +internal fun AuthErrorScreen( + state: AuthUiStateImpl.Error, + modifier: Modifier = Modifier, + onGesture: (AuthGestureImpl) -> Unit +) { + Column( + modifier = modifier.fillMaxSize().padding(Dimensions.medium), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = state.message, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodyLarge + ) + Spacer(Modifier.height(Dimensions.medium)) + Button(onClick = { onGesture(AuthGestureImpl.Action) }) { + Text( + if (state.canRetry) { + stringResource(Res.string.btn_retry) + } else { + stringResource(Res.string.btn_close) + } + ) + } + } +} + +private class AuthErrorDataProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + AuthUiStateImpl.Error( + message = "A recoverable error occurred", + canRetry = true + ), + AuthUiStateImpl.Error( + message = "A non-recoverable error occurred", + canRetry = false + ) + ) +} + +@Preview +@Composable +private fun AuthErrorPreview(@PreviewParameter(AuthErrorDataProvider::class) state: AuthUiStateImpl.Error?) { + if (null == state) return + SkillsPreviewComposition { + AuthErrorScreen( + state = state, + modifier = Modifier.fillMaxSize() + ) {} + } +} diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/ui/AuthFormScreen.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/ui/AuthFormScreen.kt new file mode 100644 index 0000000..715a6e5 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/ui/AuthFormScreen.kt @@ -0,0 +1,122 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.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.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +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.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.motorro.commonstatemachine.examples.auth.appcore.ui.preview.SkillsPreviewComposition +import com.motorro.commonstatemachine.examples.auth.appcore.ui.theme.Dimensions +import com.motorro.commonstatemachine.examples.skills.auth.implementation.Res +import com.motorro.commonstatemachine.examples.skills.auth.implementation.btn_login +import com.motorro.commonstatemachine.examples.skills.auth.implementation.btn_skip +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.input_password +import com.motorro.commonstatemachine.examples.skills.auth.implementation.input_username +import org.jetbrains.compose.resources.stringResource + +@Composable +internal fun AuthFormScreen( + state: AuthUiStateImpl.Form, + modifier: Modifier = Modifier, + onGesture: (AuthGestureImpl) -> Unit, +) { + Column( + modifier = modifier.fillMaxSize().padding(Dimensions.medium), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + TextField( + value = state.username, + onValueChange = { onGesture(AuthGestureImpl.Form.UsernameChanged(it)) }, + label = { Text(stringResource(Res.string.input_username)) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true + ) + Spacer(Modifier.height(Dimensions.medium)) + TextField( + value = state.password, + onValueChange = { onGesture(AuthGestureImpl.Form.PasswordChanged(it)) }, + label = { Text(stringResource(Res.string.input_password)) }, + modifier = Modifier.fillMaxWidth(), + visualTransformation = PasswordVisualTransformation(), + singleLine = true + ) + Text( + text = state.passwordRequirements, + modifier = Modifier.fillMaxWidth().padding(top = Dimensions.small), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(Modifier.height(Dimensions.medium)) + Button( + onClick = { onGesture(AuthGestureImpl.Action) }, + enabled = state.loginEnabled, + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(Res.string.btn_login)) + } + if (state.canSkip) { + Spacer(Modifier.height(Dimensions.medium)) + OutlinedButton( + onClick = { onGesture(AuthGestureImpl.Form.Skip) }, + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(Res.string.btn_skip)) + } + } + } +} + +private class AuthFormDataProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + AuthUiStateImpl.Form( + username = "", + password = "", + passwordRequirements = "At least 8 characters", + loginEnabled = false, + canSkip = true + ), + AuthUiStateImpl.Form( + username = "user", + password = "password", + passwordRequirements = "At least 8 characters", + loginEnabled = true, + canSkip = true + ), + AuthUiStateImpl.Form( + username = "user", + password = "password", + passwordRequirements = "At least 8 characters", + loginEnabled = true, + canSkip = false + ) + ) +} + +@Preview +@Composable +private fun AuthFormPreview(@PreviewParameter(AuthFormDataProvider::class) state: AuthUiStateImpl.Form?) { + if (null == state) return + SkillsPreviewComposition { + AuthFormScreen( + state = state, + modifier = Modifier.fillMaxSize() + ) {} + } +} diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/ui/AuthLoadingScreen.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/ui/AuthLoadingScreen.kt new file mode 100644 index 0000000..90875c9 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/ui/AuthLoadingScreen.kt @@ -0,0 +1,21 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.ui + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.motorro.commonstatemachine.examples.auth.appcore.ui.design.SkillsLoading +import com.motorro.commonstatemachine.examples.auth.appcore.ui.preview.SkillsPreviewComposition + +@Composable +internal fun AuthLoadingScreen(modifier: Modifier = Modifier) { + SkillsLoading(modifier) +} + +@Preview +@Composable +private fun AuthLoadingPreview() { + SkillsPreviewComposition { + AuthLoadingScreen(Modifier.fillMaxSize()) + } +} diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/ui/AuthScreen.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/ui/AuthScreen.kt new file mode 100644 index 0000000..f2fbfd9 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/ui/AuthScreen.kt @@ -0,0 +1,68 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.ui + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.motorro.commonstatemachine.examples.auth.appcore.ui.preview.SkillsPreviewComposition +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl + +/** + * Authentication screen + * @param state UI state + * @param onGesture Gesture handler + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun AuthScreen( + state: AuthUiStateImpl, + modifier: Modifier = Modifier, + onGesture: (AuthGestureImpl) -> Unit, +) { + val modifier = modifier.fillMaxSize() + when(state) { + AuthUiStateImpl.Loading -> AuthLoadingScreen(modifier) + is AuthUiStateImpl.Error -> AuthErrorScreen( + state = state, + modifier = modifier, + onGesture = onGesture + ) + is AuthUiStateImpl.Form -> AuthFormScreen( + state = state, + modifier = modifier, + onGesture = onGesture + ) + } +} + +private class AuthScreenDataProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + AuthUiStateImpl.Loading, + AuthUiStateImpl.Form( + username = "user", + password = "password", + passwordRequirements = "At least 8 characters", + loginEnabled = true, + canSkip = true + ), + AuthUiStateImpl.Error( + message = "A recoverable error occurred", + canRetry = true + ) + ) +} + +@Preview +@Composable +private fun SkillsScreenPreview(@PreviewParameter(AuthScreenDataProvider::class) state: AuthUiStateImpl?) { + if (null == state) return + SkillsPreviewComposition { + AuthScreen( + state = state + ) {} + } +} diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/ui/AuthUiRenderer.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/ui/AuthUiRenderer.kt new file mode 100644 index 0000000..f117aba --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/ui/AuthUiRenderer.kt @@ -0,0 +1,33 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.ui + +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthStateData +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.commonstatemachine.skills.domain.exception.AppException + +/** + * Transforms the data state to the UI state + */ +internal interface AuthUiRenderer { + /** + * Renders loading. + * The result would be an object so we don't need interstate data here. + */ + fun renderLoading(): AuthUiStateImpl + + /** + * Renders login form. + * The data could be fully taken from the data state. + * @param data Data-state to transform + * @param isValid If the form is valid or not + */ + fun renderForm(data: AuthStateData, isValid: Boolean): AuthUiStateImpl + + /** + * Renders error that takes the full screen + * - doesn't require any data-state + * - requires an error and takes a message from it + * @param error Error to present to the user + * @param canRetry Error could be retried + */ + fun renderFullScreenError(error: AppException, canRetry: Boolean): AuthUiStateImpl +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/ui/AuthUiRendererImpl.kt b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/ui/AuthUiRendererImpl.kt new file mode 100644 index 0000000..8f4e0b7 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/ui/AuthUiRendererImpl.kt @@ -0,0 +1,28 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.ui + +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthStateData +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.commonstatemachine.skills.domain.exception.AppException +import org.koin.core.annotation.Factory + +/** + * UI renderer implementation + */ +@Factory +internal class AuthUiRendererImpl : AuthUiRenderer { + + override fun renderLoading(): AuthUiStateImpl = AuthUiStateImpl.Loading + + override fun renderForm(data: AuthStateData, isValid: Boolean) = AuthUiStateImpl.Form( + username = data.username, + password = data.password, + passwordRequirements = data.passwordRequirements.description, + loginEnabled = isValid, + canSkip = data.input.skippable + ) + + override fun renderFullScreenError(error: AppException, canRetry: Boolean) = AuthUiStateImpl.Error( + message = error.message, + canRetry = canRetry + ) +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/ARCHITECTURE.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/ARCHITECTURE.md deleted file mode 120000 index f1d6e62..0000000 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/ARCHITECTURE.md +++ /dev/null @@ -1 +0,0 @@ -../../../.shared/ARCHITECTURE.md \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/ARCHITECTURE.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/ARCHITECTURE.md new file mode 100644 index 0000000..3b5362d --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/ARCHITECTURE.md @@ -0,0 +1,32 @@ +# CommonStateMachine Architecture + +![Architecture Diagram](architecture.puml) + +## Core Mechanics +- **Machine State:** A self-contained logic unit. It handles **Gestures (G)** and produces **UI States (U)**. +- **Lifecycle:** + - `doStart()`: Called when state becomes active. Use for initialization or launching side effects. + - `doProcess(gesture)`: Processes user input. + - `doClear()`: Cleanup when leaving the state. +- **Transitions:** States call `setMachineState(nextState)` to move the machine forward. No external "router" is needed. +- **UI Updates:** States call `setUiState(newState)` to update the View. +- **State Machine:** The container for the current **Machine State**. + - It maintains the state transition logic. + - `FlowStateMachine` (standard implementation) exports the current **UI State** as a `StateFlow`. + - It provides the `process(gesture)` entry point for user interaction. +- **Navigation Inversion:** Navigation is a logical state transition within the state machine (Logic/ViewModel layer). The View is passive and follows the logic. +- **ViewModel Integration:** The `ViewModel` (or equivalent lifecycle host) typically: + - Holds the `StateMachine` instance. + - Delegates user gestures to `stateMachine.process(gesture)`. + - Collects and exposes `stateMachine.uiState`. + - Calls `stateMachine.clear()` in its `onCleared()` lifecycle method to clean up resources. + +## Extra Components-Helpers +- **State Factory:** Decouples state creation from the states themselves. This allows for easy dependency injection and makes unit testing transitions simple + by mocking the factory. +- **UI Renderer:** A pure function or class that transforms the internal **Data State** into a **UI State**. This separates the business logic of "what to do" + from the presentation logic of "what to show." +- **State Context:** A common interface passed to all states in a machine. It provides access to shared tools like the State Factory, UI Renderer, + and Feature Hosts, reducing constructor boilerplate. +- **Data State:** A simple data class that holds the cumulative state of the flow (e.g., entered username, password, validation errors). It is passed between + states to maintain continuity. diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/COMMON-CHILD-FLOW.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/COMMON-CHILD-FLOW.md deleted file mode 120000 index c83c444..0000000 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/COMMON-CHILD-FLOW.md +++ /dev/null @@ -1 +0,0 @@ -../../.shared/COMMON-CHILD-FLOW.md \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/COMMON-CHILD-FLOW.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/COMMON-CHILD-FLOW.md new file mode 100644 index 0000000..e727d51 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/COMMON-CHILD-FLOW.md @@ -0,0 +1,39 @@ +# Common Child Flow + +![Common Child Flow Diagram](common-child-flow.puml) + +## Philosophy and Principles +- **Modularity via Proxying:** Features are independent modules. A parent flow hosts a child flow using `ProxyMachineState`, which bridges incompatible Gesture/UI systems. +- **Direct Data Flow:** Transitions occur in memory. Complex data or even running coroutines can be passed between states without serialization or "parceling". +- **KMP Ready:** Logic and Data are isolated from the View, making the core feature logic easily shareable across platforms. +- **Standardized Child Flows:** + - `commonflow-data`: Standardizes flow initialization and result handling. + - `commonflow-compose`: Provides a unified way to inject UI into a composition. + - `commonflow-viewmodel`: Bridges the state machine to the platform's Lifecycle and Navigation frameworks. + +## Data API +Data API is defined in the `commonflow-data` module and consists of two primary interfaces: + +### `CommonFlowDataApi` +This is the **Feature Entry Point**. It is defined in the feature module and provides the host with everything needed to launch the child flow. +- **`init(flowHost: CommonFlowHost, input: I)`**: Initializes the child flow and returns the first `MachineState`. It injects the `flowHost` so + the child can signal when it's done. +- **`getDefaultUiState()`**: Provides the initial state (like "Loading") to be displayed while the child machine is starting up. +- **`getBackGesture()`**: Defines which gesture represents "Back" in the feature, allowing the host to bridge system back-presses to the child logic. + +### `CommonFlowHost` +This is the **Communication Bridge**. It is implemented by the parent's `ProxyMachineState` and passed to the child flow. +- **`onComplete(result: R)`**: The child flow calls this method when it reaches a terminal state. The host implementation then receives the `result` and handles + the transition to the next part of the parent flow. + +## UI API +UI API is defined in the `commonflow-compose` module and consists of the following interface: + +### `CommonFlowUiApi` +This interface provides the **Feature View**. It is implemented in the feature module using `Compose` and is used by the host to render the child flow's UI. + +- **`Screen(state: U, onGesture: (G) -> Unit, modifier: Modifier)`**: A `@Composable` function that takes the current feature **UI State (U)** + and a gesture handler for feature **Gestures (G)**. It encapsulates all the UI logic for the feature, allowing the host to simply plug it into its composition. + +This separation ensures that the host doesn't need to know about the internal UI components of the child flow, only how to provide the state and handle +the gestures, which are bridged by the `ProxyMachineState`. diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/architecture.puml b/skills/commonstatemachine/common-child-flow/boilerplate/references/architecture.puml deleted file mode 120000 index df09e56..0000000 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/architecture.puml +++ /dev/null @@ -1 +0,0 @@ -../../../.shared/ARCHITECTURE.puml \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/architecture.puml b/skills/commonstatemachine/common-child-flow/boilerplate/references/architecture.puml new file mode 100644 index 0000000..4d4db8c --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/architecture.puml @@ -0,0 +1,51 @@ +@startuml +'https://plantuml.com/class-diagram + +hide empty members + +interface CommonStateMachine { + + process(gesture: G) + + clear() +} + +abstract class CommonMachineState { + + {abstract} doStart() + + {abstract} doProcess(gesture: G) + + {abstract} doClear() + # setMachineState(next: CommonMachineState) + # setUiState(ui: U) +} + +class FlowStateMachine implements CommonStateMachine { + + uiState: StateFlow +} + +class ViewModel { + - stateMachine: CommonStateMachine + + uiState: StateFlow + + process(gesture: G) +} + +interface StateFactory { + + createNextState(...): CommonMachineState +} + +interface UiRenderer { + + render(data: D): U +} + +interface StateContext { + + factory: StateFactory + + renderer: UiRenderer +} + +class DataState << (D, #FF7700) DataClass >> + +CommonStateMachine "1" *-- "1" CommonMachineState : current +ViewModel "1" *-- "1" CommonStateMachine +CommonMachineState ..> CommonMachineState : transitions +CommonMachineState -right-> StateContext : uses +StateContext "1" *-- "1" StateFactory +StateContext "1" *-- "1" UiRenderer +UiRenderer ..> DataState : renders +@enduml diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/common-child-flow.puml b/skills/commonstatemachine/common-child-flow/boilerplate/references/common-child-flow.puml deleted file mode 120000 index b135e8b..0000000 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/common-child-flow.puml +++ /dev/null @@ -1 +0,0 @@ -../../.shared/common-child-flow.puml \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/common-child-flow.puml b/skills/commonstatemachine/common-child-flow/boilerplate/references/common-child-flow.puml new file mode 100644 index 0000000..284a449 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/common-child-flow.puml @@ -0,0 +1,187 @@ +@startuml +'https://plantuml.com/class-diagram + +hide empty members + +interface CommonFlowDataApi +interface CommonFlowUiApi +abstract class CommonMachineState +abstract class CoroutineState + +CoroutineState -up-|> CommonMachineState + +package API { + interface AuthGesture + note left + Marker intent (gesture) interface for Auth feature + May include public properties available to the API client + end note + interface AuthUiState + note right + Marker UI-state interface for Auth feature + May include public properties available to the API client + end note + + class AuthInput + class AuthResult + + interface AuthDataApi { + +init(flowHost: CommonFlowHost, input: AuthInput): CommonMachineState + +getDefaultUiState(): AuthUiState + +getBackGesture(): AuthGesture? = null + } + AuthDataApi -up-|> CommonFlowDataApi: implements + AuthDataApi -down-> AuthGesture: driven by + AuthDataApi -down-> AuthUiState: displays + AuthDataApi -right-> AuthResult: returns + AuthInput -> AuthDataApi: initializes + + interface AuthUiApi { + +Screen(state: AuthUiState, onGesture: (AuthGesture) -> Unit, modifier: Modifier = Modifier) + } + AuthUiApi -up-|> CommonFlowUiApi: implements + AuthUiApi -down-> AuthGesture: emits + AuthUiApi -down-> AuthUiState: displays +} + +package Implementation { + package data { + interface AuthGestureImpl + note left + A sealed structure of all available gestures (UI intents) for the feature + end note + + object Back + note bottom + Example: Back navigation button + end note + + object Action + note bottom + Example: Main action button + end note + AuthGestureImpl -up-|> AuthGesture + AuthGestureImpl <|-down- Back + AuthGestureImpl <|-down- Action + + interface AuthUiStateImpl + note right + A sealed structure of all possible UI states for the feature + end note + + object Loading + note bottom + Example: Loading screen + end note + + class Form { + +username: String + +password: String + } + note bottom + Example: Login form + end note + AuthUiStateImpl -up-|> AuthUiState + AuthUiStateImpl <|-down- Loading + AuthUiStateImpl <|-down- Form + + class AuthStateData { + +username: String + +password: String + } + note right + A flow data state passed between machine states. + Stores inter-state data + end note + } + + package state { + interface AuthState + note top + Type-alias or an interface of the machine state + end note + AuthState -up-|> CommonMachineState + + abstract class BaseAuthState + note top + Common base class for all machine states + end note + BaseAuthState -up-|> CoroutineState + BaseAuthState -|> AuthContext: delegates implementation + + class LoadingState + LoadingState -up-|> BaseAuthState + note bottom + Example: sample machine state - Loading + end note + + class FormState + FormState -up-|> BaseAuthState + note bottom + Example: sample machine state - Login form + end note + + interface AuthStateFactory { + +startup(input: AuthInput): AuthState + +form(data: AuthStateData): AuthState + +terminated(result: AuthResult): AuthState + } + AuthStateFactory -> AuthState: creates + note left + Abstracts the state from another state creation + end note + + class AuthStateFactoryImpl + note bottom + State factory implementation + end note + AuthStateFactoryImpl -up-|> AuthStateFactory + AuthStateFactory -> BaseAuthState: creates + + interface AuthContext { + +factory: AuthStateFactory + +flowHost: CommonFlowHost + +renderer: AuthUiRenderer + } + note bottom + State-machine context available to every state + Put immutable links to common tools here: + - factory + - flow host + - etc + end note + } + + package ui { + interface AuthUiRenderer { + +renderLoading(): AuthUiState + +renderForm(data: AuthStateData): AuthUiState + +renderError(error: Throwable): AuthUiState + } + note top + Transforms data-state to AuthUiState. + Abstracts machine state from the interface data rendering + end note + AuthUiRenderer -> AuthStateData: renders + AuthUiRenderer -> AuthUiStateImpl: creates + + class AuthUiRendererImpl + AuthUiRendererImpl -up-|> AuthUiRenderer + AuthUiRendererImpl -> AuthUiStateImpl: creates + } + + class AuthDataApiImpl + note bottom + Data API implementation + Injects factory and initializes the flow by calling `init` + end note + AuthDataApiImpl -up-|> AuthDataApi + + class AuthUiApiImpl + note bottom + UI API implementation + Builds the view with Compose components inside the module + end note + AuthUiApiImpl -up-|> AuthUiApi +} +@enduml \ No newline at end of file From 4510e46d34aebc568cd60ee951e63dbb24d782f6 Mon Sep 17 00:00:00 2001 From: motorro Date: Fri, 2 Oct 2026 13:50:26 +0200 Subject: [PATCH 21/32] Add state skill Refs: #14 --- .githooks/pre-commit | 2 + .gitignore | 1 + README.md | 5 + skills-src/README.md | 20 +- skills-src/build_skills.sh | 15 +- .../.shared/ARCHITECTURE.md | 8 +- .../.shared/COMMON-CHILD-FLOW.md | 4 +- .../.shared/base-state-implementation.md | 39 ++++ .../.shared/base-test-class-implementation.md | 44 +++++ .../.shared/data-and-ui-implementation.md | 76 +++++++ .../.shared/gesture-and-ui-state-api.md | 23 +++ .../.shared/input-and-result-api.md | 22 +++ ...actory-context-interface-implementation.md | 85 ++++++++ .../.shared/state-implementation.md | 102 ++++++++++ .../.shared/state-test-implementation.md | 72 +++++++ .../.shared/test-fixtures-implementation.md | 15 ++ .../.shared/ui-renderer-implementation.md | 22 +++ .../.shared/ui-view-implementation.md | 42 ++++ .../common-child-flow/add-state/SKILL.md | 142 +++++++++++++ .../add-state/assets/example/api | 1 + .../add-state/assets/example/implementation | 1 + .../assets/example/implementation-tests | 1 + .../add-state/references/ARCHITECTURE.md | 1 + .../add-state/references/COMMON-CHILD-FLOW.md | 1 + .../add-state/references/architecture.puml | 1 + .../references/base-state-implementation.md | 1 + .../base-test-class-implementation.md | 1 + .../references/common-child-flow.puml | 1 + .../references/data-and-ui-implementation.md | 1 + .../references/gesture-and-ui-state-api.md | 1 + .../references/input-and-result-api.md | 1 + ...actory-context-interface-implementation.md | 1 + .../references/state-implementation.md | 1 + .../references/state-test-implementation.md | 1 + .../test-fixtures-implementation.md | 1 + .../references/ui-renderer-implementation.md | 1 + .../references/ui-view-implementation.md | 1 + .../wiring-transition-implementation.md | 63 ++++++ .../common-child-flow/boilerplate/SKILL.md | 60 ++++-- .../references/base-state-implementation.md | 40 +--- .../base-test-class-implementation.md | 45 +---- .../references/data-and-ui-implementation.md | 77 +------- .../references/gesture-and-ui-state-api.md | 24 +-- .../references/input-and-result-api.md | 23 +-- ...actory-context-interface-implementation.md | 86 +------- .../references/state-implementation.md | 103 +--------- .../references/state-test-implementation.md | 73 +------ .../test-fixtures-implementation.md | 16 +- .../references/ui-renderer-implementation.md | 23 +-- .../references/ui-view-implementation.md | 43 +--- skills/README.md | 20 +- .../common-child-flow/add-state/SKILL.md | 142 +++++++++++++ .../assets/example/api/AuthDataApi.kt | 8 + .../assets/example/api/AuthGesture.kt | 7 + .../add-state/assets/example/api/AuthInput.kt | 7 + .../assets/example/api/AuthResult.kt | 7 + .../add-state/assets/example/api/AuthUiApi.kt | 8 + .../assets/example/api/AuthUiState.kt | 7 + .../data/AuthStateDataTest.kt | 51 +++++ .../data/authGesturesFixtures.kt | 11 ++ .../data/dataStatesFixtures.kt | 45 +++++ .../data/uiStatesFixtures.kt | 6 + .../state/AuthenticatingStateTest.kt | 82 ++++++++ .../state/AuthenticationErrorStateTest.kt | 87 ++++++++ .../state/BaseStateTest.kt | 63 ++++++ .../state/FormStateTest.kt | 125 ++++++++++++ .../state/PreloadingErrorStateTest.kt | 91 +++++++++ .../state/PreloadingStateTest.kt | 82 ++++++++ .../ui/AuthUiRendererImplTest.kt | 63 ++++++ .../example/implementation/AuthDataApiImpl.kt | 35 ++++ .../example/implementation/AuthModule.kt | 8 + .../example/implementation/AuthUiApiImpl.kt | 27 +++ .../implementation/data/AuthGestureImpl.kt | 38 ++++ .../implementation/data/AuthStateData.kt | 23 +++ .../implementation/data/AuthUiStateImpl.kt | 36 ++++ .../implementation/state/AuthContext.kt | 25 +++ .../example/implementation/state/AuthState.kt | 10 + .../implementation/state/AuthStateFactory.kt | 54 +++++ .../state/AuthStateFactoryImpl.kt | 84 ++++++++ .../state/AuthenticatingState.kt | 81 ++++++++ .../state/AuthenticationErrorState.kt | 53 +++++ .../implementation/state/BaseAuthState.kt | 34 ++++ .../example/implementation/state/FormState.kt | 75 +++++++ .../state/PreloadingErrorState.kt | 52 +++++ .../implementation/state/PreloadingState.kt | 78 ++++++++ .../implementation/ui/AuthErrorScreen.kt | 79 ++++++++ .../implementation/ui/AuthFormScreen.kt | 122 ++++++++++++ .../implementation/ui/AuthLoadingScreen.kt | 21 ++ .../example/implementation/ui/AuthScreen.kt | 68 +++++++ .../implementation/ui/AuthUiRenderer.kt | 33 ++++ .../implementation/ui/AuthUiRendererImpl.kt | 28 +++ .../add-state/references/ARCHITECTURE.md | 38 ++++ .../add-state/references/COMMON-CHILD-FLOW.md | 41 ++++ .../add-state/references/architecture.puml | 51 +++++ .../references/base-state-implementation.md | 39 ++++ .../base-test-class-implementation.md | 44 +++++ .../references/common-child-flow.puml | 187 ++++++++++++++++++ .../references/data-and-ui-implementation.md | 76 +++++++ .../references/gesture-and-ui-state-api.md | 23 +++ .../references/input-and-result-api.md | 22 +++ ...actory-context-interface-implementation.md | 85 ++++++++ .../references/state-implementation.md | 102 ++++++++++ .../references/state-test-implementation.md | 72 +++++++ .../test-fixtures-implementation.md | 15 ++ .../references/ui-renderer-implementation.md | 22 +++ .../references/ui-view-implementation.md | 42 ++++ .../wiring-transition-implementation.md | 63 ++++++ .../common-child-flow/boilerplate/SKILL.md | 60 ++++-- .../boilerplate/references/ARCHITECTURE.md | 8 +- .../references/COMMON-CHILD-FLOW.md | 4 +- 110 files changed, 3814 insertions(+), 589 deletions(-) create mode 100644 skills-src/commonstatemachine/common-child-flow/.shared/base-state-implementation.md create mode 100644 skills-src/commonstatemachine/common-child-flow/.shared/base-test-class-implementation.md create mode 100644 skills-src/commonstatemachine/common-child-flow/.shared/data-and-ui-implementation.md create mode 100644 skills-src/commonstatemachine/common-child-flow/.shared/gesture-and-ui-state-api.md create mode 100644 skills-src/commonstatemachine/common-child-flow/.shared/input-and-result-api.md create mode 100644 skills-src/commonstatemachine/common-child-flow/.shared/renderer-factory-context-interface-implementation.md create mode 100644 skills-src/commonstatemachine/common-child-flow/.shared/state-implementation.md create mode 100644 skills-src/commonstatemachine/common-child-flow/.shared/state-test-implementation.md create mode 100644 skills-src/commonstatemachine/common-child-flow/.shared/test-fixtures-implementation.md create mode 100644 skills-src/commonstatemachine/common-child-flow/.shared/ui-renderer-implementation.md create mode 100644 skills-src/commonstatemachine/common-child-flow/.shared/ui-view-implementation.md create mode 100644 skills-src/commonstatemachine/common-child-flow/add-state/SKILL.md create mode 120000 skills-src/commonstatemachine/common-child-flow/add-state/assets/example/api create mode 120000 skills-src/commonstatemachine/common-child-flow/add-state/assets/example/implementation create mode 120000 skills-src/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests create mode 120000 skills-src/commonstatemachine/common-child-flow/add-state/references/ARCHITECTURE.md create mode 120000 skills-src/commonstatemachine/common-child-flow/add-state/references/COMMON-CHILD-FLOW.md create mode 120000 skills-src/commonstatemachine/common-child-flow/add-state/references/architecture.puml create mode 120000 skills-src/commonstatemachine/common-child-flow/add-state/references/base-state-implementation.md create mode 120000 skills-src/commonstatemachine/common-child-flow/add-state/references/base-test-class-implementation.md create mode 120000 skills-src/commonstatemachine/common-child-flow/add-state/references/common-child-flow.puml create mode 120000 skills-src/commonstatemachine/common-child-flow/add-state/references/data-and-ui-implementation.md create mode 120000 skills-src/commonstatemachine/common-child-flow/add-state/references/gesture-and-ui-state-api.md create mode 120000 skills-src/commonstatemachine/common-child-flow/add-state/references/input-and-result-api.md create mode 120000 skills-src/commonstatemachine/common-child-flow/add-state/references/renderer-factory-context-interface-implementation.md create mode 120000 skills-src/commonstatemachine/common-child-flow/add-state/references/state-implementation.md create mode 120000 skills-src/commonstatemachine/common-child-flow/add-state/references/state-test-implementation.md create mode 120000 skills-src/commonstatemachine/common-child-flow/add-state/references/test-fixtures-implementation.md create mode 120000 skills-src/commonstatemachine/common-child-flow/add-state/references/ui-renderer-implementation.md create mode 120000 skills-src/commonstatemachine/common-child-flow/add-state/references/ui-view-implementation.md create mode 100644 skills-src/commonstatemachine/common-child-flow/add-state/references/wiring-transition-implementation.md mode change 100644 => 120000 skills-src/commonstatemachine/common-child-flow/boilerplate/references/base-state-implementation.md mode change 100644 => 120000 skills-src/commonstatemachine/common-child-flow/boilerplate/references/base-test-class-implementation.md mode change 100644 => 120000 skills-src/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-implementation.md mode change 100644 => 120000 skills-src/commonstatemachine/common-child-flow/boilerplate/references/gesture-and-ui-state-api.md mode change 100644 => 120000 skills-src/commonstatemachine/common-child-flow/boilerplate/references/input-and-result-api.md mode change 100644 => 120000 skills-src/commonstatemachine/common-child-flow/boilerplate/references/renderer-factory-context-interface-implementation.md mode change 100644 => 120000 skills-src/commonstatemachine/common-child-flow/boilerplate/references/state-implementation.md mode change 100644 => 120000 skills-src/commonstatemachine/common-child-flow/boilerplate/references/state-test-implementation.md mode change 100644 => 120000 skills-src/commonstatemachine/common-child-flow/boilerplate/references/test-fixtures-implementation.md mode change 100644 => 120000 skills-src/commonstatemachine/common-child-flow/boilerplate/references/ui-renderer-implementation.md mode change 100644 => 120000 skills-src/commonstatemachine/common-child-flow/boilerplate/references/ui-view-implementation.md create mode 100644 skills/commonstatemachine/common-child-flow/add-state/SKILL.md create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/api/AuthDataApi.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/api/AuthGesture.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/api/AuthInput.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/api/AuthResult.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/api/AuthUiApi.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/api/AuthUiState.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/data/AuthStateDataTest.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/data/authGesturesFixtures.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/data/dataStatesFixtures.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/data/uiStatesFixtures.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/state/AuthenticatingStateTest.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/state/AuthenticationErrorStateTest.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/state/BaseStateTest.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/state/FormStateTest.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/state/PreloadingErrorStateTest.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/state/PreloadingStateTest.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/ui/AuthUiRendererImplTest.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/AuthDataApiImpl.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/AuthModule.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/AuthUiApiImpl.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/data/AuthGestureImpl.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/data/AuthStateData.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/data/AuthUiStateImpl.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/AuthContext.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/AuthState.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/AuthStateFactory.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/AuthStateFactoryImpl.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/AuthenticatingState.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/AuthenticationErrorState.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/BaseAuthState.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/FormState.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/PreloadingErrorState.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/PreloadingState.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/ui/AuthErrorScreen.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/ui/AuthFormScreen.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/ui/AuthLoadingScreen.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/ui/AuthScreen.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/ui/AuthUiRenderer.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/ui/AuthUiRendererImpl.kt create mode 100644 skills/commonstatemachine/common-child-flow/add-state/references/ARCHITECTURE.md create mode 100644 skills/commonstatemachine/common-child-flow/add-state/references/COMMON-CHILD-FLOW.md create mode 100644 skills/commonstatemachine/common-child-flow/add-state/references/architecture.puml create mode 100644 skills/commonstatemachine/common-child-flow/add-state/references/base-state-implementation.md create mode 100644 skills/commonstatemachine/common-child-flow/add-state/references/base-test-class-implementation.md create mode 100644 skills/commonstatemachine/common-child-flow/add-state/references/common-child-flow.puml create mode 100644 skills/commonstatemachine/common-child-flow/add-state/references/data-and-ui-implementation.md create mode 100644 skills/commonstatemachine/common-child-flow/add-state/references/gesture-and-ui-state-api.md create mode 100644 skills/commonstatemachine/common-child-flow/add-state/references/input-and-result-api.md create mode 100644 skills/commonstatemachine/common-child-flow/add-state/references/renderer-factory-context-interface-implementation.md create mode 100644 skills/commonstatemachine/common-child-flow/add-state/references/state-implementation.md create mode 100644 skills/commonstatemachine/common-child-flow/add-state/references/state-test-implementation.md create mode 100644 skills/commonstatemachine/common-child-flow/add-state/references/test-fixtures-implementation.md create mode 100644 skills/commonstatemachine/common-child-flow/add-state/references/ui-renderer-implementation.md create mode 100644 skills/commonstatemachine/common-child-flow/add-state/references/ui-view-implementation.md create mode 100644 skills/commonstatemachine/common-child-flow/add-state/references/wiring-transition-implementation.md diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 8f68637..ca137f4 100644 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -7,12 +7,14 @@ repo_root="$(git rev-parse --show-toplevel)" cd "$repo_root" if [ ! -f "skills-src/build_skills.sh" ]; then + echo "Warning: skills-src/build_skills.sh not found, skipping skills rebuild." exit 0 fi echo "Rebuilding skills/ from skills-src/..." bash skills-src/build_skills.sh +echo "Adding changes..." git add skills exit 0 diff --git a/.gitignore b/.gitignore index 419645d..ac927bf 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /.idea .DS_Store /build +/Claude outputs /artifacts /.kotlin build diff --git a/README.md b/README.md index 4c0a1b7..c94aede 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,11 @@ Key features: - Should work well in kotlin-multiplatform projects - Easy to integrate with multi-module architecture - Designed for [Jetpack Compose](https://developer.android.com/jetpack/compose) but it is not a restriction +- **Navigation inversion** - the user doesn't navigate between screens, the logic decides what to show next; several + logical states can map to the very same screen while the machine moves silently between them, and a transition can + carry live data (even a running coroutine) forward without packing it into serializable arguments the way a + screen-based navigation graph would + ([read more](https://proandroiddev.com/mvi-architecture-with-a-state-machine-basics-721c5ebed893)) - May (if you like to) work as a navigation library - Explicit `Back` gesture management with the total control of yours - Get rid of `SingleLiveEvent` for navigation, dialogs and even side effects like toasts if you diff --git a/skills-src/README.md b/skills-src/README.md index 6f238d6..20d67df 100644 --- a/skills-src/README.md +++ b/skills-src/README.md @@ -23,4 +23,22 @@ of friends as soon as the user is authenticated: > - Logical states: `list` (with the subscription) and `error`. > - Views: list, loading, error — built with the shared theme, dimensions and composables from the `appcore` module. > - Wire up DI modules for the data and UI APIs, add state tests, and create proxies (gesture, UI-state, module flow) -> to embed the flow in the main application module. \ No newline at end of file +> to embed the flow in the main application module. + +## Add a new state to an existing Common Child Flow module +[Add-state skill](commonstatemachine/common-child-flow/add-state/SKILL.md): adds one more logical state to a +feature-implementation module that already exists, without touching the module's overall boilerplate. A new +state doesn't have to be a new screen - it can just as easily be an invisible step that hands off to the next +one on its own. + - Infers the new state's gesture, UI-state and interstate data from a screen design or a text description + - Creates the state, its state-factory method, and its view + - Creates a unit test for the new state + - Wires up the transition from an existing state, whether it's triggered by a gesture or by that state's own + work completing + +For example, continuing the `friendlist` module from the example above, try adding a details screen: + +> Add a `details` state to the `friendlist` module that shows a single friend's info. +> - Reached when the user clicks a friend in the list: the `list` state should handle that click and switch to +> the new state, passing the clicked friend's id. +> - Show the friend's name and status; offer a back gesture to return to the list. \ No newline at end of file diff --git a/skills-src/build_skills.sh b/skills-src/build_skills.sh index 32240a9..f532e0a 100644 --- a/skills-src/build_skills.sh +++ b/skills-src/build_skills.sh @@ -13,15 +13,16 @@ if [[ ! "$DEST_DIR" = /* ]]; then fi echo "Copying skills from $SRC_DIR to $DEST_DIR, resolving symlinks..." + +echo "Cleaning up..." rm -rf "$DEST_DIR" mkdir -p "$DEST_DIR" -if command -v rsync >/dev/null 2>&1; then - rsync -aL --exclude=".*" --exclude="build_skills.sh" "$SRC_DIR/" "$DEST_DIR/" -else - cp -RL "$SRC_DIR/." "$DEST_DIR/" - find "$DEST_DIR" -mindepth 1 -name ".*" -exec rm -rf {} + - rm -f "$DEST_DIR/build_skills.sh" -fi +cp -RL "$SRC_DIR/." "$DEST_DIR/" +# Remove every dot-folder (e.g. the .shared source-of-truth folders) and the script itself - their +# real content only ever belongs in skills/ by being resolved through a symlink elsewhere. +find "$DEST_DIR" -mindepth 1 -name ".*" -exec rm -rf {} + +rm -f "$DEST_DIR/build_skills.sh" echo "Skills copied to $DEST_DIR." +exit 0 \ No newline at end of file diff --git a/skills-src/commonstatemachine/.shared/ARCHITECTURE.md b/skills-src/commonstatemachine/.shared/ARCHITECTURE.md index 3b5362d..94621a6 100644 --- a/skills-src/commonstatemachine/.shared/ARCHITECTURE.md +++ b/skills-src/commonstatemachine/.shared/ARCHITECTURE.md @@ -14,7 +14,13 @@ - It maintains the state transition logic. - `FlowStateMachine` (standard implementation) exports the current **UI State** as a `StateFlow`. - It provides the `process(gesture)` entry point for user interaction. -- **Navigation Inversion:** Navigation is a logical state transition within the state machine (Logic/ViewModel layer). The View is passive and follows the logic. +- **Navigation Inversion:** Navigation is a logical state transition within the state machine (Logic/ViewModel layer), not something the View drives. The user + doesn't navigate between screens - gestures (or a state's own work finishing) move the machine from one logical state to another, and whichever state is + active decides what to show by calling `setUiState`. Several logical states can therefore render the exact same UI State one after another - for example, a + preload step and an auth-check step can both just show a loading spinner - with nothing visibly changing until some state actually calls `setUiState` with + something new. The same decoupling is why a transition can carry live, non-serializable data (an open coroutine, a scope, an object reference) forward as a + plain constructor argument, unlike a screen-based navigation graph where arguments between destinations typically need to be serializable. See + [MVI Architecture with a State Machine: Basics](https://proandroiddev.com/mvi-architecture-with-a-state-machine-basics-721c5ebed893) for the full rationale. - **ViewModel Integration:** The `ViewModel` (or equivalent lifecycle host) typically: - Holds the `StateMachine` instance. - Delegates user gestures to `stateMachine.process(gesture)`. diff --git a/skills-src/commonstatemachine/common-child-flow/.shared/COMMON-CHILD-FLOW.md b/skills-src/commonstatemachine/common-child-flow/.shared/COMMON-CHILD-FLOW.md index e727d51..655ed24 100644 --- a/skills-src/commonstatemachine/common-child-flow/.shared/COMMON-CHILD-FLOW.md +++ b/skills-src/commonstatemachine/common-child-flow/.shared/COMMON-CHILD-FLOW.md @@ -4,7 +4,9 @@ ## Philosophy and Principles - **Modularity via Proxying:** Features are independent modules. A parent flow hosts a child flow using `ProxyMachineState`, which bridges incompatible Gesture/UI systems. -- **Direct Data Flow:** Transitions occur in memory. Complex data or even running coroutines can be passed between states without serialization or "parceling". +- **Direct Data Flow:** Transitions occur in memory. Complex data or even running coroutines can be passed between states without serialization or "parceling" - + unlike a screen-based navigation library (e.g. Jetpack Navigation), where arguments passed between destinations typically need to be serializable. This + follows from the same [Navigation Inversion](ARCHITECTURE.md) principle the base state machine already relies on. - **KMP Ready:** Logic and Data are isolated from the View, making the core feature logic easily shareable across platforms. - **Standardized Child Flows:** - `commonflow-data`: Standardizes flow initialization and result handling. diff --git a/skills-src/commonstatemachine/common-child-flow/.shared/base-state-implementation.md b/skills-src/commonstatemachine/common-child-flow/.shared/base-state-implementation.md new file mode 100644 index 0000000..a78ef49 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/.shared/base-state-implementation.md @@ -0,0 +1,39 @@ +# Module implementation: base state +Use this reference to implement a common base state for the flow. +Take a look at the [class diagram](common-child-flow.puml) to get the idea about components purpose. +Take a look at the [example](../assets/example/implementation/state/BaseAuthState.kt). + +## Step 1. Create a base class +Create a base abstract class that would be used as a common ancestor for the most of the logical states. +Follow the following common rules: + +- Make the class internal to the implementation module. +- The name should start with `Base` prefix. +- The class should be abstract +- The class should extend the `com.motorro.commonstatemachine.coroutines.CoroutineState` +- The class should bind the generic parameters for the [gesture](data-and-ui-implementation.md#step-1-ui-gestures) and [ui-state](data-and-ui-implementation.md#step-2-ui-states) types. +- The class should take the [context](renderer-factory-context-interface-implementation.md#step-4-state-context) parameter and implement the context by delegate. + +## Step 2. Add convenience methods to change the machine and UI states +The `com.motorro.commonstatemachine.CommonMachineState` subclass is a part of a state-machine. +It updates the outside world with the following methods: + +- `setUiState` - updates the current UI state, thus the screen to the user. +- `setMachineState` - switches the logical state in a hosting state-machine. + +To reduce the boilerplate, create two protected inline convenience methods: + +- `setMachineState(block: AuthStateFactory.() -> AuthState)` - runs the block on the factory and changes the machine state. +- `setUiState(block: AuthUiRenderer.() -> AuthUiStateImpl)` - runs the renderer and updates the UI state + +Take a look at the [example](../assets/example/implementation/state/BaseAuthState.kt) for the implementation. + +## Step 3. Basic gesture processing +The `com.motorro.commonstatemachine.CommonMachineState` processes the user gestures in the `doProcess` method. +The concrete state usually only processes those gestures it is interested in. +Example: the login form state is interested only in gestures that are relevant to login actions, it does not process +gestures of the logout screen. +If the project uses the logging framework, add a simple `doProcess` override to the base class: if the state gets the +unexpected gesture - log a warning message using the logging framework. + +Take a look at the [example](../assets/example/implementation/state/BaseAuthState.kt) for the implementation. diff --git a/skills-src/commonstatemachine/common-child-flow/.shared/base-test-class-implementation.md b/skills-src/commonstatemachine/common-child-flow/.shared/base-test-class-implementation.md new file mode 100644 index 0000000..35816f9 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/.shared/base-test-class-implementation.md @@ -0,0 +1,44 @@ +# Module implementation: base test class +Use this reference to get the idea snd create a base abstract uni-test class. +This class will handle common test-mocks and initialization for the most of machine-states unit-tests: + +- state-machine mock +- factory mock +- renderer mock +- context mock + +Most of the machine-state test will subclass this base class. + +## Step 1. Base test class +Create an abstract internal class and call `BaseStateTest`. + +## Step 2. Protected properties +Create the following protected properties to use in child test-classes: + +- `stateMachine` - `com.motorro.commonstatemachine.CommonStateMachine` mock bound with [implementation gesture and ui-state types](data-and-ui-implementation.md) +- `stateFactory` - [machine state factory mock](renderer-factory-context-interface-implementation.md#step-2-state-factory-interface) +- `renderer` - [ui state renderer mock](renderer-factory-context-interface-implementation.md#step-3-ui-renderer-interface) +- `flowHost` - `com.motorro.commonstatemachine.flow.data.CommonFlowHost` mock bound with the return type +- `context` - [interstate context mock](renderer-factory-context-interface-implementation.md#step-4-state-context) +- `nextState` - a bound `com.motorro.commonstatemachine.CommonMachineState` type to use with the state-factory mock. +- `dispatcher` - a coroutine `TestDispatcher` that will be used in test functions. + +## Step 3. Mock initialization +You will need to initialize all the mocks listed in the previous state and mock some of the functions there: + +1. Create the `init` function and mark it with the annotation to run before the test run. +2. Initialize `dispatcher` with the `UnconfinedTestDispatcher` if not instructed otherwise. +3. Initialize coroutines `Main` dispatcher with the test dispatcher from the previous step. +4. Initialize the `stateMachine` with the relaxed mock. +5. Initialize the `stateFactory` with the strict mock. +6. Initialize the `flowHost` with the relaxed mock. +7. Initialize the `renderer` with the strict mock. +8. Initialize the `nextState` with the bound type alias. +9. Initialize the `context` with the object and override the properties with the mocks from the previous steps. +10. Create the empty open fun `doInit` and call it from the `init` function. Subclasses will override `doInit` + and initialize themselves with the mocks already created. +11. Create `deinit` function and call `Dispatchers.resetMain()` there. +12. Create the common coroutine test function wrapping `runTest` and using the test dispatcher from the second step. + Subclasses will use this function to run the tests. + +Take a look at the [example](../assets/example/implementation-tests/state/BaseStateTest.kt) of such a class. diff --git a/skills-src/commonstatemachine/common-child-flow/.shared/data-and-ui-implementation.md b/skills-src/commonstatemachine/common-child-flow/.shared/data-and-ui-implementation.md new file mode 100644 index 0000000..a784446 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/.shared/data-and-ui-implementation.md @@ -0,0 +1,76 @@ +# Module implementation: data classes +Use this reference to create data classes for the feature implementation. +Take a look at the [class diagram](common-child-flow.puml) to get the idea about the data types within the module. + +## Step 1. UI gestures +Create the sealed class to enumerate all the UI events (gestures) the user could send to the model: + +- button presses +- navigation elements clicks +- input changes + +The created sealed class should implement the base Gesture type defined in the API module. +See the [corresponding instruction](gesture-and-ui-state-api.md#step-1-gestures). +Put the created class to the `data` sub-package of the implementation module. + +Follow these common rules: + +- Make the created structure internal. +- Create data objects for gestures that don't carry any data. Example: user clicks the Back button. +- Create data classes for gestures that carry some data. Example: user changes an input text and the new value is passed with the event. +- Always include the data object for the backwards navigation gesture: `Back`. +- If the user didn't give you any information on the input gestures they want, `Back` would be the only child object in a structure. +- If user gives you a picture of a user interface - inspect it and create gestures for every screen. +- If the feature includes multiple screens - group the specific gestures by screen. +- The types of the properties of the ui-gestures should be immutable! + +Take a look at the [example](../assets/example/implementation/data/AuthGestureImpl.kt) - gesture collection. + +## Step 2. UI states +Create the sealed class to enumerate all the UI states the interface could display. + +The created sealed class should implement the base UI-state defined in the API module. +See the [corresponding instruction](gesture-and-ui-state-api.md#step-2-ui-states). +Put the created class to the `data` sub-package of the implementation module. + +Follow these common rules: + +- Make the created structure internal. +- Create data objects for ui-states that don't carry any data. Example: loading screen without any text messages. +- Create data classes for ui-states that carry some data. Example: login/password entry form. +- If user gives you a picture of a user interface - inspect it and create the data state for every dynamic UI element. +- If the feature includes multiple screens - group the specific ui-states by screen. +- The types of the properties of the ui-states should be immutable! Use `@Immutable` annotation for non-stable types to help Compose compiler optimize recompositions. + If immutable collection library is used in a project - use them for the collections in UI-states. + +Take a look at the [example](../assets/example/implementation/data/AuthUiStateImpl.kt) - UI-state collection. + +## Step 3. Interstate data +Create a data class to store the flow data passed between the machine states. +Follow the user's instructions to create a data class properties. +Put the created class to the `data` sub-package of the implementation module. + +Follow these common rules: + +- Make the created data class internal. +- The types of the properties of the data-state should be immutable! +- Include the flow startup input as a property. +- Set default values for the properties if user provides the information or try to guess them. + +Take a look at the [example](../assets/example/implementation/data/AuthStateData.kt) - interstate data for password authentication. + +Important: If you create any utility function based on the interstate data (like `isValidToAuthenticate` in the example above), +add a [unit test](../assets/example/implementation-tests/data/AuthStateDataTest.kt) for it. + +## Step 4. Test fixtures +Create some fixtures and test data for the created data classes: + +1. Create a `data` sub-package in the test source set. +2. Create a fixture file for data-class gestures. +3. Create a fixture file for data-class ui-states. +4. Create a fixture file for data-class interstate data. + For the interstate data, scan the project for already defined fixtures available at the class path and + prefer using them instead of creating your own fixtures + + +Take a look at the [example](../assets/example/implementation/data/AuthStateData.kt) - interstate data for password authentication. diff --git a/skills-src/commonstatemachine/common-child-flow/.shared/gesture-and-ui-state-api.md b/skills-src/commonstatemachine/common-child-flow/.shared/gesture-and-ui-state-api.md new file mode 100644 index 0000000..69384b6 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/.shared/gesture-and-ui-state-api.md @@ -0,0 +1,23 @@ +# Module API: Gesture and UI-state +Use this reference to create the UI gesture and UI state API. + +## Mechanics +The framework behind is a Model-View-Intent architecture, so: + +- The state is updated as a result of user intents (gestures) +- The view-state is fully described by data classes/objects - ui-states + +## Step 1. Gestures +The implementation is hidden from the module consumer using the internal gesture structures. +To use the feature we export the basic marker interface that may only contain some common public properties. +This interface would be implemented later in the implementation module. +Take a look at the [class diagram](common-child-flow.puml) for more details. +Take a look at the [example](../assets/example/api/AuthGesture.kt) - common API definition + +## Step 2. UI-states +The implementation is hidden from the module consumer using the internal ui-state structures. +To use the feature we export the basic marker interface that may only contain some common public properties. +This interface would be implemented later in the implementation module. +Take a look at the [class diagram](common-child-flow.puml) for more details. +Take a look at the [example](../assets/example/api/AuthUiState.kt) - common API definition + diff --git a/skills-src/commonstatemachine/common-child-flow/.shared/input-and-result-api.md b/skills-src/commonstatemachine/common-child-flow/.shared/input-and-result-api.md new file mode 100644 index 0000000..44a6ce3 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/.shared/input-and-result-api.md @@ -0,0 +1,22 @@ +# Module API: Module initialization data and flow result +Use this reference to create the required input data and define the flow result. + +## Mechanics +The [Common Child Flow](https://github.com/motorro/CommonStateMachine/tree/master#common-child-flow-api) runs a +child state-machine flow in a parent state-machine to get some result, display some UI, etc. +Example: the main application flow could run a child flow in-place to log the user-in. + +- The child flow could be initialized by some data passed from parent. +- The child flow could return some data back to the parent on completion. + +## Step 1. Input +If the user asks to provide any input - create an immutable type - a data class or a sealed structure. +If no input type explicitly required - confirm and use `Unit` +Take a look at the [class diagram](common-child-flow.puml) for more details. +Take a look at the [example](../assets/example/api/AuthInput.kt) - common API definition + +## Step 2. Result +If the user asks to provide a flow result - create an immutable type - a data class or a sealed structure. +If no result type explicitly required - confirm and use `Unit` +Take a look at the [class diagram](common-child-flow.puml) for more details. +Take a look at the [example](../assets/example/api/AuthResult.kt) - common API definition diff --git a/skills-src/commonstatemachine/common-child-flow/.shared/renderer-factory-context-interface-implementation.md b/skills-src/commonstatemachine/common-child-flow/.shared/renderer-factory-context-interface-implementation.md new file mode 100644 index 0000000..9fe585a --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/.shared/renderer-factory-context-interface-implementation.md @@ -0,0 +1,85 @@ +# Module implementation: factory interface, ui-renderer interface, context +Use this reference to create auxiliary interfaces and classes: + +- state factory +- UI-state renderer +- Interstate context + +Take a look at the [class diagram](common-child-flow.puml) to get the idea about components purpose. + +## Step 1. CommonMachineState binding +The framework main building block is a `com.motorro.commonstatemachine.CommonMachineState` - it is a logical state of a state-machine running the flow. +The generic class should be bound with two parameters: + +- [Gesture](data-and-ui-implementation.md#step-1-ui-gestures) +- [UI-state](data-and-ui-implementation.md#step-2-ui-states) + +Create an internal type-alias and put it to the `state` sub-package. +Take a look at the [example](../assets/example/implementation/state/AuthState.kt). + +## Step 2. State factory interface +Whenever it is a time to switch to another state, the current state does not create the new state directly - it delegates +the state creation to the state-factory: + +- State-factory foundation is an interface with the methods like: `createLoading`, `createForm`, etc. +- Factory functions may pass some parameters: [interstate data](data-and-ui-implementation.md#step-3-interstate-data), errors, auxiliary data. +- Machine-states get the reference to the state factory and use it to create the new state. +- State-factory interface is internal in the module. + +Your task here is to: + +- Create a state-factory internal interface +- Put the interface `state` sub-package of the implementation module. +- Every factory function should return the `CommonMachineState` binding type created in the previous step. +- Create some startup function to build the initializing state. The method should include the [input](input-and-result-api.md#step-1-input) as a parameter. +- Create `terminated` method with the [result](input-and-result-api.md#step-2-result) as a parameter. + +Take a look at the [example](../assets/example/implementation/state/AuthState.kt). + +## Step 3. UI renderer interface +States are encouraged to use a separate renderer interface that translates the [interstate data](data-and-ui-implementation.md#step-3-interstate-data) +to the [UI state](data-and-ui-implementation.md#step-2-ui-states). + +Follow these common rules: +- Make the created interface internal. +- Put the interface into the `ui` sub-package of the implementation module. +- Create a function for every UI-state in a sealed system. +- Every function should return the sealed class ui-state type. +- Provide an [interstate data](data-and-ui-implementation.md#step-3-interstate-data) parameter if the result state is a data class. +- Provide more parameters if data is required to build the ui-state, and it could not be built using the data-state. + Example: when creating some error view-state, add an error parameter. The calling state will provide it separately. + +Take a look at the [example](../assets/example/implementation/ui/AuthUiRenderer.kt). + +## Step 4. State context +To make the tools created above accessible, create an accessor interface - a context. +The state being created will receive that context in a constructor and use it to access factory, renderer, etc. + +Follow these common rules: +- Make the created interface internal. +- Put the interface into the `state` sub-package of the implementation module. +- Add properties resolving to the following tools: + - state factory + - ui-renderer + - flow host bound with the [result type](input-and-result-api.md#step-2-result) used to interact with the parent state. + +Take a look at the [example](../assets/example/implementation/state/AuthContext.kt). + +## Step 5. State factory implementation +Add an implementation class to implement the factory interface from the previous step. +The implementation will: + +- Receive state-factories in constructor and use them to create the states with complex dependencies (see the `preloading` implementation in the [example](../assets/example/implementation/state/AuthStateFactoryImpl.kt)). +- Create simple states locally (see the `preloadError` implementation in the [example](../assets/example/implementation/state/AuthStateFactoryImpl.kt)) +- For the even simpler states you may want to implement them ad-hoc (see the `terminated` implementation in the [example](../assets/example/implementation/state/AuthStateFactoryImpl.kt)). + +Follow these common rules: + +- Create the class in the implementation module. +- If required by the DI framework used in project, add required inject annotations. +- Inject external factories and dependencies required by the state to the factory constructor. + Try to use lazy provider injection to delay the initialization. +- Pass the flow host to the factory constructor. +- Build the private context object to pass to the child states. + +See the [example](../assets/example/implementation/state/AuthStateFactoryImpl.kt). diff --git a/skills-src/commonstatemachine/common-child-flow/.shared/state-implementation.md b/skills-src/commonstatemachine/common-child-flow/.shared/state-implementation.md new file mode 100644 index 0000000..1308d91 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/.shared/state-implementation.md @@ -0,0 +1,102 @@ +# Module implementation: state implementation +Use this reference to implement the machine-state(s) for the `CommonStateMachine` framework. +Take a look at the [class diagram](common-child-flow.puml) to get the idea about components purpose. +Take a look at the [example](../assets/example/implementation/state/PreloadingState.kt). + +## Mechanics +Here's how the machine-state works: + +- All machine states subclass the basic `com.motorro.commonstatemachine.CommonMachineState` (or its child) bound with the gesture and UI-state types + as described in the basic [interface implementation](renderer-factory-context-interface-implementation.md#step-1-commonmachinestate-binding) + and [base state implementation](base-state-implementation.md) manuals. +- It'd be great if a state has a single responsibility whenever possible and limit the number of gestures it processes. + For example, the document loading could be divided into the following states: + - `LoadingState` - runs the usecase and updates the interstate data. + - `ContentState` - displays loaded data and interacts with a user. + - `ErrorState` - displays an error and if possible, offers to retry by transferring back to `LoadingState`. +- The state has the following callbacks which are called by the hosting state machine: + - `doStart` - called when the state is started. + - `doProcess` - called when the state receives a gesture. + - `doClear` - called when the state is removed from the state-machine. +- The state could call the following functions to interact with the hosting state machine: + - `setMachineState` - called to switch to another state. We tipically use the factory to create the new state. + - `setUiState` - called to update the UI state. We tipically use the renderer to update the UI. +- It is important that `setMachineState` and `setUiState` are called from when state is active (between calls to `doStart` and `doClear`). + If the call is made from the inactive state - it will fail. +- Always postpone the heavy initialization till `doStart` is called. + +## Step 1. Create a class +Put the class to the `state` sub-package of the implementation module. +Follow the following common rules: + +- Make the class internal to the implementation module. +- Subclass the [base state](base-state-implementation.md). +- Pass the [context](renderer-factory-context-interface-implementation.md#step-4-state-context) as a first parameter. +- Pass the [input data](input-and-result-api.md#step-1-input) as a parameter. + +## Step 2. Add a startup implementation (or a dummy) +When the `com.motorro.commonstatemachine.CommonMachineState` is started, the state-machine calls the `doStart` lifecycle method. +Implement the method to update the UI, start loading data, etc. +The `com.motorro.commonstatemachine.coroutines.CoroutineState`, which is an ancestor of the [abstract base state](base-state-implementation.md), has a +bound coroutine scope property: `stateScope`. Use it to run coroutines. The scope would be canceled when the state is removed from the state-machine. + +## Step 3. Add a gesture processing implementation +When the user interface emits a gesture, the state-machine calls the `onGesture` method of a current `com.motorro.commonstatemachine.CommonMachineState`. +Implement the override of a function that: + +- Will process the gestures specific to this state. +- Will call the super-method on every other unexpected gesture which are out of scope of this state. + +## Step 4. Implement a local state factory +To increase the visibility and to decrease the boilerplate, the states that require external dependencies (use-cases, managers, APIs, etc.) have their +own state factories put to the DI graph. This lifts a burden of state creation micromanagement away from +the main state factory implementation. +Create such a factory for the startup state: + +- Implement as a child class with the name `StateFactory`. +- If required by the DI framework used in project, add required inject annotations. +- Inject external dependencies required by the state to the factory constructor. +- Add a single `create` method that will accept the [context](renderer-factory-context-interface-implementation.md#step-4-state-context) and all the required interstate parameters that will come from the main factory. + +## Step 5. Create a method in the main state-factory interface +The states are to be created by the [main state-factory](renderer-factory-context-interface-implementation.md#step-2-state-factory-interface) so the states +do not have hard links to one each-other. +When you create a new state, you need to create a method in the main state-factory for it: + +- Name the function to indicate what happens inside the state. Examples: `preloading`, `error`, etc. +- Pass the common interstate if needed as a data parameter (see the `form` function in the [example](../assets/example/implementation/state/AuthStateFactory.kt)). +- Pass the specific parameters - those needed by this particular state only (see the `preload` function in the [example](../assets/example/implementation/state/AuthStateFactory.kt)). +- Implement the method in the [implementation class](renderer-factory-context-interface-implementation.md#step-5-state-factory-implementation). + +## Example 1. Preloading state +For example of a state that preloads some data with the usecase, take a look at the [PreloadingState](../assets/example/implementation/state/PreloadingState.kt) class. +The state does the following when started: + +- Sets the `Loading` UI state in `doStart` method. +- Calls the `preload` function that: + - calls a usecase to preload data some data or emulates loading if the usecase is not specified, and we are doing a dummy preloading. + - creates the valid [data-state](data-and-ui-implementation.md#step-3-interstate-data). + - switches to the next state by creating it with a factory and calling the `setMachineState` function. +- Implements the `doProcess` function to handle the back gesture or any other gestures that are relevant to this state. +- Does moderate debug-logging in lifecycle methods to track the flow. + +## Example 2. Preloading error state +If any operation fails, you'd better switch the state-machine to an error state. See [PreloadingErrorState](../assets/example/implementation/state/PreloadingErrorState.kt) +state to handle the preloading error. +The typical error state would: + +- Set the `Error` UI state in `doStart` method passing the error to display +- Handle the back gesture that would terminate the flow or return to a previous state if relevant keeping the variables needed. +- May switch back to the loading state if the error is considered recoverable. +- States of that kind are usually rather simple so you don't need any state-factory. + +## Example 3. Form state +For example of a state that handles the user input, validates and prepares the data, take a look at the [FormState](../assets/example/implementation/state/FormState.kt) class. +The state does the following when started: + +- Updates the UI state with the initial data. +- Listens to the gestures, changes the interstate data and updates the screen in the `doProcess` method. +- Changes the machine state when the action gesture is received. +- Validates the data + + diff --git a/skills-src/commonstatemachine/common-child-flow/.shared/state-test-implementation.md b/skills-src/commonstatemachine/common-child-flow/.shared/state-test-implementation.md new file mode 100644 index 0000000..4bc0b15 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/.shared/state-test-implementation.md @@ -0,0 +1,72 @@ +# Module implementation: state tests +Use this reference to create tests for the [created machine states](state-implementation.md). +`CommonMachineState` tests ensure that state transitions, UI state mapping, and side effects are correctly handled. +Typical task would be the following: + +1. If the [base-test class](base-test-class-implementation.md) is not already available at the same module, add it. +2. Create a test class and extend the base-test class. +3. Define the mocks needed for the test: use-cases, apis, etc. as the lateinit vars. +4. Override the `doInit` function and initialize the mocks. +5. Create test functions using the `test` function defined in the base class. + +## Note: Mocking use-cases and suspend functions that should never complete +Some states call supplied use-cases to load some data and update the machine state accordingly. +Usually the use-case contain some suspend function to do that. +You may want this call to never end. For example - to check the `Back` gesture terminates the flow +while the user awaits for a long loading operation. +In those case use the empty `suspendCancellableCoroutine` to emulate the operation: +```kotlin +// Mock creation +private val requirementsMock: GetPasswordRequirements = mock() + +// Suspending the load operation +everySuspend { requirementsMock.invoke() } calls { suspendCancellableCoroutine { } } +``` +Example: the [test](../assets/example/implementation-tests/state/PreloadingErrorStateTest.kt) for the [PreloadingState](../assets/example/implementation/state/PreloadingState.kt): + +- The state asynchronously loads some data with the use-case at startup. +- You need to check the `Back` pressed while loading terminates. + +## Step 1. Testing the initial rendering +If the state updates the UI, check the UI is updated on state startup: + +- If the UI-renderer is defined in the module - mock the corresponding method, check the calling arguments and a call to `CommonStateMachine::setUiState`. +- If there's no UI-renderer and the state updates the ui directly, evaluate the `CommonStateMachine::setUiState` argument directly. + +Example: the [test](../assets/example/implementation-tests/state/PreloadingErrorStateTest.kt) for the [PreloadingState](../assets/example/implementation/state/PreloadingState.kt): + +- The state asynchronously loads some data with the use-case at startup. +- You need to check the state displays `Loading`. + +## Step 2. Testing the asynchronous outcomes +If the state uses any use-cases or asynchronous operations run at startup, check their outcome: + +- UI updates if any. +- state changes if any. + + +Example: the [test](../assets/example/implementation-tests/state/PreloadingErrorStateTest.kt) for the [PreloadingState](../assets/example/implementation/state/PreloadingState.kt): + +- The state asynchronously loads some data with the use-case at startup. +- You need to check the success: state updates the interstate data and proceeds. +- You need to check the failure: state switches to the error state. + +## Step 3. Testing gestures +If the state processes any gestures in `doProcess` callback, check each gesture path: + +- use [fixtures](test-fixtures-implementation.md) if there are any for gestures. +- check the UI changes if any. +- check interstate data changes if any. +- check machine state changes if any. + +Example 1: the [test](../assets/example/implementation-tests/state/FormStateTest.kt) for the [FormState](../assets/example/implementation/state/FormState.kt): + +- checks the gestures change the interstate data. +- checks the UI updates. +- checks the data validation and state transfer. + +Example 2: the [test](../assets/example/implementation-tests/state/PreloadingErrorStateTest.kt) for the [PreloadingErrorState](../assets/example/implementation/state/PreloadingErrorState.kt): + +- checks the `Back` gesture changes the machine state. +- checks `Action` gesture conditionally changes the machine-state depending on the error. +- checks the data validation and state transfer. diff --git a/skills-src/commonstatemachine/common-child-flow/.shared/test-fixtures-implementation.md b/skills-src/commonstatemachine/common-child-flow/.shared/test-fixtures-implementation.md new file mode 100644 index 0000000..ecaf1e6 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/.shared/test-fixtures-implementation.md @@ -0,0 +1,15 @@ +# Module implementation: test fixtures +Use this reference to create test fixtures for the [data classes of the feature implementation](data-and-ui-implementation.md): + +1. Create a `data` sub-package in the test source set. +2. Create a fixture file for data-class gestures. +3. Create some ui-state fixture to use when mocking the [renderer](renderer-factory-context-interface-implementation.md#step-3-ui-renderer-interface). +4. Create a fixture file for data-class interstate data, input and result data classes. + +For the fixture data, scan the project for already defined fixtures available and prefer using them instead of creating +your own fixtures. For example, look for the modules like `domain` or `entity` and use their fixtures. Check existing feature +modules for the clue how to create fixtures and where to get the common fixture module. + +Take a look at the [example](../assets/example/implementation-tests/data/authGesturesFixtures.kt) gesture fixtures. +Take a look at the [example](../assets/example/implementation-tests/data/uiStatesFixtures.kt) ui-state fixtures. +Take a look at the [example](../assets/example/implementation-tests/data/dataStatesFixtures.kt) interstate. diff --git a/skills-src/commonstatemachine/common-child-flow/.shared/ui-renderer-implementation.md b/skills-src/commonstatemachine/common-child-flow/.shared/ui-renderer-implementation.md new file mode 100644 index 0000000..4ef14e6 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/.shared/ui-renderer-implementation.md @@ -0,0 +1,22 @@ +# Module implementation: renderer implementation +Use this reference to implement the [UI-renderer](renderer-factory-context-interface-implementation.md#step-3-ui-renderer-interface) (if used in the module). + +## Step 1. Implement renderer +Follow these steps: + +- Create the implementation class in the `ui` package of the implementation module and add the UI-renderer interface to the list of implementations. +- Enumerate all [UI-states](data-and-ui-implementation.md#step-2-ui-states) and check the source UI-renderer interface. + Find the UI-states corresponding to renderer methods. +- Implement the interface methods transforming the passed parameters to the respectful states. +- Renderer may require other dependencies - resource mappers, etc. Make the class injectable using the DI framework used in a project. + +Take a look at [example](../assets/example/implementation/ui/AuthUiRendererImpl.kt) for our Auth application. + +## Step 2. Create a renderer test +Sometimes UI has a complex model that requires a lot to render. +Secure the logic by writing a unit-test for the renderer. + +- Check every interface method +- Use [fixtures](test-fixtures-implementation.md) if available, modify them by copy-constructors if needed, or add custom. + +Take a look at the [example](../assets/example/implementation-tests/ui/AuthUiRendererImplTest.kt). \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/.shared/ui-view-implementation.md b/skills-src/commonstatemachine/common-child-flow/.shared/ui-view-implementation.md new file mode 100644 index 0000000..04476cc --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/.shared/ui-view-implementation.md @@ -0,0 +1,42 @@ +# Module implementation: basic views +Use this reference to implement the basic views for the implementation. + +## Step 1. Implement scaffolding screen +Create the scaffolding composable that will draw all the [UI States](data-and-ui-implementation.md#step-2-ui-states): + +- Follow common design system and approach in a project if available (scaffolds, appbars, themes, dimensions, etc.). +- If no design system found use the following approach: + - Use `Scaffold` as a top-level element + - Create top app bar with a module title +- The function should accept the following parameters: + - Required: `uiState` - one of the [UI State implementation](data-and-ui-implementation.md#step-2-ui-states) will be passed. + - Required: `onGesture` - a callback accepting [Gesture implementation](data-and-ui-implementation.md#step-1-ui-gestures). Make this parameter the last one. + - Optional: `modifier` with a default value in case this is a common approach in current project. +- Place `when` operator in the content section and implement all branches of the UI states. +- All functions should be internal at most + +See the [example](../assets/example/implementation/ui/AuthScreen.kt) for our sample authentication project. + +## Step 2. Implement child views +Create a separate file for child views. Create a file per each [UI State](data-and-ui-implementation.md#step-2-ui-states): + +- Follow common design system and approach in a project if available (scaffolds, appbars, themes, dimensions, etc.). +- The function should accept the following parameters: + - Required: `uiState` - the concrete [UI State implementation](data-and-ui-implementation.md#step-2-ui-states) will be passed. + - Required: `onGesture` - a callback accepting [Gesture implementation](data-and-ui-implementation.md#step-1-ui-gestures). Make this parameter the last one. + - Required: `modifier` with a default value. +- If no design system found use the following approach: + - Each screen should take `fillMaxSize()` to occupy the available space. + - Center content both horizontally (`Alignment.CenterHorizontally`) and vertically (`Arrangement.Center` for `Column` or `Alignment.Center` for `Box`). + - Use common project padding if defined in the design system (e.g., `Dimensions.medium`) for the main container. + - Use design system theme or `MaterialTheme` for colors and typography to ensure consistency. +- Evaluate each UI state and create corresponding widgets +- Use `onGesture` to pass user gestures. Consider moving the lambas in `onChange` callbacks to `remember` to optimize recompositions if you find it effective. +- Generate previews. If you think multiple previews are needed - use data providers. +- Put a child view to the `when` branches of a scaffolding screen. + +See the examples: + +- [Loading view](../assets/example/implementation/ui/AuthLoadingScreen.kt) +- [Form view](../assets/example/implementation/ui/AuthFormScreen.kt) +- [Error view](../assets/example/implementation/ui/AuthErrorScreen.kt) diff --git a/skills-src/commonstatemachine/common-child-flow/add-state/SKILL.md b/skills-src/commonstatemachine/common-child-flow/add-state/SKILL.md new file mode 100644 index 0000000..3a7f726 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/add-state/SKILL.md @@ -0,0 +1,142 @@ +--- +name: commonstatemachine-commonchildflow-add-state +description: Use this skill to add one more logical state to a feature-implementation module that + already exists and follows the CommonStateMachine Common Child Flow design. A new state doesn't + have to be a new screen - it can be an invisible step, like an auth check or a data-prep step, + that moves the flow forward while the UI keeps showing what it already shows. Use it whenever the + user asks to add a new screen, step or state to an existing flow - for example "Add a details + screen to the friend list module", "Add a change-password state to the auth flow", or "Add a step + that checks the session before the content screen". If the module doesn't exist yet, use the + boilerplate skill instead. +metadata: + author: Motorro + last-updated: '2026-10-02' + keywords: + - android + - architecture + - cross-platform + - kotlin-multiplatform + - mvi + - state-machine + - feature-flow +--- + +## Description +Use this skill to add one more logical state to an existing feature-implementation module built with +the [Common Child Flow](references/COMMON-CHILD-FLOW.md) design (see the boilerplate skill that +creates such a module in the first place). This skill does not create a new module - it extends an +existing one with a new logical state, the UI to show it (if it needs one), and (optionally) the transition +that leads to it from a state already in the module. + +A logical state is not necessarily a new screen. As the [Core Architecture](references/ARCHITECTURE.md) +describes under Navigation Inversion, the user doesn't navigate between screens - the machine moves +from one logical state to another, either on a gesture or simply because a state's own work finished, +and whichever state is active decides what the UI shows. Several states can run one after another +while the UI keeps showing the very same thing, with nothing visibly changing until some state +actually calls `setUiState` with something new. Keep this in mind while gathering requirements below: +"runs a use-case, then checks the session, then prepares some data" can just as easily be three new +logical states as a single one, with the UI showing one loading spinner throughout. + +For a deeper dive into the state machine mechanics and common components, refer to the +[Core Architecture](references/ARCHITECTURE.md). +For a deeper dive into the common child flow philosophy and design, refer to the +[Common Child Flow Architecture](references/COMMON-CHILD-FLOW.md). + +## Example +This instruction uses the same [example authentication module](assets/example) as the boilerplate +skill. All examples and code samples refer to it. Adjust the names and packages to your actual need. + +## Gathering the requirements for the new state +Before changing any code, work out what the new state needs from what the user gave you: + +- A screen design (a Figma link/export, or a picture): inspect it to infer the UI-state's visual + elements (text, inputs, lists, buttons) and the gestures the user can perform on it (taps, input + changes, navigation). Name things after what the design actually shows rather than guessing. +- A state description in words (for example: "runs a use-case, then shows the result; let the user + retry on error", or "checks if the user is still authenticated, then moves on"): infer the + gesture(s) if any, what the state does on startup (which use-case or data it needs), whether it + carries data forward to or from its neighbours, and whether it needs a UI-state of its own at all - + an invisible step can simply keep showing whatever its predecessor already set. +- If the user describes a chain of steps ("loads a token, then checks it's still valid, then loads + the profile"), that is usually several new logical states handed off to one another automatically, + not one state doing everything - see Step 6 below for how a state hands off to the next one without + any user gesture involved. +- If the user gave you both, or only part of this, use what you have and ask clarifying questions + about anything still ambiguous (what happens on error, what data is required, etc.) rather than + guessing silently. + +Follow these common rules when writing code: + +- Follow the project coding conventions or use Kotlin default. +- Follow the project naming conventions or use Kotlin default. +- If the project uses KDoc as a coding convention - add it to the created types, methods, properties. +- Add TODO comments in dummy classes and functions to point the user to provide the actual + implementation. +- Don't hesitate to ask the user to clarify what they need. + +## Step 1. Extend the gesture, UI-state and interstate data +The new state may need a gesture and a UI-state of its own, and it may need to carry extra data +between states - but not always: a state with no user input needs no new gesture, and a state that +doesn't change what's on screen can just keep reusing an existing UI-state. + +Follow [this guide](references/data-and-ui-implementation.md) to: + +1. Add the gesture case(s) for the new state to the existing sealed Gesture structure, if it needs + one (Step 1). +2. Add the UI-state case for the new state to the existing sealed UI-state structure, if it needs + one (Step 2). +3. Add properties to the interstate data if the new state needs to pass data to or from its + neighbours (Step 3). +4. Extend the test fixtures for whatever you added above (Step 4). + +## Step 2. Create the new machine state +Follow [this guide](references/state-implementation.md) to create the state itself: + +1. Subclass the [base state](references/base-state-implementation.md) (Step 1). +2. Implement its startup behaviour in `doStart` - run the use-case, update the UI state, etc (Step 2). +3. Implement gesture processing for the gesture(s) added in Step 1 above, if any (Step 3). +4. If the state needs external dependencies, add a local state factory for it (Step 4). + +## Step 3. Add a state-factory method +Create a method on the main state factory and implement it, so other states (including the one you +may change in Step 6) can create the new state: + +1. Add the method to the + [state-factory interface](references/renderer-factory-context-interface-implementation.md#step-2-state-factory-interface). +2. Implement the method in the + [state-factory implementation](references/renderer-factory-context-interface-implementation.md#step-5-state-factory-implementation), + following [this guide](references/state-implementation.md#step-5-create-a-method-in-the-main-state-factory-interface). + +## Step 4. Test the new state +Follow [this guide](references/state-test-implementation.md) to create a unit test for the new +state, based on the [base test class](references/base-test-class-implementation.md). + +## Step 5. Implement the view +If the new state has a UI-state of its own, follow these guides to render it: + +1. [Implement the UI renderer, if used](references/ui-renderer-implementation.md). +2. [Implement the composable view](references/ui-view-implementation.md) for the new UI-state. + +## Step 6. Wire the transition from another state (if instructed) +If the user told you how the flow reaches this new state from an existing one, the existing state +that leads to it needs to change too. This happens one of two ways: + +- **Automatically, when the existing state's own work finishes** - for example "once the session + check passes, load the profile". Nothing here is something the user clicks through: the existing + state's `doStart` already runs that work, and it just needs to call the new state's factory method + and switch to it once that work completes, with no gesture involved. This is the common shape for a + chain of invisible steps. +- **On a gesture** - for example "when the user clicks a friend in the list, navigate to the friend's + details". The existing state needs to handle (or already handles) that gesture and switch states in + response to it. + +Follow [this guide](references/wiring-transition-implementation.md) for both cases: + +1. For an automatic handoff, find where the existing state's startup work completes; for a + gesture-driven one, add or reuse the triggering gesture in the existing state's gesture + processing. +2. Call the state-factory method from Step 3 and switch to the new state. +3. Update the existing state's unit test to cover the new transition. + +If the user didn't say how the state is reached, skip this step and tell the user that nothing +transitions into the new state yet. diff --git a/skills-src/commonstatemachine/common-child-flow/add-state/assets/example/api b/skills-src/commonstatemachine/common-child-flow/add-state/assets/example/api new file mode 120000 index 0000000..ae54fa4 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/add-state/assets/example/api @@ -0,0 +1 @@ +../../../../../../examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/add-state/assets/example/implementation b/skills-src/commonstatemachine/common-child-flow/add-state/assets/example/implementation new file mode 120000 index 0000000..29791a5 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/add-state/assets/example/implementation @@ -0,0 +1 @@ +../../../boilerplate/assets/example/implementation \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests b/skills-src/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests new file mode 120000 index 0000000..00c18af --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests @@ -0,0 +1 @@ +../../../boilerplate/assets/example/implementation-tests \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/add-state/references/ARCHITECTURE.md b/skills-src/commonstatemachine/common-child-flow/add-state/references/ARCHITECTURE.md new file mode 120000 index 0000000..f1d6e62 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/add-state/references/ARCHITECTURE.md @@ -0,0 +1 @@ +../../../.shared/ARCHITECTURE.md \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/add-state/references/COMMON-CHILD-FLOW.md b/skills-src/commonstatemachine/common-child-flow/add-state/references/COMMON-CHILD-FLOW.md new file mode 120000 index 0000000..c83c444 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/add-state/references/COMMON-CHILD-FLOW.md @@ -0,0 +1 @@ +../../.shared/COMMON-CHILD-FLOW.md \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/add-state/references/architecture.puml b/skills-src/commonstatemachine/common-child-flow/add-state/references/architecture.puml new file mode 120000 index 0000000..932edc2 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/add-state/references/architecture.puml @@ -0,0 +1 @@ +../../../.shared/architecture.puml \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/add-state/references/base-state-implementation.md b/skills-src/commonstatemachine/common-child-flow/add-state/references/base-state-implementation.md new file mode 120000 index 0000000..6f9a632 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/add-state/references/base-state-implementation.md @@ -0,0 +1 @@ +../../.shared/base-state-implementation.md \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/add-state/references/base-test-class-implementation.md b/skills-src/commonstatemachine/common-child-flow/add-state/references/base-test-class-implementation.md new file mode 120000 index 0000000..97799ef --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/add-state/references/base-test-class-implementation.md @@ -0,0 +1 @@ +../../.shared/base-test-class-implementation.md \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/add-state/references/common-child-flow.puml b/skills-src/commonstatemachine/common-child-flow/add-state/references/common-child-flow.puml new file mode 120000 index 0000000..b135e8b --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/add-state/references/common-child-flow.puml @@ -0,0 +1 @@ +../../.shared/common-child-flow.puml \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/add-state/references/data-and-ui-implementation.md b/skills-src/commonstatemachine/common-child-flow/add-state/references/data-and-ui-implementation.md new file mode 120000 index 0000000..4278d46 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/add-state/references/data-and-ui-implementation.md @@ -0,0 +1 @@ +../../.shared/data-and-ui-implementation.md \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/add-state/references/gesture-and-ui-state-api.md b/skills-src/commonstatemachine/common-child-flow/add-state/references/gesture-and-ui-state-api.md new file mode 120000 index 0000000..17c7376 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/add-state/references/gesture-and-ui-state-api.md @@ -0,0 +1 @@ +../../.shared/gesture-and-ui-state-api.md \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/add-state/references/input-and-result-api.md b/skills-src/commonstatemachine/common-child-flow/add-state/references/input-and-result-api.md new file mode 120000 index 0000000..ee03f20 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/add-state/references/input-and-result-api.md @@ -0,0 +1 @@ +../../.shared/input-and-result-api.md \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/add-state/references/renderer-factory-context-interface-implementation.md b/skills-src/commonstatemachine/common-child-flow/add-state/references/renderer-factory-context-interface-implementation.md new file mode 120000 index 0000000..fd6c40b --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/add-state/references/renderer-factory-context-interface-implementation.md @@ -0,0 +1 @@ +../../.shared/renderer-factory-context-interface-implementation.md \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/add-state/references/state-implementation.md b/skills-src/commonstatemachine/common-child-flow/add-state/references/state-implementation.md new file mode 120000 index 0000000..0f33b1e --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/add-state/references/state-implementation.md @@ -0,0 +1 @@ +../../.shared/state-implementation.md \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/add-state/references/state-test-implementation.md b/skills-src/commonstatemachine/common-child-flow/add-state/references/state-test-implementation.md new file mode 120000 index 0000000..95762d5 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/add-state/references/state-test-implementation.md @@ -0,0 +1 @@ +../../.shared/state-test-implementation.md \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/add-state/references/test-fixtures-implementation.md b/skills-src/commonstatemachine/common-child-flow/add-state/references/test-fixtures-implementation.md new file mode 120000 index 0000000..1fda763 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/add-state/references/test-fixtures-implementation.md @@ -0,0 +1 @@ +../../.shared/test-fixtures-implementation.md \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/add-state/references/ui-renderer-implementation.md b/skills-src/commonstatemachine/common-child-flow/add-state/references/ui-renderer-implementation.md new file mode 120000 index 0000000..40cd186 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/add-state/references/ui-renderer-implementation.md @@ -0,0 +1 @@ +../../.shared/ui-renderer-implementation.md \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/add-state/references/ui-view-implementation.md b/skills-src/commonstatemachine/common-child-flow/add-state/references/ui-view-implementation.md new file mode 120000 index 0000000..213036a --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/add-state/references/ui-view-implementation.md @@ -0,0 +1 @@ +../../.shared/ui-view-implementation.md \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/add-state/references/wiring-transition-implementation.md b/skills-src/commonstatemachine/common-child-flow/add-state/references/wiring-transition-implementation.md new file mode 100644 index 0000000..3137ba3 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/add-state/references/wiring-transition-implementation.md @@ -0,0 +1,63 @@ +# Module implementation: wiring a transition from an existing state +Use this reference when the user told you how an existing machine state leads to the state you just +created. You will modify the EXISTING (calling) state, not the new one. + +Take a look at the [class diagram](common-child-flow.puml) to get the idea about the components' +purpose. + +Per Navigation Inversion (see [ARCHITECTURE.md](ARCHITECTURE.md)), a transition between logical +states doesn't need a user in the loop at all - the machine moves itself forward whenever a state +decides it's done. There are two patterns below; figure out which one the user actually means before +touching any code. + +## Pattern A: automatic handoff (no gesture) +This is the common shape for a chain of invisible steps - for example "loads a token, then checks +it's still valid, then loads the profile". None of this is something the user clicks through: each +state's own `doStart` runs its work and, once it completes, calls the next state's factory method and +switches to it - exactly like +[PreloadingState](../assets/example/implementation/state/PreloadingState.kt) already does (see [state-implementation.md, Example 1](state-implementation.md#example-1-preloading-state)): it +preloads data in `doStart` and, on success, creates the next state with the factory and calls +`setMachineState` - no gesture anywhere in the sequence. + +To wire this into an existing state: + +1. Find where its startup work currently finishes successfully (the end of `doStart`, or a + callback/coroutine completion inside it). +2. At that point, extract whatever data the new state needs - from the existing state's own + interstate data, or from whatever the just-finished work produced. +3. Call the [state-factory method](renderer-factory-context-interface-implementation.md#step-2-state-factory-interface) + created for the new state and switch to it with `setMachineState`, using the convenience method + from the [base state](base-state-implementation.md#step-2-add-convenience-methods-to-change-the-machine-and-ui-states). +4. Only call `setUiState` along the way if something actually needs to change on screen - a chain of + invisible steps can keep showing the same `Loading` UI-state the whole way through. + +A recoverable-error variant of the same idea - switching back to an earlier state once some condition +is met, rather than forward to a new one - is shown by [PreloadingErrorState](../assets/example/implementation/state/PreloadingErrorState.kt), which +switches back to the loading state when the error is considered recoverable (see [state-implementation.md, Example 2](state-implementation.md#example-2-preloading-error-state)). + +## Pattern B: triggered by a gesture +This is the case when the user's own action moves the flow forward - for example "when the user +clicks a friend in the list, navigate to the friend's details". + +1. Find or add the triggering gesture: if it already exists (for example a `SelectItem` gesture + carrying the clicked item), reuse it; otherwise add a new gesture case to the sealed Gesture + structure, following [this guide](data-and-ui-implementation.md#step-1-ui-gestures), and handle it in the existing state's gesture processing. +2. In that gesture's handling, extract whatever data the new state needs (from the gesture, or from + the existing state's own interstate data), call the new state's + [state-factory method](renderer-factory-context-interface-implementation.md#step-2-state-factory-interface), and switch to it with `setMachineState`. + +Take a look at the [example](../assets/example/implementation/state/FormState.kt): it already demonstrates this exact pattern - it changes the machine +state when its action gesture is receive (see [state-implementation.md, Example 3](state-implementation.md#example-3-form-state)). + +## Update the existing state's test +Whichever pattern applies, add a test case to the existing state's unit test that: + +- For Pattern A: drives the mocked startup dependency (use-case, flow, etc.) to complete, without + sending any gesture. +- For Pattern B: sends the triggering gesture (with fixture data if it carries any). +- Either way: asserts the state-factory method was called with the expected arguments, and that the + machine state was switched to the state the factory returned. + +Follow [the state-test guide](state-test-implementation.md#step-3-testing-gestures) for how to +structure a test like this - for Pattern A, the equivalent of "sending a gesture" is whatever already +drives the state's startup behaviour in its other tests. diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/SKILL.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/SKILL.md index 585d39e..18f62d3 100644 --- a/skills-src/commonstatemachine/common-child-flow/boilerplate/SKILL.md +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/SKILL.md @@ -1,24 +1,30 @@ --- name: commonstatemachine-commonchildflow-boilerplate -description: Use this skill to make a boilerplate for the new feature-flow using - the [CommonStateMachine](https://github.com/motorro/CommonStateMachine) architecture. - If the user asks you something like "Create a new authentication flow feature" - use this skill. +description: Use this skill to scaffold a brand-new feature-flow module with the CommonStateMachine + Common Child Flow architecture - the API and implementation Gradle modules, gestures, UI-states, + machine states, the state factory, DI wiring and tests. Use it whenever the user asks to create a + new feature flow, module, or screen flow from scratch for CommonStateMachine - for example + "Create a new authentication flow feature", "Scaffold a payment feature module", or "Set up a + CommonStateMachine flow for onboarding". If the module already exists and the user only wants to + add one more screen or state to it, use the add-state skill instead. metadata: author: Motorro - last-updated: '2026-08-12' + last-updated: '2026-10-02' keywords: - - android - - architecture - - cross-platform - - kotlin-multiplatform - - mvi - - state-machine - - feature-flow + - android + - architecture + - cross-platform + - kotlin-multiplatform + - mvi + - state-machine + - feature-flow --- ## Description -Use this skill to add a new feature boilerplate to the app based on the `CommonStateMachine` architecture. -following the `Common Child Flow` design. This design sets a common approach to use proxy state machines for a feature-flow. +Use this skill to add a new feature boilerplate to the app based on the `CommonStateMachine` +architecture, following the [Common Child Flow](references/COMMON-CHILD-FLOW.md) design. This design sets a common approach to use +proxy state machines for a feature-flow. If the module already exists, use the add-state skill +instead to add another logical state to it. For a deeper dive into the state machine mechanics and common components, refer to the [Core Architecture](references/ARCHITECTURE.md). For a deeper dive into the common child flow philosophy and design, refer to the [Common Child Flow Architecture](references/COMMON-CHILD-FLOW.md). @@ -30,7 +36,7 @@ to such an example. Adjust the names and packages to your actual need. ## Strategy You will create a set of Gradle modules that together form a feature: -- The API module: contains the definition of input and output data types for the feature. +- The API module: contains the definition of input and output data types for the feature. Example: authentication API that provides the basic interface to the authentication feature. - One or more implementation modules: contains the implementation of the API. Example: login implementation module - implements API with login/password functionality. @@ -38,6 +44,26 @@ You will create a set of Gradle modules that together form a feature: Take a look at the [class diagram](references/common-child-flow.puml). It contains an overview between all the interfaces, classes and objects in a setup using the Authentication module example. +## Gathering the requirements for the new feature +Before creating any modules, work out what the feature needs from what the user gave you: + +- The feature itself: a name, and whether it needs one implementation or several of the same API + (for example a `password` and a `social` authentication implementation side by side) - see Step 1. +- Its data source: does the user name an existing use-case, repository, or API to call (like a + `GetFriendList` use-case), or should you create a dummy/placeholder one? If they name one, use it + directly instead of inventing a new one. +- The state-flow: did the user describe what logical states the feature needs and how it moves + between them, or give you a screen design (a Figma link/export, or a picture) to work from? If so, + create those states and views in Steps 4-5 - and remember a logical state doesn't have to be a new + screen, it can just as easily be an invisible step that hands off to the next one automatically. If + the user gave you nothing specific, default to the classic Loading-Content-Error (LCE) shape with + dummy states and views. +- Integration: should this feature be hosted inside one or more existing parent flows (Step 7), or + does it stand alone for now? + +If the user gave you only part of this, use what you have and ask clarifying questions about +anything still ambiguous rather than guessing silently. + Follow the following common rules when writing code: - Follow the project coding conventions or use Kotlin default @@ -60,7 +86,7 @@ my-project # Project folder. Common package: com.motorro.commonstatem - Include the following dependencies if not provided already: - Base state-machine components: "com.motorro.commonstatemachine:commonstatemachine:x.x.x" - Coroutine extensions (if using coroutines): "com.motorro.commonstatemachine:coroutines:x.x.x" - - Common data API (state machine child flow): "com.motorro.commonstatemachine:commonflow-data:x.x.x" + - Common data API (state machine child flow): "com.motorro.commonstatemachine:commonflow-data:x.x.x" - Common UI API (Compose API if used): "com.motorro.commonstatemachine:commonflow-compose:x.x.x" - Include test dependencies that are used across the project and coroutines test dependencies if used. - Include DI dependencies that are used across the project. @@ -85,7 +111,7 @@ Then follow these guides for the API module implementation: 3. [Feature-flow data and UI API](references/data-and-ui-api.md) ## Step 3. Create implementation module -Here you will create an API implementation. +Here you will create an API implementation. If several implementations are requested by user (step 1) - repeat for each implementation. Create a package for the feature implementation. By default, use the common application package adding the feature name and `implementation`. If several implementations are requested by user (step 1) - name according to the implementation name. @@ -100,7 +126,7 @@ Then follow these guides for the module implementation: ## Step 4. Create the machine states For the user to kick-off faster, we will create some machine-states: -- If the user gave you the instructions about what would be the state-flow, or what states they need - create the states for them. +- If the user gave you the instructions about what would be the state-flow, or what states they need - create the states for them. Implement the required functionality or place TODOs in place of the code if it is not clear what to do exactly. Don't hesitate to ask the user to clarify what they need. - If the user didn't give you any specifics, create a classic LCE example: diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/base-state-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/base-state-implementation.md deleted file mode 100644 index a78ef49..0000000 --- a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/base-state-implementation.md +++ /dev/null @@ -1,39 +0,0 @@ -# Module implementation: base state -Use this reference to implement a common base state for the flow. -Take a look at the [class diagram](common-child-flow.puml) to get the idea about components purpose. -Take a look at the [example](../assets/example/implementation/state/BaseAuthState.kt). - -## Step 1. Create a base class -Create a base abstract class that would be used as a common ancestor for the most of the logical states. -Follow the following common rules: - -- Make the class internal to the implementation module. -- The name should start with `Base` prefix. -- The class should be abstract -- The class should extend the `com.motorro.commonstatemachine.coroutines.CoroutineState` -- The class should bind the generic parameters for the [gesture](data-and-ui-implementation.md#step-1-ui-gestures) and [ui-state](data-and-ui-implementation.md#step-2-ui-states) types. -- The class should take the [context](renderer-factory-context-interface-implementation.md#step-4-state-context) parameter and implement the context by delegate. - -## Step 2. Add convenience methods to change the machine and UI states -The `com.motorro.commonstatemachine.CommonMachineState` subclass is a part of a state-machine. -It updates the outside world with the following methods: - -- `setUiState` - updates the current UI state, thus the screen to the user. -- `setMachineState` - switches the logical state in a hosting state-machine. - -To reduce the boilerplate, create two protected inline convenience methods: - -- `setMachineState(block: AuthStateFactory.() -> AuthState)` - runs the block on the factory and changes the machine state. -- `setUiState(block: AuthUiRenderer.() -> AuthUiStateImpl)` - runs the renderer and updates the UI state - -Take a look at the [example](../assets/example/implementation/state/BaseAuthState.kt) for the implementation. - -## Step 3. Basic gesture processing -The `com.motorro.commonstatemachine.CommonMachineState` processes the user gestures in the `doProcess` method. -The concrete state usually only processes those gestures it is interested in. -Example: the login form state is interested only in gestures that are relevant to login actions, it does not process -gestures of the logout screen. -If the project uses the logging framework, add a simple `doProcess` override to the base class: if the state gets the -unexpected gesture - log a warning message using the logging framework. - -Take a look at the [example](../assets/example/implementation/state/BaseAuthState.kt) for the implementation. diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/base-state-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/base-state-implementation.md new file mode 120000 index 0000000..6f9a632 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/base-state-implementation.md @@ -0,0 +1 @@ +../../.shared/base-state-implementation.md \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/base-test-class-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/base-test-class-implementation.md deleted file mode 100644 index 35816f9..0000000 --- a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/base-test-class-implementation.md +++ /dev/null @@ -1,44 +0,0 @@ -# Module implementation: base test class -Use this reference to get the idea snd create a base abstract uni-test class. -This class will handle common test-mocks and initialization for the most of machine-states unit-tests: - -- state-machine mock -- factory mock -- renderer mock -- context mock - -Most of the machine-state test will subclass this base class. - -## Step 1. Base test class -Create an abstract internal class and call `BaseStateTest`. - -## Step 2. Protected properties -Create the following protected properties to use in child test-classes: - -- `stateMachine` - `com.motorro.commonstatemachine.CommonStateMachine` mock bound with [implementation gesture and ui-state types](data-and-ui-implementation.md) -- `stateFactory` - [machine state factory mock](renderer-factory-context-interface-implementation.md#step-2-state-factory-interface) -- `renderer` - [ui state renderer mock](renderer-factory-context-interface-implementation.md#step-3-ui-renderer-interface) -- `flowHost` - `com.motorro.commonstatemachine.flow.data.CommonFlowHost` mock bound with the return type -- `context` - [interstate context mock](renderer-factory-context-interface-implementation.md#step-4-state-context) -- `nextState` - a bound `com.motorro.commonstatemachine.CommonMachineState` type to use with the state-factory mock. -- `dispatcher` - a coroutine `TestDispatcher` that will be used in test functions. - -## Step 3. Mock initialization -You will need to initialize all the mocks listed in the previous state and mock some of the functions there: - -1. Create the `init` function and mark it with the annotation to run before the test run. -2. Initialize `dispatcher` with the `UnconfinedTestDispatcher` if not instructed otherwise. -3. Initialize coroutines `Main` dispatcher with the test dispatcher from the previous step. -4. Initialize the `stateMachine` with the relaxed mock. -5. Initialize the `stateFactory` with the strict mock. -6. Initialize the `flowHost` with the relaxed mock. -7. Initialize the `renderer` with the strict mock. -8. Initialize the `nextState` with the bound type alias. -9. Initialize the `context` with the object and override the properties with the mocks from the previous steps. -10. Create the empty open fun `doInit` and call it from the `init` function. Subclasses will override `doInit` - and initialize themselves with the mocks already created. -11. Create `deinit` function and call `Dispatchers.resetMain()` there. -12. Create the common coroutine test function wrapping `runTest` and using the test dispatcher from the second step. - Subclasses will use this function to run the tests. - -Take a look at the [example](../assets/example/implementation-tests/state/BaseStateTest.kt) of such a class. diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/base-test-class-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/base-test-class-implementation.md new file mode 120000 index 0000000..97799ef --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/base-test-class-implementation.md @@ -0,0 +1 @@ +../../.shared/base-test-class-implementation.md \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-implementation.md deleted file mode 100644 index a784446..0000000 --- a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-implementation.md +++ /dev/null @@ -1,76 +0,0 @@ -# Module implementation: data classes -Use this reference to create data classes for the feature implementation. -Take a look at the [class diagram](common-child-flow.puml) to get the idea about the data types within the module. - -## Step 1. UI gestures -Create the sealed class to enumerate all the UI events (gestures) the user could send to the model: - -- button presses -- navigation elements clicks -- input changes - -The created sealed class should implement the base Gesture type defined in the API module. -See the [corresponding instruction](gesture-and-ui-state-api.md#step-1-gestures). -Put the created class to the `data` sub-package of the implementation module. - -Follow these common rules: - -- Make the created structure internal. -- Create data objects for gestures that don't carry any data. Example: user clicks the Back button. -- Create data classes for gestures that carry some data. Example: user changes an input text and the new value is passed with the event. -- Always include the data object for the backwards navigation gesture: `Back`. -- If the user didn't give you any information on the input gestures they want, `Back` would be the only child object in a structure. -- If user gives you a picture of a user interface - inspect it and create gestures for every screen. -- If the feature includes multiple screens - group the specific gestures by screen. -- The types of the properties of the ui-gestures should be immutable! - -Take a look at the [example](../assets/example/implementation/data/AuthGestureImpl.kt) - gesture collection. - -## Step 2. UI states -Create the sealed class to enumerate all the UI states the interface could display. - -The created sealed class should implement the base UI-state defined in the API module. -See the [corresponding instruction](gesture-and-ui-state-api.md#step-2-ui-states). -Put the created class to the `data` sub-package of the implementation module. - -Follow these common rules: - -- Make the created structure internal. -- Create data objects for ui-states that don't carry any data. Example: loading screen without any text messages. -- Create data classes for ui-states that carry some data. Example: login/password entry form. -- If user gives you a picture of a user interface - inspect it and create the data state for every dynamic UI element. -- If the feature includes multiple screens - group the specific ui-states by screen. -- The types of the properties of the ui-states should be immutable! Use `@Immutable` annotation for non-stable types to help Compose compiler optimize recompositions. - If immutable collection library is used in a project - use them for the collections in UI-states. - -Take a look at the [example](../assets/example/implementation/data/AuthUiStateImpl.kt) - UI-state collection. - -## Step 3. Interstate data -Create a data class to store the flow data passed between the machine states. -Follow the user's instructions to create a data class properties. -Put the created class to the `data` sub-package of the implementation module. - -Follow these common rules: - -- Make the created data class internal. -- The types of the properties of the data-state should be immutable! -- Include the flow startup input as a property. -- Set default values for the properties if user provides the information or try to guess them. - -Take a look at the [example](../assets/example/implementation/data/AuthStateData.kt) - interstate data for password authentication. - -Important: If you create any utility function based on the interstate data (like `isValidToAuthenticate` in the example above), -add a [unit test](../assets/example/implementation-tests/data/AuthStateDataTest.kt) for it. - -## Step 4. Test fixtures -Create some fixtures and test data for the created data classes: - -1. Create a `data` sub-package in the test source set. -2. Create a fixture file for data-class gestures. -3. Create a fixture file for data-class ui-states. -4. Create a fixture file for data-class interstate data. - For the interstate data, scan the project for already defined fixtures available at the class path and - prefer using them instead of creating your own fixtures - - -Take a look at the [example](../assets/example/implementation/data/AuthStateData.kt) - interstate data for password authentication. diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-implementation.md new file mode 120000 index 0000000..4278d46 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/data-and-ui-implementation.md @@ -0,0 +1 @@ +../../.shared/data-and-ui-implementation.md \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/gesture-and-ui-state-api.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/gesture-and-ui-state-api.md deleted file mode 100644 index 69384b6..0000000 --- a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/gesture-and-ui-state-api.md +++ /dev/null @@ -1,23 +0,0 @@ -# Module API: Gesture and UI-state -Use this reference to create the UI gesture and UI state API. - -## Mechanics -The framework behind is a Model-View-Intent architecture, so: - -- The state is updated as a result of user intents (gestures) -- The view-state is fully described by data classes/objects - ui-states - -## Step 1. Gestures -The implementation is hidden from the module consumer using the internal gesture structures. -To use the feature we export the basic marker interface that may only contain some common public properties. -This interface would be implemented later in the implementation module. -Take a look at the [class diagram](common-child-flow.puml) for more details. -Take a look at the [example](../assets/example/api/AuthGesture.kt) - common API definition - -## Step 2. UI-states -The implementation is hidden from the module consumer using the internal ui-state structures. -To use the feature we export the basic marker interface that may only contain some common public properties. -This interface would be implemented later in the implementation module. -Take a look at the [class diagram](common-child-flow.puml) for more details. -Take a look at the [example](../assets/example/api/AuthUiState.kt) - common API definition - diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/gesture-and-ui-state-api.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/gesture-and-ui-state-api.md new file mode 120000 index 0000000..17c7376 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/gesture-and-ui-state-api.md @@ -0,0 +1 @@ +../../.shared/gesture-and-ui-state-api.md \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/input-and-result-api.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/input-and-result-api.md deleted file mode 100644 index 44a6ce3..0000000 --- a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/input-and-result-api.md +++ /dev/null @@ -1,22 +0,0 @@ -# Module API: Module initialization data and flow result -Use this reference to create the required input data and define the flow result. - -## Mechanics -The [Common Child Flow](https://github.com/motorro/CommonStateMachine/tree/master#common-child-flow-api) runs a -child state-machine flow in a parent state-machine to get some result, display some UI, etc. -Example: the main application flow could run a child flow in-place to log the user-in. - -- The child flow could be initialized by some data passed from parent. -- The child flow could return some data back to the parent on completion. - -## Step 1. Input -If the user asks to provide any input - create an immutable type - a data class or a sealed structure. -If no input type explicitly required - confirm and use `Unit` -Take a look at the [class diagram](common-child-flow.puml) for more details. -Take a look at the [example](../assets/example/api/AuthInput.kt) - common API definition - -## Step 2. Result -If the user asks to provide a flow result - create an immutable type - a data class or a sealed structure. -If no result type explicitly required - confirm and use `Unit` -Take a look at the [class diagram](common-child-flow.puml) for more details. -Take a look at the [example](../assets/example/api/AuthResult.kt) - common API definition diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/input-and-result-api.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/input-and-result-api.md new file mode 120000 index 0000000..ee03f20 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/input-and-result-api.md @@ -0,0 +1 @@ +../../.shared/input-and-result-api.md \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/renderer-factory-context-interface-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/renderer-factory-context-interface-implementation.md deleted file mode 100644 index 9fe585a..0000000 --- a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/renderer-factory-context-interface-implementation.md +++ /dev/null @@ -1,85 +0,0 @@ -# Module implementation: factory interface, ui-renderer interface, context -Use this reference to create auxiliary interfaces and classes: - -- state factory -- UI-state renderer -- Interstate context - -Take a look at the [class diagram](common-child-flow.puml) to get the idea about components purpose. - -## Step 1. CommonMachineState binding -The framework main building block is a `com.motorro.commonstatemachine.CommonMachineState` - it is a logical state of a state-machine running the flow. -The generic class should be bound with two parameters: - -- [Gesture](data-and-ui-implementation.md#step-1-ui-gestures) -- [UI-state](data-and-ui-implementation.md#step-2-ui-states) - -Create an internal type-alias and put it to the `state` sub-package. -Take a look at the [example](../assets/example/implementation/state/AuthState.kt). - -## Step 2. State factory interface -Whenever it is a time to switch to another state, the current state does not create the new state directly - it delegates -the state creation to the state-factory: - -- State-factory foundation is an interface with the methods like: `createLoading`, `createForm`, etc. -- Factory functions may pass some parameters: [interstate data](data-and-ui-implementation.md#step-3-interstate-data), errors, auxiliary data. -- Machine-states get the reference to the state factory and use it to create the new state. -- State-factory interface is internal in the module. - -Your task here is to: - -- Create a state-factory internal interface -- Put the interface `state` sub-package of the implementation module. -- Every factory function should return the `CommonMachineState` binding type created in the previous step. -- Create some startup function to build the initializing state. The method should include the [input](input-and-result-api.md#step-1-input) as a parameter. -- Create `terminated` method with the [result](input-and-result-api.md#step-2-result) as a parameter. - -Take a look at the [example](../assets/example/implementation/state/AuthState.kt). - -## Step 3. UI renderer interface -States are encouraged to use a separate renderer interface that translates the [interstate data](data-and-ui-implementation.md#step-3-interstate-data) -to the [UI state](data-and-ui-implementation.md#step-2-ui-states). - -Follow these common rules: -- Make the created interface internal. -- Put the interface into the `ui` sub-package of the implementation module. -- Create a function for every UI-state in a sealed system. -- Every function should return the sealed class ui-state type. -- Provide an [interstate data](data-and-ui-implementation.md#step-3-interstate-data) parameter if the result state is a data class. -- Provide more parameters if data is required to build the ui-state, and it could not be built using the data-state. - Example: when creating some error view-state, add an error parameter. The calling state will provide it separately. - -Take a look at the [example](../assets/example/implementation/ui/AuthUiRenderer.kt). - -## Step 4. State context -To make the tools created above accessible, create an accessor interface - a context. -The state being created will receive that context in a constructor and use it to access factory, renderer, etc. - -Follow these common rules: -- Make the created interface internal. -- Put the interface into the `state` sub-package of the implementation module. -- Add properties resolving to the following tools: - - state factory - - ui-renderer - - flow host bound with the [result type](input-and-result-api.md#step-2-result) used to interact with the parent state. - -Take a look at the [example](../assets/example/implementation/state/AuthContext.kt). - -## Step 5. State factory implementation -Add an implementation class to implement the factory interface from the previous step. -The implementation will: - -- Receive state-factories in constructor and use them to create the states with complex dependencies (see the `preloading` implementation in the [example](../assets/example/implementation/state/AuthStateFactoryImpl.kt)). -- Create simple states locally (see the `preloadError` implementation in the [example](../assets/example/implementation/state/AuthStateFactoryImpl.kt)) -- For the even simpler states you may want to implement them ad-hoc (see the `terminated` implementation in the [example](../assets/example/implementation/state/AuthStateFactoryImpl.kt)). - -Follow these common rules: - -- Create the class in the implementation module. -- If required by the DI framework used in project, add required inject annotations. -- Inject external factories and dependencies required by the state to the factory constructor. - Try to use lazy provider injection to delay the initialization. -- Pass the flow host to the factory constructor. -- Build the private context object to pass to the child states. - -See the [example](../assets/example/implementation/state/AuthStateFactoryImpl.kt). diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/renderer-factory-context-interface-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/renderer-factory-context-interface-implementation.md new file mode 120000 index 0000000..fd6c40b --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/renderer-factory-context-interface-implementation.md @@ -0,0 +1 @@ +../../.shared/renderer-factory-context-interface-implementation.md \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/state-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/state-implementation.md deleted file mode 100644 index 1308d91..0000000 --- a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/state-implementation.md +++ /dev/null @@ -1,102 +0,0 @@ -# Module implementation: state implementation -Use this reference to implement the machine-state(s) for the `CommonStateMachine` framework. -Take a look at the [class diagram](common-child-flow.puml) to get the idea about components purpose. -Take a look at the [example](../assets/example/implementation/state/PreloadingState.kt). - -## Mechanics -Here's how the machine-state works: - -- All machine states subclass the basic `com.motorro.commonstatemachine.CommonMachineState` (or its child) bound with the gesture and UI-state types - as described in the basic [interface implementation](renderer-factory-context-interface-implementation.md#step-1-commonmachinestate-binding) - and [base state implementation](base-state-implementation.md) manuals. -- It'd be great if a state has a single responsibility whenever possible and limit the number of gestures it processes. - For example, the document loading could be divided into the following states: - - `LoadingState` - runs the usecase and updates the interstate data. - - `ContentState` - displays loaded data and interacts with a user. - - `ErrorState` - displays an error and if possible, offers to retry by transferring back to `LoadingState`. -- The state has the following callbacks which are called by the hosting state machine: - - `doStart` - called when the state is started. - - `doProcess` - called when the state receives a gesture. - - `doClear` - called when the state is removed from the state-machine. -- The state could call the following functions to interact with the hosting state machine: - - `setMachineState` - called to switch to another state. We tipically use the factory to create the new state. - - `setUiState` - called to update the UI state. We tipically use the renderer to update the UI. -- It is important that `setMachineState` and `setUiState` are called from when state is active (between calls to `doStart` and `doClear`). - If the call is made from the inactive state - it will fail. -- Always postpone the heavy initialization till `doStart` is called. - -## Step 1. Create a class -Put the class to the `state` sub-package of the implementation module. -Follow the following common rules: - -- Make the class internal to the implementation module. -- Subclass the [base state](base-state-implementation.md). -- Pass the [context](renderer-factory-context-interface-implementation.md#step-4-state-context) as a first parameter. -- Pass the [input data](input-and-result-api.md#step-1-input) as a parameter. - -## Step 2. Add a startup implementation (or a dummy) -When the `com.motorro.commonstatemachine.CommonMachineState` is started, the state-machine calls the `doStart` lifecycle method. -Implement the method to update the UI, start loading data, etc. -The `com.motorro.commonstatemachine.coroutines.CoroutineState`, which is an ancestor of the [abstract base state](base-state-implementation.md), has a -bound coroutine scope property: `stateScope`. Use it to run coroutines. The scope would be canceled when the state is removed from the state-machine. - -## Step 3. Add a gesture processing implementation -When the user interface emits a gesture, the state-machine calls the `onGesture` method of a current `com.motorro.commonstatemachine.CommonMachineState`. -Implement the override of a function that: - -- Will process the gestures specific to this state. -- Will call the super-method on every other unexpected gesture which are out of scope of this state. - -## Step 4. Implement a local state factory -To increase the visibility and to decrease the boilerplate, the states that require external dependencies (use-cases, managers, APIs, etc.) have their -own state factories put to the DI graph. This lifts a burden of state creation micromanagement away from -the main state factory implementation. -Create such a factory for the startup state: - -- Implement as a child class with the name `StateFactory`. -- If required by the DI framework used in project, add required inject annotations. -- Inject external dependencies required by the state to the factory constructor. -- Add a single `create` method that will accept the [context](renderer-factory-context-interface-implementation.md#step-4-state-context) and all the required interstate parameters that will come from the main factory. - -## Step 5. Create a method in the main state-factory interface -The states are to be created by the [main state-factory](renderer-factory-context-interface-implementation.md#step-2-state-factory-interface) so the states -do not have hard links to one each-other. -When you create a new state, you need to create a method in the main state-factory for it: - -- Name the function to indicate what happens inside the state. Examples: `preloading`, `error`, etc. -- Pass the common interstate if needed as a data parameter (see the `form` function in the [example](../assets/example/implementation/state/AuthStateFactory.kt)). -- Pass the specific parameters - those needed by this particular state only (see the `preload` function in the [example](../assets/example/implementation/state/AuthStateFactory.kt)). -- Implement the method in the [implementation class](renderer-factory-context-interface-implementation.md#step-5-state-factory-implementation). - -## Example 1. Preloading state -For example of a state that preloads some data with the usecase, take a look at the [PreloadingState](../assets/example/implementation/state/PreloadingState.kt) class. -The state does the following when started: - -- Sets the `Loading` UI state in `doStart` method. -- Calls the `preload` function that: - - calls a usecase to preload data some data or emulates loading if the usecase is not specified, and we are doing a dummy preloading. - - creates the valid [data-state](data-and-ui-implementation.md#step-3-interstate-data). - - switches to the next state by creating it with a factory and calling the `setMachineState` function. -- Implements the `doProcess` function to handle the back gesture or any other gestures that are relevant to this state. -- Does moderate debug-logging in lifecycle methods to track the flow. - -## Example 2. Preloading error state -If any operation fails, you'd better switch the state-machine to an error state. See [PreloadingErrorState](../assets/example/implementation/state/PreloadingErrorState.kt) -state to handle the preloading error. -The typical error state would: - -- Set the `Error` UI state in `doStart` method passing the error to display -- Handle the back gesture that would terminate the flow or return to a previous state if relevant keeping the variables needed. -- May switch back to the loading state if the error is considered recoverable. -- States of that kind are usually rather simple so you don't need any state-factory. - -## Example 3. Form state -For example of a state that handles the user input, validates and prepares the data, take a look at the [FormState](../assets/example/implementation/state/FormState.kt) class. -The state does the following when started: - -- Updates the UI state with the initial data. -- Listens to the gestures, changes the interstate data and updates the screen in the `doProcess` method. -- Changes the machine state when the action gesture is received. -- Validates the data - - diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/state-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/state-implementation.md new file mode 120000 index 0000000..0f33b1e --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/state-implementation.md @@ -0,0 +1 @@ +../../.shared/state-implementation.md \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/state-test-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/state-test-implementation.md deleted file mode 100644 index 4bc0b15..0000000 --- a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/state-test-implementation.md +++ /dev/null @@ -1,72 +0,0 @@ -# Module implementation: state tests -Use this reference to create tests for the [created machine states](state-implementation.md). -`CommonMachineState` tests ensure that state transitions, UI state mapping, and side effects are correctly handled. -Typical task would be the following: - -1. If the [base-test class](base-test-class-implementation.md) is not already available at the same module, add it. -2. Create a test class and extend the base-test class. -3. Define the mocks needed for the test: use-cases, apis, etc. as the lateinit vars. -4. Override the `doInit` function and initialize the mocks. -5. Create test functions using the `test` function defined in the base class. - -## Note: Mocking use-cases and suspend functions that should never complete -Some states call supplied use-cases to load some data and update the machine state accordingly. -Usually the use-case contain some suspend function to do that. -You may want this call to never end. For example - to check the `Back` gesture terminates the flow -while the user awaits for a long loading operation. -In those case use the empty `suspendCancellableCoroutine` to emulate the operation: -```kotlin -// Mock creation -private val requirementsMock: GetPasswordRequirements = mock() - -// Suspending the load operation -everySuspend { requirementsMock.invoke() } calls { suspendCancellableCoroutine { } } -``` -Example: the [test](../assets/example/implementation-tests/state/PreloadingErrorStateTest.kt) for the [PreloadingState](../assets/example/implementation/state/PreloadingState.kt): - -- The state asynchronously loads some data with the use-case at startup. -- You need to check the `Back` pressed while loading terminates. - -## Step 1. Testing the initial rendering -If the state updates the UI, check the UI is updated on state startup: - -- If the UI-renderer is defined in the module - mock the corresponding method, check the calling arguments and a call to `CommonStateMachine::setUiState`. -- If there's no UI-renderer and the state updates the ui directly, evaluate the `CommonStateMachine::setUiState` argument directly. - -Example: the [test](../assets/example/implementation-tests/state/PreloadingErrorStateTest.kt) for the [PreloadingState](../assets/example/implementation/state/PreloadingState.kt): - -- The state asynchronously loads some data with the use-case at startup. -- You need to check the state displays `Loading`. - -## Step 2. Testing the asynchronous outcomes -If the state uses any use-cases or asynchronous operations run at startup, check their outcome: - -- UI updates if any. -- state changes if any. - - -Example: the [test](../assets/example/implementation-tests/state/PreloadingErrorStateTest.kt) for the [PreloadingState](../assets/example/implementation/state/PreloadingState.kt): - -- The state asynchronously loads some data with the use-case at startup. -- You need to check the success: state updates the interstate data and proceeds. -- You need to check the failure: state switches to the error state. - -## Step 3. Testing gestures -If the state processes any gestures in `doProcess` callback, check each gesture path: - -- use [fixtures](test-fixtures-implementation.md) if there are any for gestures. -- check the UI changes if any. -- check interstate data changes if any. -- check machine state changes if any. - -Example 1: the [test](../assets/example/implementation-tests/state/FormStateTest.kt) for the [FormState](../assets/example/implementation/state/FormState.kt): - -- checks the gestures change the interstate data. -- checks the UI updates. -- checks the data validation and state transfer. - -Example 2: the [test](../assets/example/implementation-tests/state/PreloadingErrorStateTest.kt) for the [PreloadingErrorState](../assets/example/implementation/state/PreloadingErrorState.kt): - -- checks the `Back` gesture changes the machine state. -- checks `Action` gesture conditionally changes the machine-state depending on the error. -- checks the data validation and state transfer. diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/state-test-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/state-test-implementation.md new file mode 120000 index 0000000..95762d5 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/state-test-implementation.md @@ -0,0 +1 @@ +../../.shared/state-test-implementation.md \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/test-fixtures-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/test-fixtures-implementation.md deleted file mode 100644 index ecaf1e6..0000000 --- a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/test-fixtures-implementation.md +++ /dev/null @@ -1,15 +0,0 @@ -# Module implementation: test fixtures -Use this reference to create test fixtures for the [data classes of the feature implementation](data-and-ui-implementation.md): - -1. Create a `data` sub-package in the test source set. -2. Create a fixture file for data-class gestures. -3. Create some ui-state fixture to use when mocking the [renderer](renderer-factory-context-interface-implementation.md#step-3-ui-renderer-interface). -4. Create a fixture file for data-class interstate data, input and result data classes. - -For the fixture data, scan the project for already defined fixtures available and prefer using them instead of creating -your own fixtures. For example, look for the modules like `domain` or `entity` and use their fixtures. Check existing feature -modules for the clue how to create fixtures and where to get the common fixture module. - -Take a look at the [example](../assets/example/implementation-tests/data/authGesturesFixtures.kt) gesture fixtures. -Take a look at the [example](../assets/example/implementation-tests/data/uiStatesFixtures.kt) ui-state fixtures. -Take a look at the [example](../assets/example/implementation-tests/data/dataStatesFixtures.kt) interstate. diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/test-fixtures-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/test-fixtures-implementation.md new file mode 120000 index 0000000..1fda763 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/test-fixtures-implementation.md @@ -0,0 +1 @@ +../../.shared/test-fixtures-implementation.md \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/ui-renderer-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/ui-renderer-implementation.md deleted file mode 100644 index 4ef14e6..0000000 --- a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/ui-renderer-implementation.md +++ /dev/null @@ -1,22 +0,0 @@ -# Module implementation: renderer implementation -Use this reference to implement the [UI-renderer](renderer-factory-context-interface-implementation.md#step-3-ui-renderer-interface) (if used in the module). - -## Step 1. Implement renderer -Follow these steps: - -- Create the implementation class in the `ui` package of the implementation module and add the UI-renderer interface to the list of implementations. -- Enumerate all [UI-states](data-and-ui-implementation.md#step-2-ui-states) and check the source UI-renderer interface. - Find the UI-states corresponding to renderer methods. -- Implement the interface methods transforming the passed parameters to the respectful states. -- Renderer may require other dependencies - resource mappers, etc. Make the class injectable using the DI framework used in a project. - -Take a look at [example](../assets/example/implementation/ui/AuthUiRendererImpl.kt) for our Auth application. - -## Step 2. Create a renderer test -Sometimes UI has a complex model that requires a lot to render. -Secure the logic by writing a unit-test for the renderer. - -- Check every interface method -- Use [fixtures](test-fixtures-implementation.md) if available, modify them by copy-constructors if needed, or add custom. - -Take a look at the [example](../assets/example/implementation-tests/ui/AuthUiRendererImplTest.kt). \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/ui-renderer-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/ui-renderer-implementation.md new file mode 120000 index 0000000..40cd186 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/ui-renderer-implementation.md @@ -0,0 +1 @@ +../../.shared/ui-renderer-implementation.md \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/ui-view-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/ui-view-implementation.md deleted file mode 100644 index 04476cc..0000000 --- a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/ui-view-implementation.md +++ /dev/null @@ -1,42 +0,0 @@ -# Module implementation: basic views -Use this reference to implement the basic views for the implementation. - -## Step 1. Implement scaffolding screen -Create the scaffolding composable that will draw all the [UI States](data-and-ui-implementation.md#step-2-ui-states): - -- Follow common design system and approach in a project if available (scaffolds, appbars, themes, dimensions, etc.). -- If no design system found use the following approach: - - Use `Scaffold` as a top-level element - - Create top app bar with a module title -- The function should accept the following parameters: - - Required: `uiState` - one of the [UI State implementation](data-and-ui-implementation.md#step-2-ui-states) will be passed. - - Required: `onGesture` - a callback accepting [Gesture implementation](data-and-ui-implementation.md#step-1-ui-gestures). Make this parameter the last one. - - Optional: `modifier` with a default value in case this is a common approach in current project. -- Place `when` operator in the content section and implement all branches of the UI states. -- All functions should be internal at most - -See the [example](../assets/example/implementation/ui/AuthScreen.kt) for our sample authentication project. - -## Step 2. Implement child views -Create a separate file for child views. Create a file per each [UI State](data-and-ui-implementation.md#step-2-ui-states): - -- Follow common design system and approach in a project if available (scaffolds, appbars, themes, dimensions, etc.). -- The function should accept the following parameters: - - Required: `uiState` - the concrete [UI State implementation](data-and-ui-implementation.md#step-2-ui-states) will be passed. - - Required: `onGesture` - a callback accepting [Gesture implementation](data-and-ui-implementation.md#step-1-ui-gestures). Make this parameter the last one. - - Required: `modifier` with a default value. -- If no design system found use the following approach: - - Each screen should take `fillMaxSize()` to occupy the available space. - - Center content both horizontally (`Alignment.CenterHorizontally`) and vertically (`Arrangement.Center` for `Column` or `Alignment.Center` for `Box`). - - Use common project padding if defined in the design system (e.g., `Dimensions.medium`) for the main container. - - Use design system theme or `MaterialTheme` for colors and typography to ensure consistency. -- Evaluate each UI state and create corresponding widgets -- Use `onGesture` to pass user gestures. Consider moving the lambas in `onChange` callbacks to `remember` to optimize recompositions if you find it effective. -- Generate previews. If you think multiple previews are needed - use data providers. -- Put a child view to the `when` branches of a scaffolding screen. - -See the examples: - -- [Loading view](../assets/example/implementation/ui/AuthLoadingScreen.kt) -- [Form view](../assets/example/implementation/ui/AuthFormScreen.kt) -- [Error view](../assets/example/implementation/ui/AuthErrorScreen.kt) diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/ui-view-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/ui-view-implementation.md new file mode 120000 index 0000000..213036a --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/ui-view-implementation.md @@ -0,0 +1 @@ +../../.shared/ui-view-implementation.md \ No newline at end of file diff --git a/skills/README.md b/skills/README.md index 6f238d6..20d67df 100644 --- a/skills/README.md +++ b/skills/README.md @@ -23,4 +23,22 @@ of friends as soon as the user is authenticated: > - Logical states: `list` (with the subscription) and `error`. > - Views: list, loading, error — built with the shared theme, dimensions and composables from the `appcore` module. > - Wire up DI modules for the data and UI APIs, add state tests, and create proxies (gesture, UI-state, module flow) -> to embed the flow in the main application module. \ No newline at end of file +> to embed the flow in the main application module. + +## Add a new state to an existing Common Child Flow module +[Add-state skill](commonstatemachine/common-child-flow/add-state/SKILL.md): adds one more logical state to a +feature-implementation module that already exists, without touching the module's overall boilerplate. A new +state doesn't have to be a new screen - it can just as easily be an invisible step that hands off to the next +one on its own. + - Infers the new state's gesture, UI-state and interstate data from a screen design or a text description + - Creates the state, its state-factory method, and its view + - Creates a unit test for the new state + - Wires up the transition from an existing state, whether it's triggered by a gesture or by that state's own + work completing + +For example, continuing the `friendlist` module from the example above, try adding a details screen: + +> Add a `details` state to the `friendlist` module that shows a single friend's info. +> - Reached when the user clicks a friend in the list: the `list` state should handle that click and switch to +> the new state, passing the clicked friend's id. +> - Show the friend's name and status; offer a back gesture to return to the list. \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/add-state/SKILL.md b/skills/commonstatemachine/common-child-flow/add-state/SKILL.md new file mode 100644 index 0000000..3a7f726 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/SKILL.md @@ -0,0 +1,142 @@ +--- +name: commonstatemachine-commonchildflow-add-state +description: Use this skill to add one more logical state to a feature-implementation module that + already exists and follows the CommonStateMachine Common Child Flow design. A new state doesn't + have to be a new screen - it can be an invisible step, like an auth check or a data-prep step, + that moves the flow forward while the UI keeps showing what it already shows. Use it whenever the + user asks to add a new screen, step or state to an existing flow - for example "Add a details + screen to the friend list module", "Add a change-password state to the auth flow", or "Add a step + that checks the session before the content screen". If the module doesn't exist yet, use the + boilerplate skill instead. +metadata: + author: Motorro + last-updated: '2026-10-02' + keywords: + - android + - architecture + - cross-platform + - kotlin-multiplatform + - mvi + - state-machine + - feature-flow +--- + +## Description +Use this skill to add one more logical state to an existing feature-implementation module built with +the [Common Child Flow](references/COMMON-CHILD-FLOW.md) design (see the boilerplate skill that +creates such a module in the first place). This skill does not create a new module - it extends an +existing one with a new logical state, the UI to show it (if it needs one), and (optionally) the transition +that leads to it from a state already in the module. + +A logical state is not necessarily a new screen. As the [Core Architecture](references/ARCHITECTURE.md) +describes under Navigation Inversion, the user doesn't navigate between screens - the machine moves +from one logical state to another, either on a gesture or simply because a state's own work finished, +and whichever state is active decides what the UI shows. Several states can run one after another +while the UI keeps showing the very same thing, with nothing visibly changing until some state +actually calls `setUiState` with something new. Keep this in mind while gathering requirements below: +"runs a use-case, then checks the session, then prepares some data" can just as easily be three new +logical states as a single one, with the UI showing one loading spinner throughout. + +For a deeper dive into the state machine mechanics and common components, refer to the +[Core Architecture](references/ARCHITECTURE.md). +For a deeper dive into the common child flow philosophy and design, refer to the +[Common Child Flow Architecture](references/COMMON-CHILD-FLOW.md). + +## Example +This instruction uses the same [example authentication module](assets/example) as the boilerplate +skill. All examples and code samples refer to it. Adjust the names and packages to your actual need. + +## Gathering the requirements for the new state +Before changing any code, work out what the new state needs from what the user gave you: + +- A screen design (a Figma link/export, or a picture): inspect it to infer the UI-state's visual + elements (text, inputs, lists, buttons) and the gestures the user can perform on it (taps, input + changes, navigation). Name things after what the design actually shows rather than guessing. +- A state description in words (for example: "runs a use-case, then shows the result; let the user + retry on error", or "checks if the user is still authenticated, then moves on"): infer the + gesture(s) if any, what the state does on startup (which use-case or data it needs), whether it + carries data forward to or from its neighbours, and whether it needs a UI-state of its own at all - + an invisible step can simply keep showing whatever its predecessor already set. +- If the user describes a chain of steps ("loads a token, then checks it's still valid, then loads + the profile"), that is usually several new logical states handed off to one another automatically, + not one state doing everything - see Step 6 below for how a state hands off to the next one without + any user gesture involved. +- If the user gave you both, or only part of this, use what you have and ask clarifying questions + about anything still ambiguous (what happens on error, what data is required, etc.) rather than + guessing silently. + +Follow these common rules when writing code: + +- Follow the project coding conventions or use Kotlin default. +- Follow the project naming conventions or use Kotlin default. +- If the project uses KDoc as a coding convention - add it to the created types, methods, properties. +- Add TODO comments in dummy classes and functions to point the user to provide the actual + implementation. +- Don't hesitate to ask the user to clarify what they need. + +## Step 1. Extend the gesture, UI-state and interstate data +The new state may need a gesture and a UI-state of its own, and it may need to carry extra data +between states - but not always: a state with no user input needs no new gesture, and a state that +doesn't change what's on screen can just keep reusing an existing UI-state. + +Follow [this guide](references/data-and-ui-implementation.md) to: + +1. Add the gesture case(s) for the new state to the existing sealed Gesture structure, if it needs + one (Step 1). +2. Add the UI-state case for the new state to the existing sealed UI-state structure, if it needs + one (Step 2). +3. Add properties to the interstate data if the new state needs to pass data to or from its + neighbours (Step 3). +4. Extend the test fixtures for whatever you added above (Step 4). + +## Step 2. Create the new machine state +Follow [this guide](references/state-implementation.md) to create the state itself: + +1. Subclass the [base state](references/base-state-implementation.md) (Step 1). +2. Implement its startup behaviour in `doStart` - run the use-case, update the UI state, etc (Step 2). +3. Implement gesture processing for the gesture(s) added in Step 1 above, if any (Step 3). +4. If the state needs external dependencies, add a local state factory for it (Step 4). + +## Step 3. Add a state-factory method +Create a method on the main state factory and implement it, so other states (including the one you +may change in Step 6) can create the new state: + +1. Add the method to the + [state-factory interface](references/renderer-factory-context-interface-implementation.md#step-2-state-factory-interface). +2. Implement the method in the + [state-factory implementation](references/renderer-factory-context-interface-implementation.md#step-5-state-factory-implementation), + following [this guide](references/state-implementation.md#step-5-create-a-method-in-the-main-state-factory-interface). + +## Step 4. Test the new state +Follow [this guide](references/state-test-implementation.md) to create a unit test for the new +state, based on the [base test class](references/base-test-class-implementation.md). + +## Step 5. Implement the view +If the new state has a UI-state of its own, follow these guides to render it: + +1. [Implement the UI renderer, if used](references/ui-renderer-implementation.md). +2. [Implement the composable view](references/ui-view-implementation.md) for the new UI-state. + +## Step 6. Wire the transition from another state (if instructed) +If the user told you how the flow reaches this new state from an existing one, the existing state +that leads to it needs to change too. This happens one of two ways: + +- **Automatically, when the existing state's own work finishes** - for example "once the session + check passes, load the profile". Nothing here is something the user clicks through: the existing + state's `doStart` already runs that work, and it just needs to call the new state's factory method + and switch to it once that work completes, with no gesture involved. This is the common shape for a + chain of invisible steps. +- **On a gesture** - for example "when the user clicks a friend in the list, navigate to the friend's + details". The existing state needs to handle (or already handles) that gesture and switch states in + response to it. + +Follow [this guide](references/wiring-transition-implementation.md) for both cases: + +1. For an automatic handoff, find where the existing state's startup work completes; for a + gesture-driven one, add or reuse the triggering gesture in the existing state's gesture + processing. +2. Call the state-factory method from Step 3 and switch to the new state. +3. Update the existing state's unit test to cover the new transition. + +If the user didn't say how the state is reached, skip this step and tell the user that nothing +transitions into the new state yet. diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/api/AuthDataApi.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/api/AuthDataApi.kt new file mode 100644 index 0000000..e03a962 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/api/AuthDataApi.kt @@ -0,0 +1,8 @@ +package com.motorro.commonstatemachine.examples.skills.auth.api +import com.motorro.commonstatemachine.flow.data.CommonFlowDataApi + +/** + * Data API used to run a child-flow in a proxy machine + * Available to a feature consumer + */ +interface AuthDataApi : CommonFlowDataApi \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/api/AuthGesture.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/api/AuthGesture.kt new file mode 100644 index 0000000..b93d81e --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/api/AuthGesture.kt @@ -0,0 +1,7 @@ +package com.motorro.commonstatemachine.examples.skills.auth.api + +/** + * A base type for authentication user gestures + * Available to a feature consumer + */ +interface AuthGesture \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/api/AuthInput.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/api/AuthInput.kt new file mode 100644 index 0000000..90aba9d --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/api/AuthInput.kt @@ -0,0 +1,7 @@ +package com.motorro.commonstatemachine.examples.skills.auth.api + +/** + * Initializing data for the feature flow + * @property skippable If true, allows user to skip authentication + */ +data class AuthInput(val skippable: Boolean) \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/api/AuthResult.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/api/AuthResult.kt new file mode 100644 index 0000000..e57a379 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/api/AuthResult.kt @@ -0,0 +1,7 @@ +package com.motorro.commonstatemachine.examples.skills.auth.api + +/** + * Feature flow result + * @param authenticated Example of a property returned to a parent flow + */ +data class AuthResult(val authenticated: Boolean) \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/api/AuthUiApi.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/api/AuthUiApi.kt new file mode 100644 index 0000000..1663448 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/api/AuthUiApi.kt @@ -0,0 +1,8 @@ +package com.motorro.commonstatemachine.examples.skills.auth.api +import com.motorro.commonstatemachine.flow.compose.CommonFlowUiApi + +/** + * UI API used display a child flow inside a parent composition + * Available to a feature consumer + */ +interface AuthUiApi: CommonFlowUiApi \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/api/AuthUiState.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/api/AuthUiState.kt new file mode 100644 index 0000000..b16324d --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/api/AuthUiState.kt @@ -0,0 +1,7 @@ +package com.motorro.commonstatemachine.examples.skills.auth.api + +/** + * A base type for authentication UI state + * Available to a feature consumer + */ +interface AuthUiState \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/data/AuthStateDataTest.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/data/AuthStateDataTest.kt new file mode 100644 index 0000000..a27db2f --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/data/AuthStateDataTest.kt @@ -0,0 +1,51 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.data + +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +internal class AuthStateDataTest { + @Test + fun returnsTrueIfValid() { + val data = AuthStateData( + input = INPUT, + passwordRequirements = PASSWORD_REQUIREMENTS, + username = "user", + password = "password", + ) + assertTrue(data.isValidToAuthenticate()) + } + + @Test + fun returnsFalseIfUsernameEmpty() { + val data = AuthStateData( + input = INPUT, + passwordRequirements = PASSWORD_REQUIREMENTS, + username = "", + password = "password", + ) + assertFalse(data.isValidToAuthenticate()) + } + + @Test + fun returnsFalseIfUsernameBlank() { + val data = AuthStateData( + input = INPUT, + passwordRequirements = PASSWORD_REQUIREMENTS, + username = " ", + password = "password", + ) + assertFalse(data.isValidToAuthenticate()) + } + + @Test + fun returnsFalseIfPasswordInvalid() { + val data = AuthStateData( + input = INPUT, + passwordRequirements = PASSWORD_REQUIREMENTS, + username = "user", + password = "short", // PASSWORD_REQUIREMENTS regex is ^.{8,}$ + ) + assertFalse(data.isValidToAuthenticate()) + } +} diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/data/authGesturesFixtures.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/data/authGesturesFixtures.kt new file mode 100644 index 0000000..45c6ec5 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/data/authGesturesFixtures.kt @@ -0,0 +1,11 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.data + +/** + * Username input changed + */ +internal val USERNAME_CHANGED = AuthGestureImpl.Form.UsernameChanged("user") + +/** + * Password input changed + */ +internal val PASSWORD_CHANGED = AuthGestureImpl.Form.PasswordChanged("password") diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/data/dataStatesFixtures.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/data/dataStatesFixtures.kt new file mode 100644 index 0000000..7b04bbf --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/data/dataStatesFixtures.kt @@ -0,0 +1,45 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.data + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthInput +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.skills.domain.authenticate.data.PasswordRequirements +import com.motorro.commonstatemachine.skills.domain.exception.IOException +import com.motorro.commonstatemachine.skills.domain.exception.UnknownException + +/** + * Input fixture + */ +internal val INPUT = AuthInput(true) + +/** + * Result fixture + */ +internal val RESULT = AuthResult(authenticated = true) + +internal val PASSWORD_REQUIREMENTS = PasswordRequirements( + regex = "^.{8,}$".toRegex(), + description = "Minimum eight characters" +) + +/** + * Initial state + */ +internal val EMPTY_STATE = AuthStateData(INPUT, PASSWORD_REQUIREMENTS) + +/** + * Valid form state + */ +internal val VALID_FORM_STATE = EMPTY_STATE.copy( + username = "user", + password = "password" +) + +/** + * Fatal error fixture + */ +internal val FATAL_ERROR = UnknownException("Something went wrong") + +/** + * Non-fatal error fixture + */ +internal val NON_FATAL_ERROR = IOException("Internet is down") \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/data/uiStatesFixtures.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/data/uiStatesFixtures.kt new file mode 100644 index 0000000..14b7733 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/data/uiStatesFixtures.kt @@ -0,0 +1,6 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.data + +/** + * Some UI-state to return from [com.motorro.commonstatemachine.examples.skills.auth.implementation.ui.AuthUiRenderer] mock + */ +internal val UI_STATE = AuthUiStateImpl.Loading \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/state/AuthenticatingStateTest.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/state/AuthenticatingStateTest.kt new file mode 100644 index 0000000..5d5f135 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/state/AuthenticatingStateTest.kt @@ -0,0 +1,82 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.FATAL_ERROR +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.UI_STATE +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.VALID_FORM_STATE +import com.motorro.commonstatemachine.skills.domain.authenticate.AuthenticateWithPassword +import dev.mokkery.MockMode +import dev.mokkery.answering.calls +import dev.mokkery.answering.returns +import dev.mokkery.answering.throws +import dev.mokkery.every +import dev.mokkery.everySuspend +import dev.mokkery.matcher.any +import dev.mokkery.mock +import dev.mokkery.verify +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.test.Test + +internal class AuthenticatingStateTest : BaseStateTest() { + private lateinit var authenticateMock: AuthenticateWithPassword + private lateinit var state: AuthenticatingState + + override fun doInit() { + authenticateMock = mock(MockMode.strict) + state = AuthenticatingState(context, VALID_FORM_STATE, authenticateMock) + every { renderer.renderLoading() } returns UI_STATE + } + + @Test + fun rendersLoadingOnStart() = test { + everySuspend { authenticateMock(any(), any()) } calls { suspendCancellableCoroutine { } } + + state.start(stateMachine) + + verify { + renderer.renderLoading() + stateMachine.setUiState(UI_STATE) + } + } + + @Test + fun terminatesWhenAuthenticated() = test { + everySuspend { authenticateMock(any(), any()) } returns Unit + every { stateFactory.terminated(any()) } returns nextState + + state.start(stateMachine) + + verify { + stateFactory.terminated(AuthResult(authenticated = true)) + stateMachine.setMachineState(nextState) + } + } + + @Test + fun transfersToErrorWhenFailed() = test { + everySuspend { authenticateMock(any(), any()) } throws FATAL_ERROR + every { stateFactory.authenticationError(any(), any()) } returns nextState + + state.start(stateMachine) + + verify { + stateFactory.authenticationError(VALID_FORM_STATE, any()) + stateMachine.setMachineState(nextState) + } + } + + @Test + fun returnsToFormOnBackWhileAuthenticating() = test { + everySuspend { authenticateMock(any(), any()) } calls { suspendCancellableCoroutine { } } + every { stateFactory.form(any()) } returns nextState + + state.start(stateMachine) + state.process(AuthGestureImpl.Back) + + verify { + stateFactory.form(VALID_FORM_STATE) + stateMachine.setMachineState(nextState) + } + } +} diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/state/AuthenticationErrorStateTest.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/state/AuthenticationErrorStateTest.kt new file mode 100644 index 0000000..163d205 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/state/AuthenticationErrorStateTest.kt @@ -0,0 +1,87 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.FATAL_ERROR +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.NON_FATAL_ERROR +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.UI_STATE +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.VALID_FORM_STATE +import com.motorro.commonstatemachine.skills.domain.exception.AppException +import dev.mokkery.answering.returns +import dev.mokkery.every +import dev.mokkery.matcher.any +import dev.mokkery.verify +import kotlin.test.Test + +internal class AuthenticationErrorStateTest : BaseStateTest() { + override fun doInit() { + every { renderer.renderFullScreenError(any(), any()) } returns UI_STATE + } + + private fun createState(error: AppException) = AuthenticationErrorState( + context, + VALID_FORM_STATE, + error + ) + + @Test + fun rendersFatalErrorOnStart() = test { + val state = createState(FATAL_ERROR) + state.start(stateMachine) + verify { + renderer.renderFullScreenError(FATAL_ERROR, canRetry = false) + stateMachine.setUiState(UI_STATE) + } + } + + @Test + fun rendersNonFatalErrorOnStart() = test { + val state = createState(NON_FATAL_ERROR) + state.start(stateMachine) + verify { + renderer.renderFullScreenError(NON_FATAL_ERROR, canRetry = true) + stateMachine.setUiState(UI_STATE) + } + } + + @Test + fun returnsToFormOnBack() = test { + val state = createState(NON_FATAL_ERROR) + every { stateFactory.form(any()) } returns nextState + + state.start(stateMachine) + state.process(AuthGestureImpl.Back) + + verify { + stateFactory.form(VALID_FORM_STATE) + stateMachine.setMachineState(nextState) + } + } + + @Test + fun retriesOnActionIfRetryable() = test { + val state = createState(NON_FATAL_ERROR) + every { stateFactory.authenticating(any()) } returns nextState + + state.start(stateMachine) + state.process(AuthGestureImpl.Action) + + verify { + stateFactory.authenticating(VALID_FORM_STATE) + stateMachine.setMachineState(nextState) + } + } + + @Test + fun returnsToFormOnActionIfNotRetryable() = test { + val state = createState(FATAL_ERROR) + every { stateFactory.form(any()) } returns nextState + + state.start(stateMachine) + state.process(AuthGestureImpl.Action) + + verify { + stateFactory.form(VALID_FORM_STATE) + stateMachine.setMachineState(nextState) + } + } +} diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/state/BaseStateTest.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/state/BaseStateTest.kt new file mode 100644 index 0000000..1d4ca54 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/state/BaseStateTest.kt @@ -0,0 +1,63 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.CommonStateMachine +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.ui.AuthUiRenderer +import com.motorro.commonstatemachine.flow.data.CommonFlowHost +import dev.mokkery.MockMode +import dev.mokkery.mock +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 + +@OptIn(ExperimentalCoroutinesApi::class) +internal abstract class BaseStateTest { + protected lateinit var stateMachine: CommonStateMachine + protected lateinit var stateFactory: AuthStateFactory + protected lateinit var renderer: AuthUiRenderer + protected lateinit var context: AuthContext + protected lateinit var flowHost: CommonFlowHost + protected lateinit var nextState: AuthState + protected lateinit var dispatcher: TestDispatcher + + @BeforeTest + fun init() { + dispatcher = UnconfinedTestDispatcher() + Dispatchers.setMain(dispatcher) + + stateMachine = mock(mode = MockMode.autoUnit) + stateFactory = mock() + flowHost = mock(mode = MockMode.autoUnit) + renderer = mock(mode = MockMode.strict) + nextState = AuthState() + + context = object : AuthContext { + override val factory: AuthStateFactory = this@BaseStateTest.stateFactory + override val flowHost: CommonFlowHost = this@BaseStateTest.flowHost + override val renderer: AuthUiRenderer = this@BaseStateTest.renderer + } + + doInit() + } + + @AfterTest + fun deinit() { + Dispatchers.resetMain() + } + + protected fun test(block: TestScope.() -> Unit) = runTest( + dispatcher, + testBody = block + ) + + protected open fun doInit() = Unit +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/state/FormStateTest.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/state/FormStateTest.kt new file mode 100644 index 0000000..80c6e7a --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/state/FormStateTest.kt @@ -0,0 +1,125 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.EMPTY_STATE +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.PASSWORD_CHANGED +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.UI_STATE +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.USERNAME_CHANGED +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.VALID_FORM_STATE +import dev.mokkery.answering.returns +import dev.mokkery.every +import dev.mokkery.matcher.any +import dev.mokkery.verify +import dev.mokkery.verify.VerifyMode +import kotlin.test.Test +import kotlin.test.assertEquals + +internal class FormStateTest : BaseStateTest() { + private lateinit var state: FormState + + override fun doInit() { + state = FormState(context, EMPTY_STATE) + every { renderer.renderForm(any(), any()) } returns UI_STATE + } + + @Test + fun rendersFormOnStart() = test { + state.start(stateMachine) + verify { + renderer.renderForm(EMPTY_STATE, isValid = false) + stateMachine.setUiState(UI_STATE) + } + } + + @Test + fun processesUsernameChanged() = test { + state.start(stateMachine) + state.process(USERNAME_CHANGED) + + val expectedData = EMPTY_STATE.copy(username = USERNAME_CHANGED.value) + verify(mode = VerifyMode.order) { + renderer.renderForm(EMPTY_STATE, isValid = false) + renderer.renderForm(expectedData, isValid = false) + } + assertEquals(USERNAME_CHANGED.value, state.data.username) + } + + @Test + fun processesPasswordChanged() = test { + state.start(stateMachine) + state.process(PASSWORD_CHANGED) + + val expectedData = EMPTY_STATE.copy(password = PASSWORD_CHANGED.value) + verify(mode = VerifyMode.order) { + renderer.renderForm(EMPTY_STATE, isValid = false) + renderer.renderForm(expectedData, isValid = false) + } + assertEquals(PASSWORD_CHANGED.value, state.data.password) + } + + @Test + fun processesAction() = test { + every { stateFactory.authenticating(any()) } returns nextState + state.data = VALID_FORM_STATE + + state.start(stateMachine) + state.process(AuthGestureImpl.Action) + + verify { + renderer.renderForm(VALID_FORM_STATE, isValid = true) + stateFactory.authenticating(VALID_FORM_STATE) + stateMachine.setMachineState(nextState) + } + } + + @Test + fun doesNotProcessActionIfInvalid() = test { + state.start(stateMachine) + state.process(AuthGestureImpl.Action) + + verify(VerifyMode.exactly(0)) { + stateFactory.authenticating(any()) + stateMachine.setMachineState(any()) + } + } + + @Test + fun processesBack() = test { + every { stateFactory.terminated(any()) } returns nextState + + state.start(stateMachine) + state.process(AuthGestureImpl.Back) + + verify { + stateFactory.terminated(AuthResult(authenticated = false)) + stateMachine.setMachineState(nextState) + } + } + + @Test + fun processesSkip() = test { + every { stateFactory.terminated(any()) } returns nextState + + state.start(stateMachine) + state.process(AuthGestureImpl.Form.Skip) + + verify { + stateFactory.terminated(AuthResult(authenticated = false)) + stateMachine.setMachineState(nextState) + } + } + + @Test + fun doesNotProcessSkipIfNotSkippable() = test { + state.data = EMPTY_STATE.copy(input = EMPTY_STATE.input.copy(skippable = false)) + + state.start(stateMachine) + state.process(AuthGestureImpl.Form.Skip) + + verify(VerifyMode.exactly(0)) { + stateFactory.terminated(any()) + stateMachine.setMachineState(any()) + } + } +} diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/state/PreloadingErrorStateTest.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/state/PreloadingErrorStateTest.kt new file mode 100644 index 0000000..cc51dc9 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/state/PreloadingErrorStateTest.kt @@ -0,0 +1,91 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.FATAL_ERROR +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.INPUT +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.NON_FATAL_ERROR +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.UI_STATE +import com.motorro.commonstatemachine.skills.domain.exception.AppException +import dev.mokkery.answering.returns +import dev.mokkery.every +import dev.mokkery.matcher.any +import dev.mokkery.verify +import kotlin.test.Test + +internal class PreloadingErrorStateTest : BaseStateTest() { + override fun doInit() { + every { renderer.renderFullScreenError(any(), any()) } returns UI_STATE + } + + private fun createState(error: AppException) = PreloadingErrorState( + context, + INPUT, + error + ) + + @Test + fun rendersFatalErrorOnStart() = test { + val state = createState(FATAL_ERROR) + state.start(stateMachine) + verify { + renderer.renderFullScreenError(FATAL_ERROR, canRetry = false) + stateMachine.setUiState(UI_STATE) + } + } + + @Test + fun rendersNonFatalErrorOnStart() = test { + val state = createState(NON_FATAL_ERROR) + state.start(stateMachine) + verify { + renderer.renderFullScreenError(NON_FATAL_ERROR, canRetry = true) + stateMachine.setUiState(UI_STATE) + } + } + + @Test + fun terminatesOnBack() = test { + val state = createState(NON_FATAL_ERROR) + + every { stateFactory.terminated(any()) } returns nextState + + state.start(stateMachine) + state.process(AuthGestureImpl.Back) + + verify { + stateFactory.terminated(AuthResult(authenticated = false)) + stateMachine.setMachineState(nextState) + } + } + + @Test + fun terminatesOnActionIfCannotRetry() = test { + val state = createState(FATAL_ERROR) + + every { stateFactory.terminated(any()) } returns nextState + + state.start(stateMachine) + state.process(AuthGestureImpl.Action) + + verify { + stateFactory.terminated(AuthResult(authenticated = false)) + stateMachine.setMachineState(nextState) + } + } + + @Test + fun reloadsOnActionIfCanRetry() = test { + val state = createState(NON_FATAL_ERROR) + + every { stateFactory.preloading(any()) } returns nextState + + state.start(stateMachine) + state.process(AuthGestureImpl.Action) + + verify { + stateFactory.preloading(INPUT) + stateMachine.setMachineState(nextState) + } + } +} diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/state/PreloadingStateTest.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/state/PreloadingStateTest.kt new file mode 100644 index 0000000..3017c63 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/state/PreloadingStateTest.kt @@ -0,0 +1,82 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.FATAL_ERROR +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.INPUT +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.PASSWORD_REQUIREMENTS +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.UI_STATE +import com.motorro.commonstatemachine.skills.domain.authenticate.GetPasswordRequirements +import dev.mokkery.answering.calls +import dev.mokkery.answering.returns +import dev.mokkery.answering.throws +import dev.mokkery.every +import dev.mokkery.everySuspend +import dev.mokkery.matcher.any +import dev.mokkery.mock +import dev.mokkery.verify +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.test.Test + +internal class PreloadingStateTest : BaseStateTest() { + private lateinit var requirementsMock: GetPasswordRequirements + private lateinit var state: PreloadingState + + override fun doInit() { + requirementsMock = mock() + state = PreloadingState(context, INPUT, requirementsMock) + every { renderer.renderLoading() } returns UI_STATE + } + + @Test + fun rendersLoadingOnStart() = test { + everySuspend { requirementsMock() } calls { suspendCancellableCoroutine { } } + + state.start(stateMachine) + + verify { + renderer.renderLoading() + stateMachine.setUiState(UI_STATE) + } + } + + @Test + fun transfersToFormWhenLoaded() = test { + everySuspend { requirementsMock() } returns PASSWORD_REQUIREMENTS + every { stateFactory.form(any()) } returns nextState + + state.start(stateMachine) + + verify { + stateFactory.form(any()) + stateMachine.setMachineState(nextState) + } + } + + @Test + fun transfersToErrorWhenFailed() = test { + everySuspend { requirementsMock() } throws FATAL_ERROR + every { stateFactory.preloadError(any(), any()) } returns nextState + + state.start(stateMachine) + + verify { + stateFactory.preloadError(INPUT, any()) + stateMachine.setMachineState(nextState) + } + } + + @Test + fun terminatesOnBackWhileLoading() = test { + everySuspend { requirementsMock() } calls { suspendCancellableCoroutine { } } + every { stateFactory.terminated(any()) } returns nextState + + state.start(stateMachine) + state.process(AuthGestureImpl.Back) + + verify { + stateFactory.terminated(AuthResult(authenticated = false)) + stateMachine.setMachineState(nextState) + } + } +} diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/ui/AuthUiRendererImplTest.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/ui/AuthUiRendererImplTest.kt new file mode 100644 index 0000000..7e59579 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation-tests/ui/AuthUiRendererImplTest.kt @@ -0,0 +1,63 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.ui + +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.EMPTY_STATE +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.FATAL_ERROR +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.VALID_FORM_STATE +import kotlin.test.Test +import kotlin.test.assertEquals + +internal class AuthUiRendererImplTest { + private val renderer = AuthUiRendererImpl() + + @Test + fun rendersLoading() { + assertEquals(AuthUiStateImpl.Loading, renderer.renderLoading()) + } + + @Test + fun rendersForm() { + val uiState = renderer.renderForm(VALID_FORM_STATE, isValid = true) + val expected = AuthUiStateImpl.Form( + username = VALID_FORM_STATE.username, + password = VALID_FORM_STATE.password, + passwordRequirements = VALID_FORM_STATE.passwordRequirements.description, + loginEnabled = true, + canSkip = true + ) + assertEquals(expected, uiState) + } + + @Test + fun rendersFormInvalid() { + val uiState = renderer.renderForm(EMPTY_STATE, isValid = false) + val expected = AuthUiStateImpl.Form( + username = EMPTY_STATE.username, + password = EMPTY_STATE.password, + passwordRequirements = EMPTY_STATE.passwordRequirements.description, + loginEnabled = false, + canSkip = true + ) + assertEquals(expected, uiState) + } + + @Test + fun rendersFullScreenError() { + val uiState = renderer.renderFullScreenError(FATAL_ERROR, canRetry = false) + val expected = AuthUiStateImpl.Error( + message = FATAL_ERROR.message, + canRetry = false + ) + assertEquals(expected, uiState) + } + + @Test + fun rendersFullScreenErrorWithRetry() { + val uiState = renderer.renderFullScreenError(FATAL_ERROR, canRetry = true) + val expected = AuthUiStateImpl.Error( + message = FATAL_ERROR.message, + canRetry = true + ) + assertEquals(expected, uiState) + } +} diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/AuthDataApiImpl.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/AuthDataApiImpl.kt new file mode 100644 index 0000000..aa90a5e --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/AuthDataApiImpl.kt @@ -0,0 +1,35 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation + +import com.motorro.commonstatemachine.CommonMachineState +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthDataApi +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthGesture +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthInput +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthUiState +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.state.AuthStateFactory +import com.motorro.commonstatemachine.flow.data.CommonFlowHost +import org.koin.core.annotation.Factory +import org.koin.core.component.KoinComponent +import org.koin.core.component.get +import org.koin.core.parameter.parametersOf + +/** + * Data API implementation + */ +@Factory +internal class AuthDataApiImpl : AuthDataApi, KoinComponent { + + @Suppress("UNCHECKED_CAST") + override fun init(flowHost: CommonFlowHost, input: AuthInput): CommonMachineState { + val factory = this@AuthDataApiImpl.get { + parametersOf(flowHost) + } + return factory.preloading(input) as CommonMachineState + } + + override fun getDefaultUiState(): AuthUiState = AuthUiStateImpl.Loading + + override fun getBackGesture(): AuthGesture = AuthGestureImpl.Back +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/AuthModule.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/AuthModule.kt new file mode 100644 index 0000000..b646f31 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/AuthModule.kt @@ -0,0 +1,8 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +class AuthModule \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/AuthUiApiImpl.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/AuthUiApiImpl.kt new file mode 100644 index 0000000..a0ccad4 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/AuthUiApiImpl.kt @@ -0,0 +1,27 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthGesture +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthUiApi +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthUiState +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.ui.AuthScreen +import org.koin.core.annotation.Factory + +/** + * UI API implementation + */ +@Factory +internal class AuthUiApiImpl : AuthUiApi { + @Composable + override fun Screen( + state: AuthUiState, + onGesture: (AuthGesture) -> Unit, + modifier: Modifier + ) = AuthScreen( + state = state as AuthUiStateImpl, + modifier = modifier, + onGesture = onGesture + ) +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/data/AuthGestureImpl.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/data/AuthGestureImpl.kt new file mode 100644 index 0000000..ba1df8e --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/data/AuthGestureImpl.kt @@ -0,0 +1,38 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.data + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthGesture + +/** + * All gestures available in this feature + */ +internal sealed class AuthGestureImpl : AuthGesture { + /** + * Backwards navigation element clicked + */ + data object Back : AuthGestureImpl() + + /** + * Default call-to-action clicked + */ + data object Action : AuthGestureImpl() + + /** + * Gestures for the login form + */ + sealed class Form : AuthGestureImpl() { + /** + * Username input changed + */ + data class UsernameChanged(val value: String) : Form() + + /** + * Password input changed + */ + data class PasswordChanged(val value: String) : Form() + + /** + * Skips the authentication + */ + data object Skip : Form() + } +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/data/AuthStateData.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/data/AuthStateData.kt new file mode 100644 index 0000000..d8cf559 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/data/AuthStateData.kt @@ -0,0 +1,23 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.data + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthInput +import com.motorro.commonstatemachine.skills.domain.authenticate.data.PasswordRequirements + +/** + * Interstate data to pass between the machine states + * @property input Flow input from the flow initialization + * @property passwordRequirements Password requirements (preloaded later) + * @property username User input for the username is stored here + * @property password User input for the password is stored here + */ +internal data class AuthStateData( + val input: AuthInput, + val passwordRequirements: PasswordRequirements, + val username: String = "", + val password: String = "" +) + +/** + * Some utility function to check the form state + */ +internal fun AuthStateData.isValidToAuthenticate() = username.isNotBlank() && passwordRequirements.regex.matches(password) diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/data/AuthUiStateImpl.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/data/AuthUiStateImpl.kt new file mode 100644 index 0000000..5476939 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/data/AuthUiStateImpl.kt @@ -0,0 +1,36 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.data + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthUiState + +/** + * All gestures available in this feature + */ +internal sealed class AuthUiStateImpl : AuthUiState { + /** + * Loading data + */ + data object Loading : AuthUiStateImpl() + + /** + * Login form + * @property username Username + * @property password Password + * @property passwordRequirements Password requirements description + * @property loginEnabled If true, the form is complete and the user can log in + * @property canSkip If true, the user can skip the authentication + */ + data class Form( + val username: String, + val password: String, + val passwordRequirements: String, + val loginEnabled: Boolean, + val canSkip: Boolean + ) : AuthUiStateImpl() + + /** + * Displays error + * @property message Error message to display + * @property canRetry If true, the error is recoverable and can be retried + */ + data class Error(val message: String, val canRetry: Boolean) : AuthUiStateImpl() +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/AuthContext.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/AuthContext.kt new file mode 100644 index 0000000..21bc639 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/AuthContext.kt @@ -0,0 +1,25 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.ui.AuthUiRenderer +import com.motorro.commonstatemachine.flow.data.CommonFlowHost + +/** + * Common tools used by states + */ +internal interface AuthContext { + /** + * State-factory + */ + val factory: AuthStateFactory + + /** + * Flow-host + */ + val flowHost: CommonFlowHost + + /** + * UI-renderer + */ + val renderer: AuthUiRenderer +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/AuthState.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/AuthState.kt new file mode 100644 index 0000000..bcf46b6 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/AuthState.kt @@ -0,0 +1,10 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.CommonMachineState +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl + +/** + * Convenient type-alias - bounds gesture and ui state system + */ +internal typealias AuthState = CommonMachineState \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/AuthStateFactory.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/AuthStateFactory.kt new file mode 100644 index 0000000..5013652 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/AuthStateFactory.kt @@ -0,0 +1,54 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthInput +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthStateData +import com.motorro.commonstatemachine.skills.domain.exception.AppException + +/** + * State-factory interface + * Abstracts the machine state from the state creation + */ +internal interface AuthStateFactory { + /** + * Creates a flow preloading state. + * The interstate data will be created by the state. + * @param input Initialization params + */ + fun preloading(input: AuthInput): AuthState + + /** + * Creates a preload error state. + * Again, there's no interstate data here as the preloading failed. + * @param input Initialization params + * @param error Error that occurred during the initialization + */ + fun preloadError(input: AuthInput, error: AppException): AuthState + + /** + * Creates authentication form state. + * Now we have the interstate data, pass it as a parameter. + * @param data Interstate data + */ + fun form(data: AuthStateData): AuthState + + /** + * Creates the authentication state. + * @param data Interstate data + */ + fun authenticating(data: AuthStateData): AuthState + + /** + * Creates an authentication error state. + * @param data Interstate data + * @param error Error that occurred during the initialization + */ + fun authenticationError(data: AuthStateData, error: AppException): AuthState + + /** + * Creates the termination state that finishes the flow. + * All interstate data is lost as not needed anymore. + * @param result Flow result + */ + fun terminated(result: AuthResult): AuthState +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/AuthStateFactoryImpl.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/AuthStateFactoryImpl.kt new file mode 100644 index 0000000..35be8ff --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/AuthStateFactoryImpl.kt @@ -0,0 +1,84 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthInput +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthStateData +import com.motorro.commonstatemachine.examples.skills.auth.implementation.ui.AuthUiRenderer +import com.motorro.commonstatemachine.flow.data.CommonFlowHost +import com.motorro.commonstatemachine.skills.domain.exception.AppException +import io.github.aakira.napier.Napier +import org.koin.core.annotation.Factory +import org.koin.core.annotation.InjectedParam + +/** + * Main state-factory implementation + */ +@Factory +internal class AuthStateFactoryImpl( + private val preloadingStateFactory: PreloadingState.StateFactory, + private val authenticatingStateFactory: AuthenticatingState.StateFactory, + @InjectedParam flowHost: CommonFlowHost, + renderer: AuthUiRenderer +) : AuthStateFactory { + + /** + * Context implementation + */ + private val context = object : AuthContext { + override val factory: AuthStateFactory = this@AuthStateFactoryImpl + override val flowHost: CommonFlowHost = flowHost + override val renderer: AuthUiRenderer = renderer + } + + /** + * Creates preloading state with help of a preloading state factory + */ + override fun preloading(input: AuthInput) = preloadingStateFactory.create( + context, + input + ) + + /** + * Creates preload error state directly + */ + override fun preloadError(input: AuthInput, error: AppException) = PreloadingErrorState( + context, + input, + error + ) + + /** + * Creates authentication form state + */ + override fun form(data: AuthStateData) = FormState( + context, + data + ) + + /** + * Creates authenticating state + */ + override fun authenticating(data: AuthStateData): AuthState = authenticatingStateFactory.create( + context, + data + ) + + /** + * Creates authentication error state + */ + override fun authenticationError(data: AuthStateData, error: AppException) = AuthenticationErrorState( + context, + data, + error + ) + + /** + * Ad-hoc terminated state implementation + */ + override fun terminated(result: AuthResult) = object : AuthState() { + override fun doStart() { + Napier.d { "Terminating flow..." } + context.flowHost.onComplete(result) + } + } +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/AuthenticatingState.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/AuthenticatingState.kt new file mode 100644 index 0000000..f337087 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/AuthenticatingState.kt @@ -0,0 +1,81 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthStateData +import com.motorro.commonstatemachine.skills.domain.authenticate.AuthenticateWithPassword +import com.motorro.commonstatemachine.skills.domain.exception.toAppException +import com.motorro.commonstatemachine.skills.domain.session.data.Username +import io.github.aakira.napier.Napier +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch +import org.koin.core.annotation.Factory + +/** + * Authenticates the user + * @param context Auth context + * @param data Interstate data + * @param authenticate Authenticate usecase + */ +internal class AuthenticatingState( + context: AuthContext, + private val data: AuthStateData, + private val authenticate: AuthenticateWithPassword +) : BaseAuthState(context) { + + /** + * State start-up + */ + override fun doStart() { + setUiState { + renderLoading() + } + doAuthenticate() + } + + /** + * Gesture processing + */ + override fun doProcess(gesture: AuthGestureImpl) { + when(gesture) { + is AuthGestureImpl.Back -> setMachineState { + Napier.d { "Back gesture. Returning to form..." } + form(data) + } + else -> super.doProcess(gesture) + } + } + + /** + * Authentication function + */ + private fun doAuthenticate() = stateScope.launch { + try { + authenticate(Username(data.username), data.password) + // Advance to the next state + setMachineState { + terminated(AuthResult(authenticated = true)) + } + } catch (e: Throwable) { + ensureActive() + Napier.w(e) { "Error during authentication" } + // Advance to the error state + setMachineState { + authenticationError(data, e.toAppException()) + } + } + } + + /** + * State factory + * State dependencies are injected here + */ + @Factory + class StateFactory(private val authenticate: AuthenticateWithPassword) { + fun create(context: AuthContext, data: AuthStateData): AuthState = AuthenticatingState( + context = context, + data = data, + authenticate = authenticate + ) + } +} diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/AuthenticationErrorState.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/AuthenticationErrorState.kt new file mode 100644 index 0000000..772a972 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/AuthenticationErrorState.kt @@ -0,0 +1,53 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthStateData +import com.motorro.commonstatemachine.skills.domain.exception.AppException +import com.motorro.commonstatemachine.skills.domain.exception.IOException +import io.github.aakira.napier.Napier + +/** + * Handles authentication error + * If the error is recoverable (IOException), allows to transfer back to authenticating to retry. + * Otherwise returns to form. + * @param context Auth context + * @param data Interstate data + * @param error Error to handle + */ +internal class AuthenticationErrorState( + context: AuthContext, + private val data: AuthStateData, + private val error: AppException +) : BaseAuthState(context) { + override fun doStart() { + setUiState { + renderFullScreenError(error, canRetry()) + } + } + + private fun canRetry(): Boolean = when(error) { + is IOException -> true + else -> false + } + + override fun doProcess(gesture: AuthGestureImpl) { + when(gesture) { + is AuthGestureImpl.Back -> setMachineState { + Napier.d { "Back gesture. Returning to form..." } + form(data) + } + is AuthGestureImpl.Action -> if (canRetry()) { + Napier.d { "Action gesture. Retrying authentication..." } + setMachineState { + authenticating(data) + } + } else { + Napier.d { "Action gesture. Returning to form..." } + setMachineState { + form(data) + } + } + else -> super.doProcess(gesture) + } + } +} diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/BaseAuthState.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/BaseAuthState.kt new file mode 100644 index 0000000..267cf74 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/BaseAuthState.kt @@ -0,0 +1,34 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.coroutines.CoroutineState +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.ui.AuthUiRenderer +import io.github.aakira.napier.Napier + +/** + * Base class for the most logic states + */ +internal abstract class BaseAuthState(context: AuthContext) : CoroutineState(), AuthContext by context { + + /** + * Common gesture processing (logs a warning for an unexpected gesture) + */ + override fun doProcess(gesture: AuthGestureImpl) { + Napier.w { "Unsupported gesture: $gesture" } + } + + /** + * Convenience function to switch a machine state + */ + protected inline fun setMachineState(block: AuthStateFactory.() -> AuthState) { + setMachineState(block(factory)) + } + + /** + * Convenience function to render UI state + */ + protected inline fun setUiState(block: AuthUiRenderer.() -> AuthUiStateImpl) { + setUiState(renderer.block()) + } +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/FormState.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/FormState.kt new file mode 100644 index 0000000..c52effb --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/FormState.kt @@ -0,0 +1,75 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthStateData +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.isValidToAuthenticate +import io.github.aakira.napier.Napier +import org.jetbrains.annotations.VisibleForTesting + +/** + * Authentication form state - an interactive state that: + * - handles user input + * - updates the UI state + * - validates the input + * - updates the interstate data + * @param context Auth context + * @param data Interstate data. Visible for testing. + */ +internal class FormState( + context: AuthContext, + @get:VisibleForTesting var data: AuthStateData, +) : BaseAuthState(context) { + + /** + * State start-up + */ + override fun doStart() { + setUiState { + renderForm(data, data.isValidToAuthenticate()) + } + } + + /** + * Updates the interstate data and renders the form + */ + private inline fun updateData(block: AuthStateData.() -> AuthStateData) { + val oldData = data + val newData = data.block() + if (oldData != newData) { + data = newData + setUiState { + renderForm(newData, newData.isValidToAuthenticate()) + } + } + } + + /** + * Processes the gesture + */ + override fun doProcess(gesture: AuthGestureImpl) { + when(gesture) { + is AuthGestureImpl.Back -> setMachineState { + terminated(AuthResult(false)) + } + is AuthGestureImpl.Action -> if (data.isValidToAuthenticate()) { + Napier.d { "Action gesture. Authenticating..." } + setMachineState { + authenticating(data) + } + } + is AuthGestureImpl.Form.UsernameChanged -> updateData { + copy(username = gesture.value) + } + is AuthGestureImpl.Form.PasswordChanged -> updateData { + copy(password = gesture.value) + } + is AuthGestureImpl.Form.Skip -> if (data.input.skippable) { + Napier.d { "Skipping authentication..." } + setMachineState { + terminated(AuthResult(false)) + } + } + } + } +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/PreloadingErrorState.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/PreloadingErrorState.kt new file mode 100644 index 0000000..61db5fc --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/PreloadingErrorState.kt @@ -0,0 +1,52 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthInput +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.skills.domain.exception.AppException +import com.motorro.commonstatemachine.skills.domain.exception.IOException +import io.github.aakira.napier.Napier + +/** + * Handles preloading error + * If the error is recoverable, allows to transfer back to loading to retry, otherwise terminates the flow. + * @param context Auth context + * @param error Error to handle + */ +internal class PreloadingErrorState( + context: AuthContext, + private val input: AuthInput, + private val error: AppException +) : BaseAuthState(context) { + override fun doStart() { + setUiState { + renderFullScreenError(error, canRetry()) + } + } + + private fun canRetry(): Boolean = when(error) { + is IOException -> true + else -> false + } + + override fun doProcess(gesture: AuthGestureImpl) { + when(gesture) { + is AuthGestureImpl.Back -> setMachineState { + Napier.d { "Back gesture. Terminating flow..." } + terminated(AuthResult(false)) + } + is AuthGestureImpl.Action -> if (canRetry()) { + Napier.d { "Action gesture. Retrying initialization..." } + setMachineState { + preloading(input) + } + } else { + Napier.d { "Action gesture. Terminating flow..." } + setMachineState { + terminated(AuthResult(false)) + } + } + else -> super.doProcess(gesture) + } + } +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/PreloadingState.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/PreloadingState.kt new file mode 100644 index 0000000..63dd138 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/state/PreloadingState.kt @@ -0,0 +1,78 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthInput +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthStateData +import com.motorro.commonstatemachine.skills.domain.authenticate.GetPasswordRequirements +import com.motorro.commonstatemachine.skills.domain.exception.toAppException +import io.github.aakira.napier.Napier +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch +import org.koin.core.annotation.Factory + +/** + * Example of a state that preloads some data with the usecase + * and creates the valid [data-state](data-and-ui-implementation.md#step-3-interstate-data). + */ +internal class PreloadingState( + context: AuthContext, + private val input: AuthInput, + private val getPasswordRequirements: GetPasswordRequirements +) : BaseAuthState(context) { + + /** + * State start-up + */ + override fun doStart() { + setUiState { + renderLoading() + } + preload() + } + + /** + * Dummy gesture processing + */ + override fun doProcess(gesture: AuthGestureImpl) { + when(gesture) { + is AuthGestureImpl.Back -> setMachineState { + terminated(AuthResult(false)) + } + else -> super.doProcess(gesture) + } + } + + /** + * Dummy preloading function + */ + private fun preload() = stateScope.launch { + try { + val passwordRequirements = getPasswordRequirements() + // Advance to the next state + setMachineState { + form(AuthStateData(input, passwordRequirements)) + } + } catch (e: Throwable) { + ensureActive() + Napier.w(e) { "Error during initialization" } + // Advance to the error state + setMachineState { + preloadError(input, e.toAppException()) + } + } + } + + /** + * State factory + * State dependencies are injected here + */ + @Factory + class StateFactory(private val getPasswordRequirements: GetPasswordRequirements) { + fun create(context: AuthContext, input: AuthInput): AuthState = PreloadingState( + context = context, + input = input, + getPasswordRequirements = getPasswordRequirements + ) + } +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/ui/AuthErrorScreen.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/ui/AuthErrorScreen.kt new file mode 100644 index 0000000..fd8c8a1 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/ui/AuthErrorScreen.kt @@ -0,0 +1,79 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.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.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.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.motorro.commonstatemachine.examples.auth.appcore.ui.preview.SkillsPreviewComposition +import com.motorro.commonstatemachine.examples.auth.appcore.ui.theme.Dimensions +import com.motorro.commonstatemachine.examples.skills.auth.implementation.Res +import com.motorro.commonstatemachine.examples.skills.auth.implementation.btn_close +import com.motorro.commonstatemachine.examples.skills.auth.implementation.btn_retry +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl +import org.jetbrains.compose.resources.stringResource + +@Composable +internal fun AuthErrorScreen( + state: AuthUiStateImpl.Error, + modifier: Modifier = Modifier, + onGesture: (AuthGestureImpl) -> Unit +) { + Column( + modifier = modifier.fillMaxSize().padding(Dimensions.medium), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = state.message, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodyLarge + ) + Spacer(Modifier.height(Dimensions.medium)) + Button(onClick = { onGesture(AuthGestureImpl.Action) }) { + Text( + if (state.canRetry) { + stringResource(Res.string.btn_retry) + } else { + stringResource(Res.string.btn_close) + } + ) + } + } +} + +private class AuthErrorDataProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + AuthUiStateImpl.Error( + message = "A recoverable error occurred", + canRetry = true + ), + AuthUiStateImpl.Error( + message = "A non-recoverable error occurred", + canRetry = false + ) + ) +} + +@Preview +@Composable +private fun AuthErrorPreview(@PreviewParameter(AuthErrorDataProvider::class) state: AuthUiStateImpl.Error?) { + if (null == state) return + SkillsPreviewComposition { + AuthErrorScreen( + state = state, + modifier = Modifier.fillMaxSize() + ) {} + } +} diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/ui/AuthFormScreen.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/ui/AuthFormScreen.kt new file mode 100644 index 0000000..715a6e5 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/ui/AuthFormScreen.kt @@ -0,0 +1,122 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.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.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +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.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.motorro.commonstatemachine.examples.auth.appcore.ui.preview.SkillsPreviewComposition +import com.motorro.commonstatemachine.examples.auth.appcore.ui.theme.Dimensions +import com.motorro.commonstatemachine.examples.skills.auth.implementation.Res +import com.motorro.commonstatemachine.examples.skills.auth.implementation.btn_login +import com.motorro.commonstatemachine.examples.skills.auth.implementation.btn_skip +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.input_password +import com.motorro.commonstatemachine.examples.skills.auth.implementation.input_username +import org.jetbrains.compose.resources.stringResource + +@Composable +internal fun AuthFormScreen( + state: AuthUiStateImpl.Form, + modifier: Modifier = Modifier, + onGesture: (AuthGestureImpl) -> Unit, +) { + Column( + modifier = modifier.fillMaxSize().padding(Dimensions.medium), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + TextField( + value = state.username, + onValueChange = { onGesture(AuthGestureImpl.Form.UsernameChanged(it)) }, + label = { Text(stringResource(Res.string.input_username)) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true + ) + Spacer(Modifier.height(Dimensions.medium)) + TextField( + value = state.password, + onValueChange = { onGesture(AuthGestureImpl.Form.PasswordChanged(it)) }, + label = { Text(stringResource(Res.string.input_password)) }, + modifier = Modifier.fillMaxWidth(), + visualTransformation = PasswordVisualTransformation(), + singleLine = true + ) + Text( + text = state.passwordRequirements, + modifier = Modifier.fillMaxWidth().padding(top = Dimensions.small), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(Modifier.height(Dimensions.medium)) + Button( + onClick = { onGesture(AuthGestureImpl.Action) }, + enabled = state.loginEnabled, + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(Res.string.btn_login)) + } + if (state.canSkip) { + Spacer(Modifier.height(Dimensions.medium)) + OutlinedButton( + onClick = { onGesture(AuthGestureImpl.Form.Skip) }, + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(Res.string.btn_skip)) + } + } + } +} + +private class AuthFormDataProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + AuthUiStateImpl.Form( + username = "", + password = "", + passwordRequirements = "At least 8 characters", + loginEnabled = false, + canSkip = true + ), + AuthUiStateImpl.Form( + username = "user", + password = "password", + passwordRequirements = "At least 8 characters", + loginEnabled = true, + canSkip = true + ), + AuthUiStateImpl.Form( + username = "user", + password = "password", + passwordRequirements = "At least 8 characters", + loginEnabled = true, + canSkip = false + ) + ) +} + +@Preview +@Composable +private fun AuthFormPreview(@PreviewParameter(AuthFormDataProvider::class) state: AuthUiStateImpl.Form?) { + if (null == state) return + SkillsPreviewComposition { + AuthFormScreen( + state = state, + modifier = Modifier.fillMaxSize() + ) {} + } +} diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/ui/AuthLoadingScreen.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/ui/AuthLoadingScreen.kt new file mode 100644 index 0000000..90875c9 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/ui/AuthLoadingScreen.kt @@ -0,0 +1,21 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.ui + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.motorro.commonstatemachine.examples.auth.appcore.ui.design.SkillsLoading +import com.motorro.commonstatemachine.examples.auth.appcore.ui.preview.SkillsPreviewComposition + +@Composable +internal fun AuthLoadingScreen(modifier: Modifier = Modifier) { + SkillsLoading(modifier) +} + +@Preview +@Composable +private fun AuthLoadingPreview() { + SkillsPreviewComposition { + AuthLoadingScreen(Modifier.fillMaxSize()) + } +} diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/ui/AuthScreen.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/ui/AuthScreen.kt new file mode 100644 index 0000000..f2fbfd9 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/ui/AuthScreen.kt @@ -0,0 +1,68 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.ui + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.motorro.commonstatemachine.examples.auth.appcore.ui.preview.SkillsPreviewComposition +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl + +/** + * Authentication screen + * @param state UI state + * @param onGesture Gesture handler + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun AuthScreen( + state: AuthUiStateImpl, + modifier: Modifier = Modifier, + onGesture: (AuthGestureImpl) -> Unit, +) { + val modifier = modifier.fillMaxSize() + when(state) { + AuthUiStateImpl.Loading -> AuthLoadingScreen(modifier) + is AuthUiStateImpl.Error -> AuthErrorScreen( + state = state, + modifier = modifier, + onGesture = onGesture + ) + is AuthUiStateImpl.Form -> AuthFormScreen( + state = state, + modifier = modifier, + onGesture = onGesture + ) + } +} + +private class AuthScreenDataProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + AuthUiStateImpl.Loading, + AuthUiStateImpl.Form( + username = "user", + password = "password", + passwordRequirements = "At least 8 characters", + loginEnabled = true, + canSkip = true + ), + AuthUiStateImpl.Error( + message = "A recoverable error occurred", + canRetry = true + ) + ) +} + +@Preview +@Composable +private fun SkillsScreenPreview(@PreviewParameter(AuthScreenDataProvider::class) state: AuthUiStateImpl?) { + if (null == state) return + SkillsPreviewComposition { + AuthScreen( + state = state + ) {} + } +} diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/ui/AuthUiRenderer.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/ui/AuthUiRenderer.kt new file mode 100644 index 0000000..f117aba --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/ui/AuthUiRenderer.kt @@ -0,0 +1,33 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.ui + +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthStateData +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.commonstatemachine.skills.domain.exception.AppException + +/** + * Transforms the data state to the UI state + */ +internal interface AuthUiRenderer { + /** + * Renders loading. + * The result would be an object so we don't need interstate data here. + */ + fun renderLoading(): AuthUiStateImpl + + /** + * Renders login form. + * The data could be fully taken from the data state. + * @param data Data-state to transform + * @param isValid If the form is valid or not + */ + fun renderForm(data: AuthStateData, isValid: Boolean): AuthUiStateImpl + + /** + * Renders error that takes the full screen + * - doesn't require any data-state + * - requires an error and takes a message from it + * @param error Error to present to the user + * @param canRetry Error could be retried + */ + fun renderFullScreenError(error: AppException, canRetry: Boolean): AuthUiStateImpl +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/ui/AuthUiRendererImpl.kt b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/ui/AuthUiRendererImpl.kt new file mode 100644 index 0000000..8f4e0b7 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/ui/AuthUiRendererImpl.kt @@ -0,0 +1,28 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.ui + +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthStateData +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.commonstatemachine.skills.domain.exception.AppException +import org.koin.core.annotation.Factory + +/** + * UI renderer implementation + */ +@Factory +internal class AuthUiRendererImpl : AuthUiRenderer { + + override fun renderLoading(): AuthUiStateImpl = AuthUiStateImpl.Loading + + override fun renderForm(data: AuthStateData, isValid: Boolean) = AuthUiStateImpl.Form( + username = data.username, + password = data.password, + passwordRequirements = data.passwordRequirements.description, + loginEnabled = isValid, + canSkip = data.input.skippable + ) + + override fun renderFullScreenError(error: AppException, canRetry: Boolean) = AuthUiStateImpl.Error( + message = error.message, + canRetry = canRetry + ) +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/add-state/references/ARCHITECTURE.md b/skills/commonstatemachine/common-child-flow/add-state/references/ARCHITECTURE.md new file mode 100644 index 0000000..94621a6 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/references/ARCHITECTURE.md @@ -0,0 +1,38 @@ +# CommonStateMachine Architecture + +![Architecture Diagram](architecture.puml) + +## Core Mechanics +- **Machine State:** A self-contained logic unit. It handles **Gestures (G)** and produces **UI States (U)**. +- **Lifecycle:** + - `doStart()`: Called when state becomes active. Use for initialization or launching side effects. + - `doProcess(gesture)`: Processes user input. + - `doClear()`: Cleanup when leaving the state. +- **Transitions:** States call `setMachineState(nextState)` to move the machine forward. No external "router" is needed. +- **UI Updates:** States call `setUiState(newState)` to update the View. +- **State Machine:** The container for the current **Machine State**. + - It maintains the state transition logic. + - `FlowStateMachine` (standard implementation) exports the current **UI State** as a `StateFlow`. + - It provides the `process(gesture)` entry point for user interaction. +- **Navigation Inversion:** Navigation is a logical state transition within the state machine (Logic/ViewModel layer), not something the View drives. The user + doesn't navigate between screens - gestures (or a state's own work finishing) move the machine from one logical state to another, and whichever state is + active decides what to show by calling `setUiState`. Several logical states can therefore render the exact same UI State one after another - for example, a + preload step and an auth-check step can both just show a loading spinner - with nothing visibly changing until some state actually calls `setUiState` with + something new. The same decoupling is why a transition can carry live, non-serializable data (an open coroutine, a scope, an object reference) forward as a + plain constructor argument, unlike a screen-based navigation graph where arguments between destinations typically need to be serializable. See + [MVI Architecture with a State Machine: Basics](https://proandroiddev.com/mvi-architecture-with-a-state-machine-basics-721c5ebed893) for the full rationale. +- **ViewModel Integration:** The `ViewModel` (or equivalent lifecycle host) typically: + - Holds the `StateMachine` instance. + - Delegates user gestures to `stateMachine.process(gesture)`. + - Collects and exposes `stateMachine.uiState`. + - Calls `stateMachine.clear()` in its `onCleared()` lifecycle method to clean up resources. + +## Extra Components-Helpers +- **State Factory:** Decouples state creation from the states themselves. This allows for easy dependency injection and makes unit testing transitions simple + by mocking the factory. +- **UI Renderer:** A pure function or class that transforms the internal **Data State** into a **UI State**. This separates the business logic of "what to do" + from the presentation logic of "what to show." +- **State Context:** A common interface passed to all states in a machine. It provides access to shared tools like the State Factory, UI Renderer, + and Feature Hosts, reducing constructor boilerplate. +- **Data State:** A simple data class that holds the cumulative state of the flow (e.g., entered username, password, validation errors). It is passed between + states to maintain continuity. diff --git a/skills/commonstatemachine/common-child-flow/add-state/references/COMMON-CHILD-FLOW.md b/skills/commonstatemachine/common-child-flow/add-state/references/COMMON-CHILD-FLOW.md new file mode 100644 index 0000000..655ed24 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/references/COMMON-CHILD-FLOW.md @@ -0,0 +1,41 @@ +# Common Child Flow + +![Common Child Flow Diagram](common-child-flow.puml) + +## Philosophy and Principles +- **Modularity via Proxying:** Features are independent modules. A parent flow hosts a child flow using `ProxyMachineState`, which bridges incompatible Gesture/UI systems. +- **Direct Data Flow:** Transitions occur in memory. Complex data or even running coroutines can be passed between states without serialization or "parceling" - + unlike a screen-based navigation library (e.g. Jetpack Navigation), where arguments passed between destinations typically need to be serializable. This + follows from the same [Navigation Inversion](ARCHITECTURE.md) principle the base state machine already relies on. +- **KMP Ready:** Logic and Data are isolated from the View, making the core feature logic easily shareable across platforms. +- **Standardized Child Flows:** + - `commonflow-data`: Standardizes flow initialization and result handling. + - `commonflow-compose`: Provides a unified way to inject UI into a composition. + - `commonflow-viewmodel`: Bridges the state machine to the platform's Lifecycle and Navigation frameworks. + +## Data API +Data API is defined in the `commonflow-data` module and consists of two primary interfaces: + +### `CommonFlowDataApi` +This is the **Feature Entry Point**. It is defined in the feature module and provides the host with everything needed to launch the child flow. +- **`init(flowHost: CommonFlowHost, input: I)`**: Initializes the child flow and returns the first `MachineState`. It injects the `flowHost` so + the child can signal when it's done. +- **`getDefaultUiState()`**: Provides the initial state (like "Loading") to be displayed while the child machine is starting up. +- **`getBackGesture()`**: Defines which gesture represents "Back" in the feature, allowing the host to bridge system back-presses to the child logic. + +### `CommonFlowHost` +This is the **Communication Bridge**. It is implemented by the parent's `ProxyMachineState` and passed to the child flow. +- **`onComplete(result: R)`**: The child flow calls this method when it reaches a terminal state. The host implementation then receives the `result` and handles + the transition to the next part of the parent flow. + +## UI API +UI API is defined in the `commonflow-compose` module and consists of the following interface: + +### `CommonFlowUiApi` +This interface provides the **Feature View**. It is implemented in the feature module using `Compose` and is used by the host to render the child flow's UI. + +- **`Screen(state: U, onGesture: (G) -> Unit, modifier: Modifier)`**: A `@Composable` function that takes the current feature **UI State (U)** + and a gesture handler for feature **Gestures (G)**. It encapsulates all the UI logic for the feature, allowing the host to simply plug it into its composition. + +This separation ensures that the host doesn't need to know about the internal UI components of the child flow, only how to provide the state and handle +the gestures, which are bridged by the `ProxyMachineState`. diff --git a/skills/commonstatemachine/common-child-flow/add-state/references/architecture.puml b/skills/commonstatemachine/common-child-flow/add-state/references/architecture.puml new file mode 100644 index 0000000..4d4db8c --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/references/architecture.puml @@ -0,0 +1,51 @@ +@startuml +'https://plantuml.com/class-diagram + +hide empty members + +interface CommonStateMachine { + + process(gesture: G) + + clear() +} + +abstract class CommonMachineState { + + {abstract} doStart() + + {abstract} doProcess(gesture: G) + + {abstract} doClear() + # setMachineState(next: CommonMachineState) + # setUiState(ui: U) +} + +class FlowStateMachine implements CommonStateMachine { + + uiState: StateFlow +} + +class ViewModel { + - stateMachine: CommonStateMachine + + uiState: StateFlow + + process(gesture: G) +} + +interface StateFactory { + + createNextState(...): CommonMachineState +} + +interface UiRenderer { + + render(data: D): U +} + +interface StateContext { + + factory: StateFactory + + renderer: UiRenderer +} + +class DataState << (D, #FF7700) DataClass >> + +CommonStateMachine "1" *-- "1" CommonMachineState : current +ViewModel "1" *-- "1" CommonStateMachine +CommonMachineState ..> CommonMachineState : transitions +CommonMachineState -right-> StateContext : uses +StateContext "1" *-- "1" StateFactory +StateContext "1" *-- "1" UiRenderer +UiRenderer ..> DataState : renders +@enduml diff --git a/skills/commonstatemachine/common-child-flow/add-state/references/base-state-implementation.md b/skills/commonstatemachine/common-child-flow/add-state/references/base-state-implementation.md new file mode 100644 index 0000000..a78ef49 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/references/base-state-implementation.md @@ -0,0 +1,39 @@ +# Module implementation: base state +Use this reference to implement a common base state for the flow. +Take a look at the [class diagram](common-child-flow.puml) to get the idea about components purpose. +Take a look at the [example](../assets/example/implementation/state/BaseAuthState.kt). + +## Step 1. Create a base class +Create a base abstract class that would be used as a common ancestor for the most of the logical states. +Follow the following common rules: + +- Make the class internal to the implementation module. +- The name should start with `Base` prefix. +- The class should be abstract +- The class should extend the `com.motorro.commonstatemachine.coroutines.CoroutineState` +- The class should bind the generic parameters for the [gesture](data-and-ui-implementation.md#step-1-ui-gestures) and [ui-state](data-and-ui-implementation.md#step-2-ui-states) types. +- The class should take the [context](renderer-factory-context-interface-implementation.md#step-4-state-context) parameter and implement the context by delegate. + +## Step 2. Add convenience methods to change the machine and UI states +The `com.motorro.commonstatemachine.CommonMachineState` subclass is a part of a state-machine. +It updates the outside world with the following methods: + +- `setUiState` - updates the current UI state, thus the screen to the user. +- `setMachineState` - switches the logical state in a hosting state-machine. + +To reduce the boilerplate, create two protected inline convenience methods: + +- `setMachineState(block: AuthStateFactory.() -> AuthState)` - runs the block on the factory and changes the machine state. +- `setUiState(block: AuthUiRenderer.() -> AuthUiStateImpl)` - runs the renderer and updates the UI state + +Take a look at the [example](../assets/example/implementation/state/BaseAuthState.kt) for the implementation. + +## Step 3. Basic gesture processing +The `com.motorro.commonstatemachine.CommonMachineState` processes the user gestures in the `doProcess` method. +The concrete state usually only processes those gestures it is interested in. +Example: the login form state is interested only in gestures that are relevant to login actions, it does not process +gestures of the logout screen. +If the project uses the logging framework, add a simple `doProcess` override to the base class: if the state gets the +unexpected gesture - log a warning message using the logging framework. + +Take a look at the [example](../assets/example/implementation/state/BaseAuthState.kt) for the implementation. diff --git a/skills/commonstatemachine/common-child-flow/add-state/references/base-test-class-implementation.md b/skills/commonstatemachine/common-child-flow/add-state/references/base-test-class-implementation.md new file mode 100644 index 0000000..35816f9 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/references/base-test-class-implementation.md @@ -0,0 +1,44 @@ +# Module implementation: base test class +Use this reference to get the idea snd create a base abstract uni-test class. +This class will handle common test-mocks and initialization for the most of machine-states unit-tests: + +- state-machine mock +- factory mock +- renderer mock +- context mock + +Most of the machine-state test will subclass this base class. + +## Step 1. Base test class +Create an abstract internal class and call `BaseStateTest`. + +## Step 2. Protected properties +Create the following protected properties to use in child test-classes: + +- `stateMachine` - `com.motorro.commonstatemachine.CommonStateMachine` mock bound with [implementation gesture and ui-state types](data-and-ui-implementation.md) +- `stateFactory` - [machine state factory mock](renderer-factory-context-interface-implementation.md#step-2-state-factory-interface) +- `renderer` - [ui state renderer mock](renderer-factory-context-interface-implementation.md#step-3-ui-renderer-interface) +- `flowHost` - `com.motorro.commonstatemachine.flow.data.CommonFlowHost` mock bound with the return type +- `context` - [interstate context mock](renderer-factory-context-interface-implementation.md#step-4-state-context) +- `nextState` - a bound `com.motorro.commonstatemachine.CommonMachineState` type to use with the state-factory mock. +- `dispatcher` - a coroutine `TestDispatcher` that will be used in test functions. + +## Step 3. Mock initialization +You will need to initialize all the mocks listed in the previous state and mock some of the functions there: + +1. Create the `init` function and mark it with the annotation to run before the test run. +2. Initialize `dispatcher` with the `UnconfinedTestDispatcher` if not instructed otherwise. +3. Initialize coroutines `Main` dispatcher with the test dispatcher from the previous step. +4. Initialize the `stateMachine` with the relaxed mock. +5. Initialize the `stateFactory` with the strict mock. +6. Initialize the `flowHost` with the relaxed mock. +7. Initialize the `renderer` with the strict mock. +8. Initialize the `nextState` with the bound type alias. +9. Initialize the `context` with the object and override the properties with the mocks from the previous steps. +10. Create the empty open fun `doInit` and call it from the `init` function. Subclasses will override `doInit` + and initialize themselves with the mocks already created. +11. Create `deinit` function and call `Dispatchers.resetMain()` there. +12. Create the common coroutine test function wrapping `runTest` and using the test dispatcher from the second step. + Subclasses will use this function to run the tests. + +Take a look at the [example](../assets/example/implementation-tests/state/BaseStateTest.kt) of such a class. diff --git a/skills/commonstatemachine/common-child-flow/add-state/references/common-child-flow.puml b/skills/commonstatemachine/common-child-flow/add-state/references/common-child-flow.puml new file mode 100644 index 0000000..284a449 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/references/common-child-flow.puml @@ -0,0 +1,187 @@ +@startuml +'https://plantuml.com/class-diagram + +hide empty members + +interface CommonFlowDataApi +interface CommonFlowUiApi +abstract class CommonMachineState +abstract class CoroutineState + +CoroutineState -up-|> CommonMachineState + +package API { + interface AuthGesture + note left + Marker intent (gesture) interface for Auth feature + May include public properties available to the API client + end note + interface AuthUiState + note right + Marker UI-state interface for Auth feature + May include public properties available to the API client + end note + + class AuthInput + class AuthResult + + interface AuthDataApi { + +init(flowHost: CommonFlowHost, input: AuthInput): CommonMachineState + +getDefaultUiState(): AuthUiState + +getBackGesture(): AuthGesture? = null + } + AuthDataApi -up-|> CommonFlowDataApi: implements + AuthDataApi -down-> AuthGesture: driven by + AuthDataApi -down-> AuthUiState: displays + AuthDataApi -right-> AuthResult: returns + AuthInput -> AuthDataApi: initializes + + interface AuthUiApi { + +Screen(state: AuthUiState, onGesture: (AuthGesture) -> Unit, modifier: Modifier = Modifier) + } + AuthUiApi -up-|> CommonFlowUiApi: implements + AuthUiApi -down-> AuthGesture: emits + AuthUiApi -down-> AuthUiState: displays +} + +package Implementation { + package data { + interface AuthGestureImpl + note left + A sealed structure of all available gestures (UI intents) for the feature + end note + + object Back + note bottom + Example: Back navigation button + end note + + object Action + note bottom + Example: Main action button + end note + AuthGestureImpl -up-|> AuthGesture + AuthGestureImpl <|-down- Back + AuthGestureImpl <|-down- Action + + interface AuthUiStateImpl + note right + A sealed structure of all possible UI states for the feature + end note + + object Loading + note bottom + Example: Loading screen + end note + + class Form { + +username: String + +password: String + } + note bottom + Example: Login form + end note + AuthUiStateImpl -up-|> AuthUiState + AuthUiStateImpl <|-down- Loading + AuthUiStateImpl <|-down- Form + + class AuthStateData { + +username: String + +password: String + } + note right + A flow data state passed between machine states. + Stores inter-state data + end note + } + + package state { + interface AuthState + note top + Type-alias or an interface of the machine state + end note + AuthState -up-|> CommonMachineState + + abstract class BaseAuthState + note top + Common base class for all machine states + end note + BaseAuthState -up-|> CoroutineState + BaseAuthState -|> AuthContext: delegates implementation + + class LoadingState + LoadingState -up-|> BaseAuthState + note bottom + Example: sample machine state - Loading + end note + + class FormState + FormState -up-|> BaseAuthState + note bottom + Example: sample machine state - Login form + end note + + interface AuthStateFactory { + +startup(input: AuthInput): AuthState + +form(data: AuthStateData): AuthState + +terminated(result: AuthResult): AuthState + } + AuthStateFactory -> AuthState: creates + note left + Abstracts the state from another state creation + end note + + class AuthStateFactoryImpl + note bottom + State factory implementation + end note + AuthStateFactoryImpl -up-|> AuthStateFactory + AuthStateFactory -> BaseAuthState: creates + + interface AuthContext { + +factory: AuthStateFactory + +flowHost: CommonFlowHost + +renderer: AuthUiRenderer + } + note bottom + State-machine context available to every state + Put immutable links to common tools here: + - factory + - flow host + - etc + end note + } + + package ui { + interface AuthUiRenderer { + +renderLoading(): AuthUiState + +renderForm(data: AuthStateData): AuthUiState + +renderError(error: Throwable): AuthUiState + } + note top + Transforms data-state to AuthUiState. + Abstracts machine state from the interface data rendering + end note + AuthUiRenderer -> AuthStateData: renders + AuthUiRenderer -> AuthUiStateImpl: creates + + class AuthUiRendererImpl + AuthUiRendererImpl -up-|> AuthUiRenderer + AuthUiRendererImpl -> AuthUiStateImpl: creates + } + + class AuthDataApiImpl + note bottom + Data API implementation + Injects factory and initializes the flow by calling `init` + end note + AuthDataApiImpl -up-|> AuthDataApi + + class AuthUiApiImpl + note bottom + UI API implementation + Builds the view with Compose components inside the module + end note + AuthUiApiImpl -up-|> AuthUiApi +} +@enduml \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/add-state/references/data-and-ui-implementation.md b/skills/commonstatemachine/common-child-flow/add-state/references/data-and-ui-implementation.md new file mode 100644 index 0000000..a784446 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/references/data-and-ui-implementation.md @@ -0,0 +1,76 @@ +# Module implementation: data classes +Use this reference to create data classes for the feature implementation. +Take a look at the [class diagram](common-child-flow.puml) to get the idea about the data types within the module. + +## Step 1. UI gestures +Create the sealed class to enumerate all the UI events (gestures) the user could send to the model: + +- button presses +- navigation elements clicks +- input changes + +The created sealed class should implement the base Gesture type defined in the API module. +See the [corresponding instruction](gesture-and-ui-state-api.md#step-1-gestures). +Put the created class to the `data` sub-package of the implementation module. + +Follow these common rules: + +- Make the created structure internal. +- Create data objects for gestures that don't carry any data. Example: user clicks the Back button. +- Create data classes for gestures that carry some data. Example: user changes an input text and the new value is passed with the event. +- Always include the data object for the backwards navigation gesture: `Back`. +- If the user didn't give you any information on the input gestures they want, `Back` would be the only child object in a structure. +- If user gives you a picture of a user interface - inspect it and create gestures for every screen. +- If the feature includes multiple screens - group the specific gestures by screen. +- The types of the properties of the ui-gestures should be immutable! + +Take a look at the [example](../assets/example/implementation/data/AuthGestureImpl.kt) - gesture collection. + +## Step 2. UI states +Create the sealed class to enumerate all the UI states the interface could display. + +The created sealed class should implement the base UI-state defined in the API module. +See the [corresponding instruction](gesture-and-ui-state-api.md#step-2-ui-states). +Put the created class to the `data` sub-package of the implementation module. + +Follow these common rules: + +- Make the created structure internal. +- Create data objects for ui-states that don't carry any data. Example: loading screen without any text messages. +- Create data classes for ui-states that carry some data. Example: login/password entry form. +- If user gives you a picture of a user interface - inspect it and create the data state for every dynamic UI element. +- If the feature includes multiple screens - group the specific ui-states by screen. +- The types of the properties of the ui-states should be immutable! Use `@Immutable` annotation for non-stable types to help Compose compiler optimize recompositions. + If immutable collection library is used in a project - use them for the collections in UI-states. + +Take a look at the [example](../assets/example/implementation/data/AuthUiStateImpl.kt) - UI-state collection. + +## Step 3. Interstate data +Create a data class to store the flow data passed between the machine states. +Follow the user's instructions to create a data class properties. +Put the created class to the `data` sub-package of the implementation module. + +Follow these common rules: + +- Make the created data class internal. +- The types of the properties of the data-state should be immutable! +- Include the flow startup input as a property. +- Set default values for the properties if user provides the information or try to guess them. + +Take a look at the [example](../assets/example/implementation/data/AuthStateData.kt) - interstate data for password authentication. + +Important: If you create any utility function based on the interstate data (like `isValidToAuthenticate` in the example above), +add a [unit test](../assets/example/implementation-tests/data/AuthStateDataTest.kt) for it. + +## Step 4. Test fixtures +Create some fixtures and test data for the created data classes: + +1. Create a `data` sub-package in the test source set. +2. Create a fixture file for data-class gestures. +3. Create a fixture file for data-class ui-states. +4. Create a fixture file for data-class interstate data. + For the interstate data, scan the project for already defined fixtures available at the class path and + prefer using them instead of creating your own fixtures + + +Take a look at the [example](../assets/example/implementation/data/AuthStateData.kt) - interstate data for password authentication. diff --git a/skills/commonstatemachine/common-child-flow/add-state/references/gesture-and-ui-state-api.md b/skills/commonstatemachine/common-child-flow/add-state/references/gesture-and-ui-state-api.md new file mode 100644 index 0000000..69384b6 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/references/gesture-and-ui-state-api.md @@ -0,0 +1,23 @@ +# Module API: Gesture and UI-state +Use this reference to create the UI gesture and UI state API. + +## Mechanics +The framework behind is a Model-View-Intent architecture, so: + +- The state is updated as a result of user intents (gestures) +- The view-state is fully described by data classes/objects - ui-states + +## Step 1. Gestures +The implementation is hidden from the module consumer using the internal gesture structures. +To use the feature we export the basic marker interface that may only contain some common public properties. +This interface would be implemented later in the implementation module. +Take a look at the [class diagram](common-child-flow.puml) for more details. +Take a look at the [example](../assets/example/api/AuthGesture.kt) - common API definition + +## Step 2. UI-states +The implementation is hidden from the module consumer using the internal ui-state structures. +To use the feature we export the basic marker interface that may only contain some common public properties. +This interface would be implemented later in the implementation module. +Take a look at the [class diagram](common-child-flow.puml) for more details. +Take a look at the [example](../assets/example/api/AuthUiState.kt) - common API definition + diff --git a/skills/commonstatemachine/common-child-flow/add-state/references/input-and-result-api.md b/skills/commonstatemachine/common-child-flow/add-state/references/input-and-result-api.md new file mode 100644 index 0000000..44a6ce3 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/references/input-and-result-api.md @@ -0,0 +1,22 @@ +# Module API: Module initialization data and flow result +Use this reference to create the required input data and define the flow result. + +## Mechanics +The [Common Child Flow](https://github.com/motorro/CommonStateMachine/tree/master#common-child-flow-api) runs a +child state-machine flow in a parent state-machine to get some result, display some UI, etc. +Example: the main application flow could run a child flow in-place to log the user-in. + +- The child flow could be initialized by some data passed from parent. +- The child flow could return some data back to the parent on completion. + +## Step 1. Input +If the user asks to provide any input - create an immutable type - a data class or a sealed structure. +If no input type explicitly required - confirm and use `Unit` +Take a look at the [class diagram](common-child-flow.puml) for more details. +Take a look at the [example](../assets/example/api/AuthInput.kt) - common API definition + +## Step 2. Result +If the user asks to provide a flow result - create an immutable type - a data class or a sealed structure. +If no result type explicitly required - confirm and use `Unit` +Take a look at the [class diagram](common-child-flow.puml) for more details. +Take a look at the [example](../assets/example/api/AuthResult.kt) - common API definition diff --git a/skills/commonstatemachine/common-child-flow/add-state/references/renderer-factory-context-interface-implementation.md b/skills/commonstatemachine/common-child-flow/add-state/references/renderer-factory-context-interface-implementation.md new file mode 100644 index 0000000..9fe585a --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/references/renderer-factory-context-interface-implementation.md @@ -0,0 +1,85 @@ +# Module implementation: factory interface, ui-renderer interface, context +Use this reference to create auxiliary interfaces and classes: + +- state factory +- UI-state renderer +- Interstate context + +Take a look at the [class diagram](common-child-flow.puml) to get the idea about components purpose. + +## Step 1. CommonMachineState binding +The framework main building block is a `com.motorro.commonstatemachine.CommonMachineState` - it is a logical state of a state-machine running the flow. +The generic class should be bound with two parameters: + +- [Gesture](data-and-ui-implementation.md#step-1-ui-gestures) +- [UI-state](data-and-ui-implementation.md#step-2-ui-states) + +Create an internal type-alias and put it to the `state` sub-package. +Take a look at the [example](../assets/example/implementation/state/AuthState.kt). + +## Step 2. State factory interface +Whenever it is a time to switch to another state, the current state does not create the new state directly - it delegates +the state creation to the state-factory: + +- State-factory foundation is an interface with the methods like: `createLoading`, `createForm`, etc. +- Factory functions may pass some parameters: [interstate data](data-and-ui-implementation.md#step-3-interstate-data), errors, auxiliary data. +- Machine-states get the reference to the state factory and use it to create the new state. +- State-factory interface is internal in the module. + +Your task here is to: + +- Create a state-factory internal interface +- Put the interface `state` sub-package of the implementation module. +- Every factory function should return the `CommonMachineState` binding type created in the previous step. +- Create some startup function to build the initializing state. The method should include the [input](input-and-result-api.md#step-1-input) as a parameter. +- Create `terminated` method with the [result](input-and-result-api.md#step-2-result) as a parameter. + +Take a look at the [example](../assets/example/implementation/state/AuthState.kt). + +## Step 3. UI renderer interface +States are encouraged to use a separate renderer interface that translates the [interstate data](data-and-ui-implementation.md#step-3-interstate-data) +to the [UI state](data-and-ui-implementation.md#step-2-ui-states). + +Follow these common rules: +- Make the created interface internal. +- Put the interface into the `ui` sub-package of the implementation module. +- Create a function for every UI-state in a sealed system. +- Every function should return the sealed class ui-state type. +- Provide an [interstate data](data-and-ui-implementation.md#step-3-interstate-data) parameter if the result state is a data class. +- Provide more parameters if data is required to build the ui-state, and it could not be built using the data-state. + Example: when creating some error view-state, add an error parameter. The calling state will provide it separately. + +Take a look at the [example](../assets/example/implementation/ui/AuthUiRenderer.kt). + +## Step 4. State context +To make the tools created above accessible, create an accessor interface - a context. +The state being created will receive that context in a constructor and use it to access factory, renderer, etc. + +Follow these common rules: +- Make the created interface internal. +- Put the interface into the `state` sub-package of the implementation module. +- Add properties resolving to the following tools: + - state factory + - ui-renderer + - flow host bound with the [result type](input-and-result-api.md#step-2-result) used to interact with the parent state. + +Take a look at the [example](../assets/example/implementation/state/AuthContext.kt). + +## Step 5. State factory implementation +Add an implementation class to implement the factory interface from the previous step. +The implementation will: + +- Receive state-factories in constructor and use them to create the states with complex dependencies (see the `preloading` implementation in the [example](../assets/example/implementation/state/AuthStateFactoryImpl.kt)). +- Create simple states locally (see the `preloadError` implementation in the [example](../assets/example/implementation/state/AuthStateFactoryImpl.kt)) +- For the even simpler states you may want to implement them ad-hoc (see the `terminated` implementation in the [example](../assets/example/implementation/state/AuthStateFactoryImpl.kt)). + +Follow these common rules: + +- Create the class in the implementation module. +- If required by the DI framework used in project, add required inject annotations. +- Inject external factories and dependencies required by the state to the factory constructor. + Try to use lazy provider injection to delay the initialization. +- Pass the flow host to the factory constructor. +- Build the private context object to pass to the child states. + +See the [example](../assets/example/implementation/state/AuthStateFactoryImpl.kt). diff --git a/skills/commonstatemachine/common-child-flow/add-state/references/state-implementation.md b/skills/commonstatemachine/common-child-flow/add-state/references/state-implementation.md new file mode 100644 index 0000000..1308d91 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/references/state-implementation.md @@ -0,0 +1,102 @@ +# Module implementation: state implementation +Use this reference to implement the machine-state(s) for the `CommonStateMachine` framework. +Take a look at the [class diagram](common-child-flow.puml) to get the idea about components purpose. +Take a look at the [example](../assets/example/implementation/state/PreloadingState.kt). + +## Mechanics +Here's how the machine-state works: + +- All machine states subclass the basic `com.motorro.commonstatemachine.CommonMachineState` (or its child) bound with the gesture and UI-state types + as described in the basic [interface implementation](renderer-factory-context-interface-implementation.md#step-1-commonmachinestate-binding) + and [base state implementation](base-state-implementation.md) manuals. +- It'd be great if a state has a single responsibility whenever possible and limit the number of gestures it processes. + For example, the document loading could be divided into the following states: + - `LoadingState` - runs the usecase and updates the interstate data. + - `ContentState` - displays loaded data and interacts with a user. + - `ErrorState` - displays an error and if possible, offers to retry by transferring back to `LoadingState`. +- The state has the following callbacks which are called by the hosting state machine: + - `doStart` - called when the state is started. + - `doProcess` - called when the state receives a gesture. + - `doClear` - called when the state is removed from the state-machine. +- The state could call the following functions to interact with the hosting state machine: + - `setMachineState` - called to switch to another state. We tipically use the factory to create the new state. + - `setUiState` - called to update the UI state. We tipically use the renderer to update the UI. +- It is important that `setMachineState` and `setUiState` are called from when state is active (between calls to `doStart` and `doClear`). + If the call is made from the inactive state - it will fail. +- Always postpone the heavy initialization till `doStart` is called. + +## Step 1. Create a class +Put the class to the `state` sub-package of the implementation module. +Follow the following common rules: + +- Make the class internal to the implementation module. +- Subclass the [base state](base-state-implementation.md). +- Pass the [context](renderer-factory-context-interface-implementation.md#step-4-state-context) as a first parameter. +- Pass the [input data](input-and-result-api.md#step-1-input) as a parameter. + +## Step 2. Add a startup implementation (or a dummy) +When the `com.motorro.commonstatemachine.CommonMachineState` is started, the state-machine calls the `doStart` lifecycle method. +Implement the method to update the UI, start loading data, etc. +The `com.motorro.commonstatemachine.coroutines.CoroutineState`, which is an ancestor of the [abstract base state](base-state-implementation.md), has a +bound coroutine scope property: `stateScope`. Use it to run coroutines. The scope would be canceled when the state is removed from the state-machine. + +## Step 3. Add a gesture processing implementation +When the user interface emits a gesture, the state-machine calls the `onGesture` method of a current `com.motorro.commonstatemachine.CommonMachineState`. +Implement the override of a function that: + +- Will process the gestures specific to this state. +- Will call the super-method on every other unexpected gesture which are out of scope of this state. + +## Step 4. Implement a local state factory +To increase the visibility and to decrease the boilerplate, the states that require external dependencies (use-cases, managers, APIs, etc.) have their +own state factories put to the DI graph. This lifts a burden of state creation micromanagement away from +the main state factory implementation. +Create such a factory for the startup state: + +- Implement as a child class with the name `StateFactory`. +- If required by the DI framework used in project, add required inject annotations. +- Inject external dependencies required by the state to the factory constructor. +- Add a single `create` method that will accept the [context](renderer-factory-context-interface-implementation.md#step-4-state-context) and all the required interstate parameters that will come from the main factory. + +## Step 5. Create a method in the main state-factory interface +The states are to be created by the [main state-factory](renderer-factory-context-interface-implementation.md#step-2-state-factory-interface) so the states +do not have hard links to one each-other. +When you create a new state, you need to create a method in the main state-factory for it: + +- Name the function to indicate what happens inside the state. Examples: `preloading`, `error`, etc. +- Pass the common interstate if needed as a data parameter (see the `form` function in the [example](../assets/example/implementation/state/AuthStateFactory.kt)). +- Pass the specific parameters - those needed by this particular state only (see the `preload` function in the [example](../assets/example/implementation/state/AuthStateFactory.kt)). +- Implement the method in the [implementation class](renderer-factory-context-interface-implementation.md#step-5-state-factory-implementation). + +## Example 1. Preloading state +For example of a state that preloads some data with the usecase, take a look at the [PreloadingState](../assets/example/implementation/state/PreloadingState.kt) class. +The state does the following when started: + +- Sets the `Loading` UI state in `doStart` method. +- Calls the `preload` function that: + - calls a usecase to preload data some data or emulates loading if the usecase is not specified, and we are doing a dummy preloading. + - creates the valid [data-state](data-and-ui-implementation.md#step-3-interstate-data). + - switches to the next state by creating it with a factory and calling the `setMachineState` function. +- Implements the `doProcess` function to handle the back gesture or any other gestures that are relevant to this state. +- Does moderate debug-logging in lifecycle methods to track the flow. + +## Example 2. Preloading error state +If any operation fails, you'd better switch the state-machine to an error state. See [PreloadingErrorState](../assets/example/implementation/state/PreloadingErrorState.kt) +state to handle the preloading error. +The typical error state would: + +- Set the `Error` UI state in `doStart` method passing the error to display +- Handle the back gesture that would terminate the flow or return to a previous state if relevant keeping the variables needed. +- May switch back to the loading state if the error is considered recoverable. +- States of that kind are usually rather simple so you don't need any state-factory. + +## Example 3. Form state +For example of a state that handles the user input, validates and prepares the data, take a look at the [FormState](../assets/example/implementation/state/FormState.kt) class. +The state does the following when started: + +- Updates the UI state with the initial data. +- Listens to the gestures, changes the interstate data and updates the screen in the `doProcess` method. +- Changes the machine state when the action gesture is received. +- Validates the data + + diff --git a/skills/commonstatemachine/common-child-flow/add-state/references/state-test-implementation.md b/skills/commonstatemachine/common-child-flow/add-state/references/state-test-implementation.md new file mode 100644 index 0000000..4bc0b15 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/references/state-test-implementation.md @@ -0,0 +1,72 @@ +# Module implementation: state tests +Use this reference to create tests for the [created machine states](state-implementation.md). +`CommonMachineState` tests ensure that state transitions, UI state mapping, and side effects are correctly handled. +Typical task would be the following: + +1. If the [base-test class](base-test-class-implementation.md) is not already available at the same module, add it. +2. Create a test class and extend the base-test class. +3. Define the mocks needed for the test: use-cases, apis, etc. as the lateinit vars. +4. Override the `doInit` function and initialize the mocks. +5. Create test functions using the `test` function defined in the base class. + +## Note: Mocking use-cases and suspend functions that should never complete +Some states call supplied use-cases to load some data and update the machine state accordingly. +Usually the use-case contain some suspend function to do that. +You may want this call to never end. For example - to check the `Back` gesture terminates the flow +while the user awaits for a long loading operation. +In those case use the empty `suspendCancellableCoroutine` to emulate the operation: +```kotlin +// Mock creation +private val requirementsMock: GetPasswordRequirements = mock() + +// Suspending the load operation +everySuspend { requirementsMock.invoke() } calls { suspendCancellableCoroutine { } } +``` +Example: the [test](../assets/example/implementation-tests/state/PreloadingErrorStateTest.kt) for the [PreloadingState](../assets/example/implementation/state/PreloadingState.kt): + +- The state asynchronously loads some data with the use-case at startup. +- You need to check the `Back` pressed while loading terminates. + +## Step 1. Testing the initial rendering +If the state updates the UI, check the UI is updated on state startup: + +- If the UI-renderer is defined in the module - mock the corresponding method, check the calling arguments and a call to `CommonStateMachine::setUiState`. +- If there's no UI-renderer and the state updates the ui directly, evaluate the `CommonStateMachine::setUiState` argument directly. + +Example: the [test](../assets/example/implementation-tests/state/PreloadingErrorStateTest.kt) for the [PreloadingState](../assets/example/implementation/state/PreloadingState.kt): + +- The state asynchronously loads some data with the use-case at startup. +- You need to check the state displays `Loading`. + +## Step 2. Testing the asynchronous outcomes +If the state uses any use-cases or asynchronous operations run at startup, check their outcome: + +- UI updates if any. +- state changes if any. + + +Example: the [test](../assets/example/implementation-tests/state/PreloadingErrorStateTest.kt) for the [PreloadingState](../assets/example/implementation/state/PreloadingState.kt): + +- The state asynchronously loads some data with the use-case at startup. +- You need to check the success: state updates the interstate data and proceeds. +- You need to check the failure: state switches to the error state. + +## Step 3. Testing gestures +If the state processes any gestures in `doProcess` callback, check each gesture path: + +- use [fixtures](test-fixtures-implementation.md) if there are any for gestures. +- check the UI changes if any. +- check interstate data changes if any. +- check machine state changes if any. + +Example 1: the [test](../assets/example/implementation-tests/state/FormStateTest.kt) for the [FormState](../assets/example/implementation/state/FormState.kt): + +- checks the gestures change the interstate data. +- checks the UI updates. +- checks the data validation and state transfer. + +Example 2: the [test](../assets/example/implementation-tests/state/PreloadingErrorStateTest.kt) for the [PreloadingErrorState](../assets/example/implementation/state/PreloadingErrorState.kt): + +- checks the `Back` gesture changes the machine state. +- checks `Action` gesture conditionally changes the machine-state depending on the error. +- checks the data validation and state transfer. diff --git a/skills/commonstatemachine/common-child-flow/add-state/references/test-fixtures-implementation.md b/skills/commonstatemachine/common-child-flow/add-state/references/test-fixtures-implementation.md new file mode 100644 index 0000000..ecaf1e6 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/references/test-fixtures-implementation.md @@ -0,0 +1,15 @@ +# Module implementation: test fixtures +Use this reference to create test fixtures for the [data classes of the feature implementation](data-and-ui-implementation.md): + +1. Create a `data` sub-package in the test source set. +2. Create a fixture file for data-class gestures. +3. Create some ui-state fixture to use when mocking the [renderer](renderer-factory-context-interface-implementation.md#step-3-ui-renderer-interface). +4. Create a fixture file for data-class interstate data, input and result data classes. + +For the fixture data, scan the project for already defined fixtures available and prefer using them instead of creating +your own fixtures. For example, look for the modules like `domain` or `entity` and use their fixtures. Check existing feature +modules for the clue how to create fixtures and where to get the common fixture module. + +Take a look at the [example](../assets/example/implementation-tests/data/authGesturesFixtures.kt) gesture fixtures. +Take a look at the [example](../assets/example/implementation-tests/data/uiStatesFixtures.kt) ui-state fixtures. +Take a look at the [example](../assets/example/implementation-tests/data/dataStatesFixtures.kt) interstate. diff --git a/skills/commonstatemachine/common-child-flow/add-state/references/ui-renderer-implementation.md b/skills/commonstatemachine/common-child-flow/add-state/references/ui-renderer-implementation.md new file mode 100644 index 0000000..4ef14e6 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/references/ui-renderer-implementation.md @@ -0,0 +1,22 @@ +# Module implementation: renderer implementation +Use this reference to implement the [UI-renderer](renderer-factory-context-interface-implementation.md#step-3-ui-renderer-interface) (if used in the module). + +## Step 1. Implement renderer +Follow these steps: + +- Create the implementation class in the `ui` package of the implementation module and add the UI-renderer interface to the list of implementations. +- Enumerate all [UI-states](data-and-ui-implementation.md#step-2-ui-states) and check the source UI-renderer interface. + Find the UI-states corresponding to renderer methods. +- Implement the interface methods transforming the passed parameters to the respectful states. +- Renderer may require other dependencies - resource mappers, etc. Make the class injectable using the DI framework used in a project. + +Take a look at [example](../assets/example/implementation/ui/AuthUiRendererImpl.kt) for our Auth application. + +## Step 2. Create a renderer test +Sometimes UI has a complex model that requires a lot to render. +Secure the logic by writing a unit-test for the renderer. + +- Check every interface method +- Use [fixtures](test-fixtures-implementation.md) if available, modify them by copy-constructors if needed, or add custom. + +Take a look at the [example](../assets/example/implementation-tests/ui/AuthUiRendererImplTest.kt). \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/add-state/references/ui-view-implementation.md b/skills/commonstatemachine/common-child-flow/add-state/references/ui-view-implementation.md new file mode 100644 index 0000000..04476cc --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/references/ui-view-implementation.md @@ -0,0 +1,42 @@ +# Module implementation: basic views +Use this reference to implement the basic views for the implementation. + +## Step 1. Implement scaffolding screen +Create the scaffolding composable that will draw all the [UI States](data-and-ui-implementation.md#step-2-ui-states): + +- Follow common design system and approach in a project if available (scaffolds, appbars, themes, dimensions, etc.). +- If no design system found use the following approach: + - Use `Scaffold` as a top-level element + - Create top app bar with a module title +- The function should accept the following parameters: + - Required: `uiState` - one of the [UI State implementation](data-and-ui-implementation.md#step-2-ui-states) will be passed. + - Required: `onGesture` - a callback accepting [Gesture implementation](data-and-ui-implementation.md#step-1-ui-gestures). Make this parameter the last one. + - Optional: `modifier` with a default value in case this is a common approach in current project. +- Place `when` operator in the content section and implement all branches of the UI states. +- All functions should be internal at most + +See the [example](../assets/example/implementation/ui/AuthScreen.kt) for our sample authentication project. + +## Step 2. Implement child views +Create a separate file for child views. Create a file per each [UI State](data-and-ui-implementation.md#step-2-ui-states): + +- Follow common design system and approach in a project if available (scaffolds, appbars, themes, dimensions, etc.). +- The function should accept the following parameters: + - Required: `uiState` - the concrete [UI State implementation](data-and-ui-implementation.md#step-2-ui-states) will be passed. + - Required: `onGesture` - a callback accepting [Gesture implementation](data-and-ui-implementation.md#step-1-ui-gestures). Make this parameter the last one. + - Required: `modifier` with a default value. +- If no design system found use the following approach: + - Each screen should take `fillMaxSize()` to occupy the available space. + - Center content both horizontally (`Alignment.CenterHorizontally`) and vertically (`Arrangement.Center` for `Column` or `Alignment.Center` for `Box`). + - Use common project padding if defined in the design system (e.g., `Dimensions.medium`) for the main container. + - Use design system theme or `MaterialTheme` for colors and typography to ensure consistency. +- Evaluate each UI state and create corresponding widgets +- Use `onGesture` to pass user gestures. Consider moving the lambas in `onChange` callbacks to `remember` to optimize recompositions if you find it effective. +- Generate previews. If you think multiple previews are needed - use data providers. +- Put a child view to the `when` branches of a scaffolding screen. + +See the examples: + +- [Loading view](../assets/example/implementation/ui/AuthLoadingScreen.kt) +- [Form view](../assets/example/implementation/ui/AuthFormScreen.kt) +- [Error view](../assets/example/implementation/ui/AuthErrorScreen.kt) diff --git a/skills/commonstatemachine/common-child-flow/add-state/references/wiring-transition-implementation.md b/skills/commonstatemachine/common-child-flow/add-state/references/wiring-transition-implementation.md new file mode 100644 index 0000000..3137ba3 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/references/wiring-transition-implementation.md @@ -0,0 +1,63 @@ +# Module implementation: wiring a transition from an existing state +Use this reference when the user told you how an existing machine state leads to the state you just +created. You will modify the EXISTING (calling) state, not the new one. + +Take a look at the [class diagram](common-child-flow.puml) to get the idea about the components' +purpose. + +Per Navigation Inversion (see [ARCHITECTURE.md](ARCHITECTURE.md)), a transition between logical +states doesn't need a user in the loop at all - the machine moves itself forward whenever a state +decides it's done. There are two patterns below; figure out which one the user actually means before +touching any code. + +## Pattern A: automatic handoff (no gesture) +This is the common shape for a chain of invisible steps - for example "loads a token, then checks +it's still valid, then loads the profile". None of this is something the user clicks through: each +state's own `doStart` runs its work and, once it completes, calls the next state's factory method and +switches to it - exactly like +[PreloadingState](../assets/example/implementation/state/PreloadingState.kt) already does (see [state-implementation.md, Example 1](state-implementation.md#example-1-preloading-state)): it +preloads data in `doStart` and, on success, creates the next state with the factory and calls +`setMachineState` - no gesture anywhere in the sequence. + +To wire this into an existing state: + +1. Find where its startup work currently finishes successfully (the end of `doStart`, or a + callback/coroutine completion inside it). +2. At that point, extract whatever data the new state needs - from the existing state's own + interstate data, or from whatever the just-finished work produced. +3. Call the [state-factory method](renderer-factory-context-interface-implementation.md#step-2-state-factory-interface) + created for the new state and switch to it with `setMachineState`, using the convenience method + from the [base state](base-state-implementation.md#step-2-add-convenience-methods-to-change-the-machine-and-ui-states). +4. Only call `setUiState` along the way if something actually needs to change on screen - a chain of + invisible steps can keep showing the same `Loading` UI-state the whole way through. + +A recoverable-error variant of the same idea - switching back to an earlier state once some condition +is met, rather than forward to a new one - is shown by [PreloadingErrorState](../assets/example/implementation/state/PreloadingErrorState.kt), which +switches back to the loading state when the error is considered recoverable (see [state-implementation.md, Example 2](state-implementation.md#example-2-preloading-error-state)). + +## Pattern B: triggered by a gesture +This is the case when the user's own action moves the flow forward - for example "when the user +clicks a friend in the list, navigate to the friend's details". + +1. Find or add the triggering gesture: if it already exists (for example a `SelectItem` gesture + carrying the clicked item), reuse it; otherwise add a new gesture case to the sealed Gesture + structure, following [this guide](data-and-ui-implementation.md#step-1-ui-gestures), and handle it in the existing state's gesture processing. +2. In that gesture's handling, extract whatever data the new state needs (from the gesture, or from + the existing state's own interstate data), call the new state's + [state-factory method](renderer-factory-context-interface-implementation.md#step-2-state-factory-interface), and switch to it with `setMachineState`. + +Take a look at the [example](../assets/example/implementation/state/FormState.kt): it already demonstrates this exact pattern - it changes the machine +state when its action gesture is receive (see [state-implementation.md, Example 3](state-implementation.md#example-3-form-state)). + +## Update the existing state's test +Whichever pattern applies, add a test case to the existing state's unit test that: + +- For Pattern A: drives the mocked startup dependency (use-case, flow, etc.) to complete, without + sending any gesture. +- For Pattern B: sends the triggering gesture (with fixture data if it carries any). +- Either way: asserts the state-factory method was called with the expected arguments, and that the + machine state was switched to the state the factory returned. + +Follow [the state-test guide](state-test-implementation.md#step-3-testing-gestures) for how to +structure a test like this - for Pattern A, the equivalent of "sending a gesture" is whatever already +drives the state's startup behaviour in its other tests. diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md b/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md index 585d39e..18f62d3 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md @@ -1,24 +1,30 @@ --- name: commonstatemachine-commonchildflow-boilerplate -description: Use this skill to make a boilerplate for the new feature-flow using - the [CommonStateMachine](https://github.com/motorro/CommonStateMachine) architecture. - If the user asks you something like "Create a new authentication flow feature" - use this skill. +description: Use this skill to scaffold a brand-new feature-flow module with the CommonStateMachine + Common Child Flow architecture - the API and implementation Gradle modules, gestures, UI-states, + machine states, the state factory, DI wiring and tests. Use it whenever the user asks to create a + new feature flow, module, or screen flow from scratch for CommonStateMachine - for example + "Create a new authentication flow feature", "Scaffold a payment feature module", or "Set up a + CommonStateMachine flow for onboarding". If the module already exists and the user only wants to + add one more screen or state to it, use the add-state skill instead. metadata: author: Motorro - last-updated: '2026-08-12' + last-updated: '2026-10-02' keywords: - - android - - architecture - - cross-platform - - kotlin-multiplatform - - mvi - - state-machine - - feature-flow + - android + - architecture + - cross-platform + - kotlin-multiplatform + - mvi + - state-machine + - feature-flow --- ## Description -Use this skill to add a new feature boilerplate to the app based on the `CommonStateMachine` architecture. -following the `Common Child Flow` design. This design sets a common approach to use proxy state machines for a feature-flow. +Use this skill to add a new feature boilerplate to the app based on the `CommonStateMachine` +architecture, following the [Common Child Flow](references/COMMON-CHILD-FLOW.md) design. This design sets a common approach to use +proxy state machines for a feature-flow. If the module already exists, use the add-state skill +instead to add another logical state to it. For a deeper dive into the state machine mechanics and common components, refer to the [Core Architecture](references/ARCHITECTURE.md). For a deeper dive into the common child flow philosophy and design, refer to the [Common Child Flow Architecture](references/COMMON-CHILD-FLOW.md). @@ -30,7 +36,7 @@ to such an example. Adjust the names and packages to your actual need. ## Strategy You will create a set of Gradle modules that together form a feature: -- The API module: contains the definition of input and output data types for the feature. +- The API module: contains the definition of input and output data types for the feature. Example: authentication API that provides the basic interface to the authentication feature. - One or more implementation modules: contains the implementation of the API. Example: login implementation module - implements API with login/password functionality. @@ -38,6 +44,26 @@ You will create a set of Gradle modules that together form a feature: Take a look at the [class diagram](references/common-child-flow.puml). It contains an overview between all the interfaces, classes and objects in a setup using the Authentication module example. +## Gathering the requirements for the new feature +Before creating any modules, work out what the feature needs from what the user gave you: + +- The feature itself: a name, and whether it needs one implementation or several of the same API + (for example a `password` and a `social` authentication implementation side by side) - see Step 1. +- Its data source: does the user name an existing use-case, repository, or API to call (like a + `GetFriendList` use-case), or should you create a dummy/placeholder one? If they name one, use it + directly instead of inventing a new one. +- The state-flow: did the user describe what logical states the feature needs and how it moves + between them, or give you a screen design (a Figma link/export, or a picture) to work from? If so, + create those states and views in Steps 4-5 - and remember a logical state doesn't have to be a new + screen, it can just as easily be an invisible step that hands off to the next one automatically. If + the user gave you nothing specific, default to the classic Loading-Content-Error (LCE) shape with + dummy states and views. +- Integration: should this feature be hosted inside one or more existing parent flows (Step 7), or + does it stand alone for now? + +If the user gave you only part of this, use what you have and ask clarifying questions about +anything still ambiguous rather than guessing silently. + Follow the following common rules when writing code: - Follow the project coding conventions or use Kotlin default @@ -60,7 +86,7 @@ my-project # Project folder. Common package: com.motorro.commonstatem - Include the following dependencies if not provided already: - Base state-machine components: "com.motorro.commonstatemachine:commonstatemachine:x.x.x" - Coroutine extensions (if using coroutines): "com.motorro.commonstatemachine:coroutines:x.x.x" - - Common data API (state machine child flow): "com.motorro.commonstatemachine:commonflow-data:x.x.x" + - Common data API (state machine child flow): "com.motorro.commonstatemachine:commonflow-data:x.x.x" - Common UI API (Compose API if used): "com.motorro.commonstatemachine:commonflow-compose:x.x.x" - Include test dependencies that are used across the project and coroutines test dependencies if used. - Include DI dependencies that are used across the project. @@ -85,7 +111,7 @@ Then follow these guides for the API module implementation: 3. [Feature-flow data and UI API](references/data-and-ui-api.md) ## Step 3. Create implementation module -Here you will create an API implementation. +Here you will create an API implementation. If several implementations are requested by user (step 1) - repeat for each implementation. Create a package for the feature implementation. By default, use the common application package adding the feature name and `implementation`. If several implementations are requested by user (step 1) - name according to the implementation name. @@ -100,7 +126,7 @@ Then follow these guides for the module implementation: ## Step 4. Create the machine states For the user to kick-off faster, we will create some machine-states: -- If the user gave you the instructions about what would be the state-flow, or what states they need - create the states for them. +- If the user gave you the instructions about what would be the state-flow, or what states they need - create the states for them. Implement the required functionality or place TODOs in place of the code if it is not clear what to do exactly. Don't hesitate to ask the user to clarify what they need. - If the user didn't give you any specifics, create a classic LCE example: diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/ARCHITECTURE.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/ARCHITECTURE.md index 3b5362d..94621a6 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/ARCHITECTURE.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/ARCHITECTURE.md @@ -14,7 +14,13 @@ - It maintains the state transition logic. - `FlowStateMachine` (standard implementation) exports the current **UI State** as a `StateFlow`. - It provides the `process(gesture)` entry point for user interaction. -- **Navigation Inversion:** Navigation is a logical state transition within the state machine (Logic/ViewModel layer). The View is passive and follows the logic. +- **Navigation Inversion:** Navigation is a logical state transition within the state machine (Logic/ViewModel layer), not something the View drives. The user + doesn't navigate between screens - gestures (or a state's own work finishing) move the machine from one logical state to another, and whichever state is + active decides what to show by calling `setUiState`. Several logical states can therefore render the exact same UI State one after another - for example, a + preload step and an auth-check step can both just show a loading spinner - with nothing visibly changing until some state actually calls `setUiState` with + something new. The same decoupling is why a transition can carry live, non-serializable data (an open coroutine, a scope, an object reference) forward as a + plain constructor argument, unlike a screen-based navigation graph where arguments between destinations typically need to be serializable. See + [MVI Architecture with a State Machine: Basics](https://proandroiddev.com/mvi-architecture-with-a-state-machine-basics-721c5ebed893) for the full rationale. - **ViewModel Integration:** The `ViewModel` (or equivalent lifecycle host) typically: - Holds the `StateMachine` instance. - Delegates user gestures to `stateMachine.process(gesture)`. diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/COMMON-CHILD-FLOW.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/COMMON-CHILD-FLOW.md index e727d51..655ed24 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/COMMON-CHILD-FLOW.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/COMMON-CHILD-FLOW.md @@ -4,7 +4,9 @@ ## Philosophy and Principles - **Modularity via Proxying:** Features are independent modules. A parent flow hosts a child flow using `ProxyMachineState`, which bridges incompatible Gesture/UI systems. -- **Direct Data Flow:** Transitions occur in memory. Complex data or even running coroutines can be passed between states without serialization or "parceling". +- **Direct Data Flow:** Transitions occur in memory. Complex data or even running coroutines can be passed between states without serialization or "parceling" - + unlike a screen-based navigation library (e.g. Jetpack Navigation), where arguments passed between destinations typically need to be serializable. This + follows from the same [Navigation Inversion](ARCHITECTURE.md) principle the base state machine already relies on. - **KMP Ready:** Logic and Data are isolated from the View, making the core feature logic easily shareable across platforms. - **Standardized Child Flows:** - `commonflow-data`: Standardizes flow initialization and result handling. From 0693ee0f305b99917348faf92e441e7f6e448586 Mon Sep 17 00:00:00 2001 From: motorro Date: Fri, 2 Oct 2026 15:23:12 +0200 Subject: [PATCH 22/32] Demo skill Refs: #14 --- examples/skills/auth/demo/build.gradle.kts | 6 + .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 1404 -> 0 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 2898 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 982 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 1772 -> 0 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 1900 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 3918 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 2884 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 5914 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 3844 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 7778 -> 0 bytes skills-src/README.md | 52 +++-- ...actory-context-interface-implementation.md | 4 +- .../wiring-transition-implementation.md | 2 +- .../common-child-flow/boilerplate/SKILL.md | 4 + .../references/di-implementation.md | 5 +- .../references/proxy-implementation.md | 3 +- .../common-child-flow/demo/SKILL.md | 202 ++++++++++++++++++ .../common-child-flow/demo/assets/example/api | 1 + .../demo/assets/example/demo/build.gradle.kts | 1 + .../demo/assets/example/demo/src | 1 + .../demo/assets/example/domain | 1 + .../demo/assets/example/implementation | 1 + .../demo/references/ARCHITECTURE.md | 1 + .../demo/references/COMMON-CHILD-FLOW.md | 1 + .../demo/references/common-child-flow.puml | 1 + .../demo/references/fixture-patterns.md | 74 +++++++ skills/README.md | 52 +++-- ...actory-context-interface-implementation.md | 4 +- .../wiring-transition-implementation.md | 2 +- .../common-child-flow/boilerplate/SKILL.md | 4 + .../references/di-implementation.md | 5 +- .../references/proxy-implementation.md | 3 +- ...actory-context-interface-implementation.md | 4 +- .../common-child-flow/demo/SKILL.md | 202 ++++++++++++++++++ .../demo/assets/example/api/AuthDataApi.kt | 8 + .../demo/assets/example/api/AuthGesture.kt | 7 + .../demo/assets/example/api/AuthInput.kt | 7 + .../demo/assets/example/api/AuthResult.kt | 7 + .../demo/assets/example/api/AuthUiApi.kt | 8 + .../demo/assets/example/api/AuthUiState.kt | 7 + .../demo/assets/example/demo/build.gradle.kts | 45 ++++ .../example/demo/src/main/AndroidManifest.xml | 26 +++ .../examples/skills/auth/demo/App.kt | 94 ++++++++ .../examples/skills/auth/demo/MainActivity.kt | 69 ++++++ .../skills/auth/demo/MainViewModel.kt | 15 ++ .../drawable-v24/ic_launcher_foreground.xml | 43 ++++ .../res/drawable/ic_launcher_background.xml | 183 ++++++++++++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 18 ++ .../mipmap-anydpi-v26/ic_launcher_round.xml | 18 ++ .../demo/src/main/res/values/strings.xml | 3 + .../demo/src/main/res/xml/backup_rules.xml | 13 ++ .../main/res/xml/data_extraction_rules.xml | 19 ++ .../main/res/xml/network_security_config.xml | 13 ++ .../authenticate/AuthenticateWithPassword.kt | 15 ++ .../authenticate/GetPasswordRequirements.kt | 13 ++ .../authenticate/data/PasswordRequirements.kt | 11 + .../example/domain/exception/AppException.kt | 35 +++ .../exception/AuthenticationException.kt | 6 + .../example/domain/exception/IOException.kt | 6 + .../domain/exception/UnknownException.kt | 6 + .../example/domain/friends/GetFriendList.kt | 11 + .../example/domain/friends/data/Friend.kt | 3 + .../example/domain/friends/data/FriendData.kt | 9 + .../example/domain/session/SessionManager.kt | 33 +++ .../example/domain/session/data/Session.kt | 29 +++ .../example/domain/session/data/Username.kt | 6 + .../example/implementation/AuthDataApiImpl.kt | 35 +++ .../example/implementation/AuthModule.kt | 8 + .../example/implementation/AuthUiApiImpl.kt | 27 +++ .../implementation/data/AuthGestureImpl.kt | 38 ++++ .../implementation/data/AuthStateData.kt | 23 ++ .../implementation/data/AuthUiStateImpl.kt | 36 ++++ .../implementation/state/AuthContext.kt | 25 +++ .../example/implementation/state/AuthState.kt | 10 + .../implementation/state/AuthStateFactory.kt | 54 +++++ .../state/AuthStateFactoryImpl.kt | 84 ++++++++ .../state/AuthenticatingState.kt | 81 +++++++ .../state/AuthenticationErrorState.kt | 53 +++++ .../implementation/state/BaseAuthState.kt | 34 +++ .../example/implementation/state/FormState.kt | 75 +++++++ .../state/PreloadingErrorState.kt | 52 +++++ .../implementation/state/PreloadingState.kt | 78 +++++++ .../implementation/ui/AuthErrorScreen.kt | 79 +++++++ .../implementation/ui/AuthFormScreen.kt | 122 +++++++++++ .../implementation/ui/AuthLoadingScreen.kt | 21 ++ .../example/implementation/ui/AuthScreen.kt | 68 ++++++ .../implementation/ui/AuthUiRenderer.kt | 33 +++ .../implementation/ui/AuthUiRendererImpl.kt | 28 +++ .../demo/references/ARCHITECTURE.md | 38 ++++ .../demo/references/COMMON-CHILD-FLOW.md | 41 ++++ .../demo/references/common-child-flow.puml | 187 ++++++++++++++++ .../demo/references/fixture-patterns.md | 74 +++++++ 93 files changed, 2783 insertions(+), 43 deletions(-) delete mode 100644 examples/skills/auth/demo/src/main/res/mipmap-hdpi/ic_launcher.webp delete mode 100644 examples/skills/auth/demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp delete mode 100644 examples/skills/auth/demo/src/main/res/mipmap-mdpi/ic_launcher.webp delete mode 100644 examples/skills/auth/demo/src/main/res/mipmap-mdpi/ic_launcher_round.webp delete mode 100644 examples/skills/auth/demo/src/main/res/mipmap-xhdpi/ic_launcher.webp delete mode 100644 examples/skills/auth/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp delete mode 100644 examples/skills/auth/demo/src/main/res/mipmap-xxhdpi/ic_launcher.webp delete mode 100644 examples/skills/auth/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp delete mode 100644 examples/skills/auth/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.webp delete mode 100644 examples/skills/auth/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 skills-src/commonstatemachine/common-child-flow/demo/SKILL.md create mode 120000 skills-src/commonstatemachine/common-child-flow/demo/assets/example/api create mode 120000 skills-src/commonstatemachine/common-child-flow/demo/assets/example/demo/build.gradle.kts create mode 120000 skills-src/commonstatemachine/common-child-flow/demo/assets/example/demo/src create mode 120000 skills-src/commonstatemachine/common-child-flow/demo/assets/example/domain create mode 120000 skills-src/commonstatemachine/common-child-flow/demo/assets/example/implementation create mode 120000 skills-src/commonstatemachine/common-child-flow/demo/references/ARCHITECTURE.md create mode 120000 skills-src/commonstatemachine/common-child-flow/demo/references/COMMON-CHILD-FLOW.md create mode 120000 skills-src/commonstatemachine/common-child-flow/demo/references/common-child-flow.puml create mode 100644 skills-src/commonstatemachine/common-child-flow/demo/references/fixture-patterns.md create mode 100644 skills/commonstatemachine/common-child-flow/demo/SKILL.md create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/api/AuthDataApi.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/api/AuthGesture.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/api/AuthInput.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/api/AuthResult.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/api/AuthUiApi.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/api/AuthUiState.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/demo/build.gradle.kts create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/AndroidManifest.xml create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/App.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/MainActivity.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/MainViewModel.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/drawable/ic_launcher_background.xml create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/values/strings.xml create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/xml/backup_rules.xml create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/xml/data_extraction_rules.xml create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/xml/network_security_config.xml create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/domain/authenticate/AuthenticateWithPassword.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/domain/authenticate/GetPasswordRequirements.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/domain/authenticate/data/PasswordRequirements.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/domain/exception/AppException.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/domain/exception/AuthenticationException.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/domain/exception/IOException.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/domain/exception/UnknownException.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/domain/friends/GetFriendList.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/domain/friends/data/Friend.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/domain/friends/data/FriendData.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/domain/session/SessionManager.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/domain/session/data/Session.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/domain/session/data/Username.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/AuthDataApiImpl.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/AuthModule.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/AuthUiApiImpl.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/data/AuthGestureImpl.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/data/AuthStateData.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/data/AuthUiStateImpl.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/AuthContext.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/AuthState.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/AuthStateFactory.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/AuthStateFactoryImpl.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/AuthenticatingState.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/AuthenticationErrorState.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/BaseAuthState.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/FormState.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/PreloadingErrorState.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/PreloadingState.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/ui/AuthErrorScreen.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/ui/AuthFormScreen.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/ui/AuthLoadingScreen.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/ui/AuthScreen.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/ui/AuthUiRenderer.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/ui/AuthUiRendererImpl.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/references/ARCHITECTURE.md create mode 100644 skills/commonstatemachine/common-child-flow/demo/references/COMMON-CHILD-FLOW.md create mode 100644 skills/commonstatemachine/common-child-flow/demo/references/common-child-flow.puml create mode 100644 skills/commonstatemachine/common-child-flow/demo/references/fixture-patterns.md diff --git a/examples/skills/auth/demo/build.gradle.kts b/examples/skills/auth/demo/build.gradle.kts index 0173a50..fd7ac87 100644 --- a/examples/skills/auth/demo/build.gradle.kts +++ b/examples/skills/auth/demo/build.gradle.kts @@ -16,6 +16,12 @@ plugins { alias(libs.plugins.koin) } +android { + defaultConfig { + minSdk = 34 + } +} + dependencies { implementation(project(":examples:skills:appcore")) implementation(project(":examples:skills:domain")) diff --git a/examples/skills/auth/demo/src/main/res/mipmap-hdpi/ic_launcher.webp b/examples/skills/auth/demo/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index c209e78ecd372343283f4157dcfd918ec5165bb3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1404 zcmV-?1%vuhNk&F=1pok7MM6+kP&il$0000G0000-002h-06|PpNX!5L00Dqw+t%{r zzW2vH!KF=w&cMnnN@{whkTw+#mAh0SV?YL=)3MimFYCWp#fpdtz~8$hD5VPuQgtcN zXl<@<#Cme5f5yr2h%@8TWh?)bSK`O z^Z@d={gn7J{iyxL_y_%J|L>ep{dUxUP8a{byupH&!UNR*OutO~0{*T4q5R6@ApLF! z5{w?Z150gC7#>(VHFJZ-^6O@PYp{t!jH(_Z*nzTK4 zkc{fLE4Q3|mA2`CWQ3{8;gxGizgM!zccbdQoOLZc8hThi-IhN90RFT|zlxh3Ty&VG z?Fe{#9RrRnxzsu|Lg2ddugg7k%>0JeD+{XZ7>Z~{=|M+sh1MF7~ zz>To~`~LVQe1nNoR-gEzkpe{Ak^7{{ZBk2i_<+`Bq<^GB!RYG+z)h;Y3+<{zlMUYd zrd*W4w&jZ0%kBuDZ1EW&KLpyR7r2=}fF2%0VwHM4pUs}ZI2egi#DRMYZPek*^H9YK zay4Iy3WXFG(F14xYsoDA|KXgGc5%2DhmQ1gFCkrgHBm!lXG8I5h*uf{rn48Z!_@ z4Bk6TJAB2CKYqPjiX&mWoW>OPFGd$wqroa($ne7EUK;#3VYkXaew%Kh^3OrMhtjYN?XEoY`tRPQsAkH-DSL^QqyN0>^ zmC>{#F14jz4GeW{pJoRpLFa_*GI{?T93^rX7SPQgT@LbLqpNA}<@2wH;q493)G=1Y z#-sCiRNX~qf3KgiFzB3I>4Z%AfS(3$`-aMIBU+6?gbgDb!)L~A)je+;fR0jWLL-Fu z4)P{c7{B4Hp91&%??2$v9iRSFnuckHUm}or9seH6 z>%NbT+5*@L5(I9j@06@(!{ZI?U0=pKn8uwIg&L{JV14+8s2hnvbRrU|hZCd}IJu7*;;ECgO%8_*W Kmw_-CKmY()leWbG diff --git a/examples/skills/auth/demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/examples/skills/auth/demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index b2dfe3d1ba5cf3ee31b3ecc1ced89044a1f3b7a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2898 zcmV-Y3$650Nk&FW3jhFDMM6+kP&il$0000G0000-002h-06|PpNWB9900E$G+qN-D z+81ABX7q?;bwx%xBg?kcwr$(C-Tex-ZCkHUw(Y9#+`E5-zuONG5fgw~E2WDng@Bc@ z24xy+R1n%~6xI#u9vJ8zREI)sb<&Il(016}Z~V1n^PU3-_H17A*Bf^o)&{_uBv}Py zulRfeE8g(g6HFhk_?o_;0@tz?1I+l+Y#Q*;RVC?(ud`_cU-~n|AX-b`JHrOIqn(-t&rOg-o`#C zh0LPxmbOAEb;zHTu!R3LDh1QO zZTf-|lJNUxi-PpcbRjw3n~n-pG;$+dIF6eqM5+L();B2O2tQ~|p{PlpNcvDbd1l%c zLtXn%lu(3!aNK!V#+HNn_D3lp z2%l+hK-nsj|Bi9;V*WIcQRTt5j90A<=am+cc`J zTYIN|PsYAhJ|=&h*4wI4ebv-C=Be#u>}%m;a{IGmJDU`0snWS&$9zdrT(z8#{OZ_Y zxwJx!ZClUi%YJjD6Xz@OP8{ieyJB=tn?>zaI-4JN;rr`JQbb%y5h2O-?_V@7pG_+y z(lqAsqYr!NyVb0C^|uclHaeecG)Sz;WV?rtoqOdAAN{j%?Uo%owya(F&qps@Id|Of zo@~Y-(YmfB+chv^%*3g4k3R0WqvuYUIA+8^SGJ{2Bl$X&X&v02>+0$4?di(34{pt* zG=f#yMs@Y|b&=HyH3k4yP&goF2LJ#tBLJNNDo6lG06r}ghC-pC4Q*=x3;|+W04zte zAl>l4kzUBQFYF(E`KJy?ZXd1tnfbH+Z~SMmA21KokJNs#eqcXWKUIC>{TuoKe^vhF z);H)o`t9j~`$h1D`#bxe@E`oE`cM9w(@)5Bp8BNukIwM>wZHfd0S;5bcXA*5KT3bj zc&_~`&{z7u{Et!Z_k78H75gXf4g8<_ul!H$eVspPeU3j&&Au=2R*Zp#M9$9s;fqwgzfiX=E_?BwVcfx3tG9Q-+<5fw z%Hs64z)@Q*%s3_Xd5>S4dg$s>@rN^ixeVj*tqu3ZV)biDcFf&l?lGwsa zWj3rvK}?43c{IruV2L`hUU0t^MemAn3U~x3$4mFDxj=Byowu^Q+#wKRPrWywLjIAp z9*n}eQ9-gZmnd9Y0WHtwi2sn6n~?i#n9VN1B*074_VbZZ=WrpkMYr{RsI ztM_8X1)J*DZejxkjOTRJ&a*lrvMKBQURNP#K)a5wIitfu(CFYV4FT?LUB$jVwJSZz zNBFTWg->Yk0j&h3e*a5>B=-xM7dE`IuOQna!u$OoxLlE;WdrNlN)1 z7**de7-hZ!(%_ZllHBLg`Ir#|t>2$*xVOZ-ADZKTN?{(NUeLU9GbuG-+Axf*AZ-P1 z0ZZ*fx+ck4{XtFsbcc%GRStht@q!m*ImssGwuK+P@%gEK!f5dHymg<9nSCXsB6 zQ*{<`%^bxB($Z@5286^-A(tR;r+p7B%^%$N5h%lb*Vlz-?DL9x;!j<5>~kmXP$E}m zQV|7uv4SwFs0jUervsxVUm>&9Y3DBIzc1XW|CUZrUdb<&{@D5yuLe%Xniw^x&{A2s z0q1+owDSfc3Gs?ht;3jw49c#mmrViUfX-yvc_B*wY|Lo7; zGh!t2R#BHx{1wFXReX*~`NS-LpSX z#TV*miO^~B9PF%O0huw!1Zv>^d0G3$^8dsC6VI!$oKDKiXdJt{mGkyA`+Gwd4D-^1qtNTUK)`N*=NTG-6}=5k6suNfdLt*dt8D| z%H#$k)z#ZRcf|zDWB|pn<3+7Nz>?WW9WdkO5(a^m+D4WRJ9{wc>Y}IN)2Kbgn;_O? zGqdr&9~|$Y0tP=N(k7^Eu;iO*w+f%W`20BNo)=Xa@M_)+o$4LXJyiw{F?a633SC{B zl~9FH%?^Rm*LVz`lkULs)%idDX^O)SxQol(3jDRyBVR!7d`;ar+D7do)jQ}m`g$TevUD5@?*P8)voa?kEe@_hl{_h8j&5eB-5FrYW&*FHVt$ z$kRF9Nstj%KRzpjdd_9wO=4zO8ritN*NPk_9avYrsF(!4))tm{Ga#OY z(r{0buexOzu7+rw8E08Gxd`LTOID{*AC1m*6Nw@osfB%0oBF5sf<~wH1kL;sd zo)k6^VyRFU`)dt*iX^9&QtWbo6yE8XXH?`ztvpiOLgI3R+=MOBQ9=rMVgi<*CU%+d1PQQ0a1U=&b0vkF207%xU0ssI2 diff --git a/examples/skills/auth/demo/src/main/res/mipmap-mdpi/ic_launcher.webp b/examples/skills/auth/demo/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d64e58ba64d180ce43ee13bf9a17835fbca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 982 zcmV;{11bDcNk&G_0{{S5MM6+kP&il$0000G0000l001ul06|PpNU8t;00Dqo+t#w^ z^1csucXz7-Qrhzl9HuHB%l>&>1tG2^vb*E&k^T3$FG1eQZ51g$uv4V+kI`0<^1Z@N zk?Jjh$olyC%l>)Xq;7!>{iBj&BjJ`P&$fsCfpve_epJOBkTF?nu-B7D!hO=2ZR}

C%4 zc_9eOXvPbC4kzU8YowIA8cW~Uv|eB&yYwAObSwL2vY~UYI7NXPvf3b+c^?wcs~_t{ ze_m66-0)^{JdOMKPwjpQ@Sna!*?$wTZ~su*tNv7o!gXT!GRgivP}ec?5>l1!7<(rT zds|8x(qGc673zrvYIz;J23FG{9nHMnAuP}NpAED^laz3mAN1sy+NXK)!6v1FxQ;lh zOBLA>$~P3r4b*NcqR;y6pwyhZ3_PiDb|%n1gGjl3ZU}ujInlP{eks-#oA6>rh&g+!f`hv#_%JrgYPu z(U^&XLW^QX7F9Z*SRPpQl{B%x)_AMp^}_v~?j7 zapvHMKxSf*Mtyx8I}-<*UGn3)oHd(nn=)BZ`d$lDBwq_GL($_TPaS{UeevT(AJ`p0 z9%+hQb6z)U9qjbuXjg|dExCLjpS8$VKQ55VsIC%@{N5t{NsW)=hNGI`J=x97_kbz@ E0Of=7!TQj4N+cqN`nQhxvX7dAV-`K|Ub$-q+H-5I?Tx0g9jWxd@A|?POE8`3b8fO$T))xP* z(X?&brZw({`)WU&rdAs1iTa0x6F@PIxJ&&L|dpySV!ID|iUhjCcKz(@mE z!x@~W#3H<)4Ae(4eQJRk`Iz3<1)6^m)0b_4_TRZ+cz#eD3f8V;2r-1fE!F}W zEi0MEkTTx}8i1{`l_6vo0(Vuh0HD$I4SjZ=?^?k82R51bC)2D_{y8mi_?X^=U?2|F{Vr7s!k(AZC$O#ZMyavHhlQ7 zUR~QXuH~#o#>(b$u4?s~HLF*3IcF7023AlwAYudn0FV~|odGH^05AYPEfR)8p`i{n zwg3zPVp{+wOsxKc>)(pMupKF!Y2HoUqQ3|Yu|8lwR=?5zZuhG6J?H`bSNk_wPoM{u zSL{c@pY7+c2kck>`^q1^^gR0QB7Y?KUD{vz-uVX~;V-rW)PDcI)$_UjgVV?S?=oLR zf4}zz{#*R_{LkiJ#0RdQLNC^2Vp%JPEUvG9ra2BVZ92(p9h7Ka@!yf9(lj#}>+|u* z;^_?KWdzkM`6gqPo9;;r6&JEa)}R3X{(CWv?NvgLeOTq$cZXqf7|sPImi-7cS8DCN zGf;DVt3Am`>hH3{4-WzH43Ftx)SofNe^-#|0HdCo<+8Qs!}TZP{HH8~z5n`ExcHuT zDL1m&|DVpIy=xsLO>8k92HcmfSKhflQ0H~9=^-{#!I1g(;+44xw~=* zxvNz35vfsQE)@)Zsp*6_GjYD};Squ83<_?^SbALb{a`j<0Gn%6JY!zhp=Fg}Ga2|8 z52e1WU%^L1}15Ex0fF$e@eCT(()_P zvV?CA%#Sy08_U6VPt4EtmVQraWJX` zh=N|WQ>LgrvF~R&qOfB$!%D3cGv?;Xh_z$z7k&s4N)$WYf*k=|*jCEkO19{h_(%W4 zPuOqbCw`SeAX*R}UUsbVsgtuG?xs(#Ikx9`JZoQFz0n*7ZG@Fv@kZk`gzO$HoA9kN z8U5{-yY zvV{`&WKU2$mZeoBmiJrEdzUZAv1sRxpePdg1)F*X^Y)zp^Y*R;;z~vOv-z&)&G)JQ{m!C9cmziu1^nHA z`#`0c>@PnQ9CJKgC5NjJD8HM3|KC(g5nnCq$n0Gsu_DXk36@ql%npEye|?%RmG)

FJ$wK}0tWNB{uH;AM~i diff --git a/examples/skills/auth/demo/src/main/res/mipmap-xhdpi/ic_launcher.webp b/examples/skills/auth/demo/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 948a3070fe34c611c42c0d3ad3013a0dce358be0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1900 zcmV-y2b1_xNk&Fw2LJ$9MM6+kP&il$0000G0001A003VA06|PpNH75a00DqwTbm-~ zullQTcXxO9ki!OCRx^i?oR|n!<8G0=kI^!JSjFi-LL*`V;ET0H2IXfU0*i>o6o6Gy zRq6Ap5(_{XLdXcL-MzlN`ugSdZY_`jXhcENAu)N_0?GhF))9R;E`!bo9p?g?SRgw_ zEXHhFG$0{qYOqhdX<(wE4N@es3VIo$%il%6xP9gjiBri+2pI6aY4 zJbgh-Ud|V%3O!IcHKQx1FQH(_*TK;1>FQWbt^$K1zNn^cczkBs=QHCYZ8b&l!UV{K z{L0$KCf_&KR^}&2Fe|L&?1I7~pBENnCtCuH3sjcx6$c zwqkNkru);ie``q+_QI;IYLD9OV0ZxkuyBz|5<$1BH|vtey$> z5oto4=l-R-Aaq`Dk0}o9N0VrkqW_#;!u{!bJLDq%0092{Ghe=F;(kn} z+sQ@1=UlX30+2nWjkL$B^b!H2^QYO@iFc0{(-~yXj2TWz?VG{v`Jg zg}WyYnwGgn>{HFaG7E~pt=)sOO}*yd(UU-D(E&x{xKEl6OcU?pl)K%#U$dn1mDF19 zSw@l8G!GNFB3c3VVK0?uyqN&utT-D5%NM4g-3@Sii9tSXKtwce~uF zS&Jn746EW^wV~8zdQ1XC28~kXu8+Yo9p!<8h&(Q({J*4DBglPdpe4M_mD8AguZFn~ ztiuO~{6Bx?SfO~_ZV(GIboeR9~hAym{{fV|VM=77MxDrbW6`ujX z<3HF(>Zr;#*uCvC*bpoSr~C$h?_%nXps@A)=l_;({Fo#6Y1+Zv`!T5HB+)#^-Ud_; zBwftPN=d8Vx)*O1Mj+0oO=mZ+NVH*ptNDC-&zZ7Hwho6UQ#l-yNvc0Cm+2$$6YUk2D2t#vdZX-u3>-Be1u9gtTBiMB^xwWQ_rgvGpZ6(C@e23c!^K=>ai-Rqu zhqT`ZQof;9Bu!AD(i^PCbYV%yha9zuoKMp`U^z;3!+&d@Hud&_iy!O-$b9ZLcSRh? z)R|826w}TU!J#X6P%@Zh=La$I6zXa#h!B;{qfug}O%z@K{EZECu6zl)7CiNi%xti0 zB{OKfAj83~iJvmpTU|&q1^?^cIMn2RQ?jeSB95l}{DrEPTW{_gmU_pqTc)h@4T>~& zluq3)GM=xa(#^VU5}@FNqpc$?#SbVsX!~RH*5p0p@w z;~v{QMX0^bFT1!cXGM8K9FP+=9~-d~#TK#ZE{4umGT=;dfvWi?rYj;^l_Zxywze`W z^Cr{55U@*BalS}K%Czii_80e0#0#Zkhlij4-~I@}`-JFJ7$5{>LnoJSs??J8kWVl6|8A}RCGAu9^rAsfCE=2}tHwl93t0C?#+jMpvr7O3`2=tr{Hg$=HlnjVG^ewm|Js0J*kfPa6*GhtB>`fN!m#9J(sU!?(OSfzY*zS(FJ<-Vb zfAIg+`U)YaXv#sY(c--|X zEB+TVyZ%Ie4L$gi#Fc++`h6%vzsS$pjz9aLt+ZL(g;n$Dzy5=m=_TV(3H8^C{r0xd zp#a%}ht55dOq?yhwYPrtp-m1xXp;4X;)NhxxUpgP%XTLmO zcjaFva^}dP3$&sfFTIR_jC=2pHh9kpI@2(6V*GQo7Ws)`j)hd+tr@P~gR*2gO@+1? zG<`_tB+LJuF|SZ9tIec;h%}}6WClT`L>HSW?E{Hp1h^+mlbf_$9zA>!ug>NALJsO{ mU%z=YwVD?}XMya)Bp;vlyE5&E_6!fzx9pwrdz474!~g(M6R?N? diff --git a/examples/skills/auth/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/examples/skills/auth/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 1b9a6956b3acdc11f40ce2bb3f6efbd845cc243f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3918 zcmV-U53%r4Nk&FS4*&pHMM6+kP&il$0000G0001A003VA06|PpNSy@$00HoY|G(*G z+qV7x14$dSO^Re!iqt-AAIE9iwr$(CZQJL$blA4B`>;C3fBY6Q8_YSjb2%a=fc}4E zrSzssacq<^nmW|Rs93PJni30R<8w<(bK_$LO4L?!_OxLl$}K$MUEllnMK|rg=f3;y z*?;3j|Nh>)p0JQ3A~rf(MibH2r+)3cyV1qF&;8m{w-S*y+0mM){KTK^M5}ksc`qX3 zy>rf^b>~l>SSHds8(I@hz3&PD@LmEs4&prkT=BjsBCXTMhN$_)+kvnl0bLKW5rEsj z*d#KXGDB4P&>etx0X+`R19yC=LS)j!mgs5M0L~+o-T~Jl!p!AJxnGAhV%~rhYUL4hlWhgES3Kb5oA&X z{}?3OBSS-{!v$nCIGj->(-TAG)8LR{htr41^gxsT8yqt2@DEG6Yl`Uma3Nd4;YUoW zTbkYl3CMU5ypMF3EIkYmWL|*BknM`0+Kq6CpvO(y$#j94e+q{vI{Zp8cV_6RK!`&C zob$*5Q|$IZ09dW=L!V zw@#2wviu|<#3lgGE8GEhcx+zBt`} zOwP8j9X%^f7i_bth4PiJ$LYtFJSCN$3xwDN;8mr*B;CJwBP2G0TMq0uNt7S^DO_wE zepk!Wrn#Z#03j{`c*Rf~y3o7?J}w?tEELRUR2cgxB*Y{LzA#pxHgf}q?u5idu>077 zd^=p)`nA}6e`|@`p?u}YU66PP_MA}Zqqe!c{nK&z%Jwq1N4e_q<#4g^xaz=ao;u|6 zwpRcW2Lax=ZGbx=Q*HhlJ`Ns#Y*r0*%!T?P*TTiX;rb)$CGLz=rSUum$)3Qyv{BL2 zO*=OI2|%(Yz~`pNEOnLp>+?T@glq-DujlIp?hdJeZ7ctP4_OKx|5@EOps3rr(pWzg zK4d3&oN-X2qN(d_MkfwB4I)_)!I_6nj2iA9u^pQ{;GckGLxBGrJUM2Wdda!k)Y>lq zmjws>dVQ*vW9lvEMkiN3wE-__6OWD0txS&Qn0n22cyj4Q*8(nG4!G{6OOwNvsrPIL zCl-$W9UwkEUVuLwyD%|inbOF*xMODZ4VMEVAq_zUxZ+K#Gdqf!DW$5f)?7UNOFMz! zrB~tuu=6X2FE(p^iqgxr+?ZK;=yz`e;C$#_@D9Lj-+TDVOrva>(#*PVbaHO>A)mhl z07OJWCqYC60518$!&c`eNBcBW%GnfaQ*$eazV^2_AW?j)h;J1nUjN(I9=0+!RVx~% z3@Tf!P0TE+98jA?WceK-}A1% zW!K)lyKcGqy#M~})315-A#2NXQ`?6NR#Apo=S!oF=JfpX>iR*49ec{7AN$xxpK{D$ z2d%Fz&rdfSqourN$~Y^NFIMV1CZ?J*bMx~H3k&meGtH@q9ra2vZxmA$S(#jaaj-g4 ztJmxG+DLV<*q<|sDXPp$X>E)#S}Vm&sRaO5P&goh2><}FEdZSXDqsL$06sAkh(e+v zAsBhKSRexgwg6tIy~GFJzaTxXD(}|+0eOwFDA%rn`X;MVwDHT9=4=g%OaJ9s%3b9>9EUTnnp0t;2Zpa{*>mk~hZqItE_!dQ zOtC>8`$l|mV43Jbudf0N6&&X;{=z}Zi}d1`2qmJ}i|0*GsulD3>GgQXHN)pkR6sf1 z?5ZU%&xtL}oH;YiAA)d*^Ndw2T$+Mjuzyzz@-SM`9df7LqTxLuIwC~S0092~+=qYv z@*ja;?Wt!T!{U?c*Z0YtGe)XbI&y-?B&G2$`JDM)(dIV9G`Sc#6?sI60de6kv+)Qb zUW~2|WjvJq3TA8`0+sWA3zRhY9a~ow)O~&StBkG2{*{TGiY~S8ep{V&Vo2l<6LWsu z^#p0-v*t2?3&aA1)ozu|%efSR=XnpX$lvTeRdKlvM!@|pM5p2w3u-6 zU>}t2xiYLS+{|%C65AzX+23Mtlq?BS&YdYcYsVjoiE&rT>;Necn6l^K)T^lmE`5u{ zm1i+-a-gc;Z&v-{;8r)z6NYfBUv+=_L}ef}qa9FX01)+Aaf+;xj(mL6|JUzGJR1|fnanb%?BPPIp>SCjP|8qE5qJ{=n5ZGw?81z3(k;pzH%1CtlX50{E7h)$h{qGKfzC`e2o`*IqA#tjA z`Fz&^%$b9F*N`)U-#6>a)Z`55`$Dd0cfcs0$d13^ONrdCu9xcv_=n#WQo8stcz3jP9|2EvdI-RhJM3%Q%oM&!OlShM|0 z?gz?wHZSnm45njLtsz8PVT1S&jAlbKg5kVam$p16=EK@Sj4EP0OtH zmJDmdc^v)x>56Qg_wmYHz6h)>kl_h$>0@J!ypv%APmjZTAQVLy6Fu50RGY&JAVNhx zrF_qG6`x9MkT;1SFWo$)l{M$;3qUDn9JwE}z zRl#E_bDRJFii61kPgBybIgp8dNW!Cc1b*^YYk-#oWLJvtM_v^hQx~9?8LD4VFFxBF z3MlrsSC%f9Oupn*ctPL0U1fwfX?`tRhPD{PSLFPQOmIt$mDy0SgpNVvHS+f#Do>h1Gn?LZU9(KaN>Q_=Y*_T zvtD7%_u^^+{g`0VGzg(VZrpVQ6Ub5M=tI_p7T93R8@3Zulu3|#{iNcu!oiHxZ4Rf*( zfmiN$$ru(*_Zqn=`Gq#OuHRTSwp7uH_SokR&|)RuW5yo=Z|_4?qU-JU+tpt>!B&Is z@N(=SG;bpVc;AO@zbmMM zScqq1)b-ZQIrs={oD}|?6y{$HNB1U0^LsBh8JI&3!GBZxOXI<}&5-$lgkAaYqhOTb z?2vEnZ$-kk;*M_17(upJF3%+iH*s0-r{vttXVB2OUwI1s^+G(Ft(U8gYFXC}#P&E^ z>T@C^tS`Z7{6HT4_nF~n>JlZtk5&qDBl6r|^kzQYe`wq!C)n@$c>WOPA61NDFj<<6 zGW71NMMhwAl!U-yqrq2xrSFqRCI8acw7?}3j;ynxo*-b7Co;g5r%^j=H@9({PXXBf z@r>U>>N;E)81wx`B4f%{PB~MHka_);%kBCb(d|Jy5!MqJ%2p`t&@L)4$T2j&-WHvG zv3(uyA_gwqNu(k?jQTtv3dgPKRZoH8prxe7>pQBW5L&dpumS&5Ld2?(sCpJjvc4L5 zEnh&?91WVm)ZdTj=fjJ$pPDdgAttLXuke+?KdKxu*;kTC(r!tQk6;gxj4h%FdHAt(^M3YvYj(!tOeN)+Hvj6+< zzyJRG?^lZfWuR#t!tUKP&(?%3v&Zd$R2YN>lB(Lq`OInY48%4%yTv2 zYe1{G`3)(PDEio5Y@-I5tUf`c%%OCJMtSW56g3iEg%3`$7XSJJHyA z<|7&N)5Xrlgv~%BO24eFd;Hd;uiK%D`EdK|quUeRZDqbh9l)%j%J#0lfrZumvA<_w zu&=AVvdChf6}eqh(bUz`(`Ue*p01{fBAcTgKyDYLs_I+YyJEk+rM@avU~>fB$n)HS zM7pfJydu`i%gfS<{PF94kZDv$t>06sAkheDzu40NJ$5CMW%n^Lls?8^p^QGWURbKu3ZduZQZ((s2? zzE`}<{;Zt7<$C|9R8A~DJ~@%x>TfP zF>TX8)@v|t)q4GjRt<}5s6hLHwRel7>V@&r-O|Av(yh;Q1A{E>Ir>p+%dHD|=l+lT zpr(Dg&>#Nu=!)6bCLr-ZS%|;h)Ij$+e@r8_{qO19QvDe=&1tmpY*0lcA^Cc-#{9fQ z<~$*<&P$Q<_jy#<$40PMofM7aQ}C=jphI`4kLg}Z7CIN#26D{-4v-_CA-LiE@(%{y!BzsU%gG`Q?sjLUf%qFSl0y)2#ae*+EI>s|i`d^V$Dn)qmzqRq6VJRY|{4ujsIU%#bnqU6MR&-1I_43=|5(6Jr;Jvert) zE?S|Tmn}Tv<-??sxV5@9t}3D=>YZ0JrQe$CO~|EY=Lj9RM&4svQHPQL6%pV5fPFiH zfXDx;l@~et{*{U*#c#Dvzu)|znDO7$#CRx)Z&yp-}SrD{&|(MQtfUz~n35@RLfUy=aqrhCX0M}J_r5QsK~NmRCR|Nm&L z41UdsLjWxSUlL41r^0K&nCCK>fdR-!MYjFg(z9_mF^C|#ZQw?`)f6uVzF^`bRnVY& zo}@M06J&_+>w9@jpaO4snmU;0t-(zYW1qVBHtuD!d?%?AtN7Plp><-1Y8Rqb20ZaP zTCgn*-Sri4Q8Xn>=gNaWQ57%!D35UkA@ksOlPB*Dvw}t02ENAqw|kFhn%ZyyW%+t{ zNdM!uqEM^;2}f+tECHbwLmH*!nZVrb$-az%t50Y2pg(HqhvY-^-lb}>^6l{$jOI6} zo_kBzj%8aX|6H5M0Y<)7pzz_wLkIpRm!;PzY)9+24wk2&TT{w--phDGDCOz{cN_ca zpnm7`$oDy=HX%0i-`769*0M6(e5j-?(?24%)<)&46y0e&6@HCDZAm9W6Ib#Y#BF6- z=30crHGg+RRTe%VBC>T00OV6F+gQDAK38Ne3N9bm|62tPccBJi)5{B z4zc^Db72XiBd}v$CF|yU{Z=M|DZ%-(XarYNclODlb1Kz1_EKLy(NSLCN`eUl(rBCL zT*jx@wNvze0|TSqgE(QArOZU)_?qH(sj#TwzElLs9q)(0u!_P|R%Cy_0JFQxgGV>1 zz4?_uq<8_gM0`c*Hh|;UMz~vrg1gQXp{ufg`hM_qU;U>+zmvc5blCLSq@PrEBSGR# z&8=2Z4uXN`F3p73ueD1l{s{k$WipAvSh5W7ABe?4)t;r@V?y`bNB5FvBuE|0VRTb< zM1Hn^?DSsJY+sX@T5xW=#>T9VEV|?<(=6|ge$X6Sb05!LFdjDcoq*gM(Zq=t;_)Le&jyt(&9jzR73noru`a# zN*<`KwGa^gZU3-)MSLF0aFag#f0<>E(bYTeHmtdbns#|I)-$)mJ`q9ctQ8g0=ET?| zdO}eZ*b_p>ygRTtR^5Ggdam=Zb5wmd{}np+Jn1d_=M`~P=M67jj})fH4ztb5yQqQW z^C|C&^LHAK-u+ooIK)yM)QM?t;|<{P;;{`p=BclzAN#JzL4jCwXkQB1Dy{=^KR`=~ zTrr)y7eiYBzSNs_DvO=4A6#EgGS-zY%Vi)N*Yb`U;6o}KR}dq{r9pT5wqZ@3NOE8- z9-(}D|Nc5732CSYQbL)!gPQ#RbD8BhK3dl{sUuPvei0tkvnJBxDEAYTesU8H$)g(Plra{VH(v3u^CO1~(+ zU0O7#)jaS4{NcwA+LuSm&VBcX2#Im3xg)W}ySNw%->orn1taZ&+d)}8gJTqA!u|5P z{yv?zol_3|(1(%M(EVU=cp?L`{Pi|ixk{U)*guFML3P!OSlz;zGA#T+E@8@cgQ_mv1o7RSU=Zo_82F?&&2r;WE z@wk}JHYEZ9nYUc(Vv~iTCa3u8e4q(yq<29VoNbKk|`mq%I6u)My=gPIDuUb&lzf4`MEA9^g8u z)vp8|$$HE9m_BTV?lOosIGa4jud=jIbw)O2eCMfyw2*S8?hjWw^nqws$O*M$3I1)x zR0PWFb3$ySOcGTe1dz%N0l;RPc`x%05FtT^f^j{YCP}*Q=lvp4$ZXrTZQHhO+w%wJn3c8j%+5C3UAFD&%8dBl_qi9D5g8fry}6Ev z2_Q~)5^N$!IU`BPh1O|=BxQ#*C5*}`lluC515$lxc-vNC)IgW=K|=z7o%cWFpndn= zX}f{`!VK02_kU+Q5a3m37J;c} zTzbxteE{GNf?yLt5X=Bzc-mio^Up0nunMCgp*ZJ;%MJvPM3QK)BryP(_v@ei4UvHr z6+sbCifQaOkL6-;5fL8$W($zZ_;CZp305C;~$hhRquZr-r)jjd1z z31%ZK{-(`P#|Um_Sivn@p$-vz46uqT>QG0B1w9znfS9A8PB2LaHdzA|_)yjXVR*l{ zkcu3@vEf7bxH0nkh`q?8FmoO_Ucui*>_a~P?qQrlZ9@+D7%MTpSnztpylXrt5!-k8_QPB?YL8Kx_On8WD zgT+111d(Op$^$&KLAN5+@?>f7F4~wFi(8TL8+szgVmcMDTp5l&k6~=rA{Dt}!gb^r zSWY<)M7D|Z2P0cEodj6E42PV>&>DFmQpgt)E-|#sSUU@uKed+F680H@<;-x{p|nuH4!_mn85rx>wz;0mPi2ZkL#k6;sznu?cXh!T0S>{w6 zL^gvR05NY64l*<+_L>On$rjx9!US;l;LX6@z}yi#2XHh)F@Oo+l)h%fq$v}DNmF2> zfs^_t0)3N-W<9-N?uedVv{)-J0W5mh#29QM5R5h&KuiRM=0Zvnf#lF=K#WlCgc#9c zS;qvh(P$!_a8JwyhI^ZJV2k+B6Z^64?w|1?5gyo6y{}923CRZfYVe1#?F% z7h2SUiNO3;T#JUOyovSs@@C1GtwipycA=*x5{BpIZ_#GCMuV8XK=x;qCNy{d7?wA~ zC+=vjls;ci&zW=6$H~4^K%v{p}Ab?U%C6Z4p%eC<3ExqU$XR<}LLF67A$Sr20DR_pJ3yeBa~ z^sw{V0FI5;UpwXsScYuhbqGQ`YQ25;6p6W^+tgL&;Ml;>S3CGpSZ>VrTn0m1$y$HU z&65)I!c?oREz};c=nLCliriqQX->4uivHTgd${GqeAlf*!P^B|jkU|*IdNP(&6C>4 zqOW$)Nw9nvjy^&`?E|gotDV{JmJ9Q~vuhy<`^C4XIUDt|j4o6rK^e8_(=YqC zuaR6TRVf@tUFHB079o4MBIh{M~4>WwnGgesQH*3?w(RA%hCZ*7)b!aNV=yOQ%o_Y=Lt0Sl*(9^jfRnC210Om$=y>*o|3z} zAR&vAdrB#mWoaB0fJSw9xw|Am$fzK>rx-~R#7IFSAwdu_EI|SRfB*yl0w8oX09H^q zAjl2?0I)v*odGJ40FVGaF&2qJq9Gv`>V>2r0|c`GX8h>CX8eHcOy>S0@<;M3<_6UM z7yCEpug5NZL!H_0>Hg_HasQGxR`rY&Z{geOy?N92Z z{lER^um|$*?*G63*njwc(R?NT)Bei*3jVzR>FWUDb^gKhtL4A=kE_1p-%Fo2`!8M} z(0AjuCiS;G{?*^1tB-uY%=)SRx&D)pK4u@>f6@KPe3}2j_har$>HqzH;UCR^ssFD0 z7h+VLO4o@_Yt>>AeaZKUxqyvxWCAjKB>qjQ30UA)#w z&=RmdwlT`7a8J8Yae=7*c8XL|{@%wA8uvCqfsNX^?UZsS>wX}QD{K}ad4y~iO*p%4 z_cS{u7Ek%?WV6em2(U9#d8(&JDirb^u~7wK4+xP$iiI6IlD|a&S)6o=kG;59N|>K1 zn(0mUqbG3YIY7dQd+*4~)`!S9m7H6HP6YcKHhBc#b%1L}VIisp%;TckEkcu0>lo@u995$<*Em;XNodjTiCdC%R+TX|_ZR#|1`RR|`^@Teh zl#w@8fI1FTx2Dy+{blUT{`^kY*V-AZUd?ZZqCS4gW(kY5?retkLbF=>p=59Nl|=sf zo1Pc|{{N4>5nt#627ylGF`3n>X%`w%bw-Y~zWM_{Si$dc82|=YhISal{N7OY?O`C4 zD|qb}6nLWJ`hUyL+E>-;ricg9J@ZNYP(x(Sct&OI$Y!QWr*=^VN;G3#i>^1n4e#Je zOVhbFbLpXVu*16enDM+ic;97@R~u&kh__kgP#!R`*rQEnA+_dLkNP~L`0alC|J;c; zeiK=s8;BsLE)KbG3BD&Br@(Ha@SBT&$?xX`=$;eeel=|R_dIr6-Ro?=HEjnsJ_b`1 zK6Yg^-6;^2aW!xeTK)A~3Rm|L^FCHB_I>jIju7ZGo&N_1*QHkxH2!!%@o4iZ?vntS;&zJdPe1dH#04YD93A44o-MpfD zP{rn_aq>U%RDvC2+bp;xPlsOzauIi3*Lf42`jVKKZCRuKdYhi>FDuL2l=v{$BCN#Q6796s%r-AG$Q^t(3c@ zD?w0UhYr11@feiyl9kY_@H8~|xlmO<8PfQmj1!$@WieW@VxR@Psxfe-v9WCi1+f>F4VL?0O~K7T?m4-u|pSkBpUJZZe*16_wAp zSYZ@;k`3;W3UHKUWc8QeI}0jH5Ly=cGWQPw(Kr2fm=-5L(d`lcXofy8tJY3@Tuadz zYWXR{mW7XT!RF#RVCe%}=tM*O6!AD3^(!8un~opNI%Uko7$5t@<8+?; zTxDys(MyyGsUjtSu9$+|_-t!U3fVb1dkK?l`17<+jfl=hrBHnDSV>^R1=TnQeyqbW z>ov#l%!1|S!1>8UUxIdhQq`_klcHVx0{?#>K3#$4GlXncwldt!g17TcvKq-jo_996 z>oA=tH9CqRl6Yw?Uc`am!V?lHJbizOJaVaScf1UP5e7Dbgabq=b!B~T&_F6?ooU>w%x0A zH~&MHJ=q`fCH{U<7MDXE4SD32cDZA)WJeWkllJ`UspWaS#eDe^kg^oU_A14UE9zG-a^g{xaXf$})Wik>gT zl#dkzGr(;h0JZDuFn(+k8wNq?PZ5grQ<+sM?wBGt@JnH6v0#or-5wBQWKU~(S_> zkE!tc*ZJ1Y&*p(xX84POb3cClRMd!^qJ#CAZfIepEj-<`VURS_yCz0(?*Ixcj4 z-!zV1_QZhpm=0<;*(nm+F>T=)o?ep@CK5I%g^VAA+RB25ab?7)A~z~egru=I1S|@v zH7tXV!0wmGS^qj#e+MY;C5eUjEAp$Y?LDkS^QPZ}8WN85?r$u<-Epi;yZ1|J2J`se z$D6DpH~2F=eI0B&=UFAUnJvZAmClJlK)sutJ?M>xpZiWV&0=G4MZP+x+p>EX=HbCz zxls%Mw?*u^;LbHWIWCyq+yi)`GmFn9J112CZda_u@YIP%i;srFg_paU02Ifij*7}l z&CF-(3|>*a|+vbNR`^RP=9G?ymEJ0Z~)d&c*UE$UMepZ zcITr{0WqhxkjUnM15js_gW=e3Uh|y6ZReaXHIz-=p`x5VvB&rH9y>Amv@^WmXFEw) zQXYrk3feir=a{jMQ+wDIkkFnZ$k{sJakHn*?u za%4b!00ev8NVLM1TY=cl?KB&55BY_MU-sg?c>=Dbz_W{(Z~c?HJi*XpYL)C6Bd8WH zt+v-#0&o~@t4qESi*)+eW%@VD0|o^yF)n0hME$UtXF$*Lvh}7sso{`|pn*JDIy5^Fm3s$5*zEE=?u5<=l8FJc3r%+H} zdfoNl2J0^~!-*mOL5o-x32|e0Im*E!yY7F7E5N)W3>+v_LBydlEx?4$RL5f2oYRD# zaR0wv(-p~wO0eLDl3K=%`{5+0Gd$ktO=W)gWlGZJ0`K z$_RNA=ckrfa;H0KA~dR^p�(p-{x$&=IACIfoAR!za)F-^da-t3#0Dycnp zwO~NVXwXCl;jE<}>%@xz|=8fIJAB?>+E{7)|4l${4ngA3G|=r z2Dyv;VVWSgZx9Wj>qUjleGl3Ei9K4>h!(lPS%8VOG>Xu0%6VDz^O=bjJmuP7>DeUv zrbI}MlHB^^d?{zv6d=@_ZD2lg1&G7UjnVN{1}9WkaM3H~btX0GtSzB+tZ^qRgWo4m z!GmimlG$=wgXCnr6j@m<1gAL46#T~5Bnm=2{^@>|t&`9mkEPddj zAvG~@Tv~TAm2i%VW}R-g(Z0)z-Y|szHr@rk>4MAyG*Ma*7Yh#H7(!-5>DZ@8r;_dx z{prSe<>~099F8vsYd2xff7uAS%7{S)f(|@me3t2$iy&NEc7OUEchp@9A|X;;IA>8!oX+y(BKJ$EzV* znR$z;!L$s7uy@{OT~nG#B!NRraT8(X##Ho!0r_o@gg0CA-9H^;-uE&?$2$nHv_00o z%cbuUc-tCx$Uh&EZ4Nf4Zgqv)Y6>usG3>GeQnxx_Z6+PcbX-+ysbt1hQ`K1LDpOE? zrAhIZhSN9yVIAOa22gn577tbc&i3|3V8NWy&!tw##`}9*x}gtI^h1DzZRA>UuaJG) zaZ7j)dq!O}{?#8Y7~7i6fHh4{`pL?>-18|p!S75Y#^DM>-S3)vuZG+Q7l@ek zQP~#cBpWgg#mApc_sPYjpw8odQuRokmTkzcNl`^CcKB7e&;zViV;{Y{o^Y$%7i0m# z62%#1Lq!RC?}lK>%mp}T!3Xv;L*0v*>USLm``N%>w>@fwC+#T&Tx2bN4w(20JB}oU zuSa6v^kXi0xPs?pbaOHnyiqq6By1EZY9OZ^^QA>{q-Hsd&m`pbQ%8121aWG-F5xf zlZ%;B{;C>X19|`^_?dVyCq>n+41w7|!tUS!{9rHlbhX=SZO5CQ^;!Du_E7*`GiR^Q w)2!4MKjfSAeNo!9>IaV6aUZ*?W>} zs4%E?srLW`CJh0GCIK@hTkrW7A15Iu%N&?Q^$0+!{Tv&|t^Y@u%!L zglTg&?Q5q#ijZ;&HBQ?FNPp;k3J5!&{^+SGq?AX~SiOM9jJMRpyP?RCr@z38AQyy&WRMaC;n4una$~nJKSp?q|s8F00c9?Q! zY_ovvjTFm+DeQM^LXJ#v0}6HRt3R1%5PT*}W!k8BEM;Jrj8dIceFo2fhzTqaB3KKk zGlCLI)gU25(#u6ch6GeB1k@eHq7l{EHXv0n6xE#ws#ri}08kkCf8hUt{|Ejb`2YW* zvg}0nSSX1m=76s?sZhRY$K=3dpJ+y*eDULGnL2}4>4nvW^7_<~wIM_5fjvwt4h1|g z)g0Z6ZFq9j<~9~b8((~TN{Z?ZQfw|is&Xp~AC61sj;xItKyCHdI|tCMC_LbXF>~vR z=w6V3^H=W4CbAgR4#xw}ETTwu2guW~=Crl@SMXv85jQ=%y!s^?m4PI0My7MWICO;- z175jm%&PcPWh8QdOU(#8bp4!N7ET-+)N}N2zk2)8ch|4Q&lPFNQgT-thu053`r*h3 z_8dI@G;`zn;lH$zX3RzIk`E8~`J=BBdR}qD%n@vVG1834)!pS1Y?zVkJGtsa(sB~y zNfMYKsOJb%5J(0ivK8d+l2D2y&5X!cg3BG!AJ}910|_${nF}sC1QF^nLIhzXk-Y#x z0)&1iK!O;Og0Ky!;`b~v%b$`S4E&fB)1NB4v@8wr( z&+NX4e^&o)ecb=)dd~C!{(1e6t?&9j{l8%U*k4)?`(L3;Qjw z#w7FS+U(94MaJKS!J9O8^$)36_J8;thW#2$y9i{bB{?M{QS_inZIJ!jwqAbfXYVd$ zQ5fC$6Nc9hFi8m^;oI-%C#BS|c8vy+@{jx6hFcf^_;2VRgkoN(0h!_VSGmgNPRsxI z8$rTo0LaYq-H5i&gtj81=&xU?H-Y2==G@uQV7E`@+2E9XQW@{&j`?EOktk|Ho{HU>ZqDzvgjwBmdex z&uZNd2C1h{{}2k6Ys9$*nFP3;K%u!MhW`uZy7Sn`1M1zs@Es&;z*Z>Gsh@-3Fe6pE zQD2@cqF((NrRevgvLsvM_8;;iNyJ5nyPyy?e!kvKjGj`6diRFBEe49Oa7wwkJFV7Z z$YT&DWloYu-H?3<0BKn9L&JYDT-SK~*6c5pi18P26$JESKRYj{T7Zk6KiRJcbvOO*{P56Q6s8msbeI3>|j>K9}Q9UBeq*inXKemCm`-<5|-$ZyN4u$(3 z&HcvqehFD%5Yrmykg-^d`=BSa8(i=>ZoC77^mWY{evp(km@aHqhUECBz76YiR+VYK zY_avFC~V3$=`6C4JhfHAQ@DZtUOwH`L;oYX6zK0-uI^?hS$ALfq}A7evR;ohJHij} zHSZdW?EKv9U1s4oD*<(0oQ*;MaQ6@cvGL zuHCPgm_NhVsgp^sfr*ia^Db}swo1?O(_Q2)y+S$CBm+g=9wCOUPbz(x)_GbaKa@A7 zuI&!ynLiZRT#V%_y_-D`0Z5lT*auoe{(U5NylTzFSJW()W-#F6*&A`LNO1bV#Y;QJ zSbLBnp|B^dtK|KIWC|No>JjWBWE@n7O)x{&^E(WMeMvp57#qA8m* zeTow*U@_86B#Fm*rxyYu5PRWaWHx8y> z*qmHEp(AMDl0v)ij(AY8fnH=~ZwwjVAbu*m5;xPfidh@ov6d8g zfJsi&!QyK53Es%sC39ts;54V68koALD4b|%tNHW0bIkZAJKa=W&FomJSEDT>W1xIX z1x%Z>AvNIsSPLcn3RTcHXb@KB?cuM)=x6fcIx>&(GxqZ8w3p#jJ(GVgc*`c0HG}dv zIop&Qim!K1NFwic%07KcjWgHBPUkq7f~lj;TPqVGTiT#cUeim>;nY`>h@a*S{qQex zQ`z62WK|Mj)Y{tfF{;T4P;c8$Q|KU?Joh zIkA^z%X7z|r>4aTh@|StTi!-r1D!g=zb#3d#{{&K3CqE$Iz-UH<%37c zRfkO`&uM%#AD3PHv`g5t0e^O%nVL0d{Xlx^EjEC3#skF@`zl-7PF^0oxW)1!C!JxR zWvuAHH?)61FKA1QeT*_sY7;_Id#!GmV4n`MO{~sv}VLSK` zXRw=Y=Clz*00B(5y^K;gCZMAzjT5+c3IC=)l(9VIDdatpxj3y89WwI|bH&$!ZEvp` zPR!T@#!(|KfI-w?!&+7$N3F6>tD{YO4Qg$d_`nNEdfVCha9vaPn0jI0`)`@*72hq! zpU5ND^P*RoEkbD5o#az(-g=Y)L>HH>Oc%}$ zT3Rs_ih0;4+Lv4Y;@Iv(;fUbQ=i-G(#>vghec~*j(I#r|5mqFiJBpzi&hzEcD{u$< zRsm0BVYn=pT;0>R(itW|*D&;O%bOc7et9ACaH#J>z3A1A~6fdP>pmbM%xzm4>|;c_?B+%sl;Qs2{t!60$^u zH1t@9^6>;?!FuusnISi$f5CL&;z?EqJN$FBuWDA#D5`cy_UvCFIVvf{c?4N0teh;d zET$7aVbj08KTQS!x?Nd1Is8q8qFzs}a=!@nJ;7FSfCY^T@D-gpw`w<6e#X3+;O}1h z$%I!M)0bg|EKUA04Qjn@+x{Rj8vt6Wn!R|3A92z}^$KfF5(#CWr4y#~re1CN4i4w0 z#GsypBR{xA3Er7sgAi(|}1-W?s~n$7?K|9WL8kpVfw-;#b9 z+mn;=ep!162U5R>_t}fOt~tE?s#m( zO-S$7>Ay6*hHdZ)7_oU915WYYCIX;hFI-U2EWYX!pllONr@Q--2o~`!isi6vTPLJ4@(|o=%NHYjo0_S&q*UQIROw@*N-By@PaQ&;YxFZ0aR zX&}LeOEz);#m~Hwm^VAY8DK}b$F4bo{jMN?d!lxKPhNklzr^Cd`0f4oJr^z=I|l`* zm8AHm*fPV`0=lF3Pnnp}&J0N1X@}-D94YvmUabFrLGSnTz7Mu^21F#O5tN#CuY9Vh zUZBH=ez%h*wkf0hBtXJh1SN3d+IF{gzT7lp)j}n?03lt;XSQRAh7qd&v;RwTYDuQ# zbI2*r<>?x-G0@hM{;%{VBD7nLKt~D`T~-HAt5;h%i0_=Ifs=yHma5dhJ+QMG?Ux(a z|E?1CMy1!~oA`FP!k~iG=t&5#>bVdz=peT8HMB6Y)#7PpETtNryT^+Rv3vpJaF^zP z{H}0-LyV9Fu21ID%wO9f1IKlFr1p4c{o-?03vyB-tr5duk^&L$;m_|f$vs`^Sl{j2 z95}oY{LlY+=ZS%J+tZoXCd0*sSU7w^gjovXn+g7uyra5{cU49@yHf#Z^Jl-$9cIfo z+AJuxH$VLb=#+uBbVmUjnx zxb1pZ@-O9=AIk4@S)m6fJ2?{HrNYwwnL3a45muuNjr;6$O`bGEM0T4A2_S$t=86*- zcO+0mywg*j#A4mU}enR_!cGmIYQ;qwfchWtFEXL)AK%*;=j znYne+hS4EMy3S)C*mZ1KI>!+)0V@9!N6H$Y}~MJ{rYuf zz^KljIWvFi-?#?V@LPR&c6Nn{!=XM z>}-h$S76;$H{E{Y%@^zlmOl^efBwa%UU+jJD9UVukQ3ti_kH-?H*RC0?M1W%FCvMB zM_+v6fk$6X2sx)-p~B3&Kl{nscK}pNLM*qjtpaf9>AU{-iPKQZR8yCg!TY}Qg*(;) z)gdvCcB%kppZc$VdvsK@)3l1{&DG!d_6OHOS`y=ITLEVu`unSKA2E%JD*DVX{LJ}K z9l>hMRDqxQh0lnpGHpVYneX}eA3Pt|2v%=q;rt)``R|#bDyB)OXY&vI_@|*}h}G?^ z@aZ4_!7cQPX`!fW_?{oT1NTwHs#l5L-0`E|y@48<3Q^HFf8=Idi zpJYD%1MkII!~|7I^WGo)IF=?{>ACnjJ_WUi39C}!Q{QnheVJqeKKqq5^o5CBde(g9 zvw$X6^jz_^E2$wSw4!q5*RG(C2_^XO$HBn_55vbl44OnTTRwRaePP0vo{K)U1#99& z<>rq7V&V(<&@I%MFoN5zrY}sz=(*-L&}1QQ*a%`u25h{cFj===17eB_uGuzG&byQ< zrm8BJZl4r_E$3k|Wo6FW0-6M7>qac5uFQsQcmkLWGfeH74S3Z_rJ!jgN++!@i=HW8 zkyjI(oPH-+-N#Qc^-mpNO`bc6r=2-<%&Wy5K1vfFJB(L_IkpS6fY^NmuL8qsgj>MD zn~BHH9WM~32_3vd=W&B)k7F9q%stJx+b_L_X-4zr^LVUMCmyCTA3sWtkvsmME?Xiy z?xOSfB=_$oY06~J-HcCq&)qcW{j;uP;?Dm}=hkq?zh&n!;m((-G-u_t|6x399Q;>A zgNpxoJNj{u|MFDH7Rhq@FCAl0dE|ddnl!oh9{Lq?@JDoR6L;C941IK`ISfdE$4S zE0AUQ8+2|Ncl_q5QkSp#AODp~(^mfP&%Au@@|TBQwoP`UU+V{6u8|)6ZA{~uKmQ*M zmrMTDU8S~8Eqi{^v0Ug&5Upcm#y7Z1(RbgZAG8jB$eRwCspQ)>5;U)oGZ&E5aeR*K z8Yt`Y0$G))Yd(Y3KH}tA4`-_QmNke5hU_|nq=xtyjwW(_o?itz>B>WM&^63bNdQ)k@-IgDHW*RW$Xo9#RzrTrCn7L2H{9Amq|qNg@#eZY=|P zCoI?2s+L)zsM%WX(NbVEY^`C>lFjIBYmJ6@DKJ0ZT4&F&WHW!dwa%QzOG!?jY_2(S zDcEzZbz*2Q!43|z))9yOP9X1Xt%DXzwY(3tl-TR=Qb_MbZYRrooh;dYYmS!U_as1(=YVB?Q_A|tNu5Ut&_q3jbfDM zoFxT^uEuH`nX3*sB%K?GuHUkweYReBwnHqh3P)~`+s3+Tj!rDA1e)8vuBv5J*IsxC zkd^~b(aGzArj08{>cnzOuy04C+C`}gb|Yz-1avxeWzev3NzcHbz_&4W@QCr$z3~w=8Ua- z`;vfG1~BP8CyLb=F7t1am~ph_#|O%$khSJ9%Vtcn)YmpgQxF?xM^_Vb+5fnpB^W0I`f%X8gb9#X{Q-yJG0{Z56aWeI&zPxnf5pdJA38bM`cYnS#x)% z`n1tFf$i)W-hGm(f9mde^=X@NcV_lFb=P`4&CI&H=IArijGwdCk&X@uQ$5xmj!~^? z#$ROCI)V-~t%L%GS#wo@U27ddR`4`3)WoB{R-4snfNrfee|kI8^bu#yDgYqOwas9# zmcb`3!kRJ`Cr=_tq)8aMt{aGtUZsqwVlj6DgCGre>AEt&x8H_in!x@uwgExIh|-mA zjdaC(29~CTVSaaF7HPbql&*9Uo8P@f)>LqCXclr}peS7_1BQ28u9PO8Eq1@`l3q9o zkfKCaO2?T?ZyA6loW<#9_c^O=m<&h}CA!ineAD@=(gbq`vyT|tiJ6#^B1$P;;qax` z55k&Q?wEh#87niLo*+n4L@65J(Nz~=Ya%7^(miLb(E>A3B@|Jjl;FU&D>o|9#7PJH z?|ago!o;WC^h=|T7PVBg(DAB}72cyUS zb(f>Bwbr!F1eTCO5fpj<{PqhY5>143p?~5ZA5H40);=@M#MYvrB6gqHbU_!GSY??i z%s=>-ciA4*zOOZHds0a(kWewZ4h(k8h(ua7HX)Au&mY~H8KY6(_cb$_&fA@QjIW-*heP3%$d!m5^AdnT}`12qA^c@!g3DOwZ5WwE2?)-yU z!)Vx#Mtxt?FzFTwK!77sy7)sMzUd->w4^bxtpM2j!b1pjgyk zGKwWGeb4)^zjy{9Es&PU1}gwg?|J#L$KJB7ett9@4M%-nGtIQr0>Fl@8-yh`-+1ed zS6r}(MeSvgSoFmH*_WPu@i?}!AB~2?;i&IxrkNg~cQ9Som98tcq)k^|eeER|Zl77t za-TVUc;DNvzVXJ%w52+#weN?+;i#{f#!Oc&z?81*N>^e~ltRS%ZI@lR{rs()HmqG! zx*}ZrI-EZ}ckJMiy>A^oofwDfC~IH)z8{VHKGT@#E5I(Ll&+MnMCl>~AV7+>Gi%mF zkU1QlKASdR0B80!YhP<$Ywi0?W2Ux45oPfxv9QolWzJPD^weBfvo4SONxP35106sAmh(e+vAs0GboFD@PvNs)jNPvarhW}0YliZEg{Gazv z+JDIpoojRVPr<*C|BTq<`6ga{5q^8^!|0cxe=rZ!zxH3%f5ZO0cQ*Z<^$Yt2{|Ek0 zyT|*F+CO@K;(owBKtGg!S^xj-Z~rga2m6nxKl9J=fBSuNKW_dLKWhJKeg^-Xe`^1? z`TyJj)8E!#>_3Y?uKrwqq3LJ#SGU>AzUO|6`nR^u&3FNN_jGOc zw)Nw`wr3yIKhgcee6IaN=ws>M{6677%)hPwx&HzC(f&u~&)6@b2kNRzBDQAP0*H73 zq%McOmRk{B3i47qRe=DA*$&odrbEJZ*pV9XXa&p@wlW~@Yfs>V{yiTtplMhgM*-Bz zsSnlq&pG;z0OUN%$~$3=g1UF+G*>+17eRbBf3=y79J}KR8owon@$1Z7MIrvvWWH)34nK2SD)GsrJ{l z1Cl#oVo3A8qY3e=aF)qzms~FG#2$LzT=gs&aVMOj>(%{y<&O0cG!nCiESl~x=^dF{ zKvj8F1K8Ng171wwM5Fh4KoQw`_c6#y$(5cAm7e}~nJ#A*fx+c9;y#&W!#VukR)ugk zKp3=+;Ut+IYn%m+r4d*<`L2h%aDnX5}^!5R|H;(34AoVWjRx(msBZvk;rCI*|~ zdOijqI@9Z{Vu!~jvHW{lBa$rnl4+!s_5sfK3bCGk-B%iDe&@-}+%fOKU|(9?V1 zHE8&@4z)Kx!RAvAs z!Wic9=o#(bg?kc-G68-m(jZ`^=XGUXb)}t(%&~sjFnV^sEX%hSy6UKC4iOhgV=BHV z2w`4g7Y=s#Vu2B_?#VQ|hP39@eArgfX>-0S+dd&^mx0*wp}>)x;c4RUgxz%;oNe?& z-7-lJ@Y^2^C;=qJsxx5|xF)*pTGhch2B&kxtn;f!7=gznk}I3}Dh}(CoMXgA5-p&kS202!l?!fT3t|HG*rIP~mS* z$Wjo}jq3}z$Qq!9yrtd3fM0N629ZM?LU$nv@Tv9b7I;D|;0H2dsA~g7Z7zp1| zB)XmrkMgF6OQr|R)HHD^TE{Y#j!~SR?b`Xt3Qs`B+x<hxexYeAjMUWdZ-*n9%(1)Wb(n2U<><7&9dwGJmrob)4%H? zlQ%z+L-^$dFhhH|@u$%97Qz?*Ynh2VG@q|?8vY&L74&fs&_b&3$x&Oyjl~LQDRRap zJU4U*R+(2Dd!G+lh8!V{pT_UJn+^1Qg6$` zqkNm(a#hWyc6SP+p5=C4HL8-m`pO`5o~`-LI?_h5CsH?F_%?nDodmz&pWR20WTpJE z?N|wSzLjMUK8E)a2tI}Lf;+;*M|h3Y(U#>)g1>zk9|Hd}oZAa2 zLYBWBoSW!Ts!RwXr^8h+U*@{9{zqS^iH)Op<;r`Uw~nc}<^$V~_i%$GFjaG?X1@E|M`h)nekvFKt`Dh-f>@|0-`Xoq)o` zx;JmzDfOV9qCx|EVpogEe0LK~tGS?5$$L_i6P$P6wIsCQaP_;d{{N=iV@+8LI}o#( zvo*Ejy=IIn{rdIQh1&q-{EuohpVOjJ^Q3lD*YTp37$^RRgn8ihpdu5{Ct%5-KO!VL zcNB6dUajXI9jkm-P|i3~GB-A(X`P1Oqqb$tcku)UJw0w3GeUijb__#QT4j%64z%EeB7S?jlWwx_7&+EEvB|6N=kV}DwnyAlX=?j`) zmU#!$*^@NIu#n_d7;WoJV@*Fbv9|yJO4;n|BNF2xy(54RyB>t~8lUOUW$&2%Nwi1y zx6JxW88>U2$#qhl^6KUbtmg9}D0o5vYDT7kWJthLGkpGnN4T>{St^_EU>4;DmLF9o zr|LqsA8_MoNLQ=}w?8u!ziSZ@PC#Y<#9uJFo-ozVo6D;<8j^1$c|qAE3ZTE5i~zmE z$BU5lw6l=EWsg^y^;8>r9qH{xfL|~PZYK#md$zZ0?o11gV<*WSW~cgy2GYGQir%wf zt4iW8D+;s*;RGrmd(-T<@2&j(Cb9xhV*l-x`TpK`xq|7p?5R%5*s!69?2c!cC*VY* z2DE^9pvOPLU!1e}wA8S8opcTJ3`NB>hY=JQnL~QFXR4K8A$BqJnoEB$wn-%u@E6Mh zCfMF4kusv3N!(aHC}4)Xs^xoOwXd%e^6pi5|DZo=Q25j+6HlJ^7FodH6y1bMROR^q zGu6)fopS`h%Sw<;ZH%TEPf+#81-#_v+@8nlR0jLcIDKQtLleOC)6yLZgC!D9X3GgS zohwU{v$jl=quD#Go^hB{`@Qw*a%`(^jyT~=q^bWgGzRj;|12J55HWdCWV}EB|K=%N z3Nq-qxJJ`>^|1MNN+q}zTB&ooE3j==AgK@^UW<^oSbeALa2peF)Th6{@sj0KyMNHZ zksk1+MXN2tv+22A%cQOGpS9)77(uP9mh+!5T5ERLvF@b}$+WvXM45Z?-kCa)fb~f1 znVbTD$Gx-0Zxc`0D@YgHakge6SL0H`-vN_x?AP0>iGH0_EE&=v83hMJgaKAI0jJXm zVxVz;X<$v6WW7}fxROO7vr#YLP;;lij5VrX{;>7kK6TtOH&6|Ar^xo>00%+u$C4@# z>!jOt6*3><171+WxoZnKDTzJtDRw+T030;yI}~uV@9fCnei^I*j>Bp&mzP2d=FPb_ zCM*l_+$LDR3B*a!A$g#>xsrZvw0lckxmMg>0aQd7tPyN=t{dgXb;Ie+T8{fZH=gdu zM7Rg9c(kg(Jg0?ARRRl=AONFKrvFj)lTY$KfT%6^6s`mk*ABGhsce*LsoD>K{z_M2 ziPpnu+lw22PfF!CoId^6n*G4H(Ix+#+N{C(da7t1BYMGEaE#PdpOLxsVD5riQXHp@OX;`S`8VnpM~)I920w~<3|mo0 zf8~Az`*?2?H&gZ&*K&bRkV@qzvMlRHXys8*Ze2+1c?5o!^+$&MHxB@4Ee5cke52R! zmn7AZtY6ST%ixgU5)%$%QcwHj7Es-Qu^kLAPwy%7pGBw_4Q9#da^W2$}axNHr03)_nw z5?yuNmXrI5HgS46)c5&}B)Tts49oU92>3xBLLy}FMUW=84DQbVq^;7_e7|(Sdz|&J z73N+M`rc2rt*oSWu#7S{*s~nH6HRHJS1SmzeXk|;CA)FI4bat3<%}nkB%;;?=F>B7ms9QSxv#@+69;@>QaR?REYX4&)=itG>rM{<{A79Rmk)`5ON#GL`*KX%}Ihk3w(RtM-WLt z?f&FLF}4N^yE!(pZ&Yj&Bc`~K0@4_}*0Om?wN|}4WJ>WL;G^H2*QpgEkGA~OET-Km zkwz|5{6dnz1U<2Pe9DNL>3g5FEIvp1jzP&2K#z~j%g6!7B;^zF+o95?fV{3mnB8*RMhCDNp>Am-3e@jNfMj?jHV$MWjk!DDKP zkAz$Y?Sr)!GUOX}qTQ5aMh|wq1uq}~joWyKl=b_LboM#wi{CMuz5x6BKlA-qy++cM01D3b7`uD z#l6M4pI;JCypO8JZ6?U&wNxR!{4oB_ zlV!x9+-&Qy6{%MQ{~yoZGkKiTSC`YS_j22~G;xUV855g2&C(zm^V!(wpcm@zn{%!g z4}JGo(sGZ1O~to-}le

UmY2RIYtNPVDpE$%vda+HD#3m z&VuXJ{BK&Qe+rBa7eq}Q(bq|tn(RrJAk|ztj2(i{d>nmQnM?;HF2k&9sA6up5tmjl z7lySlzMbifH17-m-Lwa_F&e7nOH?ESi3#ckR3tsM+jsck3`oG!uMS}|eAwVXv>}qxwq?QY%QJ0}r@^;fhuUA9W z*BVl>TGo&N004@xSiwDUXUvp51sVmqO3m)=B55aPwf@0=e}cN+$-BdKxY`YrT_4)0 z_d10#i44Q*rFr8MC>*)v$EJvz``(pb{e&*6k+b zsMz%($|1+8hn8c2?P(l@;Rb&CsZeYoCI3?2!LqjbwPXW3z4G$Qfj=cT5Yb%vY0(AX oeb?AaKtwrnc|$|zzw9vfvn^aJJ!zd)XFXqqy0000001=f@-~a#s diff --git a/skills-src/README.md b/skills-src/README.md index 20d67df..48459f8 100644 --- a/skills-src/README.md +++ b/skills-src/README.md @@ -2,43 +2,61 @@ Some AI skills for your convenience: ## Create a Common Child Flow module boilerplate -[Module boilerplate skill](commonstatemachine/common-child-flow/boilerplate/SKILL.md): creates a set of API/implementation -feature module boilerplate following the [Common Child Flow](../README.md#common-child-flow-api). - - Defines gesture/ui-states sealed structures - - Binds data and UI APIs - - Builds a basic tool set for the module implementation - - Creates a test structure and mocks for the module implementation - - Builds a DI container for the module implementation +[Module boilerplate skill](commonstatemachine/common-child-flow/boilerplate/SKILL.md): creates a set of API/implementation +feature module boilerplate following the [Common Child Flow](../README.md#common-child-flow-api). +- Defines gesture/ui-states sealed structures +- Binds data and UI APIs +- Builds a basic tool set for the module implementation +- Creates a test structure and mocks for the module implementation +- Builds a DI container for the module implementation For example, to try the skill with the [example project](../examples/skills), try creating a module to display a list of friends as soon as the user is authenticated: > Create a `friendlist` feature module that loads and displays a list of friends. > - Use `com.motorro.commonstatemachine.skills.domain.friends.GetFriendList` as the data source, staying subscribed -> to updates after the first load. + > to updates after the first load. > - Show a loading state until the first emission, then the friend list. > - On failure, check the error: offer a retry for an I/O error, otherwise terminate the flow. > - Map errors to the domain at the usecase boundary with `toAppException` from -> `com.motorro.commonstatemachine.skills.domain.exception`. + > `com.motorro.commonstatemachine.skills.domain.exception`. > - Logical states: `list` (with the subscription) and `error`. > - Views: list, loading, error — built with the shared theme, dimensions and composables from the `appcore` module. > - Wire up DI modules for the data and UI APIs, add state tests, and create proxies (gesture, UI-state, module flow) -> to embed the flow in the main application module. + > to embed the flow in the main application module. ## Add a new state to an existing Common Child Flow module [Add-state skill](commonstatemachine/common-child-flow/add-state/SKILL.md): adds one more logical state to a feature-implementation module that already exists, without touching the module's overall boilerplate. A new state doesn't have to be a new screen - it can just as easily be an invisible step that hands off to the next one on its own. - - Infers the new state's gesture, UI-state and interstate data from a screen design or a text description - - Creates the state, its state-factory method, and its view - - Creates a unit test for the new state - - Wires up the transition from an existing state, whether it's triggered by a gesture or by that state's own - work completing +- Infers the new state's gesture, UI-state and interstate data from a screen design or a text description +- Creates the state, its state-factory method, and its view +- Creates a unit test for the new state +- Wires up the transition from an existing state, whether it's triggered by a gesture or by that state's own + work completing For example, continuing the `friendlist` module from the example above, try adding a details screen: > Add a `details` state to the `friendlist` module that shows a single friend's info. > - Reached when the user clicks a friend in the list: the `list` state should handle that click and switch to -> the new state, passing the clicked friend's id. -> - Show the friend's name and status; offer a back gesture to return to the list. \ No newline at end of file + > the new state, passing the clicked friend's id. +> - Show the friend's name and status; offer a back gesture to return to the list. + +## Build a demo app for a Common Child Flow module +[Demo-app skill](commonstatemachine/common-child-flow/demo/SKILL.md): builds a small, internal-only app that +launches one existing feature module on its own, without the rest of the app around it - handy to demo a +feature to stakeholders before the real app exists, or to test it end-to-end by hand or with a UI-testing tool. +- Fakes the feature's domain use-cases with fixtures, so it runs without a real backend +- Implements any specific failure scenario the user describes (for example "fail the first call with an I/O + error, then succeed") using the real domain exception types +- Reuses the project's shared UI/theme module and whichever DI framework the project already uses, rather + than assuming either +- Wires up a minimal `Application`, `Activity` and `CommonFlowViewModel` to launch the flow standalone + +For example, continuing the `friendlist` module from the examples above, try demoing it on its own: + +> Create a demo app for the `friendlist` module so I can show it to stakeholders. +> - Fake `GetFriendList` so it emits three or four friends after a short delay, then fails once with an I/O + > error on the next emission before recovering, so I can check the `list` state's retry path. +> - Reuse the `appcore` module's theme and app bar, the same way the feature's own views do. \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/.shared/renderer-factory-context-interface-implementation.md b/skills-src/commonstatemachine/common-child-flow/.shared/renderer-factory-context-interface-implementation.md index 9fe585a..c79eb41 100644 --- a/skills-src/commonstatemachine/common-child-flow/.shared/renderer-factory-context-interface-implementation.md +++ b/skills-src/commonstatemachine/common-child-flow/.shared/renderer-factory-context-interface-implementation.md @@ -76,7 +76,9 @@ The implementation will: Follow these common rules: - Create the class in the implementation module. -- If required by the DI framework used in project, add required inject annotations. +- If the project uses a DI framework, match whichever one is already in use - check an existing module + rather than assuming Koin, which is just what the worked example uses - and add the required inject + annotations. - Inject external factories and dependencies required by the state to the factory constructor. Try to use lazy provider injection to delay the initialization. - Pass the flow host to the factory constructor. diff --git a/skills-src/commonstatemachine/common-child-flow/add-state/references/wiring-transition-implementation.md b/skills-src/commonstatemachine/common-child-flow/add-state/references/wiring-transition-implementation.md index 3137ba3..da30c21 100644 --- a/skills-src/commonstatemachine/common-child-flow/add-state/references/wiring-transition-implementation.md +++ b/skills-src/commonstatemachine/common-child-flow/add-state/references/wiring-transition-implementation.md @@ -47,7 +47,7 @@ clicks a friend in the list, navigate to the friend's details". [state-factory method](renderer-factory-context-interface-implementation.md#step-2-state-factory-interface), and switch to it with `setMachineState`. Take a look at the [example](../assets/example/implementation/state/FormState.kt): it already demonstrates this exact pattern - it changes the machine -state when its action gesture is receive (see [state-implementation.md, Example 3](state-implementation.md#example-3-form-state)). +state when its action gesture is received (see [state-implementation.md, Example 3](state-implementation.md#example-3-form-state)). ## Update the existing state's test Whichever pattern applies, add a test case to the existing state's unit test that: diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/SKILL.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/SKILL.md index 18f62d3..ebfaa5b 100644 --- a/skills-src/commonstatemachine/common-child-flow/boilerplate/SKILL.md +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/SKILL.md @@ -60,6 +60,10 @@ Before creating any modules, work out what the feature needs from what the user dummy states and views. - Integration: should this feature be hosted inside one or more existing parent flows (Step 7), or does it stand alone for now? +- Dependency injection: does the project already use a DI framework (Koin, Hilt, Dagger, or similar), + or is everything wired by hand? Check an existing module in the project rather than assuming - the + worked example in this skill happens to use Koin, but that's the example's own choice, not a + requirement. Follow whatever the project actually uses in Step 6. If the user gave you only part of this, use what you have and ask clarifying questions about anything still ambiguous rather than guessing silently. diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/di-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/di-implementation.md index 19ae9d8..6914aec 100644 --- a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/di-implementation.md +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/di-implementation.md @@ -30,7 +30,10 @@ Create the UI API implementation: See the [example](../assets/example/implementation/AuthUiApiImpl.kt) for our sample authentication UI API. ## Step 3. Define DI module(s) (optional). -Check the project setup or the user input. If the project uses DI to provide dependencies, implement the required modules. +First check which DI approach the project actually uses, if any - don't assume Koin just because the +worked example in this skill does; that's the example's own choice, not a requirement. Look at an +existing module in the project (or ask the user) to find out, and implement the required modules in +that same style. Hints: - Make sure all individual state factories are DI-enabled. See the example [StateFactory](../assets/example/implementation/state/PreloadingState.kt). diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/proxy-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/proxy-implementation.md index 78266ef..75de053 100644 --- a/skills-src/commonstatemachine/common-child-flow/boilerplate/references/proxy-implementation.md +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/proxy-implementation.md @@ -39,6 +39,7 @@ To instantiate the proxy state cleanly within the parent flow: - Parent State Factory Implementation: Inject the proxy state factory into the parent `StateFactory` implementation, and delegate the state creation call to it. - DI Registration: Ensure the proxy state factory and parent state factory implementation are annotated - for DI (e.g., `@Factory` for Koin, `@Inject` / `@ViewModelScoped` for Hilt). + for whichever DI framework the project already uses - check an existing module rather than assuming + (e.g., `@Factory` for Koin, `@Inject` / `@ViewModelScoped` for Hilt). See the [example main state factory](../assets/example/app/state/MainStateFactory.kt) and [proxy state factory](../assets/example/app/state/AuthProxyState.kt). diff --git a/skills-src/commonstatemachine/common-child-flow/demo/SKILL.md b/skills-src/commonstatemachine/common-child-flow/demo/SKILL.md new file mode 100644 index 0000000..53343bc --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/demo/SKILL.md @@ -0,0 +1,202 @@ +--- +name: commonstatemachine-commonchildflow-demo +description: Use this skill to build a small standalone demo/debug application that launches one existing + CommonStateMachine Common Child Flow feature module on its own, without building the full app around it. Use + it whenever the user asks to demo, debug, showcase, or try out a feature module in isolation - for example + "Create a demo app for the auth module", "Let me run the friendlist flow on its own to show stakeholders", or + "Set up a debug app so I can test the payment flow end-to-end with Maestro/Journey". It fakes the feature's + domain use-cases with fixtures so the flow runs without a backend, including fixtures for specific failure + scenarios the user describes (for example "make the first login attempt fail with a network error, so I can + check the retry logic"). If the feature module doesn't exist yet, use the boilerplate skill first - this + skill only wires up a way to run one that's already there. +metadata: + author: Motorro + last-updated: '2026-10-02' + keywords: + - android + - architecture + - cross-platform + - kotlin-multiplatform + - mvi + - state-machine + - feature-flow + - demo-app + - debug + - fixtures +--- + +## Description +Use this skill to build a small debug/demo application module that hosts an existing Common Child Flow feature +on its own screen, so it can be launched without the rest of the app. This is useful to demonstrate a feature to +stakeholders before the surrounding app exists, and to test a feature end-to-end by hand or with a UI-testing +tool (Maestro, Journey, etc.) without depending on a real backend. + +For a deeper dive into the state machine mechanics and common components, refer to the [Core Architecture](references/ARCHITECTURE.md). +For a deeper dive into the common child flow philosophy and design, refer to the [Common Child Flow Architecture](references/COMMON-CHILD-FLOW.md). + +## Example +This instruction uses the same [example authentication module](assets/example) as the boilerplate and add-state +skills, together with its domain module and a complete example demo app. All examples, schemes and code samples +refer to this example; adjust names and packages to your actual need. + +## Strategy +You will create one small Gradle application module that hosts an existing feature flow (an API module and one +of its implementations) on its own screen, fed by fixture (fake) domain use-cases so the flow runs without a +real backend: + +``` +my-project # Project folder +├── auth # Existing feature folder - module must already exist +│ ├── api +│ ├── implementation +│ └── demo # New: the demo/debug application module +└── ... +``` + +Take a look at the [class diagram](references/common-child-flow.puml) for an overview of how the API, +implementation, and a hosting application relate to each other. + +## Gathering the requirements for the demo app +Before creating the module, work out what you need from what the user gave you and from the project: + +- The target feature: which existing Common Child Flow module (an `api` module and one of its + `implementation` modules) is this demo for? It must already exist - if it doesn't, use the boilerplate skill + first to create it, then come back here. +- The common UI module: look for an existing shared UI/theme Gradle module in the project (search for a + top-level `Theme` composable wrapping `MaterialTheme`, and shared design components such as an app bar or a + loading indicator - `appcore` in the example). Reuse its theme and components in the demo instead of + inventing new ones. If the project has no such module, fall back to plain `MaterialTheme` and a `TopAppBar`. +- The domain use-cases and models the feature actually depends on: trace the feature implementation's + constructor-injected dependencies back to the `domain` module interfaces they call (its state-factory and + states are the place to look - see Step 3). These are exactly what you need to fake in Step 4. +- Fixture behavior: did the user ask for a specific scenario (for example "let the usecase fail the first + time with an IO error, so I can check the retry logic")? If so, implement exactly that behavior using the + real domain exception/model types. If the user gave no specifics, default to a fixture that just succeeds + (with a short delay to emulate a real call) and say so in a comment, so it's easy to find and change later. +- Dependency injection: does the project use a DI framework (Koin, Hilt, Dagger, or similar), or is it wired + by hand (plain factory functions/constructors)? Look at the feature's own implementation module and an + existing app module to find out which, and wire the fixtures the same way - don't introduce a new DI + framework just for the demo. See Step 5. +- Did the user ask for anything beyond the happy-path screen (for example starting the flow already + authenticated, or a specific `init` input)? Use it when constructing `MainViewModel` in Step 7. + +If the user gave you only part of this, use what you have and ask clarifying questions about anything still +ambiguous rather than guessing silently. + +Follow the following common rules when writing code: + +- Follow the project coding conventions or use Kotlin default +- Follow the project naming conventions or use Kotlin default +- If the project uses KDoc as a coding convention - add it to the created types, methods, properties +- Add TODO comments wherever you default a fixture to "just succeed" so the user knows it's a placeholder + +## Step 1. Confirm the target feature and module structure +Identify the feature's `api` module (its `Gesture`, `UiState`, `Input`, `Result`, `DataApi`, `UiApi` types) and +the specific `implementation` module to demo, if there are several. If the feature module doesn't exist yet, +stop here and use the boilerplate skill instead. + +Create the new module directory next to the feature's existing `api`/`implementation` modules, following the +project's module-naming convention (`demo` in the example). Use the Gradle setup common to other application +modules in the project if one exists to copy from. + +## Step 2. Create the demo module's Gradle file +This is an internal tool, not something that ships to end users - keep it minimal rather than matching the +production app's compatibility range. Set `minSdk` to a recent platform version (the latest one or two major +Android releases) instead of the project's real `minSdk`: a higher floor means you can skip legacy-compatibility +concerns later, like generating a full set of per-density launcher icons (see Step 8). + +Set up the module as an Android application (not a library), including the dependencies it actually needs: + +- The common UI module identified above (for the shared theme and components) +- The `domain` module (for the real domain exception and model types the fixtures use) +- The feature's `api` and `implementation` modules +- The base state-machine components, coroutine extensions, and the common child-flow data/compose/viewmodel + modules used elsewhere in the project +- A logging library if the project uses one (Napier in the example) so fixtures can narrate what they're doing +- The project's DI framework and its Android/Compose/ViewModel integration artifacts (Koin in the example) + +See [the example demo module's build file](assets/example/demo/build.gradle.kts) for a concrete dependency list. + +## Step 3. Trace the domain use-cases the feature depends on +Look at the feature implementation's state-factory implementation (for example `AuthStateFactoryImpl`) and the +states/state-factories it wires together. Any constructor parameter whose type lives in the `domain` package +(not the feature's own `api`/`implementation` packages) is a domain use-case or domain-provided value that the +demo app must fake, because in a real app it would come from a `usecase` module implementation that the demo +doesn't depend on. + +## Step 4. Create fixtures for the domain use-cases +Follow [this guide](references/fixture-patterns.md) to fake every domain use-case identified in Step 3, +including any specific failure scenario the user described in the requirements-gathering step. + +## Step 5. Create the Application class and the fixture DI wiring +First confirm which DI approach the project actually uses - don't assume Koin. Check the feature's own +implementation module (does it have `@Module`/`@Factory`-style annotations, a Hilt `@Module`/`@Binds` class, a +Dagger component, or just plain constructor calls somewhere?) and copy that same approach for wiring the +fixtures, so the demo looks like a natural, smaller version of how the real app would do it. + +If the project uses Koin (as in the example), create an `Application` subclass that starts the DI container, +and a DI module that includes the feature's own implementation module plus the fixture bindings from Step 4. +See [the example `App.kt`](assets/example/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/App.kt): + +```kotlin +@KoinApplication(modules = [AppModule::class]) +class App : Application() { + override fun onCreate() { + super.onCreate() + Napier.base(DebugAntilog()) + startKoin { + allowOverride(false) + androidLogger() + androidContext(this@App) + } + } +} + +@Module(includes = [AuthModule::class]) +@ComponentScan +class AppModule { + // @Single fixture bindings from Step 4 go here +} +``` + +If the project uses Hilt/Dagger, create the equivalent `@Module`/`@InstallIn` (or component) that provides the +fixtures alongside the feature's own modules, following that project's existing module structure. If the +project wires dependencies by hand with no DI framework, skip a separate module entirely: construct the +fixtures directly where you construct the feature's `DataApi`/`UiApi` implementations (typically in the +`Application` class or a small factory object), and pass them straight into the feature's constructors. + +## Step 6. Create the main Activity +Create a single `ComponentActivity` that sets Compose content wrapped in the common UI module's theme, injects +the feature's `UiApi` and a `CommonFlowViewModel`, and renders the flow with `CommonFlowComposition` inside a +`Scaffold` using the common UI module's app bar. See [the example `MainActivity.kt`](assets/example/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/MainActivity.kt) for the full pattern, +including wiring the Android back gesture through `navigationBackHandler` and finishing the activity when the flow's `finish` callback fires. + +## Step 7. Create the main ViewModel +Create a trivial `CommonFlowViewModel` subclass that takes the feature's `DataApi` via constructor injection and +passes the right `init` value for the feature's `Input` type: + +```kotlin +@KoinViewModel +class MainViewModel(api: AuthDataApi) : CommonFlowViewModel( + api = api, + init = AuthInput(skippable = true) +) +``` + +If the `Input` type needs more than a default value to make sense for a demo (for example data that normally +comes from a parent flow), ask the user what to pass, or use a clearly-marked placeholder value. + +## Step 8. Create the Android manifest and resources +Add a minimal `AndroidManifest.xml` declaring the single launcher activity, and the small resource set an +Android application module needs (`app_name` in `strings.xml`, a launcher icon, and the standard backup/data- +extraction/network-security XML files). Keep this minimal: it's an internal tool, not a production app icon. +With `minSdk` set to a recent version (Step 2), adaptive icons are guaranteed to be available, so a single +`mipmap-anydpi-v26/ic_launcher.xml` (plus its round variant) referencing a vector drawable is enough - don't +generate the legacy per-density PNG sets (`mipmap-hdpi`, `-mdpi`, `-xhdpi`, `-xxhdpi`, `-xxxhdpi`) that exist +only for backwards compatibility with older Android versions this demo doesn't need to support. Copy the +manifest/resource shape from another demo/app module in the project if one exists, adjusting the app name and +dropping anything that's there only for legacy-device support; otherwise create a minimal set from scratch. + +## Step 9. Register the module (if the project needs it) +If the project registers Gradle modules explicitly (check `settings.gradle.kts`), add the new demo module there +following the existing naming convention. diff --git a/skills-src/commonstatemachine/common-child-flow/demo/assets/example/api b/skills-src/commonstatemachine/common-child-flow/demo/assets/example/api new file mode 120000 index 0000000..ae54fa4 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/demo/assets/example/api @@ -0,0 +1 @@ +../../../../../../examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/demo/assets/example/demo/build.gradle.kts b/skills-src/commonstatemachine/common-child-flow/demo/assets/example/demo/build.gradle.kts new file mode 120000 index 0000000..2812e83 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/demo/assets/example/demo/build.gradle.kts @@ -0,0 +1 @@ +../../../../../../../examples/skills/auth/demo/build.gradle.kts \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/demo/assets/example/demo/src b/skills-src/commonstatemachine/common-child-flow/demo/assets/example/demo/src new file mode 120000 index 0000000..2c9d9fc --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/demo/assets/example/demo/src @@ -0,0 +1 @@ +../../../../../../../examples/skills/auth/demo/src/ \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/demo/assets/example/domain b/skills-src/commonstatemachine/common-child-flow/demo/assets/example/domain new file mode 120000 index 0000000..829e27c --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/demo/assets/example/domain @@ -0,0 +1 @@ +../../../../../../examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/ \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/demo/assets/example/implementation b/skills-src/commonstatemachine/common-child-flow/demo/assets/example/implementation new file mode 120000 index 0000000..55c188d --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/demo/assets/example/implementation @@ -0,0 +1 @@ +../../../../../../examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/demo/references/ARCHITECTURE.md b/skills-src/commonstatemachine/common-child-flow/demo/references/ARCHITECTURE.md new file mode 120000 index 0000000..f1d6e62 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/demo/references/ARCHITECTURE.md @@ -0,0 +1 @@ +../../../.shared/ARCHITECTURE.md \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/demo/references/COMMON-CHILD-FLOW.md b/skills-src/commonstatemachine/common-child-flow/demo/references/COMMON-CHILD-FLOW.md new file mode 120000 index 0000000..c83c444 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/demo/references/COMMON-CHILD-FLOW.md @@ -0,0 +1 @@ +../../.shared/COMMON-CHILD-FLOW.md \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/demo/references/common-child-flow.puml b/skills-src/commonstatemachine/common-child-flow/demo/references/common-child-flow.puml new file mode 120000 index 0000000..b135e8b --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/demo/references/common-child-flow.puml @@ -0,0 +1 @@ +../../.shared/common-child-flow.puml \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/demo/references/fixture-patterns.md b/skills-src/commonstatemachine/common-child-flow/demo/references/fixture-patterns.md new file mode 100644 index 0000000..553aebe --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/demo/references/fixture-patterns.md @@ -0,0 +1,74 @@ +# Creating fixtures for a demo app's domain use-cases + +A demo app must run the feature flow without a real backend, so every domain use-case interface the feature +implementation depends on (see Step 3 of the main guide) needs a fake (fixture) implementation, wired into DI +from the demo app's own application module - the same place a real app would instead wire the production +`usecase` module's implementations. + +## Default: succeed quietly + +If the user gave no specific scenario for a use-case, make its fixture succeed on every call, after a short +`delay()` (one to two seconds) so the demo still shows its loading UI realistically. Return or construct a +plausible instance of the domain model the interface returns - don't leave it `null` or throw +`NotImplementedError`. Add a short comment noting this is a fixture default the user can change. + +## Specific scenario: fail then succeed (or fail a fixed number of times) + +When the user describes something like "let it fail the first time with an IO error, so I can check the retry +logic", implement the fixture as a stateful fake: a mutable counter captured in its closure, advanced on every +call, branching on its value. Use the *real* domain exception types (from the `domain.exception` package, e.g. +`IOException`, `AuthenticationException` - never a generic `RuntimeException`) so the feature's own +error-mapping code is exercised the same way it would be against a real backend. Log what's happening, tagged +with the interface's simple name, so the emulated branch is visible while demoing: + +```kotlin +@Single +fun getPasswordRequirements(): GetPasswordRequirements { + var attempt = 1 + val tag = GetPasswordRequirements::class.simpleName!! + return object : GetPasswordRequirements { + override suspend fun invoke(): PasswordRequirements { + delay(2.seconds) + return when (attempt) { + 1 -> { + Napier.d(tag = tag) { "Emulating non-fatal error" } + attempt = 2 + throw IOException("Network error") + } + else -> { + Napier.d(tag = tag) { "Emulating success" } + attempt = 1 + PasswordRequirements(regex = "^.{8,}$".toRegex(), description = "Minimum eight characters") + } + } + } + } +} +``` + +Repeat this pattern for every use-case the user wants to control, cycling the counter back to its first value +after the sequence completes so the demo can be replayed without restarting the app. + +## Always-fail scenario + +For "it should always fail with X", skip the counter and just throw the requested exception every time (still +behind a `delay()`), optionally logging that this is a permanent emulated failure. + +## Multi-step scenarios + +If the user describes a longer sequence across several use-cases (for example "preloading succeeds, but the +authentication call fails twice before succeeding"), give each use-case its own independent counter - don't +share one counter across fixtures unless the user is explicitly describing a single shared piece of state (like +a session). + +## Wiring the fixtures in + +Wire every fixture using whichever approach the project already uses for DI (see Step 5 of the main guide) - +don't introduce a new framework just for the demo. With Koin, bind each fixture with a singleton-scoped +function inside the demo app's own module, the one that `includes` the feature's own implementation module; +that `includes` relationship is what pulls in the feature's states, factories and API implementations, and the +fixtures declared alongside it supply the domain-level dependencies those classes expect but don't themselves +provide. With Hilt/Dagger, provide the same fixtures from an equivalent `@Module`/`@Provides` (or `@Binds`) set +installed alongside the feature's own module. With no DI framework at all, construct the fixtures directly +where the feature's `DataApi`/`UiApi` implementations are constructed, and pass them straight in as +constructor arguments. diff --git a/skills/README.md b/skills/README.md index 20d67df..48459f8 100644 --- a/skills/README.md +++ b/skills/README.md @@ -2,43 +2,61 @@ Some AI skills for your convenience: ## Create a Common Child Flow module boilerplate -[Module boilerplate skill](commonstatemachine/common-child-flow/boilerplate/SKILL.md): creates a set of API/implementation -feature module boilerplate following the [Common Child Flow](../README.md#common-child-flow-api). - - Defines gesture/ui-states sealed structures - - Binds data and UI APIs - - Builds a basic tool set for the module implementation - - Creates a test structure and mocks for the module implementation - - Builds a DI container for the module implementation +[Module boilerplate skill](commonstatemachine/common-child-flow/boilerplate/SKILL.md): creates a set of API/implementation +feature module boilerplate following the [Common Child Flow](../README.md#common-child-flow-api). +- Defines gesture/ui-states sealed structures +- Binds data and UI APIs +- Builds a basic tool set for the module implementation +- Creates a test structure and mocks for the module implementation +- Builds a DI container for the module implementation For example, to try the skill with the [example project](../examples/skills), try creating a module to display a list of friends as soon as the user is authenticated: > Create a `friendlist` feature module that loads and displays a list of friends. > - Use `com.motorro.commonstatemachine.skills.domain.friends.GetFriendList` as the data source, staying subscribed -> to updates after the first load. + > to updates after the first load. > - Show a loading state until the first emission, then the friend list. > - On failure, check the error: offer a retry for an I/O error, otherwise terminate the flow. > - Map errors to the domain at the usecase boundary with `toAppException` from -> `com.motorro.commonstatemachine.skills.domain.exception`. + > `com.motorro.commonstatemachine.skills.domain.exception`. > - Logical states: `list` (with the subscription) and `error`. > - Views: list, loading, error — built with the shared theme, dimensions and composables from the `appcore` module. > - Wire up DI modules for the data and UI APIs, add state tests, and create proxies (gesture, UI-state, module flow) -> to embed the flow in the main application module. + > to embed the flow in the main application module. ## Add a new state to an existing Common Child Flow module [Add-state skill](commonstatemachine/common-child-flow/add-state/SKILL.md): adds one more logical state to a feature-implementation module that already exists, without touching the module's overall boilerplate. A new state doesn't have to be a new screen - it can just as easily be an invisible step that hands off to the next one on its own. - - Infers the new state's gesture, UI-state and interstate data from a screen design or a text description - - Creates the state, its state-factory method, and its view - - Creates a unit test for the new state - - Wires up the transition from an existing state, whether it's triggered by a gesture or by that state's own - work completing +- Infers the new state's gesture, UI-state and interstate data from a screen design or a text description +- Creates the state, its state-factory method, and its view +- Creates a unit test for the new state +- Wires up the transition from an existing state, whether it's triggered by a gesture or by that state's own + work completing For example, continuing the `friendlist` module from the example above, try adding a details screen: > Add a `details` state to the `friendlist` module that shows a single friend's info. > - Reached when the user clicks a friend in the list: the `list` state should handle that click and switch to -> the new state, passing the clicked friend's id. -> - Show the friend's name and status; offer a back gesture to return to the list. \ No newline at end of file + > the new state, passing the clicked friend's id. +> - Show the friend's name and status; offer a back gesture to return to the list. + +## Build a demo app for a Common Child Flow module +[Demo-app skill](commonstatemachine/common-child-flow/demo/SKILL.md): builds a small, internal-only app that +launches one existing feature module on its own, without the rest of the app around it - handy to demo a +feature to stakeholders before the real app exists, or to test it end-to-end by hand or with a UI-testing tool. +- Fakes the feature's domain use-cases with fixtures, so it runs without a real backend +- Implements any specific failure scenario the user describes (for example "fail the first call with an I/O + error, then succeed") using the real domain exception types +- Reuses the project's shared UI/theme module and whichever DI framework the project already uses, rather + than assuming either +- Wires up a minimal `Application`, `Activity` and `CommonFlowViewModel` to launch the flow standalone + +For example, continuing the `friendlist` module from the examples above, try demoing it on its own: + +> Create a demo app for the `friendlist` module so I can show it to stakeholders. +> - Fake `GetFriendList` so it emits three or four friends after a short delay, then fails once with an I/O + > error on the next emission before recovering, so I can check the `list` state's retry path. +> - Reuse the `appcore` module's theme and app bar, the same way the feature's own views do. \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/add-state/references/renderer-factory-context-interface-implementation.md b/skills/commonstatemachine/common-child-flow/add-state/references/renderer-factory-context-interface-implementation.md index 9fe585a..c79eb41 100644 --- a/skills/commonstatemachine/common-child-flow/add-state/references/renderer-factory-context-interface-implementation.md +++ b/skills/commonstatemachine/common-child-flow/add-state/references/renderer-factory-context-interface-implementation.md @@ -76,7 +76,9 @@ The implementation will: Follow these common rules: - Create the class in the implementation module. -- If required by the DI framework used in project, add required inject annotations. +- If the project uses a DI framework, match whichever one is already in use - check an existing module + rather than assuming Koin, which is just what the worked example uses - and add the required inject + annotations. - Inject external factories and dependencies required by the state to the factory constructor. Try to use lazy provider injection to delay the initialization. - Pass the flow host to the factory constructor. diff --git a/skills/commonstatemachine/common-child-flow/add-state/references/wiring-transition-implementation.md b/skills/commonstatemachine/common-child-flow/add-state/references/wiring-transition-implementation.md index 3137ba3..da30c21 100644 --- a/skills/commonstatemachine/common-child-flow/add-state/references/wiring-transition-implementation.md +++ b/skills/commonstatemachine/common-child-flow/add-state/references/wiring-transition-implementation.md @@ -47,7 +47,7 @@ clicks a friend in the list, navigate to the friend's details". [state-factory method](renderer-factory-context-interface-implementation.md#step-2-state-factory-interface), and switch to it with `setMachineState`. Take a look at the [example](../assets/example/implementation/state/FormState.kt): it already demonstrates this exact pattern - it changes the machine -state when its action gesture is receive (see [state-implementation.md, Example 3](state-implementation.md#example-3-form-state)). +state when its action gesture is received (see [state-implementation.md, Example 3](state-implementation.md#example-3-form-state)). ## Update the existing state's test Whichever pattern applies, add a test case to the existing state's unit test that: diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md b/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md index 18f62d3..ebfaa5b 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md @@ -60,6 +60,10 @@ Before creating any modules, work out what the feature needs from what the user dummy states and views. - Integration: should this feature be hosted inside one or more existing parent flows (Step 7), or does it stand alone for now? +- Dependency injection: does the project already use a DI framework (Koin, Hilt, Dagger, or similar), + or is everything wired by hand? Check an existing module in the project rather than assuming - the + worked example in this skill happens to use Koin, but that's the example's own choice, not a + requirement. Follow whatever the project actually uses in Step 6. If the user gave you only part of this, use what you have and ask clarifying questions about anything still ambiguous rather than guessing silently. diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/di-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/di-implementation.md index 19ae9d8..6914aec 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/di-implementation.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/di-implementation.md @@ -30,7 +30,10 @@ Create the UI API implementation: See the [example](../assets/example/implementation/AuthUiApiImpl.kt) for our sample authentication UI API. ## Step 3. Define DI module(s) (optional). -Check the project setup or the user input. If the project uses DI to provide dependencies, implement the required modules. +First check which DI approach the project actually uses, if any - don't assume Koin just because the +worked example in this skill does; that's the example's own choice, not a requirement. Look at an +existing module in the project (or ask the user) to find out, and implement the required modules in +that same style. Hints: - Make sure all individual state factories are DI-enabled. See the example [StateFactory](../assets/example/implementation/state/PreloadingState.kt). diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/proxy-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/proxy-implementation.md index 78266ef..75de053 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/proxy-implementation.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/proxy-implementation.md @@ -39,6 +39,7 @@ To instantiate the proxy state cleanly within the parent flow: - Parent State Factory Implementation: Inject the proxy state factory into the parent `StateFactory` implementation, and delegate the state creation call to it. - DI Registration: Ensure the proxy state factory and parent state factory implementation are annotated - for DI (e.g., `@Factory` for Koin, `@Inject` / `@ViewModelScoped` for Hilt). + for whichever DI framework the project already uses - check an existing module rather than assuming + (e.g., `@Factory` for Koin, `@Inject` / `@ViewModelScoped` for Hilt). See the [example main state factory](../assets/example/app/state/MainStateFactory.kt) and [proxy state factory](../assets/example/app/state/AuthProxyState.kt). diff --git a/skills/commonstatemachine/common-child-flow/boilerplate/references/renderer-factory-context-interface-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/renderer-factory-context-interface-implementation.md index 9fe585a..c79eb41 100644 --- a/skills/commonstatemachine/common-child-flow/boilerplate/references/renderer-factory-context-interface-implementation.md +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/renderer-factory-context-interface-implementation.md @@ -76,7 +76,9 @@ The implementation will: Follow these common rules: - Create the class in the implementation module. -- If required by the DI framework used in project, add required inject annotations. +- If the project uses a DI framework, match whichever one is already in use - check an existing module + rather than assuming Koin, which is just what the worked example uses - and add the required inject + annotations. - Inject external factories and dependencies required by the state to the factory constructor. Try to use lazy provider injection to delay the initialization. - Pass the flow host to the factory constructor. diff --git a/skills/commonstatemachine/common-child-flow/demo/SKILL.md b/skills/commonstatemachine/common-child-flow/demo/SKILL.md new file mode 100644 index 0000000..53343bc --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/SKILL.md @@ -0,0 +1,202 @@ +--- +name: commonstatemachine-commonchildflow-demo +description: Use this skill to build a small standalone demo/debug application that launches one existing + CommonStateMachine Common Child Flow feature module on its own, without building the full app around it. Use + it whenever the user asks to demo, debug, showcase, or try out a feature module in isolation - for example + "Create a demo app for the auth module", "Let me run the friendlist flow on its own to show stakeholders", or + "Set up a debug app so I can test the payment flow end-to-end with Maestro/Journey". It fakes the feature's + domain use-cases with fixtures so the flow runs without a backend, including fixtures for specific failure + scenarios the user describes (for example "make the first login attempt fail with a network error, so I can + check the retry logic"). If the feature module doesn't exist yet, use the boilerplate skill first - this + skill only wires up a way to run one that's already there. +metadata: + author: Motorro + last-updated: '2026-10-02' + keywords: + - android + - architecture + - cross-platform + - kotlin-multiplatform + - mvi + - state-machine + - feature-flow + - demo-app + - debug + - fixtures +--- + +## Description +Use this skill to build a small debug/demo application module that hosts an existing Common Child Flow feature +on its own screen, so it can be launched without the rest of the app. This is useful to demonstrate a feature to +stakeholders before the surrounding app exists, and to test a feature end-to-end by hand or with a UI-testing +tool (Maestro, Journey, etc.) without depending on a real backend. + +For a deeper dive into the state machine mechanics and common components, refer to the [Core Architecture](references/ARCHITECTURE.md). +For a deeper dive into the common child flow philosophy and design, refer to the [Common Child Flow Architecture](references/COMMON-CHILD-FLOW.md). + +## Example +This instruction uses the same [example authentication module](assets/example) as the boilerplate and add-state +skills, together with its domain module and a complete example demo app. All examples, schemes and code samples +refer to this example; adjust names and packages to your actual need. + +## Strategy +You will create one small Gradle application module that hosts an existing feature flow (an API module and one +of its implementations) on its own screen, fed by fixture (fake) domain use-cases so the flow runs without a +real backend: + +``` +my-project # Project folder +├── auth # Existing feature folder - module must already exist +│ ├── api +│ ├── implementation +│ └── demo # New: the demo/debug application module +└── ... +``` + +Take a look at the [class diagram](references/common-child-flow.puml) for an overview of how the API, +implementation, and a hosting application relate to each other. + +## Gathering the requirements for the demo app +Before creating the module, work out what you need from what the user gave you and from the project: + +- The target feature: which existing Common Child Flow module (an `api` module and one of its + `implementation` modules) is this demo for? It must already exist - if it doesn't, use the boilerplate skill + first to create it, then come back here. +- The common UI module: look for an existing shared UI/theme Gradle module in the project (search for a + top-level `Theme` composable wrapping `MaterialTheme`, and shared design components such as an app bar or a + loading indicator - `appcore` in the example). Reuse its theme and components in the demo instead of + inventing new ones. If the project has no such module, fall back to plain `MaterialTheme` and a `TopAppBar`. +- The domain use-cases and models the feature actually depends on: trace the feature implementation's + constructor-injected dependencies back to the `domain` module interfaces they call (its state-factory and + states are the place to look - see Step 3). These are exactly what you need to fake in Step 4. +- Fixture behavior: did the user ask for a specific scenario (for example "let the usecase fail the first + time with an IO error, so I can check the retry logic")? If so, implement exactly that behavior using the + real domain exception/model types. If the user gave no specifics, default to a fixture that just succeeds + (with a short delay to emulate a real call) and say so in a comment, so it's easy to find and change later. +- Dependency injection: does the project use a DI framework (Koin, Hilt, Dagger, or similar), or is it wired + by hand (plain factory functions/constructors)? Look at the feature's own implementation module and an + existing app module to find out which, and wire the fixtures the same way - don't introduce a new DI + framework just for the demo. See Step 5. +- Did the user ask for anything beyond the happy-path screen (for example starting the flow already + authenticated, or a specific `init` input)? Use it when constructing `MainViewModel` in Step 7. + +If the user gave you only part of this, use what you have and ask clarifying questions about anything still +ambiguous rather than guessing silently. + +Follow the following common rules when writing code: + +- Follow the project coding conventions or use Kotlin default +- Follow the project naming conventions or use Kotlin default +- If the project uses KDoc as a coding convention - add it to the created types, methods, properties +- Add TODO comments wherever you default a fixture to "just succeed" so the user knows it's a placeholder + +## Step 1. Confirm the target feature and module structure +Identify the feature's `api` module (its `Gesture`, `UiState`, `Input`, `Result`, `DataApi`, `UiApi` types) and +the specific `implementation` module to demo, if there are several. If the feature module doesn't exist yet, +stop here and use the boilerplate skill instead. + +Create the new module directory next to the feature's existing `api`/`implementation` modules, following the +project's module-naming convention (`demo` in the example). Use the Gradle setup common to other application +modules in the project if one exists to copy from. + +## Step 2. Create the demo module's Gradle file +This is an internal tool, not something that ships to end users - keep it minimal rather than matching the +production app's compatibility range. Set `minSdk` to a recent platform version (the latest one or two major +Android releases) instead of the project's real `minSdk`: a higher floor means you can skip legacy-compatibility +concerns later, like generating a full set of per-density launcher icons (see Step 8). + +Set up the module as an Android application (not a library), including the dependencies it actually needs: + +- The common UI module identified above (for the shared theme and components) +- The `domain` module (for the real domain exception and model types the fixtures use) +- The feature's `api` and `implementation` modules +- The base state-machine components, coroutine extensions, and the common child-flow data/compose/viewmodel + modules used elsewhere in the project +- A logging library if the project uses one (Napier in the example) so fixtures can narrate what they're doing +- The project's DI framework and its Android/Compose/ViewModel integration artifacts (Koin in the example) + +See [the example demo module's build file](assets/example/demo/build.gradle.kts) for a concrete dependency list. + +## Step 3. Trace the domain use-cases the feature depends on +Look at the feature implementation's state-factory implementation (for example `AuthStateFactoryImpl`) and the +states/state-factories it wires together. Any constructor parameter whose type lives in the `domain` package +(not the feature's own `api`/`implementation` packages) is a domain use-case or domain-provided value that the +demo app must fake, because in a real app it would come from a `usecase` module implementation that the demo +doesn't depend on. + +## Step 4. Create fixtures for the domain use-cases +Follow [this guide](references/fixture-patterns.md) to fake every domain use-case identified in Step 3, +including any specific failure scenario the user described in the requirements-gathering step. + +## Step 5. Create the Application class and the fixture DI wiring +First confirm which DI approach the project actually uses - don't assume Koin. Check the feature's own +implementation module (does it have `@Module`/`@Factory`-style annotations, a Hilt `@Module`/`@Binds` class, a +Dagger component, or just plain constructor calls somewhere?) and copy that same approach for wiring the +fixtures, so the demo looks like a natural, smaller version of how the real app would do it. + +If the project uses Koin (as in the example), create an `Application` subclass that starts the DI container, +and a DI module that includes the feature's own implementation module plus the fixture bindings from Step 4. +See [the example `App.kt`](assets/example/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/App.kt): + +```kotlin +@KoinApplication(modules = [AppModule::class]) +class App : Application() { + override fun onCreate() { + super.onCreate() + Napier.base(DebugAntilog()) + startKoin { + allowOverride(false) + androidLogger() + androidContext(this@App) + } + } +} + +@Module(includes = [AuthModule::class]) +@ComponentScan +class AppModule { + // @Single fixture bindings from Step 4 go here +} +``` + +If the project uses Hilt/Dagger, create the equivalent `@Module`/`@InstallIn` (or component) that provides the +fixtures alongside the feature's own modules, following that project's existing module structure. If the +project wires dependencies by hand with no DI framework, skip a separate module entirely: construct the +fixtures directly where you construct the feature's `DataApi`/`UiApi` implementations (typically in the +`Application` class or a small factory object), and pass them straight into the feature's constructors. + +## Step 6. Create the main Activity +Create a single `ComponentActivity` that sets Compose content wrapped in the common UI module's theme, injects +the feature's `UiApi` and a `CommonFlowViewModel`, and renders the flow with `CommonFlowComposition` inside a +`Scaffold` using the common UI module's app bar. See [the example `MainActivity.kt`](assets/example/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/MainActivity.kt) for the full pattern, +including wiring the Android back gesture through `navigationBackHandler` and finishing the activity when the flow's `finish` callback fires. + +## Step 7. Create the main ViewModel +Create a trivial `CommonFlowViewModel` subclass that takes the feature's `DataApi` via constructor injection and +passes the right `init` value for the feature's `Input` type: + +```kotlin +@KoinViewModel +class MainViewModel(api: AuthDataApi) : CommonFlowViewModel( + api = api, + init = AuthInput(skippable = true) +) +``` + +If the `Input` type needs more than a default value to make sense for a demo (for example data that normally +comes from a parent flow), ask the user what to pass, or use a clearly-marked placeholder value. + +## Step 8. Create the Android manifest and resources +Add a minimal `AndroidManifest.xml` declaring the single launcher activity, and the small resource set an +Android application module needs (`app_name` in `strings.xml`, a launcher icon, and the standard backup/data- +extraction/network-security XML files). Keep this minimal: it's an internal tool, not a production app icon. +With `minSdk` set to a recent version (Step 2), adaptive icons are guaranteed to be available, so a single +`mipmap-anydpi-v26/ic_launcher.xml` (plus its round variant) referencing a vector drawable is enough - don't +generate the legacy per-density PNG sets (`mipmap-hdpi`, `-mdpi`, `-xhdpi`, `-xxhdpi`, `-xxxhdpi`) that exist +only for backwards compatibility with older Android versions this demo doesn't need to support. Copy the +manifest/resource shape from another demo/app module in the project if one exists, adjusting the app name and +dropping anything that's there only for legacy-device support; otherwise create a minimal set from scratch. + +## Step 9. Register the module (if the project needs it) +If the project registers Gradle modules explicitly (check `settings.gradle.kts`), add the new demo module there +following the existing naming convention. diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/api/AuthDataApi.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/api/AuthDataApi.kt new file mode 100644 index 0000000..e03a962 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/api/AuthDataApi.kt @@ -0,0 +1,8 @@ +package com.motorro.commonstatemachine.examples.skills.auth.api +import com.motorro.commonstatemachine.flow.data.CommonFlowDataApi + +/** + * Data API used to run a child-flow in a proxy machine + * Available to a feature consumer + */ +interface AuthDataApi : CommonFlowDataApi \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/api/AuthGesture.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/api/AuthGesture.kt new file mode 100644 index 0000000..b93d81e --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/api/AuthGesture.kt @@ -0,0 +1,7 @@ +package com.motorro.commonstatemachine.examples.skills.auth.api + +/** + * A base type for authentication user gestures + * Available to a feature consumer + */ +interface AuthGesture \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/api/AuthInput.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/api/AuthInput.kt new file mode 100644 index 0000000..90aba9d --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/api/AuthInput.kt @@ -0,0 +1,7 @@ +package com.motorro.commonstatemachine.examples.skills.auth.api + +/** + * Initializing data for the feature flow + * @property skippable If true, allows user to skip authentication + */ +data class AuthInput(val skippable: Boolean) \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/api/AuthResult.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/api/AuthResult.kt new file mode 100644 index 0000000..e57a379 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/api/AuthResult.kt @@ -0,0 +1,7 @@ +package com.motorro.commonstatemachine.examples.skills.auth.api + +/** + * Feature flow result + * @param authenticated Example of a property returned to a parent flow + */ +data class AuthResult(val authenticated: Boolean) \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/api/AuthUiApi.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/api/AuthUiApi.kt new file mode 100644 index 0000000..1663448 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/api/AuthUiApi.kt @@ -0,0 +1,8 @@ +package com.motorro.commonstatemachine.examples.skills.auth.api +import com.motorro.commonstatemachine.flow.compose.CommonFlowUiApi + +/** + * UI API used display a child flow inside a parent composition + * Available to a feature consumer + */ +interface AuthUiApi: CommonFlowUiApi \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/api/AuthUiState.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/api/AuthUiState.kt new file mode 100644 index 0000000..b16324d --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/api/AuthUiState.kt @@ -0,0 +1,7 @@ +package com.motorro.commonstatemachine.examples.skills.auth.api + +/** + * A base type for authentication UI state + * Available to a feature consumer + */ +interface AuthUiState \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/build.gradle.kts b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/build.gradle.kts new file mode 100644 index 0000000..fd7ac87 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/build.gradle.kts @@ -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. + */ + +plugins { + alias(libs.plugins.motorro.android.app) + alias(libs.plugins.koin) +} + +android { + defaultConfig { + minSdk = 34 + } +} + +dependencies { + implementation(project(":examples:skills:appcore")) + implementation(project(":examples:skills:domain")) + implementation(project(":examples:skills:auth:api")) + implementation(project(":examples:skills:auth:implementation")) + implementation(project(":commonstatemachine")) + implementation(project(":coroutines")) + implementation(project(":commonflow:commonflow-data")) + implementation(project(":commonflow:commonflow-compose")) + implementation(project(":commonflow:commonflow-viewmodel")) + + implementation(libs.napier) + + implementation(platform(libs.koin.bom)) + implementation(libs.koin.core) + implementation(libs.koin.annotations) + implementation(libs.koin.core.viewmodel) + implementation(libs.koin.android) + implementation(libs.koin.compose) + implementation(libs.koin.compose.viewmodel) +} diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/AndroidManifest.xml b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/AndroidManifest.xml new file mode 100644 index 0000000..e347350 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/App.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/App.kt new file mode 100644 index 0000000..a79d35e --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/App.kt @@ -0,0 +1,94 @@ +package com.motorro.commonstatemachine.examples.skills.auth.demo + +import android.app.Application +import com.motorro.commonstatemachine.examples.skills.auth.implementation.AuthModule +import com.motorro.commonstatemachine.skills.domain.authenticate.AuthenticateWithPassword +import com.motorro.commonstatemachine.skills.domain.authenticate.GetPasswordRequirements +import com.motorro.commonstatemachine.skills.domain.authenticate.data.PasswordRequirements +import com.motorro.commonstatemachine.skills.domain.exception.AuthenticationException +import com.motorro.commonstatemachine.skills.domain.exception.IOException +import com.motorro.commonstatemachine.skills.domain.session.data.Username +import io.github.aakira.napier.DebugAntilog +import io.github.aakira.napier.Napier +import kotlinx.coroutines.delay +import org.koin.android.ext.koin.androidContext +import org.koin.android.ext.koin.androidLogger +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.KoinApplication +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single +import org.koin.plugin.module.dsl.startKoin +import kotlin.time.Duration.Companion.seconds + +@KoinApplication(modules = [AppModule::class]) +class App : Application() { + override fun onCreate() { + super.onCreate() + + Napier.base(DebugAntilog()) + + startKoin { + allowOverride(false) + androidLogger() + androidContext(this@App) + } + } +} + +@Module(includes = [AuthModule::class]) +@ComponentScan +class AppModule { + + @Single + fun getPasswordRequirements(): GetPasswordRequirements { + var attempt = 1 + val tag = GetPasswordRequirements::class.simpleName!! + return object : GetPasswordRequirements { + override suspend fun invoke(): PasswordRequirements { + delay(2.seconds) + when (attempt) { + 1 -> { + Napier.d(tag = tag) { "Emulating non-fatal error" } + attempt = 2 + throw IOException("Network error") + } + else -> { + Napier.d(tag = tag) { "Emulating success" } + attempt = 1 + return PasswordRequirements( + regex = "^.{8,}$".toRegex(), + description = "Minimum eight characters" + ) + } + } + } + } + } + + @Single + fun authenticateWithPassword(): AuthenticateWithPassword { + var attempt = 1 + val tag = AuthenticateWithPassword::class.simpleName!! + return object : AuthenticateWithPassword { + override suspend fun invoke(username: Username, password: String) { + delay(2.seconds) + when (attempt) { + 1 -> { + Napier.d(tag = tag) { "Emulating credentials error" } + attempt = 2 + throw AuthenticationException("Invalid credentials") + } + 2 -> { + Napier.d(tag = tag) { "Emulating non-fatal error" } + attempt = 3 + throw IOException("Network error") + } + else -> { + Napier.d(tag = tag) { "Emulating success" } + attempt = 1 + } + } + } + } + } +} diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/MainActivity.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/MainActivity.kt new file mode 100644 index 0000000..9f986c2 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/MainActivity.kt @@ -0,0 +1,69 @@ +package com.motorro.commonstatemachine.examples.skills.auth.demo + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.BackHandler +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.motorro.commonstatemachine.examples.auth.appcore.ui.design.SkillsAppBar +import com.motorro.commonstatemachine.examples.auth.appcore.ui.theme.SkillsTheme +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthUiApi +import com.motorro.commonstatemachine.flow.viewmodel.CommonFlowComposition +import io.github.aakira.napier.Napier +import org.koin.compose.koinInject +import org.koin.compose.viewmodel.koinViewModel + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + SkillsTheme { + MainScreen { + finish() + } + } + } + } +} + +@Composable +fun MainScreen(onComplete: () -> Unit) { + val viewModel: MainViewModel = koinViewModel() + val uiApi: AuthUiApi = koinInject() + + CommonFlowComposition( + viewModel = viewModel, + navigationBackHandler = { enabled, onBack -> + BackHandler(enabled, onBack) + }, + content = { state, onGesture -> + Scaffold( + topBar = { + SkillsAppBar( + title = stringResource(R.string.app_name), + modifier = Modifier.fillMaxWidth(), + topLevel = true, + ) + } + ) { paddingValues -> + uiApi.Screen( + state, + onGesture, + Modifier.padding(paddingValues) + ) + } + }, + finish = { + Napier.i { "Finished with: $it" } + onComplete() + } + ) +} + diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/MainViewModel.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/MainViewModel.kt new file mode 100644 index 0000000..88d2fcd --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/MainViewModel.kt @@ -0,0 +1,15 @@ +package com.motorro.commonstatemachine.examples.skills.auth.demo + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthDataApi +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthGesture +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthInput +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthUiState +import com.motorro.commonstatemachine.flow.viewmodel.CommonFlowViewModel +import org.koin.core.annotation.KoinViewModel + +@KoinViewModel +class MainViewModel (api: AuthDataApi) : CommonFlowViewModel( + api = api, + init = AuthInput(skippable = true) +) \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/drawable-v24/ic_launcher_foreground.xml b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..40ce517 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/drawable/ic_launcher_background.xml b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..997ef0b --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..0147ca8 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..0147ca8 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/values/strings.xml b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/values/strings.xml new file mode 100644 index 0000000..3e25bc5 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Auth Demo + \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/xml/backup_rules.xml b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000..fa0f996 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/xml/data_extraction_rules.xml b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..9ee9997 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/xml/network_security_config.xml b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..c64cff3 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/xml/network_security_config.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + 10.0.2.2 + + diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/authenticate/AuthenticateWithPassword.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/authenticate/AuthenticateWithPassword.kt new file mode 100644 index 0000000..1a58d49 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/authenticate/AuthenticateWithPassword.kt @@ -0,0 +1,15 @@ +package com.motorro.commonstatemachine.skills.domain.authenticate + +import com.motorro.commonstatemachine.skills.domain.session.data.Username + +/** + * Authenticates with a password + */ +interface AuthenticateWithPassword { + /** + * Authenticates with [username] and [password] + * @param username Username + * @param password Password + */ + suspend operator fun invoke(username: Username, password: String) +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/authenticate/GetPasswordRequirements.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/authenticate/GetPasswordRequirements.kt new file mode 100644 index 0000000..cfc66ff --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/authenticate/GetPasswordRequirements.kt @@ -0,0 +1,13 @@ +package com.motorro.commonstatemachine.skills.domain.authenticate + +import com.motorro.commonstatemachine.skills.domain.authenticate.data.PasswordRequirements + +/** + * Loads password requirements + */ +interface GetPasswordRequirements { + /** + * Loads password requirements + */ + suspend operator fun invoke(): PasswordRequirements +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/authenticate/data/PasswordRequirements.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/authenticate/data/PasswordRequirements.kt new file mode 100644 index 0000000..01bb73a --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/authenticate/data/PasswordRequirements.kt @@ -0,0 +1,11 @@ +package com.motorro.commonstatemachine.skills.domain.authenticate.data + +/** + * Password requirements + * @property regex Regular expression to match the password + * @property description Description of the password requirements + */ +data class PasswordRequirements( + val regex: Regex, + val description: String +) \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/exception/AppException.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/exception/AppException.kt new file mode 100644 index 0000000..4812838 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/exception/AppException.kt @@ -0,0 +1,35 @@ +package com.motorro.commonstatemachine.skills.domain.exception + +/** + * Base domain exception + * @property message Error message + * @property cause Error cause if any + */ +abstract class AppException(final override val message: String, final override val cause: Throwable? = null) : RuntimeException() { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as AppException + + if (message != other.message) return false + if (cause != other.cause) return false + + return true + } + + override fun hashCode(): Int { + var result = message.hashCode() + result = 31 * result + (cause?.hashCode() ?: 0) + return result + } +} + +/** + * Converts exception to domain + */ +fun Throwable.toAppException(): AppException = when(this) { + is AppException -> this + else -> UnknownException(message ?: "UNKNOWN exception", this) +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/exception/AuthenticationException.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/exception/AuthenticationException.kt new file mode 100644 index 0000000..6bd8c7f --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/exception/AuthenticationException.kt @@ -0,0 +1,6 @@ +package com.motorro.commonstatemachine.skills.domain.exception + +/** + * Authentication exception + */ +class AuthenticationException(message: String, cause: Throwable? = null) : AppException(message, cause) \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/exception/IOException.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/exception/IOException.kt new file mode 100644 index 0000000..ab5320d --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/exception/IOException.kt @@ -0,0 +1,6 @@ +package com.motorro.commonstatemachine.skills.domain.exception + +/** + * IO exception + */ +class IOException(message: String, cause: Throwable? = null) : AppException(message, cause) \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/exception/UnknownException.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/exception/UnknownException.kt new file mode 100644 index 0000000..2259626 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/exception/UnknownException.kt @@ -0,0 +1,6 @@ +package com.motorro.commonstatemachine.skills.domain.exception + +/** + * Unknown exception + */ +class UnknownException(message: String, cause: Throwable? = null) : AppException(message, cause) \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/friends/GetFriendList.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/friends/GetFriendList.kt new file mode 100644 index 0000000..1a19efd --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/friends/GetFriendList.kt @@ -0,0 +1,11 @@ +package com.motorro.commonstatemachine.skills.domain.friends + +import com.motorro.commonstatemachine.skills.domain.friends.data.Friend +import kotlinx.coroutines.flow.Flow + +/** + * Retrieves a friend list + */ +interface GetFriendList { + operator fun invoke(): Flow> +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/friends/data/Friend.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/friends/data/Friend.kt new file mode 100644 index 0000000..dae8b90 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/friends/data/Friend.kt @@ -0,0 +1,3 @@ +package com.motorro.commonstatemachine.skills.domain.friends.data + +data class Friend(val id: Int, val data: FriendData) \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/friends/data/FriendData.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/friends/data/FriendData.kt new file mode 100644 index 0000000..da8cb5d --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/friends/data/FriendData.kt @@ -0,0 +1,9 @@ +package com.motorro.commonstatemachine.skills.domain.friends.data + +/** + * Friend properties + */ +data class FriendData( + val name: String, + val age: Int +) \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/session/SessionManager.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/session/SessionManager.kt new file mode 100644 index 0000000..76f8b7b --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/session/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.commonstatemachine.skills.domain.session + +import com.motorro.commonstatemachine.skills.domain.session.data.Session +import kotlinx.coroutines.flow.Flow + +/** + * Session manager + */ +interface SessionManager { + /** + * Session state + */ + val session: Flow + + /** + * Updates session state + * @param session New session state + */ + suspend fun update(session: Session) +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/session/data/Session.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/session/data/Session.kt new file mode 100644 index 0000000..29f49fd --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/session/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.commonstatemachine.skills.domain.session.data + +/** + * Session state + */ +sealed class Session { + /** + * No session + */ + data object None : Session() + + /** + * Session is active + */ + data class Active(val username: Username) : Session() +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/session/data/Username.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/session/data/Username.kt new file mode 100644 index 0000000..bcd923c --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/domain/session/data/Username.kt @@ -0,0 +1,6 @@ +package com.motorro.commonstatemachine.skills.domain.session.data + +import kotlin.jvm.JvmInline + +@JvmInline +value class Username(val value: String) \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/AuthDataApiImpl.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/AuthDataApiImpl.kt new file mode 100644 index 0000000..aa90a5e --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/AuthDataApiImpl.kt @@ -0,0 +1,35 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation + +import com.motorro.commonstatemachine.CommonMachineState +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthDataApi +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthGesture +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthInput +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthUiState +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.state.AuthStateFactory +import com.motorro.commonstatemachine.flow.data.CommonFlowHost +import org.koin.core.annotation.Factory +import org.koin.core.component.KoinComponent +import org.koin.core.component.get +import org.koin.core.parameter.parametersOf + +/** + * Data API implementation + */ +@Factory +internal class AuthDataApiImpl : AuthDataApi, KoinComponent { + + @Suppress("UNCHECKED_CAST") + override fun init(flowHost: CommonFlowHost, input: AuthInput): CommonMachineState { + val factory = this@AuthDataApiImpl.get { + parametersOf(flowHost) + } + return factory.preloading(input) as CommonMachineState + } + + override fun getDefaultUiState(): AuthUiState = AuthUiStateImpl.Loading + + override fun getBackGesture(): AuthGesture = AuthGestureImpl.Back +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/AuthModule.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/AuthModule.kt new file mode 100644 index 0000000..b646f31 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/AuthModule.kt @@ -0,0 +1,8 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan +class AuthModule \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/AuthUiApiImpl.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/AuthUiApiImpl.kt new file mode 100644 index 0000000..a0ccad4 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/AuthUiApiImpl.kt @@ -0,0 +1,27 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthGesture +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthUiApi +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthUiState +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.ui.AuthScreen +import org.koin.core.annotation.Factory + +/** + * UI API implementation + */ +@Factory +internal class AuthUiApiImpl : AuthUiApi { + @Composable + override fun Screen( + state: AuthUiState, + onGesture: (AuthGesture) -> Unit, + modifier: Modifier + ) = AuthScreen( + state = state as AuthUiStateImpl, + modifier = modifier, + onGesture = onGesture + ) +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/data/AuthGestureImpl.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/data/AuthGestureImpl.kt new file mode 100644 index 0000000..ba1df8e --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/data/AuthGestureImpl.kt @@ -0,0 +1,38 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.data + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthGesture + +/** + * All gestures available in this feature + */ +internal sealed class AuthGestureImpl : AuthGesture { + /** + * Backwards navigation element clicked + */ + data object Back : AuthGestureImpl() + + /** + * Default call-to-action clicked + */ + data object Action : AuthGestureImpl() + + /** + * Gestures for the login form + */ + sealed class Form : AuthGestureImpl() { + /** + * Username input changed + */ + data class UsernameChanged(val value: String) : Form() + + /** + * Password input changed + */ + data class PasswordChanged(val value: String) : Form() + + /** + * Skips the authentication + */ + data object Skip : Form() + } +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/data/AuthStateData.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/data/AuthStateData.kt new file mode 100644 index 0000000..d8cf559 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/data/AuthStateData.kt @@ -0,0 +1,23 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.data + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthInput +import com.motorro.commonstatemachine.skills.domain.authenticate.data.PasswordRequirements + +/** + * Interstate data to pass between the machine states + * @property input Flow input from the flow initialization + * @property passwordRequirements Password requirements (preloaded later) + * @property username User input for the username is stored here + * @property password User input for the password is stored here + */ +internal data class AuthStateData( + val input: AuthInput, + val passwordRequirements: PasswordRequirements, + val username: String = "", + val password: String = "" +) + +/** + * Some utility function to check the form state + */ +internal fun AuthStateData.isValidToAuthenticate() = username.isNotBlank() && passwordRequirements.regex.matches(password) diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/data/AuthUiStateImpl.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/data/AuthUiStateImpl.kt new file mode 100644 index 0000000..5476939 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/data/AuthUiStateImpl.kt @@ -0,0 +1,36 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.data + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthUiState + +/** + * All gestures available in this feature + */ +internal sealed class AuthUiStateImpl : AuthUiState { + /** + * Loading data + */ + data object Loading : AuthUiStateImpl() + + /** + * Login form + * @property username Username + * @property password Password + * @property passwordRequirements Password requirements description + * @property loginEnabled If true, the form is complete and the user can log in + * @property canSkip If true, the user can skip the authentication + */ + data class Form( + val username: String, + val password: String, + val passwordRequirements: String, + val loginEnabled: Boolean, + val canSkip: Boolean + ) : AuthUiStateImpl() + + /** + * Displays error + * @property message Error message to display + * @property canRetry If true, the error is recoverable and can be retried + */ + data class Error(val message: String, val canRetry: Boolean) : AuthUiStateImpl() +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/AuthContext.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/AuthContext.kt new file mode 100644 index 0000000..21bc639 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/AuthContext.kt @@ -0,0 +1,25 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.ui.AuthUiRenderer +import com.motorro.commonstatemachine.flow.data.CommonFlowHost + +/** + * Common tools used by states + */ +internal interface AuthContext { + /** + * State-factory + */ + val factory: AuthStateFactory + + /** + * Flow-host + */ + val flowHost: CommonFlowHost + + /** + * UI-renderer + */ + val renderer: AuthUiRenderer +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/AuthState.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/AuthState.kt new file mode 100644 index 0000000..bcf46b6 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/AuthState.kt @@ -0,0 +1,10 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.CommonMachineState +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl + +/** + * Convenient type-alias - bounds gesture and ui state system + */ +internal typealias AuthState = CommonMachineState \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/AuthStateFactory.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/AuthStateFactory.kt new file mode 100644 index 0000000..5013652 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/AuthStateFactory.kt @@ -0,0 +1,54 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthInput +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthStateData +import com.motorro.commonstatemachine.skills.domain.exception.AppException + +/** + * State-factory interface + * Abstracts the machine state from the state creation + */ +internal interface AuthStateFactory { + /** + * Creates a flow preloading state. + * The interstate data will be created by the state. + * @param input Initialization params + */ + fun preloading(input: AuthInput): AuthState + + /** + * Creates a preload error state. + * Again, there's no interstate data here as the preloading failed. + * @param input Initialization params + * @param error Error that occurred during the initialization + */ + fun preloadError(input: AuthInput, error: AppException): AuthState + + /** + * Creates authentication form state. + * Now we have the interstate data, pass it as a parameter. + * @param data Interstate data + */ + fun form(data: AuthStateData): AuthState + + /** + * Creates the authentication state. + * @param data Interstate data + */ + fun authenticating(data: AuthStateData): AuthState + + /** + * Creates an authentication error state. + * @param data Interstate data + * @param error Error that occurred during the initialization + */ + fun authenticationError(data: AuthStateData, error: AppException): AuthState + + /** + * Creates the termination state that finishes the flow. + * All interstate data is lost as not needed anymore. + * @param result Flow result + */ + fun terminated(result: AuthResult): AuthState +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/AuthStateFactoryImpl.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/AuthStateFactoryImpl.kt new file mode 100644 index 0000000..35be8ff --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/AuthStateFactoryImpl.kt @@ -0,0 +1,84 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthInput +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthStateData +import com.motorro.commonstatemachine.examples.skills.auth.implementation.ui.AuthUiRenderer +import com.motorro.commonstatemachine.flow.data.CommonFlowHost +import com.motorro.commonstatemachine.skills.domain.exception.AppException +import io.github.aakira.napier.Napier +import org.koin.core.annotation.Factory +import org.koin.core.annotation.InjectedParam + +/** + * Main state-factory implementation + */ +@Factory +internal class AuthStateFactoryImpl( + private val preloadingStateFactory: PreloadingState.StateFactory, + private val authenticatingStateFactory: AuthenticatingState.StateFactory, + @InjectedParam flowHost: CommonFlowHost, + renderer: AuthUiRenderer +) : AuthStateFactory { + + /** + * Context implementation + */ + private val context = object : AuthContext { + override val factory: AuthStateFactory = this@AuthStateFactoryImpl + override val flowHost: CommonFlowHost = flowHost + override val renderer: AuthUiRenderer = renderer + } + + /** + * Creates preloading state with help of a preloading state factory + */ + override fun preloading(input: AuthInput) = preloadingStateFactory.create( + context, + input + ) + + /** + * Creates preload error state directly + */ + override fun preloadError(input: AuthInput, error: AppException) = PreloadingErrorState( + context, + input, + error + ) + + /** + * Creates authentication form state + */ + override fun form(data: AuthStateData) = FormState( + context, + data + ) + + /** + * Creates authenticating state + */ + override fun authenticating(data: AuthStateData): AuthState = authenticatingStateFactory.create( + context, + data + ) + + /** + * Creates authentication error state + */ + override fun authenticationError(data: AuthStateData, error: AppException) = AuthenticationErrorState( + context, + data, + error + ) + + /** + * Ad-hoc terminated state implementation + */ + override fun terminated(result: AuthResult) = object : AuthState() { + override fun doStart() { + Napier.d { "Terminating flow..." } + context.flowHost.onComplete(result) + } + } +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/AuthenticatingState.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/AuthenticatingState.kt new file mode 100644 index 0000000..f337087 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/AuthenticatingState.kt @@ -0,0 +1,81 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthStateData +import com.motorro.commonstatemachine.skills.domain.authenticate.AuthenticateWithPassword +import com.motorro.commonstatemachine.skills.domain.exception.toAppException +import com.motorro.commonstatemachine.skills.domain.session.data.Username +import io.github.aakira.napier.Napier +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch +import org.koin.core.annotation.Factory + +/** + * Authenticates the user + * @param context Auth context + * @param data Interstate data + * @param authenticate Authenticate usecase + */ +internal class AuthenticatingState( + context: AuthContext, + private val data: AuthStateData, + private val authenticate: AuthenticateWithPassword +) : BaseAuthState(context) { + + /** + * State start-up + */ + override fun doStart() { + setUiState { + renderLoading() + } + doAuthenticate() + } + + /** + * Gesture processing + */ + override fun doProcess(gesture: AuthGestureImpl) { + when(gesture) { + is AuthGestureImpl.Back -> setMachineState { + Napier.d { "Back gesture. Returning to form..." } + form(data) + } + else -> super.doProcess(gesture) + } + } + + /** + * Authentication function + */ + private fun doAuthenticate() = stateScope.launch { + try { + authenticate(Username(data.username), data.password) + // Advance to the next state + setMachineState { + terminated(AuthResult(authenticated = true)) + } + } catch (e: Throwable) { + ensureActive() + Napier.w(e) { "Error during authentication" } + // Advance to the error state + setMachineState { + authenticationError(data, e.toAppException()) + } + } + } + + /** + * State factory + * State dependencies are injected here + */ + @Factory + class StateFactory(private val authenticate: AuthenticateWithPassword) { + fun create(context: AuthContext, data: AuthStateData): AuthState = AuthenticatingState( + context = context, + data = data, + authenticate = authenticate + ) + } +} diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/AuthenticationErrorState.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/AuthenticationErrorState.kt new file mode 100644 index 0000000..772a972 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/AuthenticationErrorState.kt @@ -0,0 +1,53 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthStateData +import com.motorro.commonstatemachine.skills.domain.exception.AppException +import com.motorro.commonstatemachine.skills.domain.exception.IOException +import io.github.aakira.napier.Napier + +/** + * Handles authentication error + * If the error is recoverable (IOException), allows to transfer back to authenticating to retry. + * Otherwise returns to form. + * @param context Auth context + * @param data Interstate data + * @param error Error to handle + */ +internal class AuthenticationErrorState( + context: AuthContext, + private val data: AuthStateData, + private val error: AppException +) : BaseAuthState(context) { + override fun doStart() { + setUiState { + renderFullScreenError(error, canRetry()) + } + } + + private fun canRetry(): Boolean = when(error) { + is IOException -> true + else -> false + } + + override fun doProcess(gesture: AuthGestureImpl) { + when(gesture) { + is AuthGestureImpl.Back -> setMachineState { + Napier.d { "Back gesture. Returning to form..." } + form(data) + } + is AuthGestureImpl.Action -> if (canRetry()) { + Napier.d { "Action gesture. Retrying authentication..." } + setMachineState { + authenticating(data) + } + } else { + Napier.d { "Action gesture. Returning to form..." } + setMachineState { + form(data) + } + } + else -> super.doProcess(gesture) + } + } +} diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/BaseAuthState.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/BaseAuthState.kt new file mode 100644 index 0000000..267cf74 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/BaseAuthState.kt @@ -0,0 +1,34 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.coroutines.CoroutineState +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.ui.AuthUiRenderer +import io.github.aakira.napier.Napier + +/** + * Base class for the most logic states + */ +internal abstract class BaseAuthState(context: AuthContext) : CoroutineState(), AuthContext by context { + + /** + * Common gesture processing (logs a warning for an unexpected gesture) + */ + override fun doProcess(gesture: AuthGestureImpl) { + Napier.w { "Unsupported gesture: $gesture" } + } + + /** + * Convenience function to switch a machine state + */ + protected inline fun setMachineState(block: AuthStateFactory.() -> AuthState) { + setMachineState(block(factory)) + } + + /** + * Convenience function to render UI state + */ + protected inline fun setUiState(block: AuthUiRenderer.() -> AuthUiStateImpl) { + setUiState(renderer.block()) + } +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/FormState.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/FormState.kt new file mode 100644 index 0000000..c52effb --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/FormState.kt @@ -0,0 +1,75 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthStateData +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.isValidToAuthenticate +import io.github.aakira.napier.Napier +import org.jetbrains.annotations.VisibleForTesting + +/** + * Authentication form state - an interactive state that: + * - handles user input + * - updates the UI state + * - validates the input + * - updates the interstate data + * @param context Auth context + * @param data Interstate data. Visible for testing. + */ +internal class FormState( + context: AuthContext, + @get:VisibleForTesting var data: AuthStateData, +) : BaseAuthState(context) { + + /** + * State start-up + */ + override fun doStart() { + setUiState { + renderForm(data, data.isValidToAuthenticate()) + } + } + + /** + * Updates the interstate data and renders the form + */ + private inline fun updateData(block: AuthStateData.() -> AuthStateData) { + val oldData = data + val newData = data.block() + if (oldData != newData) { + data = newData + setUiState { + renderForm(newData, newData.isValidToAuthenticate()) + } + } + } + + /** + * Processes the gesture + */ + override fun doProcess(gesture: AuthGestureImpl) { + when(gesture) { + is AuthGestureImpl.Back -> setMachineState { + terminated(AuthResult(false)) + } + is AuthGestureImpl.Action -> if (data.isValidToAuthenticate()) { + Napier.d { "Action gesture. Authenticating..." } + setMachineState { + authenticating(data) + } + } + is AuthGestureImpl.Form.UsernameChanged -> updateData { + copy(username = gesture.value) + } + is AuthGestureImpl.Form.PasswordChanged -> updateData { + copy(password = gesture.value) + } + is AuthGestureImpl.Form.Skip -> if (data.input.skippable) { + Napier.d { "Skipping authentication..." } + setMachineState { + terminated(AuthResult(false)) + } + } + } + } +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/PreloadingErrorState.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/PreloadingErrorState.kt new file mode 100644 index 0000000..61db5fc --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/PreloadingErrorState.kt @@ -0,0 +1,52 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthInput +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.skills.domain.exception.AppException +import com.motorro.commonstatemachine.skills.domain.exception.IOException +import io.github.aakira.napier.Napier + +/** + * Handles preloading error + * If the error is recoverable, allows to transfer back to loading to retry, otherwise terminates the flow. + * @param context Auth context + * @param error Error to handle + */ +internal class PreloadingErrorState( + context: AuthContext, + private val input: AuthInput, + private val error: AppException +) : BaseAuthState(context) { + override fun doStart() { + setUiState { + renderFullScreenError(error, canRetry()) + } + } + + private fun canRetry(): Boolean = when(error) { + is IOException -> true + else -> false + } + + override fun doProcess(gesture: AuthGestureImpl) { + when(gesture) { + is AuthGestureImpl.Back -> setMachineState { + Napier.d { "Back gesture. Terminating flow..." } + terminated(AuthResult(false)) + } + is AuthGestureImpl.Action -> if (canRetry()) { + Napier.d { "Action gesture. Retrying initialization..." } + setMachineState { + preloading(input) + } + } else { + Napier.d { "Action gesture. Terminating flow..." } + setMachineState { + terminated(AuthResult(false)) + } + } + else -> super.doProcess(gesture) + } + } +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/PreloadingState.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/PreloadingState.kt new file mode 100644 index 0000000..63dd138 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/state/PreloadingState.kt @@ -0,0 +1,78 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.state + +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthInput +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthStateData +import com.motorro.commonstatemachine.skills.domain.authenticate.GetPasswordRequirements +import com.motorro.commonstatemachine.skills.domain.exception.toAppException +import io.github.aakira.napier.Napier +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch +import org.koin.core.annotation.Factory + +/** + * Example of a state that preloads some data with the usecase + * and creates the valid [data-state](data-and-ui-implementation.md#step-3-interstate-data). + */ +internal class PreloadingState( + context: AuthContext, + private val input: AuthInput, + private val getPasswordRequirements: GetPasswordRequirements +) : BaseAuthState(context) { + + /** + * State start-up + */ + override fun doStart() { + setUiState { + renderLoading() + } + preload() + } + + /** + * Dummy gesture processing + */ + override fun doProcess(gesture: AuthGestureImpl) { + when(gesture) { + is AuthGestureImpl.Back -> setMachineState { + terminated(AuthResult(false)) + } + else -> super.doProcess(gesture) + } + } + + /** + * Dummy preloading function + */ + private fun preload() = stateScope.launch { + try { + val passwordRequirements = getPasswordRequirements() + // Advance to the next state + setMachineState { + form(AuthStateData(input, passwordRequirements)) + } + } catch (e: Throwable) { + ensureActive() + Napier.w(e) { "Error during initialization" } + // Advance to the error state + setMachineState { + preloadError(input, e.toAppException()) + } + } + } + + /** + * State factory + * State dependencies are injected here + */ + @Factory + class StateFactory(private val getPasswordRequirements: GetPasswordRequirements) { + fun create(context: AuthContext, input: AuthInput): AuthState = PreloadingState( + context = context, + input = input, + getPasswordRequirements = getPasswordRequirements + ) + } +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/ui/AuthErrorScreen.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/ui/AuthErrorScreen.kt new file mode 100644 index 0000000..fd8c8a1 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/ui/AuthErrorScreen.kt @@ -0,0 +1,79 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.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.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.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.motorro.commonstatemachine.examples.auth.appcore.ui.preview.SkillsPreviewComposition +import com.motorro.commonstatemachine.examples.auth.appcore.ui.theme.Dimensions +import com.motorro.commonstatemachine.examples.skills.auth.implementation.Res +import com.motorro.commonstatemachine.examples.skills.auth.implementation.btn_close +import com.motorro.commonstatemachine.examples.skills.auth.implementation.btn_retry +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl +import org.jetbrains.compose.resources.stringResource + +@Composable +internal fun AuthErrorScreen( + state: AuthUiStateImpl.Error, + modifier: Modifier = Modifier, + onGesture: (AuthGestureImpl) -> Unit +) { + Column( + modifier = modifier.fillMaxSize().padding(Dimensions.medium), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = state.message, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodyLarge + ) + Spacer(Modifier.height(Dimensions.medium)) + Button(onClick = { onGesture(AuthGestureImpl.Action) }) { + Text( + if (state.canRetry) { + stringResource(Res.string.btn_retry) + } else { + stringResource(Res.string.btn_close) + } + ) + } + } +} + +private class AuthErrorDataProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + AuthUiStateImpl.Error( + message = "A recoverable error occurred", + canRetry = true + ), + AuthUiStateImpl.Error( + message = "A non-recoverable error occurred", + canRetry = false + ) + ) +} + +@Preview +@Composable +private fun AuthErrorPreview(@PreviewParameter(AuthErrorDataProvider::class) state: AuthUiStateImpl.Error?) { + if (null == state) return + SkillsPreviewComposition { + AuthErrorScreen( + state = state, + modifier = Modifier.fillMaxSize() + ) {} + } +} diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/ui/AuthFormScreen.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/ui/AuthFormScreen.kt new file mode 100644 index 0000000..715a6e5 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/ui/AuthFormScreen.kt @@ -0,0 +1,122 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.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.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +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.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.motorro.commonstatemachine.examples.auth.appcore.ui.preview.SkillsPreviewComposition +import com.motorro.commonstatemachine.examples.auth.appcore.ui.theme.Dimensions +import com.motorro.commonstatemachine.examples.skills.auth.implementation.Res +import com.motorro.commonstatemachine.examples.skills.auth.implementation.btn_login +import com.motorro.commonstatemachine.examples.skills.auth.implementation.btn_skip +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.input_password +import com.motorro.commonstatemachine.examples.skills.auth.implementation.input_username +import org.jetbrains.compose.resources.stringResource + +@Composable +internal fun AuthFormScreen( + state: AuthUiStateImpl.Form, + modifier: Modifier = Modifier, + onGesture: (AuthGestureImpl) -> Unit, +) { + Column( + modifier = modifier.fillMaxSize().padding(Dimensions.medium), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + TextField( + value = state.username, + onValueChange = { onGesture(AuthGestureImpl.Form.UsernameChanged(it)) }, + label = { Text(stringResource(Res.string.input_username)) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true + ) + Spacer(Modifier.height(Dimensions.medium)) + TextField( + value = state.password, + onValueChange = { onGesture(AuthGestureImpl.Form.PasswordChanged(it)) }, + label = { Text(stringResource(Res.string.input_password)) }, + modifier = Modifier.fillMaxWidth(), + visualTransformation = PasswordVisualTransformation(), + singleLine = true + ) + Text( + text = state.passwordRequirements, + modifier = Modifier.fillMaxWidth().padding(top = Dimensions.small), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(Modifier.height(Dimensions.medium)) + Button( + onClick = { onGesture(AuthGestureImpl.Action) }, + enabled = state.loginEnabled, + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(Res.string.btn_login)) + } + if (state.canSkip) { + Spacer(Modifier.height(Dimensions.medium)) + OutlinedButton( + onClick = { onGesture(AuthGestureImpl.Form.Skip) }, + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(Res.string.btn_skip)) + } + } + } +} + +private class AuthFormDataProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + AuthUiStateImpl.Form( + username = "", + password = "", + passwordRequirements = "At least 8 characters", + loginEnabled = false, + canSkip = true + ), + AuthUiStateImpl.Form( + username = "user", + password = "password", + passwordRequirements = "At least 8 characters", + loginEnabled = true, + canSkip = true + ), + AuthUiStateImpl.Form( + username = "user", + password = "password", + passwordRequirements = "At least 8 characters", + loginEnabled = true, + canSkip = false + ) + ) +} + +@Preview +@Composable +private fun AuthFormPreview(@PreviewParameter(AuthFormDataProvider::class) state: AuthUiStateImpl.Form?) { + if (null == state) return + SkillsPreviewComposition { + AuthFormScreen( + state = state, + modifier = Modifier.fillMaxSize() + ) {} + } +} diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/ui/AuthLoadingScreen.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/ui/AuthLoadingScreen.kt new file mode 100644 index 0000000..90875c9 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/ui/AuthLoadingScreen.kt @@ -0,0 +1,21 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.ui + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.motorro.commonstatemachine.examples.auth.appcore.ui.design.SkillsLoading +import com.motorro.commonstatemachine.examples.auth.appcore.ui.preview.SkillsPreviewComposition + +@Composable +internal fun AuthLoadingScreen(modifier: Modifier = Modifier) { + SkillsLoading(modifier) +} + +@Preview +@Composable +private fun AuthLoadingPreview() { + SkillsPreviewComposition { + AuthLoadingScreen(Modifier.fillMaxSize()) + } +} diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/ui/AuthScreen.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/ui/AuthScreen.kt new file mode 100644 index 0000000..f2fbfd9 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/ui/AuthScreen.kt @@ -0,0 +1,68 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.ui + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.motorro.commonstatemachine.examples.auth.appcore.ui.preview.SkillsPreviewComposition +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthGestureImpl +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl + +/** + * Authentication screen + * @param state UI state + * @param onGesture Gesture handler + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun AuthScreen( + state: AuthUiStateImpl, + modifier: Modifier = Modifier, + onGesture: (AuthGestureImpl) -> Unit, +) { + val modifier = modifier.fillMaxSize() + when(state) { + AuthUiStateImpl.Loading -> AuthLoadingScreen(modifier) + is AuthUiStateImpl.Error -> AuthErrorScreen( + state = state, + modifier = modifier, + onGesture = onGesture + ) + is AuthUiStateImpl.Form -> AuthFormScreen( + state = state, + modifier = modifier, + onGesture = onGesture + ) + } +} + +private class AuthScreenDataProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + AuthUiStateImpl.Loading, + AuthUiStateImpl.Form( + username = "user", + password = "password", + passwordRequirements = "At least 8 characters", + loginEnabled = true, + canSkip = true + ), + AuthUiStateImpl.Error( + message = "A recoverable error occurred", + canRetry = true + ) + ) +} + +@Preview +@Composable +private fun SkillsScreenPreview(@PreviewParameter(AuthScreenDataProvider::class) state: AuthUiStateImpl?) { + if (null == state) return + SkillsPreviewComposition { + AuthScreen( + state = state + ) {} + } +} diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/ui/AuthUiRenderer.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/ui/AuthUiRenderer.kt new file mode 100644 index 0000000..f117aba --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/ui/AuthUiRenderer.kt @@ -0,0 +1,33 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.ui + +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthStateData +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.commonstatemachine.skills.domain.exception.AppException + +/** + * Transforms the data state to the UI state + */ +internal interface AuthUiRenderer { + /** + * Renders loading. + * The result would be an object so we don't need interstate data here. + */ + fun renderLoading(): AuthUiStateImpl + + /** + * Renders login form. + * The data could be fully taken from the data state. + * @param data Data-state to transform + * @param isValid If the form is valid or not + */ + fun renderForm(data: AuthStateData, isValid: Boolean): AuthUiStateImpl + + /** + * Renders error that takes the full screen + * - doesn't require any data-state + * - requires an error and takes a message from it + * @param error Error to present to the user + * @param canRetry Error could be retried + */ + fun renderFullScreenError(error: AppException, canRetry: Boolean): AuthUiStateImpl +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/ui/AuthUiRendererImpl.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/ui/AuthUiRendererImpl.kt new file mode 100644 index 0000000..8f4e0b7 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/ui/AuthUiRendererImpl.kt @@ -0,0 +1,28 @@ +package com.motorro.commonstatemachine.examples.skills.auth.implementation.ui + +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthStateData +import com.motorro.commonstatemachine.examples.skills.auth.implementation.data.AuthUiStateImpl +import com.motorro.commonstatemachine.skills.domain.exception.AppException +import org.koin.core.annotation.Factory + +/** + * UI renderer implementation + */ +@Factory +internal class AuthUiRendererImpl : AuthUiRenderer { + + override fun renderLoading(): AuthUiStateImpl = AuthUiStateImpl.Loading + + override fun renderForm(data: AuthStateData, isValid: Boolean) = AuthUiStateImpl.Form( + username = data.username, + password = data.password, + passwordRequirements = data.passwordRequirements.description, + loginEnabled = isValid, + canSkip = data.input.skippable + ) + + override fun renderFullScreenError(error: AppException, canRetry: Boolean) = AuthUiStateImpl.Error( + message = error.message, + canRetry = canRetry + ) +} \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/references/ARCHITECTURE.md b/skills/commonstatemachine/common-child-flow/demo/references/ARCHITECTURE.md new file mode 100644 index 0000000..94621a6 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/references/ARCHITECTURE.md @@ -0,0 +1,38 @@ +# CommonStateMachine Architecture + +![Architecture Diagram](architecture.puml) + +## Core Mechanics +- **Machine State:** A self-contained logic unit. It handles **Gestures (G)** and produces **UI States (U)**. +- **Lifecycle:** + - `doStart()`: Called when state becomes active. Use for initialization or launching side effects. + - `doProcess(gesture)`: Processes user input. + - `doClear()`: Cleanup when leaving the state. +- **Transitions:** States call `setMachineState(nextState)` to move the machine forward. No external "router" is needed. +- **UI Updates:** States call `setUiState(newState)` to update the View. +- **State Machine:** The container for the current **Machine State**. + - It maintains the state transition logic. + - `FlowStateMachine` (standard implementation) exports the current **UI State** as a `StateFlow`. + - It provides the `process(gesture)` entry point for user interaction. +- **Navigation Inversion:** Navigation is a logical state transition within the state machine (Logic/ViewModel layer), not something the View drives. The user + doesn't navigate between screens - gestures (or a state's own work finishing) move the machine from one logical state to another, and whichever state is + active decides what to show by calling `setUiState`. Several logical states can therefore render the exact same UI State one after another - for example, a + preload step and an auth-check step can both just show a loading spinner - with nothing visibly changing until some state actually calls `setUiState` with + something new. The same decoupling is why a transition can carry live, non-serializable data (an open coroutine, a scope, an object reference) forward as a + plain constructor argument, unlike a screen-based navigation graph where arguments between destinations typically need to be serializable. See + [MVI Architecture with a State Machine: Basics](https://proandroiddev.com/mvi-architecture-with-a-state-machine-basics-721c5ebed893) for the full rationale. +- **ViewModel Integration:** The `ViewModel` (or equivalent lifecycle host) typically: + - Holds the `StateMachine` instance. + - Delegates user gestures to `stateMachine.process(gesture)`. + - Collects and exposes `stateMachine.uiState`. + - Calls `stateMachine.clear()` in its `onCleared()` lifecycle method to clean up resources. + +## Extra Components-Helpers +- **State Factory:** Decouples state creation from the states themselves. This allows for easy dependency injection and makes unit testing transitions simple + by mocking the factory. +- **UI Renderer:** A pure function or class that transforms the internal **Data State** into a **UI State**. This separates the business logic of "what to do" + from the presentation logic of "what to show." +- **State Context:** A common interface passed to all states in a machine. It provides access to shared tools like the State Factory, UI Renderer, + and Feature Hosts, reducing constructor boilerplate. +- **Data State:** A simple data class that holds the cumulative state of the flow (e.g., entered username, password, validation errors). It is passed between + states to maintain continuity. diff --git a/skills/commonstatemachine/common-child-flow/demo/references/COMMON-CHILD-FLOW.md b/skills/commonstatemachine/common-child-flow/demo/references/COMMON-CHILD-FLOW.md new file mode 100644 index 0000000..655ed24 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/references/COMMON-CHILD-FLOW.md @@ -0,0 +1,41 @@ +# Common Child Flow + +![Common Child Flow Diagram](common-child-flow.puml) + +## Philosophy and Principles +- **Modularity via Proxying:** Features are independent modules. A parent flow hosts a child flow using `ProxyMachineState`, which bridges incompatible Gesture/UI systems. +- **Direct Data Flow:** Transitions occur in memory. Complex data or even running coroutines can be passed between states without serialization or "parceling" - + unlike a screen-based navigation library (e.g. Jetpack Navigation), where arguments passed between destinations typically need to be serializable. This + follows from the same [Navigation Inversion](ARCHITECTURE.md) principle the base state machine already relies on. +- **KMP Ready:** Logic and Data are isolated from the View, making the core feature logic easily shareable across platforms. +- **Standardized Child Flows:** + - `commonflow-data`: Standardizes flow initialization and result handling. + - `commonflow-compose`: Provides a unified way to inject UI into a composition. + - `commonflow-viewmodel`: Bridges the state machine to the platform's Lifecycle and Navigation frameworks. + +## Data API +Data API is defined in the `commonflow-data` module and consists of two primary interfaces: + +### `CommonFlowDataApi` +This is the **Feature Entry Point**. It is defined in the feature module and provides the host with everything needed to launch the child flow. +- **`init(flowHost: CommonFlowHost, input: I)`**: Initializes the child flow and returns the first `MachineState`. It injects the `flowHost` so + the child can signal when it's done. +- **`getDefaultUiState()`**: Provides the initial state (like "Loading") to be displayed while the child machine is starting up. +- **`getBackGesture()`**: Defines which gesture represents "Back" in the feature, allowing the host to bridge system back-presses to the child logic. + +### `CommonFlowHost` +This is the **Communication Bridge**. It is implemented by the parent's `ProxyMachineState` and passed to the child flow. +- **`onComplete(result: R)`**: The child flow calls this method when it reaches a terminal state. The host implementation then receives the `result` and handles + the transition to the next part of the parent flow. + +## UI API +UI API is defined in the `commonflow-compose` module and consists of the following interface: + +### `CommonFlowUiApi` +This interface provides the **Feature View**. It is implemented in the feature module using `Compose` and is used by the host to render the child flow's UI. + +- **`Screen(state: U, onGesture: (G) -> Unit, modifier: Modifier)`**: A `@Composable` function that takes the current feature **UI State (U)** + and a gesture handler for feature **Gestures (G)**. It encapsulates all the UI logic for the feature, allowing the host to simply plug it into its composition. + +This separation ensures that the host doesn't need to know about the internal UI components of the child flow, only how to provide the state and handle +the gestures, which are bridged by the `ProxyMachineState`. diff --git a/skills/commonstatemachine/common-child-flow/demo/references/common-child-flow.puml b/skills/commonstatemachine/common-child-flow/demo/references/common-child-flow.puml new file mode 100644 index 0000000..284a449 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/references/common-child-flow.puml @@ -0,0 +1,187 @@ +@startuml +'https://plantuml.com/class-diagram + +hide empty members + +interface CommonFlowDataApi +interface CommonFlowUiApi +abstract class CommonMachineState +abstract class CoroutineState + +CoroutineState -up-|> CommonMachineState + +package API { + interface AuthGesture + note left + Marker intent (gesture) interface for Auth feature + May include public properties available to the API client + end note + interface AuthUiState + note right + Marker UI-state interface for Auth feature + May include public properties available to the API client + end note + + class AuthInput + class AuthResult + + interface AuthDataApi { + +init(flowHost: CommonFlowHost, input: AuthInput): CommonMachineState + +getDefaultUiState(): AuthUiState + +getBackGesture(): AuthGesture? = null + } + AuthDataApi -up-|> CommonFlowDataApi: implements + AuthDataApi -down-> AuthGesture: driven by + AuthDataApi -down-> AuthUiState: displays + AuthDataApi -right-> AuthResult: returns + AuthInput -> AuthDataApi: initializes + + interface AuthUiApi { + +Screen(state: AuthUiState, onGesture: (AuthGesture) -> Unit, modifier: Modifier = Modifier) + } + AuthUiApi -up-|> CommonFlowUiApi: implements + AuthUiApi -down-> AuthGesture: emits + AuthUiApi -down-> AuthUiState: displays +} + +package Implementation { + package data { + interface AuthGestureImpl + note left + A sealed structure of all available gestures (UI intents) for the feature + end note + + object Back + note bottom + Example: Back navigation button + end note + + object Action + note bottom + Example: Main action button + end note + AuthGestureImpl -up-|> AuthGesture + AuthGestureImpl <|-down- Back + AuthGestureImpl <|-down- Action + + interface AuthUiStateImpl + note right + A sealed structure of all possible UI states for the feature + end note + + object Loading + note bottom + Example: Loading screen + end note + + class Form { + +username: String + +password: String + } + note bottom + Example: Login form + end note + AuthUiStateImpl -up-|> AuthUiState + AuthUiStateImpl <|-down- Loading + AuthUiStateImpl <|-down- Form + + class AuthStateData { + +username: String + +password: String + } + note right + A flow data state passed between machine states. + Stores inter-state data + end note + } + + package state { + interface AuthState + note top + Type-alias or an interface of the machine state + end note + AuthState -up-|> CommonMachineState + + abstract class BaseAuthState + note top + Common base class for all machine states + end note + BaseAuthState -up-|> CoroutineState + BaseAuthState -|> AuthContext: delegates implementation + + class LoadingState + LoadingState -up-|> BaseAuthState + note bottom + Example: sample machine state - Loading + end note + + class FormState + FormState -up-|> BaseAuthState + note bottom + Example: sample machine state - Login form + end note + + interface AuthStateFactory { + +startup(input: AuthInput): AuthState + +form(data: AuthStateData): AuthState + +terminated(result: AuthResult): AuthState + } + AuthStateFactory -> AuthState: creates + note left + Abstracts the state from another state creation + end note + + class AuthStateFactoryImpl + note bottom + State factory implementation + end note + AuthStateFactoryImpl -up-|> AuthStateFactory + AuthStateFactory -> BaseAuthState: creates + + interface AuthContext { + +factory: AuthStateFactory + +flowHost: CommonFlowHost + +renderer: AuthUiRenderer + } + note bottom + State-machine context available to every state + Put immutable links to common tools here: + - factory + - flow host + - etc + end note + } + + package ui { + interface AuthUiRenderer { + +renderLoading(): AuthUiState + +renderForm(data: AuthStateData): AuthUiState + +renderError(error: Throwable): AuthUiState + } + note top + Transforms data-state to AuthUiState. + Abstracts machine state from the interface data rendering + end note + AuthUiRenderer -> AuthStateData: renders + AuthUiRenderer -> AuthUiStateImpl: creates + + class AuthUiRendererImpl + AuthUiRendererImpl -up-|> AuthUiRenderer + AuthUiRendererImpl -> AuthUiStateImpl: creates + } + + class AuthDataApiImpl + note bottom + Data API implementation + Injects factory and initializes the flow by calling `init` + end note + AuthDataApiImpl -up-|> AuthDataApi + + class AuthUiApiImpl + note bottom + UI API implementation + Builds the view with Compose components inside the module + end note + AuthUiApiImpl -up-|> AuthUiApi +} +@enduml \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/demo/references/fixture-patterns.md b/skills/commonstatemachine/common-child-flow/demo/references/fixture-patterns.md new file mode 100644 index 0000000..553aebe --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/references/fixture-patterns.md @@ -0,0 +1,74 @@ +# Creating fixtures for a demo app's domain use-cases + +A demo app must run the feature flow without a real backend, so every domain use-case interface the feature +implementation depends on (see Step 3 of the main guide) needs a fake (fixture) implementation, wired into DI +from the demo app's own application module - the same place a real app would instead wire the production +`usecase` module's implementations. + +## Default: succeed quietly + +If the user gave no specific scenario for a use-case, make its fixture succeed on every call, after a short +`delay()` (one to two seconds) so the demo still shows its loading UI realistically. Return or construct a +plausible instance of the domain model the interface returns - don't leave it `null` or throw +`NotImplementedError`. Add a short comment noting this is a fixture default the user can change. + +## Specific scenario: fail then succeed (or fail a fixed number of times) + +When the user describes something like "let it fail the first time with an IO error, so I can check the retry +logic", implement the fixture as a stateful fake: a mutable counter captured in its closure, advanced on every +call, branching on its value. Use the *real* domain exception types (from the `domain.exception` package, e.g. +`IOException`, `AuthenticationException` - never a generic `RuntimeException`) so the feature's own +error-mapping code is exercised the same way it would be against a real backend. Log what's happening, tagged +with the interface's simple name, so the emulated branch is visible while demoing: + +```kotlin +@Single +fun getPasswordRequirements(): GetPasswordRequirements { + var attempt = 1 + val tag = GetPasswordRequirements::class.simpleName!! + return object : GetPasswordRequirements { + override suspend fun invoke(): PasswordRequirements { + delay(2.seconds) + return when (attempt) { + 1 -> { + Napier.d(tag = tag) { "Emulating non-fatal error" } + attempt = 2 + throw IOException("Network error") + } + else -> { + Napier.d(tag = tag) { "Emulating success" } + attempt = 1 + PasswordRequirements(regex = "^.{8,}$".toRegex(), description = "Minimum eight characters") + } + } + } + } +} +``` + +Repeat this pattern for every use-case the user wants to control, cycling the counter back to its first value +after the sequence completes so the demo can be replayed without restarting the app. + +## Always-fail scenario + +For "it should always fail with X", skip the counter and just throw the requested exception every time (still +behind a `delay()`), optionally logging that this is a permanent emulated failure. + +## Multi-step scenarios + +If the user describes a longer sequence across several use-cases (for example "preloading succeeds, but the +authentication call fails twice before succeeding"), give each use-case its own independent counter - don't +share one counter across fixtures unless the user is explicitly describing a single shared piece of state (like +a session). + +## Wiring the fixtures in + +Wire every fixture using whichever approach the project already uses for DI (see Step 5 of the main guide) - +don't introduce a new framework just for the demo. With Koin, bind each fixture with a singleton-scoped +function inside the demo app's own module, the one that `includes` the feature's own implementation module; +that `includes` relationship is what pulls in the feature's states, factories and API implementations, and the +fixtures declared alongside it supply the domain-level dependencies those classes expect but don't themselves +provide. With Hilt/Dagger, provide the same fixtures from an equivalent `@Module`/`@Provides` (or `@Binds`) set +installed alongside the feature's own module. With no DI framework at all, construct the fixtures directly +where the feature's `DataApi`/`UiApi` implementations are constructed, and pass them straight in as +constructor arguments. From 0abc25b6fc97b18492a415cbe756a777ddf06796 Mon Sep 17 00:00:00 2001 From: motorro Date: Fri, 2 Oct 2026 17:08:30 +0200 Subject: [PATCH 23/32] Another flow inside the demo Refs: #14 --- .../common-child-flow/demo/SKILL.md | 49 ++++++++++++++----- .../common-child-flow/demo/SKILL.md | 49 ++++++++++++++----- 2 files changed, 74 insertions(+), 24 deletions(-) diff --git a/skills-src/commonstatemachine/common-child-flow/demo/SKILL.md b/skills-src/commonstatemachine/common-child-flow/demo/SKILL.md index 53343bc..849dff5 100644 --- a/skills-src/commonstatemachine/common-child-flow/demo/SKILL.md +++ b/skills-src/commonstatemachine/common-child-flow/demo/SKILL.md @@ -13,16 +13,16 @@ metadata: author: Motorro last-updated: '2026-10-02' keywords: - - android - - architecture - - cross-platform - - kotlin-multiplatform - - mvi - - state-machine - - feature-flow - - demo-app - - debug - - fixtures + - android + - architecture + - cross-platform + - kotlin-multiplatform + - mvi + - state-machine + - feature-flow + - demo-app + - debug + - fixtures --- ## Description @@ -69,6 +69,9 @@ Before creating the module, work out what you need from what the user gave you a - The domain use-cases and models the feature actually depends on: trace the feature implementation's constructor-injected dependencies back to the `domain` module interfaces they call (its state-factory and states are the place to look - see Step 3). These are exactly what you need to fake in Step 4. +- Nested feature dependencies: does the feature host another Common Child Flow module of its own via a proxy - + the same way the example app module hosts `auth`? If so, find out whether the user wants the real + implementation wired in, or a lightweight Data/UI API mock instead - see Step 3. - Fixture behavior: did the user ask for a specific scenario (for example "let the usecase fail the first time with an IO error, so I can check the retry logic")? If so, implement exactly that behavior using the real domain exception/model types. If the user gave no specifics, default to a fixture that just succeeds @@ -124,6 +127,28 @@ states/state-factories it wires together. Any constructor parameter whose type l demo app must fake, because in a real app it would come from a `usecase` module implementation that the demo doesn't depend on. +While you're looking at those same states, also check for a different kind of dependency: a constructor +parameter whose type is a `DataApi`/`UiApi` pair from a *different* feature's `api` module, together with a +`ProxyMachineState` inside this feature's own states. That means this feature hosts another Common Child Flow +module of its own - exactly how the example app module hosts `auth`. If you find one, it needs its own +decision, separate from the domain-use-case fixtures above: + +- **Use the real implementation** - the simplest option when that other feature module already exists in the + project and doesn't itself need faking. Add it as a dependency and wire its real `DataApi`/`UiApi` + implementation the normal way, the same as any other implementation module. Ask the user which + implementation to use if there's more than one, or it's not obvious from the project. +- **Create a minimal Data/UI API mock** - when the real implementation pulls in things you don't want in a + demo (its own backend calls, its own heavy DI graph), or the user explicitly wants that nested feature faked + too. Implement just enough of its `DataApi`/`UiApi` interfaces to satisfy the hosting proxy: a `DataApi.init()` + that returns an ad-hoc `CommonMachineState` which completes immediately with a fixed result (similar to the + `terminated` ad-hoc state in [the example `AuthStateFactoryImpl`](assets/example/implementation/state/AuthStateFactoryImpl.kt)), + and a `UiApi.Screen` that renders a simple placeholder - a label is enough. This mock is deliberately much + thinner than a real implementation module: it doesn't need its own states, gestures, or UI-states, just + enough to hand the hosting proxy state a result so the flow can continue. + +If the user didn't say which approach they want for a nested dependency like this, ask - it's exactly the kind +of decision that shouldn't be guessed silently. + ## Step 4. Create fixtures for the domain use-cases Follow [this guide](references/fixture-patterns.md) to fake every domain use-case identified in Step 3, including any specific failure scenario the user described in the requirements-gathering step. @@ -178,8 +203,8 @@ passes the right `init` value for the feature's `Input` type: ```kotlin @KoinViewModel class MainViewModel(api: AuthDataApi) : CommonFlowViewModel( - api = api, - init = AuthInput(skippable = true) + api = api, + init = AuthInput(skippable = true) ) ``` diff --git a/skills/commonstatemachine/common-child-flow/demo/SKILL.md b/skills/commonstatemachine/common-child-flow/demo/SKILL.md index 53343bc..849dff5 100644 --- a/skills/commonstatemachine/common-child-flow/demo/SKILL.md +++ b/skills/commonstatemachine/common-child-flow/demo/SKILL.md @@ -13,16 +13,16 @@ metadata: author: Motorro last-updated: '2026-10-02' keywords: - - android - - architecture - - cross-platform - - kotlin-multiplatform - - mvi - - state-machine - - feature-flow - - demo-app - - debug - - fixtures + - android + - architecture + - cross-platform + - kotlin-multiplatform + - mvi + - state-machine + - feature-flow + - demo-app + - debug + - fixtures --- ## Description @@ -69,6 +69,9 @@ Before creating the module, work out what you need from what the user gave you a - The domain use-cases and models the feature actually depends on: trace the feature implementation's constructor-injected dependencies back to the `domain` module interfaces they call (its state-factory and states are the place to look - see Step 3). These are exactly what you need to fake in Step 4. +- Nested feature dependencies: does the feature host another Common Child Flow module of its own via a proxy - + the same way the example app module hosts `auth`? If so, find out whether the user wants the real + implementation wired in, or a lightweight Data/UI API mock instead - see Step 3. - Fixture behavior: did the user ask for a specific scenario (for example "let the usecase fail the first time with an IO error, so I can check the retry logic")? If so, implement exactly that behavior using the real domain exception/model types. If the user gave no specifics, default to a fixture that just succeeds @@ -124,6 +127,28 @@ states/state-factories it wires together. Any constructor parameter whose type l demo app must fake, because in a real app it would come from a `usecase` module implementation that the demo doesn't depend on. +While you're looking at those same states, also check for a different kind of dependency: a constructor +parameter whose type is a `DataApi`/`UiApi` pair from a *different* feature's `api` module, together with a +`ProxyMachineState` inside this feature's own states. That means this feature hosts another Common Child Flow +module of its own - exactly how the example app module hosts `auth`. If you find one, it needs its own +decision, separate from the domain-use-case fixtures above: + +- **Use the real implementation** - the simplest option when that other feature module already exists in the + project and doesn't itself need faking. Add it as a dependency and wire its real `DataApi`/`UiApi` + implementation the normal way, the same as any other implementation module. Ask the user which + implementation to use if there's more than one, or it's not obvious from the project. +- **Create a minimal Data/UI API mock** - when the real implementation pulls in things you don't want in a + demo (its own backend calls, its own heavy DI graph), or the user explicitly wants that nested feature faked + too. Implement just enough of its `DataApi`/`UiApi` interfaces to satisfy the hosting proxy: a `DataApi.init()` + that returns an ad-hoc `CommonMachineState` which completes immediately with a fixed result (similar to the + `terminated` ad-hoc state in [the example `AuthStateFactoryImpl`](assets/example/implementation/state/AuthStateFactoryImpl.kt)), + and a `UiApi.Screen` that renders a simple placeholder - a label is enough. This mock is deliberately much + thinner than a real implementation module: it doesn't need its own states, gestures, or UI-states, just + enough to hand the hosting proxy state a result so the flow can continue. + +If the user didn't say which approach they want for a nested dependency like this, ask - it's exactly the kind +of decision that shouldn't be guessed silently. + ## Step 4. Create fixtures for the domain use-cases Follow [this guide](references/fixture-patterns.md) to fake every domain use-case identified in Step 3, including any specific failure scenario the user described in the requirements-gathering step. @@ -178,8 +203,8 @@ passes the right `init` value for the feature's `Input` type: ```kotlin @KoinViewModel class MainViewModel(api: AuthDataApi) : CommonFlowViewModel( - api = api, - init = AuthInput(skippable = true) + api = api, + init = AuthInput(skippable = true) ) ``` From f4960892f0323d7d2cc8facbad3868a44fa143ff Mon Sep 17 00:00:00 2001 From: motorro Date: Fri, 2 Oct 2026 17:54:47 +0200 Subject: [PATCH 24/32] Another flow inside the demo (a-b variants) Refs: #14 --- .../common-child-flow/demo/SKILL.md | 20 ++++++++++++++++--- .../common-child-flow/demo/SKILL.md | 20 ++++++++++++++++--- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/skills-src/commonstatemachine/common-child-flow/demo/SKILL.md b/skills-src/commonstatemachine/common-child-flow/demo/SKILL.md index 849dff5..216698e 100644 --- a/skills-src/commonstatemachine/common-child-flow/demo/SKILL.md +++ b/skills-src/commonstatemachine/common-child-flow/demo/SKILL.md @@ -72,6 +72,10 @@ Before creating the module, work out what you need from what the user gave you a - Nested feature dependencies: does the feature host another Common Child Flow module of its own via a proxy - the same way the example app module hosts `auth`? If so, find out whether the user wants the real implementation wired in, or a lightweight Data/UI API mock instead - see Step 3. +- Multiple variants: did the user ask for more than one demo so they can compare different combinations side + by side - say, two implementations of the feature itself, or two implementations of a nested feature + dependency, for an A/B comparison? If so, each combination gets its own demo module (see Step 1) rather than + a single module with a runtime switch. - Fixture behavior: did the user ask for a specific scenario (for example "let the usecase fail the first time with an IO error, so I can check the retry logic")? If so, implement exactly that behavior using the real domain exception/model types. If the user gave no specifics, default to a fixture that just succeeds @@ -98,9 +102,16 @@ Identify the feature's `api` module (its `Gesture`, `UiState`, `Input`, `Result` the specific `implementation` module to demo, if there are several. If the feature module doesn't exist yet, stop here and use the boilerplate skill instead. +If the user asked to compare multiple variants side by side (see "Multiple variants" above), treat each +combination as its own demo module rather than switching between them inside one module with a flag or a build +variant - keeping each demo small and disposable is the whole point of this skill. Create one module per +combination, named to say what it demos - `demo-a`/`demo-b` if the user didn't suggest better names, or +something more descriptive like `demo-frienddetails-v2` when it's really one specific implementation being +compared. + Create the new module directory next to the feature's existing `api`/`implementation` modules, following the -project's module-naming convention (`demo` in the example). Use the Gradle setup common to other application -modules in the project if one exists to copy from. +project's module-naming convention (`demo` in the example, or the variant names above when there's more than +one). Use the Gradle setup common to other application modules in the project if one exists to copy from. ## Step 2. Create the demo module's Gradle file This is an internal tool, not something that ships to end users - keep it minimal rather than matching the @@ -136,7 +147,10 @@ decision, separate from the domain-use-case fixtures above: - **Use the real implementation** - the simplest option when that other feature module already exists in the project and doesn't itself need faking. Add it as a dependency and wire its real `DataApi`/`UiApi` implementation the normal way, the same as any other implementation module. Ask the user which - implementation to use if there's more than one, or it's not obvious from the project. + implementation to use if there's more than one, or it's not obvious from the project. If the user wants to + compare two or more of that nested feature's implementations side by side, that's the "multiple variants" + case from the requirements-gathering step above: build one demo module per implementation, not a switch + inside one module. - **Create a minimal Data/UI API mock** - when the real implementation pulls in things you don't want in a demo (its own backend calls, its own heavy DI graph), or the user explicitly wants that nested feature faked too. Implement just enough of its `DataApi`/`UiApi` interfaces to satisfy the hosting proxy: a `DataApi.init()` diff --git a/skills/commonstatemachine/common-child-flow/demo/SKILL.md b/skills/commonstatemachine/common-child-flow/demo/SKILL.md index 849dff5..216698e 100644 --- a/skills/commonstatemachine/common-child-flow/demo/SKILL.md +++ b/skills/commonstatemachine/common-child-flow/demo/SKILL.md @@ -72,6 +72,10 @@ Before creating the module, work out what you need from what the user gave you a - Nested feature dependencies: does the feature host another Common Child Flow module of its own via a proxy - the same way the example app module hosts `auth`? If so, find out whether the user wants the real implementation wired in, or a lightweight Data/UI API mock instead - see Step 3. +- Multiple variants: did the user ask for more than one demo so they can compare different combinations side + by side - say, two implementations of the feature itself, or two implementations of a nested feature + dependency, for an A/B comparison? If so, each combination gets its own demo module (see Step 1) rather than + a single module with a runtime switch. - Fixture behavior: did the user ask for a specific scenario (for example "let the usecase fail the first time with an IO error, so I can check the retry logic")? If so, implement exactly that behavior using the real domain exception/model types. If the user gave no specifics, default to a fixture that just succeeds @@ -98,9 +102,16 @@ Identify the feature's `api` module (its `Gesture`, `UiState`, `Input`, `Result` the specific `implementation` module to demo, if there are several. If the feature module doesn't exist yet, stop here and use the boilerplate skill instead. +If the user asked to compare multiple variants side by side (see "Multiple variants" above), treat each +combination as its own demo module rather than switching between them inside one module with a flag or a build +variant - keeping each demo small and disposable is the whole point of this skill. Create one module per +combination, named to say what it demos - `demo-a`/`demo-b` if the user didn't suggest better names, or +something more descriptive like `demo-frienddetails-v2` when it's really one specific implementation being +compared. + Create the new module directory next to the feature's existing `api`/`implementation` modules, following the -project's module-naming convention (`demo` in the example). Use the Gradle setup common to other application -modules in the project if one exists to copy from. +project's module-naming convention (`demo` in the example, or the variant names above when there's more than +one). Use the Gradle setup common to other application modules in the project if one exists to copy from. ## Step 2. Create the demo module's Gradle file This is an internal tool, not something that ships to end users - keep it minimal rather than matching the @@ -136,7 +147,10 @@ decision, separate from the domain-use-case fixtures above: - **Use the real implementation** - the simplest option when that other feature module already exists in the project and doesn't itself need faking. Add it as a dependency and wire its real `DataApi`/`UiApi` implementation the normal way, the same as any other implementation module. Ask the user which - implementation to use if there's more than one, or it's not obvious from the project. + implementation to use if there's more than one, or it's not obvious from the project. If the user wants to + compare two or more of that nested feature's implementations side by side, that's the "multiple variants" + case from the requirements-gathering step above: build one demo module per implementation, not a switch + inside one module. - **Create a minimal Data/UI API mock** - when the real implementation pulls in things you don't want in a demo (its own backend calls, its own heavy DI graph), or the user explicitly wants that nested feature faked too. Implement just enough of its `DataApi`/`UiApi` interfaces to satisfy the hosting proxy: a `DataApi.init()` From ebe235f18c8a47cac25b422e40235797382df9a7 Mon Sep 17 00:00:00 2001 From: motorro Date: Sat, 3 Oct 2026 14:29:11 +0200 Subject: [PATCH 25/32] Build flavors for A-B variants Refs: #14 --- .github/workflows/check.yml | 2 +- .run/Check All.run.xml | 7 +- AGENTS.md | 6 +- examples/skills/auth/demo/build.gradle.kts | 23 ++++ .../examples/skills/auth/demo/AppModule.kt | 83 +++++++++++++ .../connectionErrors/res/values/strings.xml | 3 + .../examples/skills/auth/demo/AppModule.kt | 60 +++++++++ .../demo/src/happyPath/res/values/strings.xml | 3 + .../examples/skills/auth/demo/App.kt | 69 ----------- skills-src/README.md | 114 ++++++++++++++---- .../common-child-flow/demo/SKILL.md | 95 +++++++++++---- skills/README.md | 114 ++++++++++++++---- .../common-child-flow/demo/SKILL.md | 95 +++++++++++---- .../demo/assets/example/demo/build.gradle.kts | 23 ++++ .../examples/skills/auth/demo/AppModule.kt | 83 +++++++++++++ .../connectionErrors/res/values/strings.xml | 3 + .../examples/skills/auth/demo/AppModule.kt | 60 +++++++++ .../demo/src/happyPath/res/values/strings.xml | 3 + .../examples/skills/auth/demo/App.kt | 69 ----------- 19 files changed, 678 insertions(+), 237 deletions(-) create mode 100644 examples/skills/auth/demo/src/connectionErrors/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/AppModule.kt create mode 100644 examples/skills/auth/demo/src/connectionErrors/res/values/strings.xml create mode 100644 examples/skills/auth/demo/src/happyPath/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/AppModule.kt create mode 100644 examples/skills/auth/demo/src/happyPath/res/values/strings.xml create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/connectionErrors/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/AppModule.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/connectionErrors/res/values/strings.xml create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/happyPath/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/AppModule.kt create mode 100644 skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/happyPath/res/values/strings.xml diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 0d34b1a..c073595 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:app: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 :examples:book:app:assembleDebug :examples:skills:auth:demo:assembleDebug :examples:skills:app:assembleDebug checkLegacyAbi --no-daemon --no-configuration-cache + run: ./gradlew runUnitTests :examples:lce:assembleDebug :examples:welcome:app: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 :examples:book:app:assembleDebug :examples:skills:auth:demo:assembleHappyPathDebug :examples:skills:auth:demo:assembleConnectionErrorsDebug :examples:skills:app:assembleDebug checkLegacyAbi --no-daemon --no-configuration-cache - name: Upload problems report uses: actions/upload-artifact@v4 if: failure() diff --git a/.run/Check All.run.xml b/.run/Check All.run.xml index 349f91f..a9c45b1 100644 --- a/.run/Check All.run.xml +++ b/.run/Check All.run.xml @@ -20,7 +20,8 @@