やりたかったことは多言語対応の一環だった。flutter_localizationsの生成コードでBuildContextからAppLocalizationは取得できるが、それと別にOSの言語設定を監視したかった。そのために他のウィジェットを包むだけのものを用意して、ミドルウェア的な挙動でOSの言語設定をアプリのDBに保管したかった。でもなぜかそれを適応すると、今度はgo_routerのStatefulShellRouteを使った遷移が機能しなくなってしまった。
(defn listen-state
[^m/Widget .child]
(reify :extends m/StatefulWidget
:no-meta true
(createState
[_this]
(reify :extends m/State
:no-meta true
(initState [this]
(.initState ^super this))
(dispose [this]
(.dispose ^super this))
(build [_this _context]
child)))))
このコードで問題は最小限で再現していた。遷移しても、child
の変更はUIに反映しなかった。なぜだ?あっちこっちにprintln
仕込んでも直感的にはわからなかった。どうやらFlutterのStatefulWidgetの振る舞いを十分に理解していなかった。
Dartコードを見るともうちょっとわかりやすい違いだけど、StatefulWidgetはまさにWidget + Stateの組み合わせである。最初に描画のために作られるときに状態(State)が作られ、Widgetの構造が変わっても状態は引き継がれる。StatefulWidgetというよりもStateWithWidgetと言ってStateの方が主体と考えた方がいいかも。
ライフサイクル的にはcreateStateで作られて、後は状態の変更(setStateなど)または周辺(主に親)Widgetの変更(didUpdateWidget)でbuildが再度呼ばれる。でもcreateStateは最初の構成のときにしか行われない。
上記の俺のコードを見ればその理解の上で問題は明確になる。最初にcreateStateでreifyされるStateは、buildメソッドが固定でchildを返す。「最初の初期化の時点の」childを。理解が足りなかったから、知らず初期childを閉包してしまった。didUpdateWidgetのドキュメントを読んだら「Stateのwidget要素がフレームワークによって新しいWidgetに置き換えられる」とあったので理解した。俺のbuildはそのwidget要素((.-widget this)
かな?)の値を参照してればよかったが、そうじゃなくて固定で初期childを返していた。
対応としては自前でStatefulWidgetをreifyするんじゃなく、ClojureDartのFlutterライブラリが提供する機能である :managed
に頼って実装を変えたら問題なく動いた。具体的に多言語情報を監視するだけのWidgetsBindingObserverを:managedで管理している。
:managed
[_ (doto ^m/WidgetsBindingObserver
(reify ^:mixin m/WidgetsBindingObserver
(didChangeLocales [_this locales]
;; 処理
))
m/WidgetsBinding.instance.addObserver)
:dispose m/WidgetsBinding.instance.removeObserver]
これでわざわざ自前でStatefulWidgetまで作らなくても、やりたいことは達成できた。