Mastering Screen Lifecycle in Jetpack Compose Navigation

When developing with Jetpack Compose, a common challenge is detecting when a screen becomes visible (to refresh data) or when it moves to the background (to pause a video).

While Compose provides onDispose, this only triggers when a Composable is completely removed from the UI tree. It cannot detect when a screen is still in the backstack but no longer visible to the user.

In this post, we’ll explore how to leverage the fact that Navigation Compose uses NavBackStackEntry as a LifecycleOwner to perfectly manage screen-level events.

 

🧑🏻‍💻 1. The Core: Who is the LocalLifecycleOwner?

In Compose, you can access the current lifecycle via LocalLifecycleOwner.current. However, its identity changes depending on your app's architecture:

  • Directly under an Activity: LifecycleOwner = Activity
  • Using Navigation Compose: LifecycleOwner = NavBackStackEntry

When using Navigation Compose, each destination is wrapped in a NavBackStackEntry. When you navigate from Screen A to Screen B, the Activity remains RESUMED, but Screen A’s NavBackStackEntry transitions to the STOPPED state.

By monitoring this, you can capture lifecycle events specific to that individual screen.

 

🧑🏻‍💻 2. The Solution: Implementing ScreenLifecycleObserver

To make this reusable, we can create a custom Composable function that observes these state changes safely.

 

🧑🏻‍💻 3. Real-World Patterns

A. Refreshing Data on Screen Return

Using LaunchedEffect(Unit) only runs once when the screen is first created. If you want to refresh data every time a user navigates back to the screen, use ON_RESUME.

B. Pausing and Resuming Video

Automatically pause video when the user navigates away or minimizes the app, and resume it when they return.

C. Tracking Screen Time (Analytics)

Start a timer on ON_RESUME and send the duration on ON_PAUSE.

 

🧑🏻‍💻 Summary: When to Use What?

Finally, let's distinguish between onDispose and Lifecycle events:

If you are using Navigation Compose, LocalLifecycleOwner.current is a powerful tool. Using it correctly ensures a robust app that respects system resources and provides a seamless user experience.

I hope this guide helps you manage screen lifecycles in your Compose projects!


Embracing AGP 9 and JDK 21: A Gradual Path to Android Build Optimization

As Android developers, major AGP (Android Gradle Plugin) updates are always significant. AGP 9 in particular promises a stricter, faster build environment, moving away from ambiguous configurations.

Instead of waiting for its full release and then scrambling to fix issues, why not start preparing your project now, gradually aligning it with "AGP 9 standards" through your gradle.properties file?

 

🤔 Why JDK 21 and AGP 9 Now? (The Ultimate Synergy)

When transitioning to AGP 9, updating to JDK 21 isn't just a "requirement"; it's a powerful "booster" that dramatically enhances your development experience.

  • Performance Synchronization: JDK 21's improved resource management, including features like Virtual Threads, allows Gradle to fully leverage its parallel build capabilities, leading to more stable and efficient builds.
  • Language Specification Alignment: By targeting Java 21, you bridge potential gaps in type inference and bytecode generation in mixed Java/Kotlin projects, especially as Kotlin 2.x gains traction.
  • Precision R8 Optimization: AGP 9 is optimized to parse and transform class files generated by JDK 21. This means that even with stricter settings, R8 can more accurately understand modern code structures, reducing the need for excessive keep rules while safely shrinking code.

This combination offers the kind of seamless experience you get from pairing the latest OS with the latest CPU.

 

🤔 Prepare with gradle.properties: 10 Flags to Enable Today

The strategy is simple: enable one flag at a time, fix any errors that arise, and then move to the next. This iterative approach is the most reliable way to prepare for AGP 9.

1. Structure Enforcement (Clean Up Your Project)

  • android.uniquePackageNames=true: Prevents duplicate package names across modules, eliminating resource conflicts.
  • android.usesSdkInManifest.disallowed=true: Enforces placing minSdk, targetSdk, etc., in build.gradle instead of AndroidManifest.xml.
  • android.defaults.buildfeatures.resvalues=true: Explicitly controls the generation of resValue entries.

2. Build Speed Enhancements

  • android.enableAppCompileTimeRClass=true: Uses lightweight R classes during app compilation, significantly improving build times for large projects.
  • android.sdk.defaultTargetSdkToCompileSdkIfUnset=true: Automatically sets targetSdk to compileSdk if unspecified, preventing inconsistent behavior.
  • android.dependency.useConstraints=true: Utilizes Gradle's "Constraints" feature for dependency resolution, making library version management more robust.

3. Aggressive R8 / Optimization Settings (The Biggest Hurdle)

  • android.r8.strictFullModeForKeepRules=true: Enables R8's Full Mode. This maximizes optimization but requires precise keep rules for code that relies on reflection, potentially leading to crashes if not handled correctly.
  • android.r8.optimizedResourceShrinking=true: Employs a more advanced algorithm for removing unused resources, leading to smaller app sizes.

4. Next-Gen Defaults

  • android.builtInKotlin=true: Prioritizes AGP's built-in Kotlin support.
  • android.newDsl=false: Use this to maintain the current DSL while preparing for future changes.

 

🤔 Conclusion: One Flag at a Time for a Smoother Future

The AGP 9 update is akin to a major cleanup. Attempting it all at once can be overwhelming, but tackling it gradually makes it incredibly rewarding.

Why not start with android.uniquePackageNames=true? With each flag you enable, your project will move closer to a more modern, robust, and efficient build environment.

👉 AGP 9.0 移行ガイド:新旧コード比較で見るモダンビルド設定


Kotlin・KSP・Compose Compiler を安全に更新する Renovate 設定術(developブランチ運用編)

Jetpack Compose Compiler が Kotlin に強く依存していた時代を経て、
今では少しずつ依存関係が緩やかになってきました。
それでも Kotlin・KSP・Compose Compiler の3つは依然として密接に関係しており、
バージョンのズレひとつでビルドが崩壊するリスクがあります。

この記事では、develop ブランチをメインに運用しつつ、
それらを安全かつ一貫性を保って更新するための Renovate 設定を紹介します。

 

🧩 Kotlin・KSP・Compose Compiler の三位一体更新

Compose Compiler は Kotlin コンパイラと深く結びついて動作するため、
Kotlin のメジャーアップデートが入ると、それに対応した Compose Compiler が必要になります。

さらに、KSP(Kotlin Symbol Processing)も Kotlin バージョンに追随するため、
この3つは基本的に「セットで更新する」のが鉄則です。


{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": ["config:base"],
  "baseBranches": ["develop"],
  "packageRules": [
    {
      "groupName": "Kotlin, KSP and Compose Compiler",
      "groupSlug": "kotlin",
      "matchPackagePrefixes": [
        "com.google.devtools.ksp",
        "org.jetbrains.compose.compiler"
      ],
      "matchPackagePatterns": [
        "org.jetbrains.kotlin.*"
      ]
    },
    {
      "description": "Do not automerge without CI",
      "matchUpdateTypes": ["minor", "patch", "digest"],
      "automerge": false
    }
  ]
}

 

⚙️ 設定の意図を読み解く

この設定は、単に自動更新を行うだけでなく、
Kotlin 界隈の依存を安全に、かつチームの開発フローに合わせて管理することを意識しています。


baseBranches: ["develop"]

Renovate のデフォルトは main や master に対して PR を作りますが、
実際の開発フローでは「開発用ブランチ(develop)」に更新を入れたいケースが多いですよね。

"baseBranches": ["develop"] を指定しておくことで、
更新 PR が常に develop ブランチに向けて作成されるようになります。

本番リリース前にテストや検証を挟める安全設計です。

 

🔗 groupName: 三つ巴のアップデートを1つにまとめる

groupName は、関連する依存をひとまとめにするためのグループ名。
ここでは "Kotlin, KSP and Compose Compiler" としており、
3つのパッケージを同時に1つの PR にまとめてくれます。


"matchPackagePrefixes": [
  "com.google.devtools.ksp",
  "org.jetbrains.compose.compiler"
],
"matchPackagePatterns": [
  "org.jetbrains.kotlin.*"
]

この指定で次のような依存が同時更新対象になります:


- Kotlin (org.jetbrains.kotlin)

- KSP (com.google.devtools.ksp)

- Compose Compiler (org.jetbrains.compose.compiler)

以前は androidx.compose.compiler でしたが、
現在の Compose Multiplatform では org.jetbrains.compose.compiler に移行しているため、この設定がより正確です。

 

🚫 automerge: false の哲学

Renovate には更新を自動マージする機能がありますが、
Kotlin 系の更新はそれに向きません。

理由は単純で、CI でのビルド確認が欠かせないからです。


{
  "description": "Do not automerge without CI",
  "matchUpdateTypes": ["minor", "patch", "digest"],
  "automerge": false
}

「CI による確認を通らない限り、自動マージさせない」というルールです。

特に Kotlin の minor アップデートでは内部 API が変わることもあり、
Compose Compiler や KSP が対応していない可能性があります。

PR を作成したあと CI を通し、問題なければ手動でマージする。

これが最も安全な流れです。

 

🧭 運用のヒントとまとめ

この設定は、いわば「Kotlin エコシステム用 Renovate セーフティモード」。

自動化の恩恵を受けつつも、壊れやすい依存を慎重に扱うための現実的な妥協点です。

項目 意図
baseBranches 更新PRをdevelop向けにして安全確認を確保
groupName Kotlin / KSP / Compose Compiler を同時に更新
automerge: false CI確認なしでの自動マージを防止
matchPackagePrefixes / matchPackagePatterns 最新の Compose 構成(Compose Multiplatform 等)に対応

 

🪶 まとめ:安全第一の Renovate 運用へ

Renovate は「ただの自動更新ボット」ではなく、
チームのアップデート戦略をコード化できるツールです。

Kotlin、KSP、Compose Compiler のような密接な関係を持つ依存こそ、
グルーピングとマージ制御で慎重に扱うべき対象。

develop ベースで CI を通すこの設定は、
自動化と安全性のバランスを取る最適解のひとつと言えます。

👉 Renovate Docs


【JetpackCompose】 Composable 関数の Modifier について知らなかった3つのルール

【JetpackCompose】 Composable 関数の Modifier について知らなかった流儀

detekt に指摘されながら、ドキュメントを読む。


こんなコードがあったとして。


Column {
    InnerContent()
}

@Composable
private fun InnerContent() {
    Text(...)
    Image(...)
    Button(...)
}

以下、知らなかった流儀。

 

🧩 Composable 関数はレイアウトを一つだけ出力する

A composable function should emit either 0 or 1 pieces of layout, but no more. A composable function should be cohesive, and not rely on what function it is called from.

コンポーザブル関数は、レイアウトのピースを0個または1個だけ発行するべきで、それ以上は発行してはいけません。コンポーザブル関数は結束性を持ち、呼び出される関数に依存すべきではありません。

レイアウトのネストのコストはあまり気にしなくて良い、とのこと。

Nesting of layouts has a drastically lower cost vs the view system, so developers should not try to minimize UI layers at the cost of correctness.

レイアウトのネストには、ビューシステムに比べてはるかに低いコストがかかるため、UIの階層を正確性の犠牲にして最小化しようとしないべきです。

👉 Do not emit multiple pieces of content - Twitter Jetpack Compose Rules hatena-bookmark

ということで、一見不要に見える Column を追加する。


@Composable
private fun InnerContent() {
    Column {
        Text(...)
        Image(...)
        Button(...)
    }
}

 

🧩 デフォルトを持つパラメータ modifier: Nodifier = Modifier は必須

They are especially important for your public components, as they allow callers to customize the component to their wishes.

特に、公開コンポーネントにとって Modifier は非常に重要であり、呼び出し元がコンポーネントを希望に合わせてカスタマイズできるようにします。

👉 When should I expose modifier parameters? - Jetpack Compose Rules hatena-bookmark

Composables that accept a Modifier as a parameter to be applied to the whole component represented by the composable function should name the parameter modifier and assign the parameter a default value of Modifier.

Composable 関数内でコンポーザブル関数を表すコンポーネント全体に適用するための修飾子をパラメータとして受け入れる場合、そのパラメータは "modifier" という名前を付け、パラメータに Modifier のデフォルト値を割り当てるべきです。

👉 Modifiers should have default parameters - Jetpack Compose Rules hatena-bookmark

ということで、親からの Modifier を受け入れるようにします。デフォルト値もつけておきます。


@Composable
private fun InnerContent(modifier: Modifier = Modifier) {
    Column {
        Text(...)
        Image(...)
        Button(...)
    }
}

 

🧩 受け取った Modifier パラメータは最上位のレイアウトにのみ適用する

Modifiers should be applied once as a first modifier in the chain to the root-most layout in the component implementation. Since modifiers aim to modify the external behaviors and appearance of the component, they must be applied to the top-most layout and be the first modifiers in the hierarchy. It is allowed to chain other modifiers to the modifier passed as a param if needed.

Modifier は、コンポーネントの実装内でルートのレイアウトに最初の Modifier として一度適用すべきです。Modifier はコンポーネントの外部の動作や外観を変更することを目的としているため、最上位のレイアウトに適用し、階層内で最初の Modifier である必要があります。必要に応じて、パラメータとして渡された Modifier に他の Modifier を連鎖させることは許可されています。

👉 Modifiers should be used at the top-most layout of the component - Jetpack Compose Rules hatena-bookmark

ということで、最上位ルートの Column で適用します。


@Composable
private fun InnerContent(modifier: Modifier = Modifier) {
    Column(modifier = modifier) {
        Text(...)
        Image(...)
        Button(...)
    }
}

 

🧩 まとめ

@Composable 内のレイアウトに関する Modifier は、親 (呼び出し元) から持ってきて、最上位のレイアウトで一度だけ適用する。

detekt を使うことで、なんとなく記述していた部分がスッキリしてきます。

👉 【AndroidStudio】detekt で JetpackCompose 記述をチェックする hatena-bookmark


【detekt / ktlint】元 Twitter 管理者 @mrmans0n らによって Jetpack Compose Rules フォークが進化している件

そもそも Twitter Jetpack Compose Rules が話題になったのは2、3年前、

その後の commits が過疎とは思っていましたが。

【detekt / ktlint】元 Twitter 管理者 @mrmans0n らによって Jetpack Compose Rules フォークが進化している件
👉 twitter/compose-rules: Static checks to aid with a healthy adoption of Compose hatena-bookmark

issues を見ていると以下のようなコメンツ。

Since both @mrmans0n and @chrisbanes left Twitter (sorry guys 😢), the future doesn't look so bright. At least not under Twitter's scope. Maybe this project can find a new home at Google, for instance?

👉 The future of this project · Issue #112 · twitter/compose-rules hatena-bookmark

I started my fork in https://github.com/mrmans0n/compose-rules

First order of business was supporting ktlint 0.48.x and kotlin 1.8.x. I submitted them as a PR in this repo too, to ease with transition, but I doubt anybody is looking on the Twitter side (all the admins of this repo don't work at the company any longer, and our permissions were removed).

I will work towards a maven central publication for my fork as soon as possible.

👉 The future of this project · Issue #112 · twitter/compose-rules hatena-bookmark

しれっと、フォークが元の管理者によって進化を続けています。

【detekt / ktlint】元 Twitter 管理者 @mrmans0n らによって Jetpack Compose Rules フォークが進化している件

Note
This repository is a fork of the Twitter Jetpack Compose Rules by its original maintainer. As none of the admins/maintainers continue working at the company, its development will continue here from now on. If you come from that project, check out the migration guide.

注意
このリポジトリは、元の管理者による Twitter Jetpack Compose Rules のフォークです。会社の管理者/保守者がもはや働いていないため、今後はこちらで開発が続行されます。もしあなたがそのプロジェクトから来た場合、移行ガイドをチェックしてください。

👉 mrmans0n/compose-rules: Static checks to aid with a healthy adoption of Compose. Maintained fork of the Twitter Compose rules. hatena-bookmark

使ってみると以下のような新しいルールで怒られる。


dependencies {
    detektPlugins "io.nlopez.compose.rules:detekt:<VERSION>"
}

The main Modifier of a @Composable should be applied once as a first modifier in the chain to the root-most layout in the component implementation.

You should move the modifier usage to the appropriate parent Composable.

See https://mrmans0n.github.io/compose-rules/rules/#modifiers-should-be-used-at-the-top-most-layout-of-the-component for more information. [ModifierNotUsedAtRoot]

@Composable のメインの Modifier は、コンポーネントの実装内の最もルートに配置されたレイアウトに対して最初の Modifier として一度だけ適用すべきです。Modifier の使用を適切な親の Composable に移動すべきです。

という感じで、Twitter オリジナル版では見つからなかった Modifier 使いまわし部分の違反が見つかります。

良さげです。

変更された rule をざっくり config.yml で確認する。

【detekt / ktlint】元 Twitter 管理者 @mrmans0n らによって Jetpack Compose Rules フォークが進化している件
👉 Comparing da89b71..027a205 · mrmans0n/compose-rules hatena-bookmark

👉 compose-rules/rules/detekt/src/main/resources/config/config.yml at main · mrmans0n/compose-rules hatena-bookmark
👉 compose-rules/rules/detekt/src/main/resources/config/config.yml at main · twitter/compose-rules hatena-bookmark

新しく追加されたルールを見てみます、

👉 compose-rules/docs/rules.md at main · mrmans0n/compose-rules hatena-bookmark

 

🧑‍💻 DefaultsVisibility - ComponentDefaults object should match the composable visibility

ComponentDefaults object should match the composable visibility
If your composable has an associated Defaults object to contain its default values, this object should have the same visibility as the composable itself. This will allow consumers to be able to interact or build upon the original intended defaults, as opposed to having to maintain their own set of defaults by copy-pasting.

ComponentDefaults オブジェクトはコンポーザブルの可視性に合わせるべきです。
コンポーザブルに関連付けられたデフォルト値を保持する Defaults オブジェクトがある場合、このオブジェクトはコンポーザブル自体と同じ可視性を持つべきです。これにより、コンシューマーは元々意図されたデフォルト値に対して対話したり、それを拡張したりすることができ、コピーアンドペーストで独自のデフォルト値セットを保守する必要がなくなります。

👉 compose-rules/docs/rules.md at main · mrmans0n/compose-rules hatena-bookmark

 

🧑‍💻 ModifierClickableOrder - Modifier order matters

Modifier order matters
The order of modifier functions is very important. Each function makes changes to the Modifierreturned by the previous function, the sequence affects the final result. Let's see an example of this:

修飾子の順序が重要です
修飾子関数の順序は非常に重要です。各関数は前の関数によって返された Modifier に変更を加え、その順序が最終的な結果に影響します。これについて例を見てみましょう:


@Composable
fun MyCard(modifier: Modifier = Modifier) {
    Column(
        modifier
            // Tapping on it does a ripple, the ripple is bound incorrectly to the composable
            .clickable { /* TODO */ }
            // Create rounded corners
            .clip(shape = RoundedCornerShape(8.dp))
            // Background with rounded corners
            .background(color = backgroundColor, shape = RoundedCornerShape(8.dp))
    ) {
        // rest of the implementation
    }
}

The entire area, including the clipped area and the clipped background, responds to clicks. This means that the ripple will fill it all, even the areas that we wanted to trim from the shape.

We can address this by simply reordering the modifiers.

切り抜かれた領域と切り抜かれた背景を含む、全体のエリアがクリックに反応します。つまり、リップルは、形状からトリミングしたい領域も含めて、全体を満たします。

この問題は、修飾子を単に再順序化することで解決できます。


@Composable
fun MyCard(modifier: Modifier = Modifier) {
    Column(
        modifier
            // Create rounded corners
            .clip(shape = RoundedCornerShape(8.dp))
            // Background with rounded corners
            .background(color = backgroundColor, shape = RoundedCornerShape(8.dp))
            // Tapping on it does a ripple, the ripple is bound incorrectly to the composable
            .clickable { /* TODO */ }
    ) {
        // rest of the implementation
    }
}

👉 compose-rules/docs/rules.md at main · mrmans0n/compose-rules hatena-bookmark

 

🧑‍💻 ModifierNaming - Naming modifiers properly

Naming modifiers properly
Composables that accept a Modifier as a parameter to be applied to the whole component represented by the composable function should name the parameter modifier.

In cases where Composables accept modifiers to be applied to a specific subcomponent should name the parameter xModifier (e.g. fooModifier for a Foo subcomponent) and follow the same guidelines above for default values and behavior.

適切な修飾子の命名
修飾子を受け入れるコンポーザブルは、そのコンポーザブル関数によって表されるコンポーネント全体に適用されるためのパラメータに modifier という名前を付けるべきです。

コンポーザブルが特定のサブコンポーネントに適用する修飾子を受け入れる場合、そのパラメータには xModifier(たとえば Foo サブコンポーネントの場合は fooModifier)という名前を付け、デフォルト値と動作に関して上記のガイドラインに従うべきです。

👉 compose-rules/docs/rules.md at main · mrmans0n/compose-rules hatena-bookmark

 

🧑‍💻 ModifierNotUsedAtRoot - Modifiers should be used at the top-most layout of the component

Modifiers should be used at the top-most layout of the component
Modifiers should be applied once as a first modifier in the chain to the root-most layout in the component implementation. Since modifiers aim to modify the external behaviors and appearance of the component, they must be applied to the top-most layout and be the first modifiers in the hierarchy. It is allowed to chain other modifiers to the modifier passed as a param if needed.

Modifier はコンポーネントの最上位レイアウトで使用するべきです
Modifier はコンポーネントの外部の動作との外観を変更することを目的としているため、最上位のレイアウトに適用され、階層の最初の Modifier に配置される必要があります。必要であれば、他の Modifier パラメーターとして、渡された Modifier に連鎖させることは許容されています。

👉 compose-rules/docs/rules.md at main · mrmans0n/compose-rules hatena-bookmark

 

🧑‍💻 PreviewAnnotationNaming - Naming multipreview annotations properly

Properly Naming Multipreview Annotations
Multipreview annotations should be named using "Previews" as a prefix. These annotations must be explicitly named to ensure they are easily distinguishable as an alternative to the @Preview when used.

適切な名前付け:マルチプレビューアノテーション
マルチプレビューアノテーションは、接頭辞として「Previews」を使用して命名すべきです。これらのアノテーションは、使用時に @Preview の代替として明確に識別できるように、明示的に名前をつける必要があります。

👉 compose-rules/docs/rules.md at main · mrmans0n/compose-rules hatena-bookmark

 

🧑‍💻 まとめ

どこまで管理が続くかは心配ですが、とりあえず今は良い感じです。

👉 【AndroidStudio】detekt で JetpackCompose 記述をチェックする hatena-bookmark
👉 【Gradle Plugin】detekt「baseline」とは hatena-bookmark