Jetpack ComposeとFlutterでコードを見比べてみた


## はじめに

Androidアプリ開発において、Jetpack ComposeとFlutterはどちらも主要なUI構築ツールとして注目を集めています。両者は同じ目的（モバイルアプリのUI構築）を持ちながらも、異なるアプローチを取っています。

今回の記事では、同じ画面を構成する際に、これらのツールでどのようなコードの違いが生まれるのかを検証してみました。具体的なコードの比較を通じて、それぞれの特徴や利点を考察していきます。

## 準備

比較のため、シンプルなカウンターアプリを作成しました。Flutterのデモアプリで最初から用意されているカウンター機能を、Jetpack Composeでも同様に実装することで、両者の違いを明確にすることができます。



実装は`MainActivity`に記述し、以下の機能を実装しました：
- カウント表示
- インクリメントボタン
- アプリバー
- テーマの適用

## 実装コード

```MainActivity.dart
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), 
    );
  }
}

```

```MainActivity.kt
package com.example.comparecode

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
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.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
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 com.example.comparecode.ui.theme.CompareCodeTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            CompareCodeTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    CounterApp()
                }
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CounterApp() {
    var counter by remember { mutableIntStateOf(0) }

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("Jetpack Compose Demo") }
            )
        },
        floatingActionButton = {
            FloatingActionButton(onClick = { counter++ }) {
                Icon(Icons.Filled.Add, contentDescription = "Increment")
            }
        }
    ) { paddingValues ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(paddingValues),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center
        ) {
            Text(
                text = "You have pushed the button this many times:",
                style = MaterialTheme.typography.bodyLarge
            )
            Text(
                text = counter.toString(),
                style = MaterialTheme.typography.headlineMedium
            )
        }
    }
}

@Preview(showBackground = true)
@Composable
fun CounterAppPreview() {
    CompareCodeTheme {
        CounterApp()
    }
}
```

## コードの主な違い

### 1. 言語の違い
- Flutter: Dart言語を使用
  - Googleが開発したオブジェクト指向プログラミング言語
  - JavaScriptに似た構文を持ち、学習曲線が比較的緩やか
  - 強力な型システムとNull安全性を備える
  - コンパイル時に最適化され、高速な実行が可能
  - ホットリロード機能により、開発中の変更が即座に反映
  - クロスプラットフォーム開発に特化した設計
```dart:widget.dart
// Dartの特徴的な構文
class MyWidget extends StatelessWidget {
  final String title;  // 不変のプロパティ
  const MyWidget({required this.title});  // 必須パラメータ
}
```
- Jetpack Compose: Kotlin言語を使用
  - JetBrainsが開発したJVM言語
  - Javaとの完全な相互運用性を持つ
  - Null安全性、コルーチン、拡張関数などの現代的な機能を備える
  - 関数型プログラミングの機能が充実
  - Android開発の第一言語として採用
  - 簡潔で表現力の高い構文
```kotlin:widget.kt
// Kotlinの特徴的な構文
@Composable
fun MyWidget(
    title: String,  // 型推論が可能
    modifier: Modifier = Modifier  // デフォルト引数
) {
    // 関数型プログラミングの機能を活用
}
```

### 2. 状態管理の違い
- Flutter:
```dart:home.dart
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }
}
```
- Jetpack Compose:
```kotlin:counter.kt
var counter by remember { mutableIntStateOf(0) }
```

### 3. UIの構築方法
- Flutter: クラスベースのウィジェットツリー
```dart:widget.dart
return Scaffold(
  appBar: AppBar(...),
  body: Center(
    child: Column(...)
  )
)
```
- Jetpack Compose: 関数ベースのComposable
```kotlin:compose.kt
@Composable
fun CounterApp() {
  Scaffold(
    topBar = { ... },
    floatingActionButton = { ... }
  ) { ... }
}
```

### 4. プレビュー機能
- Flutter: ホットリロードで実機/エミュレータで確認
- Jetpack Compose: `@Preview`アノテーションで直接プレビュー可能
  - そのため、アプリをわざわざビルドしなくてもその画面の確認ができる
  - アプリのビルドは数分間かかるためこの機能は開発効率を上げる一助となっている
```kotlin:preview.kt
@Preview(showBackground = true)
@Composable
fun CounterAppPreview() {
  CompareCodeTheme {
    CounterApp()
  }
}
```

### 5. レイアウトの指定方法
- Flutter: `mainAxisAlignment`や`crossAxisAlignment`を使用
  - `mainAxisAlignment`: メイン軸（Columnの場合は縦方向、Rowの場合は横方向）に沿った配置を指定
  - `crossAxisAlignment`: 交差軸（Columnの場合は横方向、Rowの場合は縦方向）に沿った配置を指定
  - 例：`MainAxisAlignment.center`で中央揃え、`MainAxisAlignment.spaceBetween`で均等配置など
```dart:layout.dart
Column(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[...]
)
```
- Jetpack Compose: `Arrangement`と`Alignment`を使用
  - `Arrangement`: 要素間の配置方法を指定（`Arrangement.Center`、`Arrangement.SpaceBetween`など）
  - `Alignment`: 要素の整列方法を指定（`Alignment.CenterHorizontally`、`Alignment.Top`など）
  - より直感的な命名規則を採用し、配置の意図が明確
```kotlin:layout.kt
Column(
  horizontalAlignment: Alignment.CenterHorizontally,
  verticalArrangement: Arrangement.Center
)
```

### 6. テーマの適用方法
- Flutter: `ThemeData`を使用
  - アプリ全体のテーマを定義するためのクラス
  - `colorScheme`で色の設定、`textTheme`でテキストスタイル、`useMaterial3`でMaterial Design 3の適用を指定
  - 子ウィジェットは`Theme.of(context)`でテーマにアクセス可能
  - カスタムテーマは`ThemeData`を継承して作成
```dart:theme.dart
theme: ThemeData(
  colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
  useMaterial3: true,
)
```
- Jetpack Compose: カスタムテーマクラスを使用
  - `MaterialTheme`をベースにしたカスタムテーマを定義
  - 色やタイポグラフィ、シェイプなどの設定を一括で管理
  - `@Composable`関数として定義され、より柔軟なテーマの適用が可能
  - コンポーネントは`MaterialTheme`オブジェクトを通じてテーマにアクセス
```kotlin:theme.kt
CompareCodeTheme {
  Surface(...) {
    CounterApp()
  }
}
```

### 7. コードの構造
- Flutter: より伝統的なオブジェクト指向プログラミングのアプローチ
  - クラスベースの設計（`StatelessWidget`や`StatefulWidget`を継承）
  - 状態管理は`State`クラスで行い、`setState()`で更新を通知
  - ウィジェットツリーはクラスの階層構造として表現
  - メソッドのオーバーライド（`build()`など）でUIを構築
  - 継承やカプセル化などのOOPの概念を活用
```dart:home.dart
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}
```
- Jetpack Compose: より関数型プログラミングに近いアプローチ
  - 関数ベースの設計（`@Composable`関数）
  - 状態管理は`remember`や`mutableStateOf`を使用
  - UIは関数の呼び出し階層として表現
  - 副作用の分離と純粋な関数の使用を重視
  - イミュータブルな状態管理と宣言的なUI構築
```kotlin:counterApp.kt
@Composable
fun CounterApp() {
  var counter by remember { mutableIntStateOf(0) }
  // ...
}
```

### 8. 状態の更新方法
- Flutter: `setState()`を使用して明示的に状態更新を通知
  - `State`クラス内で状態を管理し、`setState()`を呼び出して更新を通知
  - `setState()`は同期的に実行され、即座にUIの再構築をトリガー
  - 状態の変更とUIの更新が密結合
  - 複数の状態更新を一括で行う場合は`setState()`内にまとめる
  - パフォーマンスの観点から、必要な部分のみを更新するよう注意が必要
```dart:incrementCounter.dart
void _incrementCounter() {
  setState(() {
    _counter++;
  });
}
```
- Jetpack Compose: 状態変数の変更が自動的にUIの再構成をトリガー
  - `remember`と`mutableStateOf`を使用して状態を管理
  - 状態の変更が自動的にUIの再構成をトリガー
  - 状態の変更とUIの更新が分離されている
  - 複数の状態を個別に管理可能
  - 効率的な再構成のため、変更された部分のみが更新される
```kotlin:mutable.kt
var counter by remember { mutableIntStateOf(0) }
// 状態の変更が自動的にUIを更新
counter++
```
## 所感

今回の比較を通じて、いくつかの興味深い発見がありました。

### プレビュー機能の違い
Jetpack Composeに慣れている身としては、Flutterのプレビュー方法の違いに大きなギャップを感じました。Jetpack Composeでは`@Preview`アノテーションを使用して簡単にプレビューが可能ですが、Flutterではホットリロードを使用する必要があります。この違いは開発効率に大きな影響を与える可能性があります。

### 状態管理のアプローチ
状態の更新方法について、両フレームワークで大きく異なるアプローチを取っていることが印象的でした。Flutterでは`setState()`を使用して明示的に状態を管理する必要がありますが、Jetpack Composeでは状態の変更が自動的にUIの再構成をトリガーします。この違いは、パフォーマンスのチューニングと開発の手間のトレードオフを考える必要があることを示唆しています。

### フレームワーク選択の重要性
Androidアプリ開発という同じ目的を持ちながらも、フレームワークによって実装方法や開発体験が大きく異なると分かりました。これは、プロジェクトの要件や開発チームの経験、パフォーマンスの要件などに応じて、適切なツールを選択することの重要性を示しています。

### まとめ
今回の比較を通じて、モダンなUIフレームワークの選択は、単なる技術的な決定ではなく、プロジェクトの成功に大きく影響する重要な判断であることを実感しました。それぞれのフレームワークには長所と短所があり、それらを理解した上で適切な選択を行うことが、効率的な開発につながると考えられます。