Flutterの公式ドキュメントを読んで理解する 〜Introduction to widgets〜

# 目的
Flutter自体は何度か使ったことがあるものの、ただ「使える」だけでなく、ちゃんと理解して使えるようになるべく、一度基本に立ち戻り、公式のドキュメントにしっかり目を通すことにした。
記事の内容はほぼ以下のページの和訳（＋意訳）ですが、~~1週間後の自分と~~、自分と同じように英語のドキュメントを敬遠している方のために記録しました。

# 今回読んだ公式ドキュメントのページ
https://flutter.dev/docs/development/ui/widgets-intro

# 内容
## Hello world
```dart
import 'package:flutter/material.dart';

void main() {
  runApp(
    Center(
      child: Text(
        'Hello, world!',
        textDirection: TextDirection.ltr,
      ),
    ),
  );
}
```
* 最小限のFlutterはアプリは、上記のように`runApp()`の引数に*Widget*を指定することで作成できる
* 上記の例では、`Center` Widgetとその子Widgetである`Text`Widgetが使われている。
* 上記では、`textDirection`が指定されているが、`Center`Widgetの代わりに`MaterialApp`Widgetを指定すると、自動で設定される。
* Widgetには、`StatelessWidget`と`StatefulWidget`の2種類がある。
* そのWidgetが状態を持つか否かに応じて、どちらかの拡張として新たなWidgetを定義する。
* Widgetのメインとなるのが`build`関数である。

## Basic widgets
* Flutterには次のような強力な基本Widgetが備わっている

### Text
* スタイルが適用されたテキストを作成できる。

### Row, Column
* 水平方向(`Row`)と垂直方向(`Column`)といったflexibleなレイアウトを作成できる。
* Webのflexboxレイアウトがベースとなっている。

> (Vue.jsで言うところの`v-row`,`v-col`、SwiftUIの`VStack`,`HStack`と似ているのかな？)

### Stack
* 縦か横に並べる代わりに、`Stack`を使うことで、描画された順に重ねるように配置することができる。
* `Stack`の子として、`Positioned`Widgetを使うと、`Stack`の上端、下端、右端、左端に相対的に配置できる。
* Webで言うところの、absoluteレイアウトがベースとなっている。

### Container
* 長方形の要素を作成できる。
* `BoxDecoration`と合わせて使うことで、背景や境界、陰影のような装飾ができる。
* size, margin, padding, constraintも適用することができる。
* さらに、matrixを使うことで3次元空間に変換できる（！？）

### マテリアルアイコン
`pubspec.yaml`にの`flutter`セクションに`uses-material-design: true`を指定することで、マテリアルアイコンが使えるようになる。

### 上記Widgetの使用例
<p class="codepen" data-height="367" data-theme-id="dark" data-default-tab="js,result" data-user="popy1017" data-slug-hash="QWjJGWJ" data-preview="true" style="height: 367px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="flutter-hello-world-2">
  <span>See the Pen <a href="https://codepen.io/popy1017/pen/QWjJGWJ">
  flutter-hello-world-2</a> by popy1017 (<a href="https://codepen.io/popy1017">@popy1017</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

* `MyAppBar`Widgetは、高さ56px(デバイス依存)・左右のpaddingが8pxの`Container`Widgetを作成している。
* `MyAppBar`Widgetは、子Widgetとして`Row`Widgetを使用している。
* `title`Widgetは、`Expanded`でマークされている。
* `Expanded`Widgetは、他の子WIdgetによって消費されていない残りのスペースを満たすように展開されることを意味している。
* 引数`flex`を指定し利用するスペースの割合を決めることで、複数の子`Expanded`Widgetを持つことができる。
* `MyScaffold`Widgetは、垂直の列に子Widgetを並べている。
* 列の最上部には、`MyAppBar`Widgetを配置し、タイトルで使うために`Text`Widgetを渡している。
* 他のWidgetに引数としてWidgetを渡すのは、様々な使い道で再利用できる一般的なWidgetを作る強力な方法である。
* `MyScaffold`Widgetは、最後に`Expanded`を使ってbodyの残りのスペースを埋めている。

# Using Material Components
* Flutterは、Material Designに従ったアプリを作るのに役立つ多くのWidgetを提供している。
* マテリアルアプリは、`MaterialApp`Widgetで始める。
* `MaterialApp`Widgetは、アプリのRootに多く便利なWidget()を作成してくれる。
* `Navigator`はその一つで、文字列で識別されるWidgetのスタック(Route)を管理する。
* `Navigator`を使うと、画面間の移動がスムーズにできる。
* `MaterialApp`は使っても使わなくても良いが、良い方法である。
* 上記のサンプルを書き換えると、以下のようになる。

<p class="codepen" data-height="576" data-theme-id="dark" data-default-tab="js,result" data-user="popy1017" data-slug-hash="ZEbmLrX" data-preview="true" style="height: 576px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="flutter_materialApp_widget">
  <span>See the Pen <a href="https://codepen.io/popy1017/pen/ZEbmLrX">
  flutter_materialApp_widget</a> by popy1017 (<a href="https://codepen.io/popy1017">@popy1017</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

* `MyAppBar`widgetは`AppBar`widgetで、`MyScaffold`widgetは`Scaffold`widgetで置き換えている。
* これらは、`material.dart`のもので、App barでは陰影があったり、フローティングアクションボタンがあったりなど、よりマテリアルっぽく見えるだろう。
* Widgetは、他のWidgetに引数として渡されることに注意する。
* `Scaffold`widgetは、名前付きの引数として様々なWidgetを受け取り、それぞれは`Scaffold`レイアウトの適切な場所に配置される。
* 同様に、`AppBar`widgetには、`leading`widgetや、`title`widget、`action`widgetを渡せる
* このパターンはフレームワークを通して繰り返され、独自のWidgetを作る際には検討する必要がある。
* もう1つのデザインとして、iOSっぽい見た目の`Cupertino`デザインが用意されている。

>Material デザインのWidgetの方が充実しているらしい。

# Handling gestures
```dart
class MyButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        print('MyButton was tapped!');
      },
      child: Container(
        height: 36.0,
        padding: const EdgeInsets.all(8.0),
        margin: const EdgeInsets.symmetric(horizontal: 8.0),
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(5.0),
          color: Colors.lightGreen[500],
        ),
        child: Center(
          child: Text('Engage'),
        ),
      ),
    );
  }
}
```
* `GestureDetector`widgetは視覚表現は持たないが、代わりにユーザーによるジェスチャーを検出する。
* ユーザーが`Container`をタップしたとき、`GestureDetector`は`onTap()`関数を呼ぶ。
* `GestureDetector`ではタップの他にも、ドラッグやスケールといったジェスチャーも検出できる。
* `GestureDetector`は他のWidgetに任意のコールバックを提供している。
* 例えば、`IconButton, RaisedButton, FloatingActionButton`widgetは、タップ時に発火する`onPressed()`コールバックを持っている。

# Changing widgets in response to input
* `StatelessWidget`は、親のWidgetから引数を受け取り、それを`final`メンバ変数に保持している。
* Widgetが`build()`を要求されると、これらの保存された値を使用して、Widgetが作成される。
* 例えば、ユーザーのインプットを受け付けるような、より複雑なアプリを作るためには、いくつかの状態を保持する必要がある。
* Flutterは、状態を保持するために、`StatefulWidge`を使っている。
> 簡単なアプリ以外では、なるべく`StatefultWidget`を使わず、`BLoC`や`Provider`で状態管理をした方が良いらしい。2020年5月現在は`Provider`による状態管理が主流らしいが、それについては後日勉強する。

```dart
class Counter extends StatefulWidget {
  // This class is the configuration for the state. It holds the
  // values (in this case nothing) provided by the parent and used
  // by the build  method of the State. Fields in a Widget
  // subclass are always marked "final".

  @override
  _CounterState createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _counter = 0;

  void _increment() {
    setState(() {
      // This call to setState tells the Flutter framework that
      // something has changed in this State, which causes it to rerun
      // the build method below so that the display can reflect the
      // updated values. If you change _counter without calling
      // setState(), then the build method won't be called again,
      // and so nothing would appear to happen.
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called,
    // for instance, as done by the _increment method above.
    // The Flutter framework has been optimized to make rerunning
    // build methods fast, so that you can just rebuild anything that
    // needs updating rather than having to individually change
    // instances of widgets.
    return Row(
      children: <Widget>[
        RaisedButton(
          onPressed: _increment,
          child: Text('Increment'),
        ),
        Text('Count: $_counter'),
      ],
    );
  }
}
```
* `StatefulWidget`と`State`が分かれているのは、ライフサイクルが異なるためである。
* `Widget`は一時的なオブジェクトであり、現在の状態におけるアプリの表現に使われる
* `State`オブジェクトは、`build()`の呼び出し間では永続的であり、情報を記憶することができる
* 上記の例では、ユーザーの入力を直接`build()`で使っている。
* より複雑なアプリでは、Widgetの階層の様々な部分が問題の原因の可能性がある。
* 例えば、あるWidgetで日付や場所といった情報を入力させ、別のWidgetでその情報を表示する場合など
* Flutterでは、変更の通知はコールバックによってWidget階層を上に流れる
* 現在の状態は、`StatelessWidget`に下に流れる

```dart
class CounterDisplay extends StatelessWidget {
  CounterDisplay({this.count});

  final int count;

  @override
  Widget build(BuildContext context) {
    return Text('Count: $count');
  }
}

class CounterIncrementor extends StatelessWidget {
  CounterIncrementor({this.onPressed});

  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      onPressed: onPressed,
      child: Text('Increment'),
    );
  }
}

class Counter extends StatefulWidget {
  @override
  _CounterState createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _counter = 0;

  void _increment() {
    setState(() {
      ++_counter;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Row(children: <Widget>[
      CounterIncrementor(onPressed: _increment),
      CounterDisplay(count: _counter),
    ]);
  }
}
```
* 上記の例では、カウンターを表示する部分(`CounterDisplay`)と、カウンターを変更する部分(`CounterIncrementor`)とが明示的に分けてある
* 最終的な結果は同じだが、責任の分離ができ、単純さを維持しながら、個々のWigetでより複雑なものをカプセル化できる。

# Bringing it all together
* ここでは、架空のショッピングアプリを使って、上記の概念を盛り込んだより完全な例を示す
* ショッピングアプリでは、以下ができる
    * 様々な商品を表示する
    * ショッピングカートを維持する

```dart 
class Product {
  const Product({this.name});
  final String name;
}

typedef void CartChangedCallback(Product product, bool inCart);

class ShoppingListItem extends StatelessWidget {
  ShoppingListItem({this.product, this.inCart, this.onCartChanged})
      : super(key: ObjectKey(product));

  final Product product;
  final bool inCart;
  final CartChangedCallback onCartChanged;

  Color _getColor(BuildContext context) {
    // The theme depends on the BuildContext because different parts
    // of the tree can have different themes.
    // The BuildContext indicates where the build is
    // taking place and therefore which theme to use.

    return inCart ? Colors.black54 : Theme.of(context).primaryColor;
  }

  TextStyle _getTextStyle(BuildContext context) {
    if (!inCart) return null;

    return TextStyle(
      color: Colors.black54,
      decoration: TextDecoration.lineThrough,
    );
  }

  @override
  Widget build(BuildContext context) {
    return ListTile(
      onTap: () {
        onCartChanged(product, inCart);
      },
      leading: CircleAvatar(
        backgroundColor: _getColor(context),
        child: Text(product.name[0]),
      ),
      title: Text(product.name, style: _getTextStyle(context)),
    );
  }
}
```
* `ShoppingListItem`widgetは、`StatelessWidget`の一般的パターンに従っている
* コンストラクタで`final`メンバ変数に受け取った値を保持し、`build()`で使う
* 例えば、booleanの`inCartは、現在のテーマのPrimaryカラーと、グレーの2種類の視覚的変化を与える。
* リストのアイテムをタップしたとき、Widgetは`inCart`の値を直接変えるのではない。
* 代わりに、親Widgetから受け取った`onCartChanged`関数を呼ぶ。
* このパターンでは、高い階層のWidgetで状態を管理することができ、状態を長く保持することが可能である。
* 極端な話、`runApp()`に渡されたWidgetに保存された状態は、アプリの存続期間中保持することができる
* 親が`onCartChanged`を受け取ると、親は内部的な状態を更新し、それが新しい`inCart`の値を持った`ShoppingListItem`の新しいインスタンスのリビルド、作成のトリガーとなる
* リビルド時は`ShoppingListItem`の新しいインスタンスを生成するが、フレームワークが前のWidgetと新しいWidgetの差分を比較しているため、軽い操作でできる。

以下は、変更可能な状態を保持する親Widgetの例である。

```dart
class ShoppingList extends StatefulWidget {
  ShoppingList({Key key, this.products}) : super(key: key);

  final List<Product> products;

  // The framework calls createState the first time a widget
  // appears at a given location in the tree.
  // If the parent rebuilds and uses the same type of
  // widget (with the same key), the framework re-uses the State object
  // instead of creating a new State object.

  @override
  _ShoppingListState createState() => _ShoppingListState();
}

class _ShoppingListState extends State<ShoppingList> {
  Set<Product> _shoppingCart = Set<Product>();

  void _handleCartChanged(Product product, bool inCart) {
    setState(() {
      // When a user changes what's in the cart, you need to change
      // _shoppingCart inside a setState call to trigger a rebuild.
      // The framework then calls build, below,
      // which updates the visual appearance of the app.

      if (!inCart)
        _shoppingCart.add(product);
      else
        _shoppingCart.remove(product);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Shopping List'),
      ),
      body: ListView(
        padding: EdgeInsets.symmetric(vertical: 8.0),
        children: widget.products.map((Product product) {
          return ShoppingListItem(
            product: product,
            inCart: _shoppingCart.contains(product),
            onCartChanged: _handleCartChanged,
          );
        }).toList(),
      ),
    );
  }
}

void main() {
  runApp(MaterialApp(
    title: 'Shopping App',
    home: ShoppingList(
      products: <Product>[
        Product(name: 'Eggs'),
        Product(name: 'Flour'),
        Product(name: 'Chocolate chips'),
      ],
    ),
  ));
}
```
* `ShoppingList`クラスは、Widgetが可変な状態を保持する`StatefulWidget`の拡張である。
* `ShoppingList`widgetが最初にツリーに追加されるとき、ツリー内のその場所に関連づける新しいインスタンス`_ShoppingListState`を作成するために、フレームワークは`createState()`関数を呼び出す。
* `State`のサブクラスはたいてい、privateであることを示すためにアンダースコアで始まる名前をつける
* このWidgetの親がリビルドされるとき、親Widgetは`ShoppingList`の新しいインスタンスを作るが、フレームワークは`createState()`を再度呼び出すのではなく、すでにツリーにある`_ShoppingListState`を再利用する。
* 現在の`ShoppingList`のプロパティにアクセスするために、`_ShoppingListState`のプロパティを使うことができる。
* 親Widgetが新しい`ShoppingList`をリビルドして作成する場合、`_ShoppingListState`は新しいWidget値で再構築する
* Widgetのプロパティが変わったときに通知を受け取りたい場合は、`didUpdateWidget()`をオーバーライドする
* それには、`oldWidget`が渡され、古いWidgetと現在のWidget の比較ができる。
* `onCartChanged`コールバックを処理する際、`_ShoppingListState`は`_shoppingCart`に製品を追加または削除することにより、内部状態を変更する
* 内部状態の変更をフレームワークに通知するためには、それらの呼び出しを`setState()`でラップする
* `setState`を呼び出すと、このWidgetはdirtyとしてマークされ、スクリーンを更新する必要がある次のタイミングでリビルドされるようにスケジュールされる。
* Widgetの内部状態を変更する際、`setState`を呼び出すのを忘れると、フレームワークはWidgetがdirtyであることを認識できず、`build()`を呼ばないかもしれない。つまり状態の変更がUIに反映されない
* このやり方で状態を管理することで、子Widgetの作成と更新の分かれたコードを書かなくてすむ
* その代わりに、`build()`を書くだけ

# Responding to widget lifecycle events
* `StatefulWidget`の`createState()`を呼び出した後、フレームワークはツリーに新しい状態オブジェクトを挿入し、その状態オブジェクトの`initState()`を呼び出す。
* `State`のサブクラスは、一度だけ行う必要がある処理を実行するために`initState()`をオーバーライドできる
* 例えば、アニメーションを設定したりとか
* 状態オブジェクトが不要になったときは、`dispose()`が実行される

# Keys
* キーを使用して、Widgetのリビルド時に、他のWidgetと一致するWidgetを制御する
* デフォルトでは、`runtimeType`と表示される順番に応じて現在と以前のWidgetを照合する
* キーを使用すると、2つのWidgetが同じキーと、同じ`runtimeType`を持つ必要がある
* キーは、同じタイプのWidgetのインスタンスをたくさん使うWidgetで役立つ
* 例えば、`ShoppingList`widgetは、`ShoppingListItem`インスタンスを表示領域いっぱいに並べている
    * キーがない場合、現在のbuildの最初のエントリは、スクロールして見えなくなったとしても、いつでも前のbuildの最初のエントリと同期する。(意味がわからん)

>Keyの使い方について調査した。
英語の動画だが、日本語字幕もありわかりやすかった。
https://www.youtube.com/watch?v=kn0EOS-ZiIc
なんとなくだが、`StatefulWidget`の場合、keyとruntimeTypeを見て再描画するかの判定を行うが、デフォルトではkeyがnullになっているため、runtimeTypeが同じWidgetは変更する必要なしと判定されてしまうっぽい。Vue.jsの`v-for`に`key`をつけるのとなんとなく似ている気がする。