1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

目的

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

今回読んだ公式ドキュメントのページ

内容

Hello world

import 'package:flutter/material.dart';

void main() {
  runApp(
    Center(
      child: Text(
        'Hello, world!',
        textDirection: TextDirection.ltr,
      ),
    ),
  );
}
  • 最小限のFlutterはアプリは、上記のようにrunApp()の引数にWidgetを指定することで作成できる
  • 上記の例では、Center Widgetとその子WidgetであるTextWidgetが使われている。
  • 上記では、textDirectionが指定されているが、CenterWidgetの代わりにMaterialAppWidgetを指定すると、自動で設定される。
  • Widgetには、StatelessWidgetStatefulWidgetの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の子として、PositionedWidgetを使うと、Stackの上端、下端、右端、左端に相対的に配置できる。
  • Webで言うところの、absoluteレイアウトがベースとなっている。

Container

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

マテリアルアイコン

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

上記Widgetの使用例

See the Pen flutter-hello-world-2 by popy1017 (@popy1017) on CodePen.

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

Using Material Components

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

See the Pen flutter_materialApp_widget by popy1017 (@popy1017) on CodePen.

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

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

Handling gestures

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'),
        ),
      ),
    );
  }
}
  • GestureDetectorwidgetは視覚表現は持たないが、代わりにユーザーによるジェスチャーを検出する。
  • ユーザーがContainerをタップしたとき、GestureDetectoronTap()関数を呼ぶ。
  • GestureDetectorではタップの他にも、ドラッグやスケールといったジェスチャーも検出できる。
  • GestureDetectorは他のWidgetに任意のコールバックを提供している。
  • 例えば、IconButton, RaisedButton, FloatingActionButtonwidgetは、タップ時に発火するonPressed()コールバックを持っている。

Changing widgets in response to input

  • StatelessWidgetは、親のWidgetから引数を受け取り、それをfinalメンバ変数に保持している。
  • Widgetがbuild()を要求されると、これらの保存された値を使用して、Widgetが作成される。
  • 例えば、ユーザーのインプットを受け付けるような、より複雑なアプリを作るためには、いくつかの状態を保持する必要がある。
  • Flutterは、状態を保持するために、StatefulWidgeを使っている。

簡単なアプリ以外では、なるべくStatefultWidgetを使わず、BLoCProviderで状態管理をした方が良いらしい。2020年5月現在はProviderによる状態管理が主流らしいが、それについては後日勉強する。

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'),
      ],
    );
  }
}
  • StatefulWidgetStateが分かれているのは、ライフサイクルが異なるためである。
  • Widgetは一時的なオブジェクトであり、現在の状態におけるアプリの表現に使われる
  • Stateオブジェクトは、build()の呼び出し間では永続的であり、情報を記憶することができる
  • 上記の例では、ユーザーの入力を直接build()で使っている。
  • より複雑なアプリでは、Widgetの階層の様々な部分が問題の原因の可能性がある。
  • 例えば、あるWidgetで日付や場所といった情報を入力させ、別のWidgetでその情報を表示する場合など
  • Flutterでは、変更の通知はコールバックによってWidget階層を上に流れる
  • 現在の状態は、StatelessWidgetに下に流れる
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

  • ここでは、架空のショッピングアプリを使って、上記の概念を盛り込んだより完全な例を示す
  • ショッピングアプリでは、以下ができる
    • 様々な商品を表示する
    • ショッピングカートを維持する
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)),
    );
  }
}
  • ShoppingListItemwidgetは、StatelessWidgetの一般的パターンに従っている
  • コンストラクタでfinalメンバ変数に受け取った値を保持し、build()で使う
  • 例えば、booleanの`inCartは、現在のテーマのPrimaryカラーと、グレーの2種類の視覚的変化を与える。
  • リストのアイテムをタップしたとき、WidgetはinCartの値を直接変えるのではない。
  • 代わりに、親Widgetから受け取ったonCartChanged関数を呼ぶ。
  • このパターンでは、高い階層のWidgetで状態を管理することができ、状態を長く保持することが可能である。
  • 極端な話、runApp()に渡されたWidgetに保存された状態は、アプリの存続期間中保持することができる
  • 親がonCartChangedを受け取ると、親は内部的な状態を更新し、それが新しいinCartの値を持ったShoppingListItemの新しいインスタンスのリビルド、作成のトリガーとなる
  • リビルド時はShoppingListItemの新しいインスタンスを生成するが、フレームワークが前のWidgetと新しいWidgetの差分を比較しているため、軽い操作でできる。

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

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の拡張である。
  • ShoppingListwidgetが最初にツリーに追加されるとき、ツリー内のその場所に関連づける新しいインスタンス_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

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

Keys

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

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

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?