WidgetsFlutterBindingから見るmixinの使い方
Dartにはmixinという多重継承を実現する仕組みがあるのですが、私は使いどころというのがいまいち分かっていません。
しかしFlutter Frameworkのコードを読んでいくと、その入口であるWidgetsFlutterBinding
で思い切りmixinが使われています。その使い方をまとめることでmixin利用の意義を考えます。
WidgetsFlutterBinding is 何
Flutterアプリを作るとき、main
関数内で呼び出しているのがrunApp()
です。
void main() {
runApp(MyApp());
}
そのrunApp()
の実装は以下のようになっています。
void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..scheduleAttachRootWidget(app)
..scheduleWarmUpFrame();
}
早速出てきましたね。WidgetsFlutterBinding.ensureInitialized()
というのがWidgetsFlutterBinding
の初期化処理をしています。
WidgetsFlutterBinding
はFlutter FrameworkとFlutter Engineをつなげる役割を持つクラスです。ツリーを構築したりユーザ入力を処理したりレンダリングパイプラインを呼び出したりとFlutterの重要な部分を色々と受け持っています。
Flutterの機能をrunApp()
以前に使いたい場合にはWidgetsFlutterBinding.ensureInitialized()
を呼び出すといい、という技があるためなんとなく名前を知っている人もいると思います。
WidgetsFlutterBindingの実装
多くの機能を持つクラスですが、WidgetsFlutterBinding
の実装自体は以下だけです。
//...
class WidgetsFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding, ServicesBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
//...
static WidgetsBinding ensureInitialized() {
if (WidgetsBinding.instance == null)
WidgetsFlutterBinding();
return WidgetsBinding.instance!;
}
}
要は
- スーパークラス
BindingBase
- ミックスイン
GestureBinding
SchedulerBinding
ServicesBinding
PaintingBinding
SemanticsBinding
RendererBinding
WidgetsBinding
が裏に潜んでいることが見て取れます。継承とmixinを使ってクラスを定義しているのですが、まずはmixinでなにができるのかを見ていきましょう。
mixinとは?
mixinは一部のプログラミング言語で実装されている仕組みで、多重継承のようなことを可能にします。with句を用いて1つのクラスに複数のmixinを適用することができます。
詳しくは以下のリンクを参照のこと。
宣言
mixinは以下のような構文で宣言できます。
mixin M1 on I1, I2 {
void field1() {
}
int field2 = 0;
}
これでM1というmixin(とinterface)が定義されます。Dart2.xではまだクラス宣言からもmixinが定義されますが、こちらは将来的に削除されるかもしれません。
on I1, I2
のようにmixin宣言でon以降にクラスを1個以上指定することでM1に対してスーパークラス制約を付けることができます。I1, I2
はrequired superinterfaceと呼ばれます。またon
を省略することも可能で、その場合はon Object
と書いたことと同じ扱いとなります。
mixin M2 { }
// 上と同じ意味になる
mixin M2 on Object { }
mixin宣言M1をinterfaceとして利用する場合は、以下のようなinterfaceになるように振る舞います。
//以下がmixinで宣言したものと同等
abstract class M1$super implements I1, I2 { }
abstract class M1 extends M1$super { body' }
//このように使える
class C implements M1 {
...
}
ここから、mixinのrequired superinterfaceは、mixinの適用先に実装を求めるimplements的な作用を持つことが言えます。mixin内で実装が要求されないのはmixin自体が実質的に抽象として振る舞うためです。mixin自体をインスタンス化することはできず、必ず実装を持つクラスとともに使われます。しかしmixinは実際クラスと同等のフィールドを持つことができるため、完全に抽象ではありません。
クラスへの適用
クラスへの適用はwith句を使って以下のように書くことができます。
class Base { }
mixin M3 on Base { }
class C extends Base with M3 { }
この場合、Baseに対してM3をmixinしたものをCが継承しているという流れになっています。
Base with M3
の部分で作られているクラスをmixin applicationと呼びます。extends
を省いてclass C with M
のように書くことも可能ですが、これはclass C extends Object with M
と同じ扱いであり、Mのmixin先はCではなくObjectです。
mixinは実質抽象であると言いましたが、required superinterfaceがなければmixin自体に未実装は存在しないためどのクラスに対してもmixinすることができます。
mixinがon句を持つ、つまりrequired superinterfaceを持つ場合はmixin先がそのすべてを実装していなければなりません。つまり以下のようになります
class Base2 { }
// on Object
mixin M4 { }
//クラスはObjectを継承しているので問題なし
class C1 extends Base2 with M4 { }
mixin M5 on I1 { }
// error: Base2にI1の実装がないため
class C2 extends Base2 with M5 { }
class Base3 implements I1 { }
// Base3はI1の実装をもつためOK
class C3 extends Base3 with M5 { }
// error: C4がI1の実装を持っていても関係ないのでだめ
class C4 extends Base2 with M5 implements I1 { }
mixin M6 on I1, I2 { }
class Base4 extends Base3 implements I2 { }
// Base4はI1とI2どちらの実装も持っているのでOK
class C5 extends Base4 with M6 { }
with句に複数のmixinを渡した場合(B with M1, M2
)は、B with M1
で生成された抽象クラスXに対してX with M2
がmixin applicationになります。雑に言えば
B with M1, M2, M3
は
((B with M1) with M2) with M3
のように左から順に解釈されます。
当然実際にこのような書き方はできません。
with句は順序を持つため、スーパークラス制約で互いを制約することはできません。
// error
mixin M1 on M2 { }
mixin M2 on M3 { }
mixin M3 on M1 { }
スーパー呼び出し
on句によるスーパークラス制約を付けることで、mixin内でスーパー呼び出しが可能になります。
class Base5 {
void field1() {
print("in Base5");
}
}
mixin M7 on Base5 {
void field2() {
super.field1();
}
}
on句で指定されたinterfaceに含まれるメンバであれば呼び出せます。この制約はスーパー呼び出し先が存在することを保証するためのものですが、実際の呼び出しの優先順はmixin applicationの生成時に決まるため、必ずしもon句に書いたinterfaceのメンバが選択されるとは限りません。
mixin M8 {
void field1() {
print("in M8");
}
}
// M7のスーパークラスの対象は`Base5 with M8`になるため
// field1()はM8で上書きされる
class C extends Base5 with M8, M7 { }
C().field2(); // -> in M8
このためmixinでのスーパー呼び出し先はmixin宣言だけでは定まらないことになります。ちょっと読みにくいですね...
ここまでの話からrequired superinterfaceは実質的にスーパークラス制約を付けることとmixin内でのスーパー呼び出しを安全にすることのみに使われていると考えることができます。
よってクラスの関係性を見たいときには(使いまわしているのでなければ)無視してよいと言えると思います。
WidgetsFlutterBindingでの使われ方
戻ってWidgetsFlutterBinding
がどのような構成になっているのかを考えたいと思います。
on句は無視できるということ、with句以下はBindingBase
に適用されていることを考慮すると、以下のように書けます。
withはそれより上のかたまりに対して適用していることに注意してください。on句のスーパークラス制約によりwith内のmixinの順番が制限されています。例えばRendererBinding
はServicesBinding
,SchedulerBinding
,GestureBinding
,SemanticsBinding
を要求するのでそれらより前に適用することはできません。
初期化処理
さて、実際にこのWidgetsFlutterBinding
がどのように動作するのかを見ていきましょう。特徴的なのは初期化時の処理です。
WidgetsFlutterBinding.ensureInitialized()
が呼ばれると、初めて呼び出す場合はインスタンス作成のためにWidgetsFlutterBinding()
を呼ぶことになります。
しかし、WidgetsFlutterBinding
はコンストラクタを宣言していません。
Dartではコンストラクタを宣言していない場合でも、スーパークラスのコンストラクタは呼ばれます。
class C extends S { }
class S {
S() {
print("in S");
}
}
C(); // -> in S
よってBindingBase
のコンストラクタが呼ばれることが分かります。
abstract class BindingBase {
//...
BindingBase() {
//...
initInstances();
//...
}
//...
}
上のようにその中でinitInstances()
が呼ばれているのですが、問題はこれが何を指しているのか、ということです。一応BindingBase.initInstances()
を見ると
void initInstances() {
assert(!_debugInitialized);
assert(() {
_debugInitialized = true;
return true;
}());
}
と、実質中身は無くスーパー呼び出しを要求していることから別に実装が存在することが考えられます。しかし継承先であるWidgetsFlutterBinding
はオーバーライドしていませんでした。
クラスへの適用 の部分でも話しましたが、mixinはwith句で元のクラスを上書きしていきます。よってmixinに実装があればそちらが呼ばれることになります。実際に7つ全てのmixinがinitInstances()
を実装しています。
上書きしていくため最初に呼ばれるのはwith句の最後にあるWidgetsBinding.initInstances()
です。それらを考慮すると以下のような呼び出し順序になっていることが分かります。
それぞれの実装でsuper.initInstances()
が冒頭で呼ばれているため、コードが実行される順序は逆順になります。
mixin WidgetsBinding {
void initInstances() {
super.initInstances();
_instance = this;
assert(() {
_debugAddStackFilters();
return true;
}());
// Initialization of [_buildOwner] has to be done after
// [super.initInstances] is called, as it requires [ServicesBinding] to
// properly setup the [defaultBinaryMessenger] instance.
_buildOwner = BuildOwner();
buildOwner!.onBuildScheduled = _handleBuildScheduled;
window.onLocaleChanged = handleLocaleChanged;
window.onAccessibilityFeaturesChanged = handleAccessibilityFeaturesChanged;
SystemChannels.navigation.setMethodCallHandler(_handleNavigationInvocation);
assert(() {
FlutterErrorDetails.propertiesTransformers.add(debugTransformDebugCreator);
return true;
}());
}
//...
}
まとめ
ここまでmixinの概要とWidgetsFlutterBinding
でどのように使われているかを見てきました。WidgetsFlutterBinding
では主に「クラス機能の分割」という意図でmixinをしていると考えられます。並列な機能をひとつのクラスから分割するにはDartの言語仕様ではmixinしかできないことなので選ばれたのだと思います(C#とかだとpartial classが該当するかな?)。
またmixinについてはmixinした後のスーパー呼び出しの決定アルゴリズムは明快でよいと思うのですが、mixin単体だけだと何が呼び出されるかわからない、というのはいささか読みづらいと感じます。そのあたりはIDEパワーでなんとかしてくれ、なんですかね。
なのでクラス分割の用途でmixinを個人で使う場合は、互いに独立な(他のmixinの呼び出しを持たない)程度で使うというのが可読性を考えるとよいのかなと思います。
参考
Discussion