【Flutter入門】短期集中講座やったので要点を整理してみた

Android／iOS向けアプリ作りたいけどなんかいい開発環境ないかなぁ。
Electronのスマートフォン版あったら便利なんだけどなぁ。
Flutter？聞いたことはあるけどDartが不人気なことで有名だしなぁ。
（念の為改めて調査してみる）
は？Dartめっちゃいいじゃん！なにこのC言語系の正当進化版みたいな可読性！？何が不人気なの？？　→完全にただの記憶違いでしたm(\_ \_)m
Flutterもデスクトップ・モバイル・Webをカバーした理想的な環境っぽいなぁ。
よし、まずは入門だ！
（最終更新：2020.09.27）

## 作業した環境

- OS：macOS Catalina バージョン10.15.7

## 集中講座の元動画様（英語）

[Flutter Crash Course](https://www.youtube.com/watch?v=1gDhl4leEzA)
この記事の位置づけとして、基本的に元動画様に沿って進めていって、補助として個人的に記録しておきたいと思った情報を日本語でわかりやすくざっくりまとめた感じです。

## 作るもの

シンプルな単語ペアジェネレータ。
ランダムに生成された単語ペアをリスト表示する。
気に入った単語ペアはタップで印をつけることができ、別ページで一覧を確認できる。

![完成イメージ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262290/cd1b3041-a238-31fd-63a2-b0cc8a2b1236.png)

## Flutterとは

Google製UIツールキット。
一つの共通化されたコードをもとにモバイル・Web・デスクトップ向けのネイティブクロスプラットフォームアプリが開発できる。
パフォーマンスが極めて高い。

## Dartとは

Flutterが採用しているオブジェクト指向型プログラミング言語。
UIプログラミングに最適化されている。
すべてのプラットフォームで高速に動作する。
文法的には、例えるならJavaの要素を持つJavaScript。

## Widgets

DartではすべてのパーツがWidgetである。
マテリアルデザインを採用している。
Scaffold（土台）、AppBar、Container、Image、Icon、などなど。
StatelessとStatefulがある。
build関数で作成する。

## Stateless Widgets

状態を持たない静的なWidgetのこと。
実行中変化せず、自身を再描画する必要がないUIに使われる。

## Statefull Widgets

状態を持っている動的なWidgetのこと。
実行中に変化し、自身を再描画する必要があるUIに使われる。

## 開発環境構築

ざっくりと今回必要な環境をまとめておきます。

- Flutter本体
  - [Flutter](https://flutter.dev/docs/get-started/install)から環境にあったFlutterをインストール（圧縮ファイルを解凍して任意の場所に配置）
  - flutter/binにパスを通す

- Visual Studio Code
  - [Visual Studio Code](https://azure.microsoft.com/ja-jp/products/visual-studio-code)からインストール
  - Flutterエクステンションを追加
  - Dartエクステンションを追加
- Xcode
  - AppStoreから最新のXcodeをインストール
  - 下記コマンドを実行してXcodeを設定
    - `sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer`
    - `sudo xcodebuild -runFirstLaunch`
- Android Studio
  - [Android Studio](https://developer.android.com/studio?hl=ja)から最新のAndroid Studioをインストール
  - Flutterプラグインを追加
  - 仮想デバイスを作成

## 開発環境構築チェック

テンプレートアプリを動かして開発環境が構築されたことを確認します。
※ここまでで詳細な手順やトラブルシューティングは省いているので、適宜自分で調べながらになるかと思います。

ターミナルで作業ディレクトリに移動して下記コマンドを実行

```bash
flutter create wordpair_generator
cd wordpair_generator
code .
```

★ターミナルから`code`コマンドが使えない場合は一度VSCodeを開いて、

```txt
Cmd+Shift+P
shell と入力
Shell Command: Install 'code' command in PATH を選択
```

とすると使えるようになる。

wordpair_generatorプロジェクトをVSCodeで開けたら、右下の「No Device」をクリックして「Start iOS Simulator」を選択し、シミュレータを立ち上げます。

上部のメニューから、
Run > Start Debugging > Dart & Flutter
と選択してデバッグ起動します。

テンプレートアプリが無事起動し、右下の＋ボタンをクリックしたら画面中央の数字が増えていく、という挙動を確認できればOKです。

![テンプレートアプリ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262290/cbfb1433-0856-1cfb-0afa-e60ced0b6420.png)

## コーディング準備

コードを打ち込んで行く前に、いくつか準備しておきます。

私の場合、Dartのおすすめセッティングとかいうものを適用した結果、保存のたびに強制フォーマットされてめちゃくちゃやりにくかったのでコマンドパレット`Cmd+Shift+P`から`open settings`と打って設定ファイル（json）を開き、設定を一部OFFにしました。

また、動画とは順番が前後しますが、事前に依存パッケージをインストールしておくとスムーズかもしれません。

```yaml:pubspec.yaml
# 〜（省略）〜
dependencies:
  flutter:
    sdk: flutter
  english_words: ^3.1.5 # この行を追加
# 〜（省略）〜
```

## コーディング（main.dart）

エントリポイントのある、メインファイルです。
これだけだとまだ動きません。

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(primaryColor: Colors.purple[900]),
      home: RandomWords()
    );
  }
}
```

## コーディング（random_words.dart）

画面の構成やランダムな単語ペアの生成・表示といったロジックを担当するファイルです。

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

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

class RandomWordsState extends State<RandomWords> {
  final _randomWordPairs = <WordPair>[];
  final _savedWordPairs = Set<WordPair>();

  Widget _buildList() {
    return ListView.builder(
      padding: const EdgeInsets.all(16),
      itemBuilder: (context, item) {
        if (item.isOdd) {
          return Divider();
        }

        final index = item ~/ 2;

        if (index >= _randomWordPairs.length) {
          _randomWordPairs.addAll(generateWordPairs().take(10));
        }

        return _buildRow(_randomWordPairs[index]);
      }
    );
  }

  Widget _buildRow(WordPair pair) {
    final alreadySaved = _savedWordPairs.contains(pair);

    return ListTile(
      title: Text(pair.asPascalCase, style: TextStyle(fontSize: 18.0)),
      trailing: Icon(
        alreadySaved ? Icons.favorite : Icons.favorite_border,
        color: alreadySaved ? Colors.red : null),
      onTap: () {
        setState(() {
          if (alreadySaved) {
            _savedWordPairs.remove(pair);
          }
          else {
            _savedWordPairs.add(pair);
          }
        });
      }
    );
  }
  
  void _pushSaved() {
    Navigator.of(context).push(
      MaterialPageRoute(
        builder: (BuildContext context) {
          final Iterable<ListTile> tiles = _savedWordPairs.map((WordPair pair) {
            return ListTile(
              title: Text(pair.asPascalCase, style: TextStyle(fontSize: 16.0))
            );
          });

          final List<Widget> divided = ListTile.divideTiles(
            context: context,
            tiles: tiles
          ).toList();

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

  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('WordPair Generator'),
        actions: <Widget>[
          IconButton(
            icon: Icon(Icons.list),
            onPressed: _pushSaved)
        ]),
      body: _buildList()
    );
  }
}
```

## 特に気になった文法

Dartの文法はJavaやJavaScriptの流れを組んだような、素直でわかりやすい文法だと感じていますが、初見で特に気になったりわからなかったものについてまとめます。

- 「~/」除算、整数（int）の結果を返す
- 「_」アンダースコア始まりの識別子でプライベートアクセス指定を表す

## トラブルシューティング

- 実行したままコードをいじるとエラーが出て何も表示されなくなる
  - 原因はわかりませんが、実行中のアプリを再起動すると解決しました。

## あとがき

Dartについて、よく考えられた直感的な命名の各種機能がそろっていて、文法も素直で堅実な印象で書いていて楽しいと感じました。
ただ、UIに最適化された言語だからなのか、コンストラクタにコンストラクタをどんどん重ねて作っていくやりかたがやりやすいので、気をつけないとついつい"ダーティー"なコードを書いてしまいがちとも思いました。
そこがまたいいのかもしれませんね。