こんにちは!Diverse広報担当です。
先日、Diverse Meetup #2となる『Flutter開発者必見!あの有名ライブラリの内部実装を解説』を開催しました!
Diverseのリードエンジニアとして活躍する菊池 紘さんがご登壇。
Providerとはそもそも何かという基本の解説から、MultiProviderが何をしているのかコードリーディングしたり、予定の1時間をオーバーするほどの熱気でお話いただきました。
今回は、そんなイベントの様子を前編・中編・後編に分けてお伝えしたいと思います。
まずは前編、事前知識の確認の部分からどうぞ!
▼目次
当日の資料と動画を公開します
残念ながら当日参加していただけなかった方のために、資料と動画を公開させていただきます!
イベントルポを大公開します
ここからは、当日のイベント(前編)のエッセンスを文字にまとめています。「動画を見ている時間がない!」「要点だけサラッと理解したい!」という方はこちらをご覧ください!
▼Providerとは?(00:00)
菊池 紘(以下、菊池):依存性注入と状態通知のフレームワークということで、ご存知の方は非常に多いと思います。
特徴としては、この4つが挙げられます。
- 離れた子孫Widgetにオブジェクトを提供
- Widgetツリーの深さに関わらずO(1)で取得可能
- 状態の変更に伴ってWidgetのrebuildが可能
- シンプルで使いやすいAPI
使い方の例としてはこういう感じで、
Provider にString の "Hello" という値を入れたい時は、Provider の型引数に注入したい値の型を指定します。
あとは子孫になる Widget として、子孫側で Provider.of()、型引数に注入した値の型、context に現在の BuildContext を指定すると、注入された値 "Hello" をこの式で取得できて、結果としてこのTextは "Hello, world!" というテキストを出力する、という動作になります。
また、状態通知を行うことができます。
一番簡単な、ValueNotifier を使った Counter を作った例をご紹介したいと思います。
まず初期値としてゼロを持っていて、countUp というメソッドを呼ばれるとその値が1ずつ増えていくカウンターを、この Counter クラスで用意します。この Counter クラスが数値を持っていて、value というところがどんどん countUp されていく状態になっています。
この Counter を使って実際の Widget を組んでみるときは、ChangeNotifierProvider というものを使用します。これは ValueNotifier というクラスの親クラスが ChangeNotifier というクラスで、「ChangeNotifier を継承しているクラスをprovider で注入するためにはChangeNotifierProvider を使ってください」という風にマニュアルに記述があるからだと考えていただければいいと思います。
注入するクラスは Counter クラスですね。create のところで Counter を作るようなメソッドを...関数を仕込んでいきます。
builder の中には子孫 Widget を作っていきますが、現在の count を表示するテキストの内容が、Provider.of()、Counter、contextがあって、valueがあってという風になっています。
先ほど listen を true にすればと言ったんですが、Provider.of() の名前付きの省略可能引数に listen というのがありまして、デフォルトでは true になっています。今はそれを省略しているので、何かしら値の変更があると、この BuildContext を使用している build メソッド、この場合は builder の引数に指定されているこの範囲のメソッドがもう一度呼ばれるようになります。
状態の変更をどのように行うかですが、ボタンを押したときに Provider.of() でまた取ってきます。今回は Counter のインスタンスそのものが必要であって Counter が抱えている状態が必要ではないので、listen を false にしています。countUpを呼ぶと、このボタンが押された時に状態が変わっていく、ということが実現できます。
▼InheritedWidgetのキホン(03:43)
菊池:次に、provider の中でも使われているInheritedWidget についてお話をします。これは、ツリーの下部に情報伝播をするための Widget になっています。
この Widget よりも下に、この Widget とそれが抱えているインスタンスを伝播するための Widget です。その性質を使って依存性注入に使うケースが多いと思います。
先ほど、BLoC パターンをする時もProvider を使うと便利という話もありましたが、その BLoC オブジェクトを常に抱えておくため、ツリーの下部の方でその BLoC オブジェクトを取得してきて使用するために内部的にもInheritedWidget のこの性質を使えます。
この InheritedWidget が変更通知機能も持っているので、必要あれば rebuild を走らせることもできます。
InheritedWidget が基本の Widget になるんですけども、それを継承した InheritedListener というのがあります。これは Listenable を継承しているクラスを変更監視する際に、変更監視する機能を提供してくれるという便利なラッパーになっています。provider の作者のにRemiさんという方は、この InheritedWidget の扱いを簡単にするためにprovider を作ったようです。
InheritedWidget をどのようにしてツリーの上から取得していくのか見ていきたいと思います。結論として、BuildContext というものを使います。
BuildContext の中に、getElementForInherited WidgetOfExactTypeというようなメソッドがあったりします。
ここで、flutter のマニュアルを見てみましょう。
BuildContext のメソッドです。型変数 "T"に指定されたクラスをそのインスタンスを取得する。ただしその "T" は InheritedWidget を継承したサブクラスである必要がある、ということですね。
もし見つからなければ null が返ってくる。これが、 O(1) 、定数オーダー。ツリーの深さがいくら深くても一定の時間で Widget を取得することができる、と書かれています。
もうひとつ、インスタンスの更新を検知してrebuildもしたい場合には、dependOnInheritedWidgetOfExactType というのを使います。
こちらも見てみますと、 "T" の宣言はさっきの getElementForInheritedWidgetOfExactType と同じようになっていますので、Inherited Widgetを継承したなにかしらのクラスを指定する、という風になっています。
こちらは、dependOn なので、"T" として指定したクラスで作られているインスタンスに"T" として指定したクラスで作られているインスタンスに何かしらの変更があれば、登録した BuildContext のbuild メソッドをもう一度呼び出すということをしてrebuild をかけてくれます。
ちょっと余談ですけども、BuildContext というクラスの名前を何度も出してますけれども、その実態は、Element というクラスになっています。
Element でも同様のメソッドを使用することは可能です。Element は後ほどちょっと出てきます。
▼InheritedWidgetを使う(06:36)
実際に InheritedWidget を使って Provider と同じようなことをするには、どうしたらいいのかをお話をします。
ValueInheritedWidget というものを作っています。例によってInheritedWidget を継承したものです。
メンバとしてString 型の value というものを抱えるようにします。それはコンストラクタで value を受けるという形になってます。
Provider は of というメソッドで値を取得できるようになっているように、InheritedWidget を使って祖先の Widget を取得する場合は、static メソッドで of というものを生やして、引数に BuildContext を取るというものが一般的なようです。
Theme.of() とか、あとは Navigator.of() とか、そういった関数で目にすることも多いかもしれません。
実際に値を取得していきましょう。getElementForInheritedWidgetOfExactType を BuildContext に対して使います。
型引数に今作っているこの ValueInheritedWidget を指定します。もし祖先に ValueInheritedWidget がなければ、null が返ってくるとマニュアルにも書かれていましたので、null だったら値なしとして返します。もし値があれば実は Element 型が返ってきます。
Elemet の Widget というのは参照すると、実際にツリーに注入されているこの IngeritedWidget のインスタンスを取得することが出来ますので、ダウンキャストして value を取得すると...
ValueInheritedWidget を作って、そのchild、子孫の Widget のどこかに、ValueInheritedWidget.of() を使用したコードを書くという形になります。
これで "Hello World!" は出力できるという形で、冒頭の Provider を使った例とほとんど同じ挙動をします。
状態変更を通知する場合は、InheritedListener を使うというお話を先ほどしました。先ほども countUp する Counter の例をお出ししましたけれども、それを、InheritedWidget を使ってProvider を使った時と同じように、ツリーの下部で状態変更を監視するようにして一緒に rebuild を走るようにするにはどうしたらいいかというと、InheritedListener を継承した CounterInhertedWidget というのを作って、それを試してみようと思います。
この CounterInheritedWidget が、メンバに先ほどの Counter を抱えます。notifier という名前で抱えてます。こちらも of、値を取得できるようにします。
provider と同じように、省略可能な引数にlisten というものを取るようにしました。
false だった場合は値をただ単に取得するだけで、true だった場合は、指定した BuildContext に対して処理をかけるようにします。listen が true 、変更を監視する場合は先ほどお話ししたdependOnInheritedWidgetOfExactType を使用します。
これでWidget そのものが返ってくるので、メンバに入ってる notifier、これを返します。このメソッドが戻り値を返した際には、既に BuildContext に対して変更を通知するようにしてください、という listener が仕掛けられているので、これを呼び出すだけで、変更の監視が可能になっています。false だった場合には値を取得するだけで先ほどと同じようなコードを書いています。
どちらも本当は null チェックが必要なんですけれども、紙面の都合で省いています。
使い方は、先ほどの ValueInheritedWidget と同じようにして、祖先の方に CounterInheritedWiddget を入れて、子孫の方にそれを使用した Widget 群を置くということをします。
さいごに
お読み頂き有難うございました!
前編のイベントルポはこれで終了です。中編、後編もまもなくUPするので、ぜひご覧ください!
▼次回イベント予告
Diverse Meetup #3が、2021/05/12(水)19:00〜20:00に開催予定!
今回のテーマは『Lookerを使ったデータドリブンなアプローチ』です。
ご興味ある方は奮ってご応募ください!
▼Diverseの情報発信について
Diverseはこのようなイベントの他にも各所で情報発信を行っています。興味のある方はぜひ覗いてください!