Flutterでパフォーマンス最適化：クラス分けとconstの活用術

### **はじめに**

Flutterでは、`setState()`を使うと、`build()`メソッドが再実行され、ウィジェットツリー内のすべてのウィジェットが再生成されます。アプリが複雑になると、こうした**無駄な再ビルドがパフォーマンス低下の原因**になります。

そこで、この記事では、「クラス分け」と「`const`の活用」を使って、**無駄な再ビルドを防ぐ最適化テクニック**を解説します。

---

## **1. `setState()`による再ビルドの仕組み**

`setState()`が呼ばれると、その`State`クラスの`build()`メソッドが**再実行され**、ツリー内のすべてのウィジェットが再生成されます。

例えば、次のようにカウンターの値が変わるたびに、`CounterPage`の`build()`が再実行されます。

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

このような**無駄な再ビルドを減らすこと**が、Flutterアプリのパフォーマンスを向上させるために重要です。

---

## **2. `const`の役割と制限**

### **2.1 `const`の役割**

**`const`を使うと、Flutterは「このウィジェットは変わらない」と判断し、再生成をスキップ**します。

親ウィジェットの`build()`が再実行されても、**「このウィジェットは前回と同じ」と判断できれば、再描画がスキップされます**。

---

### **2.2 `const`の制限**

ただし、**`const`は状態に依存しないウィジェット**でしか使えません。

例えば、`_counter`のような変数が含まれるウィジェットには`const`を付けられません。

```dart
// ❌ エラーになります
const Text('$_counter');  // 状態に依存するためconst不可
```

また、**同じ`build()`内に`const`を使っても、Flutterは毎回「このウィジェットが同じかどうか」を判断**しなければなりません。これが、「**クラス分けが必要な理由**」です。

以下に詳細をまとめました。

---

## **3. なぜクラス分けが必要なのか？**

**クラス分けをすることで、Flutterが「このウィジェットは変わらない」という判断を1回だけで済ませられる**ようになります。

具体的には、**クラスごとに`const`を付けることで、親ウィジェットの`setState()`が呼ばれても、そのクラス全体を再生成しなくて済む**ようになります。

### **クラス分けのメリット**

- **クラス分けをしない場合：**
    - `setState()`が呼ばれるたびに、`build()`内のウィジェットが再生成されます。
    - **`const`を付けていても、毎回「同じかどうか」の判断コストが発生**します。
- **クラス分けをした場合：**
    - **`const`付きのクラスはFlutterが1回だけ「同じ」と判断**し、再描画も再生成も完全にスキップします。

---

## **4. コード例：クラス分けによる最適化**

### **改善前：クラス分けしていない場合**

```dart
class CounterPage extends StatefulWidget {
  @override
  _CounterPageState createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int _counter = 0;

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

  @override
  Widget build(BuildContext context) {
    print('CounterPage build');
    return Scaffold(
      appBar: AppBar(title: const Text('Counter')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('$_counter', style: const TextStyle(fontSize: 48)),
            const SizedBox(height: 20),
            GestureDetector(
              onTap: _incrementCounter,
              child: Container(
                padding: const EdgeInsets.all(16.0),
                color: Colors.blue,
                child: const Text('Tap to Increment'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
```

### **問題点：**

- `setState()`を呼ぶたびに、`build()`内のウィジェットがすべて再生成されます。
- **`const`を付けていても、毎回「同じか」を比較する処理が発生**します。

---

### **改善後：クラス分けして最適化する場合**

```dart
class CounterPage extends StatefulWidget {
  @override
  _CounterPageState createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int _counter = 0;

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

  @override
  Widget build(BuildContext context) {
    print('CounterPage build');
    return Scaffold(
      appBar: AppBar(title: const Text('Counter')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            CounterDisplay(counter: _counter),  // 状態に依存する部分
            const SizedBox(height: 20),  // 再利用される
            const IncrementArea(),  // 再利用される
          ],
        ),
      ),
    );
  }
}

class CounterDisplay extends StatelessWidget {
  final int counter;
  const CounterDisplay({required this.counter});

  @override
  Widget build(BuildContext context) {
    print('CounterDisplay build');
    return Text('$counter', style: const TextStyle(fontSize: 48));
  }
}

class IncrementArea extends StatelessWidget {
  const IncrementArea();

  @override
  Widget build(BuildContext context) {
    print('IncrementArea build');
    return GestureDetector(
      onTap: () => print('Tapped!'),
      child: Container(
        padding: const EdgeInsets.all(16.0),
        color: Colors.blue,
        child: const Text('Tap to Increment'),
      ),
    );
  }
}
```

### **改善点：**

- **`CounterDisplay`だけが`_counter`の変化に応じて再ビルドされます**。
- **`IncrementArea`と`SizedBox`は`const`なので、1度だけ判断され、以降は再利用**されます。

---

## **5. まとめ**

- **クラス分け**と**`const`の活用**により、Flutterの「このウィジェットは変わらない」という判断を1回だけにできます。
- クラス分けせず、同じ`build()`内に`const`を持たせると、**毎回「同じかどうか」を判断する手間**がかかります。
- この最適化により、**パフォーマンスが向上し、よりスムーズなアプリ**を提供できます。

---

### **おわりに**

Flutterでの開発では、**「状態に依存する部分」と「依存しない部分」をしっかり分けること**が重要です。

この最適化手法を活用して、無駄な再ビルドを減らし、より効率的なアプリ開発を目指しましょう！