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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ build/

# Properties
local.properties
local.env
settings.xml

# Idea
Expand Down
21 changes: 12 additions & 9 deletions Writerside/cfg/buildprofiles.xml
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE buildprofiles SYSTEM "https://resources.jetbrains.com/writerside/1.0/build-profiles.dtd">
<buildprofiles xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://resources.jetbrains.com/writerside/1.0/build-profiles.xsd">

<build-profile instance="modo-docs">
<variables>
<noindex-content>false</noindex-content>
<product-web-url>https://github.com/ikarenkov/Modo</product-web-url>
<!-- Search Engine Indexing -->
<noindex-content>false</noindex-content>

<enable-browser-edits>true</enable-browser-edits>
<browser-edits-url>https://github.com/ikarenkov/Modo/blob/dev/Writerside/</browser-edits-url>
<!-- Product URLs -->
<product-web-url>https://github.com/ikarenkov/Modo</product-web-url>

<showDownloadButton>true</showDownloadButton>
<download-title>GitHub</download-title>
<download-page>%github_url%</download-page>
</variables>
<!-- Browser Edits (Edit on GitHub) -->
<enable-browser-edits>true</enable-browser-edits>
<browser-edits-url>https://github.com/ikarenkov/Modo/edit/dev/Writerside/</browser-edits-url>

<showDownloadButton>true</showDownloadButton>
<download-title>GitHub</download-title>
<download-page>%github_url%</download-page>
</build-profile>

</buildprofiles>
285 changes: 229 additions & 56 deletions Writerside/topics/Lifecycle.md
Original file line number Diff line number Diff line change
@@ -1,90 +1,263 @@
# Lifecycle

<show-structure for="chapter,procedure" depth="2"/>

This article covers the lifetime of screen instances and their integration with the Android Lifecycle.

## Screen Instance Lifecycle

The lifetime of a screen instance is guaranteed to match the application (process) lifetime when you integrate Modo using built-in functions such as
`Modo.rememberRootScreen`. Regardless of how many times a screen is recomposed, or whether the activity and/or fragment is recreated, the screen
instance remains consistent. This allows you to safely inject the screen instance into your DI container.
Screen instances in Modo have a long lifetime:

## Android Lifecycle Integration
- Screen instances live as long as your app process (not tied to Activity or Fragment lifecycle)
- The same instance survives recomposition and configuration changes (rotation, language change, etc.)
- Safe to inject into your DI container if its lifetime is shorter or equal to the screen

Modo provides seamless [integration](%github_code_url%/modo-compose/src/main/java/com/github/terrakok/modo/android/ModoScreenAndroidAdapter.kt)
with a Android Lifecycle for your screens.
This is guaranteed when using `Modo.rememberRootScreen()` and similar built-in functions.

You can use `LocalLifecycleOwner` inside `Screen.Content` to access the lifecycle of a screen. This will return the nearest screen's lifecycle owner.
## Lifecycle Basics

Modo provides seamless [integration](%github_code_url%/modo-compose/src/main/java/com/github/terrakok/modo/android/ModoScreenAndroidAdapter.kt)
with Android Lifecycle. Each screen gets its own `LifecycleOwner` that can be retrieved by using `LocalLifecycleOwner`:

```kotlin
class SampleScreen : Screen {
override fun Content(modifier: Modifier) {
val lifecycleOwner = LocalLifecycleOwner.current
// Use lifecycleOwner to observe lifecycle events
// Access via LocalLifecycleOwner (standard Compose API)
val lifecycleOwner = LocalLifecycleOwner.current
val lifecycleState by lifecycleOwner.lifecycle.currentStateAsState()

// Observe lifecycle events
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> {
// Screen is ready for interactions
}
Lifecycle.Event.ON_PAUSE -> {
// Screen is hiding
}
else -> {}
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}
```

### Lifecycle States and Events
{ collapsible="true" collapsed-title="How to access lifecycle in your screen" }

Screen lifecycle is controlled by three main factors:

- **Composition state**: Lifecycle responds to entering/leaving composition
- **Parent lifecycle**: Child state never exceeds parent state and can be updated by a parent (see
[Parent-Child Lifecycle Coordination](#parent-child-lifecycle-coordination))
- **Transitions**: Screen can be resumed only after animations are complete (see [Transition Lifecycle Control](#transition-lifecycle-control))

Each screen progresses through the standard Android Lifecycle states:

<table>
<tr>
<td><b>State</b></td>
<td><b>Meaning</b></td>
</tr>
<tr>
<td>INITIALIZED</td>
<td>The screen is constructed (instance created) but has never been displayed.</td>
</tr>
<tr>
<td>CREATED</td>
<td>The screen was displayed at least once.</td>
</tr>
<tr>
<td>STARTED</td>
<td>The screen is in composition.</td>
</tr>
<tr>
<td>RESUMED</td>
<td>Ready for user interaction. The screen is STARTED, visible, and all transitions are complete.</td>
</tr>
<tr>
<td>DESTROYED</td>
<td>The screen is removed from the navigation graph, and all resources are cleaned up.</td>
</tr>
</table>

Screens move through states sequentially. Each transition happens when something changes in the screen's lifecycle:

```mermaid
sequenceDiagram
participant DESTROYED
participant INITIALIZED
participant CREATED
participant STARTED
participant RESUMED

INITIALIZED->>CREATED: First composition
CREATED->>STARTED: Entering composition
STARTED->>RESUMED: Show animation completes<br/>(immediate if no animation)
RESUMED->>STARTED: Hide transition starts
STARTED->>CREATED: Leaving composition
CREATED->>DESTROYED: Removed from navigation
```

> **Note**: Parent lifecycle can propagate events and limit child states.
> See [Parent-Child Lifecycle Coordination](#parent-child-lifecycle-coordination) for details.

## Parent-Child Lifecycle Coordination

The lifecycle of parent and child screens follows strict rules to ensure consistency:

### Rules

**Rule**: A child's lifecycle state never exceeds its parent's state.

```
Parent: RESUMED → Child can reach: RESUMED
Parent: STARTED → Child can reach: STARTED (blocked from RESUMED)
Parent: CREATED → Child can reach: CREATED (blocked from STARTED)
```

**Event Propagation from Parent**:

> **Note**: When a screen enters composition, it subscribes to its parent's lifecycle. Events are propagated from parent to child while the
> subscription is active (screen is in composition).

| **Parent Event** | **Propagation Behavior** |
|------------------|------------------------------------------------------------------------------------------------------------|
| `ON_CREATE` | Never propagated (child subscribes after its own creation) |
| `ON_START` | Always propagated → Child moves to STARTED |
| `ON_RESUME` | Propagated but gated by transitions and activation → Child moves to RESUMED only if all conditions are met |
| `ON_PAUSE` | Always propagated → Child immediately moves to STARTED |
| `ON_STOP` | Always propagated → Child immediately moves to CREATED |
| `ON_DESTROY` | Conditionally propagated (blocked during config changes to preserve SavedStateRegistry) |

### Examples

#### Parent with transition, child without transition { collapsible="true" }

```mermaid
sequenceDiagram
participant Parent
participant Child

Note over Parent: CREATED
Note over Parent: Entering composition
Note over Parent: STARTED
Note over Parent: Animating...
Parent->>Child: Child enters composition
Note over Child: CREATED → STARTED
Note over Child: Waiting for parent...
Note over Parent: Animation completes
Note over Parent: RESUMED
Parent->>Child: Parent resumed
Note over Child: RESUMED
```

Even though the child has no animation, it waits at STARTED until the parent reaches RESUMED.

Here’s an overview of lifecycle states and their meanings in the context of the screen lifecycle:
#### Screen Rotation (Configuration Change) { collapsible="true" }

| **State** | **Meaning** |
|-----------------|-----------------------------------------------------------------------------------------------------------------------------|
| **INITIALIZED** | The screen is constructed (instance created) but has never been displayed. |
| **CREATED** | The screen is in the navigation hierarchy and can be reached from the `RootScreen` integrated with an Activity or Fragment. |
| **STARTED** | `Screen.Content` is in composition. |
| **RESUMED** | The screen is **STARTED**, and there are no unfinished transitions for this screen or its parent. |
| **DESTROYED** | The screen is removed from the navigation graph. |
```mermaid
sequenceDiagram
participant Activity
participant StackScreen
participant Screen

> `ON_CREATE` and `ON_DESTROY` are dispatched once per screen instance.
Note over Activity,Screen: All RESUMED

### Screen Transitions and Lifecycle
Note right of Activity: Rotation starts
Note over Activity: STARTED
Activity->>StackScreen: ON_PAUSE
Note over StackScreen: STARTED
StackScreen->>Screen: ON_PAUSE
Note over Screen: STARTED

Modo provides a convenient way to track when screen transitions start and finish. These events are tied to the `ON_RESUME` and `ON_PAUSE` lifecycle
events. Here’s a summary:
Note over Activity: CREATED
Activity->>StackScreen: ON_STOP
Note over StackScreen: CREATED
StackScreen->>Screen: ON_STOP
Note over Screen: CREATED

| **Event** | **With Transition** | **Without Transition** |
|---------------|------------------------------------------------------------------------------------------------|---------------------------------------------|
| **ON_RESUME** | Dispatched when there are no unfinished transitions, and the parent is in the `RESUMED` state. | Dispatched when the parent is in `RESUMED`. |
| **ON_PAUSE** | Dispatched when a hiding transition starts. | Dispatched immediately before `ON_STOP`. |
Note over Activity: DESTROYED
Note right of Activity: ON_DESTROY NOT propagated<br/>(screens preserved)

### Parent-Child Lifecycle Propagation
Note right of Activity: Activity recreated
Note over Activity: CREATED

The lifecycle of parent and child screens follows a set of rules, ensuring consistency and predictability:
Note over Activity: STARTED
Activity->>StackScreen: ON_START
Note over StackScreen: STARTED
StackScreen->>Screen: ON_START
Note over Screen: STARTED

Note over Activity: RESUMED
Activity->>StackScreen: ON_RESUME
Note over StackScreen: RESUMED
StackScreen->>Screen: ON_RESUME
Note over Screen: RESUMED
```

1. A screen's `Lifecycle.State` is always less than or equal to (`<=`) its parent's state.
2. A child screen is not moved to the `RESUMED` state until its parent is also in the `RESUMED` state.
3. When a screen's lifecycle state is downgraded, its child screens are also moved to the same state.
4. When a screen reaches the `RESUMED` state and its child screens are ready to resume, the children's lifecycles are also moved to `RESUMED`.
During configuration changes, screens are preserved (not destroyed) and reattach to the new Activity instance.

### Practical Example: Keyboard Management
## Transition Lifecycle Control

A practical use case for these lifecycle events is managing the keyboard. For example, you can show and hide the keyboard using `ON_RESUME` and
`ON_PAUSE` events:
Screens animated by `ScreenTransition` cannot reach RESUMED state until the animation completes:

* `ON_RESUME` indicates that the screen is ready for user input (transitions are finished)
* `ON_PAUSE` indicates that the screen is not ready for user input (transitions are starting)
- **Without transition**: STARTED → RESUMED (immediate)
- **With transition**: STARTED → (animation playing) → RESUMED (after animation completes)

This indirectly affects nested screens because of [parent-child lifecycle propagation](#parent-child-lifecycle-coordination).

## Practical Examples

### Managing Keyboard

Show/hide keyboard based on screen visibility:

```kotlin
val lifecycleOwner = LocalLifecycleOwner.current
val keyboardController = LocalSoftwareKeyboardController.current
DisposableEffect(this) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> {
focusRequester.requestFocus()
}
Lifecycle.Event.ON_PAUSE -> {
focusRequester.freeFocus()
keyboardController?.hide()
@Composable
fun LoginScreenContent(modifier: Modifier) {
val lifecycleOwner = LocalLifecycleOwner.current
val keyboardController = LocalSoftwareKeyboardController.current
val focusRequester = remember { FocusRequester() }

DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> {
// Screen is fully visible, focus input and show keyboard
focusRequester.requestFocus()
}
Lifecycle.Event.ON_PAUSE -> {
// Screen is hiding, clear focus and hide keyboard
focusRequester.freeFocus()
keyboardController?.hide()
}
else -> {}
}
else -> {}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}

TextField(
value = text,
onValueChange = { text = it },
modifier = Modifier.focusRequester(focusRequester)
)
}
```

## Debugging Screens Lifecycle

Enable logging to see lifecycle events:

```kotlin
ModoDevOptions.onScreenPreDisposeListener = { screen ->
Log.d("Modo", "Screen pre-dispose: ${screen.screenKey}")
}

ModoDevOptions.onScreenDisposeListener = { screen ->
Log.d("Modo", "Screen disposed: ${screen.screenKey}")
}
TextField(text, setText, modifier = Modifier.focusRequester(focusRequester))
```
12 changes: 6 additions & 6 deletions Writerside/v.list
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE vars SYSTEM "https://resources.jetbrains.com/writerside/1.0/vars.dtd">
<vars>
<var name="product" value="Modo" />
<var name="latest_version" value="0.9.0-rc1" />
<var name="latest_modo_compose_dependency" value="com.github.terrakok:modo-compose:%latest_version%" />
<var name="github_code_url" value="https://github.com/ikarenkov/Modo/tree/dev/" />
<var name="github_url" value="https://github.com/ikarenkov/Modo/" />
<var name="ikarenkov_github_url" value="https://github.com/ikarenkov" />
<var name="product" value="Modo"/>
<var name="latest_version" value="0.11.0-rc2"/>
<var name="latest_modo_compose_dependency" value="com.github.terrakok:modo-compose:%latest_version%"/>
<var name="github_code_url" value="https://github.com/ikarenkov/Modo/tree/dev/"/>
<var name="github_url" value="https://github.com/ikarenkov/Modo/"/>
<var name="ikarenkov_github_url" value="https://github.com/ikarenkov"/>
</vars>
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ detekt-composeRules = { module = "io.nlopez.compose.rules:detekt", version.ref =
detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detektVersion" }
leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanaryAndroid" }
test-junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version = "5.10.1" }
test-androidx-arch-core = { group = "androidx.arch.core", name = "core-testing", version = "2.2.0" }

debug-logcat = { group = "com.squareup.logcat", name = "logcat", version = "0.1" }

Expand Down
Loading
Loading