Flutter学習3日目 -公式を読む 4. スターターFlutterアプリを作成する-

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

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アプリを作成する-（イマココ）

今回は公式ドキュメントの[Write your first Flutter app, part 1](https://flutter.dev/docs/get-started/codelab)ページを進めていきます！
part1〜4とプロファイルまたはリリースの実行まであり、充実してそうで嬉しいです😊

最初に注意書きが有りました。

>This is a guide to creating your first Flutter app. If you are familiar with object-oriented code and basic programming concepts such as variables, loops, and conditionals, you can complete this tutorial. You don’t need previous experience with Dart, mobile, or web programming.

訳：
これは、最初の Flutter アプリを作成するためのガイドです。オブジェクト指向のコードと、変数、ループ、条件式などの基本的なプログラミングの概念に慣れていれば、このチュートリアルを完成させることができます。Dart、モバイル、Web プログラミングの経験は必要ありません。

Dart全く触ったこと無い（Sassでちょこっと見たぐらい）ので、Dartの経験がなくても大丈夫なのはありがたいです🥺モバイル開発も、シミュレーターを起動するぐらいしかやったことないのでそんな人でも扱えるアプリサンプルが公式ドキュメントであるのは信頼が置けます…！

それではやっていきましょう🤳

## パート1で作成するもの

パート1ではスタートアップ企業の提案された名前を生成するシンプルなアプリを作ります。ユーザーは名前を選択したり、選択解除したりすることができ、最適なものを保存します。コードは、一度に10個の名前をlazilyに生成します。ユーザーがスクロールすると、より多くの名前が生成されます。ユーザーがスクロールできる範囲に制限はありません。

## パート1で学習する内容

- iOS、Android、およびWebで自然に見えるFlutterアプリの作成方法
- Flutterアプリの基本構造
- 機能を拡張するためのパッケージの検索と使用
- ホットリロードを使用して開発サイクルを短縮する
- ステートフルウィジェットを実装する方法
- 無限の遅延読み込みリストを作成する方法

## 準備

アプリをコンパイルしてウェブで実行する場合は、Webサポートを有効にする必要があります（現在ベータ版です）。

```
flutter channel beta
flutter upgrade
flutter config --enable-web
```

![スクリーンショット 2020-08-01 5.53.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62182/54e887ac-bb3b-a169-b25c-748d7b1b496f.png)

実行するとこんな感じ。

configコマンドは一度だけ実行する必要があります。Webサポートを有効にすると、作成したFlutterアプリはすべてWeb用にコンパイルされます。IDE でデバイスプルダウンの下、または flutterデバイスを使ったコマンドラインで、Chromeとウェブサーバーがリストアップされているのがわかるはずです。Chromeデバイスは自動的に Chrome を起動します。Web サーバーはアプリをホストするサーバーを起動し、どのブラウザからでもアプリを読み込めるようにします。開発中はChromeデバイスを使用してDevToolsを使用できるようにし、他のブラウザでテストする場合はWebサーバーを使用します。

## ステップ1：スターターFlutterアプリを作成する

### まずはテンプレートからFlutterアプリを作成

前回やった[Create the app](https://flutter.dev/docs/get-started/test-drive?tab=vscode#create-app)の手順通りにシンプルなテンプレートのFlutterアプリを作成します。

VS Codeを起動してCommand Paletteにflutterを入力。

![スクリーンショット 2020-08-01 6.08.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62182/101c5bd5-1c68-c967-04ba-de48831af9e4.png)

New Projectを選択してstartup_namerという名前をつけます。

![スクリーンショット 2020-08-01 6.08.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62182/a4fbb399-394b-76fd-e583-8c3a41557b74.png)

※このときコマンドパレットでFlutter: New Projectが表示されていない場合はFlutterとDartのプラグインがインストールされていない可能性があります。

![スクリーンショット 2020-08-01 6.09.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62182/d2a3832c-2e43-80a5-3c97-b4689b4719ab.png)

暫く待つとlib/main.dartが表示されます。

F5を押してシミュレーターも起動させておきます。

### テンプレートを編集

lib/main.dartの内容を置き換えます。main.dartのコードをすべて削除し、画面の中央に「Hello World」と表示するために次のコードをコピペします。

```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 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'),
        ),
      ),
    );
  }
}
```
![スクリーンショット 2020-08-01 6.23.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62182/e3d614e9-f041-bdbc-e6b6-ec27759a30b1.png)

### このコードの解説💡

- この例では、Materialアプリを作成しています。[Material](https://material.io/design)は、モバイルやウェブで標準的に使用されているビジュアルデザイン言語です。Flutter には豊富な Material ウィジェットが用意されています。
- main() メソッドは矢印 (=>) 記法を使用しています。1行の関数やメソッドには矢印表記を使用します。

```dart

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

- アプリはStatelessWidgetを拡張し、アプリ自体をウィジェットにします。Flutterでは、整列、パディング、レイアウトなど、ほとんどすべてがウィジェットになっています。

```dart
class MyApp extends StatelessWidget {...
```
- MaterialライブラリのScaffoldウィジェットは、デフォルトのアプリバー、タイトル、ホーム画面のウィジェットツリーを保持するbodyプロパティを提供します。ウィジェットのサブツリーは非常に複雑です。

```dart
home: Scaffold(
  appBar: AppBar(
    title: Text('Welcome to Flutter'),
  ),
  body: Center(
    child: Text('Hello World'),
  ),
),
```
- ウィジェットの主な仕事は、他の下位レベルのウィジェットの表示方法を記述する build() メソッドを提供することです。

```dart

Widget build(BuildContext context) {
```
- この例のボディは、テキスト子ウィジェットを含むセンターウィジェットで構成されています。Center ウィジェットは、ウィジェットのサブツリーを画面の中央に配置します。

```dart

body: Center(
  child: Text('Hello World'),
),
```

## ステップ2：外部パッケージを使用する

このステップでは[english_words](https://pub.dev/packages/english_words)という名前のオープンソースパッケージを使用します。これには数千の最もよく使われる英語の単語といくつかのユーティリティ関数が含まれています。

english_wordsパッケージ及び他の多くのオープンソースパッケージは[pub.dev](https://pub.dev/)で見つけることができます。

pubspec.yamlファイルは、Flutterアプリのアセットと依存関係を管理します。このpubspec.yamlにenglish_words（v3.1.5以降）を追加します。

```diff:pubspec.yaml
dependencies:            
  flutter:            
    sdk: flutter            
  cupertino_icons: ^0.1.2            
+ english_words: ^3.1.5            
```

保存すると自動で`flutter pub get`が実行され、プロジェクトにインストールされたすべてのパッケージとそのバージョン番号のリストを含む pubspec.lock ファイルも自動生成されます。

lib/main.dartで新しいパッケージをインポートします。

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

文字列「Hello World」を使用する代わりに、英語の単語パッケージを使用してテキストを生成します。

```diff:main.dart
class MyApp extends StatelessWidget {            
  @override            
  Widget build(BuildContext context) {            
+   final wordPair = WordPair.random();            
    return MaterialApp(            
      title: 'Welcome to Flutter',            
      home: Scaffold(
```
```diff:main.dart
        title: Text('Welcome to Flutter'),            
      ),            
      body: Center(            
-       child: Text('Hello World'),            
+       child: Text(wordPair.asPascalCase),            
      ),            
    ),            
  );
```
※`asPascalCase`は最初の単語を含む文字列内の各単語が大文字で始まることを意味します。

これで保存する度にホットリロードが走り、ランダムな単語のペアが表示されるようになります。

![スクリーンショット 2020-08-01 8.25.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62182/16c74647-7e47-77d7-5048-dc4986fdf55d.png)![スクリーンショット 2020-08-01 8.25.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62182/898d0716-2afd-fbc6-e7ec-3a7cc588c72c.png)

## ステップ3：ステートフルウィジェットを追加する

ステートレス・ウィジェットは不変で、そのプロパティは変更できません。

ステートフルウィジェットは、ウィジェットの有効期間中に変更される可能性のある状態を維持します。ステートフルウィジェットを実装するには、少なくとも2つのクラスが必要です。
1) StatefulWidgetのインスタンスを生成するクラス 
2) State クラス
です。ステートフルウィジェットクラスはそれ自体が不変で、捨てたり再生成したりすることができますが、ステートクラスはウィジェットの有効期間中は持続します。

このステップでは、ステートフルウィジェットであるRandomWordsを追加し、そのステートクラスである_RandomWordsStateを作成します。そして、既存のMyAppステートレスウィジェットの中でRandomWordsを子ウィジェットとして使用します。

ステートフルウィジェットのためのボイラプレートコードを作成します。
lib/main.dartで、すべてのコードの後にカーソルを置き、新しい行から始めるためにReturnを数回入力します。stfulと入力するとステートフルウィジェットを作成する候補が出てきます。

![スクリーンショット 2020-08-01 8.37.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62182/a86f9c40-b363-a6da-78d6-ab50c0d0a515.png)

Returnキーを押して承諾すると、2つのクラスのボイラプレートコードが表示されます。

![スクリーンショット 2020-08-01 8.40.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62182/04cfcd01-9cd3-58a7-9108-6adadb5d47aa.png)

カーソルの位置にステートフルウィジェットの名前となるRandomWordsを入力します。
RandomWordsウィジェットはStateクラスを作成する以外にはほとんど何もしません。

![スクリーンショット 2020-08-01 8.41.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62182/4d4c2ea2-e4f0-0920-8982-ea91f2752438.png)

デフォルトでは、State クラスの名前の前に"_"が付きます。識別子の前にアンダーバーを付けることは Dart 言語のプライバシーを保護し、State オブジェクトのベストプラクティスとして推奨されています。

これで両方のクラスは以下のようになりました。

```dart:main.dart

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

class _RandomWordsState extends State<RandomWords> {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}
```

RandomWordsStateのbuild()メソッドを更新します。

```dart:main.dart

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

以下のdiffの通りに変更を行い、MyAppから単語生成コードを削除します。

```diff
class MyApp extends StatelessWidget {                
  @override                
  Widget build(BuildContext context) {                
-   final wordPair = WordPair.random();                
    return MaterialApp(                
      title: 'Welcome to Flutter',                
      home: Scaffold(
```
```diff
        title: Text('Welcome to Flutter'),                
      ),                
      body: Center(                
-       child: Text(wordPair.asPascalCase),                
+       child: RandomWords(),                
      ),                
    ),
  );                
}
```

## ステップ4：無限スクロールListViewを作成する

このステップでは、_RandomWordsStateを展開して、単語のペアリングのリストを生成して表示します。ユーザーがスクロールすると、リスト（ListViewウィジェットに表示される）は無限に増えていきます。ListViewのビルダーファクトリコンストラクタを使用すると、必要に応じてリストビューをlazilyに構築することができます。

提案された単語のペアリングを保存するための _RandomWordsState クラスに _suggestions list を追加します。また、フォントサイズを大きくするための変数 _biggerFont を追加します。

```dart:main.dart

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

次に、_RandomWordsStateクラスに_buildSuggestions()関数を追加します。このメソッドは、提案された単語のペアリングを表示するListViewをビルドします。

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

```dart

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]);
      });
}
```

### このコードの解説💡

itemBuilder コールバックは、提案された単語の組み合わせごとに 1 回呼び出され、各提案を ListTile 行に配置します。偶数行の場合は、単語ペアのリストタイル行が追加されます。奇数行の場合は、分割ウィジェットが追加され、エントリを視覚的に分離します。小さいデバイスでは、分割器が見にくいかもしれないので注意してください。

```dart

itemBuilder: (context, i) {
```

ListViewの各行の前に高さ1ピクセルの分割器ウィジェットを追加します。

```dart

if (i.isOdd) return Divider();
```
式 i ~/ 2 は i を 2 で割って整数の結果を返します。例: 1, 2, 3, 4, 5 は 0, 1, 1, 1, 2, 2 になります。 これは ListView の単語ペアリングの実際の数を計算します。

```dart
final index = i ~/ 2;
```
利用可能な単語ペアリングの数に達した場合は、さらに10個の単語ペアリングを生成してサジェストリストに追加します。

```dart

_suggestions.addAll(generateWordPairs().take(10));
```
_buildSuggestions()関数は、単語ペアごとに1回_buildRow()を呼び出します。

RandomWordsStateに_buildRow()関数を追加。

```dart

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

RandomWordsState クラスの build() メソッドを更新して、単語生成ライブラリを直接呼び出すのではなく、_buildSuggestions() を使用するようにします。(Scaffold は基本的なマテリアルデザインのビジュアルレイアウトを実装しています。) 

```dart

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text('Startup Name Generator'),
    ),
    body: _buildSuggestions(),
  );
}
```

MyAppクラスで、タイトルを変更してbuild()メソッドを更新し、ホームをRandomWordsウィジェットに変更します。

```diff

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

完成です🎉

![スクリーンショット 2020-08-01 9.34.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62182/b3ad71d6-9b99-e483-23a1-827b4490b216.png)

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

```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';
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',            
      home: RandomWords(),
    );
  }
}

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

class _RandomWordsState extends State<RandomWords> {
  final _suggestions = <WordPair>[];
  final _biggerFont = TextStyle(fontSize: 18.0);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Startup Name Generator'),
      ),
      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) {
    return ListTile(
      title: Text(
        pair.asPascalCase,
        style: _biggerFont,
      ),
    );
  }
}
```

# 感想
ソースコード書ける段階にようやく入れて嬉しいです！🎉
Dartを初めて本格的に見たんですが、TSと少し似てるなあと思いながら進めていました。
立ち位置的にも似ているし、思ったよりも今後スムーズに進みそうです。

次回は[Write Your First Flutter App, part 2](https://codelabs.developers.google.com/codelabs/first-flutter-app-pt2/#0)を進めていきます！