Flutter学習8日目 -公式を読む 9. インタラクティブな機能の実装-

**※自分の学習記録のメモとしてこの記事を書いています。**

1日目：[Flutter学習1日目 -公式を読む 1. インストール編-](https://qiita.com/umiremix/items/940f38e03be4b71dc33d)
2日目前編：[Flutter学習2日目前編 -公式を読む 2. エディターの設定編-](https://qiita.com/umiremix/items/0ff6600a6a9f996a3a74)
2日目後編：[Flutter学習2日目後編 -公式を読む 3. テストドライブ編-](https://qiita.com/umiremix/items/f1a84f96199054fbebf2)
︙
6日目：[Flutter学習6日目 -公式を読む 7. ウィジェットの紹介-](https://qiita.com/umiremix/items/3844119d4510884bf3b1)
7日目：[Flutter学習7日目 -公式を読む 8. レイアウトのチュートリアル-](https://qiita.com/umiremix/items/9404986cc2f80a81c6bf)
8日目：Flutter学習8日目 -公式を読む 9. インタラクティブな機能の実装-

今日は[Adding interactivity to your Flutter app](https://flutter.dev/docs/development/ui/interactive)ページを読み進めて、インタラクティブな機能の実装について学んでいきます！
今回で公式を読む編は一区切りつくので、次回は[Udacity online Flutter Training](https://www.udacity.com/course/build-native-mobile-apps-with-flutter--ud905)をやっていく予定です！

## ステートフルウィジェットとステートレスウィジェット

ウィジェットはステートフルかステートレスのどちらかです。ウィジェットが変更できる場合、例えば、ユーザーが操作すると、それはステートフルです。

ステートレスウィジェットは変更されません。`Icon` 、 `IconButton` 、 `Text` はステートレスウィジェットの例です。ステートレスウィジェットは `StatelessWidget` のサブクラスです。

ステートフルウィジェットは動的です。例えば、ユーザーのインタラクションによってトリガーされたイベントやデータを受信したときに、その外観を変更することができます。 `Checkbox` 、 `Radio` 、 `Slider` 、 `InkWell` 、 `Form` および `TextField` はステートフルウィジェットの例です。ステートフルウィジェットは 
 `StatefulWidget` のサブクラスです。

ウィジェットの状態は `State` オブジェクトに格納され、ウィジェットの状態と外観を分離します。状態は、スライダーの現在値やチェックボックスのチェックの有無など、変更可能な値で構成されています。ウィジェットの状態が変更されると、ステートオブジェクトは `setState()` を呼び出し、フレームワークにウィジェットの再描画を指示します。

## ステートフルウィジェットの作成

### ステップ1: ウィジェットの状態を管理するオブジェクトを決める

ウィジェットの状態はいくつかの方法で管理できますが、この例ではウィジェット自身である`FavoriteWidget`が状態を管理します。この例では、星のトグルは親ウィジェットや他のUIに影響を与えない独立したアクションなので、ウィジェットは内部的にステートを管理することができます。

### ステップ 2: StatefulWidgetのサブクラス

`FavoriteWidget` クラスは自身の状態を管理するので、`createState()` をオーバーライドしてStateオブジェクトを作成します。フレームワークはウィジェットをビルドするときに `createState()` を呼び出します。この例では、次のステップで実装する `_FavoriteWidgetState` のインスタンスを返します。

```dart
class FavoriteWidget extends StatefulWidget {
  @override
  _FavoriteWidgetState createState() => _FavoriteWidgetState();
}
```

### ステップ3：サブクラスの状態

`_FavoriteWidgetState` クラスは、ウィジェットの有効期間中に変更可能なミューティアブルデータを格納します。アプリを最初に起動すると、UIには赤い星が表示され、湖が41の「いいね！」と一緒に「お気に入り」ステータスを持っていることを示します。これらの値は `_isFavorited` と `_favoriteCount` フィールドに保存されます。


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

このクラスでは、`build()` メソッドも定義されており、赤い `IconButton` と `Text` を含む行が作成されます。`IconButton` (`Icon` の代わりに `IconButton` を使用しています) は `onPressed` プロパティを持っており、タップを処理するためのコールバック関数 (`_toggleFavorite`) を定義しています。次にコールバック関数を定義します。

```dart
class _FavoriteWidgetState extends State<FavoriteWidget> {
  // ···
  @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'),
          ),
        ),
      ],
    );
  }
}
```

`IconButton` が押されたときに呼び出される `_toggleFavorite()` メソッドは、 `setState()` を呼び出します。 `setState()` の呼び出しは非常に重要で、ウィジェットの状態が変更されたことと、ウィジェットを再描画する必要があることをフレームワークに伝えるためです。 `setState()` の関数引数は、これら2つの状態の間でUIを切り替えます。

- 星のアイコンと数字の41
- star_borderのアイコンと数字の40

```dart
void _toggleFavorite() {
  setState(() {
    if (_isFavorited) {
      _favoriteCount -= 1;
      _isFavorited = false;
    } else {
      _favoriteCount += 1;
      _isFavorited = true;
    }
  });
}
```

### ステップ4: ステートフルウィジェットをウィジェットツリーにプラグインする

カスタムのステートフル ウィジェットをアプリの `build()` メソッドのウィジェット ツリーに追加します。まず、 `Icon` と `Text` を作成するコードを探して削除します。同じ場所で、ステートフル ウィジェットを作成します。

```diff     
-  Icon(            
+  FavoriteWidget(),            
-    Icons.star,            
-    color: Colors.red[500],            
-  ),            
-  Text('41'),
```

これで完了です🎉アプリをホットリロードすると、星のアイコンがタップに反応するようになりました。

![名称未設定-1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62182/ab94e296-4172-a470-7539-5a7a69343888.gif)

ここまでのコードの全体像はこちら。

```dart:main.dart
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

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

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) {
    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'),
          ),
        ),
      ],
    );
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    Widget titleSection = Container(
      padding: const EdgeInsets.all(32),
      child: Row(
        children: [
          Expanded(
            /*1*/
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                /*2*/
                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],
                  ),
                ),
              ],
            ),
          ),
          /*3*/
          FavoriteWidget(),
        ],
      ),
    );
    Color color = Theme.of(context).primaryColor;
    Widget buttonSection = Container(
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          _buildButtonColumn(color, Icons.call, 'CALL'),
          _buildButtonColumn(color, Icons.near_me, 'ROUTE'),
          _buildButtonColumn(color, Icons.share, 'SHARE'),
        ],
      ),
    );
    Widget textSection = Container(
      padding: const EdgeInsets.all(32),
      child: Text(
        'Lake Oeschinen lies at the foot of the Blüemlisalp in the Bernese '
            'Alps. Situated 1,578 meters above sea level, it is one of the '
            'larger Alpine Lakes. A gondola ride from Kandersteg, followed by a '
            'half-hour walk through pastures and pine forest, leads you to the '
            'lake, which warms to 20 degrees Celsius in the summer. Activities '
            'enjoyed here include rowing, and riding the summer toboggan run.',
        softWrap: true,
      ),
    );
    return MaterialApp(
      title: 'Flutter layout demo',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Flutter layout demo'),
        ),
        body: ListView(
          children: [
            Image.asset(            
              'images/lake.jpg',            
              width: 600,            
              height: 240,            
              fit: BoxFit.cover,            
            ),
            titleSection,
            buttonSection,
            textSection
          ],
        ),
      ),
    );
  }
  Column _buildButtonColumn(Color color, IconData icon, String label) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Icon(icon, color: color),
        Container(
          margin: const EdgeInsets.only(top: 8),
          child: Text(
            label,
            style: TextStyle(
              fontSize: 12,
              fontWeight: FontWeight.w400,
              color: color,
            ),
          ),
        ),
      ],
    );
  }
}
```

## 状態を管理する

ステートフルウィジェットの状態を管理するのは誰ですか? ウィジェット自体か？親ウィジェット? 両方? 別のオブジェクト？答えは... 状況によります。ウィジェットをインタラクティブにするには、いくつかの有効な方法があります。ウィジェット設計をする人は、ウィジェットがどのように使用されることを期待しているかに基づいて決定します。ここでは、状態を管理する最も一般的な方法を紹介します。

- ウィジェットは自身の状態を管理します。
- 親はウィジェットの状態を管理します。
- ミックスアンドマッチのアプローチ。

どのようにして、どのアプローチを使用するかを決めるのでしょうか？以下の原則を参考にしてください。

- 問題の状態がユーザデータ、例えばチェックボックスのチェック済み・チェック外モード、スライダーの位置などであれば、状態は親ウィジェットによって管理されるのがベストです。

- 問題の状態がアニメーションのような美的なものであれば、状態はウィジェット自身が管理するのがベストです。

疑わしい場合は、親ウィジェットで状態を管理することから始めましょう。

3つの簡単な例を作成して、状態を管理するさまざまな方法の例を示します。TapboxA、TapboxB、TapboxCです。それぞれの例は似たような動作をします-タップすると緑とグレーのボックスを切り替えるコンテナを作成します。`_active`な場合は緑、非アクティブな場合は灰色になります。

これらの例では、`GestureDetector` を使用してコンテナ上のアクティビティをキャプチャしています。

### ウィジェットは自身の状態を管理します

ウィジェットの状態を内部的に管理することが最も理にかなっていることもあります。例えば、`ListView`はコンテンツがレンダーボックスを超えると自動的にスクロールします。`ListView`を使っているほとんどの開発者はListViewのスクロール動作を管理したくないので、`ListView`自身がスクロールオフセットを管理しています。

`_TapboxAState`クラスは、

- `TapboxA`の状態を管理します。
- ボックスの現在の色を決定する`_active`ブール値を定義します。
- 関数 `_handleTap()` を定義し、ボックスがタップされたとき、`_active` を更新し、`setState()` 関数を呼び出して UI を更新します。
- ウィジェットのすべてのインタラクティブな動作を実装します。

```dart

// TapboxA manages its own state.

//------------------------- TapboxA ----------------------------------

class TapboxA extends StatefulWidget {
  TapboxA({Key key}) : super(key: key);

  @override
  _TapboxAState createState() => _TapboxAState();
}

class _TapboxAState extends State<TapboxA> {
  bool _active = false;

  void _handleTap() {
    setState(() {
      _active = !_active;
    });
  }

  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _handleTap,
      child: Container(
        child: Center(
          child: Text(
            _active ? 'Active' : 'Inactive',
            style: TextStyle(fontSize: 32.0, color: Colors.white),
          ),
        ),
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color: _active ? Colors.lightGreen[700] : Colors.grey[600],
        ),
      ),
    );
  }
}

//------------------------- MyApp ----------------------------------

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Flutter Demo'),
        ),
        body: Center(
          child: TapboxA(),
        ),
      ),
    );
  }
}
```

タップで緑色になりました🎉
![1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62182/7da506ec-deae-e957-db3e-bcb9b45beffc.gif)

### 親ウィジェットはウィジェットの状態を管理します

多くの場合、親ウィジェットが状態を管理し、更新のタイミングを子ウィジェットに伝えることが最も理にかなっています。例えば、`IconButton` はアイコンをタップ可能なボタンとして扱うことができます。`IconButton`がステートレスなのは、親ウィジェットがボタンがタップされたかどうかを知る必要があり、適切なアクションを取ることができると判断したからです。

次の例では、TapboxBはコールバックを通じて親ウィジェットに状態をエクスポートしています。TapboxBはステートを管理しないので、StatelessWidgetをサブクラス化しています。

ParentWidgetStateクラス：

- TapboxB の `_active` 状態を管理します。
- ボックスがタップされたときに呼び出されるメソッド `_handleTapboxChanged()` をインプリメントしています。
- 状態が変更されると、`setState()` を呼び出して UI を更新します。

TapboxBクラス：

- すべての状態は親によって処理されるため、StatelessWidgetを拡張しています。
- タップが検出されると親に通知します。

```dart
// ParentWidget manages the state for TapboxB.

//------------------------ ParentWidget --------------------------------

class ParentWidget extends StatefulWidget {
  @override
  _ParentWidgetState createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  bool _active = false;

  void _handleTapboxChanged(bool newValue) {
    setState(() {
      _active = newValue;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      child: TapboxB(
        active: _active,
        onChanged: _handleTapboxChanged,
      ),
    );
  }
}

//------------------------- TapboxB ----------------------------------

class TapboxB extends StatelessWidget {
  TapboxB({Key key, this.active: false, @required this.onChanged})
      : super(key: key);

  final bool active;
  final ValueChanged<bool> onChanged;

  void _handleTap() {
    onChanged(!active);
  }

  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _handleTap,
      child: Container(
        child: Center(
          child: Text(
            active ? 'Active' : 'Inactive',
            style: TextStyle(fontSize: 32.0, color: Colors.white),
          ),
        ),
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color: active ? Colors.lightGreen[700] : Colors.grey[600],
        ),
      ),
    );
  }
}
```

### ミックスアンドマッチのアプローチ

いくつかのウィジェットでは、ミックスアンドマッチのアプローチが最も理にかなっています。このシナリオでは、ステートフルウィジェットがステートの一部を管理し、親ウィジェットがステートの他の側面を管理します。

`TapboxC`の例では、下にタップすると、ボックスの周りに濃い緑色のボーダーが表示されます。上にタップすると、境界線は消え、ボックスの色が変わります。`TapboxC` は、`_active` の状態を親にエクスポートしますが、`_highlight `の状態は内部で管理しています。この例では、`_ParentWidgetState` と `_TapboxCState` という 2 つの`State`オブジェクトを持っています。

`ParentWidgetState` オブジェクト：

- `_active` な状態を管理します。
- ボックスがタップされたときに呼び出されるメソッド`_handleTapboxChanged()` をインプリメントしています。
- タップが発生し、`_active` 状態が変化したときに `setState()` を呼び出して UI を更新します。

TapboxCState オブジェクト：

- `_highlight`の状態を管理します。
- `GestureDetector` はすべてのタップイベントをリッスンします。ユーザーが下にタップすると、ハイライトが追加されます（濃い緑色の境界線として実装されています）。ユーザーがタップを離すと、ハイライトが削除されます。
- `setState()` を呼び出して、タップダウン、タップアップ、またはタップキャンセル時に UI を更新し、 `_highlight` の状態を変更します。
- タップイベントでは、その状態変化を親ウィジェットに渡し、ウィジェットプロパティを使用して適切なアクションを実行します。

```dart
//---------------------------- ParentWidget ----------------------------

class ParentWidget extends StatefulWidget {
  @override
  _ParentWidgetState createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  bool _active = false;

  void _handleTapboxChanged(bool newValue) {
    setState(() {
      _active = newValue;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      child: TapboxC(
        active: _active,
        onChanged: _handleTapboxChanged,
      ),
    );
  }
}

//----------------------------- TapboxC ------------------------------

class TapboxC extends StatefulWidget {
  TapboxC({Key key, this.active: false, @required this.onChanged})
      : super(key: key);

  final bool active;
  final ValueChanged<bool> onChanged;

  _TapboxCState createState() => _TapboxCState();
}

class _TapboxCState extends State<TapboxC> {
  bool _highlight = false;

  void _handleTapDown(TapDownDetails details) {
    setState(() {
      _highlight = true;
    });
  }

  void _handleTapUp(TapUpDetails details) {
    setState(() {
      _highlight = false;
    });
  }

  void _handleTapCancel() {
    setState(() {
      _highlight = false;
    });
  }

  void _handleTap() {
    widget.onChanged(!widget.active);
  }

  Widget build(BuildContext context) {
    // This example adds a green border on tap down.
    // On tap up, the square changes to the opposite state.
    return GestureDetector(
      onTapDown: _handleTapDown, // Handle the tap events in the order that
      onTapUp: _handleTapUp, // they occur: down, up, tap, cancel
      onTap: _handleTap,
      onTapCancel: _handleTapCancel,
      child: Container(
        child: Center(
          child: Text(widget.active ? 'Active' : 'Inactive',
              style: TextStyle(fontSize: 32.0, color: Colors.white)),
        ),
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color:
              widget.active ? Colors.lightGreen[700] : Colors.grey[600],
          border: _highlight
              ? Border.all(
                  color: Colors.teal[700],
                  width: 10.0,
                )
              : null,
        ),
      ),
    );
  }
}

```

# 感想

インタラクティブなウィジェットの設計は、「よりリーダブルにするにはどうしたらいいのか」と悩みそうだと思いながら読み進めていました。
これで一旦公式を読むのは終わりにして、次回は[Udacity online Flutter Training](https://www.udacity.com/course/build-native-mobile-apps-with-flutter--ud905)をやっていく予定です！