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

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

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)
3日目: [Flutter学習3日目 -公式を読む 4. スターターFlutterアプリを作成する-](https://qiita.com/umiremix/items/796cc8a7520629cae3b1)
4日目: Flutter学習4日目 -公式を読む 5. インタラクティブ機能を実装-（イマココ）

今回は[Write Your First Flutter App, part 2](https://codelabs.developers.google.com/codelabs/first-flutter-app-pt2/#0)ページの通り、3日目で作成したスターターFlutterアプリにインタラクティブ機能を追加したり、ルート（2ページ目）を実装していきます！✊

## パート2で学ぶこと

- iOS、Android、Webで自然に見えるFlutterアプリの書き方
開発サイクルを早くするためのホットリロードの使い方
- ステートフルウィジェットにインタラクティブ性を追加する方法
- セカンドスクリーンの作成方法とナビゲート方法
- テーマを使ってアプリの見た目を変える方法

## パート2で作るもの
スタートアップ企業のために、提案された名前の無限のリストを生成するシンプルなモバイルアプリから始めます。コードラボが終了するまでに、エンドユーザーは名前を選択したり、選択解除したりして、最適なものを保存することができます。アプリバーの右上にあるリストアイコンをタップすると、お気に入りの名前だけをリストアップした新しいページ（ルートと呼ばれる）に移動します。

## Flutter環境をセットアップする
パート1を完了していない場合は、[Write your first Flutter app, part 1](https://codelabs.developers.google.com/codelabs/first-flutter-app-pt1/#0)を参照してください。

## リストにアイコンを追加

このステップでは、各行にハート♡のアイコンを追加します。次のステップでは、タップ可能にしてお気に入りを保存します。

`RandomWordsState`に`_saved` `Set`を追加します。この`Set`には、ユーザーがお気に入りにした単語のペアリングが保存されます。適切に実装された`Set`は重複したエントリを許可しないので、`List`よりも`Set`の方が好ましいです。

```diff

class _RandomWordsState extends State<RandomWords> {
  final _suggestions = <WordPair>[];
+ final _saved = Set<WordPair>();
  final _biggerFont = TextStyle(fontSize: 18.0);
  ...
}
```

`_buildRow`関数では、単語のペアリングがまだお気に入りに追加されていないことを確認するために、`alreadySaved`チェックを追加します。

```diff

Widget _buildRow(WordPair pair) {
+ final alreadySaved = _saved.contains(pair);
  ...
}
```

テキストの後にアイコンを追加します。

```diff

Widget _buildRow(WordPair pair) {
  final alreadySaved = _saved.contains(pair);
  return ListTile(
    title: Text(
      pair.asPascalCase,
      style: _biggerFont,
    ),
+   trailing: Icon(
+     alreadySaved ? Icons.favorite : Icons.favorite_border,
+     color: alreadySaved ? Colors.red : null,
+   ),
  );
}
```

これで各列にハートが表示されるはずですが、まだインタラクティブではありません。

![スクリーンショット 2020-08-02 8.00.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62182/c3d5a8dd-4876-d4a1-6448-6fe58c7c2c3e.png)

## インタラクティブ性の追加

このステップでは、ハートのアイコンをタップ可能にします。ユーザーがリスト内の項目をタップしてお気に入りの状態を切り替えると、その単語のペアが保存されたお気に入りのセットに追加または削除されます。

これを行うには、`_buildRow`関数を修正します。既にお気に入りに追加されている単語エントリを再度タップすると、お気に入りから削除されます。タイルがタップされた場合、この関数は `setState()` を呼び出してフレームワークに状態が変わったことを通知します。

以下のように、`_buildRow`メソッドに`onTap`を追加します。

```diff

Widget _buildRow(WordPair pair) {
  final alreadySaved = _saved.contains(pair);
  return ListTile(
    title: Text(
      pair.asPascalCase,
      style: _biggerFont,
    ),
    trailing: Icon(
      alreadySaved ? Icons.favorite : Icons.favorite_border,
      color: alreadySaved ? Colors.red : null,
    ),
+   onTap: () {
+     setState(() {
+       if (alreadySaved) {
+         _saved.remove(pair);
+       } else { 
+         _saved.add(pair); 
+       } 
+     });
+   },
  );
}
```

Tip💡：Flutter のリアクティブスタイルフレームワークでは、`setState()` を呼び出すと、`State` オブジェクトの `build()` メソッドが呼び出され、UI が更新されます。

タイルをタップすることで、エントリーをお気に入りにしたり、お気に入りから外したりすることができます。

![スクリーンショット 2020-08-02 8.11.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62182/982d3216-591d-fc37-8758-096ab879cfbe.png)

## 新しい画面に遷移する

このステップでは、お気に入りを表示する新しいページ (Flutter のルートと呼ばれる) を追加します。ホームルートと新しいルートの間をナビゲートする方法を学びます。

Flutterでは、`Navigator`はアプリのルートを含むスタックを管理します。`Navigator`のスタックにルートをプッシュすると、そのルートの表示が更新されます。`Navigator`のスタックからルートをポップすると、表示は前のルートに戻ります。

次に、`_RandomWordsState` の `build` メソッドで `AppBar` にリストアイコンを追加します。ユーザがリストアイコンをクリックすると、保存したお気に入りを含む新しいルートが `Navigator` にプッシュされ、アイコンが表示されます。

アイコンとそれに対応するアクションを`build`メソッドに追加します。

```dart

class _RandomWordsState extends State<RandomWords> {
  ...
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Startup Name Generator'),
        actions: [
          IconButton(icon: Icon(Icons.list), onPressed: _pushSaved),
        ],
      ),
      body: _buildSuggestions(),
    );
  }
  ...
}
```

Tip💡: ウィジェットのプロパティの中には、単一のウィジェット(`child `)を取り、他のプロパティ(例えば `action`)は、角括弧(`[]`)で示されるように、ウィジェットの配列(`children`)を取ります。

`RandomWordsState`クラスに`_pushSaved()`関数を追加します。

```dart

void _pushSaved() {
}
```

次に、ルートを構築して`Navigator`のスタックにプッシュします。そのアクションにより、新しいルートが表示されるように画面が変更されます。新しいページのコンテンツは、`MaterialPageRoute`の`builder`プロパティに匿名関数でビルドされます。

`Navigator.push`を呼び出して、ナビゲータのスタックにルートをプッシュします。

```dart

void _pushSaved() {
  Navigator.of(context).push(
  );
}
```

次に、`MaterialPageRoute`とそのビルダーを追加します。とりあえず、`ListTile`の行を生成するコードを追加します。`ListTile`の`divideTiles()`メソッドは、各`ListTile`間に水平方向の間隔を追加します。`divide`変数は、便利な関数`toList()`によってリストに変換された最終的な行を保持します。

```dart

void _pushSaved() {
    Navigator.of(context).push(
      MaterialPageRoute<void>(
        builder: (BuildContext context) {
          final tiles = _saved.map(
            (WordPair pair) {
              return ListTile(
                title: Text(
                  pair.asPascalCase,
                  style: _biggerFont,
                ),
              );
            },
          );
          final divided = ListTile.divideTiles(
            context: context,
            tiles: tiles,
          ).toList();

          return Scaffold(
            appBar: AppBar(
              title: Text('Saved Suggestions'),
            ),
            body: ListView(children: divided),
          );
        },
      ),
    );
  }
}
```

`builder `プロパティは、`SavedSuggestions` という名前の新しいルートのアプリバーを含む`Scaffold`を返します。新しいルートの本体は `ListTiles` 行を含む `ListView` で構成されています。各行は区切り線で区切られています。

これでお気に入りしたリストが表示されるようになりました🎉

![スクリーンショット 2020-08-02 8.49.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62182/5612838e-4cae-4bd7-a969-89a7948534a1.png)
![スクリーンショット 2020-08-02 8.49.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62182/7a4a9723-26a5-2878-703b-9d324073ef06.png)

## テーマを使ってUIを変更する

このステップでは、アプリのテーマを変更します。テーマはアプリの外観をコントロールします。物理デバイスまたはエミュレータに依存するデフォルトのテーマを使用するか、ブランディングを反映させるためにテーマをカスタマイズすることができます。

[ThemeData](https://api.flutter.dev/flutter/material/ThemeData-class.html)クラスを設定することで、アプリのテーマを簡単に変更できます。アプリはデフォルトのテーマを使用しますが、アプリのprimary colorを白に変更します。

```diff

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Startup Name Generator',
+     theme: ThemeData(
+       primaryColor: Colors.white,
+     ),
      home: RandomWords(),
    );
  }
}
```

Primary colorが変更されたことによりアプリバーの色が変わりました🎉

![スクリーンショット 2020-08-02 8.58.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62182/90233d6d-5267-a039-5425-e273636b7747.png)

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

```dart:marin.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';
import 'package:english_words/english_words.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Startup Name Generator',    
      theme: ThemeData(
        primaryColor: Colors.white,
      ),        
      home: RandomWords(),
    );
  }
}

class RandomWords extends StatefulWidget {
  @override
  _RandomWordsState createState() => _RandomWordsState();
}

class _RandomWordsState extends State<RandomWords> {
  final _suggestions = <WordPair>[];
  final _saved = Set<WordPair>();
  final _biggerFont = TextStyle(fontSize: 18.0);
  void _pushSaved() {
    Navigator.of(context).push(
      MaterialPageRoute<void>(
        builder: (BuildContext context) {
          final tiles = _saved.map(
            (WordPair pair) {
              return ListTile(
                title: Text(
                  pair.asPascalCase,
                  style: _biggerFont,
                ),
              );
            },
          );
          final divided = ListTile.divideTiles(
            context: context,
            tiles: tiles,
          ).toList();
          return Scaffold(
            appBar: AppBar(
              title: Text('Saved Suggestions'),
            ),
            body: ListView(children: divided),
          );
        },
      ),
    );
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Startup Name Generator'),
        actions: [
          IconButton(icon: Icon(Icons.list), onPressed: _pushSaved),
        ],
      ),
      body: _buildSuggestions(),
    );
  }
  Widget _buildSuggestions() {
    return ListView.builder(
        padding: EdgeInsets.all(16.0),
        itemBuilder: (context, i) {
          if (i.isOdd) return Divider(); 
          final index = i ~/ 2; 
          if (index >= _suggestions.length) {
            _suggestions.addAll(generateWordPairs().take(10));  
          }
          return _buildRow(_suggestions[index]);
        });
  }
  Widget _buildRow(WordPair pair) {
    final alreadySaved = _saved.contains(pair); 
    return ListTile(
      title: Text(
        pair.asPascalCase,
        style: _biggerFont,
      ),
      trailing: Icon(
        alreadySaved ? Icons.favorite : Icons.favorite_border,
        color: alreadySaved ? Colors.red : null,
      ),
      onTap: () {
        setState(() {
          if (alreadySaved) {
            _saved.remove(pair);
          } else { 
            _saved.add(pair); 
          } 
        });
      },
    );
  }
}
```

# 感想

今回はアプリらしい動きが学べたので、かんたんなアプリだったらもう既に作れそうで一段落したな〜と達成感がありました✨
Qiita記事書くのもようやく慣れてきました😂

次は[Learn more](https://flutter.dev/docs/get-started/learn-more)の[Flutter for web developers](https://flutter.dev/docs/get-started/flutter-for/web-devs)を読み進めます！