【Jetpack Compose】@Composable のライフサイクルをシンプルに使うには DisposableEffect

なんか、いろんな方法があるんだなあ、と。

👉 Lifecycle を Compose と統合する  |  App architecture  |  Android Developers

👉 ComposeでLifecycleを監視する(2023年9月バージョン) - Kenji Abe - Medium

いろいろ試しながらどこから使っていくのがいいか。を考える。

 

🤔 参考になるコードたち

調べてみると、

DisposableEffect を中心に考えるのがいいだろう、

と思える。


@Composable
fun LifecycleEffect(
  onCreate: () -> Unit = { },
  onStart: () -> Unit = { },
  onResume: () -> Unit = { },
  onPause: () -> Unit = { },
  onStop: () -> Unit = { },
  onDestroy: () -> Unit = { },
  onAny: () -> Unit = { }
) {
  val lifecycleOwner = LocalLifecycleOwner.current
  DisposableEffect(lifecycleOwner) {
    val observer = LifecycleEventObserver { _, event ->
      when (event) {
        Event.ON_CREATE -> onCreate()
        Event.ON_START -> onStart()
        Event.ON_RESUME -> onResume()
        Event.ON_PAUSE -> onPause()
        Event.ON_STOP -> onStop()
        Event.ON_DESTROY -> onDestroy()
        Event.ON_ANY -> onAny()
      }
    }
    lifecycleOwner.lifecycle.addObserver(observer)
    onDispose {
      lifecycleOwner.lifecycle.removeObserver(observer)
    }
  }
}

👉 PatchNote-Android/app/src/main/java/com/easyhz/patchnote/core/common/util/Lifecycle.kt at e24ff1d32831b54177348e079412c95b17e3f0f4 · easyhz/PatchNote-Android


@Composable
fun LifecycleEffect(
  lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
  onStart: (suspend () -> Unit)? = null,
  onPause: (suspend () -> Unit)? = null,
  onStop: (suspend () -> Unit)? = null
) {
  val scope = rememberCoroutineScope()
  val currentOnStart by rememberUpdatedState(onStart)
  val currentOnPause by rememberUpdatedState(onPause)
  val currentOnStop by rememberUpdatedState(onStop)

  DisposableEffect(lifecycleOwner) {
    val observer = LifecycleEventObserver { _, event ->
      scope.launch {
        when (event) {
          Lifecycle.Event.ON_START -> currentOnStart?.invoke()
          Lifecycle.Event.ON_PAUSE -> currentOnPause?.invoke()
          Lifecycle.Event.ON_STOP -> currentOnStop?.invoke()
          else -> {}
        }
      }
    }
    lifecycleOwner.lifecycle.addObserver(observer)

    onDispose {
      lifecycleOwner.lifecycle.removeObserver(observer)
    }
  }
}

👉 itunes/app/src/main/java/com/simgesengun/itunes/ui/launchedEffect/LifecycleEffect.kt at cc63faa4eb78f22a7e2e8b95c78e1ca30f4f11ad · simgesengun/itunes


@Immutable
enum class LifecycleEvent {
  OnStop
}

@Immutable
data class LifecycleHandler(
  val id: String,
  val event: LifecycleEvent,
  val onEvent: () -> Unit
)

@Composable
fun <R : Any> DestinationScope<R>.LifecycleEffect(
  event: LifecycleEvent,
  onEvent: () -> Unit
) {
  val id = remember { randomUUID() }
  DisposableEffect(this, id, event, onEvent) {
    navigator.update {
      it.copy(lifecycleHandlers = it.lifecycleHandlers + LifecycleHandler(
        id = id,
        event = event,
        onEvent = onEvent
      ))
    }
    onDispose {
      navigator.update { dest ->
        dest.copy(lifecycleHandlers = dest.lifecycleHandlers.filter { it.id != id })
      }
    }
  }
}

👉 showcase/common/src/iosMain/kotlin/dev/ahmedmourad/showcase/common/navigation/LifecycleHandler.kt at 83cc58c29acc2dc3bbcc4de0644e79e74826514e · AhmedMourad0/showcase

 

🤔 まとめ

まずは、ここらから記述していくのがいいのではないか。


@Composable
@NonRestartableComposable
fun DisposableLifecycleEffect(
  lifecycle: Lifecycle = LocalLifecycleOwner.current.lifecycle,
  onResume: () -> Unit,
  onPause: () -> Unit,
) {
  DisposableEffect(lifecycle) {
    val observer = LifecycleEventObserver { _, event ->
      when (event) {
        Lifecycle.Event.ON_RESUME -> onResume()
        Lifecycle.Event.ON_PAUSE -> onPause()
        else -> { }
      }
    }
    lifecycle.addObserver(observer)
    onDispose {
      lifecycle.removeObserver(observer)
    }
  }
}

👉 florisboard/app/src/main/kotlin/dev/patrickgold/florisboard/lib/compose/DisposableLifecycleEffect.kt at 35fd70ce6d2de05cf8be44fcd9a16b3b6d537453 · florisboard/florisboard

記述位置は Screen。

@NonRestartableComposable の意味を考えながら書く。

フォアグラウンド、バックグラウンドの切り分けにも使えるか。


Android Studio キャッシュの種類とそれぞれのクリーンの操作

ビルド速度が遅いので、


org.gradle.caching=true

としたら、署名付きビルドが更新されない。

「キャッシュ」のせいなのは分かるが、

いろいろ種類がありそうだよな、

Android Studio の「キャッシュ」って。

 

🤔 キャッシュの種類

ChatGPT に聞くと、3つあるそうです。

もちろん信用はしてないです。

IDE キャッシュ
コード補完、シンタックスハイライト、プロジェクトインデックス、ビルド設定など

Gradle キャッシュ
依存ライブラリ、ビルドキャッシュ(Gradleがダウンロードしたファイルなど)

ビルドキャッシュ
build/ フォルダ内の成果物やコンパイル済みファイルなど

そして、今回の調べるきっかけとなった


org.gradle.caching=true

は「ビルドキャッシュ」のようです。

 

🤔 まとめ

いきなり、分かったことをまとめます。


org.gradle.caching=true

は、ビルドキャッシュ。

キャッシュの範囲は、


ビルドキャッシュ < Gradle キャッシュ < IDE キャッシュ

のようなので、

キャッシュを消したいときは、


[Build] - [Clean Project]


[Build] - [Rebuild Project]


./gradlew clean


rm -rf ~/.gradle/caches


[File] → [Invalidate Caches / Restart]


rm -rf ~/.gradle

を上から順番に試していくのがいいと思います。

もちろん信用はしてません。

公式読むと良いです。

👉 Build Cache - docs.gradle.org

 

🧑🏻‍💻 おまけ

今回のような場合、

Gradle タスクに追加しておく方法もあるらしい。


tasks.withType(Sign).configureEach {
    outputs.cacheIf { false }
}

記述場所としては、

基本的に、android {} の外、末尾に記述する。

マルチモジュールなら subprojects {} 内に。


Jetpack Compose 「Modifierは子1つ目だけに適用する」の考え方

実際のコード例で考えてみる。


Column {
  Text(
    text = "Hello",
    modifier = Modifier.padding(16.dp)
  )
  Text(
    text = "World",
    modifier = Modifier.padding(16.dp)
  )
}


val modifier = Modifier.padding(16.dp)

Column {
  Text(text = "Hello", modifier = modifier)
  Text(text = "World", modifier = modifier)
}


Column(
  modifier = Modifier.padding(16.dp)
) {
  Text(text = "Hello")
  Text(text = "World")
}


@Composable
fun ParentLayout() {
  Column(
    modifier = Modifier.verticalScroll(rememberScrollState())
  ) {
    for (i in 1..10) {
      ListItem(
        text = "Item $i",
        modifier = Modifier.padding(8.dp)
      )
    }
  }
}

@Composable
fun ListItem(modifier: Modifier, text: String) {
  Text(
    text = text,
    modifier = modifier.background(Color.LightGray)
  )
}

共有することの意味は「統一性」だろうけども、

見通しが悪くなるので「1つ目まで」としているのだろう。

「まずは個別につけてから共通部分をホイストしていく。」

「親から渡すときは子まで影響。孫は個別に。」

そんな考え方の順番が簡単で自然だと思います。


Jetpack Compose から SwiftUI に来ましたが今の謎をどうにかしたい 😩

Swift 初心者です。Kotlin からきました。

Apple 公式サンプルコードを3日間 ROM ってました。

👉 sample-backyard-birds/Multiplatform/Birds/BirdsSearchResults.swift at main · apple/sample-backyard-birds hatena-bookmark

どうも納得ができないので書き換えてみました。

動かしてみると3つとも特に問題ないような感じに見えました。

ネットで調べていると、どうもこちらも変化が激しいようで、どの記事を信じたらいいのか分かりません。

コードを見比べながら分からないこと、今後調べたいこと、を洗い出してみます。

 

😩 init()

ここで必要なのですか。なくても引数は同じ。


init(searchText: Binding<String>, @ViewBuilder content: @escaping (Bird) -> Content) {

パフォーマンス的な何か、なのでしょうか。

 

😩 KeyPath


_birds = Query(sort: \.creationDate)

すべて、Xcode 任せなのですが、省略できないんです。Bird が。

「Path」というぐらいなのでどこかに通せばいいと思っているのですが。

 

😩 Property Wrapper

いきなりでてくる _(アンダースコア)付きのこれ ROM 勢としては驚きました。

どこにもないのにいきなり登場してくる。

どこかに何か隠れてますか。


struct BirdsSearchResults<Content: View>: View {
  @Binding var searchText: String

  init(searchText: Binding<String>, ...) {
    _searchText = searchText

以下すべて挙動が同じに見えます。


struct BirdsSearchResults<Content: View>: View {
  @Binding var searchText: String


struct BirdsSearchResults<Content: View>: View {
  private var searchText: String

  init(searchText: Binding<String>, ...) {
    self.searchText = searchText.wrappedValue


struct BirdsSearchResults<Content: View>: View {
  private var searchText: Binding<String>

  init(searchText: Binding<String>, ...) {
    self.searchText = searchText

  var body: some View {
    let searchText = self.searchText.wrappedValue

ここの部分。


_searchText = searchText
_birds = Query(sort: \.creationDate)

以下記事で勉強したのですが。

👉 SwiftUI Property WrappersクラスのwrappedValue・projectedValue一覧表 #Swift - Qiita hatena-bookmark

隠しているものを丸出しに露出させることなどできるのでしょうか。

 

😩 $0

この記述よく見かけます。


birds.filter {
  $0.speciesName.contains(

分かりづらい感じがしますが、なぜ具体的なものに置き換えないのでしょうか。

Kotlin でも同様な記述があり、よく怒られていました。

 

😩 まとめ

対象の Apple サンプルコードは WWDC2023 のものなので約1年前ぐらいで、そんなに古くはないと思っています。

どうかどうかよろしくおねがいします。



【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