Chapter 13

 02.画面の更新

kazutxt
kazutxt
2023.03.19に更新

画面の更新

画面がWidgetで構築されていることが理解できたところで、次は画面を更新する方法を見ていきましょう。カウンタアプリでは、ボタンをタップすると自然に画面の値が増えていました。
本節では、この仕組みがどうなっているのか解説していきます。

画面の更新の流れ

まず、カウンタアプリでボタンをタップするたびにカウントが増えていく仕組みを解説します。

要点になるのは、下記の5箇所です。

  1. 表示部分をパラメタ化するために変数を準備する。
  2. 変数をTextのWidgetの中で参照し画面に表示する。
  3. ボタンがタップされた時の関数を定義し、呼び出す。
  4. 関数内のsetStateの中で変数を更新する。
  5. インクリメント後の値で画面を更新する。

特に、4のsetState内での変数更新が重要になっています。
setStateを使うことによって、値の変更がFlutterのフレームワークに伝わり関係する要素を自動で更新します。
今回は_counterがインクリメントされており、この変更が伝わり画面も更新されるという流れになっています。

文字列の更新例

カウンタアプリでは数値でしたが、今度は偶数/奇数を表示する文字列で更新を試してみます。
新規にプロジェクトを作成し、下記のように修正して動かします。

lib/main.dart
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
+ String _type = "偶数";
  void _incrementCounter() {
    setState(() {
      _counter++;
+     if (_counter % 2 == 0) {
+        _type = "偶数";
+      } else {
+        _type = "奇数";
+      }
    });
  }
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        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.headline4,
            ),
+           Text('$_type', style: TextStyle(fontSize: 20, color: Colors.red))
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

変更点は3点です。

  1. _typeというStringの変数を準備します。
  2. Columnの3番目の要素に_typeを表示するTextを追加します。
  3. setStateの中で、_counterの値が偶数か奇数か判定し_typeに偶数/奇数の文字列を入れて、変更が伝わるようにします。

Widgetの表示/非表示の切り替え

更新ではなくWidgetの表示/非表示を切り替えたい場合があります。
このような場合は、Widgetの前にif文を入れて、判定結果がtrueの時だけ表示させることもできます。

lib/main.dart
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
- String _type = "偶数";
  void _incrementCounter() {
    setState(() {
      _counter++;
-     if (_counter % 2 == 0) {
-        _type = "偶数";
-      } else {
-        _type = "奇数";
-      }
    });
  }
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        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.headline4,
            ),
-           Text('$_type', style: TextStyle(fontSize: 20, color: Colors.red))
+           if (_counter % 2 == 0)
+             const Text('偶数です', style: TextStyle(fontSize: 20, color: Colors.red)),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), 
    );
  }
}

if文を用いて、カウンタの値が偶数の時だけTextの「偶数です」が画面に表示されるようになっています。
このように、特定の条件の時だけWidgetを画面に表示させることもできます。

StatefulWidgetとStatelessWidgetの違い

続いて、画面の更新と関わりが深いStatefulWidgetについて解説します。

statefulとstatelessの違い

StatefulWidgetを解説する前に、statefulとstatelessについて確認します。

端的にいうと、statefulは状態を持ち状態の変化を覚えているstatelessは状態を持たず常に同じ状態になっているということです。
具体的な例としては(初期状態から変化して)「ライトがついている」「カウンタが10である」「ログインしている」などが状態を持っているといえます。

状態を実現する方法はいくつかありますが、一番シンプルな実現方法は変数を用いることです。
変数に状態の情報を入れておき、変数の値による条件分岐などによって処理を変えることはstatefulといえます。逆に、statelessは状態の情報を持たず、どんな時でも同じ入力には同じ出力を返します。

statefulとstatelessをまとめると以下のようになります。

  • stateful : 状態を持つ。動的で変化する。状態によって同じ入力でも出力が変わることがある。
  • stateless : 状態を持たない。静的で変化しない。どんな時でも同じ入力に対して同じ出力になる。

状態を持つメリットとデメリット

状態を持つメリットは、何度も同じ情報を渡さなくても良いことです。
アプリやサーバで処理が進んでいくと、いくつもの情報を持たなければなりません。その際に、状態を持たないと全ての情報を毎回入力や送信する必要が出てきます。そこで、入力/送信/計算/加工した情報を覚えておくことで次回から処理を省略できます。

状態を持つデメリットは、制御が複雑になることです。
覚えておくべき状態の数が複数あり、なおかつそれぞれの状態がとりえる値も複数ある場合は、組み合わせが多くなり制御が大変になります。
状態同士に依存関係があったり有効期限があったりする場合には、不正な状態を防止したり正常な状態にリカバリしたりするなどの管理上の問題も発生します。

StatefulWidget(カウンタアプリ)

それでは、カウンタアプリでStatefulWidgetがどのように使われているか見ていきましょう。

lib/main.dart
// import、main関数、MyAppはデフォルトから変更がないため省略

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

  final String title;
  
  // _MyHomePageStateを利用する
  State<MyHomePage> createState() => _MyHomePageState();
}
// State
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  
  Widget build(BuildContext context) {
    // 中略
  }
}

まず、このカウンタアプリはボタンを押すたびにカウントが増えていくアプリのため、状態を持つstatefulなアプリです。
この状態を持つために、statefulなWidgetと状態を表す2つのクラスが利用されています。

  • StatefulWidgetを継承したクラス : MyHomePage
  • Stateを継承したクラス : _MyHomePageState

MyHomePageの中でどんな状態を持つかという部分に_MyHomePageStateが利用されています。ロジックや状態の保持の仕方などは、_MyHomePageState側で実装します。

StatelessWidget

一方、Widgetをstatelessにする場合は、StatefulWidgetとStateの2つのクラスを準備する必要はなく、StatelessWidgetを継承したクラスを作り、Stateと同じようにbuildメソッドでWidget を配置します。

lib/main.dart
// import、main関数、MyAppはデフォルトから変更がないため省略

// StatelessWidget
class MyHomePage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("StatelesPage"),
      ),
      body: Text("書き換えしないページ")
     );
  }
}