LoginSignup
37
18

More than 5 years have passed since last update.

[翻訳] A Tour of the Flutter Widget Framework

Last updated at Posted at 2017-02-01

https://flutter.io/widgets-intro/ のほぼGoogle翻訳ベース日本語訳です。記事ではFlutterでUIを構築するベースとなるWidgetについて解説しており、React/Fluxインスパイアな印象で実装しやすそうです :sparkles:

Introduction

Flutter Widgetは、Reactからインスピレーションを受けた関数型リアクティブフレームワークを使用して構築されています。 主なアイデアは、UIをWidgetから構築することです。 Widgetは、現在の設定と状態を考慮して、ビューの外観を記述します。 Widgetの状態が変わると、Widgetは描画を再構築します。ある状態から次の状態に遷移する際の内部的なレンダリングツリーに加えられる変更を最小にするため、フレームワークは一つ前の描画との差分を計算します。

Hello World

最小のFlutterアプリケーションはWidgetを一つ引数に取るrunApp関数を呼び出すものです:

import 'package:flutter/material.dart';

void main() {
  runApp(new Center(child: new Text('Hello, world!')));
}

スクリーンショット 2017-01-29 16.50.03.png

runApp関数は与えられたWidgetをWidgetツリーのルートにします。 この例では、Widgetツリーは2つのWidget、つまり Center Widgetとその子である Text Widgetで構成されています。 フレームワークは、ルートWidgetに画面を覆わせるよう強制します。これは、テキスト「Hello、world」が画面の中央に表示されることを意味します。

アプリケーションの作成時には、Widgetが状態を管理するかどうかによって StatelessWidget または StatefulWidget のサブクラスである新しいWidgetを作成します。 Widgetの主な仕事は、build関数を実装することです。build関数は、Widgetを配下に含むWidgetの集まりとして記述します。 フレームワークは、そのプロセスが基礎となるレンダーオブジェクトを表すWidget内で終わるまで、それらのWidgetを順番に構築します。

Basic widgets

Main article: Widgets Overview - Layout Models

Flutterには強力な基本Widgetが付属していますが、その中で次のものがよく使用されています:

  • TextText Widgetを使用すると、アプリケーション内でスタイル付きのテキストを作成できます。

  • Row, Column: これらのフレックスWidgetを使用すると、水平方向(Row)と垂直方向(Column)の両方で柔軟なレイアウトを作成できます。そのデザインはウェブのフレックスボックスレイアウトモデルに基づいています。

  • StackStack Widgetを使用すると、直線的に(水平方向または垂直方向に)指向させるのではなく、Widgetをペイント順にスタックすることができます。スタックの子に Positioned Widgetを使用すると、スタックの上端、右端、下端、または左端を基準にして配置できます。スタックは、Webの絶対配置レイアウトモデルに基づいています。

  • ContainerContainer Widgetでは、四角形のビジュアルエレメントを作成できます。Containerは、背景、罫線、または影などの BoxDecoration で飾ることができます。Containerは、マージン、パディング、およびサイズに制約を適用することもできます。さらに、Containerは、行列を使用して3次元空間で変換することができます。

以下は、これらのWidgetと他のWidgetを組み合わせた単純なWidgetの例です:

import 'package:flutter/material.dart';

class MyAppBar extends StatelessWidget {
  // 訳追記 Dartの名前付きコンストラクタ記法と、クラスフィールドへのアサインを省略する記法の組み合わせ
  // new MyAppBar(title: foo)
  MyAppBar({this.title});

  // ウィジェットサブクラスのフィールドは常にfinalとマークされます
  final Widget title;

  @override
  Widget build(BuildContext context) {
    return new Container(
      height: 56.0, // in logical pixels
      padding: const EdgeInsets.symmetric(horizontal: 8.0),
      decoration: new BoxDecoration(backgroundColor: Colors.blue[500]),
      // Row is a horizontal, linear layout.
      child: new Row(
        // <Widget> is the type of items in the list.
        children: <Widget>[
          new IconButton(
            icon: new Icon(Icons.menu),
            tooltip: 'Navigation menu',
            onPressed: null, // null disables the button
          ),
          // Expanded expands its child to fill the available space.
          new Expanded(
            child: title,
          ),
          new IconButton(
            icon: new Icon(Icons.search),
            tooltip: 'Search',
            onPressed: null,
          ),
        ],
      ),
    );
  }
}

class MyScaffold extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Material is a conceptual piece of paper on which the UI appears.
    return new Material(
      // Column is a vertical, linear layout.
      child: new Column(
        children: <Widget>[
          new MyAppBar(
            title: new Text(
              'Example title',
              style: Theme.of(context).primaryTextTheme.title,
            ),
          ),
          new Expanded(
            child: new Center(
              child: new Text('Hello, world!'),
            ),
          )
        ],
      ),
    );
  }
}

void main() {
  runApp(new MaterialApp(
    title: 'My app', // used by the OS task switcher
    home: new MyScaffold(),
  ));
}

スクリーンショット 2017-01-29 16.58.20.png

pubspec.yamlファイルのflutterセクションにuses-material-design:trueエントリがあることを確認してください。 これにより、定義済みのマテリアルアイコンのセットを使用することができます。

name: my_app
flutter:
  uses-material-design: true

多くのWidgetは、テーマデータを継承し表示を適切にするために MaterialApp 関数でラップする必要があります。したがって、 MaterialApp を使用してアプリケーションを実行します。

MyAppBarWidget は、デバイスに依存しない56ピクセルの高さを持ち、左右両方に内部パディングが8ピクセルの Container を作成します。Container の内部では、 MyAppBarRow レイアウトを使用して子を整理します。真ん中のchild、タイトルWidgetは Expanded とマークされます。つまり、他の子が消費していない残りの空き領域を埋めるように展開されます。複数の Expanded 子を持つことができ、 flex 引数を Expanded にとすることで使用可能な領域を消費する割合を決定できます。

MyScaffoldWidget は、その子を縦の列に編成します。列の最上部では、 MyAppBar のインスタンスを配置し、アプリケーションバーにテキストWidgetを渡してタイトルとして使用します。Widgetを他のWidgetに引数として渡すことは、さまざまな方法で再利用できる汎用Widgetを作成する強力な手法です。最後に、 MyScaffold は、 Expanded を使用して、残りのスペースを本体に埋め込みます。本体は、中央に配置されたメッセージで構成されています。

Using material design

Main article: Widgets Overview - Material Design Widgets

Flutterには、Material Designに従ったアプリケーションを構築するのに役立つ多数のWidgetが用意されています。 マテリアルデザインアプリは MaterialApp Widgetで始まります。アプリケーションのルートには、文字列で識別されたWidgetのスタック(「ルート」とも呼ばれます)を管理するナビゲータを含む多くの有用なWidgetが構築されています。 ナビゲータを使用すると、アプリケーションの画面間をスムーズに移行できます。 MaterialApp Widgetの使用は完全にオプションですが、良い方法です。

import 'package:flutter/material.dart';

void main() {
  runApp(new MaterialApp(
    title: 'Flutter Tutorial',
    home: new TutorialHome(),
  ));
}

class TutorialHome extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Scafold is a layout for the major material design widgets.
    return new Scaffold(
      appBar: new AppBar(
        leading: new IconButton(
          icon: new Icon(Icons.menu),
          tooltip: 'Navigation menu',
          onPressed: null,
        ),
        title: new Text('Example title'),
        actions: <Widget>[
          new IconButton(
            icon: new Icon(Icons.search),
            tooltip: 'Search',
            onPressed: null,
          ),
        ],
      ),
      // body is the majority of the screen.
      body: new Center(
        child: new Text('Hello, world!'),
      ),
      floatingActionButton: new FloatingActionButton(
        tooltip: 'Add', // used by assistive technologies
        child: new Icon(Icons.add),
        onPressed: null,
      ),
    );
  }
}

スクリーンショット 2017-01-29 17.24.51.png

MyAppBarMyScaffold をmaterial.dartの AppBarScaffold Widgetに切り替えたので、私たちのアプリはMaterial Designのように少し見えるようになりました。 たとえば、アプリケーションバーには影があり、タイトルテキストは正しいスタイルを自動的に継承します。 また、おまけにフローティングアクションボタンも追加しました。

他のWidgetへの引数としてWidgetを再び渡していることに注目してください。 Scaffold Widgetは、名前付き引数としていくつかの異なるWidgetを取ります。それぞれのWidgetは、適切な場所の Scaffold レイアウトに配置されています。 同様に、 AppBar Widgetは、 title Widgetの leadingactions Widgetを渡すことができます。 このパターンはフレームワーク全体で繰り返され、独自のWidgetを設計する際に考慮する可能性があります。

Handling gestures

Main article: Gestures in Flutter

ほとんどのアプリケーションには、システムとのユーザーのインタラクションが含まれています。インタラクティブなアプリケーションを構築するための第一歩は、入力ジェスチャーを検出することです。 シンプルなボタンを作成してその動作を見てみましょう:

class MyButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new GestureDetector(
      onTap: () {
        print('MyButton was tapped!');
      },
      child: new Container(
        height: 36.0,
        padding: const EdgeInsets.all(8.0),
        margin: const EdgeInsets.symmetric(horizontal: 8.0),
        decoration: new BoxDecoration(
          borderRadius: new BorderRadius.circular(5.0),
          backgroundColor: Colors.lightGreen[500],
        ),
        child: new Center(
          child: new Text('Engage'),
        ),
      ),
    );
  }
}

GestureDetectorWidget には視覚的な表現はなく、代わりにユーザーが行ったジェスチャーが検出されます。 ユーザーが Container をタップすると、 GestureDetectoronTapコールバックを呼び出します。この場合、コンソールにメッセージが出力されます。 GestureDetector を使用して、タップ、ドラッグ、スケールなどのさまざまな入力ジェスチャを検出できます。

多くのWidgetは GestureDetector を使用して、他のWidgetに対してオプションのコールバックを提供します。 たとえば、 IconButtonRaisedButton 、および FloatingActionButton Widgetには、ユーザーがWidgetをタップしたときにトリガーされる onPressed コールバックがあります。

Changing widgets in response to input

Main articles: StatefulWidget, State.setState

ここまでは、ステートレスなWidgetしか使用していませんでした。 ステートレスWidgetは、親Widgetから引数を受け取り、最終的なメンバー変数に格納します。 Widgetの build 関数が呼ばれると、格納されたこれらの値を使用して、作成されたWidgetの新しい引数が生成されます。

たとえば、より興味深い方法でユーザの入力に反応するなど、より複雑なエクスペリエンスを構築するために、アプリケーションは通常、ある状態を保持します。 Flutterは StatefulWidgets を使用してこのアイデアを実行します。 StatefulWidgets はStateオブジェクトを生成する方法を知っている特別なWidgetで、Stateオブジェクトを保持するために使用されます。 前述の RaisedButton を使用して、この基本的な例を考えてみましょう。

class Counter extends StatefulWidget {
  // このクラスは状態の設定を行います。 それは親から渡された値を保持し(この場合は何もない)
  // Stateのbuildメソッドで使われます。 Widgetサブクラスのフィールドは、
  // 常に「final」とマークされます。

  // 訳追加: Dartではアンダースコアで始まるフィールドやクラスなどはprivateとして扱われます
  @override
  _CounterState createState() => new _CounterState();
}

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

  void _increment() {
    setState(() {
      // setStateを呼ぶとFlutterフレームワークにこのStateで値の変化が起こった
      // ことを知らせます。それによりbuildメソッドが再度実行される可能性があり、
      // それにより更新された値を表示に反映することができます。
      // setState()を呼び出さずに_counterを変更した場合、
      // buildメソッドは再び呼び出されないため、何も起こらないように見えます。
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    // このメソッドは、上記の_incrementCounterメソッドのように、
    // setStateが呼び出されるたびに再実行されます。 
    // Flutterフレームワークは、ビルドメソッドの再実行を高速化するように
    // 最適化されているため、Widgetのインスタンスを個別に変更する必要がなく、
    // 更新が必要なものだけを再構築できます。
    return new Row(
      children: <Widget>[
        new RaisedButton(
          onPressed: _increment,
          child: new Text('Increment'),
        ),
        new Text('Count: $_counter'),
      ],
    );
  }
}

あなたはなぜStatefulWidgetとStateが別々のオブジェクトであるのか不思議に思うかもしれません。 Flutterでは、これらの2つのタイプのオブジェクトは異なるライフサイクルを持ちます。 Widgetは一時オブジェクトであり、現在の状態でアプリケーションのプレゼンテーションを構築するために使用されます。 一方、Stateオブジェクトは、 build() の呼び出しの間、永続的であり、情報を覚えておくことができます。

上記の例では、ユーザーの入力を受け入れ、ビルドメソッドで直接結果を使用しています。 より複雑なアプリケーションでは、Widget階層のさまざまな部分が異なる懸念を引き起こす可能性があります。 たとえば、あるWidgetは、日付や場所などの特定の情報を収集する目的で複雑なユーザーインターフェースを提示し、別のWidgetはその情報を使用してプレゼンテーション全体を変更することがあります。

Flutterでは、変更通知は、現在の状態がプレゼンテーションを行うステートレスWidgetに「ダウン」しながら、コールバックを使用してWidget階層を「上」にフローさせます。 このフローをリダイレクトする共通の親はStateです。 このやや複雑な例を使って、実際の動作を見てみましょう:

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

  final int count;

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

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

  final VoidCallback onPressed;

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

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

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

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

  @override
  Widget build(BuildContext context) {
    return new Row(children: <Widget>[
      new CounterIncrementor(onPressed: _increment),
      new CounterDisplay(count: _counter),
    ]);
  }
}

カウンタの表示(CounterDisplay)とカウンタの変更(CounterIncrementor)の関心を明確に分離して、2つの新しいステートレスWidgetをどのように作成したかに注目してください。 結果は前の例と同じですが、責任の分離により、親の単純さを維持しながら個々のWidgetに複雑さをカプセル化することができます。

Bringing it all together

上で紹介した概念をまとめたより完全な例を考えてみましょう。 様々な製品を表示し、商品をショッピングカートに入れることができる架空のショッピングアプリケーションを作ってみます。 プレゼンテーションクラス ShoppingListItem を定義することから始めましょう:

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

// 訳注: CartChangedCallbackという名前で(Product, bool) => voidな型を定義
typedef void CartChangedCallback(Product product, bool inCart);

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

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

  Color _getColor(BuildContext context) {
    // テーマは、ツリーのさまざまな部分が異なるテーマを持つことができるため、
    // BuildContextに依存します。 BuildContextはビルドがどこで行われているか、
    // したがってどのテーマを使用するかを示します。

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

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

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

  @override
  Widget build(BuildContext context) {
    return new ListItem(
      onTap: () {
        onCartChanged(product, !inCart);
      },
      leading: new CircleAvatar(
        backgroundColor: _getColor(context),
        child: new Text(product.name[0]),
      ),
      title: new Text(product.name, style: _getTextStyle(context)),
    );
  }
}

ShoppingListItemWidget は、ステートレスWidgetの共通パターンに従います。 コンストラクタで受け取った値を final メンバ変数に格納し、 build 関数で使用します。 たとえば、現在のテーマのプライマリカラーとグレーを使用する別の視覚アピアランスを inCart ブール値を使って切り替えます。

ユーザーがリストアイテムをタップすると、Widgetはその inCart 値を直接変更しません。 代わりに、Widgetは親Widgetから受け取った onCartChanged 関数を呼び出します。 このパターンを使用すると、状態をWidget階層の上位に格納できます。これにより、状態が長期間維持されます。 極端な場合、 runApp に渡されたWidgetに格納されている状態は、アプリケーションの存続期間中存続します。

親が onCartChanged コールバックを受け取ると、親はその内部状態を更新し、親が新しい inCart 値を持つ ShoppingListItem の新しいインスタンスを再構築して作成するようにします。 親は再構築時に ShoppingListItem の新しいインスタンスを作成しますが、フレームワークは新しく構築されたWidgetと以前に構築されたWidgetを比較し、その違いのみを基礎となるレンダーオブジェクトに適用します。

mutableな状態を格納する親Widgetの例を見てみましょう:

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

  final List<Product> products;

  // フレームワークは、Widgetがツリーの所定の場所に初めて表示されたときに
  // createStateを呼び出します。 親が同じタイプのWidget(同じKeyを持つ)を
  // 再構築して使用する場合、フレームワークは新しいStateオブジェクトを作成する代わりに
  // Stateオブジェクトを再利用します。

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

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

  void _handleCartChanged(Product product, bool inCart) {
    setState(() {
      // ユーザーがカート内のものを変更するときは、setState呼び出しの中で
      // _shoppingCartを変更して再構築をトリガーする必要があります。
      // フレームワークは次に配下Widgetのbuild関数を呼び出して、
      // アプリケーションの表示を更新します。

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

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Shopping List'),
      ),
      body: new MaterialList(
        type: MaterialListType.oneLineWithAvatar,
        children: config.products.map((Product product) {
          return new ShoppingListItem(
            product: product,
            inCart: _shoppingCart.contains(product),
            onCartChanged: _handleCartChanged,
          );
        }),
      ),
    );
  }
}

final List<Product> _kProducts = <Product>[
  new Product(name: 'Eggs'),
  new Product(name: 'Flour'),
  new Product(name: 'Chocolate chips'),
];

void main() {
  runApp(new MaterialApp(
    title: 'Shopping List',
    home: new ShoppingList(products: _kProducts),
  ));
}

スクリーンショット 2017-01-29 18.28.05.png

ShoppingList クラスは StatefulWidget を継承します。つまり、このWidgetは可変状態を格納します。 ShoppingListWidget が最初にツリーに挿入されると、フレームワークは createState 関数を呼び出して _ShoppingListState の新しいインスタンスを作成し、ツリー内のその位置に関連付けます。 (一般的にStateのサブクラスには、プライベートな実装の詳細であることを示すために、名前の先頭にアンダースコアを付けることに注意してください)。このWidgetの親が再構築されると、親は ShoppingList の新しいインスタンスを作成しますが、フレームワークは createState を再度呼び出すのではなく、すでにツリー内にある _ShoppingListState インスタンスを再利用します。

現在の ShoppingList のプロパティにアクセスするには、 _ShoppingListState はその config プロパティを使用できます。 親が再構築して新しい ShoppingList を作成する場合、 _ShoppingListState も新しい config の値で再構築されます。 config プロパティが変更されたときに通知を受けたい場合は、 oldConfig に渡される didUpdateConfig 関数をオーバーライドして、古い config と現在の config を比較させることができます。

onCartChanged コールバックを処理するとき、 _ShoppingListState は、 _shoppingCart から商品を追加または削除することによって、内部状態を変更します。 フレームワークが内部状態を変更することを通知するために、これらの呼び出しを setState 呼び出しでラップします。 setState を呼び出すと、このWidgetはダーティとマークされ、次にアプリが画面を更新する必要があるときに再構築されるようにスケジュールされます。 Widgetの内部状態を変更するときに setState を忘れた場合、フレームワークはWidgetが汚れているとは認識せず、Widgetのビルド関数を呼び出さない可能性があります。

このように状態を管理することで、子Widgetを作成および更新するためのコードを別途記述する必要はありません。 代わりに、両方の状況を処理する build 関数を実装するだけです。

Responding to widget lifecycle events

Main article: State

StatefulWidgetで createState を呼び出した後、フレームワークは新しい状態オブジェクトをツリーに挿入し、状態オブジェクトに対して initState を呼び出します。 State のサブクラスは、 initState をオーバーライドして、1回だけ実行する必要のある作業を行うことができます。 たとえば、 initState をオーバーライドしてアニメーションを設定したり、プラットフォームサービスに登録することができます。 initState の実装は、 super.initStateを呼び出して開始する必要があります。

状態オブジェクトがもはや必要でなくなると、フレームワークは状態オブジェクトに対して dispose を呼び出します。 dispose 関数をオーバーライドしてクリーンアップ作業を行うことができます。 たとえば、 dispose をオーバーライドしてタイマーをキャンセルしたり、プラットフォームサービスの unsbscribe することができます。 dispose の実装は、通常、 super.dispose を呼び出すことで終了します。

Keys

Main article: Key

Widgetの再構築時にキーを使用して、フレームワークがどの他のWidgetと一致するかを制御することができます。 デフォルトでは、フレームワークは runtimeType と表示される順序に従って、現在のビルドと前のビルドのWidgetを一致させます。 キーでは、フレームワークは2つのWidgetが同じキーと同じ runtimeType を持つことを要求します。

キーは、同じ種類のWidgetのインスタンスを多数作成するWidgetで最も便利です。 たとえば、 ShoppingListItem インスタンスを構築して可視領域を埋める ShoppingListWidget は、次のようになります。

  • キーがなければ、現在のビルドの最初のエントリは、意味的にリストの最初のエントリが画面からスクロールしてもはやビューポートに表示されなくても、前のビルドの最初のエントリと常に同期します。
  • リスト内の各エントリに「セマンティック」キーを割り当てることにより、フレームワークは一致するセマンティックキー、したがって同様の(または同一の)視覚的外観を持つエントリを同期させるので、無限リストをより効率的にすることができる。 さらに、エントリを意味的に同期するということは、ステートフルな子Widgetに保持されている状態が、ビューポート内の同じ数値位置のエントリではなく、同じセマンティックエントリに関連付けられたままであることを意味します。

Global Keys

Main article: GlobalKey

グローバルキーを使用して、子Widgetを一意に識別できます。 グローバルキーは、兄弟間で一意である必要のあるローカルキーとは異なり、Widget階層全体でグローバルに一意でなければなりません。 グローバルに一意であるため、グローバルキーを使用してWidgetに関連付けられた状態を取得できます。

37
18
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
37
18