🫧

改めて宣言的UIの何が良いのか見つめ直す

2024/12/15に公開

はじめに

近年、AndroidアプリケーションのUI開発において、従来のXMLベースのレイアウトから、Jetpack Composeを用いた宣言的UIへの移行が加速しています。プロダクションのコードでもJetpackComposeへの移行をしているところは多いと思います。しかし、なぜ宣言的UIへ移行するのでしょうか?本記事は、宣言的UIの価値と、なぜこの移行がモダンな開発手法として注目されているのかを自分なりにまとめたものです。

宣言的UI(Declarative UI)とは

そもそも宣言的UIとはなんでしょうか?なぜ"宣言的"UIと呼ばれているのでしょうか?
宣言的UI(Declarative UI)とは、「どのように」(How)ではなく「何を」(What)表示するかを記述するUIの実装アプローチです。この命名の由来は、プログラミングパラダイムにおける「宣言型プログラミング」(Declarative Programming)に基づいているようです。

Google の公式ドキュメントでは、以下のように説明されています:

Jetpack Compose is a modern declarative UI Toolkit for Android. Compose makes it easier to write and maintain your app UI by providing a declarative API that allows you to render your app UI without imperatively mutating frontend views.

参照: Jetpack Compose 公式ドキュメント - Thinking in Compose

従来の命令的アプローチ(XMLレイアウト)

<!-- activity_main.xml -->
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">
    
    <TextView
        android:id="@+id/counterText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="0" />
        
    <Button
        android:id="@+id/incrementButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Increment" />
</LinearLayout>
// MainActivity.kt
class MainActivity : AppCompatActivity() {
    private var counter = 0
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        findViewById<Button>(R.id.incrementButton).setOnClickListener {
            counter++
            findViewById<TextView>(R.id.counterText).text = counter.toString()
        }
    }
}

宣言的アプローチ(Jetpack Compose)

@Composable
fun Counter() {
    var counter by remember { mutableStateOf(0) }
    
    Column {
        Text(text = counter.toString())
        Button(onClick = { counter++ }) {
            Text("Increment")
        }
    }
}

※上記は一例(従来の命令的アプローチではDataBindingでの実装などもある)

XMLレイアウトの課題と宣言的UIによる解決

1. 状態管理の複雑さ

XMLレイアウトの課題:

  • UIの状態とビューの状態を同期させる必要がある
  • findViewByIdによる参照の取得
  • Null安全性の考慮
  • ライフサイクル管理の複雑さ

Composeでの解決:

@Composable
fun UserProfile(user: User) {
    var isEditing by remember { mutableStateOf(false) }
    
    Column {
        if (isEditing) {
            EditProfile(user)
        } else {
            DisplayProfile(user)
        }
        Button(onClick = { isEditing = !isEditing }) {
            Text(if (isEditing) "保存" else "編集")
        }
    }
}

2. レイアウトの再利用性

XMLレイアウトの課題:

  • カスタムビューの作成が煩雑
  • レイアウトの入れ子が深くなると可読性が低下
  • スタイルの再利用には複雑な継承関係が必要

Composeでの解決:

@Composable
fun CustomCard(
    title: String,
    content: @Composable () -> Unit
) {
    Card(
        modifier = Modifier.padding(8.dp),
        elevation = 4.dp
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(
                text = title,
                style = MaterialTheme.typography.h6
            )
            content()
        }
    }
}

なぜ宣言的UIへの移行がモダンとなっているのか

  1. 開発効率の向上

    • コードの記述量が減少
    • プレビューが容易
    • ホットリロードのサポート
  2. 型安全性の向上

    • コンパイル時の型チェック
    • IDEのサポートが充実

メリット

  1. コードの一元管理

    • UIとロジックが同じファイルで管理可能
    • コンポーネントの依存関係が明確
  2. 状態管理の簡素化

    @Composable
    fun StateExample() {
        var text by remember { mutableStateOf("") }
        
        TextField(
            value = text,
            onValueChange = { text = it }
        )
    }
    

デメリット

  1. 学習コスト

    • 新しいパラダイムの理解が必要
    • チーム全体での知識のアップデートが必要
  2. 既存コードとの互換性

    • 段階的な移行が必要
    • 既存のXMLレイアウトとの併用時の考慮が必要
  3. パフォーマンスの考慮

    • 不適切な実装によるrecompositionの増加

まとめ

宣言的UIとは従来の命令的な実装から何を表示させるかにフォーカスしたアプローチであることを確認しました。
UIもkotlinで記述出来ることの快適さと、Composableの再利用性、DataBindingに比べてより少ないコードでの実装が特に大きなメリットだと感じています。
時間がある時に記事の内容もアップデートしていきたいと思います。

参考資料

Discussion