💻

[Flutter入門(5)] 公式チュートリアルのステップ2:Adding interactivity(略)をやってみる

2020/07/01に公開

Flutterの公式サイトに チュートリアル として紹介されているコンテンツを順にやってみようと思います。

今回は2つ目の Adding interactivity to your Flutter app です。

前回作成したアプリは現在下図のような見た目になっています。

今回はこの画面の赤い★アイコンをタップできるようにしていきます。下図のように、タップするたびに「お気に入り追加済み」と「お気に入り未追加」の状態がトグルするような動作を実現します。

StatefulウィジェットとStatelessウィジェット

実際にコードを触っていく前に、StatefulウィジェットとStatelessウィジェットについておさらいしておきましょう。

ウィジェットには2種類あり、それが StatefulStateless です。

Statefulウィジェットは状態を持つウィジェットで、ユーザーの入力などに応じて何かしら状態が変化することがあるなら、それはStatefulウィジェットです。

Statelessウィジェットは状態を持ちません。つまり、生成されてから破棄されるまでの間、保有している値が一切変化しません。 Icon IconButton Text などがStatelessウィジェットの一例です。Statelessウィジェットは、 StatelessWidget クラスのサブクラスとして実装されています。

Statefulウィジェットの例としては、 Checkbox Radio Slider InkWell Form TextField など があります。Statefulウィジェットは StatefulWidget クラスのサブクラスとして実装されています。

ウィジェットの状態は、 State オブジェクトによって保持されます。ウィジェットの表現と状態は別々のクラスによって管理されているということです。

State オブジェクトは、状態を表す何かしら可変な値を持っています。例えばスライダーにおける「現在の値」や、チェックボックスにおける「チェックされているかどうか」などの情報がそれに当たります。

ウィジェットの状態が変更されると、 State オブジェクトは setState() というメソッドを呼び出すことによって、フレームワークにウィジェットの再描画を依頼します。

Step 1:ウィジェットの状態をどう管理するかを決める

実は、ウィジェットの状態を管理する方法には選択肢がいくつかあります。

今回のサンプルアプリでは FavoriteWidget というウィジェットを作って、自分自身に状態の管理をさせることにします。

ウィジェットと状態の分離についてより詳細に学びたい場合は、公式サイトの Managing state というコンテンツを参照するとよいでしょう。(本ブログでの解説記事は こちら

Step 2: StatefulWidget を継承したウィジェットを作る

まずは、状態を持つStatefulウィジェットのクラスを作ります。 lib/main.dart の末尾に以下のコードを書き足しましょう。

class FavoriteWidget extends StatefulWidget {
  
  _FavoriteWidgetState createState() => _FavoriteWidgetState();
}

FavoriteWidget クラスは自分の状態を自分で管理します。そのために、 State オブジェクトを生成する createState() メソッドをオーバーライドしています。

createState() メソッドは、ウィジェットがビルドされるタイミングでフレームワークによって実行されます。この例では、 createState() メソッドは _FavoriteWidgetState クラスのインスタンスを返しています。これは次のステップで実装しましょう。

Step 3: State を継承した状態クラスを作る

FavoriteWidget ウィジェットの状態を保有するのが _FavoriteWidgetState クラスです。

今回は、アプリが起動したときには

  • 自分はすでにお気に入りに追加している状態(というイメージ)
  • 表示内容としては、赤色で塗り潰された★アイコンと、 41 という数字(お気に入りに追加されている数を表しているイメージ)

という状態にしたいので、 lib/main.dart の末尾にひとまず以下のようなクラス定義を追記しましょう。

class _FavoriteWidgetState extends State<FavoriteWidget> {
  bool _isFavorited = true;
  int _favoriteCount = 41;
}

さらに、このクラスに build() メソッドを実装します。このメソッドで FavoriteWidget の実際の中身を構築します。

  class _FavoriteWidgetState extends State<FavoriteWidget> {
    bool _isFavorited = true;
    int _favoriteCount = 41;
+ 
+   @override
+   Widget build(BuildContext context) {
+     return Row(
+       mainAxisSize: MainAxisSize.min,
+       children: [
+         Container(
+           padding: EdgeInsets.all(0),
+           child: IconButton(
+             icon: (_isFavorited ? Icon(Icons.star) : Icon(Icons.star_border)),
+             color: Colors.red[500],
+             onPressed: _toggleFavorite,
+           ),
+         ),
+         SizedBox(
+           width: 18,
+           child: Container(
+             child: Text('$_favoriteCount'),
+           ),
+         ),
+       ],
+     );
+   }
  }

Icon ではなく IconButton を使っている点に注意してください。 IconButtononPressed というプロパティを持っていて、タップされたときに実行したい処理をコールバックとして渡すことができます。

ここでは _toggleFavorite というメソッドを渡しています。では、次にこのメソッドを実装しましょう。

  class _FavoriteWidgetState extends State<FavoriteWidget> {
    bool _isFavorited = true;
    int _favoriteCount = 41;
  
+   void _toggleFavorite() {
+     setState(() {
+       if (_isFavorited) {
+         _favoriteCount -= 1;
+         _isFavorited = false;
+       } else {
+         _favoriteCount += 1;
+         _isFavorited = true;
+       }
+     });
+   }
+ 
    @override
    Widget build(BuildContext context) {
    // ...

_toggleFavorite() メソッドは、 setState() メソッドを呼び出しています。

setState() を呼び出すという行為には特別な意味があります。先述したとおり、これによってフレームワークにウィジェットの状態が変わったことを通知し、ウィジェットの再描画を依頼しているのです。

setState() に引数として渡しているクロージャーの中で、 _isFavorited および _favoriteCount という変数の値を更新しています。

こうしてウィジェットが再描画されると、Stateクラスの build() メソッドが実行され、

icon: (_isFavorited ? Icon(Icons.star) : Icon(Icons.star_border)),

この部分の条件分岐によってウィジェットの見た目が変化するというわけです。

Step 4:作ったStatefulウィジェットを画面にはめ込む

あとは作ったウィジェットを実際に画面にはめ込むだけです。

titleSection を組み立てているコードの以下の箇所を差し替えましょう。

  class MyApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      Widget titleSection = Container(
        padding: const EdgeInsets.all(32),
        child: Row(
          children: [
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Container(
                    padding: const EdgeInsets.only(bottom: 8),
                    child: Text(
                      'Oeschinen Lake Campground',
                      style: TextStyle(
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ),
                  Text(
                    'Kandersteg, Switzerland',
                    style: TextStyle(
                      color: Colors.grey[500],
                    ),
                  ),
                ],
              ),
            ),
-           Icon(
-             Icons.star,
-             color: Colors.red[500],
-           ),
-           Text('41'),
+           FavoriteWidget(),
          ],
        ),
      );

      // ...

これで、以下のようにタップすると状態が切り替わるようなUIが実装できました👍

GitHubで編集を提案

Discussion