🍜

WidgetsFlutterBindingから見るmixinの使い方

2021/11/25に公開

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();
}

https://github.com/flutter/flutter/blob/3595343e20/packages/flutter/lib/src/widgets/binding.dart#L1034

早速出てきましたね。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!;
  }
}

https://github.com/flutter/flutter/blob/f9c4b227213fe468bf221d2413d575cd446069dd/packages/flutter/lib/src/widgets/binding.dart#L1237

要は

  • スーパークラス
    • BindingBase
  • ミックスイン
    • GestureBinding
    • SchedulerBinding
    • ServicesBinding
    • PaintingBinding
    • SemanticsBinding
    • RendererBinding
    • WidgetsBinding

が裏に潜んでいることが見て取れます。継承とmixinを使ってクラスを定義しているのですが、まずはmixinでなにができるのかを見ていきましょう。

mixinとは?

mixinは一部のプログラミング言語で実装されている仕組みで、多重継承のようなことを可能にします。with句を用いて1つのクラスに複数のmixinを適用することができます。
詳しくは以下のリンクを参照のこと。

https://github.com/dart-lang/language/blob/master/accepted/2.1/super-mixins/feature-specification.md

https://ntaoo.hatenablog.com/entry/2018/12/02/192827

https://spec.dart.dev/DartLangSpecDraft.pdf

宣言

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, I2required 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 M2mixin 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に適用されていることを考慮すると、以下のように書けます。

WidgetsFlutterBindingの構成

withはそれより上のかたまりに対して適用していることに注意してください。on句のスーパークラス制約によりwith内のmixinの順番が制限されています。例えばRendererBindingServicesBinding,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()です。それらを考慮すると以下のような呼び出し順序になっていることが分かります。

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の呼び出しを持たない)程度で使うというのが可読性を考えるとよいのかなと思います。

参考

https://medium.com/flutter-community/dart-what-are-mixins-3a72344011f3

https://github.com/dart-lang/language/blob/master/accepted/2.1/super-mixins/feature-specification.md

https://ntaoo.hatenablog.com/entry/2018/12/02/192827

https://spec.dart.dev/DartLangSpecDraft.pdf

https://qiita.com/kurun_pan/items/04f34a47cc8cee0fe542

https://api.flutter.dev/flutter/widgets/WidgetsFlutterBinding-class.html

Discussion