Jetpack ComposeのComposable関数の引数に別のモジュールのクラスを使うときの注意点


さて、Composeは引数に同じオブジェクトを渡したときにいい感じにComposable関数をスキップしてくれます。
しかしそれには条件があります。

appモジュールと modelモジュールがあって、
appモジュールにはComposeが適応されていてComposable関数があって、
modelモジュールはシンプルにArticleクラスがあるとしましょう。

このうちちゃんと同じオブジェクトを渡したときにスキップしてくれるのはなんとComposable2のみです。

app module ------------> model module
fun Composable1()        class Articles
fun Composable2()        class Article
fun Composable3()                                         

app module

@Composable
fun Composable1(articles: Articles) {
  Text(text = "Hello $articles!")
}

@Stable
data class UiModel(val articles: Articles)

@Composable
fun Composable2(uiModel: UiModel) {
  Text(text = "Hello $uiModel!")
  uiModel.articles.articles.forEach {
    Composable3(it)
  }
}

@Composable
fun Composable3(article: Article) {
  Text(text = "Hello $article!")
}

model module

data class Articles(val articles: List<Article>)
data class Article(val title: String)

これはCompose Compiler Metricsを使うことで確認できます。 restartableになっていて、skippableになっていないものは引数がたとえ一緒でもskipされません。
app_release-composables.txt

restartable fun Composable1(
  unstable articles: Articles
)
restartable skippable fun Composable2(
  stable uiModel: UiModel
)
restartable fun Composable3(
  unstable article: Article
)

どうしたらいいのか?

3つ解決策が考えられます。

解決策1: @Stableになっているクラスでラップする

1つ目はComposable2の関数のようにUiModelクラスでラップすることです。これによりComposeにこのオブジェクトは変更されないことを通知することでうまく動きます。この場合は常にStableなクラスでラップすることを意識しながら開発する必要があります。

@Stable
data class UiModel(val articles: Articles)

@Composable
fun Composable2(uiModel: UiModel) {
  Text(text = "Hello $uiModel!")
  uiModel.articles.articles.forEach {
    Composable3(it)
  }
}

解決策2: モデルのあるモジュールでもComposeのコンパイラーを動かす。

このArticlesなどがあるmodelモジュールもComposeコンパイラーを動かしてしまいます。
これによってComposeがこのArticleなどのクラスのことを知ることができるので、うまく動きます。
しかし、これを行うにはComposeのruntimeのdependencyも必要になるので、Jetpack ComposeをUIフレームワークとして扱う場合は、UIとの分離という面ではちょっともやもやするかもしれません。(Composableの仕組みをこの変更可能かどうかを管理する仕組みとして考えれば入れてもいいと考えられるかもしれません。)

ただ、この場合でもfun Composable1(articles: Articles)ではRecomposeは防げません。なぜならListはMutableList(ArrayList)のインスタンスであることができるため、Stableではないためです。ただ、これはどの解決策を取っても同じ問題になります。

unstable class Articles {
  unstable val articles: List<Article>
  <runtime stability> = Unstable
}

解決策3: UIモデルに変換する

もちろん、これをして、UIの中のクラスを使えば解決します。これによって関心事の分離が可能になります。
ただ、もちろんメリットだけではなく、これは毎回の開発でマッピングを実装し、テストし、レビュー、メンテナンスしていくということになるので、コストを払うことになります。

(これはGuide to app architectureでも触れられています。 https://developer.android.com/jetpack/guide/data-layer?hl=en#business-models )

まとめ

それぞれこの解決策にはメリデメがあるので、この中で自分がとれそうな方法をとっていくのが良いと思います。

もりさんがプロダクトの中で気づいて問題を教えてくれて、それを調べました
元ネタ (自分が質問したもの)

実験したソースコード