概要
flutterでのWidgetは大きく分けて、4つあります。
- StatelessWidget
- StatefullWidget
- InheritedWidget
- RenderObjectWidget
です。
StatelessWidgetとStatefullWidgetはサンプルでよく見かけます。
RenderObjectWidgetとは簡単に言えば、レンダリングするWidgetでほとんどのWidgetがこれになります。
InheritedWidgetは、おそらく初級向けのサンプルや記事にはでてこないのではないでしょうか。また、公式ドキュメントには具体的な実装方法が書いていません。
この記事ではInheritedWidgetのできることとサンプルを提供したいと思います。
その前にBuildContextについて理解していないと難しいので、こちらの記事を先に閲覧することをお勧めします。
何ができるのか
まずは、ドキュメントを確認してみましょう。
Base class for widgets that efficiently propagate information down the tree.
To obtain the nearest instance of a particular type of inherited widget from a build context, use BuildContext.inheritFromWidgetOfExactType.
Inherited widgets, when referenced in this way, will cause the consumer to rebuild when the inherited widget itself changes state.
訳)
ツリーの下に情報を効率的に伝達するウィジェットの基本クラス。
ビルドコンテキストから継承されたウィジェットの特定の型の最も近いインスタンスを取得するには、
BuildContext.inheritFromWidgetOfExactType
を使用します。
継承されたウィジェットは、このように参照されると、継承されたウィジェット自体が状態を変更したときにコンシューマを再構築させます。
普通のWidgetは、ツリー構造の下のWidgetが先祖の変更を感知して、自身をリビルドする or しないの判断はできません。
しかし、InheritedWidgetを利用すれば、それができるのです。
すごく簡単な図ですが、Cボタンを押下したら、Stateが変更されて、Aのテキストだけがリビルドされて表示が変わる。
こういったことができます。
余談ですが、InheritedWidgetの記事の説明で、「子孫から親のStateにアクセスを可能にする」的な記述が書かれたりしていますが、それはInheritedWidgetの説明としては完全ではないです。そもそも、それはInheritedWidgetを使わなくともBuildContxtで簡単に実現可能です。(それは後ほど記述します)
重要なのは子孫が親の変更を感知できて、buildメソッドを再度呼ぶことで再構築し直すことができることと先祖をO(1)で取得できることです。
サンプルアプリの仕様
プラスボタン(=WidgetC)を押下すると_HomePageStateの_counterが増えて上部のテキスト(=WidgetA)が変更されます。
真ん中の「I am a widget that will not be rebuilt.」=(WidgetB)は変更されません。
InheritedWidgetを使わない場合
まずは、使わない場合のソースです。
この場合、ボタンを押下するたびに、WidgetA,WidgetB,WidgetC共にbuildメソッドが呼ばれます。
これは説明不要だと思います。
class TopPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(
title: Text('Demo'),
),
body: HomePage(),
),
);
}
}
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
WidgetA(_counter),
WidgetB(),
WidgetC(_incrementCounter),
],
),
);
}
}
class WidgetA extends StatelessWidget {
final int counter;
WidgetA(this.counter);
@override
Widget build(BuildContext context) {
return Center(
child: Text(
'${counter}',
style: Theme.of(context).textTheme.display1,
),
);
}
}
class WidgetB extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text('I am a widget that will not be rebuilt.');
}
}
class WidgetC extends StatelessWidget {
final void Function() incrementCounter;
WidgetC(this.incrementCounter);
@override
Widget build(BuildContext context) {
return RaisedButton(
onPressed: () {
incrementCounter();
},
child: Icon(Icons.add),
);
}
}
AndroidStudioのFlutter Performanceを見ると実際にWidgetA,WidgetB,WidgetCがリビルドされているのがわかります。
InheritedWidgetを使う場合
まずは全体のソースを載せます。
class TopPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HomePage(
child: Scaffold(
appBar: AppBar(
title: Text('InheritedWidget Demo'),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
WidgetA(),
WidgetB(),
WidgetC(),
],
),
),
),
);
}
}
class _MyInheritedWidget extends InheritedWidget {
_MyInheritedWidget({
Key key,
@required Widget child,
@required this.data,
}) : super(key: key, child: child);
final HomePageState data;
@override
bool updateShouldNotify(_MyInheritedWidget oldWidget) {
return true;
}
}
class HomePage extends StatefulWidget {
HomePage({
Key key,
this.child,
}) : super(key: key);
final Widget child;
@override
HomePageState createState() => HomePageState();
static HomePageState of(BuildContext context, {bool rebuild = true}) {
if (rebuild) {
return (context.inheritFromWidgetOfExactType(_MyInheritedWidget) as _MyInheritedWidget).data;
}
return (context.ancestorWidgetOfExactType(_MyInheritedWidget) as _MyInheritedWidget).data;
// 実は下を使うの方が良い
// return (context.ancestorInheritedElementForWidgetOfExactType(_MyInheritedWidget).widget as _MyInheritedWidget).data;
}
}
class HomePageState extends State<HomePage> {
int counter = 0;
void _incrementCounter() {
setState(() {
counter++;
});
}
@override
Widget build(BuildContext context) {
return _MyInheritedWidget(
data: this,
child: widget.child,
);
}
}
class WidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
final HomePageState state = HomePage.of(context);
return Center(
child: Text(
'${state.counter}',
style: Theme.of(context).textTheme.display1,
),
);
}
}
class WidgetB extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text('I am a widget that will not be rebuilt.');
}
}
class WidgetC extends StatelessWidget {
@override
Widget build(BuildContext context) {
final HomePageState state = HomePage.of(context, rebuild: false);
return RaisedButton(
onPressed: () {
state._incrementCounter();
},
child: Icon(Icons.add),
);
}
}
WidgetBとWidgetCはリビルドされていないのがわかります。
説明
InheritedWidgetを使うことで変わった部分をみていきましょう
WidgetA,WidgetC
WidgetBは変わりません。
WidgetA、WidgetCともにStateの取得方法が変わっています。
旧バージョンはコンストラクタで_counter変数とincrementCounter関数を受け取っていました。
final HomePageState state = HomePage.of(context); // WidgetAの場合
final HomePageState state = HomePage.of(context, rebuild: false); // WidgetCの場合
新バージョンでは、HomePageStateを取得しています。
WidgetCの場合は、rebuildを明示的にfalseにしています。WidgetC=ボタンは変化しないのでrebuildをさせないことを知らせています。
WidgetCは、Stateのメソッドを呼び出したいのでStateに依存しているだけです。
では、HomePageのofメソッドをみてみましょう。
HomePage
static HomePageState of(BuildContext context, {bool rebuild = true}) {
if (rebuild) {
return (context.inheritFromWidgetOfExactType(_MyInheritedWidget) as _MyInheritedWidget).data;
}
return (context.ancestorWidgetOfExactType(_MyInheritedWidget) as _MyInheritedWidget).data;
// 実は下を使うの方が良い
// return (context.ancestorInheritedElementForWidgetOfExactType(_MyInheritedWidget).widget as _MyInheritedWidget).data;
}
このメソッドの処理は先祖を辿って _MyInheritedWidget
を探しています。
_MyInheritedWidget
に HomePageState
を持たせることでState情報を子供Widgetに渡せるようにしています。
どうして先祖が取得できるのかは、BuildContextの仕組みがわかれば簡単にわかります。こちらの記事を参照してください。
inheritFromWidgetOfExactType
と ancestorWidgetOfExactType
の違いは、呼び出したcontextが保持するwidgetがリビルドされるかされないかです。
またinheritFromWidgetOfExactType
は InheritedWidget
の取得専用のメソッドです。
これが呼ばれるとリビルドされます。
ancestorWidgetOfExactType
は、指定したWidgetをcontextから辿って取得するだけのメソッドです。
コメントで記述している ancestorInheritedElementForWidgetOfExactType
は、inheritFromWidgetOfExactTypeと同様にO(1)のコストで取得できます。
こういうわけで、WidgetA=(Text)はリビルドされて、WidgetC=(Button)はリビルドされないのです。
では、旧バージョンになかった _MyInheritedWidget
はどういう実装なのか見てましょう。
_MyInheritedWidget
class _MyInheritedWidget extends InheritedWidget {
_MyInheritedWidget({
Key key,
@required Widget child,
@required this.data,
}) : super(key: key, child: child);
final HomePageState data;
@override
bool updateShouldNotify(_MyInheritedWidget oldWidget) {
return true;
}
}
ここのポイントは、InheritedWidget
を継承していることです。
HomePageState.of
で探しているのはこのクラスです。
updateShouldNotify
メソッドは、inheritFromWidgetOfExactType
を呼び出すWidgetにリビルドをすることを通知するかをboolで返します。
子孫が利用したいのは、HomePageState
なのでdataフィールドで保持しています。
HomePageState
@override
Widget build(BuildContext context) {
return _MyInheritedWidget(
data: this,
child: widget.child,
);
}
ここでのポイントは、_MyInheritedWidget
とchildで親が指定したwidgetを指定していることです。
旧バージョンは、ここでWidegtA,WidgetB,WidgetCがありました。それは変更される(setStateされた時にbuildする)WidgetなのでStateのbuildでインスタンスしなければなりませんでした。
新バージョンでは、InheritedWidgetを使ってビルドを伝えるのでここでインスタンスしてはいけません。
なので、親が定義する方になっています。
TopPage
で、実際にWidegtA,WidgetB,WidgetCをインスタンスしているのは、TopPage
になります。
class TopPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
・・・
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
WidgetA(), // それぞれのWidgetのインスタンス化はこのクラスに移動
WidgetB(),
WidgetC(),
],
),
・・・
}
}
InheritedModelについて
InheritedWidgetを継承しているInheritedModelというのがあります。
これは、InheritedWidgetの変更を購読している子孫Widgetの中で、一部のWidgetだけに変更を伝えたい場合に使えます。
単純に文字列のキーを送って、それに該当する子孫だけに変更を伝えます。
上記サンプルのfinal HomePageState state = HomePage.of(context, rebuild: false);
で、rebuildするかどうかを送っているのと同じことです。
違う部分だけ記述しておきます。
@override
HomePageState createState() => HomePageState();
- static HomePageState of(BuildContext context, {bool rebuild = true}) {
- if (rebuild) {
- return (context.inheritFromWidgetOfExactType(_MyInheritedWidget) as _MyInheritedWidget).data;
- }
- return (context.ancestorWidgetOfExactType(_MyInheritedWidget) as _MyInheritedWidget).data;
+ static HomePageState of(BuildContext context, String aspect) {
+ return InheritedModel.inheritFrom<_MyInheritedWidget>(context, aspect: aspect).data;
}
}
-class _MyInheritedWidget extends InheritedWidget {
・・・
+class _MyInheritedWidget extends InheritedModel {
+ @override
+ bool updateShouldNotifyDependent(_MyInheritedWidget old, Set aspects) {
+ return aspects.contains('A'); // A文字列が送られてきた場合だけ通知する
+ }
}
class WidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
- final HomePageState state = HomePage.of(context);
+ final HomePageState state = HomePage.of(context, 'A'); // A文字列なのでリビルドする
class WidgetC extends StatelessWidget {
@override
Widget build(BuildContext context) {
- final HomePageState state = HomePage.of(context, rebuild: false);
+ final HomePageState state = HomePage.of(context, 'C'); // C文字列なのでリビルドされない
Builder Widgetできめ細かいリビルド
inheritFromWidgetOfExactType
を呼び出すbuild内で一部のWidgetだけ再構築させたくない場合があります。わざわざ別クラスに切り出すほどの大きさじゃない場合です。
以下のコードで、Text("AAAAA")はリビルドさせたくない。
class WidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
final HomePageState state = HomePage.of(context);
return Column(
children: <Widget>[
Center(
child: Text(
'${state.counter}',
style: Theme.of(context).textTheme.display1,
),
),
Text("AAAAA"), // ここはリビルドさせたくない
],
);
}
}
BuildContextとInheritedWidgetの仕組みがわかっていれば、簡単なことです。
Builder Widgetを使って、対象のcontextを小さくします。そのcontextで inheritFromWidgetOfExactType
を呼び出せば、そのWidgetだけがリビルドされます。
return Column(
children: <Widget>[
Center(
child: Builder(builder: (context){
final HomePageState state = HomePage.of(context);
return Text(
'${state.counter}',
style: Theme.of(context).textTheme.display1,
);
}),
),
Text("AAAAA"),
],
);
buildメソッドの引数のBuildContextのwidgetは、WidgetAですが、Builder内のBuildContextのwidgetは、Builderなので、Builder内だけがリビルドされるという仕組みです。
その他
子孫が親のStateを取得するだけならInheritedWidgetじゃなくても可能
最初に「子孫から親のStateにアクセスを可能にする」は、InheritedWidgetの説明としては間違い。と記述しました。
それだけだったら他の方法でできるからです。その実装を載せておきます。
class TopPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(
title: Text('Demo'),
),
body: HomePage(),
),
);
}
}
class HomePage extends StatefulWidget {
HomePageState state; // Stateを保持する
@override
HomePageState createState() {
state = HomePageState();
return state;
}
}
class HomePageState extends State<HomePage> {
int counter = 0; // privateを辞める
void incrementCounter() { // privateを辞める
setState(() {
counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
WidgetA(),
WidgetB(),
WidgetC(),
],
),
);
}
}
class WidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
final HomePage widget = context.ancestorWidgetOfExactType(HomePage); // これでStateを取得
final HomePageState state = widget?.state;
return Center(
child: Text(
'${state == null ? 0 : state.counter}',
style: Theme.of(context).textTheme.display1,
),
);
}
}
class WidgetB extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text('I am a widget that will not be rebuilt.');
}
}
class WidgetC extends StatelessWidget {
@override
Widget build(BuildContext context) {
final HomePage widget = context.ancestorWidgetOfExactType(HomePage);
final HomePageState state = widget?.state;
return RaisedButton(
onPressed: () {
state?.incrementCounter();
},
child: Icon(Icons.add),
);
}
}
単純に ancestorWidgetOfExactType
メソッドを使って、StatefullWidgetのStateを使っているだけです。
ただし、この方法では、親の変更を子孫が感知できないので、同じ値のままです。
また、このメソッドはO(N)のコストがかかってしまいます。
なので、InheritedWidgetを使うのです。
最後に
flutter界隈でよく使われるデザインパターンで、Scoped ModelとBLoC(Business Logic of component)があります。
これらのパターンの実装は、InheritedWidgetを利用しています。
また、flutter自体の実装でもInheritedWidgetは多用されています。
初級を抜け出すには、まずは、InheritedWidgetから始めると良いかと思います。