🍃

Jetpack ComposeがFlutterよりも圧倒的に優れているひとつのこと

2022/06/13に公開1

概要

初期のFlutterから使っていた私からするとJetpack ComposeよりもFlutterの良い面がたくさん見えてきますが、それもJetpack Composeのバージョンアップとともに差が縮まってきているように思えます。(人によってはJetpack Composeの方が初めから優れていると思う人もいると思いますので、あくまで主観です。)

ただし、元々Jetpack Composeの方が圧倒的にFlutterよりも優れている面があります。
それはFlutterでいうウィジェットの再ビルド処理がJetpack Composeではほぼ意識しなくてもよいということです。Jetpack Composeの方がFlutterより優れている点は他にもたくさんあると思いますが、これはすごいと思えるのでこの点について、記述したいと思います。

サンプルアプリ

以下のようなアプリを作ります。A, B, Cのコンポーザブル関数があり、Cのボタンを押下するとAのテキストが変わります。その際にどこが再コンポジションするかを調べます。

Flutterの場合

今ではsetState()を直接触ることはないかもしれませんが、setState()で記述すると全ての再ビルドが走ります。それを避けるために、これも直接触ることはないですが、InheritedWidgetを使います。ProviderもRiverPodも状態管理をするライブラリのほとんどは、内部でInheritedWidgetを使っているはずです。

以下のリンクは、2019年01月に書いた記事なので利用しているメソッドが若干古いのですが、内容は今のバージョンで書いてもほぼ同じになります。
見ていただけるとわかると思いますが、InheritedWidgetを使うとかなり冗長な表現になります。
https://qiita.com/ko2ic/items/d7b744f19f213ef1e647

サンプルのような単純なアプリを作る際もこんなに書かないといけません。

Jetpack Composeの場合

Jetpack Composeの場合は以下だけです。シンプルでわかりやすく、冗長な表現もありません。
FlutterのInheritedWidgetと比べると圧倒的だと思いませんか?

@Composable
fun CountUpScreen(
  modifier: Modifier = Modifier,
) {
  var count: Int by remember { mutableStateOf(0) }
  SideEffect { println("CountUpScreen") }
  Column(
    modifier = modifier.fillMaxSize(),
    verticalArrangement = Arrangement.SpaceEvenly,
    horizontalAlignment = Alignment.CenterHorizontally,
  ) {

    ACompose(
      count = count
    )
    BCompose()
    CCompose {
      count++
    }
  }
}

@Composable
private fun ACompose(count: Int) {
  SideEffect { println("ACompose") }
  Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
    Text(
      text = "$count"
    )
    ChildACompose()
  }
}

@Composable
private fun ChildACompose() {
  SideEffect { println("ChildACompose") }
  Text(
    text = "ChildACompose"
  )
}

@Composable
private fun BCompose() {
  SideEffect { println("BCompose") }
  Text(
    text = "I am composable that will not be recompose"
  )
}

@Composable
private fun CCompose(onClick: () -> Unit) {
  SideEffect { println("CCompose") }
  Button(onClick = {
    onClick()
  }) {
    Icon(Icons.Outlined.Add, contentDescription = "+")
  }
}

Jetpack Composeをあまり知らない人のために書きますが、SideEffect関数は、コンポジション(Flutterでいうbuild)があったときに呼ばれます。このサンプルではログを出しています。

ボタンを押下時のログの結果は以下です。大本のCountUpScreenとAだけが再コンポジションしています。

I/System.out: CountUpScreen
I/System.out: ACompose

優れている点

Flutterに比べてシンプルでわかりやすく冗長な部分もないことがわかります。

が、実はこれだけではありません。よく見るともっとすごいことが起きています。
以下の部分です。

@Composable
private fun ACompose(count: Int) {
  SideEffect { println("ACompose") }
  Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
    Text(
      text = "$count"
    )
    ChildACompose()
  }
}

@Composable
private fun ChildACompose() {
  SideEffect { println("ChildACompose") }
  Text(
    text = "ChildACompose"
  )
}

ChildACompose()が、AComposeの中に含まれています。
AComposeが再コンポジションされていると書きましたが、その中にあるChildACompose()は再コンポジションされていません。

もし、Flutterだったら、親のウィジェットがリビルドされていたら、そこに含まれている子ウィジェットもリビルドされてしまいます。

このようにJetpack Composeは必要な箇所だけを再Compositonをするという優れものなのです。

いやぁ、これはすごいことだと思います。
Flutterだと細かなパフォーマンスチューニングを気にしだすと必須の知識が、Jetpack Composeだとよしなにやってくれるのです。
これは素晴らしい!

Flutterにのぼせている(?)方々、一度Jetpack Composeを触ってみるのもいいかもしれませんよw

NewsPicks の Zenn

Discussion

nakama refactnakama refact

Compose初心者ですが、なるほどと理解しました。ありがとうございますm(__)m