※自分の学習記録のメモとしてこの記事を書いています。
1日目:Flutter学習1日目 -公式を読む 1. インストール編-
2日目前編:Flutter学習2日目前編 -公式を読む 2. エディターの設定編-
2日目後編:Flutter学習2日目後編 -公式を読む 3. テストドライブ編-
3日目:Flutter学習3日目 -公式を読む 4. スターターFlutterアプリを作成する-
4日目:Flutter学習4日目 -公式を読む 5. インタラクティブ機能を実装-
5日目:Flutter学習5日目 -公式を読む 6. Web開発者のためのFlutter-
6日目:Flutter学習6日目 -公式を読む 7. ウィジェットの紹介-(イマココ)
日が空いてしまいましたが挫けずに今週も更新していきます…。😌
今日は公式のIntroduction to widgets
ページを読み進めます!
はじめに
Flutterウィジェットは、Reactからインスピレーションを得た最新のフレームワークを使用して構築されています。ウィジェットからUIを構築することが主な考え方です。ウィジェットは現在の構成と状態が与えられた場合のビューの外観を記述します。ウィジェットの状態が変化すると、ウィジェットはその記述を再構築します。フレームワークは、ある状態から次の状態に移行するために必要な、基礎となるレンダーツリーの最小の変更を決定するために、以前の記述と比較して、その記述を再構築します。
Hello world
最小限のFlutterアプリ runApp() はウィジェットで関数を呼び出すだけです。
import 'package:flutter/material.dart';
void main() {
runApp(
Center(
child: Text(
'Hello, world!',
textDirection: TextDirection.ltr,
),
),
);
}
runApp() 関数は、与えられたウィジェットを受け取り、それをウィジェットツリーのルートにします。この例では、ウィジェットツリーは、 Center ウィジェットとその子ウィジェットである Text ウィジェットの2つのウィジェットで構成されています。フレームワークは、ルートウィジェットが画面をカバーするように強制的に設定します。この例では、テキストの方向を指定する必要があります。 MaterialApp ウィジェットを使用する場合は、後ほど説明するように、この処理を行います。
アプリを書くときには、ウィジェットがステートを管理するかどうかに応じて、 StatelessWidget または StatefulWidget のサブクラスである新しいウィジェットを作成するのが一般的です。ウィジェットの主な仕事は build() 関数を実装することです。フレームワークは、ウィジェットのジオメトリを計算して記述する RenderObject を表すウィジェットでプロセスがボトムアウトするまで、これらのウィジェットを順番にビルドしていきます。
基本的なウィジェット
Flutterには強力な基本ウィジェットが付属していますが、その中でも以下のものがよく使われています。
Text
Text ウィジェットを使用すると、アプリケーション内でスタイル化されたテキストを作成することができます。
Row, Column
これらのフレックスウィジェットを使用すると、水平方向(Row)と垂直方向(Column)の両方の方向に柔軟なレイアウトを作成できます。これらのオブジェクトの設計は、Webのフレックスボックスレイアウトモデルに基づいています。
Stack
直線的な方向 (水平または垂直) の代わりに、Stack ウィジェットでは、ペイント順にウィジェットを重ねて配置することができます。そして、Stack の子ウィジェットで Positioned ウィジェットを使用して、スタックの上、右、下、左端からの相対的な位置に配置することができます。Stack はWebの絶対配置(absolute)レイアウトモデルに基づいています。
Container
Container ウィジェットを使用すると、長方形のビジュアル要素を作成することができます。コンテナは背景、ボーダー、影などの BoxDecoration で装飾できます。Containerは、そのサイズに適用されるマージン、パディング、および制約を設定することもできます。さらに、Containerはmatrixを使用して3次元空間で変換できます。
以下は、これらのウィジェットと他のウィジェットを組み合わせたいくつかのシンプルなウィジェットです。
import 'package:flutter/material.dart';
class MyAppBar extends StatelessWidget {
MyAppBar({this.title});
// Fields in a Widget subclass are always marked "final".
final Widget title;
@override
Widget build(BuildContext context) {
return Container(
height: 56.0, // in logical pixels
padding: const EdgeInsets.symmetric(horizontal: 8.0),
decoration: BoxDecoration(color: Colors.blue[500]),
// Row is a horizontal, linear layout.
child: Row(
// <Widget> is the type of items in the list.
children: <Widget>[
IconButton(
icon: Icon(Icons.menu),
tooltip: 'Navigation menu',
onPressed: null, // null disables the button
),
// Expanded expands its child to fill the available space.
Expanded(
child: title,
),
IconButton(
icon: 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 Material(
// Column is a vertical, linear layout.
child: Column(
children: <Widget>[
MyAppBar(
title: Text(
'Example title',
style: Theme.of(context).primaryTextTheme.headline6,
),
),
Expanded(
child: Center(
child: Text('Hello, world!'),
),
),
],
),
);
}
}
void main() {
runApp(MaterialApp(
title: 'My app', // used by the OS task switcher
home: MyScaffold(),
));
}
pubspec.yaml ファイルの flutter セクションに uses-material-design: true エントリがあることを確認してください。これにより、定義済みのマテリアルアイコンのセットを使用することができます。
name: my_app
flutter:
uses-material-design: true
多くの Material Design ウィジェットは、テーマデータを継承するために、正しく表示するために MaterialApp の内部にある必要があります。そのため、 MaterialApp でアプリケーションを実行します。
MyAppBar ウィジェットは、デバイスに依存しない 56 ピクセルの高さの Container を作成し、内部のパディングは左右とも 8 ピクセルです。コンテナ内では、 MyAppBar は Row レイアウトを使用して子を整理します。真ん中の子である title ウィジェットは、 Expanded とマークされています。複数の Expanded の子を持つことができ、 Expanded への flex 引数を使用して、使用可能なスペースを消費する割合を決定することができます。
MyScaffold ウィジェットは、その子を縦の列に整理します。列の上部に MyAppBar のインスタンスを配置し、アプリバーにタイトルとして使用する Text ウィジェットを渡します。他のウィジェットへの引数としてウィジェットを渡すことは、様々な方法で再利用できる汎用ウィジェットを作成することができる強力なテクニックです。最後に、 MyScaffold は、 Expanded を使用して、残りのスペースを中央に配置されたメッセージで構成されるボディで埋めます。
マテリアルコンポーネントの使用
Flutter には、Material Design に従ったアプリを構築するのに役立つ多くのウィジェットが用意されています。MaterialApp ウィジェットは、「ルート」とも呼ばれる文字列で識別されたウィジェットのスタックを管理する Navigator など、多くの便利なウィジェットをアプリのルートに構築します。 Navigator を使用すると、アプリケーションの画面間をスムーズに遷移できます。 MaterialApp ウィジェットの使用は完全に任意ですが、良い練習になります。
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
title: 'Flutter Tutorial',
home: TutorialHome(),
));
}
class TutorialHome extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Scaffold is a layout for the major Material Components.
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: Icon(Icons.menu),
tooltip: 'Navigation menu',
onPressed: null,
),
title: Text('Example title'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.search),
tooltip: 'Search',
onPressed: null,
),
],
),
// body is the majority of the screen.
body: Center(
child: Text('Hello, world!'),
),
floatingActionButton: FloatingActionButton(
tooltip: 'Add', // used by assistive technologies
child: Icon(Icons.add),
onPressed: null,
),
);
}
}
コードが MyAppBar と MyScaffold から AppBar と Scaffold ウィジェットに切り替わった今、 material.dart からアプリが少しだけMaterialに見えてきました。例えば、アプリバーには影がついていて、タイトルテキストは自動的に正しいスタイリングを継承するようになっています。フローティングアクションボタンも追加されました。
ウィジェットが他のウィジェットの引数として渡されていることに注目してください。Scaffold ウィジェットは、名前付きの引数としていくつかの異なるウィジェットを受け取り、それぞれが Scaffold レイアウトの適切な場所に配置されます。同様に、AppBar ウィジェットでは、leading ウィジェットと title ウィジェットの action 用のウィジェットを渡すことができます。このパターンはフレームワーク全体で繰り返され、独自のウィジェットを設計する際に考慮すべきことです。
ジェスチャーの処理
ほとんどのアプリケーションには、システムとの何らかの形でのユーザインタラクションが含まれています。インタラクティブなアプリケーションを構築する最初のステップは、入力ジェスチャーを検出することです。シンプルなボタンを作成して、それがどのように動作するかを見てみましょう。
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 ウィジェットは視覚的な表現を持たず、代わりにユーザによるジェスチャーを検出します。ユーザがコンテナをタップすると、GestureDetector は onTap() コールバックを呼び出します。GestureDetector を使用して、タップ、ドラッグ、スケールなど、さまざまな入力ジェスチャを検出することができます。
多くのウィジェットでは、GestureDetector を使用して他のウィジェットのオプションのコールバックを提供しています。たとえば、IconButton、RaisedButton、FloatingActionButton ウィジェットには、ユーザがウィジェットをタップしたときにトリガーされる onPressed() コールバックがあります。
入力に応じてウィジェットを変更する
これまでのところ、このページではステートレス・ウィジェットのみを使用しています。ステートレス・ウィジェットは親ウィジェットから引数を受け取り、最終的なメンバ変数に格納します。ウィジェットが build() を要求されたとき、ウィジェットはこれらの保存された値を使用して、作成するウィジェットの新しい引数を導出します。
より複雑な体験を構築するために、例えば、ユーザーの入力に対してより面白い反応をするために、アプリケーションは通常、何らかの状態を保持します。Flutter は、この考えを実現するために StatefulWidgets を使用しています。 StatefulWidgets は、ステートオブジェクトを生成する方法を知っている特別なウィジェットで、ステートを保持するために使用されます。先ほどの RaisedButton を使った基本的な例を考えてみましょう。
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 が別のオブジェクトなのか不思議に思うかもしれません。Flutter では、この2種類のオブジェクトは異なるライフサイクルを持っています。ウィジェットは一時的なオブジェクトで、現在の状態でアプリケーションのプレゼンテーションを構築するために使用されます。一方、ステートオブジェクトは build() の呼び出しの間に永続的に保持され、情報を記憶することができます。
上の例では、ユーザの入力を受け付け、その結果を直接 build() メソッドで使用しています。例えば、あるウィジェットは日付や場所などの特定の情報を収集する目的で複雑なユーザーインターフェースを表示し、別のウィジェットはその情報を使って全体の表示を変更することができます。
Flutter では、変更通知はコールバックによってウィジェット階層の「上」に流れ、現在の状態はプレゼンテーションを行うステートレスウィジェットに「下」に流れます。このフローをリダイレクトする共通の親は State です。以下の少し複雑な例は、実際にどのように動作するかを示しています。
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),
]);
}
}
2つの新しいステートレス・ウィジェットが作成され、カウンタの表示(CounterDisplay)とカウンタの変更(CounterIncrementor)がきれいに分離されていることに注目してください。結果は前の例と同じですが、責任を分離することで、親ウィジェットのシンプルさを維持しつつ、より複雑さを個々のウィジェットにカプセル化することができます。
すべてをまとめる
以下に、これらの概念をまとめたより完全な例を示します。仮定のショッピング・アプリケーションは、販売のために提供される様々な商品を表示し、購入を目的としたショッピング・カートを維持します。まず、プレゼンテーション・クラスである ShoppingListItem を定義します。
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'),
],
);
}
}
ShoppingListItem ウィジェットは、ステートレス・ウィジェットの一般的なパターンに従います。コンストラクタで受け取った値を最終的なメンバ変数に格納し、それを build() 関数の間に使用します。例えば、 inCart booleanは、現在のテーマの原色を使用するものと、グレーを使用するものの2つの外観を切り替えます。
ユーザがリストアイテムをタップしたとき、ウィジェットは直接 inCart の値を変更しません。その代わりに、ウィジェットは親ウィジェットから受け取った onCartChanged 関数を呼び出します。このパターンでは、ウィジェット階層の上位にステートを保存することができ、ステートはより長い期間持続します。極端に言えば、 runApp() に渡されたウィジェットに保存された状態は、アプリケーションの寿命まで持続します。
親が onCartChanged コールバックを受信すると、親は内部状態を更新し、親は再構築して新しい inCart 値を持つ ShoppingListItem の新しいインスタンスを作成するようにトリガーします。親は再構築時に
ShoppingListItem の新しいインスタンスを作成しますが、フレームワークは新しく構築されたウィジェットを以前に構築されたウィジェットと比較し、その違いだけを基礎となる RenderObject に適用するので、その操作は安く済みます。
ここでは、可変な状態を保存する親ウィジェットの例を示します。
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 クラスは StatefulWidget を継承しています。ShoppingList ウィジェットが最初にツリーに挿入されると、フレームワークは createState() 関数を呼び出して _ShoppingListState の新しいインスタンスを作成し、ツリー内のその場所に関連付けます。 (State のサブクラスは通常、プライベートな実装の詳細であることを示すために、先頭にアンダースコアを付けて名前が付けられていることに注意してください)。このウィジェットの親がリビルドすると、親は ShoppingList の新しいインスタンスを作成しますが、フレームワークは createState を再度呼び出すのではなく、既にツリーにある _ShoppingListState インスタンスを再利用します。
現在の ShoppingList のプロパティにアクセスするには、 _ShoppingListState はそのウィジェット・プロパティを使用できます。親が再構築して新しい ShoppingList を作成すると、 _ShoppingListState は新しいウィジェットの値で再構築します。ウィジェットのプロパティが変更されたときに通知を受けたい場合は、 didUpdateWidget() 関数をオーバーライドします。
onCartChanged コールバックを処理するとき、 _ShoppingListState は、 _shoppingCart から商品を追加または削除することで内部状態を変更します。内部状態を変更したことをフレームワークに知らせるために、 setState() 呼び出しでそれらの呼び出しをラップします。 setState を呼び出すと、このウィジェットはダーティなものとしてマークされ、次回アプリが画面を更新する必要があるときに再構築されるようにスケジュールされます。ウィジェットの内部状態を変更するときに setState を呼び出すのを忘れた場合、フレームワークはウィジェットがダーティであることを認識せず、ウィジェットの build() 関数を呼び出さない可能性があり、ユーザー インターフェイスが変更された状態を反映して更新されない可能性があります。この方法でステートを管理することで、子ウィジェットの作成と更新のために別のコードを書く必要はありません。代わりに、ビルド関数を実装するだけで、両方の状況を処理します。
ウィジェットのライフサイクルイベントへの対応
StatefulWidget で createState() を呼び出した後、フレームワークは新しいステートオブジェクトをツリーに挿入し、ステートオブジェクトに対して initState() を呼び出します。State のサブクラスは initState をオーバーライドして、一度だけ必要な作業を行うことができます。例えば、アニメーションを設定したり、プラットフォームサービスを購読したりするために initState をオーバーライドします。initState の実装は、 super.initState を呼び出すことで開始する必要があります。
ステートオブジェクトが不要になると、フレームワークはステートオブジェクトに対して dispose() を呼び出します。 dispose 関数をオーバーライドしてクリーンアップ作業を行います。例えば、タイマーをキャンセルしたり、プラットフォームサービスからの退会を解除するために dispose をオーバーライドします。 dispose の実装は、通常 super.dispose を呼び出して終了します。
キー
ウィジェットのリビルド時にフレームワークが他のウィジェットとマッチするウィジェットを制御するためにキーを使います。デフォルトでは、フレームワークは現在のビルドと以前のビルドのウィジェットを runtimeType と表示順に従ってマッチします。キーを使うと、フレームワークは2つのウィジェットが同じ runtimeType と同じキーを持っていることを要求します。
キーは、同じタイプのウィジェットの多くのインスタンスをビルドするウィジェットで最も便利です。例えば、ShoppingList ウィジェットは、その可視領域を埋めるのに十分な数の ShoppingListItem インスタンスをビルドします。
キーがなければ、現在のビルドの最初のエントリは、意味的にはリストの最初のエントリが画面外にスクロールしてビューポートに表示されなくなっても、常に前のビルドの最初のエントリと同期します。
リストの各エントリに「セマンティック」キーを割り当てることで、無限リストはより効率的になります。フレームワークは、一致するセマンティックキーを持つエントリを同期するので、似たような(あるいは同じ)視覚的な外観を持つことになります。さらに、セマンティックにエントリを同期するということは、ステートフルな子ウィジェットで保持される状態は、ビューポート内の同じ数値位置にあるエントリではなく、同じセマンティックエントリにアタッチされたままであることを意味します。
グローバルキー
グローバルキーを使用して、子ウィジェットを一意に識別します。グローバル・キーは、兄弟間でのみ一意である必要があるローカル・キーとは異なり、ウィジェット階層全体でグローバルに一意である必要があります。グローバルキーはグローバルに一意であるため、グローバルキーはウィジェットに関連付けられた状態を取得するために使用することができます。
感想
日が空いてしまったので大分忘れてしまっている部分があり、読み進めるのに苦労しました…。継続が一番大事ですね😭
ウィジェットについては開発する上で必ず理解していないといけない部分だと思うので、何度も読み直してきちんと理解していきたいと思います!
次回はBuilding layoutsを読み進めてレイアウト作成について学ぼうと思います🌊🏄♀️