Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions docs/architecture/android.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Architecture — Android
# Android architecture

## Components

Expand All @@ -8,9 +8,9 @@ Arkitekt provides two ViewModel base classes for the Android / Compose path.

`BaseCoreViewModel<VS : ViewState>` from the **core** module is an abstract `ViewModel` that gives you:

- `viewState: VS` — the screen's view state instance
- `events: Flow<Event<VS>>` — a channel-backed flow of one-shot events
- `sendEvent(event)` — sends an event to the UI
- `viewState: VS`: the screen's view state instance
- `events: Flow<Event<VS>>`: a channel-backed flow of one-shot events
- `sendEvent(event)`: sends an event to the UI

Use this when you do **not** need to execute use cases.

Expand Down Expand Up @@ -44,10 +44,10 @@ class HomeViewModel @Inject constructor(

`CoroutineScopeOwner` (from `cr-usecases`) is implemented by `BaseViewModel` and provides:

- `useCaseScope` — the scope for executing use cases, backed by `viewModelScope`
- `getWorkerDispatcher()` — returns `Dispatchers.IO` by default; override for testing
- `launchWithHandler {}` — launches a coroutine with try-catch that calls `defaultErrorHandler` and logs to `UseCaseErrorHandler.globalOnErrorLogger`
- `defaultErrorHandler(exception)` — by default rethrows the exception; override to customize error handling
- `useCaseScope`: the scope for executing use cases, backed by `viewModelScope`
- `getWorkerDispatcher()`: returns `Dispatchers.IO` by default; override for testing
- `launchWithHandler {}`: launches a coroutine with try-catch that calls `defaultErrorHandler` and logs to `UseCaseErrorHandler.globalOnErrorLogger`
- `defaultErrorHandler(exception)`: by default rethrows the exception; override to customize error handling

### Obtaining the ViewModel in Compose

Expand All @@ -61,7 +61,7 @@ fun HomeScreen(
}
```

## State Management
## State management

### ViewState

Expand Down Expand Up @@ -100,7 +100,7 @@ class FormViewState @Inject constructor() : ViewState {
}
```

### Observing State in Compose
### Observing state in Compose

Use Compose state delegation to observe ViewState fields:

Expand All @@ -118,7 +118,7 @@ fun HomeScreen(viewModel: HomeViewModel = hiltViewModel()) {
}
```

### StateFlow Alternative
### StateFlow alternative

If you prefer `StateFlow` over Compose `mutableStateOf`, you can expose a `StateFlow` from the ViewModel and collect it in the Composable with `collectAsState()`:

Expand All @@ -135,7 +135,7 @@ val title by viewModel.viewState.title.collectAsState()

Events are one-shot messages sent from a ViewModel to a Composable. They are backed by a `Channel`, which guarantees single delivery even during screen rotation.

### Defining Events
### Defining events

Define events as a sealed class extending `Event<VS>`:

Expand All @@ -146,15 +146,15 @@ sealed class HomeEvent : Event<HomeViewState>() {
}
```

### Sending Events
### Sending events

Send an event from the ViewModel:

```kotlin
sendEvent(ShowDetailEvent)
```

### Collecting Events in Compose
### Collecting events in Compose

Use `EventsEffect` to collect events in a Composable:

Expand All @@ -170,7 +170,7 @@ fun HomeScreen(viewModel: HomeViewModel = hiltViewModel()) {

`EventsEffect` is an extension function on `BaseCoreViewModel` from the **compose** module. It launches a coroutine that collects events for the lifetime of the Composable.

`onEvent<E>` is a type-safe filter — it checks whether the received event is of type `E` and executes the lambda only when it matches.
`onEvent<E>` is a type-safe filter: it checks whether the received event is of type `E` and executes the lambda only when it matches.

## Navigation

Expand Down
42 changes: 21 additions & 21 deletions docs/architecture/kmp/components.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Components — KMP
# Components (KMP)

The `decompose` module provides the building blocks for KMP presentation logic, built on top of the [Decompose](https://arkivanov.github.io/Decompose/) library.

Expand All @@ -8,17 +8,17 @@ The `decompose` module provides the building blocks for KMP presentation logic,

Constructor parameters:

- `componentContext: GenericComponentContext<*>` — Decompose component context
- `defaultState: VS` — initial state value
- `lifecycleScope: CoroutineScope` — scope tied to the component lifecycle (defaults to `MainScope()`; inject your own scope in tests)
- `componentContext: GenericComponentContext<*>` is the Decompose component context
- `defaultState: VS` is the initial state value
- `lifecycleScope: CoroutineScope` is the scope tied to the component lifecycle (defaults to `MainScope()`; inject your own scope in tests)

Key members:

- `componentState: MutableStateFlow<VS>` — protected mutable state
- `lifecycleScope: CoroutineScope` — public, open; cancelled automatically when the component is destroyed
- `events: Flow<E>` — single-subscriber flow of one-shot UI events backed by a buffered `Channel`
- `sendUiEvent(event: E)` — protected function to emit an event
- `fun Flow<VS>.asStateFlow(started): StateFlow<VS>` — protected helper to convert a `Flow` to a `StateFlow` within the component scope
- `componentState: MutableStateFlow<VS>` is the protected mutable state
- `lifecycleScope: CoroutineScope` is public and open; it is cancelled automatically when the component is destroyed
- `events: Flow<E>` is a single-subscriber flow of one-shot UI events backed by a buffered `Channel`
- `sendUiEvent(event: E)` is a protected function to emit an event
- `fun Flow<VS>.asStateFlow(started): StateFlow<VS>` is a protected helper to convert a `Flow` to a `StateFlow` within the component scope

### Events semantics

Expand All @@ -38,7 +38,7 @@ init {
}
```

When the lifecycle is **destroyed**, `lifecycleScope` is cancelled and the `events` channel is closed — any subsequent `sendUiEvent` calls are no-ops.
When the lifecycle is **destroyed**, `lifecycleScope` is cancelled and the `events` channel is closed. Any subsequent `sendUiEvent` calls are no-ops.

## ArkitektComponentContext

Expand All @@ -47,7 +47,7 @@ When the lifecycle is **destroyed**, `lifecycleScope` is cancelled and the `even
The recommended pattern is to define an `AppComponentContext` interface in your project that self-references the type parameter, and a `DefaultAppComponentContext` implementation that delegates to a standard Decompose `ComponentContext`:

```kotlin
// commonMain — define once per project
// commonMain: define once per project
interface AppComponentContext : ArkitektComponentContext<AppComponentContext>

class DefaultAppComponentContext(componentContext: ComponentContext) :
Expand All @@ -65,12 +65,12 @@ class DefaultAppComponentContext(componentContext: ComponentContext) :
}
```

### Creating the Root Component
### Creating the root component

On Android, Arkitekt is designed to be used with Decompose's [`retainedComponent`](https://arkivanov.github.io/Decompose/component/instance-retaining/#retained-components) to create the root component. This retains the entire component tree across configuration changes, similar to AndroidX `ViewModel`:

```kotlin
// Android Activity — onCreate
// Android Activity, onCreate
val rootComponent = retainedComponent { componentContext ->
RootNavHostComponent(DefaultAppComponentContext(componentContext))
}
Expand All @@ -79,7 +79,7 @@ val rootComponent = retainedComponent { componentContext ->
!!! note
`retainedComponent` is an Android-specific extension on `ComponentActivity`. It should be called once in `onCreate`. On iOS, create a `DefaultComponentContext` with the application lifecycle and pass it directly.

Child components always receive their `AppComponentContext` from the parent — they never construct it themselves. Decompose creates a scoped child context automatically when you call `childStack`, `childSlot`, or similar APIs.
Child components always receive their `AppComponentContext` from the parent; they never construct it themselves. Decompose creates a scoped child context automatically when you call `childStack`, `childSlot`, or similar APIs.

## AppComponent

Expand All @@ -93,15 +93,15 @@ abstract class AppComponent<VS : Any, E : Any>(
AppComponentContext by componentContext
```

Because `AppComponent` delegates `AppComponentContext`, all context services — `lifecycle`, `stateKeeper`, `instanceKeeper`, `backHandler` — are directly accessible on every component without going through `componentContext`. This is the recommended approach for both screen components and nav-host components.
Because `AppComponent` delegates `AppComponentContext`, all context services (`lifecycle`, `stateKeeper`, `instanceKeeper`, `backHandler`) are directly accessible on every component without going through `componentContext`. This is the recommended approach for both screen components and nav-host components.

This pattern also makes it easy to integrate with use cases: implement `CoroutineScopeOwner` directly on `AppComponent` to make use case execution available in every component by default — see [Use Cases](../../use-cases/overview.md).
This pattern also makes it easy to integrate with use cases: implement `CoroutineScopeOwner` directly on `AppComponent` to make use case execution available in every component by default. See [Use Cases](../../use-cases/overview.md).

## Koin Factory Generation
## Koin factory generation

Use the `@GenerateFactory` annotation to generate Koin dependency injection factories for your components. See the [Factory Generator](factory-generator.md) page for full details and KSP configuration.

## Complete Example
## Complete example

```kotlin
@GenerateFactory
Expand Down Expand Up @@ -133,11 +133,11 @@ class HomeComponent(
```

!!! note
`lifecycle.doOnStart` is the recommended place to trigger work that should run each time the component becomes active. The `lifecycle` property is available implicitly because `AppComponent` delegates `AppComponentContext`. This also makes components easy to unit test — you can control the lifecycle externally and verify behavior at each stage.
`lifecycle.doOnStart` is the recommended place to trigger work that should run each time the component becomes active. The `lifecycle` property is available implicitly because `AppComponent` delegates `AppComponentContext`. This also makes components easy to unit test: you can control the lifecycle externally and verify behavior at each stage.

!!! note
`BaseComponent` does **not** implement `CoroutineScopeOwner` directly. To execute use cases, implement `CoroutineScopeOwner` in your component and set `useCaseScope = lifecycleScope`.

## Example Application
## Example application

For a full KMP application using all the recommended patterns described in this documentation, see the [KMP Futured Template](https://github.com/futuredapp/kmp-futured-template) repository.
For a full KMP application that uses the patterns described in this documentation, see the [KMP Futured Template](https://github.com/futuredapp/kmp-futured-template) repository.
8 changes: 4 additions & 4 deletions docs/architecture/kmp/events.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Events — KMP
# Events (KMP)

Events are one-shot messages sent from a Component to the UI layer.

## Defining Events
## Defining events

Define events as a sealed interface extending `UiEvent`:

Expand All @@ -13,15 +13,15 @@ sealed interface HomeUiEvent : UiEvent {
}
```

## Sending Events
## Sending events

Send an event from the Component:

```kotlin
sendUiEvent(HomeUiEvent.ShowToast)
```

## Collecting Events in Compose
## Collecting events in Compose

Use `EventsEffect` from the **decompose** module:

Expand Down
10 changes: 5 additions & 5 deletions docs/architecture/kmp/factory-generator.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Factory Generator (Koin)
# Factory generator (Koin)

The `@GenerateFactory` annotation triggers KSP code generation to create [Koin](https://insert-koin.io/) dependency injection factory objects for Decompose components. This eliminates the need to manually wire Koin dependencies when creating components in navigation factories.

Expand All @@ -15,7 +15,7 @@ class HomeComponent(
) : BaseComponent<HomeState, HomeUiEvent>(componentContext, HomeState())
```

## Generated Output
## Generated output

The annotation processor generates an `internal object` factory that implements `KoinComponent`:

Expand All @@ -32,7 +32,7 @@ internal object HomeComponentFactory : KoinComponent {
```

- Parameters marked with `@InjectedParam` become `createComponent()` parameters and are forwarded to Koin via `parametersOf`
- The component itself is resolved from Koin — it must be registered in your Koin module (typically as a `factory`)
- The component itself is resolved from Koin, so it must be registered in your Koin module (typically as a `factory`)
- All non-`@InjectedParam` constructor dependencies (e.g. `SomeUseCase`) are resolved by Koin from the DI graph

```kotlin
Expand All @@ -48,7 +48,7 @@ val homeModule = module {
}
```

## Calling the Factory
## Calling the factory

Use the generated factory inside a nav-host's `childStack` child factory:

Expand All @@ -68,7 +68,7 @@ val stack = childStack(

`ctx` is the child `AppComponentContext` provided by Decompose; `navigation` is the nav-host's navigation instance passed through as an `@InjectedParam`.

## KSP Configuration for KMP
## KSP configuration for KMP

!!! warning "Important"
Use `kspCommonMainMetadata` only. Do **not** add the processor to platform-specific configurations (`kspAndroid`, `kspIosArm64`, etc.) as this would cause duplicate generation.
Expand Down
24 changes: 12 additions & 12 deletions docs/architecture/kmp/navigation-advanced.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Navigation — Advanced
# Advanced navigation

This page covers advanced navigation patterns for the KMP / Decompose path. For the basics, see [Navigation — KMP](navigation.md).
This page covers advanced navigation patterns for the KMP / Decompose path. For the basics, see [Navigation (KMP)](navigation.md).

## Consolidated Navigation
## Consolidated navigation

For nav-hosts with many screens, we recommend consolidating all navigation logic into a dedicated class. This keeps the nav-host component decluttered and navigation logic easy to find.

Expand Down Expand Up @@ -65,13 +65,13 @@ internal class HomeNavHostComponent(
```

!!! tip
This pattern is not enforced by the library — for simple nav-hosts with one or two screens, inline anonymous objects (as shown in the [Parent Component](navigation.md#parent-component) section) work fine. The consolidated approach pays off as the number of screens and cross-screen navigation grows.
This pattern is not enforced by the library. For simple nav-hosts with one or two screens, inline anonymous objects (as shown in the [Parent component](navigation.md#parent-component) section) work fine. The consolidated approach pays off as the number of screens and cross-screen navigation grows.

## Passing Results Between Screens
## Passing results between screens

Use `ResultFlow<T>` to send a value from a child screen back to its parent. The parent creates the flow, passes it in the navigation config, and collects results. The child calls `sendResult()` when it has a value to return.

Because `ResultFlow` is a `Flow`, it must be declared `@Serializable` in the config using `ResultFlowSerializer`. On deserialization it is recreated as an empty flow — the parent always holds the live instance.
Because `ResultFlow` is a `Flow`, it must be declared `@Serializable` in the config using `ResultFlowSerializer`. On deserialization it is recreated as an empty flow, and the parent always holds the live instance.

```kotlin
// Navigation config
Expand All @@ -91,17 +91,17 @@ private fun openPicker() {

// In the child (picker) component
fun onItemSelected(item: String) {
resultFlow.sendResult(item) // suspending — call from a coroutine
resultFlow.sendResult(item) // suspending; call from a coroutine
navigation.back()
}
```

## ResultFlow API Reference
## ResultFlow API reference

`ResultFlow<T>` extends `Flow<T>` and adds the ability to send values back from a child destination to a parent.

- Backed by `MutableSharedFlow` internally
- Serializable — recreated as an empty flow during deserialization, making it safe for use in navigation configurations
- Serializable: recreated as an empty flow during deserialization, which makes it safe to use in navigation configurations
- Provides `suspend fun sendResult(item: T)` for emitting results

### Creating a ResultFlow
Expand All @@ -110,9 +110,9 @@ fun onItemSelected(item: String) {
val resultFlow = ResultFlow<String>()
```

### Serialization in Navigation Configs
### Serialization in navigation configs

Navigation configurations must be `@Serializable`. Use `ResultFlowSerializer` to annotate `ResultFlow` properties inside a config — it serializes as a no-op and recreates an empty flow on deserialization. The parent always holds the live instance, so the child never needs to reconstruct the flow.
Navigation configurations must be `@Serializable`. Use `ResultFlowSerializer` to annotate `ResultFlow` properties inside a config. It serializes as a no-op and recreates an empty flow on deserialization. The parent always holds the live instance, so the child never needs to reconstruct the flow.

```kotlin
@Serializable
Expand All @@ -121,7 +121,7 @@ data class PickerConfig(
)
```

### Collecting Results in a NavHost
### Collecting results in a NavHost

Create the flow in the parent nav-host, start collecting immediately using `lifecycleScope`, then push the config:

Expand Down
Loading
Loading