Flutterウィジェットは、Reactからインスピレーションを得た最新のフレームワークを使用して構築されています。
中心的な考え方は、ウィジェットからUIを構築することです。 ウィジェットは、現在の構成と状態を前提として、ビューがどのように表示されるかを記述します。
ウィジェットの状態が変化すると、ウィジェットはその描写を再構築します。
フレームワークは、ステートが移り変わる際のレンダーツリーの必要最小限の変更を実現するために、変更前の描写との差分を把握します。
Contents
Hello world
最も小さいFlutterのアプリはシンプルにrunApp()関数をウィジェットと共に呼び出すものです。
import 'package:flutter/material.dart'; void main() { runApp( Center( child: Text( 'Hello, world!', textDirection: TextDirection.ltr, ), ), ); }
runApp( )関数はWidgetを引数にとり、そのWidgetをウィジェットツリーのルートに配置します。このサンプルでは、ウィジェットツリーは二つのウィジェット、CenterウィジェットとTextウィジェットで構成されています。
フレームワークはルートウィジェットが画面を占有するようにします。つまり”Hello world”の文字列が画面の中央に表示されることになります。
この場合、テキストの方向を指定する必要があります。 MaterialAppウィジェットを使用すると、後で説明するように、これが自動的に処理されます。
アプリをコーディングしていると、StatelessWidgetか、あるいはStatefulWidgetのサブクラスを宣言することがよくあります。その二つの違いは何らかのstate(状態)を管理するかどうかです。
ウィジェットの主な仕事はbuild()メソッドを実装することです。build()メソッドは他の子孫ウィジェットを描画します。
フレームワークはそれらの子孫ウィジェットを順番に一番下の、内在するレンダーオブジェクトを表すウィジェットまで繰り返しビルドします。
Basic widgets
Flutterにはいくつものパワフルな基本的なウィジェットがあり、以下に示すものがよく使われます。
工事中🏗
Stack
Stackウィジェットを使用すると、線形方向(水平方向または垂直方向)ではなく、ウィジェットをペイント順に重ねて配置できます。 次に、Stackの子にPositionedウィジェットを使用して、スタックの上端、右端、下端、または左端を基準にしてそれらを配置できます。 スタックは、Webのabsolute positioning layout modelに基づいています。
Handling gestures
ほとんどのアプリには、何らかのシステムと一緒にユーザーインタラクションとしてのフォームがあります。インタラクティブなアプリを開発する第一歩は、入力ジェスチャーを検知することです。
シンプルなボタンを設置して、どのように機能するか見てみましょう。
GestureDetectorウィジェットは視覚的な表現は何もありませんが、代わりにユーザーからのジェスチャーを検知する機能を加えることができます。
//main.dart import 'package:flutter/material.dart'; void main() { runApp( MyApp(), ); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar(), body: MyWidget(), ), ); } } class MyWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Center( child: MyButton(), ); } } class MyButton extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( onTap: () { print('MyButton was tapped!'); }, child: Container( height: 50.0, padding: const EdgeInsets.all(8.0), margin: const EdgeInsets.symmetric(horizontal: 8.0), decoration: BoxDecoration( borderRadius: BorderRadius.circular(5.0), color: Colors.lightBlue[300], ), child: Center( child: Text('Push'), ), ), ); } } class MyButton extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( onTap: () { print('MyButton was tapped!'); }, child: Container( height: 50.0, padding: const EdgeInsets.all(8.0), margin: const EdgeInsets.symmetric(horizontal: 8.0), decoration: BoxDecoration( borderRadius: BorderRadius.circular(5.0), color: Colors.lightBlue[300], ), child: Center( child: Text('Push'), ), ), ); } }
ユーザーがContainerウィジェットをタップした時、GestureDetectorウィジェットは自身のonTapコールバックを呼び出します。上記のサンプルではボタンを押すとコンソールにメッセージを表示します。
GestureDetectorウィジェットを使用することで、タップ、ドラッグ、スケールなどいろいろなインプットジェスチャーを検知することができます。
多くのウィジェットは、GestureDetectorを使用して、他のウィジェットにオプションのコールバックを提供します。 たとえば、IconButton、ElevatedButton、およびFloatingActionButtonウィジェットには、ユーザーがウィジェットをタップしたときにトリガーされるonPressed()コールバックがあります。
Changing widgets in response to input
ここまで全てstateless widget(ステートレス ウィジェット)を見てきました。Stateless widgetは親widget(ウィジェット)から引数を受け取り、それをfinalなメンバ変数として格納します。
finalを先頭につけて宣言した変数は定数(実行時に一度だけ初期化できる定数)となります。
ウィジェットがbuild()を要求されると、ウィジェットはこれらの格納されたメンバ変数(メンバプロパティ)を使用して、自身の子孫ウィジェットをビルドするために必要な処理を実行します。
より複雑なUX(ユーザーエクスペリエンス)を構築するために、–例えばユーザー入力に対してより面白い、興味深いリアクションを実現する、など–アプリはいくつかのstate(状態)を運ぶことがあります。
FlutterではStatefulWidget(ステートフルウィジェット)を使ってそれを実現します。
StatefulWidgetは特別なウィジェットで、Stateオブジェクトの生成方法を知っています。
Stateオブジェクトはstate(状態)を保持するために使用されます。ElevatedButtonを使用した、下記の基本的なサンプルを考えます。
import 'package:flutter/material.dart'; void main() { runApp( MyApp(), ); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar(), body: Counter(), ), ); } } class Counter extends StatefulWidget { // このクラスは、stateの設定にあたるものです。 // これは、親によって提供され、Stateのbuildメソッドによって // 使用される値(この場合は何もありません)を保持します。 @override _CounterState createState() => _CounterState(); } class _CounterState extends State<Counter> { int _counter = 0; void _increment() { setState(() { //setStateメソッドの呼び出しは、Flutterフレームワークに対して // 何らかのStateの変化があったことを伝えます。 //setStateメソッドの呼び出しにより、state(状態)のアップデートを //画面表示に反映させるためビルドの再実行が行われます。 //setStateを実行せずに_counter変数を変更しても、buildメソッドが //実行されないので、画面上では何も起こりません。 // }); } @override Widget build(BuildContext context) { // buildメソッドはsetStateメソッドが呼び出されるたびに実行されます。 // 例えば_incrementメソッドが実行されると、 // Flutterフレームワークは、buildメソッドの再実行を迅速に行うために最適化を行います。 //ウィジェットのインスタンスを個別に変更するのではなく、 //更新が必要なものをすべて再構築できるようにします。 return Row( children: <Widget>[ ElevatedButton( onPressed: _increment, child: Text('Increment'), ), Text('Count: $_counter'), ], ); } }
あなたはStatefulWidgetとStateが切り離されていることに疑問をもたれるかもしれません。
Flutterでは、StatefulWidgetとStateでライフサイクルが異なります。Widegtは、現在の状態(state)に基づいて画面表示を構築するための一時的なオブジェクトです。
一方Stateオブジェクトはbuild()メソッドが呼び出されても無くならずに存在し続けます。
これにより情報(state、状態)を覚え続けることが可能です。
上記のカウンターのサンプルではユーザーインプットを受け取り、build()メソッドの中でその結果を直接受け取っています。
_CounterStateクラスの中で、状態(_counter)の変化を直接、自身(_CounterState)のbuild()メソッドの中で使用している、という意味。だと思われる。
より複雑なアプリケーションでは、ウィジェット階層のさまざまな部分がさまざまな関心の原因となる可能性があります。
別の階層のウィジェットが受け取ったジェスチャーの結果により自身のビルドを再実行する、というようなケース。
たとえば、あるウィジェットは、日付や場所などの特定の情報を収集することを目的とした複雑なユーザーインターフェイスを表示し、別のウィジェットはその情報を使用して全体的な表示を変更する場合があります。
Flutterでは、コールバックを使用して「状態の変更の原因となる現象の通知(例えばボタンが押された、など)」をウィジェット階層を上る形で伝搬します。
そして現在の状態(state)をウィジェット階層を降る形で、表示を担当するstatelessWidgetに伝えます。
このフローをリダイレクトする共通の親はStateです。 次の少し複雑な例は、これが実際にどのように機能するかを示しています。
import 'package:flutter/material.dart'; void main() { runApp( MyApp(), ); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar(), body: Counter(), ), ); } } 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 ElevatedButton( 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)ウィジェット
- カウント値をを変更する(ためのボタン)役割の(CounterIncrementer)ウィジェット
を作っていることに注目してください。
ここの言葉の使い方から「関心」とは「そのウィジェットが担当する仕事、役割、責任」くらいの意味だと思われます。「関心の分離」は役割を分けて一つのウィジェットが一つの専門の役割を持つ、という感じ。
一つ前のサンプルと、挙動自体は同じですが、責任(役割)を分離することで、非常に複雑なアプリになってもそれぞれのウィジェットに専門の役割をカプセル化することができ、それにより親ウィジェットでの保守性をあげることができます。
Bringing it all together
工事中🏗
Responding to widget lifecycle events
StatefulWidgetのcreateState()メソッドが呼び出されると、フレームワークはtreeに新しいstateオブジェクトを挿入し、stateオブジェクトのinitState()メソッドを呼び出します。
一度だけ発生することが必要な処理を行うため、Stateクラスのサブクラスは、initState()メソッドをオーバーライドすることができます。
例えば、アニメーションを設定したり、プラットフォームサービスをサブスクライブするためにinitState()メソッドをオーバーライドします。
initState()メソッドを実装するときは、一番最初にsuper.initState()メソッドを呼び出す必要があります。
stateオブジェクトが必要なくなった時は、フレームワークはstateオブジェクトのdispose()メソッドを呼び出します。disopse()メソッドをオーバーライドしてクリーンアップ処理を行わせます。
クリーンアップはインスタンスを消去してリソース(メモリ)を開放すること。これをしないと不要なデータがメモリを占有し続けることでパフォーマンスが低下する可能性がある。
例えば、タイマーをキャンセルしたり、プラットフォームのサブスクライブを終了したりするためにdispose()メソッドをオーバーライドします。
dispose()メソッドを実装する時は、最後にsuper.disposeを呼び出す必要があります。
Keys
キーを使用して、ウィジェットの再構築時にフレームワークが他のウィジェットと一致するウィジェットを制御します。
デフォルトでは、フレームワークは、「runtimeType(型)」と、ウィジェットが表示される「順序」に従って、現在および以前のビルドのウィジェットを同じものかどうかチェックします。
キーを使用する場合、フレームワークは、2つのウィジェットに同じキーと同じruntimeTypeを要求します。
キー(Key)は、同じタイプのウィジェットの多くのインスタンスを構築するウィジェットで最も役立ちます。 たとえば、ShoppingListウィジェットが、表示領域を埋めるのに十分なShoppingListItemインスタンスを作成するような場合です。
キーがないと、リストの最初のエントリが画面からスクロールされてビューポートに表示されなくなった場合でも、現在のビルドの最初のエントリは常に前のビルドの最初のエントリと同期します。
↑前の画面のスクロール位置を記憶して、その場所を表示したいけど、それができない、という状況。
リスト内の各エントリに「セマンティック」キーを割り当てることにより、フレームワークがエントリを一致するセマンティックキーと同期し、したがって類似した(または同一の)視覚的外観を実現するため、無限リストをより効率的にすることができます。 さらに、エントリをセマンティックに同期するということは、ステートフル子ウィジェットに保持されている状態が、ビューポートの同じ数値位置にあるエントリではなく、同じセマンティックエントリにアタッチされたままになることを意味します。
Global keys
グローバルキーを使用して、子ウィジェットを一意に識別します。 グローバルキーは、兄弟間でのみ一意である必要があるローカルキーとは異なり、ウィジェット階層全体でグローバルに一意である必要があります。 これらはグローバルに一意であるため、グローバルキーを使用してウィジェットに関連付けられた状態を取得できます。
参考
https://flutter.dev/docs/development/ui/widgets-intro