https://flutter.io/widgets-intro/ のほぼGoogle翻訳ベース日本語訳です。記事ではFlutterでUIを構築するベースとなるWidgetについて解説しており、React/Fluxインスパイアな印象で実装しやすそうです
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!')));
}
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が付属していますが、その中で次のものがよく使用されています:
-
Text
:Text
Widgetを使用すると、アプリケーション内でスタイル付きのテキストを作成できます。 -
Row
,Column
: これらのフレックスWidgetを使用すると、水平方向(Row)と垂直方向(Column)の両方で柔軟なレイアウトを作成できます。そのデザインはウェブのフレックスボックスレイアウトモデルに基づいています。 -
Stack
:Stack
Widgetを使用すると、直線的に(水平方向または垂直方向に)指向させるのではなく、Widgetをペイント順にスタックすることができます。スタックの子にPositioned
Widgetを使用すると、スタックの上端、右端、下端、または左端を基準にして配置できます。スタックは、Webの絶対配置レイアウトモデルに基づいています。 -
Container
:Container
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(),
));
}
pubspec.yamlファイルのflutterセクションにuses-material-design:trueエントリがあることを確認してください。 これにより、定義済みのマテリアルアイコンのセットを使用することができます。
name: my_app
flutter:
uses-material-design: true
多くのWidgetは、テーマデータを継承し表示を適切にするために MaterialApp
関数でラップする必要があります。したがって、 MaterialApp
を使用してアプリケーションを実行します。
MyAppBarWidget
は、デバイスに依存しない56ピクセルの高さを持ち、左右両方に内部パディングが8ピクセルの Container
を作成します。Container
の内部では、 MyAppBar
は Row
レイアウトを使用して子を整理します。真ん中の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,
),
);
}
}
MyAppBar
と MyScaffold
をmaterial.dartの AppBar
と Scaffold
Widgetに切り替えたので、私たちのアプリはMaterial Designのように少し見えるようになりました。 たとえば、アプリケーションバーには影があり、タイトルテキストは正しいスタイルを自動的に継承します。 また、おまけにフローティングアクションボタンも追加しました。
他のWidgetへの引数としてWidgetを再び渡していることに注目してください。 Scaffold
Widgetは、名前付き引数としていくつかの異なるWidgetを取ります。それぞれのWidgetは、適切な場所の Scaffold
レイアウトに配置されています。 同様に、 AppBar
Widgetは、 title
Widgetの leading
と actions
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
をタップすると、 GestureDetector
は onTap
コールバックを呼び出します。この場合、コンソールにメッセージが出力されます。 GestureDetector
を使用して、タップ、ドラッグ、スケールなどのさまざまな入力ジェスチャを検出できます。
多くのWidgetは GestureDetector
を使用して、他のWidgetに対してオプションのコールバックを提供します。 たとえば、 IconButton
、 RaisedButton
、および 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),
));
}
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に関連付けられた状態を取得できます。