[Flutter] Write Your First Flutter App, part 1 (の翻訳)

Flutter の codelab part1 の翻訳です。
https://flutter.io/get-started/codelab/

英語力皆無 & Dart未経験 & 初Flutter な自分が書いているので、間違いや理解しづらい点などあるかと思います。変なところがあればご指摘ください :angel:

# Write Your First Flutter App, part 1
これからスタートアップ会社の名前の候補を生成するアプリを作ってみます。このアプリはテーブル(List)の各セルに候補となる文字列を表示し、ユーザは表示された候補の中からベストな名前をselectしたりunselectすることができます。コードはユーザのスクロールに合わせて名前を遅延生成します。ユーザがスクロールすれば名前はそれに従って生成され、またどこまでもスクロールすることができます。

![startup_namer_1_opt.gif](https://qiita-image-store.s3.amazonaws.com/0/104872/74994a7e-dac1-259b-18bf-5dbf08eba14b.gif)

---

Part 1 で学ぶこと

* iOSとAndroidの両方で自然な見栄えのアプリをFlutterで書く方法
* Flutterアプリの基本的な構造
* 機能を拡張するためのパッケージ (package)の探し方と使い方
* より高速な開発のためのホットリロード (hot reload) の利用
* Stateful widget の作り方
* 無限かつ遅延的にロードされるリストの作り方

---


## Step 1: Create the starter Flutter app

1. Replace the contents of `lib/main.dart` の中身を以下の内容に置き換えてください。もともとあったコードはすべて削除し、以下のコードを貼り付けてください。このコードは画面の中央に「Hello World」と表示させるものです。

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

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

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

2. IDEの緑色の矢印ボタンを押して実行すると、AndroidやiOSに出力されます。

iPhoneXだとこうなります。

<img width="488" alt="HelloWorld_iPhoneX.png" src="https://qiita-image-store.s3.amazonaws.com/0/104872/da336258-495d-e235-2303-1b119e8b82e8.png">


Androidだとこう。

![HelloWorld_Android.png](https://qiita-image-store.s3.amazonaws.com/0/104872/4d9716e3-30f3-8e2b-81e2-cead7ec93d21.png)

### Observation
* この example では Material app を作っています。 Material とはモバイルやWebで標準的な visual design languageです。Flutter はリッチな Material Widget を提供します。
* `main` メソッドは `=>` (太矢印, fat arrow) を利用しています。 `=>` 表記は1行の関数やメソッドを記述するために使います。
* `MyApp` は `StatelessWidget` を継承していてそれ自体が widget です。 Flutter の世界では、ほぼすべてが widget です。 alignment, padding, layout なども widget です。
* Material Library に所属する `Scaffold` widget は app bar, title, ホーム画面用の widget ツリーを保持する body プロパティを提供します。 widget のツリーはかなり複雑になりえます。
* widget の主な役割は `build()` メソッドを提供することです。 これは、下位レベルの widget を用いてその widget をどのように描画するべきなのかを記述するものです。
* 今回の例では body は `Text` widget を child として保持する `Center` widget で成り立っています。 `Center` widget はそのサブツリーを画面の中央に配置します。

---


## Step 2: Use an external package
ここでは、 `english_words` という open-source パッケージを使ってみましょう。このパッケージは数千よく使用される英単語といくつかの便利な関数を提供します。

`english_words` パッケージは他の多くのパッケージと同様に、 [pub.dartlang.org](https://pub.dartlang.org/flutter/) で見つけることができます。

<img width="989" alt="find_package.png" src="https://qiita-image-store.s3.amazonaws.com/0/104872/389f9471-f219-a11d-e01c-aa51028a0600.png">

1. pubspec ファイルは Flutter アプリにおいて asset と 依存するパッケージなどを管理するものです。 `pubspec.yaml` の依存パッケージリストに `english_words` (3.1.0以上) を追加しましょう。

    ```yaml
    dependencies:
      flutter:
        sdk: flutter

      cupertino_icons: ^0.1.0
      english_words: ^3.1.0

    ```
2. IDE の Editor View にて、 `Packages Get` をクリックします。これはパッケージをプロジェクトに引っ張ってきます。Console画面にこんな出力がされます。

    ```
    > flutter packages get
    Running "flutter packages get" in `YOUR_APP_NAME`...
    Process finished with exit code 0
    ```

    これが実行されると、`pubspec.lock` が自動生成されプロジェクトに引っ張ってきたすべてのパッケージ名とバージョン番号が記述されます。

3. `lib/main.dart` に新しいパッケージの import を記述します。

    ```dart
    import 'package:flutter/material.dart';
    import 'package:english_words/english_words.dart';
    ```
    `impor...` と入力していくと IDE はインポートするパッケージを補完してくれるでしょう。入力を完了すると、この1行がグレーアウトされます。これは(今の所)インポートしたパッケージが使用されていないことを示しています。
4. "Hello World" の代わりに english_words パッケージで生成した単語を表示してみましょう。
    以下のように先程の `lib/main.dart` の中身を書き換えます。
    
    ```dart
    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) {
        final wordPair = WordPair.random(); // ★追加
        return MaterialApp(
          title: 'Welcome to Flutter',
          home: Scaffold(
            appBar: AppBar(
              title: Text('Welcome to Flutter'),
            ),
            body: Center(
              //child: Text('Hello World'), // ★コメントアウト
              child: Text(wordPair.asPascalCase),  // ★追加
            ),
          ),
        );
      }
    }
    ```
    ここで「Pascal case」というのは UpperCamelCase のことです。C++やSwiftなどの多くの言語におけるクラス名の命名法と同じ形式です。

5. 今、アプリが動いている状態であれば、`CMD+S` (上書き保存)もしくは雷マークをクリックすることで hot reload が発動します。Hot reload を実行するたびに、ホーム画面の中央に違う名前がランダムに表示されます。なぜ毎回違う名前が表示されるかというと、 `wordPair` は `build()` の中で生成されており、 `build()` は `MaterialApp` が描画を要求するたび、または、Flutter Inspector で Platform を切り替えるたびに呼ばれるからです。

## Step3: Add a Stateful widget
State**less** widgets (状態なしwidget) は immutable, つまり、すべてのプロパティは `final` で、値の変更はできません。

State**ful** widgets (状態ありwidget)はそれが生きている限り、状態(プロパティ)が維持・変更されます。Stateful widget を実装するためには、少なくとも2つのクラスが必要です。

1. StatefulWidget
2. State

StatefulWidget class が State class を作成します。StatefulWidget class は、それ自体は immutable つまり値変更できません、しかし State class は widget のが生きている限り存続します。

このステップでは、 `RandomWords` という stateful widget を追加します。このクラスは、その State である `RandomWordsState` クラスを生成します。そして `RandomWords` widget をすでにある `MyApp` stateless widget の中に child として追加します。

1. まず最小限の state class を作成します。以下を `main.dart` の最後に追加してください。
    
    ```dart
    class RandomWordsState extends State<RandomWords> {
      // TODO Add build method
    }
    ```
    `State<RandomWords>` という宣言に着目してください。これは、 `RandomWords` class が使用することに特化した generic [State](https://docs.flutter.io/flutter/widgets/State-class.html) class を使用する、ということを意味します。App の状態の殆どはここに置かれます。ここに `RandomWords` widget のための state が保持されます。この class はユーザのスクロール操作によって無限的に生成される word pair を保存します。また、 ユーザがハートアイコンをタップすることでトグルできるお気に入りの状態(これは Part 2 で！)も保存します。

    `RandomWordsState` は `RandomWords` class に依存します。それを追加しましょう。

2. 状態を持つ `RandomWords` widget を `main.dart` に追加します。`RandomWords` widget はその State class を作成すること以外、ほとんど何もしません。
    
    ```dart
    class RandomWords extends StatefulWidget {
      @override
      RandomWordsState createState() => new RandomWordsState();
    }
    ```
    この state class を追加すると、IDE は build method が足りない、と怒ります。なので次の段階で、基本的な word pair を生成する処理を build method として記述します。生成するコードを MyApp から `RandomWordsState` に移動させます。

3. `RandomWordsState` に `build()` method を追加します。
    
    ```dart
    class RandomWordsState extends State<RandomWords> {
      @override
      Widget build(BuildContext context) {
        final wordPair = WordPair.random();
        return Text(wordPair.asPascalCase);
      }
    }
    ```

4. `MyApp ` から単語を生成するコードを削除します。
    
    ```dart
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        final wordPair = WordPair.random();  // ★ ここを削除

        return MaterialApp(
          title: 'Welcome to Flutter',
          home: Scaffold(
            appBar: AppBar(
              title: Text('Welcome to Flutter'),
            ),
            body: Center(
              //child: Text(wordPair.asPascalCase), // ★この行を
              child: RandomWords(), // ★ こう変える
            ),
          ),
        );
      }
    }
    ```

5. App をリスタート(緑の再生ボタンやCMD+Sキーで)させます。変更前と挙動は同じになるはずです。つまりHot Reload するたびに違う単語が表示されます。


---

もし hot reload の際に以下のような警告が出た場合、 App をリスタートさせてみてください。


>Reloading…
>Some program elements were changed during reload but did not run when the view was reassembled; you may need to restart the app (by pressing “R”) for the changes to have an effect.


これはおそらく偽陽性(つまり本当は問題がない)です。リスタートさせれば変更が UI に反映されていることが確認できるでしょう。

---


## Step 4: Create an infinite scrolling ListView
このステップでは、 `RandomWordsState` を拡張し、 word pair のリストを生成するようにします。ユーザがスクロールするにしたがって、リストは `ListView` widget に表示されます。そしてこのリストは無限に増えます。 `ListView` の `builder` factory constructor を使うことで、list view を要求に応じて遅延的に作ることができます。

1. 候補として生成した単語群を保存するために、 `RandomWordsState` class に `_suggestions` を追加します。また、フォントサイズをより大きくするために、 `biggerFont` 変数も追加します。

    >Dart言語では、アンダースコア `_` をつけることで、library 内からのみ見えるようにできます。

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

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

    次は、 `RandomWordsState` に `_buildSuggestions()` を追加します。このメソッドは候補となる単語を表示する `ListView` を作成します。

    `ListView` class は `itemBuilder` という builder property を提供します。ここには匿名関数を渡します。渡した関数は `BuildContext` と 行番号 `i` が渡されます。この行番号は 0 から始まり、1ずつインクリメントされます。これに応じて単語を生成します。この方式を採用することで、ユーザのスクロール操作に応じて無限に単語を提示するリストを作ることができます。

2. 以下の `_buildSugge()` 関数を `RandomWordsState` class に追加します。(コメントは消しても良いです。)
    
    ```dart
    class RandomWordsState extends State<RandomWords> {
      ...
      Widget _buildSuggestions() {
        return ListView.builder(
          padding: const EdgeInsets.all(16.0),
          // itemBuilder は単語ごとに1回呼び出され、その単語を1つの ListTile として配置します。
          // 偶数行なら、この関数は ListTile を生成し、
          // 奇数業なら、この関数は Divider widget (区切り線)を生成します。
          // ただし、小さなデバイスではこの線は見えづらいかもしれません。
          itemBuilder: (context, i) {
            // 1 pixel の区切り線を各行の前に表示します。
            if (i.isOdd) return Divider();

            // "i ~/ 2" という文法は、 i を 2で割って整数を返します。
            // 例えば、 i が 1, 2, 3, 4, 5 なら結果は 0, 1, 1, 2, 2 となります。
            // これは ListView 内の実際の単語数を計算します。つまり区切り線の数を引きます。
            final index = i ~/ 2;
            // もし利用可能な単語群の最後に到達したら...
            if (index >= _suggestions.length) {
              // ...最後まで来たら、更に10単語生成し、suggestions list に追加します。
              _suggestions.addAll(generateWordPairs().take(10));
            }
            return _buildRow(_suggestions[index]);
          }
        );
      }
    }
    ```

    `_buildSuggestions()` 関数は `_buildRow()` を1単語ごとに呼びます。この関数は単語を `ListTile` に配置します。 `ListTile` は次のステップでリストの row (行) をより魅力的にしてくれます。

3. `_buildRow()` メソッドを `RandomRowrdsState` に追加します。
    
    ```dart
    class RandomWordsState extends State<RandomWords> {
      ...

      Widget _buildRow(WordPair pair) {
        return ListTile(
          title: Text(
            pair.asPascalCase,
            style: _biggerFont,
          ),
        );
      }
    }
    ```
4. `RandomWordsState` の `build()` メソッドを `_buildSuggestions()` を使うように編集します。単語生成ライブラリを直接呼ばないようにします。([Scaffold](https://docs.flutter.io/flutter/material/Scaffold-class.html) は Material Design の基本的なレイアウトを提供します。)

    ```dart
    class RandomWordsState extends State<RandomWords> {
      ...
      @override
      Widget build(BuildContext context) {
        // final wordPair = WordPair.random(); // ★削除
        // return Text(wordPair.asPascalCase); // ★削除
        return Scaffold (
          appBar: AppBar(
            title: Text('Startup Name Generator'),
          ),
          body: _buildSuggestions(),
        );
      }
      ...
    }
    ```

5. `MyApp` の build method も更新しましょう。home を `RandomWords` widget にするだけです。

    もともとの記述を以下のように変更します。

    ```dart
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Startup Name Generator',
          home: RandomWords(),
        );
      }
    }
    ```

6. App をリスタートさせましょう。どれだけスクロールしても単語がリストに表示されることが確認できるでしょう。

iOS。

<img width="495" alt="part1_complete_ios.png" src="https://qiita-image-store.s3.amazonaws.com/0/104872/3621b4a8-47be-f567-0be1-2238dae32e67.png">




Android

<img width="420" alt="part1_complete_android.png" src="https://qiita-image-store.s3.amazonaws.com/0/104872/ac0cee63-7291-7785-3064-35f098286933.png">