Flutter入門 ~ First App ~

## はじめに
最近，僕の周りで話題のFlutterを触ってみました。
[Write Your First Flutter App](https://flutter.io/get-started/codelab/)の内容をそのままやっていきます。

### 前提
- Dart, Flutterはインストール済
- 開発環境は作成済（IntelliJを使用）
- iOSで実行を確認

## Step1: Hello World
とりあえず，Flutterでアプリ作成します。

```bash
$ flutter create first_app
```
このコマンドでFlutterのアプリが作成されます。
変更するのは，基本`lib/main.dart`ファイル。
`lib/main.dart`ファイルを以下の内容で書き換えます。

```dart
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Welcome to Flutter',
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text('Welcome to Flutter'),
        ),
        body: new Center(
          child: new Text('Hello World'),
        ),
      ),
    );
  }
}
```
よくわからないけど，とりあえずRun。

mainメソッドの`=>`は1行で関数を書くときに使える記法。MyAppインスタンスを渡してアプリを実行してるっぽいです。
たぶん，Javaとか読める人はなんとなくやってることわかる気がする。。
全体のViewを`StatelessWidget`が司っていて，Viewをbuildしてます。`MaterialApp`がtitle, home, bodyプロパティを持っていてそれぞれに任意のview widgetを渡すことで画面を描画しているようです。

Dart，始めてでもなかなか読みやすいですね。

## Step2: 外部のパッケージを使う

パッケージを導入してみます。Flutterのパッケージは[Flutter Packages](https://pub.dartlang.org/flutter/)で探せます。ここでは，[english_words](https://pub.dartlang.org/packages/english_words)を導入しています。
Flutterではパッケージを`pubspec.yaml`ファイルで管理します。

```dart
dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^0.1.0
  english_words: ^3.1.0
```
一番下に`english_words`を追加。
追加するとIntelliJの上の方にPackages getというのがでるのでそれを押せばpackageが導入できます。
`lib/main.dart`を以下のように書き換えます。

```dart
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final wordPair = new WordPair.random();
    return new MaterialApp(
      title: 'Welcome to Flutter',
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text('Welcome to Flutter'),
        ),
        body: new Center(
          // child: new Text('Hello World'),
          child: new Text(wordPair.asPascalCase),
        ),
      ),
    );
  }
}
```
これで，bodyに表示される文字列が変わるリロードする度に変わります。
非常に簡単ですね。

## Step3: Stateful Widgetを追加する

Stateless Widgetはその名の通り，動きがないウィジェットです。
Stateful Widgetのライフタイムの中で変化する状態を保持します。Stateful Widgetは少なくとも2つのクラスを使用します。

1. Stateクラス
2. StatefulWidgetクラス

StatefulWidgetクラスはStateクラスのインスタンスを生成するクラスで，それ自体では変化することはないですが，StateクラスはWidgetのライフタイムを保持しています。
ここでは，RandomWordsStateクラスを生成するRandomWordsというStatefulクラスを追加してみます。RandomWordsStateクラスは，Widgetに表示されたお気に入りワードのペアを保持します。

まず，stateful RandomWordsウィジェットを`main.dart`に追加します。
MyAppクラスの外に記述することに注意！

```dart
class RandomWords extends StatefulWidget {
  @override
  State<StatefulWidget> createState();
}
```

次に，RandomWordsStateクラスを追加します。全てのアプリのコードは，RandomWordsウィジェットの状態を保持するこのクラスに属します。このクラスが具体的にすることは，

- ユーザのスクロールによって生成されるワードのペアを保持する
- ユーザがハートアイコンをトグルすることでリストに追加/削除されるお気に入りワードのペアを保持する

の2つです。

そして，このクラスを追加すると，buildメソッドがないよと怒られるので，buildメソッドを追加します。このbuildメソッドでは，ワードペアの生成を行います。
MyAppクラスからRandomWordsStateクラスへワード生成のコードを移しておきます。buildメソッドは以下のようになります。

```dart
class RandomWords extends StatefulWidget {
  @override
  createState() => new RandomWordsState();
}
```
MyAppの方のワード生成のコードは削除しておきます。

```dart
class RandomWordsState extends State<RandomWords> {
  @override
  Widget build(BuildContext context) {
    final wordPair = new WordPair.random();
    return (new Text(wordPair.asPascalCase));
  }
}
```

ここまでで一旦実行すると，Step2と同じ用にリロードする度に中央のワードが変更されるという動作を確認できると思います。

## Step4: 無限Scroll Viewを作成する

このステップでは，RandomWordsStateクラスを拡張して，ワードのリストを生成して表示します。
ListViewの`builder`ファクトリーコンストラクタは，要求に応じてリストビューを作成します。

まず，提案されたワードを保持する`_suggestions`リストをRandomWordsStateクラスに追加します。
ちなみに，変数の頭のアンダースコア(`_`)は，Dartにおいてプライバシー変数であることを意味します。
更に，`biggerFont`変数もフォントサイズを大きくするために定義しておきます。

```dart
class RandomWordsState extends State<RandomWords> {
  final _suggestions = <WordPair>[];

  final _biggerFont = const TextStyle(fontSize: 18.0);
  @override
  Widget build(BuildContext context) {
    final wordPair = new WordPair.random();
    return (new Text(wordPair.asPascalCase));
  }
}
```

次に，`_buildSuggestions()`関数を追加します。このメソッドは提案されたワードを生成するリストビューを作成する関数となります。
ListViewクラスはビルダープロパティ`itemBuilder`（ファクトリビルダーとコールバック関数）
BuildContextとイテレータ i がこの関数に渡されるパラメータとなります。イテレータは0から始まり，ワードを提案する度に関数が呼ばれてインクリメントされます。

```dart
  Widget _buildSuggestions() {
    return new ListView.builder(
      padding: const EdgeInsets.all(16.0),
      // itemBuilderのコールバックはワードが提案される度に呼ばれ，ListTitleの行にそれを配置する
      itemBuilder: (context, i) {
        if (i.isOdd) return new Divider();
        final index = i ~/ 2;
        if (index >= _suggestions.length) {
          _suggestions.addAll(generateWordPairs().take(10));
        }
        return _buildRow(_suggestions[index]);
      }
    );
  }
}
```

このままでは動きません。なぜなら，`_buildSuggestions`関数は`_buildRow`をワードペア毎に読んでいるからです。この関数は，リストタイトルに次のステップで注目する行生成のための関数で，新しいペアを表示する役割を担います。

`_buildRow`関数は，以下のように実装されます。

```dart
class RandomWordsState extends State<RandomWords> {
  ...

  Widget _buildRow(WordPair pair) {
    return new ListTile(
      title: new Text(
        pair.asPascalCase, style: _biggerFont,
      ),
    );
  }
}
```

そして，RandomWordsStateを以下のように変更します。

```dart
class RandomWordsState extends State<RandomWords> {
  final _suggestions = <WordPair>[];

  final _biggerFont = const TextStyle(fontSize: 18.0);
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Startup Name Generator'),
      ),
      body: _buildSuggestions(),
    );
  }
  ...

}
```

最後に，MyAppクラスを変更します。

```dart
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Welcome to Flutter',
      home: new RandomWords(),
    );
  }
}
```

これで，ユーザがスクロールすると自動でワードを生成して表示するリストビューが完成です。

## Step5: インタラクティブなアプリを作成する

次に，各行にお気に入りボタンを追加します。そのためには，ユーザがタップできるようにしないといけませんね。

まず，RandomWordsStateに`_saved`セットを追加します。

```dart
class RandomWordsState extends State<RandomWords> {
  final _suggestions = <WordPair>[];

  final _saved = new Set<WordPair>();

  final _biggerFont = const TextStyle(fontSize: 18.0);
 ...

}
```

次に，`_buildRow`関数にお気に入りに追加されているかを確認するための`alreadySaved`を追加し，ハート型のアイコンをリストに表示します。

```dart
class RandomWordsState extends State<RandomWords> {
  ...

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

ここまででリロードすると，リストの行の右端にハート型のアイコンが表示されているはずです。

最後に，タップできるように`_buildRow`関数を変更します。
`buildRow`関数の完成形は以下。

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

## Step6: 新しいScreenに誘導する
前のステップでお気に入りボタンを追加したので，このステップではお気に入りリストの画面を追加して，画面遷移をやってみます。

Flutterでは，Navigatorが画面遷移はアプリのルートを含むstackを管理します。

- Navigatorのスタックにルートがプッシュされると，そのルートに画面が更新される
- NavigatorのSタックからポップされると，前のルートに戻る

では，実際に追加して遷移処理してみます。
まず，RandomWordsStateのbuildメソッドでAppBarにリストアイコンを追加します。ユーザがこれをクリックすると，お気に入りが追加要素をもった新しいルートがNavigatorにプッシュされ，アイコンを表示します。

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

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

```dart
class RandomWordsState extends State<RandomWords> {
  ...
  void _pushSaved() {}
}
```
ここでアプリをホットリロードしてみると，AppBarにリストアイコンが追加されていることがわかります。

せっかくアイコンを追加したので，タップしたときの処理を追加します。
ユーザがAppBarのリストアイコンをタップした際に，ルートを生成してNavigatorスタックにプッシュします。このアクションで，新しいルートを表示して画面を変更します。

新しいページの内容は，無名関数MaterialPageRouteの`builder`プロパティでビルドされます。

下のように，Navigatorのスタックにルートをプッシュする`Navigator.push`を追加し，MaterialPageRouteとそのビルダーを追加します。ここでは，ListTileの行を生成するコードを追加しておきます。
ListTileの`divideTiles()`メソッドが各ListTileの間に水平のスペースを入れてくれます。
`devided`変数が最後の行を保持していて，これは簡易関数`toList()`によってリストに変換されます。

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

ビルダープロパティはScaffoldを返します。Scaffoldは新しいルートのためのAppBar，"SavedSuggestions"を持っています。新しいルートのbodyはListTiles行を含むListViewで構成されていて，各行はdividerによって分割されています。

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

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

ホットリロードし，適当にお気に入りを追加して画面遷移すると，新しい画面が表示されるはずです。ちなみに，`return new Scaffold`以下を書かないと，次の画面が表示されず真っ黒な画面が表示されます。

ちなみに，前の画面に戻るためにわざわざ`Navigator.pop`を実装する必要はありません。勝手に新しい画面のAppBarに自動で"戻る"ボタンが追加されているはずです。

## Step7: テーマを使ってUIを変更する
最後のステップですが，アプリのテーマで遊んでみます。

アプリのテーマはThemeDataクラスをいじることで簡単に変更することができます。今はデフォルトテーマを使っているので，白をベースとした色に変更してみます。

MyAppを次のように変更します。

```dart
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Welcome to Flutter',
      theme: new ThemeData(
        primaryColor: Colors.amber
      ),
      home: new RandomWords(),
    );
  }
}
```

ホットリロードすると色がかわることがわかります。

MaterialライブラリのColorsクラスはたくさんの色の定数を提供していますので，いろんな色に変更して遊んでみると良いと思います。ホットリロードしながら変更すると簡単に確認しながら変更できます。

## 終わりに
ここまでやった感想としては，Flutterは非常によく作られてるなと思いました。
dartも始めてでも直感的でわかりやすいし，学習コストは低そうという印象。
本気でいろいろやり始めると壁があるのかもしれませんが，個人的にはやってみる価値はあるかなという感じです。