0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Jetpack Compose】再コンポーズの仕組みを理解する

Posted at

はじめに

こちらの書籍を読んで、Jetpack Composeの再コンポーズのしくみについて学んだことをまとめました

動作環境

Android Studio Koala | 2024.1.1 Patch 1
Apple M2 Pro

サンプルコード

MainActivity.kt
package com.example.sampleapplication

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.sampleapplication.ui.theme.SampleApplicationTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            SampleApplicationTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    LayoutA(
                        modifier = Modifier
                            .fillMaxSize()
                            .padding(innerPadding)
                    )
                }
            }
        }
    }
}

@Composable
fun LayoutA(modifier: Modifier = Modifier) {
    LayoutB(modifier = modifier)
}

@Composable
fun LayoutB(modifier: Modifier = Modifier) {
    Column(
        modifier = modifier,
        horizontalAlignment = Alignment.CenterHorizontally, // 水平方向の中央揃え
        verticalArrangement = Arrangement.Center // 垂直方向の中央揃え
    ) {
        var count by remember { mutableIntStateOf(0) }
        Button(
            onClick = { count++ },
            modifier = Modifier.padding(bottom = 16.dp)
        ) {
            Text("ボタン")
        }
        Text("カウント: $count")
    }
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    SampleApplicationTheme {
        LayoutA()
    }
}

ビルドして以下の画面になればOKです
中央のボタンを押すとカウントアップします

Screenshot_20250205_091326.png

解説

メモリ上のUI構造

サンプルコードは、LayoutAの下にLayoutBがあり、さらにその下にColumn、その配下にTextとButtonがあるという階層構造になっています。(さらにButtonの配下にTextがあります)
このコードを実行すると次のような、コンポーザブル関数の親子関係を表現する木構造がメモリ上に構築されます。

image.png

状態の更新に伴うUIの更新

State(今回の場合、状態変数 count)は、Composeフレームワークによって監視され、変更があった場合フレームワークはその変化を検知し、再コンポーズ(UIの更新)を実行します

image.png

次の1行がStateに対応します。状態変数をラッパーすることでComposeフレームワークの監視対象にできると思ってもらえればいいと思います

var count by remember { mutableIntStateOf(0) }

ここでは、IntFloatなどのプリミティブな変数をComposeフレームワークの監視対象にするため、mutableIntStateOfを使ってラップしています。引数で初期値を設定しています。

これで、count変数が変更されると、それに伴ってUIの更新処理が走るようになりました。これを再コンポーズと呼びます。

再コンポーズはなるべく小さい範囲で実行される

再コンポーズ(UIの更新処理)は、効率的な処理の観点からなるべく小さい範囲で実行されます。

再コンポーズの起点

  • 基本的に再コンポーズの起点となるのはStateを保持しているコンポーザブル関数です
  • ただし、そのコンポーザブル関数がinlineの場合は、その親のコンポーザブル関数が起点になります

inline関数は、コンパイル時にその呼び出し元にコードが展開されるため、
実質的にその関数自体は存在せず、親のコンポーザブル関数の一部として扱われます。そのため、再コンポーズの起点は親のコンポーザブル関数となります。

今回の場合

  • Stateを持っているのはColumnですが、Columnはinline関数であるため、その親であるLayoutBが起点となります
  • つまり、LayoutAは再コンポーズの対象外となり、countが変化しても再描画されません

再コンポーズのスキップ

効率化の観点から再コンポーズの対象範囲だったとしても、再描画されないコンポーザブル関数もあります

スキップの条件

  • コンポーザブル関数の引数が変化しないこと

今回の場合

  • 再コンポーズごとに、コンポーザブル関数の前回の引数と今回の引数を比較して変化があれば再コンポーズを実行します
  • つまり、Text("カウント: $count")はボタンタップ毎に引数が変化するため再コンポーズされ、Text("このコンポーザブル関数はスキップされる")はボタンタップ毎に引数が変化しないため、再コンポーズの対象から外れます
  • このようにして Jetpack Composeは、効率的な再描画処理を実現しています
skipped.kt
@Composable
fun LayoutB(modifier: Modifier = Modifier) {
    Column(
        modifier = modifier,
        horizontalAlignment = Alignment.CenterHorizontally, // 水平方向の中央揃え
        verticalArrangement = Arrangement.Center // 垂直方向の中央揃え
    ) {
        var count by remember { mutableIntStateOf(0) }
        Button(
            onClick = { count++ },
            modifier = Modifier.padding(bottom = 16.dp)
        ) {
            Text("ボタン")
        }
        Text("カウント: $count")
        Text("このコンポーザブル関数はスキップされる") // スキップ
    }
}
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?