diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100644 index 0000000..ca137f4 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,20 @@ +#!/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 + 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/.github/workflows/check.yml b/.github/workflows/check.yml index 7c2b62f..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: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: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/.gitignore b/.gitignore index cdbd6ea..ac927bf 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ /.idea .DS_Store /build +/Claude outputs +/artifacts /.kotlin build /captures diff --git a/.run/Check All.run.xml b/.run/Check All.run.xml new file mode 100644 index 0000000..a9c45b1 --- /dev/null +++ b/.run/Check All.run.xml @@ -0,0 +1,44 @@ + + + + + + + false + true + false + + false + false + + false + false + false + false + + + \ No newline at end of file diff --git a/.run/Journeys.run.xml b/.run/Journeys.run.xml new file mode 100644 index 0000000..03a2f26 --- /dev/null +++ b/.run/Journeys.run.xml @@ -0,0 +1,37 @@ + + + + + + + true + true + false + + true + + + true + true + + false + true + false + false + + + + + + \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..05adb52 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,63 @@ +# 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 +There are two folders managing skills: +- **`skills-src`**: Source files used to develop and maintain skills. **Always edit skill files in `skills-src`**, never directly in `skills/`. + - **Shared resources**: Reusable files and templates are stored in `.shared/` subfolders. + - **Asset symlinks**: Code examples in `assets/` subfolders are symlinked from `examples/skills/` so they remain compilable and testable. +- **`skills`**: Contains complete, self-contained generated skills for distribution (e.g. `npx skills`). Do not edit files here directly—they are automatically generated from `skills-src/` via [the build script](skills-src/build_skills.sh) (executed automatically by [.githooks/pre-commit](.githooks/pre-commit) upon commit). + +--- + +## 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) { ... }`). + +## Disk operations +- When moving or renaming files, use `git mv` instead of deleting the old file and creating a new file at the new path, so file history and changes remain tracked. +- When deleting files, use `git rm` so the removal is properly tracked by Git. + diff --git a/README.md b/README.md index f030a5e..1ce3dfd 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,76 @@ # 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) +## TL;DR (for AI agents and quick answers) + +**What this is**: a lightweight Kotlin Multiplatform state-machine library for building MVI-pattern apps +(Android, Compose Multiplatform, and beyond) around an explicit finite-state machine instead of reducer +if/else logic or a navigation graph. Core idea - **Navigation Inversion**: the state machine decides what +screen to show next, not a nav graph; several logical states can map to the same screen, and a transition can +carry live data (even a running coroutine) forward without serializing it into navigation arguments. + +**Reach for it when you need to**: +- Replace sprawling reducer logic for a multi-step flow (onboarding, checkout, auth) with explicit states and + transitions that unit-test in isolation. +- Embed a self-contained feature module (built by another team, or living in its own Gradle module) inside a + host app's flow by running it as a nested state machine within one host state - a `ProxyMachineState` + (see [Adopting foreign state-flow](#adopting-foreign-state-flow)) - or, just as easily, run the very same + module standalone behind a traditional per-screen ViewModel and navigation setup via `CommonFlowViewModel`. + Check the [Common Child Flow](#common-child-flow-api) section below for the full pattern (`ProxyMachineState`, `CommonFlowHost`, + `CommonFlowDataApi`, `CommonFlowUiApi`, `CommonFlowViewModel`, `CommonFlowComposition`). +- Run independent state machines together on one screen (`MultiMachineState`) - in parallel, or one-active-at-a-time. + Check [Running state-machines in parallel](#running-state-machines-in-parallel-composition) section below for the full pattern. +- Scaffold, extend, demo, or write AI-driven UI tests for a Common Child Flow feature module with a coding + agent - see [AI Skills](#ai-skills) below. + +**Key vocabulary** (the exact terms used throughout this repo and its AI skills): + +| Term | What it is | +|-------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `CommonMachineState` | One logical state: processes gesture `G`, emits UI state `U`. | +| `CommonStateMachine` | Holds the current state, dispatches gestures to it, exposes the UI-state stream. | +| `ProxyMachineState` | A state that runs a nested state-machine inside it, translating gestures/UI-states between parent and child systems - the mechanism behind both parallel composition and Common Child Flow. | +| `MultiMachineState` / `ProxyMachineContainer` | Runs several state machines side by side under one parent state. | +| `Common Child Flow` | A pattern to create a feature module with a child state-machine, hosted via a `ProxyMachineState` so host and feature never share concrete types. Great for DI, build flavors, A-B testing, etc. | +| `CommonFlowHost` | Interface the parent (proxy) provides to the child flow, so the child can interract with the host and signal its flow completion. | +| `CommonFlowDataApi` | Interface the child flow exposes to initiate itself and adapt its gestures/UI-states to the hosting flow. | +| `CommonFlowUiApi` | Interface with a single `Screen` method to inject the child flow's UI into the host's composition. | +| `CommonFlowViewModel` / `CommonFlowComposition` | Bridges a Common Child Flow module into traditional per-screen architecture - a ready-made ViewModel + Compose scaffold that works with Activities, Fragments or Compose navigation | + +**Gradle coordinates** (replace `x.x.x` with the latest release): +```groovy +implementation "com.motorro.commonstatemachine:commonstatemachine:x.x.x" // core +implementation "com.motorro.commonstatemachine:coroutines:x.x.x" // coroutine extensions (optional) +implementation "com.motorro.commonstatemachine:lifecycle:x.x.x" // view-lifecycle-aware proxies (optional) +implementation "com.motorro.commonstatemachine:commonflow-data:x.x.x" // Common Child Flow: Child state-flow layer +implementation "com.motorro.commonstatemachine:commonflow-compose:x.x.x" // Common Child Flow: Child Compose UI layer +implementation "com.motorro.commonstatemachine:commonflow-viewmodel:x.x.x" // Common Child Flow: ViewModel bridge +``` + +The full guide below covers the basic Load-Content-Error example, state factories and dependency provision, +multi-module adoption, and parallel composition in depth - this TL;DR is a map to it, not a replacement. + +## 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 @@ -47,7 +113,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 +142,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 @@ -90,9 +156,14 @@ 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 +- 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 +174,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 +186,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 +214,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 +232,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 +257,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 +282,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 +290,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 +352,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 +374,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 +390,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 +410,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 +431,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 +445,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 +465,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 +507,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 +550,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 +584,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 +612,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 +641,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 +656,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 +676,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 +687,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 +718,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 +740,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 +749,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 +772,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 +791,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 +815,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 +899,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 +963,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 +973,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 +1020,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 +1050,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 +1072,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 +1093,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 +1106,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 +1122,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 +1170,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 +1188,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 +1201,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 +1240,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 +1256,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 +1283,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 +1308,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 +1333,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 +1381,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 +1420,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 +1455,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 +1494,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..9c244b9 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/PublishingPlugin.kt @@ -0,0 +1,100 @@ +import com.motorro.gradle.convention.getProjectProperty +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.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 + 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/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 8928362..aa80982 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.KotlinCompile plugins { alias(libs.plugins.android.app) apply false @@ -26,20 +26,14 @@ 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 alias(libs.plugins.nexus.publish) 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" @@ -49,27 +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 { - compilerOptions { - freeCompilerArgs.addAll(listOf( - "-opt-in=kotlin.RequiresOptIn", - "-Xexpect-actual-classes" - )) - } - } - tasks.withType().configureEach { forkEvery = 100 testLogging { @@ -116,9 +89,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." } @@ -129,7 +102,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 +112,12 @@ tasks.register("runBooksExampleUnitTests") { description = "Run unit tests for books app." } +tasks.register("runSkillsExampleUnitTests") { + dependsOn(":examples:skills:app:testDebugUnitTest") + dependsOn(":examples:skills:auth:implementation:allTests") + description = "Run unit tests for skills." +} + tasks.register("displayVersion") { description = "Display application version name" doLast { @@ -156,7 +135,8 @@ tasks.register("runUnitTests") { "runWelcomeExampleUnitTests", "runTimerExampleUnitTests", "runDiExampleUnitTests", - "runBooksExampleUnitTests" + "runBooksExampleUnitTests", + "runSkillsExampleUnitTests" ) group = "verification" description = "Run unit tests for all modules." 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/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 new file mode 100644 index 0000000..f3d63af --- /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 modularised 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/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 new file mode 100644 index 0000000..a0125c2 --- /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 modularised 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..ffc430b 100644 --- a/commonflow/viewmodel/api/commonflow-viewmodel.klib.api +++ b/commonflow/viewmodel/api/commonflow-viewmodel.klib.api @@ -41,15 +41,15 @@ sealed class <#A: out kotlin/Any, #B: out kotlin/Any?> com.motorro.commonstatema } final class <#A1: out kotlin/Any?> Terminated : com.motorro.commonstatemachine.flow.viewmodel.data/BaseFlowUiState { // com.motorro.commonstatemachine.flow.viewmodel.data/BaseFlowUiState.Terminated|null[0] - constructor (#A1?) // com.motorro.commonstatemachine.flow.viewmodel.data/BaseFlowUiState.Terminated.|(1:0?){}[0] + constructor (#A1) // com.motorro.commonstatemachine.flow.viewmodel.data/BaseFlowUiState.Terminated.|(1:0){}[0] final val backHandlerEnabled // com.motorro.commonstatemachine.flow.viewmodel.data/BaseFlowUiState.Terminated.backHandlerEnabled|{}backHandlerEnabled[0] final fun (): kotlin/Boolean // com.motorro.commonstatemachine.flow.viewmodel.data/BaseFlowUiState.Terminated.backHandlerEnabled.|(){}[0] final val result // com.motorro.commonstatemachine.flow.viewmodel.data/BaseFlowUiState.Terminated.result|{}result[0] - final fun (): #A1? // com.motorro.commonstatemachine.flow.viewmodel.data/BaseFlowUiState.Terminated.result.|(){}[0] + final fun (): #A1 // com.motorro.commonstatemachine.flow.viewmodel.data/BaseFlowUiState.Terminated.result.|(){}[0] - final fun component1(): #A1? // com.motorro.commonstatemachine.flow.viewmodel.data/BaseFlowUiState.Terminated.component1|component1(){}[0] - final fun copy(#A1? = ...): com.motorro.commonstatemachine.flow.viewmodel.data/BaseFlowUiState.Terminated<#A1> // com.motorro.commonstatemachine.flow.viewmodel.data/BaseFlowUiState.Terminated.copy|copy(1:0?){}[0] + final fun component1(): #A1 // com.motorro.commonstatemachine.flow.viewmodel.data/BaseFlowUiState.Terminated.component1|component1(){}[0] + final fun copy(#A1 = ...): com.motorro.commonstatemachine.flow.viewmodel.data/BaseFlowUiState.Terminated<#A1> // com.motorro.commonstatemachine.flow.viewmodel.data/BaseFlowUiState.Terminated.copy|copy(1:0){}[0] final fun equals(kotlin/Any?): kotlin/Boolean // com.motorro.commonstatemachine.flow.viewmodel.data/BaseFlowUiState.Terminated.equals|equals(kotlin.Any?){}[0] final fun hashCode(): kotlin/Int // com.motorro.commonstatemachine.flow.viewmodel.data/BaseFlowUiState.Terminated.hashCode|hashCode(){}[0] final fun toString(): kotlin/String // com.motorro.commonstatemachine.flow.viewmodel.data/BaseFlowUiState.Terminated.toString|toString(){}[0] @@ -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] @@ -85,7 +90,12 @@ final val com.motorro.commonstatemachine.flow.viewmodel.data/com_motorro_commons final val com.motorro.commonstatemachine.flow.viewmodel.data/com_motorro_commonstatemachine_flow_viewmodel_data_BaseFlowUiState_Terminated$stableprop // com.motorro.commonstatemachine.flow.viewmodel.data/com_motorro_commonstatemachine_flow_viewmodel_data_BaseFlowUiState_Terminated$stableprop|#static{}com_motorro_commonstatemachine_flow_viewmodel_data_BaseFlowUiState_Terminated$stableprop[0] 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 <#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 08d7dc3..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,96 +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() - 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-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) @@ -114,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/consumer-rules.pro b/commonflow/viewmodel/consumer-rules.pro new file mode 100644 index 0000000..e69de29 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/commonflow/viewmodel/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/viewmodel/CommonFlowComposition.kt b/commonflow/viewmodel/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/viewmodel/CommonFlowComposition.kt index 0a6604f..a9d0bb1 100644 --- a/commonflow/viewmodel/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/viewmodel/CommonFlowComposition.kt +++ b/commonflow/viewmodel/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/viewmodel/CommonFlowComposition.kt @@ -26,7 +26,7 @@ fun < G: Any, U: Any, R> CommonFlowComposition( viewModel: CommonFlowViewModel, navigationBackHandler: @Composable (enabled: Boolean, onBack: () -> Unit) -> Unit = { _, _ -> }, content: @Composable (U, (G) -> Unit) -> Unit, - finish: (R?) -> Unit + finish: (R) -> Unit ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val process = remember<(G) -> Unit> { diff --git a/commonflow/viewmodel/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/viewmodel/data/BaseFlowUiState.kt b/commonflow/viewmodel/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/viewmodel/data/BaseFlowUiState.kt index eda52c1..6095bdb 100644 --- a/commonflow/viewmodel/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/viewmodel/data/BaseFlowUiState.kt +++ b/commonflow/viewmodel/src/commonMain/kotlin/com/motorro/commonstatemachine/flow/viewmodel/data/BaseFlowUiState.kt @@ -14,7 +14,7 @@ sealed class BaseFlowUiState { /** * Flow terminated */ - data class Terminated(val result: R?) : BaseFlowUiState() { + data class Terminated(val result: R) : BaseFlowUiState() { override val backHandlerEnabled: Boolean = false } } 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/consumer-rules.pro b/commonstatemachine/consumer-rules.pro new file mode 100644 index 0000000..e69de29 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/consumer-rules.pro b/coroutines/consumer-rules.pro new file mode 100644 index 0000000..e69de29 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/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/app/src/main/kotlin/com/motorro/statemachine/books/app/App.kt b/examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/App.kt similarity index 82% rename from examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/App.kt rename to examples/books/app/src/main/kotlin/com/motorro/commonstatemachine/examples/books/app/App.kt index b00c8e2..f96bccd 100644 --- a/examples/books/app/src/main/kotlin/com/motorro/statemachine/books/app/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.app +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/di/app/src/main/kotlin/com/motorro/statemachine/di/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/di/app/src/main/kotlin/com/motorro/statemachine/di/app/App.kt rename to examples/books/book/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/books/book/demo/App.kt index ccc579b..8c5347f 100644 --- a/examples/di/app/src/main/kotlin/com/motorro/statemachine/di/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.di.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/consumer-rules.pro b/examples/commoncore/consumer-rules.pro new file mode 100644 index 0000000..e69de29 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/androidcore/src/main/java/com/motorro/statemachine/androidcore/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/androidcore/src/main/java/com/motorro/statemachine/androidcore/ui/theme/Color.kt rename to examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/ui/theme/Color.kt index 6e5361e..75302e7 100644 --- a/examples/androidcore/src/main/java/com/motorro/statemachine/androidcore/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.androidcore.ui.theme +package com.motorro.commonstatemachine.examples.commoncore.ui.theme import androidx.compose.ui.graphics.Color diff --git a/examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/ui/theme/Dimensions.kt b/examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/ui/theme/Dimensions.kt new file mode 100644 index 0000000..88b55c1 --- /dev/null +++ b/examples/commoncore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/commoncore/ui/theme/Dimensions.kt @@ -0,0 +1,34 @@ +package com.motorro.commonstatemachine.examples.commoncore.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/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/books/book/demo/src/main/kotlin/com/motorro/statemachine/books/book/demo/App.kt b/examples/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/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/di/app/src/main/kotlin/com/motorro/commonstatemachine/examples/di/app/App.kt index 5567f41..b894336 100644 --- a/examples/books/book/demo/src/main/kotlin/com/motorro/statemachine/books/book/demo/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.books.book.demo +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 c0b1e92..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,90 +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")) - - 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/java/com/motorro/statemachine/lifecycle/App.kt deleted file mode 100644 index 5624198..0000000 --- a/examples/lifecycle/src/main/java/com/motorro/statemachine/lifecycle/App.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.lifecycle - -import android.app.Application -import timber.log.Timber - -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/App.kt b/examples/lifecycle/src/main/kotlin/com/motorro/commonstatemachine/examples/lifecycle/App.kt similarity index 81% rename from examples/welcome/welcome/src/main/java/com/motorro/statemachine/App.kt rename to examples/lifecycle/src/main/kotlin/com/motorro/commonstatemachine/examples/lifecycle/App.kt index 889e383..74c38ff 100644 --- a/examples/welcome/welcome/src/main/java/com/motorro/statemachine/App.kt +++ b/examples/lifecycle/src/main/kotlin/com/motorro/commonstatemachine/examples/lifecycle/App.kt @@ -11,13 +11,13 @@ * limitations under the License. */ -package com.motorro.statemachine +package com.motorro.commonstatemachine.examples.lifecycle 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() { override fun onCreate() { super.onCreate() @@ -25,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"> + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/App.kt b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/App.kt new file mode 100644 index 0000000..2aea628 --- /dev/null +++ b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/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/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/MainActivity.kt b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/MainActivity.kt new file mode 100644 index 0000000..d8d6ccf --- /dev/null +++ b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/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/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/MainModule.kt b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/MainModule.kt new file mode 100644 index 0000000..9f15aa7 --- /dev/null +++ b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/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/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/MainViewModel.kt b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/MainViewModel.kt new file mode 100644 index 0000000..000ebb7 --- /dev/null +++ b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/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/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/data/MainGesture.kt b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/data/MainGesture.kt new file mode 100644 index 0000000..8126794 --- /dev/null +++ b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/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.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/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/data/MainUiState.kt b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/data/MainUiState.kt new file mode 100644 index 0000000..c25ca24 --- /dev/null +++ b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/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.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/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/state/AuthProxyState.kt b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/state/AuthProxyState.kt new file mode 100644 index 0000000..9f1bcd1 --- /dev/null +++ b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/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.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/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/state/CheckingAuthentication.kt b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/state/CheckingAuthentication.kt new file mode 100644 index 0000000..713e591 --- /dev/null +++ b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/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/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/state/ContentState.kt b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/state/ContentState.kt new file mode 100644 index 0000000..6d22d89 --- /dev/null +++ b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/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/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/state/MainContext.kt b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/state/MainContext.kt new file mode 100644 index 0000000..d92d16a --- /dev/null +++ b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/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.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/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/state/MainState.kt b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/state/MainState.kt new file mode 100644 index 0000000..140d336 --- /dev/null +++ b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/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.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/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/state/MainStateFactory.kt b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/state/MainStateFactory.kt new file mode 100644 index 0000000..5b5d987 --- /dev/null +++ b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/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.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/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/ui/MainScreen.kt b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/ui/MainScreen.kt new file mode 100644 index 0000000..566c749 --- /dev/null +++ b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/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.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/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/ui/MainUiRenderer.kt b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app/ui/MainUiRenderer.kt new file mode 100644 index 0000000..97c4bf4 --- /dev/null +++ b/examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/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/examples/welcome/welcome/src/main/res/drawable-v24/ic_launcher_foreground.xml b/examples/skills/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/skills/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/skills/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/skills/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/skills/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/skills/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/skills/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/skills/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/skills/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/skills/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/skills/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/skills/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/skills/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/skills/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/skills/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/skills/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/skills/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/skills/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/skills/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/skills/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/skills/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/skills/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/skills/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/skills/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/skills/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/skills/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/skills/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/skills/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp 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/commonstatemachine/examples/skills/app/state/BaseStateTest.kt b/examples/skills/app/src/test/kotlin/com/motorro/commonstatemachine/examples/skills/app/state/BaseStateTest.kt new file mode 100644 index 0000000..7f686a3 --- /dev/null +++ b/examples/skills/app/src/test/kotlin/com/motorro/commonstatemachine/examples/skills/app/state/BaseStateTest.kt @@ -0,0 +1,61 @@ +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 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/commonstatemachine/examples/skills/app/state/CheckingAuthenticationTest.kt b/examples/skills/app/src/test/kotlin/com/motorro/commonstatemachine/examples/skills/app/state/CheckingAuthenticationTest.kt new file mode 100644 index 0000000..8aae00c --- /dev/null +++ b/examples/skills/app/src/test/kotlin/com/motorro/commonstatemachine/examples/skills/app/state/CheckingAuthenticationTest.kt @@ -0,0 +1,104 @@ +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 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 new file mode 100644 index 0000000..f509912 --- /dev/null +++ b/examples/skills/appcore/build.gradle.kts @@ -0,0 +1,28 @@ +/* +* 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. +*/ + +plugins { + alias(libs.plugins.motorro.cmp) +} + +kotlin { + sourceSets { + commonMain.dependencies { + api(libs.compose.multiplatform.icons) + implementation(libs.compose.multiplatform.navigationevent) + implementation(libs.compose.multiplatform.preview) + } + } +} + + 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/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/commonstatemachine/examples/auth/appcore/ui/design/SkillsAppBar.kt b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/design/SkillsAppBar.kt new file mode 100644 index 0000000..e2223e4 --- /dev/null +++ b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/design/SkillsAppBar.kt @@ -0,0 +1,99 @@ +package com.motorro.commonstatemachine.examples.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.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 + +/** + * 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/commonstatemachine/examples/auth/appcore/ui/design/SkillsLoading.kt b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/design/SkillsLoading.kt new file mode 100644 index 0000000..fe1010f --- /dev/null +++ b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/design/SkillsLoading.kt @@ -0,0 +1,15 @@ +package com.motorro.commonstatemachine.examples.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/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/preview/SkillsPreviewComposition.kt b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/preview/SkillsPreviewComposition.kt new file mode 100644 index 0000000..9f2a203 --- /dev/null +++ b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/preview/SkillsPreviewComposition.kt @@ -0,0 +1,28 @@ +package com.motorro.commonstatemachine.examples.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.commonstatemachine.examples.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/commonstatemachine/examples/auth/appcore/ui/theme/Color.kt b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/theme/Color.kt new file mode 100644 index 0000000..421d7fc --- /dev/null +++ b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.motorro.commonstatemachine.examples.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/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/commonstatemachine/examples/auth/appcore/ui/theme/SkillsTheme.kt b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/theme/SkillsTheme.kt new file mode 100644 index 0000000..3631d0e --- /dev/null +++ b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/theme/SkillsTheme.kt @@ -0,0 +1,37 @@ +package com.motorro.commonstatemachine.examples.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/commonstatemachine/examples/auth/appcore/ui/theme/Type.kt b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/theme/Type.kt new file mode 100644 index 0000000..2ac1237 --- /dev/null +++ b/examples/skills/appcore/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/auth/appcore/ui/theme/Type.kt @@ -0,0 +1,18 @@ +package com.motorro.commonstatemachine.examples.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/build.gradle.kts b/examples/skills/auth/api/build.gradle.kts new file mode 100644 index 0000000..be1f8cc --- /dev/null +++ b/examples/skills/auth/api/build.gradle.kts @@ -0,0 +1,29 @@ +/* +* 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. +*/ + +plugins { + alias(libs.plugins.motorro.cmp) +} + +kotlin { + sourceSets { + commonMain.dependencies { + implementation(project(":commonstatemachine")) + implementation(project(":coroutines")) + implementation(project(":commonflow:commonflow-data")) + implementation(project(":commonflow:commonflow-compose")) + implementation(libs.kotlin.coroutines.core) + } + } +} + 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/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api/AuthDataApi.kt b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api/AuthDataApi.kt new file mode 100644 index 0000000..e03a962 --- /dev/null +++ b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/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/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api/AuthGesture.kt b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api/AuthGesture.kt new file mode 100644 index 0000000..b93d81e --- /dev/null +++ b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/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/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api/AuthInput.kt b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api/AuthInput.kt new file mode 100644 index 0000000..90aba9d --- /dev/null +++ b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/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/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api/AuthResult.kt b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api/AuthResult.kt new file mode 100644 index 0000000..e57a379 --- /dev/null +++ b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/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/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api/AuthUiApi.kt b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api/AuthUiApi.kt new file mode 100644 index 0000000..1663448 --- /dev/null +++ b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/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/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api/AuthUiState.kt b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/api/AuthUiState.kt new file mode 100644 index 0000000..b16324d --- /dev/null +++ b/examples/skills/auth/api/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/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/examples/welcome/welcome/.gitignore b/examples/skills/auth/demo/.gitignore similarity index 100% rename from examples/welcome/welcome/.gitignore rename to examples/skills/auth/demo/.gitignore diff --git a/examples/skills/auth/demo/build.gradle.kts b/examples/skills/auth/demo/build.gradle.kts new file mode 100644 index 0000000..f500670 --- /dev/null +++ b/examples/skills/auth/demo/build.gradle.kts @@ -0,0 +1,69 @@ +/* + * Copyright 2026 Nikolai Kotchetkov. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + alias(libs.plugins.motorro.android.app) + alias(libs.plugins.koin) +} + +android { + defaultConfig { + minSdk = 34 + } + + flavorDimensions += "demo" + productFlavors { + create("happyPath") { + dimension = "demo" + applicationIdSuffix = ".happypath" + isDefault = true + } + create("connectionErrors") { + dimension = "demo" + applicationIdSuffix = ".connectionerrors" + } + } + + sourceSets { + getByName("happyPath") { + java.directories.add("src/happyPath/kotlin") + res.directories.add("src/happyPath/res") + } + getByName("connectionErrors") { + java.directories.add("src/connectionErrors/kotlin") + res.directories.add("src/connectionErrors/res") + } + } +} + +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/examples/welcome/welcome/proguard-rules.pro b/examples/skills/auth/demo/proguard-rules.pro similarity index 100% rename from examples/welcome/welcome/proguard-rules.pro rename to examples/skills/auth/demo/proguard-rules.pro diff --git a/examples/skills/auth/demo/src/connectionErrors/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/AppModule.kt b/examples/skills/auth/demo/src/connectionErrors/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/AppModule.kt new file mode 100644 index 0000000..c98df26 --- /dev/null +++ b/examples/skills/auth/demo/src/connectionErrors/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/AppModule.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2026 Nikolai Kotchetkov. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.motorro.commonstatemachine.examples.skills.auth.demo + +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.IOException +import com.motorro.commonstatemachine.skills.domain.session.data.Username +import io.github.aakira.napier.Napier +import kotlinx.coroutines.delay +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single +import kotlin.time.Duration.Companion.seconds + +/** + * Demo fixtures for Connection Errors flavor + */ +@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 connection error" } + attempt = 2 + throw IOException("Network error") + } + else -> { + Napier.d(tag = tag) { "Emulating success on retry" } + 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 connection error" } + attempt = 2 + throw IOException("Network error") + } + else -> { + Napier.d(tag = tag) { "Emulating success on retry" } + attempt = 1 + } + } + } + } + } +} diff --git a/examples/skills/auth/demo/src/connectionErrors/res/values/strings.xml b/examples/skills/auth/demo/src/connectionErrors/res/values/strings.xml new file mode 100644 index 0000000..fe206a4 --- /dev/null +++ b/examples/skills/auth/demo/src/connectionErrors/res/values/strings.xml @@ -0,0 +1,3 @@ + + Auth Demo (Connection Errors) + diff --git a/examples/skills/auth/demo/src/happyPath/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/AppModule.kt b/examples/skills/auth/demo/src/happyPath/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/AppModule.kt new file mode 100644 index 0000000..33043c3 --- /dev/null +++ b/examples/skills/auth/demo/src/happyPath/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/AppModule.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2026 Nikolai Kotchetkov. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.motorro.commonstatemachine.examples.skills.auth.demo + +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.session.data.Username +import io.github.aakira.napier.Napier +import kotlinx.coroutines.delay +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single +import kotlin.time.Duration.Companion.seconds + +/** + * Demo fixtures for Happy Path flavor + */ +@Module(includes = [AuthModule::class]) +@ComponentScan +class AppModule { + + @Single + fun getPasswordRequirements(): GetPasswordRequirements { + val tag = GetPasswordRequirements::class.simpleName!! + return object : GetPasswordRequirements { + override suspend fun invoke(): PasswordRequirements { + delay(2.seconds) + Napier.d(tag = tag) { "Emulating success" } + return PasswordRequirements( + regex = "^.{8,}$".toRegex(), + description = "Minimum eight characters" + ) + } + } + } + + @Single + fun authenticateWithPassword(): AuthenticateWithPassword { + val tag = AuthenticateWithPassword::class.simpleName!! + return object : AuthenticateWithPassword { + override suspend fun invoke(username: Username, password: String) { + delay(2.seconds) + Napier.d(tag = tag) { "Emulating success" } + } + } + } +} diff --git a/examples/skills/auth/demo/src/happyPath/res/values/strings.xml b/examples/skills/auth/demo/src/happyPath/res/values/strings.xml new file mode 100644 index 0000000..a822831 --- /dev/null +++ b/examples/skills/auth/demo/src/happyPath/res/values/strings.xml @@ -0,0 +1,3 @@ + + Auth Demo (Happy Path) + 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/commonstatemachine/examples/skills/auth/demo/App.kt b/examples/skills/auth/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/App.kt new file mode 100644 index 0000000..584c1be --- /dev/null +++ b/examples/skills/auth/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/App.kt @@ -0,0 +1,25 @@ +package com.motorro.commonstatemachine.examples.skills.auth.demo + +import android.app.Application +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 = [AppModule::class]) +class App : Application() { + override fun onCreate() { + super.onCreate() + + Napier.base(DebugAntilog()) + + startKoin { + allowOverride(false) + androidLogger() + androidContext(this@App) + } + } +} + diff --git a/examples/skills/auth/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/MainActivity.kt b/examples/skills/auth/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/MainActivity.kt new file mode 100644 index 0000000..86d2f19 --- /dev/null +++ b/examples/skills/auth/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/MainActivity.kt @@ -0,0 +1,115 @@ +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.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +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.Dimensions +import com.motorro.commonstatemachine.examples.auth.appcore.ui.theme.SkillsTheme +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +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 { + + val (result, setResult) = remember { + mutableStateOf(null) + } + + SkillsTheme { + Scaffold( + topBar = { + SkillsAppBar( + title = stringResource(R.string.app_name), + modifier = Modifier.fillMaxWidth(), + topLevel = true + ) + } + ) { paddingValues -> + if (null == result) { + MainScreen( + modifier = Modifier.padding(paddingValues), + onComplete = setResult + ) + } else { + ResultScreen(result) { + finish() + } + } + } + } + } + } +} + +/** + * Feature composition + */ +@Composable +private fun MainScreen(modifier: Modifier = Modifier, onComplete: (AuthResult) -> Unit) { + val viewModel: MainViewModel = koinViewModel() + val uiApi: AuthUiApi = koinInject() + + CommonFlowComposition( + viewModel = viewModel, + navigationBackHandler = { enabled, onBack -> + BackHandler(enabled, onBack) + }, + content = { state, onGesture -> + uiApi.Screen( + state = state, + onGesture = onGesture, + modifier = modifier + ) + }, + finish = { + Napier.i { "Finished with: $it" } + onComplete(it) + } + ) +} + +/** + * Describes the flow result + */ +@Composable +private fun ResultScreen(result: AuthResult, onComplete: () -> Unit) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(Dimensions.medium, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + style = MaterialTheme.typography.bodyLarge, + text = stringResource(R.string.result_authenticated, result.authenticated) + ) + Button(onClick = onComplete) { + Text(text = stringResource(R.string.btn_exit)) + } + } +} + 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/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/commoncore/src/androidMain/AndroidManifest.xml b/examples/skills/auth/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 73% rename from examples/commoncore/src/androidMain/AndroidManifest.xml rename to examples/skills/auth/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index a4b25cb..0147ca8 100644 --- a/examples/commoncore/src/androidMain/AndroidManifest.xml +++ b/examples/skills/auth/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -12,4 +12,7 @@ ~ limitations under the License. --> - \ No newline at end of file + + + + \ No newline at end of file diff --git a/examples/androidcore/src/main/AndroidManifest.xml b/examples/skills/auth/demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml similarity index 73% rename from examples/androidcore/src/main/AndroidManifest.xml rename to examples/skills/auth/demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index cfc666d..0147ca8 100644 --- a/examples/androidcore/src/main/AndroidManifest.xml +++ b/examples/skills/auth/demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -12,6 +12,7 @@ ~ limitations under the License. --> - - - \ No newline at end of file + + + + \ No newline at end of file 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..4d15595 --- /dev/null +++ b/examples/skills/auth/demo/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + Auth Demo + Authenticated: %b + Exit + \ 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 new file mode 100644 index 0000000..ec5590b --- /dev/null +++ b/examples/skills/auth/implementation/build.gradle.kts @@ -0,0 +1,42 @@ +/* +* 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. +*/ + +plugins { + alias(libs.plugins.motorro.cmp) + alias(libs.plugins.mockery) + alias(libs.plugins.koin) +} + +kotlin { + sourceSets { + commonMain.dependencies { + implementation(project(":commonstatemachine")) + implementation(project(":coroutines")) + 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(project.dependencies.platform(libs.koin.bom)) + implementation(libs.koin.core) + implementation(libs.koin.annotations) + implementation(libs.napier) + } + commonTest.dependencies { + implementation(libs.test.kotlin.coroutines) + } + } +} + + 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/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..e0fe510 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/composeResources/values/strings.xml @@ -0,0 +1,9 @@ + + Authentication + Username + Password + Login + Skip + Retry + Close + diff --git a/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/AuthDataApiImpl.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/AuthDataApiImpl.kt new file mode 100644 index 0000000..aa90a5e --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/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/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/AuthModule.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/AuthModule.kt new file mode 100644 index 0000000..b646f31 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/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/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/commonstatemachine/examples/skills/auth/implementation/data/AuthGestureImpl.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/AuthGestureImpl.kt new file mode 100644 index 0000000..ba1df8e --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/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/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/AuthStateData.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/AuthStateData.kt new file mode 100644 index 0000000..d8cf559 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/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/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/AuthUiStateImpl.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/AuthUiStateImpl.kt new file mode 100644 index 0000000..5476939 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/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/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthContext.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthContext.kt new file mode 100644 index 0000000..21bc639 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/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/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/commonstatemachine/examples/skills/auth/implementation/state/AuthStateFactory.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthStateFactory.kt new file mode 100644 index 0000000..5013652 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/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/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthStateFactoryImpl.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthStateFactoryImpl.kt new file mode 100644 index 0000000..35be8ff --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/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/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthenticatingState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthenticatingState.kt new file mode 100644 index 0000000..f337087 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/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/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthenticationErrorState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthenticationErrorState.kt new file mode 100644 index 0000000..772a972 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/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/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/BaseAuthState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/BaseAuthState.kt new file mode 100644 index 0000000..267cf74 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/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/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/FormState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/FormState.kt new file mode 100644 index 0000000..c52effb --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/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/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/PreloadingErrorState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/PreloadingErrorState.kt new file mode 100644 index 0000000..61db5fc --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/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/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/PreloadingState.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/PreloadingState.kt new file mode 100644 index 0000000..63dd138 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/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/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthErrorScreen.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthErrorScreen.kt new file mode 100644 index 0000000..fd8c8a1 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/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/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthFormScreen.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthFormScreen.kt new file mode 100644 index 0000000..0172cfc --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthFormScreen.kt @@ -0,0 +1,142 @@ +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.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +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.focus.FocusDirection +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.input.ImeAction +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, +) { + val focusManager = LocalFocusManager.current + + 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, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + keyboardActions = KeyboardActions( + onNext = { focusManager.moveFocus(FocusDirection.Down) } + ) + ) + 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, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions( + onDone = { + focusManager.clearFocus() + if (state.loginEnabled) { + onGesture(AuthGestureImpl.Action) + } + } + ) + ) + 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/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthLoadingScreen.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthLoadingScreen.kt new file mode 100644 index 0000000..90875c9 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/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/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthScreen.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthScreen.kt new file mode 100644 index 0000000..f2fbfd9 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/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/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthUiRenderer.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthUiRenderer.kt new file mode 100644 index 0000000..f117aba --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/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/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthUiRendererImpl.kt b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthUiRendererImpl.kt new file mode 100644 index 0000000..8f4e0b7 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonMain/kotlin/com/motorro/commonstatemachine/examples/skills/auth/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/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/AuthStateDataTest.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/AuthStateDataTest.kt new file mode 100644 index 0000000..a27db2f --- /dev/null +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/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/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/authGesturesFixtures.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/authGesturesFixtures.kt new file mode 100644 index 0000000..45c6ec5 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/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/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/dataStatesFixtures.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/data/dataStatesFixtures.kt new file mode 100644 index 0000000..7b04bbf --- /dev/null +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/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/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/commonstatemachine/examples/skills/auth/implementation/state/AuthenticatingStateTest.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthenticatingStateTest.kt new file mode 100644 index 0000000..5d5f135 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/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/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthenticationErrorStateTest.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/AuthenticationErrorStateTest.kt new file mode 100644 index 0000000..163d205 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/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/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/BaseStateTest.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/BaseStateTest.kt new file mode 100644 index 0000000..1d4ca54 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/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/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/FormStateTest.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/FormStateTest.kt new file mode 100644 index 0000000..80c6e7a --- /dev/null +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/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/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/PreloadingErrorStateTest.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/PreloadingErrorStateTest.kt new file mode 100644 index 0000000..cc51dc9 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/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/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/PreloadingStateTest.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/state/PreloadingStateTest.kt new file mode 100644 index 0000000..3017c63 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/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/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthUiRendererImplTest.kt b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ui/AuthUiRendererImplTest.kt new file mode 100644 index 0000000..7e59579 --- /dev/null +++ b/examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/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/examples/skills/auth/journey/.gitignore b/examples/skills/auth/journey/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/examples/skills/auth/journey/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/examples/skills/auth/journey/build.gradle.kts b/examples/skills/auth/journey/build.gradle.kts new file mode 100644 index 0000000..2d0f253 --- /dev/null +++ b/examples/skills/auth/journey/build.gradle.kts @@ -0,0 +1,67 @@ +@file:Suppress("UnstableApiUsage") + +/* +* 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 + } + + testOptions { + suites { + create("journeys") { + targets { + create("default") { + + } + } + useJunitEngine { + inputs += listOf(com.android.build.api.dsl.AgpTestSuiteInputParameters.TESTED_APKS) + includeEngines += listOf("journeys-test-engine") + enginesDependencies(libs.junit.platform.launcher) + enginesDependencies(libs.junit.platform.engine) + enginesDependencies(libs.journeys.junit.engine) + } + targetVariants += listOf("debug") + } + } + } +} + +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/examples/skills/auth/journey/proguard-rules.pro b/examples/skills/auth/journey/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/examples/skills/auth/journey/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/journey/src/journeys/auth.journey.xml b/examples/skills/auth/journey/src/journeys/auth.journey.xml new file mode 100644 index 0000000..b9083f0 --- /dev/null +++ b/examples/skills/auth/journey/src/journeys/auth.journey.xml @@ -0,0 +1,10 @@ + + + Test authentication happy path with valid credentials + + Wait for the authentication form to open. + Type "user" into the username field and "password" into the password field. + Tap the Login button to submit the form. + Verify that authentication succeeds and the screen displays "authenticated: true". + + diff --git a/examples/skills/auth/journey/src/journeys/authentication_failure.journey.xml b/examples/skills/auth/journey/src/journeys/authentication_failure.journey.xml new file mode 100644 index 0000000..9c3c511 --- /dev/null +++ b/examples/skills/auth/journey/src/journeys/authentication_failure.journey.xml @@ -0,0 +1,12 @@ + + + Test that submitting a wrong password for a known username shows an authentication error, and that closing it returns to the form + + Wait for the authentication form to open. + Type "user" into the username field and "wrongpass" into the password field. Submit the form with the Login button. + Verify that an error screen appears showing the message "Invalid username or password". + Verify that the screen shows a Close button, not a Retry button. + Tap the Close button. + Verify that the authentication form opens. + + diff --git a/examples/skills/auth/journey/src/journeys/invalid_password.journey.xml b/examples/skills/auth/journey/src/journeys/invalid_password.journey.xml new file mode 100644 index 0000000..772be8e --- /dev/null +++ b/examples/skills/auth/journey/src/journeys/invalid_password.journey.xml @@ -0,0 +1,11 @@ + + + Test that login button remains disabled when password does not meet requirements + + Wait for the authentication form to open. + Type "user" into the username field. + Read and verify the password requirements displayed on the screen. + Type some invalid password into the password field. + Verify that the Login submit button is disabled. + + diff --git a/examples/skills/auth/journey/src/main/AndroidManifest.xml b/examples/skills/auth/journey/src/main/AndroidManifest.xml new file mode 100644 index 0000000..e347350 --- /dev/null +++ b/examples/skills/auth/journey/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/skills/auth/journey/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/journey/App.kt b/examples/skills/auth/journey/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/journey/App.kt new file mode 100644 index 0000000..de82446 --- /dev/null +++ b/examples/skills/auth/journey/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/journey/App.kt @@ -0,0 +1,25 @@ +package com.motorro.commonstatemachine.examples.skills.auth.journey + +import android.app.Application +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 = [AppModule::class]) +class App : Application() { + override fun onCreate() { + super.onCreate() + + Napier.base(DebugAntilog()) + + startKoin { + allowOverride(false) + androidLogger() + androidContext(this@App) + } + } +} + diff --git a/examples/skills/auth/journey/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/journey/AppModule.kt b/examples/skills/auth/journey/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/journey/AppModule.kt new file mode 100644 index 0000000..8833678 --- /dev/null +++ b/examples/skills/auth/journey/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/journey/AppModule.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2026 Nikolai Kotchetkov. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.motorro.commonstatemachine.examples.skills.auth.journey + +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.session.data.Username +import io.github.aakira.napier.Napier +import kotlinx.coroutines.delay +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single +import kotlin.time.Duration.Companion.seconds + +const val USERNAME = "user" +const val PASSWORD = "password" + +/** + * Demo fixtures for Happy Path flavor + */ +@Module(includes = [AuthModule::class]) +@ComponentScan +class AppModule { + + @Single + fun getPasswordRequirements(): GetPasswordRequirements { + val tag = GetPasswordRequirements::class.simpleName!! + return object : GetPasswordRequirements { + override suspend fun invoke(): PasswordRequirements { + delay(2.seconds) + Napier.d(tag = tag) { "Emulating success" } + return PasswordRequirements( + regex = "^.{8,}$".toRegex(), + description = "Minimum eight characters" + ) + } + } + } + + @Single + fun authenticateWithPassword(): AuthenticateWithPassword { + val tag = AuthenticateWithPassword::class.simpleName!! + return object : AuthenticateWithPassword { + override suspend fun invoke(username: Username, password: String) { + delay(2.seconds) + if (USERNAME != username.value || PASSWORD != password) { + Napier.d(tag = tag) { "Emulating failure" } + throw AuthenticationException("Invalid username or password") + } + Napier.d(tag = tag) { "Emulating success" } + } + } + } +} diff --git a/examples/skills/auth/journey/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/journey/MainActivity.kt b/examples/skills/auth/journey/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/journey/MainActivity.kt new file mode 100644 index 0000000..fbdc9c0 --- /dev/null +++ b/examples/skills/auth/journey/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/journey/MainActivity.kt @@ -0,0 +1,114 @@ +package com.motorro.commonstatemachine.examples.skills.auth.journey + +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.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +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.Dimensions +import com.motorro.commonstatemachine.examples.auth.appcore.ui.theme.SkillsTheme +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +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 { + + val (result, setResult) = remember { + mutableStateOf(null) + } + + SkillsTheme { + Scaffold( + topBar = { + SkillsAppBar( + title = stringResource(R.string.app_name), + modifier = Modifier.fillMaxWidth(), + topLevel = true + ) + } + ) { paddingValues -> + if (null == result) { + MainScreen( + modifier = Modifier.padding(paddingValues), + onComplete = setResult + ) + } else { + ResultScreen(result) { + finish() + } + } + } + } + } + } +} + +/** + * Feature composition + */ +@Composable +private fun MainScreen(modifier: Modifier = Modifier, onComplete: (AuthResult) -> Unit) { + val viewModel: MainViewModel = koinViewModel() + val uiApi: AuthUiApi = koinInject() + + CommonFlowComposition( + viewModel = viewModel, + navigationBackHandler = { enabled, onBack -> + BackHandler(enabled, onBack) + }, + content = { state, onGesture -> + uiApi.Screen( + state = state, + onGesture = onGesture, + modifier = modifier + ) + }, + finish = { + Napier.i { "Finished with: $it" } + onComplete(it) + } + ) +} + +/** + * Describes the flow result + */ +@Composable +private fun ResultScreen(result: AuthResult, onComplete: () -> Unit) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(Dimensions.medium, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + style = MaterialTheme.typography.bodyLarge, + text = stringResource(R.string.result_authenticated, result.authenticated) + ) + Button(onClick = onComplete) { + Text(text = stringResource(R.string.btn_exit)) + } + } +} diff --git a/examples/skills/auth/journey/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/journey/MainViewModel.kt b/examples/skills/auth/journey/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/journey/MainViewModel.kt new file mode 100644 index 0000000..d3df77a --- /dev/null +++ b/examples/skills/auth/journey/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/journey/MainViewModel.kt @@ -0,0 +1,15 @@ +package com.motorro.commonstatemachine.examples.skills.auth.journey + +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/journey/src/main/res/drawable-v24/ic_launcher_foreground.xml b/examples/skills/auth/journey/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..40ce517 --- /dev/null +++ b/examples/skills/auth/journey/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/journey/src/main/res/drawable/ic_launcher_background.xml b/examples/skills/auth/journey/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..997ef0b --- /dev/null +++ b/examples/skills/auth/journey/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/welcome/commonregister/src/androidMain/res/values/strings.xml b/examples/skills/auth/journey/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 73% rename from examples/welcome/commonregister/src/androidMain/res/values/strings.xml rename to examples/skills/auth/journey/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 289a66a..0147ca8 100644 --- a/examples/welcome/commonregister/src/androidMain/res/values/strings.xml +++ b/examples/skills/auth/journey/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -12,7 +12,7 @@ ~ limitations under the License. --> - - Password should be at least 6 chars long - Passwords mismatch - \ No newline at end of file + + + + \ No newline at end of file diff --git a/examples/welcome/login/src/main/AndroidManifest.xml b/examples/skills/auth/journey/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml similarity index 73% rename from examples/welcome/login/src/main/AndroidManifest.xml rename to examples/skills/auth/journey/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index cfc666d..0147ca8 100644 --- a/examples/welcome/login/src/main/AndroidManifest.xml +++ b/examples/skills/auth/journey/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -12,6 +12,7 @@ ~ limitations under the License. --> - - - \ No newline at end of file + + + + \ No newline at end of file diff --git a/examples/skills/auth/journey/src/main/res/values/strings.xml b/examples/skills/auth/journey/src/main/res/values/strings.xml new file mode 100644 index 0000000..4d15595 --- /dev/null +++ b/examples/skills/auth/journey/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + Auth Demo + Authenticated: %b + Exit + \ No newline at end of file diff --git a/examples/skills/auth/journey/src/main/res/xml/backup_rules.xml b/examples/skills/auth/journey/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000..fa0f996 --- /dev/null +++ b/examples/skills/auth/journey/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/examples/skills/auth/journey/src/main/res/xml/data_extraction_rules.xml b/examples/skills/auth/journey/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..9ee9997 --- /dev/null +++ b/examples/skills/auth/journey/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/examples/skills/auth/journey/src/main/res/xml/network_security_config.xml b/examples/skills/auth/journey/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..c64cff3 --- /dev/null +++ b/examples/skills/auth/journey/src/main/res/xml/network_security_config.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + 10.0.2.2 + + diff --git a/examples/skills/domain/build.gradle.kts b/examples/skills/domain/build.gradle.kts new file mode 100644 index 0000000..d1b4748 --- /dev/null +++ b/examples/skills/domain/build.gradle.kts @@ -0,0 +1,25 @@ +/* +* 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. +*/ + +plugins { + alias(libs.plugins.motorro.kmp) +} + +kotlin { + sourceSets { + commonMain.dependencies { + api(libs.kotlin.coroutines.core) + } + } +} + 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/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/authenticate/AuthenticateWithPassword.kt b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/authenticate/AuthenticateWithPassword.kt new file mode 100644 index 0000000..1a58d49 --- /dev/null +++ b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/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/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/authenticate/GetPasswordRequirements.kt b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/authenticate/GetPasswordRequirements.kt new file mode 100644 index 0000000..cfc66ff --- /dev/null +++ b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/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/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/authenticate/data/PasswordRequirements.kt b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/domain/authenticate/data/PasswordRequirements.kt new file mode 100644 index 0000000..01bb73a --- /dev/null +++ b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/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/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..4812838 --- /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 (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/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/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/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..1a19efd --- /dev/null +++ b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/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/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..bcd923c --- /dev/null +++ b/examples/skills/domain/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/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/examples/skills/usecase/build.gradle.kts b/examples/skills/usecase/build.gradle.kts new file mode 100644 index 0000000..5eaed0a --- /dev/null +++ b/examples/skills/usecase/build.gradle.kts @@ -0,0 +1,31 @@ +@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. +*/ + +plugins { + alias(libs.plugins.motorro.cmp) + alias(libs.plugins.koin) +} +kotlin { + 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/consumer-rules.pro b/examples/skills/usecase/consumer-rules.pro new file mode 100644 index 0000000..e69de29 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..2ed4ece --- /dev/null +++ b/examples/skills/usecase/src/commonMain/kotlin/com/motorro/commonstatemachine/skills/usecase/Fixtures.kt @@ -0,0 +1,32 @@ +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 + +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" + } + + 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/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/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 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/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/consumer-rules.pro b/examples/timer/consumer-rules.pro new file mode 100644 index 0000000..e69de29 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/welcome/app/.gitignore b/examples/welcome/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/examples/welcome/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file 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/welcome/app/proguard-rules.pro b/examples/welcome/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/examples/welcome/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/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/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/examples/welcome/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..40ce517 --- /dev/null +++ b/examples/welcome/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/welcome/app/src/main/res/drawable/ic_launcher_background.xml b/examples/welcome/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..997ef0b --- /dev/null +++ b/examples/welcome/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/welcome/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/examples/welcome/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..0147ca8 --- /dev/null +++ b/examples/welcome/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/examples/welcome/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/examples/welcome/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..0147ca8 --- /dev/null +++ b/examples/welcome/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/welcome/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/examples/welcome/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/examples/welcome/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/examples/welcome/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/examples/welcome/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/examples/welcome/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/examples/welcome/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/examples/welcome/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/examples/welcome/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/examples/welcome/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/examples/welcome/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/examples/welcome/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/examples/welcome/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/examples/welcome/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/examples/welcome/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/examples/welcome/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/examples/welcome/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/examples/welcome/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/examples/welcome/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/examples/welcome/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/examples/welcome/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/examples/welcome/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/examples/welcome/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/examples/welcome/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/examples/welcome/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/examples/welcome/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/examples/welcome/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/examples/welcome/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/examples/welcome/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/examples/welcome/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ 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/consumer-rules.pro b/examples/welcome/commonapi/consumer-rules.pro new file mode 100644 index 0000000..e69de29 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/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/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/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/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.properties b/gradle.properties index 7e59424..6c9672b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,7 @@ # limitations under the License. # #Gradle -org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M" +org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx4096M" org.gradle.caching=true org.gradle.configuration-cache=true org.gradle.configureondemand=false @@ -23,3 +23,5 @@ kotlin.mpp.enableCInteropCommonization=true #Android android.useAndroidX=true android.nonTransitiveRClass=true +# Opt in to AGP test suite support. +android.experimental.testSuiteSupport=true diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties index baa28d1..de6cdfe 100644 --- a/gradle/gradle-daemon-jvm.properties +++ b/gradle/gradle-daemon-jvm.properties @@ -1,13 +1,12 @@ #This file is generated by updateDaemonJvm -toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect -toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect -toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect -toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect -toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/7083b89563e7ce20943037b8cd2b8cc2/redirect -toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/060bbb778a1f55ea705fdebd2ccfeab9/redirect -toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect -toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect -toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d09679dc60fe5aa05ef7d03efdefac20/redirect -toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/ed4e3bf2f5e7c5d9aabc4cbd8acd555e/redirect -toolchainVendor=JETBRAINS +toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/542351b649eb2bd122f0bdd21a8e74af/redirect +toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/65e59d9b9d79dc0a8e44790703568088/redirect +toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/542351b649eb2bd122f0bdd21a8e74af/redirect +toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/65e59d9b9d79dc0a8e44790703568088/redirect +toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/23cf3c8c56e7e6e5fd44824184a52b46/redirect +toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/de7b909e926af950f67dc8cb2cd33f0e/redirect +toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/542351b649eb2bd122f0bdd21a8e74af/redirect +toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/65e59d9b9d79dc0a8e44790703568088/redirect +toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/916f6e47bbe16857b6188d9f385a936a/redirect +toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/f9aa63bae008193aeb384bd02c8b44f8/redirect toolchainVersion=21 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c2117f6..9380c5b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,28 +1,40 @@ [versions] +androidMinSdkVersion = "24" +androidTargetSdkVersion = "37" +androidCompileSdkVersion = "37" +androidMinTestSdk = "28" androidGradlePlugin = "9.2.1" -androidxAppcompat = "1.7.1" -androidxCore = "1.19.0" +androidxAppcompat = "1.8.0" +androidxCore = "1.19.1" androidxActivity = "1.13.0" -androidxFragment = "1.8.9" +androidxFragment = "1.9.1" androidxHilt = "1.4.0" -compose = "2026.06.01" -composeMultiplatform = "1.11.1" +compose = "2026.09.00" +composeMultiplatform = "1.12.1" +composeMultiplatformMaterial = "1.9.0" +composeMultiplatformMaterialIcons = "1.7.3" +composeHotReload = "1.2.0" viewmodelMultiplatform = "2.11.0" -lificyclelMultiplatform = "2.11.0" +lifecycleMultiplatform = "2.11.0" +navigationMultiplatform = "1.1.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" +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" +napier = "2.7.1" testAndroidxArch = "2.2.0" testAndroidxCore = "1.7.0" testAndroidxEspresso = "3.7.0" @@ -30,82 +42,131 @@ 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" +junitPlatformLauncherVersion = "6.1.3" +junitPlatformEngineVersion = "6.1.3" +journeysJunitEngineVersion = "0.3.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 = "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] -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-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" } +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 = "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" } +junit-platform-launcher = { group = "org.junit.platform", name = "junit-platform-launcher", version.ref = "junitPlatformLauncherVersion" } +junit-platform-engine = { group = "org.junit.platform", name = "junit-platform-engine", version.ref = "junitPlatformEngineVersion" } +journeys-junit-engine = { group = "com.android.tools.journeys", name = "journeys-junit-engine", version.ref = "journeysJunitEngineVersion" } [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/consumer-rules.pro b/lifecycle/consumer-rules.pro new file mode 100644 index 0000000..e69de29 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/consumer-rules.pro b/platformtest/consumer-rules.pro new file mode 100644 index 0000000..e69de29 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 734d5d4..a138fc4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,15 +1,17 @@ +@file:Suppress("UnstableApiUsage") + /* - * 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. - */ +* 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. +*/ pluginManagement { repositories { @@ -37,6 +39,8 @@ dependencyResolutionManagement { } rootProject.name = "CommonStateMachine" +includeBuild("build-logic") + // Libraries include(":tmap") include(":commonstatemachine") @@ -53,10 +57,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", @@ -72,10 +74,20 @@ include( ":examples:books:domain", ":examples:books:book", ":examples:books:book:demo", - ":examples:books:app" + ":examples:books:app", + ":examples:skills:app", + ":examples:skills:domain", + ":examples:skills:usecase", + ":examples:skills:appcore", + ":examples:skills:auth:api", + ":examples:skills:auth:implementation", + ":examples:skills:auth:demo", + ":examples:skills:auth:journey" ) + + diff --git a/skills-src/README.md b/skills-src/README.md new file mode 100644 index 0000000..9c4b170 --- /dev/null +++ b/skills-src/README.md @@ -0,0 +1,148 @@ +# AI skills +A set of AI skills for working with [CommonStateMachine](..)'s Common Child Flow architecture: scaffolding a new +feature module, extending one that already exists, building a standalone demo app to show a feature off or test +it on its own, and setting up AI-driven UI tests for one. Point your AI assistant at the one you need and +describe what you want in plain language - each skill asks for anything it's missing. + +## Assumed project architecture +These skills assume a particular shape for the surrounding project. None of them requires exactly this layout - +each skill checks the actual project before guessing anything project-specific - but this is the architecture the +examples below, and the skills' own vocabulary ("domain", "use-case", "DI framework"), refer to: + +- A **domain module** (`domain` in the example) that declares the use-case interfaces, their input/output models, + and the domain exception types - pure contracts, no implementation. Feature modules call these interfaces + without depending on wherever their real implementations happen to live, which is what lets a demo app (see + the demo skill below) fake them with fixtures instead. The interfaces don't have to live in one shared + `domain` module specifically - a feature can just as well use interfaces declared in another feature's own + `api` module - these skills only care that the interfaces exist and can be faked, not where they're declared. +- A **common UI module** (`appcore` in the example) with the shared design system - theme, typography, and common + composables such as an app bar or a loading indicator - that every feature's views, and any demo app, reuse + instead of rebuilding their own. +- One or more **Common Child Flow feature modules**, each split into an `api` module (its + `Gesture`/`UiState`/`Input`/`Result`/`DataApi`/`UiApi` contracts) and one or more `implementation` modules (its + states, state-factory, DI module and views) - see the + [Common Child Flow architecture](../README.md#common-child-flow-api) for how these fit together. +- An **app module** - the composition root that starts the application, wires up the DI container, and hosts the + feature flows via proxies. +- Some **DI framework** (Koin in the example, but Hilt, Dagger, or none at all are equally fine - every skill + checks what the project actually uses rather than assuming Koin) that ties the use-case implementations, the + feature implementations, and the app together. + +A typical layout looks like this: + +``` +my-project +├── domain # Use-case interfaces, domain models, domain exceptions - no implementation +├── appcore # Shared UI: theme, design system, common composables +├── auth # One Common Child Flow feature module... +│ ├── api +│ └── implementation +├── friendlist # ...and another +│ ├── api +│ └── implementation +└── app # Composition root: Application, MainActivity, DI wiring +``` + +For the complete, runnable version of this layout - the one every skill's examples below refer to - see +[`examples/skills`](../examples/skills). + +## 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. + +## 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. + +## 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 + - Can build several variants of the same demo side by side - different fixture scenarios, different nested + implementations - as Gradle product flavors of one module, so each installs as its own app on the device + +### Simple demo: one feature, one scenario +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. + +### Multiple demos side by side: A-B testing and scenario comparisons +Say `friendlist` hosts a `frienddetails` child flow of its own (proxied the same way the example app hosts +`auth`), and you want to compare two things at once rather than build and rebuild a single demo by hand: + +> Build two variants of the `friendlist` demo side by side so I can compare the `frienddetails` flow's two +> implementations: `friendDetailsV1` and `friendDetailsV2`. +> - Use Gradle product flavors of the same demo module instead of separate modules - name them after the +> implementations they demo. +> - Keep `friendlist`'s own fixtures and the shared UI identical across both variants; only the nested +> `frienddetails` dependency differs per flavor. +> - Give each flavor its own `applicationIdSuffix` and app name so both install side by side on the same +> device at once. + +The same approach works for comparing fixture behavior instead of implementations - for example a `happyPath` +variant where every use-case succeeds, next to a `connectionErrors` variant where the same use-cases fail once +with an I/O error before succeeding on retry, so you can demo both the normal flow and its error handling +without switching builds. + +## Set up AI-driven UI tests for a Common Child Flow module +[Journeys skill](commonstatemachine/common-child-flow/journeys/SKILL.md): sets up an +[Android Journeys](https://developer.android.com/studio/preview/journeys) test suite for an existing feature +module - a small host app, fed by fixtures, plus natural-language `.journey.xml` files that Gemini executes and +verifies against it. Independent of the demo-app skill, though it follows the same overall shape. + - Guesses the feature's real screens and possible actions from its gestures, UI states and composables, rather + than inventing them + - Proposes journey scenarios (happy path, validation cases, failure cases) for you to confirm before writing + any files + - Fakes the feature's domain use-cases with fixtures that match what a scenario actually needs - including + fixtures that branch on a specific typed value, not just a call counter + - Wires up the AGP test-suite Gradle block and a minimal host app, reusing the project's shared UI module and + DI framework + +For example, continuing the `friendlist` module from the examples above: + +> Set up journey tests for the `friendlist` module. +> - Cover the happy path (friends load and display) and the retry path after an I/O error on the first load. +> - Suggest any other scenarios worth covering once you've looked at the module's states and gestures. \ 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..f532e0a --- /dev/null +++ b/skills-src/build_skills.sh @@ -0,0 +1,28 @@ +#!/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..." + +echo "Cleaning up..." +rm -rf "$DEST_DIR" +mkdir -p "$DEST_DIR" + +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 new file mode 100644 index 0000000..94621a6 --- /dev/null +++ b/skills-src/commonstatemachine/.shared/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-src/commonstatemachine/.shared/architecture.puml b/skills-src/commonstatemachine/.shared/architecture.puml new file mode 100644 index 0000000..4d4db8c --- /dev/null +++ b/skills-src/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-src/commonstatemachine/common-child-flow/.shared/COMMON-CHILD-FLOW.md b/skills-src/commonstatemachine/common-child-flow/.shared/COMMON-CHILD-FLOW.md new file mode 100644 index 0000000..655ed24 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/.shared/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-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/common-child-flow.puml b/skills-src/commonstatemachine/common-child-flow/.shared/common-child-flow.puml new file mode 100644 index 0000000..284a449 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/.shared/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-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/fixture-patterns.md b/skills-src/commonstatemachine/common-child-flow/.shared/fixture-patterns.md new file mode 100644 index 0000000..003d262 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/.shared/fixture-patterns.md @@ -0,0 +1,106 @@ +# Creating fixtures for a feature's domain use-cases + +A demo app or a journey host app must run the feature flow without a real backend, so every domain use-case +interface the feature implementation depends on (see the main guide's domain-use-case tracing step) needs a fake +(fixture) implementation, wired into DI from the host 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. + +## Matching a specific input + +Some scenarios - a journey that types a specific value and expects a specific outcome, or a demo the user wants +to drive by hand with known credentials - need the fixture to branch on what it's actually called with, not just +on a call counter. Compare the argument against a known, fixed value and only succeed for it; throw the real +domain exception for anything else, so the branch genuinely reflects what was entered rather than taking the +same path regardless of input: + +```kotlin +const val VALID_USERNAME = "user" +const val VALID_PASSWORD = "password" + +@Single +fun authenticateWithPassword(): AuthenticateWithPassword { + val tag = AuthenticateWithPassword::class.simpleName!! + return object : AuthenticateWithPassword { + override suspend fun invoke(username: Username, password: String) { + delay(2.seconds) + if (VALID_USERNAME == username.value && VALID_PASSWORD == password) { + Napier.d(tag = tag) { "Emulating success" } + } else { + Napier.d(tag = tag) { "Emulating authentication failure" } + throw AuthenticationException("Invalid credentials") + } + } + } +} +``` + +Keep the expected value as a named constant so anything that needs to refer to it - a journey's typed input, a +person running the app by hand - can use the same value by name instead of guessing it out of the code. + +## 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 the main guide's DI-wiring +step) - don't introduce a new framework just for the host app. With Koin, bind each fixture with a +singleton-scoped function inside the host 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-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..c79eb41 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/.shared/renderer-factory-context-interface-implementation.md @@ -0,0 +1,87 @@ +# 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 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. +- 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..da30c21 --- /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 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: + +- 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 new file mode 100644 index 0000000..ebfaa5b --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/SKILL.md @@ -0,0 +1,162 @@ +--- +name: commonstatemachine-commonchildflow-boilerplate +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-10-02' + 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](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). + +## 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. + +## 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? +- 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. + +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-src/commonstatemachine/common-child-flow/boilerplate/assets/example/api b/skills-src/commonstatemachine/common-child-flow/boilerplate/assets/example/api new file mode 120000 index 0000000..681fb02 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/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/boilerplate/assets/example/app b/skills-src/commonstatemachine/common-child-flow/boilerplate/assets/example/app new file mode 120000 index 0000000..700d179 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/assets/example/app @@ -0,0 +1 @@ +../../../../../../examples/skills/app/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/app \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation b/skills-src/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation new file mode 120000 index 0000000..fe05eb8 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/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/boilerplate/assets/example/implementation-tests b/skills-src/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests new file mode 120000 index 0000000..ec06e70 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation-tests @@ -0,0 +1 @@ +../../../../../../examples/skills/auth/implementation/src/commonTest/kotlin/com/motorro/commonstatemachine/examples/skills/auth/implementation/ \ No newline at end of file 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 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 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/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 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/di-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/di-implementation.md new file mode 100644 index 0000000..6914aec --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/di-implementation.md @@ -0,0 +1,47 @@ +# 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). +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). +- 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 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 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/proxy-implementation.md b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/proxy-implementation.md new file mode 100644 index 0000000..75de053 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/boilerplate/references/proxy-implementation.md @@ -0,0 +1,45 @@ +# 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 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/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 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 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 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 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 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-src/commonstatemachine/common-child-flow/demo/SKILL.md b/skills-src/commonstatemachine/common-child-flow/demo/SKILL.md new file mode 100644 index 0000000..6c10428 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/demo/SKILL.md @@ -0,0 +1,407 @@ +--- +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. +- 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 combination to compare or keep side by side - say, a + "happy path" demo next to a "connection errors" demo of the very same implementation, or two different + implementations of the feature itself or of a nested feature dependency? If so, build them as Gradle product + flavors of this same demo module rather than separate modules, so you're not duplicating the manifest, + Activity, and ViewModel for every combination - see Step 2 for the flavor setup and Step 5 for what moves + into each flavor's own source set. +- 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. +- What should happen when the flow completes? Default to showing a short result screen with what the flow + actually produced and a button to exit from there, rather than closing the app immediately - that's what lets + someone running the demo by hand, or a UI-testing tool watching the screen, see the outcome. See 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. + +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. + +This is a single module even when the user wants several combinations compared side by side (see "Multiple +variants" above) - don't create a separate Gradle module per combination. Steps 2 and 5 cover how each +combination becomes its own Gradle product flavor of this one module instead, so the manifest, Activity, and +ViewModel are written once and shared. + +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. + +If the user asked to compare multiple combinations side by side (the "Multiple variants" case), add a product +flavor per combination instead of a second module. Name each flavor after what it actually demos - +`happyPath`/`connectionErrors` for different fixture scenarios of the same implementation, +`friendDetailsV1`/`friendDetailsV2` for different nested implementations - and give each one its own +`applicationIdSuffix` so every variant installs as its own app and several can sit on the same device (or +emulator) at once instead of overwriting each other: + +```kotlin +android { + flavorDimensions += "demo" + productFlavors { + create("happyPath") { + dimension = "demo" + applicationIdSuffix = ".happypath" + } + create("connectionErrors") { + dimension = "demo" + applicationIdSuffix = ".connectionerrors" + } + } +} +``` + +If the project keeps Kotlin sources under a `kotlin/` directory rather than Android Gradle's default `java/` +(as the example project does), point each flavor's source set at it explicitly, or Gradle won't find the +flavor-specific code from Step 5: + +```kotlin +android { + sourceSets { + getByName("happyPath") { + java.directories.add("src/happyPath/kotlin") + res.directories.add("src/happyPath/res") + } + getByName("connectionErrors") { + java.directories.add("src/connectionErrors/kotlin") + res.directories.add("src/connectionErrors/res") + } + } +} +``` + +When the variants also wire different implementations of a nested feature dependency (Step 3), that dependency +becomes flavor-scoped instead of plain `implementation(...)`: + +```kotlin +dependencies { + // Shared by every flavor + implementation(project(":examples:skills:friendlist:implementation")) + // One nested implementation per flavor + "happyPathImplementation"(project(":examples:skills:frienddetails:implementation-a")) + "connectionErrorsImplementation"(project(":examples:skills:frienddetails:implementation-b")) +} +``` + +When the variants only differ in fixture behavior for the same implementation (as in the example), every +flavor shares the same dependencies and only Step 5's DI module differs. + +Everything else in this guide still applies to the module as a whole; Step 5 covers the other code that needs a +flavor-specific split, and Step 8 covers giving each flavor its own app name so they're told apart once +installed side by side. + +## 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. + +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. 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: give each implementation its own Gradle product flavor + (Step 2) rather than picking just one. +- **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. + +## 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. + +If you added product flavors in Step 2, don't duplicate the whole `Application` class per flavor - only the DI +module actually needs to differ, since that's what holds the fixture bindings and the `includes` that may point +at a different nested implementation. Keep `App.kt` (the `Application` subclass, `startKoin`, logging setup) in +`src/main` exactly as above, referencing `AppModule::class` generically. Pull just the `@Module`/`@ComponentScan` +class itself out of `App.kt` into its own `AppModule.kt`, and give each flavor its own copy in its own source set: + +``` +demo/src/ +├── main/kotlin/.../App.kt # Shared: Application + startKoin, references AppModule::class +├── happyPath/kotlin/.../AppModule.kt # This flavor's fixtures (all succeed) +└── connectionErrors/kotlin/.../AppModule.kt # This flavor's fixtures (fail once, then succeed) +``` + +See [the example `happyPath` AppModule.kt](assets/example/demo/src/happyPath/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/AppModule.kt) +and [`connectionErrors` AppModule.kt](assets/example/demo/src/connectionErrors/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/AppModule.kt): +same shape and the same `@Module(includes = [AuthModule::class])`, different fixture bodies. If the variants +also wire different nested implementations (Step 3), point each copy's `includes` at that flavor's own +implementation module instead of keeping it identical across copies. + +Don't do this source-set split for a single-combination demo - it's only worth the extra files once there's +more than one flavor to tell apart. + +## 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, wiring the Android back gesture through `navigationBackHandler`. + +Rather than closing the activity the moment the flow's `finish` callback fires, capture the `Result` value it +hands you in a bit of remembered state and swap to a small result screen that shows it, with a button that exits +from there. This way, running the demo - by hand, or with a UI-testing tool watching the screen - shows what the +flow actually produced, not just that it finished: + +```kotlin +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + val (result, setResult) = remember { mutableStateOf(null) } + + SkillsTheme { + Scaffold( + topBar = { SkillsAppBar(title = stringResource(R.string.app_name), topLevel = true) } + ) { paddingValues -> + if (null == result) { + MainScreen(modifier = Modifier.padding(paddingValues), onComplete = setResult) + } else { + ResultScreen(result) { finish() } + } + } + } + } + } +} + +@Composable +private fun MainScreen(modifier: Modifier = Modifier, onComplete: (AuthResult) -> Unit) { + val viewModel: MainViewModel = koinViewModel() + val uiApi: AuthUiApi = koinInject() + + CommonFlowComposition( + viewModel = viewModel, + navigationBackHandler = { enabled, onBack -> BackHandler(enabled, onBack) }, + content = { state, onGesture -> uiApi.Screen(state = state, onGesture = onGesture, modifier = modifier) }, + finish = { onComplete(it) } + ) +} + +@Composable +private fun ResultScreen(result: AuthResult, onComplete: () -> Unit) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(Dimensions.medium, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + style = MaterialTheme.typography.bodyLarge, + text = stringResource(R.string.result_authenticated, result.authenticated) + ) + Button(onClick = onComplete) { + Text(text = stringResource(R.string.btn_exit)) + } + } +} +``` + +See [the example `MainActivity.kt`](assets/example/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/MainActivity.kt) for the full file. + +Write the result screen's text from the feature's actual `Result` type - a boolean like the example's +`authenticated`, an id, a count, whatever fields it actually carries. If the `Result` type carries nothing +worth showing (a bare `Unit` or marker type), a static confirmation message is enough - ask the user if it's not +obvious what to show. The result text and the exit button's label need their own entries in `strings.xml` +alongside `app_name` (see Step 8). + +If the user explicitly asks for the simpler behavior instead - close the app the moment the flow completes, no +result screen - skip the remembered `result` state and call `finish()` directly from the `finish` callback: +`finish = { finish() }`. + +## 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. +If Step 6's result screen is in use, add its two strings to the same `strings.xml` - one for the result text +(with a format placeholder for whatever field of the `Result` type it displays, like `result_authenticated` in +the example) and one for the exit button's label (`btn_exit` in the example). +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. + +If you added product flavors in Step 2, give each one its own `app_name` by overriding the string in that +flavor's own `res/values/strings.xml` (`src//res/values/strings.xml`) - for example "Auth Demo (Happy +Path)" and "Auth Demo (Connection Errors)". Combined with each flavor's `applicationIdSuffix` from Step 2, this +is what lets every variant sit on the device as its own separately-launchable app instead of one overwriting +another, and lets you tell them apart in the launcher. + +## 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/architecture.puml b/skills-src/commonstatemachine/common-child-flow/demo/references/architecture.puml new file mode 120000 index 0000000..932edc2 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/demo/references/architecture.puml @@ -0,0 +1 @@ +../../../.shared/architecture.puml \ 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 120000 index 0000000..6e90105 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/demo/references/fixture-patterns.md @@ -0,0 +1 @@ +../../.shared/fixture-patterns.md \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/journeys/SKILL.md b/skills-src/commonstatemachine/common-child-flow/journeys/SKILL.md new file mode 100644 index 0000000..fc59d0f --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/journeys/SKILL.md @@ -0,0 +1,406 @@ +--- +name: commonstatemachine-commonchildflow-journeys +description: Use this skill to set up an Android Journeys (AI-driven, natural-language UI test) suite for an + existing CommonStateMachine Common Child Flow feature module. Use it whenever the user asks to write UI tests, + journey tests, or AI-driven tests for a feature module - for example "Write journey tests for the auth + module", "Set up Journeys to test the friendlist flow end-to-end", or "Create Gemini-driven UI tests for the + payment flow". It builds a small standalone host app fed by fixtures (so the suite runs without a backend), + guesses the feature's actual screens and possible actions from its gestures, UI states and composables, and + proposes journey scenarios for the user to confirm before writing the `.journey.xml` files themselves. This + skill is independent of the demo-app skill - it isn't a prerequisite for it and doesn't build on it - but + follows the same overall approach and reuses the same fixture-writing guide. If the feature module doesn't + exist yet, use the boilerplate skill first - this skill only wires up a way to test one that's already there. +metadata: + author: Motorro + last-updated: '2026-10-06' + keywords: + - android + - architecture + - cross-platform + - kotlin-multiplatform + - mvi + - state-machine + - feature-flow + - testing + - ui-testing + - journeys + - gemini + - ai-testing +--- + +## Description +Use this skill to set up an [Android Journeys](https://developer.android.com/studio/preview/journeys) test +suite for an existing Common Child Flow feature: a small host application module, fed by fixture (fake) domain +use-cases so it runs without a real backend, with natural-language `.journey.xml` files that describe user +flows for Gemini to execute and verify - no view-id locators or Espresso scripts to write or maintain. + +This is a separate skill from the demo-app skill - not a prerequisite for it, and not built on top of it - but +it follows the same overall shape (a small host app with fixtures) and reuses the same fixture-writing guide, so +a project that has both skills will see a family resemblance between a `demo` module and a `journey` module. + +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). +For the Journeys framework's own file format, how to run a suite, and its one-time Gemini authentication setup, +refer to [Journey authoring and running](references/journey-authoring.md). + +## Example +This instruction uses [example authentication module](assets/example), together with its domain module and a +complete example journey module. 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 domain use-cases, with an AGP test suite that runs +natural-language `.journey.xml` files against it: + +``` +my-project # Project folder +├── auth # Existing feature folder - module must already exist +│ ├── api +│ ├── implementation +│ └── journey # New: the journey test host 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 journey module +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 suite for? It must already exist - if it doesn't, use the boilerplate skill + first to create it, then come back here. See Step 1. +- 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 host app instead of + inventing new ones. If the project has no such module, fall back to plain `MaterialTheme` and a `TopAppBar`. +- The feature's actual screens and possible actions: trace them from the feature's own gestures, UI states and + composables rather than assuming - see Step 2. +- Journey scenarios: did the user already describe the scenarios they want tested (a happy path, a specific + validation or failure case), or should you propose some from what Step 2 finds? Either way, confirm the final + list with the user before writing any files - see Step 3. +- 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 5). These are exactly what you need to fake in Step 6. +- 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 5. +- 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 this module. See Step 7. +- 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 9. +- What should happen when the flow completes? Default to showing a short result screen with what the flow + actually produced, the same way the demo skill's host app does - a journey step can then verify the result + text directly instead of only checking that the app closed. See Step 8. + +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 test, 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 (`journey` in the example). Use the Gradle setup common to other application +modules in the project if one exists to copy from. + +## Step 2. Guess the module's UI flow from its gestures, UI states, and composables +A journey step is matched against what Gemini actually sees on screen, so the journey text has to describe the +feature's real screens, fields, labels and conditions - not an abstraction of them. Build that picture before +writing anything, by looking at: + +- The feature's *concrete* UI-state implementation, not the empty marker interface its `api` module declares + (`AuthUiState` in the example is just `interface AuthUiState` - the real shape lives in `implementation`, as + `AuthUiStateImpl.Form`/`.Loading`/`.Error` and similar). Each concrete subtype is one screen; its fields are + what that screen actually shows. +- The feature's *concrete* gesture implementation, the same way (`AuthGestureImpl` rather than the bare + `AuthGesture` marker) - each gesture is one thing the user can do, and which screen's state accepts it tells + you which screen that action belongs to. +- The UI renderer's composables (for example `AuthFormScreen`, `AuthLoadingScreen`, `AuthErrorScreen`, wired + together by `AuthUiRendererImpl`) for the actual button and field labels - read them from the feature's own + string resources, not invented text - and for the conditions that show, hide, enable or disable an element (a + Skip button only when `canSkip`, a Retry label instead of Close only when `canRetry`, a Login button disabled + until `loginEnabled`). + +Put this together into a short map: for each screen, what it shows, what can be done from it, and which gesture +leads to which next screen. Steps 3 and 11 draw their wording directly from this map. + +## Step 3. Suggest and confirm journey scenarios +Using the screen map from Step 2, and the domain use-cases and fixture behavior from Steps 5-6, propose a short +list of scenarios rather than silently picking one - typically: + +- the happy path all the way through to the flow's result screen +- one scenario per client-side condition the UI itself enforces (a field validation message, a button that + stays disabled until input is valid) +- one scenario per domain-level failure a fixture can produce (an error screen, a retry path) - if the user + described a specific scenario for Step 6, that's a scenario here too +- one scenario per branch of a nested feature dependency, if Step 5 found one with more than one outcome + +If the user already gave you their own list of scenarios, use it instead of inventing one, but still sanity-check +each one against the screen map from Step 2 (a scenario that refers to a field or button that doesn't exist +can't be written faithfully). Confirm the final list with the user before Step 11 writes any files - this is a +design decision, not something to guess silently. + +## Step 4. Create the journey module's Gradle file +This is an internal testing 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`, the same reasoning as the demo skill applies here too. + +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 journey module's build file](assets/example/journey/build.gradle.kts) for a concrete +dependency list. + +Then add the AGP test-suite block that actually runs the journeys: + +```kotlin +android { + testOptions { + suites { + create("journeys") { + targets { + create("default") { } + } + useJunitEngine { + inputs += listOf(com.android.build.api.dsl.AgpTestSuiteInputParameters.TESTED_APKS) + includeEngines += listOf("journeys-test-engine") + enginesDependencies(libs.junit.platform.launcher) + enginesDependencies(libs.junit.platform.engine) + enginesDependencies(libs.journeys.junit.engine) + } + targetVariants += listOf("debug") + } + } + } +} +``` + +The suite's name (`journeys` here) doubles as the convention folder name AGP scans for `.journey.xml` files +(`src/journeys/`) - keep them matching, or rename both together. `targetVariants` points the suite at the plain +`debug` build, since this module has no product flavors (see Step 1) - there's only one variant to target. + +Check the project's version catalog (`gradle/libs.versions.toml`) for `journeys-junit-engine`, +`junit-platform-launcher`, and `junit-platform-engine` aliases, and add them if the project doesn't have them +yet (`com.android.tools.journeys:journeys-junit-engine`, `org.junit.platform:junit-platform-launcher`, +`org.junit.platform:junit-platform-engine`). + +## Step 5. 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 this +module must fake, because in a real app it would come from a `usecase` module implementation that this module +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 test + host (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. + +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 6. Create fixtures for the domain use-cases +Follow [this guide](references/fixture-patterns.md) to fake every domain use-case identified in Step 5. + +A journey's fixtures need to produce the exact outcomes its confirmed scenarios (Step 3) check for - and +a scenario that depends on *what was entered* (a specific username and password, say) needs a fixture that +actually branches on its input and matches against a known, fixed value, rather than one that always takes +the same path regardless of what's passed in (see the guide's "Matching a specific input" section). Keep +those expected values as named constants so the journey files from Step 11 can refer to the exact same values. + +## Step 7. 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 this module 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 6. +See [the example `App.kt`](assets/example/journey/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/journey/App.kt) +and [`AppModule.kt`](assets/example/journey/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/journey/AppModule.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 6 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. + +This module never needs the flavor-scoped source-set split the demo skill uses for comparing several variants +(see Step 1) - there's only ever one `AppModule`, in `src/main`. + +## Step 8. 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, wiring the Android back gesture through `navigationBackHandler`. + +Rather than closing the activity the moment the flow's `finish` callback fires, capture the `Result` value it +hands you in a bit of remembered state and swap to a small result screen that shows it, with a button that exits +from there - the same pattern the demo skill uses. This matters even more here than in a demo: it gives a +journey something concrete to verify (the result screen's actual text) instead of only being able to check that +the app closed: + +```kotlin +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + val (result, setResult) = remember { mutableStateOf(null) } + + SkillsTheme { + Scaffold( + topBar = { SkillsAppBar(title = stringResource(R.string.app_name), topLevel = true) } + ) { paddingValues -> + if (null == result) { + MainScreen(modifier = Modifier.padding(paddingValues), onComplete = setResult) + } else { + ResultScreen(result) { finish() } + } + } + } + } + } +} + +@Composable +private fun MainScreen(modifier: Modifier = Modifier, onComplete: (AuthResult) -> Unit) { + val viewModel: MainViewModel = koinViewModel() + val uiApi: AuthUiApi = koinInject() + + CommonFlowComposition( + viewModel = viewModel, + navigationBackHandler = { enabled, onBack -> BackHandler(enabled, onBack) }, + content = { state, onGesture -> uiApi.Screen(state = state, onGesture = onGesture, modifier = modifier) }, + finish = { onComplete(it) } + ) +} + +@Composable +private fun ResultScreen(result: AuthResult, onComplete: () -> Unit) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(Dimensions.medium, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + style = MaterialTheme.typography.bodyLarge, + text = stringResource(R.string.result_authenticated, result.authenticated) + ) + Button(onClick = onComplete) { + Text(text = stringResource(R.string.btn_exit)) + } + } +} +``` + +See [the example `MainActivity.kt`](assets/example/journey/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/journey/MainActivity.kt) +for the full file. Write the result screen's text from the feature's actual `Result` type, and phrase a +"verify the result" journey action (Step 11) against that same text. As with the demo skill, putting this text +in `strings.xml` is just the example's own style, not a rule - a hardcoded string works just as well for an +internal test host. + +## Step 9. 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 testing (for example data that normally +comes from a parent flow), ask the user what to pass, or use a clearly-marked placeholder value. + +## Step 10. 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 test host, not a production app +icon. With `minSdk` set to a recent version (Step 4), 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 module 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 11. Create the journey files +Write one `.journey.xml` file per scenario confirmed in Step 3, under `src/journeys/` (matching the suite name +from Step 4), each with a `name`, a `description`, and an ordered list of `action` steps written as plain, +present-tense sentences - what to wait for, what to type or tap, what to verify - using the exact field labels, +button text and conditions found in Step 2, never internal identifiers: + +```xml + + + Test authentication happy path with valid credentials + + Wait for the authentication form to open. + Type "user" into the username field and "password" into the password field. + Tap the Login button to submit the form. + Verify that authentication succeeds and the screen displays "authenticated: true". + + +``` + +Keep each file to one scenario - a happy path and a validation-error case are two files, not one file with +branches - so a failing run points at one specific thing that broke. See +[the example journey files](assets/example/journey/src/journeys) for the scenarios this walkthrough produces, +and [Journey authoring and running](references/journey-authoring.md) for the full format reference, how to run +a suite locally, and the one-time Gemini authentication setup. + +## Step 12. Register the module (if the project needs it) +If the project registers Gradle modules explicitly (check `settings.gradle.kts`), add the new journey module +there following the existing naming convention. diff --git a/skills-src/commonstatemachine/common-child-flow/journeys/assets/example/api b/skills-src/commonstatemachine/common-child-flow/journeys/assets/example/api new file mode 120000 index 0000000..ae54fa4 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/domain b/skills-src/commonstatemachine/common-child-flow/journeys/assets/example/domain new file mode 120000 index 0000000..829e27c --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/implementation b/skills-src/commonstatemachine/common-child-flow/journeys/assets/example/implementation new file mode 120000 index 0000000..55c188d --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/journey/build.gradle.kts b/skills-src/commonstatemachine/common-child-flow/journeys/assets/example/journey/build.gradle.kts new file mode 120000 index 0000000..1a6c3a7 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/journeys/assets/example/journey/build.gradle.kts @@ -0,0 +1 @@ +../../../../../../../examples/skills/auth/journey/build.gradle.kts \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/journeys/assets/example/journey/src b/skills-src/commonstatemachine/common-child-flow/journeys/assets/example/journey/src new file mode 120000 index 0000000..23dc450 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/journeys/assets/example/journey/src @@ -0,0 +1 @@ +../../../../../../../examples/skills/auth/journey/src \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/journeys/references/ARCHITECTURE.md b/skills-src/commonstatemachine/common-child-flow/journeys/references/ARCHITECTURE.md new file mode 120000 index 0000000..f1d6e62 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/journeys/references/ARCHITECTURE.md @@ -0,0 +1 @@ +../../../.shared/ARCHITECTURE.md \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/journeys/references/COMMON-CHILD-FLOW.md b/skills-src/commonstatemachine/common-child-flow/journeys/references/COMMON-CHILD-FLOW.md new file mode 120000 index 0000000..c83c444 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/journeys/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/journeys/references/architecture.puml b/skills-src/commonstatemachine/common-child-flow/journeys/references/architecture.puml new file mode 120000 index 0000000..932edc2 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/journeys/references/architecture.puml @@ -0,0 +1 @@ +../../../.shared/architecture.puml \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/journeys/references/common-child-flow.puml b/skills-src/commonstatemachine/common-child-flow/journeys/references/common-child-flow.puml new file mode 120000 index 0000000..b135e8b --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/journeys/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/journeys/references/fixture-patterns.md b/skills-src/commonstatemachine/common-child-flow/journeys/references/fixture-patterns.md new file mode 120000 index 0000000..6e90105 --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/journeys/references/fixture-patterns.md @@ -0,0 +1 @@ +../../.shared/fixture-patterns.md \ No newline at end of file diff --git a/skills-src/commonstatemachine/common-child-flow/journeys/references/journey-authoring.md b/skills-src/commonstatemachine/common-child-flow/journeys/references/journey-authoring.md new file mode 100644 index 0000000..a0fa3ff --- /dev/null +++ b/skills-src/commonstatemachine/common-child-flow/journeys/references/journey-authoring.md @@ -0,0 +1,76 @@ +# Writing and running Android Journeys + +[Android Journeys](https://developer.android.com/studio/preview/journeys) (`com.android.tools.journeys:journeys-junit-engine`) +is an AI-driven UI-testing framework: instead of view-id locators or Espresso scripts, a journey file describes +what to do and check in plain-language steps. At each step the test runner captures a screenshot and the current +UI hierarchy, sends both to Gemini along with your instruction, and Gemini decides and performs the matching +action (tap, type, swipe, or an assertion) until the step's condition is met. + +This is what makes the UI-flow guessing step in the main guide matter: because a step is matched by Gemini +against what's actually rendered, the journey's wording has to use the screen's real labels and real +conditions (the exact button text, the exact field label, whether a control is enabled) - not invented ones or +internal identifiers. + +## Journey file format + +A `.journey.xml` file has a `name`, a `description`, and an ordered list of `action` steps written as complete +sentences, in present tense, one user-observable action or check per step: + +```xml + + + Test authentication happy path with valid credentials + + Wait for the authentication form to open. + Type "user" into the username field and "password" into the password field. + Tap the Login button to submit the form. + Verify that authentication succeeds and the screen displays "authenticated: true". + + +``` + +Keep each file to a single scenario - a happy path and a validation-error case are two files, not one file with +branches - so a failure points at one specific thing that broke. + +## Where the files live, and the suite that finds them + +AGP scans for `.journey.xml` files inside `src//`, where `` is literally the name given to +the `testOptions.suites.create(...)` block in the module's `build.gradle.kts` (see the main guide's Gradle-setup +step). This skill names the suite `journeys`, so the files live under `src/journeys/` - if that name ever +changes, the folder has to move with it. + +## Running a suite + +```bash +./gradlew :path:to:the:journey:module:journeysDefault +``` + +(`journeysDefault` because the suite is named `journeys` and its target is named `default` - adjust both parts +to match whatever this project's module and suite are actually called.) To run a single file instead of the +whole suite: + +```bash +JOURNEYS_FILTER=happy_path.journey.xml ./gradlew :path:to:the:journey:module:journeysDefault +``` + +## One-time setup: Gemini authentication + +The `journeys-junit-engine` runner talks to Gemini via Google Application Default Credentials, not an API key +in the project. This is a one-time environment setup, not something this skill's generated files need to carry: + +- **Android Studio**: Settings/Preferences -> Tools -> Gemini -> sign in, and enable Journeys under Studio Labs + if it's not already on. +- **Local terminal / CLI**: `gcloud auth application-default login` once per machine. +- **CI**: a service account with the Vertex AI User role, authenticated via `GOOGLE_APPLICATION_CREDENTIALS` + or impersonation - see the project's own CI setup for the exact mechanism in use. + +## Two gotchas worth knowing before a run + +- **Emulator soft keyboard**: emulators treat the host's keyboard as physical hardware by default, which + suppresses the on-screen keyboard. If Gemini expects to see and dismiss a soft keyboard that never appears, + typing and keyboard-dismissal steps can misfire. Fix with + `adb shell settings put secure show_ime_with_hard_keyboard 1`, or permanently in the AVD's `config.ini` + (`hw.keyboard = no`). +- **Cost**: each action step sends a screenshot and UI-hierarchy payload to Gemini - roughly 1,500-2,500 tokens + per step, so a 10-step journey is on the order of 20,000 tokens. Prefer specific, unambiguous action wording + (fewer retries) and `JOURNEYS_FILTER` for iterating on one file instead of re-running the whole suite. diff --git a/skills/README.md b/skills/README.md new file mode 100644 index 0000000..9c4b170 --- /dev/null +++ b/skills/README.md @@ -0,0 +1,148 @@ +# AI skills +A set of AI skills for working with [CommonStateMachine](..)'s Common Child Flow architecture: scaffolding a new +feature module, extending one that already exists, building a standalone demo app to show a feature off or test +it on its own, and setting up AI-driven UI tests for one. Point your AI assistant at the one you need and +describe what you want in plain language - each skill asks for anything it's missing. + +## Assumed project architecture +These skills assume a particular shape for the surrounding project. None of them requires exactly this layout - +each skill checks the actual project before guessing anything project-specific - but this is the architecture the +examples below, and the skills' own vocabulary ("domain", "use-case", "DI framework"), refer to: + +- A **domain module** (`domain` in the example) that declares the use-case interfaces, their input/output models, + and the domain exception types - pure contracts, no implementation. Feature modules call these interfaces + without depending on wherever their real implementations happen to live, which is what lets a demo app (see + the demo skill below) fake them with fixtures instead. The interfaces don't have to live in one shared + `domain` module specifically - a feature can just as well use interfaces declared in another feature's own + `api` module - these skills only care that the interfaces exist and can be faked, not where they're declared. +- A **common UI module** (`appcore` in the example) with the shared design system - theme, typography, and common + composables such as an app bar or a loading indicator - that every feature's views, and any demo app, reuse + instead of rebuilding their own. +- One or more **Common Child Flow feature modules**, each split into an `api` module (its + `Gesture`/`UiState`/`Input`/`Result`/`DataApi`/`UiApi` contracts) and one or more `implementation` modules (its + states, state-factory, DI module and views) - see the + [Common Child Flow architecture](../README.md#common-child-flow-api) for how these fit together. +- An **app module** - the composition root that starts the application, wires up the DI container, and hosts the + feature flows via proxies. +- Some **DI framework** (Koin in the example, but Hilt, Dagger, or none at all are equally fine - every skill + checks what the project actually uses rather than assuming Koin) that ties the use-case implementations, the + feature implementations, and the app together. + +A typical layout looks like this: + +``` +my-project +├── domain # Use-case interfaces, domain models, domain exceptions - no implementation +├── appcore # Shared UI: theme, design system, common composables +├── auth # One Common Child Flow feature module... +│ ├── api +│ └── implementation +├── friendlist # ...and another +│ ├── api +│ └── implementation +└── app # Composition root: Application, MainActivity, DI wiring +``` + +For the complete, runnable version of this layout - the one every skill's examples below refer to - see +[`examples/skills`](../examples/skills). + +## 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. + +## 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. + +## 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 + - Can build several variants of the same demo side by side - different fixture scenarios, different nested + implementations - as Gradle product flavors of one module, so each installs as its own app on the device + +### Simple demo: one feature, one scenario +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. + +### Multiple demos side by side: A-B testing and scenario comparisons +Say `friendlist` hosts a `frienddetails` child flow of its own (proxied the same way the example app hosts +`auth`), and you want to compare two things at once rather than build and rebuild a single demo by hand: + +> Build two variants of the `friendlist` demo side by side so I can compare the `frienddetails` flow's two +> implementations: `friendDetailsV1` and `friendDetailsV2`. +> - Use Gradle product flavors of the same demo module instead of separate modules - name them after the +> implementations they demo. +> - Keep `friendlist`'s own fixtures and the shared UI identical across both variants; only the nested +> `frienddetails` dependency differs per flavor. +> - Give each flavor its own `applicationIdSuffix` and app name so both install side by side on the same +> device at once. + +The same approach works for comparing fixture behavior instead of implementations - for example a `happyPath` +variant where every use-case succeeds, next to a `connectionErrors` variant where the same use-cases fail once +with an I/O error before succeeding on retry, so you can demo both the normal flow and its error handling +without switching builds. + +## Set up AI-driven UI tests for a Common Child Flow module +[Journeys skill](commonstatemachine/common-child-flow/journeys/SKILL.md): sets up an +[Android Journeys](https://developer.android.com/studio/preview/journeys) test suite for an existing feature +module - a small host app, fed by fixtures, plus natural-language `.journey.xml` files that Gemini executes and +verifies against it. Independent of the demo-app skill, though it follows the same overall shape. + - Guesses the feature's real screens and possible actions from its gestures, UI states and composables, rather + than inventing them + - Proposes journey scenarios (happy path, validation cases, failure cases) for you to confirm before writing + any files + - Fakes the feature's domain use-cases with fixtures that match what a scenario actually needs - including + fixtures that branch on a specific typed value, not just a call counter + - Wires up the AGP test-suite Gradle block and a minimal host app, reusing the project's shared UI module and + DI framework + +For example, continuing the `friendlist` module from the examples above: + +> Set up journey tests for the `friendlist` module. +> - Cover the happy path (friends load and display) and the retry path after an I/O error on the first load. +> - Suggest any other scenarios worth covering once you've looked at the module's states and gestures. \ 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..0172cfc --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/assets/example/implementation/ui/AuthFormScreen.kt @@ -0,0 +1,142 @@ +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.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +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.focus.FocusDirection +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.input.ImeAction +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, +) { + val focusManager = LocalFocusManager.current + + 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, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + keyboardActions = KeyboardActions( + onNext = { focusManager.moveFocus(FocusDirection.Down) } + ) + ) + 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, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions( + onDone = { + focusManager.clearFocus() + if (state.loginEnabled) { + onGesture(AuthGestureImpl.Action) + } + } + ) + ) + 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..c79eb41 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/add-state/references/renderer-factory-context-interface-implementation.md @@ -0,0 +1,87 @@ +# 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 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. +- 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..da30c21 --- /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 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: + +- 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 new file mode 100644 index 0000000..ebfaa5b --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/SKILL.md @@ -0,0 +1,162 @@ +--- +name: commonstatemachine-commonchildflow-boilerplate +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-10-02' + 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](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). + +## 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. + +## 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? +- 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. + +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/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..0172cfc --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/assets/example/implementation/ui/AuthFormScreen.kt @@ -0,0 +1,142 @@ +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.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +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.focus.FocusDirection +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.input.ImeAction +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, +) { + val focusManager = LocalFocusManager.current + + 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, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + keyboardActions = KeyboardActions( + onNext = { focusManager.moveFocus(FocusDirection.Down) } + ) + ) + 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, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions( + onDone = { + focusManager.clearFocus() + if (state.loginEnabled) { + onGesture(AuthGestureImpl.Action) + } + } + ) + ) + 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 new file mode 100644 index 0000000..94621a6 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/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/boilerplate/references/COMMON-CHILD-FLOW.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/COMMON-CHILD-FLOW.md new file mode 100644 index 0000000..655ed24 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/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/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/base-state-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/base-state-implementation.md new file mode 100644 index 0000000..a78ef49 --- /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](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/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..35816f9 --- /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-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/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 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..41148fa --- /dev/null +++ b/skills/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/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..a784446 --- /dev/null +++ b/skills/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/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..6914aec --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/di-implementation.md @@ -0,0 +1,47 @@ +# 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). +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). +- 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 new file mode 100644 index 0000000..69384b6 --- /dev/null +++ b/skills/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/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..44a6ce3 --- /dev/null +++ b/skills/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/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..75de053 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/proxy-implementation.md @@ -0,0 +1,45 @@ +# 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 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 new file mode 100644 index 0000000..c79eb41 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/boilerplate/references/renderer-factory-context-interface-implementation.md @@ -0,0 +1,87 @@ +# 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 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. +- 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..1308d91 --- /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(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/boilerplate/references/state-test-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/state-test-implementation.md new file mode 100644 index 0000000..4bc0b15 --- /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 new file mode 100644 index 0000000..ecaf1e6 --- /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-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/boilerplate/references/ui-renderer-implementation.md b/skills/commonstatemachine/common-child-flow/boilerplate/references/ui-renderer-implementation.md new file mode 100644 index 0000000..4ef14e6 --- /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-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/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) 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..6c10428 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/SKILL.md @@ -0,0 +1,407 @@ +--- +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. +- 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 combination to compare or keep side by side - say, a + "happy path" demo next to a "connection errors" demo of the very same implementation, or two different + implementations of the feature itself or of a nested feature dependency? If so, build them as Gradle product + flavors of this same demo module rather than separate modules, so you're not duplicating the manifest, + Activity, and ViewModel for every combination - see Step 2 for the flavor setup and Step 5 for what moves + into each flavor's own source set. +- 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. +- What should happen when the flow completes? Default to showing a short result screen with what the flow + actually produced and a button to exit from there, rather than closing the app immediately - that's what lets + someone running the demo by hand, or a UI-testing tool watching the screen, see the outcome. See 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. + +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. + +This is a single module even when the user wants several combinations compared side by side (see "Multiple +variants" above) - don't create a separate Gradle module per combination. Steps 2 and 5 cover how each +combination becomes its own Gradle product flavor of this one module instead, so the manifest, Activity, and +ViewModel are written once and shared. + +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. + +If the user asked to compare multiple combinations side by side (the "Multiple variants" case), add a product +flavor per combination instead of a second module. Name each flavor after what it actually demos - +`happyPath`/`connectionErrors` for different fixture scenarios of the same implementation, +`friendDetailsV1`/`friendDetailsV2` for different nested implementations - and give each one its own +`applicationIdSuffix` so every variant installs as its own app and several can sit on the same device (or +emulator) at once instead of overwriting each other: + +```kotlin +android { + flavorDimensions += "demo" + productFlavors { + create("happyPath") { + dimension = "demo" + applicationIdSuffix = ".happypath" + } + create("connectionErrors") { + dimension = "demo" + applicationIdSuffix = ".connectionerrors" + } + } +} +``` + +If the project keeps Kotlin sources under a `kotlin/` directory rather than Android Gradle's default `java/` +(as the example project does), point each flavor's source set at it explicitly, or Gradle won't find the +flavor-specific code from Step 5: + +```kotlin +android { + sourceSets { + getByName("happyPath") { + java.directories.add("src/happyPath/kotlin") + res.directories.add("src/happyPath/res") + } + getByName("connectionErrors") { + java.directories.add("src/connectionErrors/kotlin") + res.directories.add("src/connectionErrors/res") + } + } +} +``` + +When the variants also wire different implementations of a nested feature dependency (Step 3), that dependency +becomes flavor-scoped instead of plain `implementation(...)`: + +```kotlin +dependencies { + // Shared by every flavor + implementation(project(":examples:skills:friendlist:implementation")) + // One nested implementation per flavor + "happyPathImplementation"(project(":examples:skills:frienddetails:implementation-a")) + "connectionErrorsImplementation"(project(":examples:skills:frienddetails:implementation-b")) +} +``` + +When the variants only differ in fixture behavior for the same implementation (as in the example), every +flavor shares the same dependencies and only Step 5's DI module differs. + +Everything else in this guide still applies to the module as a whole; Step 5 covers the other code that needs a +flavor-specific split, and Step 8 covers giving each flavor its own app name so they're told apart once +installed side by side. + +## 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. + +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. 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: give each implementation its own Gradle product flavor + (Step 2) rather than picking just one. +- **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. + +## 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. + +If you added product flavors in Step 2, don't duplicate the whole `Application` class per flavor - only the DI +module actually needs to differ, since that's what holds the fixture bindings and the `includes` that may point +at a different nested implementation. Keep `App.kt` (the `Application` subclass, `startKoin`, logging setup) in +`src/main` exactly as above, referencing `AppModule::class` generically. Pull just the `@Module`/`@ComponentScan` +class itself out of `App.kt` into its own `AppModule.kt`, and give each flavor its own copy in its own source set: + +``` +demo/src/ +├── main/kotlin/.../App.kt # Shared: Application + startKoin, references AppModule::class +├── happyPath/kotlin/.../AppModule.kt # This flavor's fixtures (all succeed) +└── connectionErrors/kotlin/.../AppModule.kt # This flavor's fixtures (fail once, then succeed) +``` + +See [the example `happyPath` AppModule.kt](assets/example/demo/src/happyPath/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/AppModule.kt) +and [`connectionErrors` AppModule.kt](assets/example/demo/src/connectionErrors/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/AppModule.kt): +same shape and the same `@Module(includes = [AuthModule::class])`, different fixture bodies. If the variants +also wire different nested implementations (Step 3), point each copy's `includes` at that flavor's own +implementation module instead of keeping it identical across copies. + +Don't do this source-set split for a single-combination demo - it's only worth the extra files once there's +more than one flavor to tell apart. + +## 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, wiring the Android back gesture through `navigationBackHandler`. + +Rather than closing the activity the moment the flow's `finish` callback fires, capture the `Result` value it +hands you in a bit of remembered state and swap to a small result screen that shows it, with a button that exits +from there. This way, running the demo - by hand, or with a UI-testing tool watching the screen - shows what the +flow actually produced, not just that it finished: + +```kotlin +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + val (result, setResult) = remember { mutableStateOf(null) } + + SkillsTheme { + Scaffold( + topBar = { SkillsAppBar(title = stringResource(R.string.app_name), topLevel = true) } + ) { paddingValues -> + if (null == result) { + MainScreen(modifier = Modifier.padding(paddingValues), onComplete = setResult) + } else { + ResultScreen(result) { finish() } + } + } + } + } + } +} + +@Composable +private fun MainScreen(modifier: Modifier = Modifier, onComplete: (AuthResult) -> Unit) { + val viewModel: MainViewModel = koinViewModel() + val uiApi: AuthUiApi = koinInject() + + CommonFlowComposition( + viewModel = viewModel, + navigationBackHandler = { enabled, onBack -> BackHandler(enabled, onBack) }, + content = { state, onGesture -> uiApi.Screen(state = state, onGesture = onGesture, modifier = modifier) }, + finish = { onComplete(it) } + ) +} + +@Composable +private fun ResultScreen(result: AuthResult, onComplete: () -> Unit) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(Dimensions.medium, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + style = MaterialTheme.typography.bodyLarge, + text = stringResource(R.string.result_authenticated, result.authenticated) + ) + Button(onClick = onComplete) { + Text(text = stringResource(R.string.btn_exit)) + } + } +} +``` + +See [the example `MainActivity.kt`](assets/example/demo/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/MainActivity.kt) for the full file. + +Write the result screen's text from the feature's actual `Result` type - a boolean like the example's +`authenticated`, an id, a count, whatever fields it actually carries. If the `Result` type carries nothing +worth showing (a bare `Unit` or marker type), a static confirmation message is enough - ask the user if it's not +obvious what to show. The result text and the exit button's label need their own entries in `strings.xml` +alongside `app_name` (see Step 8). + +If the user explicitly asks for the simpler behavior instead - close the app the moment the flow completes, no +result screen - skip the remembered `result` state and call `finish()` directly from the `finish` callback: +`finish = { finish() }`. + +## 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. +If Step 6's result screen is in use, add its two strings to the same `strings.xml` - one for the result text +(with a format placeholder for whatever field of the `Result` type it displays, like `result_authenticated` in +the example) and one for the exit button's label (`btn_exit` in the example). +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. + +If you added product flavors in Step 2, give each one its own `app_name` by overriding the string in that +flavor's own `res/values/strings.xml` (`src//res/values/strings.xml`) - for example "Auth Demo (Happy +Path)" and "Auth Demo (Connection Errors)". Combined with each flavor's `applicationIdSuffix` from Step 2, this +is what lets every variant sit on the device as its own separately-launchable app instead of one overwriting +another, and lets you tell them apart in the launcher. + +## 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..f500670 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/build.gradle.kts @@ -0,0 +1,69 @@ +/* + * Copyright 2026 Nikolai Kotchetkov. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + alias(libs.plugins.motorro.android.app) + alias(libs.plugins.koin) +} + +android { + defaultConfig { + minSdk = 34 + } + + flavorDimensions += "demo" + productFlavors { + create("happyPath") { + dimension = "demo" + applicationIdSuffix = ".happypath" + isDefault = true + } + create("connectionErrors") { + dimension = "demo" + applicationIdSuffix = ".connectionerrors" + } + } + + sourceSets { + getByName("happyPath") { + java.directories.add("src/happyPath/kotlin") + res.directories.add("src/happyPath/res") + } + getByName("connectionErrors") { + java.directories.add("src/connectionErrors/kotlin") + res.directories.add("src/connectionErrors/res") + } + } +} + +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/connectionErrors/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/AppModule.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/connectionErrors/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/AppModule.kt new file mode 100644 index 0000000..c98df26 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/connectionErrors/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/AppModule.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2026 Nikolai Kotchetkov. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.motorro.commonstatemachine.examples.skills.auth.demo + +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.IOException +import com.motorro.commonstatemachine.skills.domain.session.data.Username +import io.github.aakira.napier.Napier +import kotlinx.coroutines.delay +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single +import kotlin.time.Duration.Companion.seconds + +/** + * Demo fixtures for Connection Errors flavor + */ +@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 connection error" } + attempt = 2 + throw IOException("Network error") + } + else -> { + Napier.d(tag = tag) { "Emulating success on retry" } + 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 connection error" } + attempt = 2 + throw IOException("Network error") + } + else -> { + Napier.d(tag = tag) { "Emulating success on retry" } + attempt = 1 + } + } + } + } + } +} diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/connectionErrors/res/values/strings.xml b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/connectionErrors/res/values/strings.xml new file mode 100644 index 0000000..fe206a4 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/connectionErrors/res/values/strings.xml @@ -0,0 +1,3 @@ + + Auth Demo (Connection Errors) + diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/happyPath/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/AppModule.kt b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/happyPath/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/AppModule.kt new file mode 100644 index 0000000..33043c3 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/happyPath/kotlin/com/motorro/commonstatemachine/examples/skills/auth/demo/AppModule.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2026 Nikolai Kotchetkov. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.motorro.commonstatemachine.examples.skills.auth.demo + +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.session.data.Username +import io.github.aakira.napier.Napier +import kotlinx.coroutines.delay +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single +import kotlin.time.Duration.Companion.seconds + +/** + * Demo fixtures for Happy Path flavor + */ +@Module(includes = [AuthModule::class]) +@ComponentScan +class AppModule { + + @Single + fun getPasswordRequirements(): GetPasswordRequirements { + val tag = GetPasswordRequirements::class.simpleName!! + return object : GetPasswordRequirements { + override suspend fun invoke(): PasswordRequirements { + delay(2.seconds) + Napier.d(tag = tag) { "Emulating success" } + return PasswordRequirements( + regex = "^.{8,}$".toRegex(), + description = "Minimum eight characters" + ) + } + } + } + + @Single + fun authenticateWithPassword(): AuthenticateWithPassword { + val tag = AuthenticateWithPassword::class.simpleName!! + return object : AuthenticateWithPassword { + override suspend fun invoke(username: Username, password: String) { + delay(2.seconds) + Napier.d(tag = tag) { "Emulating success" } + } + } + } +} diff --git a/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/happyPath/res/values/strings.xml b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/happyPath/res/values/strings.xml new file mode 100644 index 0000000..a822831 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/happyPath/res/values/strings.xml @@ -0,0 +1,3 @@ + + Auth Demo (Happy Path) + 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..584c1be --- /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,25 @@ +package com.motorro.commonstatemachine.examples.skills.auth.demo + +import android.app.Application +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 = [AppModule::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/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..86d2f19 --- /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,115 @@ +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.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +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.Dimensions +import com.motorro.commonstatemachine.examples.auth.appcore.ui.theme.SkillsTheme +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +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 { + + val (result, setResult) = remember { + mutableStateOf(null) + } + + SkillsTheme { + Scaffold( + topBar = { + SkillsAppBar( + title = stringResource(R.string.app_name), + modifier = Modifier.fillMaxWidth(), + topLevel = true + ) + } + ) { paddingValues -> + if (null == result) { + MainScreen( + modifier = Modifier.padding(paddingValues), + onComplete = setResult + ) + } else { + ResultScreen(result) { + finish() + } + } + } + } + } + } +} + +/** + * Feature composition + */ +@Composable +private fun MainScreen(modifier: Modifier = Modifier, onComplete: (AuthResult) -> Unit) { + val viewModel: MainViewModel = koinViewModel() + val uiApi: AuthUiApi = koinInject() + + CommonFlowComposition( + viewModel = viewModel, + navigationBackHandler = { enabled, onBack -> + BackHandler(enabled, onBack) + }, + content = { state, onGesture -> + uiApi.Screen( + state = state, + onGesture = onGesture, + modifier = modifier + ) + }, + finish = { + Napier.i { "Finished with: $it" } + onComplete(it) + } + ) +} + +/** + * Describes the flow result + */ +@Composable +private fun ResultScreen(result: AuthResult, onComplete: () -> Unit) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(Dimensions.medium, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + style = MaterialTheme.typography.bodyLarge, + text = stringResource(R.string.result_authenticated, result.authenticated) + ) + Button(onClick = onComplete) { + Text(text = stringResource(R.string.btn_exit)) + } + } +} + 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..4d15595 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/demo/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + Auth Demo + Authenticated: %b + Exit + \ 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..0172cfc --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/assets/example/implementation/ui/AuthFormScreen.kt @@ -0,0 +1,142 @@ +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.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +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.focus.FocusDirection +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.input.ImeAction +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, +) { + val focusManager = LocalFocusManager.current + + 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, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + keyboardActions = KeyboardActions( + onNext = { focusManager.moveFocus(FocusDirection.Down) } + ) + ) + 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, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions( + onDone = { + focusManager.clearFocus() + if (state.loginEnabled) { + onGesture(AuthGestureImpl.Action) + } + } + ) + ) + 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/architecture.puml b/skills/commonstatemachine/common-child-flow/demo/references/architecture.puml new file mode 100644 index 0000000..4d4db8c --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/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/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..003d262 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/demo/references/fixture-patterns.md @@ -0,0 +1,106 @@ +# Creating fixtures for a feature's domain use-cases + +A demo app or a journey host app must run the feature flow without a real backend, so every domain use-case +interface the feature implementation depends on (see the main guide's domain-use-case tracing step) needs a fake +(fixture) implementation, wired into DI from the host 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. + +## Matching a specific input + +Some scenarios - a journey that types a specific value and expects a specific outcome, or a demo the user wants +to drive by hand with known credentials - need the fixture to branch on what it's actually called with, not just +on a call counter. Compare the argument against a known, fixed value and only succeed for it; throw the real +domain exception for anything else, so the branch genuinely reflects what was entered rather than taking the +same path regardless of input: + +```kotlin +const val VALID_USERNAME = "user" +const val VALID_PASSWORD = "password" + +@Single +fun authenticateWithPassword(): AuthenticateWithPassword { + val tag = AuthenticateWithPassword::class.simpleName!! + return object : AuthenticateWithPassword { + override suspend fun invoke(username: Username, password: String) { + delay(2.seconds) + if (VALID_USERNAME == username.value && VALID_PASSWORD == password) { + Napier.d(tag = tag) { "Emulating success" } + } else { + Napier.d(tag = tag) { "Emulating authentication failure" } + throw AuthenticationException("Invalid credentials") + } + } + } +} +``` + +Keep the expected value as a named constant so anything that needs to refer to it - a journey's typed input, a +person running the app by hand - can use the same value by name instead of guessing it out of the code. + +## 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 the main guide's DI-wiring +step) - don't introduce a new framework just for the host app. With Koin, bind each fixture with a +singleton-scoped function inside the host 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/commonstatemachine/common-child-flow/journeys/SKILL.md b/skills/commonstatemachine/common-child-flow/journeys/SKILL.md new file mode 100644 index 0000000..fc59d0f --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/SKILL.md @@ -0,0 +1,406 @@ +--- +name: commonstatemachine-commonchildflow-journeys +description: Use this skill to set up an Android Journeys (AI-driven, natural-language UI test) suite for an + existing CommonStateMachine Common Child Flow feature module. Use it whenever the user asks to write UI tests, + journey tests, or AI-driven tests for a feature module - for example "Write journey tests for the auth + module", "Set up Journeys to test the friendlist flow end-to-end", or "Create Gemini-driven UI tests for the + payment flow". It builds a small standalone host app fed by fixtures (so the suite runs without a backend), + guesses the feature's actual screens and possible actions from its gestures, UI states and composables, and + proposes journey scenarios for the user to confirm before writing the `.journey.xml` files themselves. This + skill is independent of the demo-app skill - it isn't a prerequisite for it and doesn't build on it - but + follows the same overall approach and reuses the same fixture-writing guide. If the feature module doesn't + exist yet, use the boilerplate skill first - this skill only wires up a way to test one that's already there. +metadata: + author: Motorro + last-updated: '2026-10-06' + keywords: + - android + - architecture + - cross-platform + - kotlin-multiplatform + - mvi + - state-machine + - feature-flow + - testing + - ui-testing + - journeys + - gemini + - ai-testing +--- + +## Description +Use this skill to set up an [Android Journeys](https://developer.android.com/studio/preview/journeys) test +suite for an existing Common Child Flow feature: a small host application module, fed by fixture (fake) domain +use-cases so it runs without a real backend, with natural-language `.journey.xml` files that describe user +flows for Gemini to execute and verify - no view-id locators or Espresso scripts to write or maintain. + +This is a separate skill from the demo-app skill - not a prerequisite for it, and not built on top of it - but +it follows the same overall shape (a small host app with fixtures) and reuses the same fixture-writing guide, so +a project that has both skills will see a family resemblance between a `demo` module and a `journey` module. + +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). +For the Journeys framework's own file format, how to run a suite, and its one-time Gemini authentication setup, +refer to [Journey authoring and running](references/journey-authoring.md). + +## Example +This instruction uses [example authentication module](assets/example), together with its domain module and a +complete example journey module. 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 domain use-cases, with an AGP test suite that runs +natural-language `.journey.xml` files against it: + +``` +my-project # Project folder +├── auth # Existing feature folder - module must already exist +│ ├── api +│ ├── implementation +│ └── journey # New: the journey test host 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 journey module +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 suite for? It must already exist - if it doesn't, use the boilerplate skill + first to create it, then come back here. See Step 1. +- 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 host app instead of + inventing new ones. If the project has no such module, fall back to plain `MaterialTheme` and a `TopAppBar`. +- The feature's actual screens and possible actions: trace them from the feature's own gestures, UI states and + composables rather than assuming - see Step 2. +- Journey scenarios: did the user already describe the scenarios they want tested (a happy path, a specific + validation or failure case), or should you propose some from what Step 2 finds? Either way, confirm the final + list with the user before writing any files - see Step 3. +- 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 5). These are exactly what you need to fake in Step 6. +- 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 5. +- 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 this module. See Step 7. +- 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 9. +- What should happen when the flow completes? Default to showing a short result screen with what the flow + actually produced, the same way the demo skill's host app does - a journey step can then verify the result + text directly instead of only checking that the app closed. See Step 8. + +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 test, 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 (`journey` in the example). Use the Gradle setup common to other application +modules in the project if one exists to copy from. + +## Step 2. Guess the module's UI flow from its gestures, UI states, and composables +A journey step is matched against what Gemini actually sees on screen, so the journey text has to describe the +feature's real screens, fields, labels and conditions - not an abstraction of them. Build that picture before +writing anything, by looking at: + +- The feature's *concrete* UI-state implementation, not the empty marker interface its `api` module declares + (`AuthUiState` in the example is just `interface AuthUiState` - the real shape lives in `implementation`, as + `AuthUiStateImpl.Form`/`.Loading`/`.Error` and similar). Each concrete subtype is one screen; its fields are + what that screen actually shows. +- The feature's *concrete* gesture implementation, the same way (`AuthGestureImpl` rather than the bare + `AuthGesture` marker) - each gesture is one thing the user can do, and which screen's state accepts it tells + you which screen that action belongs to. +- The UI renderer's composables (for example `AuthFormScreen`, `AuthLoadingScreen`, `AuthErrorScreen`, wired + together by `AuthUiRendererImpl`) for the actual button and field labels - read them from the feature's own + string resources, not invented text - and for the conditions that show, hide, enable or disable an element (a + Skip button only when `canSkip`, a Retry label instead of Close only when `canRetry`, a Login button disabled + until `loginEnabled`). + +Put this together into a short map: for each screen, what it shows, what can be done from it, and which gesture +leads to which next screen. Steps 3 and 11 draw their wording directly from this map. + +## Step 3. Suggest and confirm journey scenarios +Using the screen map from Step 2, and the domain use-cases and fixture behavior from Steps 5-6, propose a short +list of scenarios rather than silently picking one - typically: + +- the happy path all the way through to the flow's result screen +- one scenario per client-side condition the UI itself enforces (a field validation message, a button that + stays disabled until input is valid) +- one scenario per domain-level failure a fixture can produce (an error screen, a retry path) - if the user + described a specific scenario for Step 6, that's a scenario here too +- one scenario per branch of a nested feature dependency, if Step 5 found one with more than one outcome + +If the user already gave you their own list of scenarios, use it instead of inventing one, but still sanity-check +each one against the screen map from Step 2 (a scenario that refers to a field or button that doesn't exist +can't be written faithfully). Confirm the final list with the user before Step 11 writes any files - this is a +design decision, not something to guess silently. + +## Step 4. Create the journey module's Gradle file +This is an internal testing 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`, the same reasoning as the demo skill applies here too. + +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 journey module's build file](assets/example/journey/build.gradle.kts) for a concrete +dependency list. + +Then add the AGP test-suite block that actually runs the journeys: + +```kotlin +android { + testOptions { + suites { + create("journeys") { + targets { + create("default") { } + } + useJunitEngine { + inputs += listOf(com.android.build.api.dsl.AgpTestSuiteInputParameters.TESTED_APKS) + includeEngines += listOf("journeys-test-engine") + enginesDependencies(libs.junit.platform.launcher) + enginesDependencies(libs.junit.platform.engine) + enginesDependencies(libs.journeys.junit.engine) + } + targetVariants += listOf("debug") + } + } + } +} +``` + +The suite's name (`journeys` here) doubles as the convention folder name AGP scans for `.journey.xml` files +(`src/journeys/`) - keep them matching, or rename both together. `targetVariants` points the suite at the plain +`debug` build, since this module has no product flavors (see Step 1) - there's only one variant to target. + +Check the project's version catalog (`gradle/libs.versions.toml`) for `journeys-junit-engine`, +`junit-platform-launcher`, and `junit-platform-engine` aliases, and add them if the project doesn't have them +yet (`com.android.tools.journeys:journeys-junit-engine`, `org.junit.platform:junit-platform-launcher`, +`org.junit.platform:junit-platform-engine`). + +## Step 5. 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 this +module must fake, because in a real app it would come from a `usecase` module implementation that this module +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 test + host (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. + +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 6. Create fixtures for the domain use-cases +Follow [this guide](references/fixture-patterns.md) to fake every domain use-case identified in Step 5. + +A journey's fixtures need to produce the exact outcomes its confirmed scenarios (Step 3) check for - and +a scenario that depends on *what was entered* (a specific username and password, say) needs a fixture that +actually branches on its input and matches against a known, fixed value, rather than one that always takes +the same path regardless of what's passed in (see the guide's "Matching a specific input" section). Keep +those expected values as named constants so the journey files from Step 11 can refer to the exact same values. + +## Step 7. 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 this module 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 6. +See [the example `App.kt`](assets/example/journey/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/journey/App.kt) +and [`AppModule.kt`](assets/example/journey/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/journey/AppModule.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 6 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. + +This module never needs the flavor-scoped source-set split the demo skill uses for comparing several variants +(see Step 1) - there's only ever one `AppModule`, in `src/main`. + +## Step 8. 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, wiring the Android back gesture through `navigationBackHandler`. + +Rather than closing the activity the moment the flow's `finish` callback fires, capture the `Result` value it +hands you in a bit of remembered state and swap to a small result screen that shows it, with a button that exits +from there - the same pattern the demo skill uses. This matters even more here than in a demo: it gives a +journey something concrete to verify (the result screen's actual text) instead of only being able to check that +the app closed: + +```kotlin +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + val (result, setResult) = remember { mutableStateOf(null) } + + SkillsTheme { + Scaffold( + topBar = { SkillsAppBar(title = stringResource(R.string.app_name), topLevel = true) } + ) { paddingValues -> + if (null == result) { + MainScreen(modifier = Modifier.padding(paddingValues), onComplete = setResult) + } else { + ResultScreen(result) { finish() } + } + } + } + } + } +} + +@Composable +private fun MainScreen(modifier: Modifier = Modifier, onComplete: (AuthResult) -> Unit) { + val viewModel: MainViewModel = koinViewModel() + val uiApi: AuthUiApi = koinInject() + + CommonFlowComposition( + viewModel = viewModel, + navigationBackHandler = { enabled, onBack -> BackHandler(enabled, onBack) }, + content = { state, onGesture -> uiApi.Screen(state = state, onGesture = onGesture, modifier = modifier) }, + finish = { onComplete(it) } + ) +} + +@Composable +private fun ResultScreen(result: AuthResult, onComplete: () -> Unit) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(Dimensions.medium, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + style = MaterialTheme.typography.bodyLarge, + text = stringResource(R.string.result_authenticated, result.authenticated) + ) + Button(onClick = onComplete) { + Text(text = stringResource(R.string.btn_exit)) + } + } +} +``` + +See [the example `MainActivity.kt`](assets/example/journey/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/journey/MainActivity.kt) +for the full file. Write the result screen's text from the feature's actual `Result` type, and phrase a +"verify the result" journey action (Step 11) against that same text. As with the demo skill, putting this text +in `strings.xml` is just the example's own style, not a rule - a hardcoded string works just as well for an +internal test host. + +## Step 9. 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 testing (for example data that normally +comes from a parent flow), ask the user what to pass, or use a clearly-marked placeholder value. + +## Step 10. 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 test host, not a production app +icon. With `minSdk` set to a recent version (Step 4), 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 module 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 11. Create the journey files +Write one `.journey.xml` file per scenario confirmed in Step 3, under `src/journeys/` (matching the suite name +from Step 4), each with a `name`, a `description`, and an ordered list of `action` steps written as plain, +present-tense sentences - what to wait for, what to type or tap, what to verify - using the exact field labels, +button text and conditions found in Step 2, never internal identifiers: + +```xml + + + Test authentication happy path with valid credentials + + Wait for the authentication form to open. + Type "user" into the username field and "password" into the password field. + Tap the Login button to submit the form. + Verify that authentication succeeds and the screen displays "authenticated: true". + + +``` + +Keep each file to one scenario - a happy path and a validation-error case are two files, not one file with +branches - so a failing run points at one specific thing that broke. See +[the example journey files](assets/example/journey/src/journeys) for the scenarios this walkthrough produces, +and [Journey authoring and running](references/journey-authoring.md) for the full format reference, how to run +a suite locally, and the one-time Gemini authentication setup. + +## Step 12. Register the module (if the project needs it) +If the project registers Gradle modules explicitly (check `settings.gradle.kts`), add the new journey module +there following the existing naming convention. diff --git a/skills/commonstatemachine/common-child-flow/journeys/assets/example/api/AuthDataApi.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/api/AuthDataApi.kt new file mode 100644 index 0000000..e03a962 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/api/AuthGesture.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/api/AuthGesture.kt new file mode 100644 index 0000000..b93d81e --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/api/AuthInput.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/api/AuthInput.kt new file mode 100644 index 0000000..90aba9d --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/api/AuthResult.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/api/AuthResult.kt new file mode 100644 index 0000000..e57a379 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/api/AuthUiApi.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/api/AuthUiApi.kt new file mode 100644 index 0000000..1663448 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/api/AuthUiState.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/api/AuthUiState.kt new file mode 100644 index 0000000..b16324d --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/domain/authenticate/AuthenticateWithPassword.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/domain/authenticate/AuthenticateWithPassword.kt new file mode 100644 index 0000000..1a58d49 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/domain/authenticate/GetPasswordRequirements.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/domain/authenticate/GetPasswordRequirements.kt new file mode 100644 index 0000000..cfc66ff --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/domain/authenticate/data/PasswordRequirements.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/domain/authenticate/data/PasswordRequirements.kt new file mode 100644 index 0000000..01bb73a --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/domain/exception/AppException.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/domain/exception/AppException.kt new file mode 100644 index 0000000..4812838 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/domain/exception/AuthenticationException.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/domain/exception/AuthenticationException.kt new file mode 100644 index 0000000..6bd8c7f --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/domain/exception/IOException.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/domain/exception/IOException.kt new file mode 100644 index 0000000..ab5320d --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/domain/exception/UnknownException.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/domain/exception/UnknownException.kt new file mode 100644 index 0000000..2259626 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/domain/friends/GetFriendList.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/domain/friends/GetFriendList.kt new file mode 100644 index 0000000..1a19efd --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/domain/friends/data/Friend.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/domain/friends/data/Friend.kt new file mode 100644 index 0000000..dae8b90 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/domain/friends/data/FriendData.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/domain/friends/data/FriendData.kt new file mode 100644 index 0000000..da8cb5d --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/domain/session/SessionManager.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/domain/session/SessionManager.kt new file mode 100644 index 0000000..76f8b7b --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/domain/session/data/Session.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/domain/session/data/Session.kt new file mode 100644 index 0000000..29f49fd --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/domain/session/data/Username.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/domain/session/data/Username.kt new file mode 100644 index 0000000..bcd923c --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/implementation/AuthDataApiImpl.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/implementation/AuthDataApiImpl.kt new file mode 100644 index 0000000..aa90a5e --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/implementation/AuthModule.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/implementation/AuthModule.kt new file mode 100644 index 0000000..b646f31 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/implementation/AuthUiApiImpl.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/implementation/AuthUiApiImpl.kt new file mode 100644 index 0000000..a0ccad4 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/implementation/data/AuthGestureImpl.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/implementation/data/AuthGestureImpl.kt new file mode 100644 index 0000000..ba1df8e --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/implementation/data/AuthStateData.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/implementation/data/AuthStateData.kt new file mode 100644 index 0000000..d8cf559 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/implementation/data/AuthUiStateImpl.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/implementation/data/AuthUiStateImpl.kt new file mode 100644 index 0000000..5476939 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/implementation/state/AuthContext.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/implementation/state/AuthContext.kt new file mode 100644 index 0000000..21bc639 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/implementation/state/AuthState.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/implementation/state/AuthState.kt new file mode 100644 index 0000000..bcf46b6 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/implementation/state/AuthStateFactory.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/implementation/state/AuthStateFactory.kt new file mode 100644 index 0000000..5013652 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/implementation/state/AuthStateFactoryImpl.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/implementation/state/AuthStateFactoryImpl.kt new file mode 100644 index 0000000..35be8ff --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/implementation/state/AuthenticatingState.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/implementation/state/AuthenticatingState.kt new file mode 100644 index 0000000..f337087 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/implementation/state/AuthenticationErrorState.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/implementation/state/AuthenticationErrorState.kt new file mode 100644 index 0000000..772a972 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/implementation/state/BaseAuthState.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/implementation/state/BaseAuthState.kt new file mode 100644 index 0000000..267cf74 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/implementation/state/FormState.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/implementation/state/FormState.kt new file mode 100644 index 0000000..c52effb --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/implementation/state/PreloadingErrorState.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/implementation/state/PreloadingErrorState.kt new file mode 100644 index 0000000..61db5fc --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/implementation/state/PreloadingState.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/implementation/state/PreloadingState.kt new file mode 100644 index 0000000..63dd138 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/implementation/ui/AuthErrorScreen.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/implementation/ui/AuthErrorScreen.kt new file mode 100644 index 0000000..fd8c8a1 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/implementation/ui/AuthFormScreen.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/implementation/ui/AuthFormScreen.kt new file mode 100644 index 0000000..0172cfc --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/assets/example/implementation/ui/AuthFormScreen.kt @@ -0,0 +1,142 @@ +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.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +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.focus.FocusDirection +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.input.ImeAction +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, +) { + val focusManager = LocalFocusManager.current + + 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, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + keyboardActions = KeyboardActions( + onNext = { focusManager.moveFocus(FocusDirection.Down) } + ) + ) + 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, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions( + onDone = { + focusManager.clearFocus() + if (state.loginEnabled) { + onGesture(AuthGestureImpl.Action) + } + } + ) + ) + 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/journeys/assets/example/implementation/ui/AuthLoadingScreen.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/implementation/ui/AuthLoadingScreen.kt new file mode 100644 index 0000000..90875c9 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/implementation/ui/AuthScreen.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/implementation/ui/AuthScreen.kt new file mode 100644 index 0000000..f2fbfd9 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/implementation/ui/AuthUiRenderer.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/implementation/ui/AuthUiRenderer.kt new file mode 100644 index 0000000..f117aba --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/implementation/ui/AuthUiRendererImpl.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/implementation/ui/AuthUiRendererImpl.kt new file mode 100644 index 0000000..8f4e0b7 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/assets/example/journey/build.gradle.kts b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/build.gradle.kts new file mode 100644 index 0000000..2d0f253 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/build.gradle.kts @@ -0,0 +1,67 @@ +@file:Suppress("UnstableApiUsage") + +/* +* 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 + } + + testOptions { + suites { + create("journeys") { + targets { + create("default") { + + } + } + useJunitEngine { + inputs += listOf(com.android.build.api.dsl.AgpTestSuiteInputParameters.TESTED_APKS) + includeEngines += listOf("journeys-test-engine") + enginesDependencies(libs.junit.platform.launcher) + enginesDependencies(libs.junit.platform.engine) + enginesDependencies(libs.journeys.junit.engine) + } + targetVariants += listOf("debug") + } + } + } +} + +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/journeys/assets/example/journey/src/journeys/auth.journey.xml b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/journeys/auth.journey.xml new file mode 100644 index 0000000..b9083f0 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/journeys/auth.journey.xml @@ -0,0 +1,10 @@ + + + Test authentication happy path with valid credentials + + Wait for the authentication form to open. + Type "user" into the username field and "password" into the password field. + Tap the Login button to submit the form. + Verify that authentication succeeds and the screen displays "authenticated: true". + + diff --git a/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/journeys/authentication_failure.journey.xml b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/journeys/authentication_failure.journey.xml new file mode 100644 index 0000000..9c3c511 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/journeys/authentication_failure.journey.xml @@ -0,0 +1,12 @@ + + + Test that submitting a wrong password for a known username shows an authentication error, and that closing it returns to the form + + Wait for the authentication form to open. + Type "user" into the username field and "wrongpass" into the password field. Submit the form with the Login button. + Verify that an error screen appears showing the message "Invalid username or password". + Verify that the screen shows a Close button, not a Retry button. + Tap the Close button. + Verify that the authentication form opens. + + diff --git a/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/journeys/invalid_password.journey.xml b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/journeys/invalid_password.journey.xml new file mode 100644 index 0000000..772be8e --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/journeys/invalid_password.journey.xml @@ -0,0 +1,11 @@ + + + Test that login button remains disabled when password does not meet requirements + + Wait for the authentication form to open. + Type "user" into the username field. + Read and verify the password requirements displayed on the screen. + Type some invalid password into the password field. + Verify that the Login submit button is disabled. + + diff --git a/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/main/AndroidManifest.xml b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/main/AndroidManifest.xml new file mode 100644 index 0000000..e347350 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/journey/App.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/journey/App.kt new file mode 100644 index 0000000..de82446 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/journey/App.kt @@ -0,0 +1,25 @@ +package com.motorro.commonstatemachine.examples.skills.auth.journey + +import android.app.Application +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 = [AppModule::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/journeys/assets/example/journey/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/journey/AppModule.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/journey/AppModule.kt new file mode 100644 index 0000000..8833678 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/journey/AppModule.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2026 Nikolai Kotchetkov. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.motorro.commonstatemachine.examples.skills.auth.journey + +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.session.data.Username +import io.github.aakira.napier.Napier +import kotlinx.coroutines.delay +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single +import kotlin.time.Duration.Companion.seconds + +const val USERNAME = "user" +const val PASSWORD = "password" + +/** + * Demo fixtures for Happy Path flavor + */ +@Module(includes = [AuthModule::class]) +@ComponentScan +class AppModule { + + @Single + fun getPasswordRequirements(): GetPasswordRequirements { + val tag = GetPasswordRequirements::class.simpleName!! + return object : GetPasswordRequirements { + override suspend fun invoke(): PasswordRequirements { + delay(2.seconds) + Napier.d(tag = tag) { "Emulating success" } + return PasswordRequirements( + regex = "^.{8,}$".toRegex(), + description = "Minimum eight characters" + ) + } + } + } + + @Single + fun authenticateWithPassword(): AuthenticateWithPassword { + val tag = AuthenticateWithPassword::class.simpleName!! + return object : AuthenticateWithPassword { + override suspend fun invoke(username: Username, password: String) { + delay(2.seconds) + if (USERNAME != username.value || PASSWORD != password) { + Napier.d(tag = tag) { "Emulating failure" } + throw AuthenticationException("Invalid username or password") + } + Napier.d(tag = tag) { "Emulating success" } + } + } + } +} diff --git a/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/journey/MainActivity.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/journey/MainActivity.kt new file mode 100644 index 0000000..fbdc9c0 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/journey/MainActivity.kt @@ -0,0 +1,114 @@ +package com.motorro.commonstatemachine.examples.skills.auth.journey + +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.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +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.Dimensions +import com.motorro.commonstatemachine.examples.auth.appcore.ui.theme.SkillsTheme +import com.motorro.commonstatemachine.examples.skills.auth.api.AuthResult +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 { + + val (result, setResult) = remember { + mutableStateOf(null) + } + + SkillsTheme { + Scaffold( + topBar = { + SkillsAppBar( + title = stringResource(R.string.app_name), + modifier = Modifier.fillMaxWidth(), + topLevel = true + ) + } + ) { paddingValues -> + if (null == result) { + MainScreen( + modifier = Modifier.padding(paddingValues), + onComplete = setResult + ) + } else { + ResultScreen(result) { + finish() + } + } + } + } + } + } +} + +/** + * Feature composition + */ +@Composable +private fun MainScreen(modifier: Modifier = Modifier, onComplete: (AuthResult) -> Unit) { + val viewModel: MainViewModel = koinViewModel() + val uiApi: AuthUiApi = koinInject() + + CommonFlowComposition( + viewModel = viewModel, + navigationBackHandler = { enabled, onBack -> + BackHandler(enabled, onBack) + }, + content = { state, onGesture -> + uiApi.Screen( + state = state, + onGesture = onGesture, + modifier = modifier + ) + }, + finish = { + Napier.i { "Finished with: $it" } + onComplete(it) + } + ) +} + +/** + * Describes the flow result + */ +@Composable +private fun ResultScreen(result: AuthResult, onComplete: () -> Unit) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(Dimensions.medium, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + style = MaterialTheme.typography.bodyLarge, + text = stringResource(R.string.result_authenticated, result.authenticated) + ) + Button(onClick = onComplete) { + Text(text = stringResource(R.string.btn_exit)) + } + } +} diff --git a/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/journey/MainViewModel.kt b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/journey/MainViewModel.kt new file mode 100644 index 0000000..d3df77a --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/main/kotlin/com/motorro/commonstatemachine/examples/skills/auth/journey/MainViewModel.kt @@ -0,0 +1,15 @@ +package com.motorro.commonstatemachine.examples.skills.auth.journey + +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/journeys/assets/example/journey/src/main/res/drawable-v24/ic_launcher_foreground.xml b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..40ce517 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/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/journeys/assets/example/journey/src/main/res/drawable/ic_launcher_background.xml b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..997ef0b --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..0147ca8 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/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/journeys/assets/example/journey/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/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/journeys/assets/example/journey/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/journeys/assets/example/journey/src/main/res/values/strings.xml b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/main/res/values/strings.xml new file mode 100644 index 0000000..4d15595 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + Auth Demo + Authenticated: %b + Exit + \ No newline at end of file diff --git a/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/main/res/xml/backup_rules.xml b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000..fa0f996 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/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/journeys/assets/example/journey/src/main/res/xml/data_extraction_rules.xml b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..9ee9997 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/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/journeys/assets/example/journey/src/main/res/xml/network_security_config.xml b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..c64cff3 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/assets/example/journey/src/main/res/xml/network_security_config.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + 10.0.2.2 + + diff --git a/skills/commonstatemachine/common-child-flow/journeys/references/ARCHITECTURE.md b/skills/commonstatemachine/common-child-flow/journeys/references/ARCHITECTURE.md new file mode 100644 index 0000000..94621a6 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/references/COMMON-CHILD-FLOW.md b/skills/commonstatemachine/common-child-flow/journeys/references/COMMON-CHILD-FLOW.md new file mode 100644 index 0000000..655ed24 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/references/architecture.puml b/skills/commonstatemachine/common-child-flow/journeys/references/architecture.puml new file mode 100644 index 0000000..4d4db8c --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/references/common-child-flow.puml b/skills/commonstatemachine/common-child-flow/journeys/references/common-child-flow.puml new file mode 100644 index 0000000..284a449 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/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/journeys/references/fixture-patterns.md b/skills/commonstatemachine/common-child-flow/journeys/references/fixture-patterns.md new file mode 100644 index 0000000..003d262 --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/references/fixture-patterns.md @@ -0,0 +1,106 @@ +# Creating fixtures for a feature's domain use-cases + +A demo app or a journey host app must run the feature flow without a real backend, so every domain use-case +interface the feature implementation depends on (see the main guide's domain-use-case tracing step) needs a fake +(fixture) implementation, wired into DI from the host 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. + +## Matching a specific input + +Some scenarios - a journey that types a specific value and expects a specific outcome, or a demo the user wants +to drive by hand with known credentials - need the fixture to branch on what it's actually called with, not just +on a call counter. Compare the argument against a known, fixed value and only succeed for it; throw the real +domain exception for anything else, so the branch genuinely reflects what was entered rather than taking the +same path regardless of input: + +```kotlin +const val VALID_USERNAME = "user" +const val VALID_PASSWORD = "password" + +@Single +fun authenticateWithPassword(): AuthenticateWithPassword { + val tag = AuthenticateWithPassword::class.simpleName!! + return object : AuthenticateWithPassword { + override suspend fun invoke(username: Username, password: String) { + delay(2.seconds) + if (VALID_USERNAME == username.value && VALID_PASSWORD == password) { + Napier.d(tag = tag) { "Emulating success" } + } else { + Napier.d(tag = tag) { "Emulating authentication failure" } + throw AuthenticationException("Invalid credentials") + } + } + } +} +``` + +Keep the expected value as a named constant so anything that needs to refer to it - a journey's typed input, a +person running the app by hand - can use the same value by name instead of guessing it out of the code. + +## 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 the main guide's DI-wiring +step) - don't introduce a new framework just for the host app. With Koin, bind each fixture with a +singleton-scoped function inside the host 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/commonstatemachine/common-child-flow/journeys/references/journey-authoring.md b/skills/commonstatemachine/common-child-flow/journeys/references/journey-authoring.md new file mode 100644 index 0000000..a0fa3ff --- /dev/null +++ b/skills/commonstatemachine/common-child-flow/journeys/references/journey-authoring.md @@ -0,0 +1,76 @@ +# Writing and running Android Journeys + +[Android Journeys](https://developer.android.com/studio/preview/journeys) (`com.android.tools.journeys:journeys-junit-engine`) +is an AI-driven UI-testing framework: instead of view-id locators or Espresso scripts, a journey file describes +what to do and check in plain-language steps. At each step the test runner captures a screenshot and the current +UI hierarchy, sends both to Gemini along with your instruction, and Gemini decides and performs the matching +action (tap, type, swipe, or an assertion) until the step's condition is met. + +This is what makes the UI-flow guessing step in the main guide matter: because a step is matched by Gemini +against what's actually rendered, the journey's wording has to use the screen's real labels and real +conditions (the exact button text, the exact field label, whether a control is enabled) - not invented ones or +internal identifiers. + +## Journey file format + +A `.journey.xml` file has a `name`, a `description`, and an ordered list of `action` steps written as complete +sentences, in present tense, one user-observable action or check per step: + +```xml + + + Test authentication happy path with valid credentials + + Wait for the authentication form to open. + Type "user" into the username field and "password" into the password field. + Tap the Login button to submit the form. + Verify that authentication succeeds and the screen displays "authenticated: true". + + +``` + +Keep each file to a single scenario - a happy path and a validation-error case are two files, not one file with +branches - so a failure points at one specific thing that broke. + +## Where the files live, and the suite that finds them + +AGP scans for `.journey.xml` files inside `src//`, where `` is literally the name given to +the `testOptions.suites.create(...)` block in the module's `build.gradle.kts` (see the main guide's Gradle-setup +step). This skill names the suite `journeys`, so the files live under `src/journeys/` - if that name ever +changes, the folder has to move with it. + +## Running a suite + +```bash +./gradlew :path:to:the:journey:module:journeysDefault +``` + +(`journeysDefault` because the suite is named `journeys` and its target is named `default` - adjust both parts +to match whatever this project's module and suite are actually called.) To run a single file instead of the +whole suite: + +```bash +JOURNEYS_FILTER=happy_path.journey.xml ./gradlew :path:to:the:journey:module:journeysDefault +``` + +## One-time setup: Gemini authentication + +The `journeys-junit-engine` runner talks to Gemini via Google Application Default Credentials, not an API key +in the project. This is a one-time environment setup, not something this skill's generated files need to carry: + +- **Android Studio**: Settings/Preferences -> Tools -> Gemini -> sign in, and enable Journeys under Studio Labs + if it's not already on. +- **Local terminal / CLI**: `gcloud auth application-default login` once per machine. +- **CI**: a service account with the Vertex AI User role, authenticated via `GOOGLE_APPLICATION_CREDENTIALS` + or impersonation - see the project's own CI setup for the exact mechanism in use. + +## Two gotchas worth knowing before a run + +- **Emulator soft keyboard**: emulators treat the host's keyboard as physical hardware by default, which + suppresses the on-screen keyboard. If Gemini expects to see and dismiss a soft keyboard that never appears, + typing and keyboard-dismissal steps can misfire. Fix with + `adb shell settings put secure show_ime_with_hard_keyboard 1`, or permanently in the AVD's `config.ini` + (`hw.keyboard = no`). +- **Cost**: each action step sends a screenshot and UI-hierarchy payload to Gemini - roughly 1,500-2,500 tokens + per step, so a 10-step journey is on the order of 20,000 tokens. Prefer specific, unambiguous action wording + (fewer retries) and `JOURNEYS_FILTER` for iterating on one file instead of re-running the whole suite. 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/consumer-rules.pro b/tmap/consumer-rules.pro new file mode 100644 index 0000000..e69de29 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