🐣

Widgetクラスについて調べてみた

2022/02/05に公開約7,000字

Flutterを2021年6月から始めた初学者です。
自分でアウトプットすることに苦手意識があったので、克服の意味をこめて記事を書いてみます。
内容に不備や補足などありましたらご連絡ください。

はじめに

まず、Flutterについて軽くさわりの話をします。
Flutterは基本的にウィジェットによって構成されており、いろんなウィジェットを配置することで画面表示や画面遷移、アニメーション処理などを実現しています。
そして、ウィジェット中には画面に実際に表示するウィジェットや画面表示を整えるウィジェット、動きをつけるウィジェットなど異なる振る舞いをするものがあります。
今回、その大元であるWidgetクラスについて調査してみました。

そもそもWidgetクラスって?

Flutterを学び始めるとまず最初にStatelessWidgetとStatefulWidgetを使ってアプリを作りますね。
StatelessWidgetは表示内容を更新しない画面で使い、StatefulWidgetは表示内容などを更新する画面で使いました。
(StatelessWidgetでもProviderやRiverpodなどを使えば更新可能です。)
これらのクラスはWidgetクラスを継承しています。
では、Widgetクラスとはそもそもどんなクラスでしょう?
ソースコードのコメントには以下のように記載されています。

Widgets themselves have no mutable state (all their fields must be final).

ウィジェット自身は 変更可能(mutable)状態(state)持たない(have no)
ここで状態(state)とはアプリケーションを構成するデータと捉えます。
Widgetクラスが保持するすべてのデータは変更不可となります。
ソースコードを見ても @imutable のアノテーションがついてますね。


abstract class Widget extends DiagnosticableTree {

Widgetクラス自体がimmutableということは、それを継承しているStatelessWidgetとStatefulWidgetもimmutableであり、それらが保持しているデータの変更が行えないということになります。
ここで、疑問になるのが状態の変更が行えるStatefulWidgetですが、これについては後述します。

Widgetクラスを継承している様々なウィジェット

ということでここからが本題です。
Widgetクラスを継承しているウィジェットについて調べてみました。
調べ方は簡単。VS Codeを開いて、Widgetクラスを開きF12を押し、そこで継承しているクラスを探してみます。
Widgetクラスが記載されているframework.dartで以下のWidgetが見つかりました。
※Flutterのバージョンは2.8.1を利用しています。

  • StatelessWidget
  • StatefulWidget
  • ProxyWidget
  • RenderObjectWidget
  • _NullWidget

さて、1つプライベートな奴が出てきましたが、上から1つずつクラスに記載されているコメントを見ていきます。
Flutterはソースコードのコメントが充実しているのでこういう時に助かりますね!

StatelessWidget

おなじみのやつです。名前の通り State(状態)less(〜のない) なWidgetです。
先頭に以下のように記載されていますね。

A widget that does not require mutable state.

変更可能(mutable)状態(state)必要としない(not require) ウィジェットである。
Widgetクラスとは違い「変更可能な状態を持たない」ではなく、「変更可能な状態を必要としない」と少し表現が変わってます。
「必要ないならこっち使ってね」ってことですね。

せっかくなのでStatelessWidgetで再度、F12を押して継承しているウィジェットを見てましょう。
たくさん出てきましたので一例を記載します。

  • Card
  • DataTable
  • Dialog
  • ScrollView
  • Container
  • GestureDetector
  • Text

見てみると、なんとなくわかるものもありますが、ScrollViewみたいに描画内容が更新されそうなものも含まれてますね。

StatefulWidget

こちらもおなじみです。これも名前の通り State(状態)ful(〜の性質を持つ) Widgetです。
コメントには以下のように記載されています。

A widget that has mutable state.

変更可能(mutable)状態(State)を持つ ウィジェットである。
はい、ちょっと待った。
最初にWidgetクラスに書いてあった 「ウィジェット自身は変更可能な状態を持たない」 はどこいった?Widgetクラスは変更不可(immutable) だったのでは?
コメントを読み進めると書いてありました。

[StatefulWidget] instances themselves are immutable and store their mutable
state either in separate [State] objects that are created by the
[createState] method, or in objects to which that [State] subscribes, for
example [Stream] or [ChangeNotifier] objects, to which references are stored
in final fields on the [StatefulWidget] itself.

英語苦手なのでDeepLさんの力を借りてざっくり要約すると、StatefulWidgetインスタンス自身は 変更不可(immutable) であり、createStateメソッドで生成されたStateオブジェクト をStatefulWidgetの finalフィールド に保持する。
Stateオブジェクトを保持して、状態管理自体をStateオブジェクトに委譲することでWidgetクラスのルールを保っているというわけですね。

ここで新たな疑問が生まれます。
じゃあ、状態管理できるStateクラスって何者よ?
次にStateクラスのコメント見てみます。

The logic and internal state for a [StatefulWidget].

StatefulWidgetのためのロジックと内部状態である。
この文章だけではわかりにくいですね。

[State] objects are created by the framework by calling the
[StatefulWidget.createState] method when inflating a [StatefulWidget] to
insert it into the tree.

ここでもDeepLさんの力を借りて要約してみます。
Stateオブジェクトは、ツリーに挿入するためにStatefulWidgetを展開する時に、フレームワークによってStatefulWidget.createStateメソッドの呼び出されることで作成される。
ここでツリーという言葉が出てきましたが、これを深掘りすると話が発散しそうなのでここでは深く追いません。

StatelessWidget同様に継承しているWidgetを少し見てましょう。

  • MaterialApp
  • CalendarDatePicker
  • PaginatedDataTable
  • Scaffold
  • Slider
  • TextField
  • RawGestureDetector

中にはStatelessWidgetを継承しているものと似た名前のウィジェットもありますね。

ProxyWidget

見慣れないWidgetが出てきました。
コメントを見てみましょう。

A widget that has a child widget provided to it, instead of building a new widget.

新しいWidgetが作成される代わりその子Widgetが提供される Widgetであると書かれています。
さらに読み進めてみます。

Useful as a base class for other widgets, such as [InheritedWidget] and
[ParentDataWidget].
 
InheritedWidgetやParentDataWidgetのような他のWidgetのベースとなるクラスである。
なんだそれは?
先にF12で継承しているWidgetを見てみると、確かにコメントに書いてある2つのウィジェットが見つかりました。

InheritedWidgetは先祖にあるウィジェットに効率的にアクセス可能なウィジェットですね。
階層が深くなってもパフォーマンスが劣化しないようになります。

ParentDataWidgetは初ものです。
コメント見てみます。

Base class for widgets that hook [ParentData] information to children of
[RenderObjectWidget]s.

子となるRenderObjectWidget に対して ParentDataをフックするウィジェットのためのベースとなるクラスである。

内容を見るに親が保持している情報を元に子Widget毎に設定を変える場合などに使われるようです。
コメントにはStackの例が記載されています。

For example, [Stack] uses the [Positioned] parent data widget to position each child.

Stackクラスは子Widgetの位置決めに親WidgetのデータにPositionedを利用している。

とはいえ、上記のWidgetのベースとなるProxyWidgetは直接使うことはなさそうです。
パッケージやフレームワークを作成する際にどこかで役立つかも?

RenderObjectWidget

名前を直訳すると RenderObject(描画オブジェクト) のWidgetです。
とりあえずコメントをみてみます。

RenderObjectWidgets provide the configuration for [RenderObjectElement]s, which wrap [RenderObject]s, which provide the actual rendering of the application.

RenderObjectWidgetはアプリケーションに実際にレンダリングを提供するRenderObjectラップするRenderObjectElement設定を提供する とあります。
実際にレンダリングを提供とは?
実はこれまで出てきたWidgetたちにはレンダリングする機能がなく、RenderObjectElemntが実際のレンダリングを行なっているとかなんとか。
実際のレンダリングについてはWidgetとElementの関係やElementについての理解が必要なので割愛します。
良い記事がたくさんあるのでそちらで勉強してください。

先に記載したStatelessWidgetを継承していたTextウィジェットですが、ソースコードを追っていくとRichTextウィジェットを内部で生成しています。

text.dart

  Widget build(BuildContext context) {
    // <中略>
    Widget result = RichText(

RichTextウィジェット自体はMultiChildRenderObjectWidgetというRenderObjectWidgetを継承したクラスです。

basic.dart
class RichText extends MultiChildRenderObjectWidget {

つまり、描画を行うWidgetはこのRenderObjectWidgetを継承した何かしらのウィジェットを生成しているということですね。

_NullWidget

最後のWidgetですが、このWidgetにはコメントがありません。
中身を見てみるとcreateElementメソッドを実装してあり、エラーをスローするようになっています。

  
  Element createElement() => throw UnimplementedError();

とりあえず何に使われているかソースコードを追ってみます。
_NullElementなるものが呼び出してます。

class _NullElement extends Element {
  _NullElement() : super(_NullWidget());

さらに追っていきます。

abstract class RenderObjectElement extends Element {

  
  List<Element> updateChildren(List<Element> oldChildren, List<Widget> newWidgets, { Set<Element>? forgottenChildren, List<Object?>? slots }) {
    // <中略>

   final List<Element> newChildren = oldChildren.length == newWidgets.length ?
        oldChildren : List<Element>.filled(newWidgets.length, _NullElement.instance);
    // <中略>

    assert(newChildren.every((Element element) => element is! _NullElement));
    return newChildren;
  }
class MultiChildRenderObjectElement extends RenderObjectElement {

  
  void mount(Element? parent, Object? newSlot) {
    super.mount(parent, newSlot);
    final List<Element> children = List<Element>.filled(widget.children.length, _NullElement.instance);
    Element? previousChild;

}

RenderObjectElementとMultiChildRenderObjectElementに行きつきました。
描画更新の際に新旧のツリーのサイズが異なる場合にElementツリーを_NullElementで初期化しているように見えます。(間違っていたらごめんなさい。)
プライベートで利用しているWidgetなので、今回はあまり深追いしません。

終わりに

以上、ソースコードとドキュメントを読み解きながらWidgetについて調べてみました。
ここまで長々とお付き合いいただきありがとうございました。

Discussion

ログインするとコメントできます