#祝、Flutter1.0! 僕は君の実装が気になったよ。
普段はサーバー側の人間なのだが、前々からiOS/android/(&web)のクロスプラットフォーム開発(クロス開発)に関心を持っている。
だいぶ前になるがkotlinでiOSを書くことに関心を持った際には、RoboVMに相当入れ込んで、1年弱、android開発者に転身したほどに。クロス開発の案件に関わる事ができる前にRoboVM自体がバイアウトされて消えて、グーグル&オラクル周りの分析なども無駄になってしまったのだけれど。
さて、ここで列挙することはしないけれども、クロス開発の選択肢は結構多い。選択肢が多いことは、クロス開発の決定版がなかなかないことの裏返しでもある。
使いやすく守備範囲が広いクロス開発プラットフォームを作りきることは難しいのだ。
そんな困難を突破しつつあると思われるFlutterについてはこのアドベントカレンダーの力の入った他記事に譲るとして、ここでは、クロス開発を実現する見事な実装例として、Flutterのソースコードに注目したい。
私の関心事は、ドメイン駆動設計(DDD)の観点から、Flutterのソースコードはどれほどクリーンか、ということだ。DDDについての検討は、エンタープライズアプリケーションを念頭になされることが多い。例えば、DDD界隈の有名本のひとつ「クリーンアーキテクチャ」には以下の図もそうだ。
ただ、コアとなる抽象的な実装を手厚く行い、プラットフォームに依存した実装はなるべく簡素(humble)に、といったドメイン駆動開発の原則は、アプリケーション開発全般に望まれるものである。
iOS/androidの各プラットフォームは変化を続けていく。そのため、Flutterのようなクロス開発フレームワークは、ターゲットの変化に強い作りとしなければならない。ついに1.0に達した、その実装は興味深い。
Flutterの実装言語のDartはグーグルの稼ぎ頭AdSenseのサービスを支えている。
かつてのオラクルv.s.グーグルのAndroidJavaを巡る裁判の頃に、グーグルにより開発が開始されたFlutterには、相当力のあるエンジニアが投入されてきたものと推察される。ここでは、そんな読み応えがありそうなFlutter のコードを読み始めて感じたところを軽く書いておく。
Flutterのdartソースコード
はじめに、Flutterのdartソースコードを量的に概観(cloc)しておく。
https://github.com/flutter/flutter
$ cloc flutter/
2643 text files.
2286 unique files.
705 files ignored.
github.com/AlDanial/cloc v 1.74 T=111.10 s (17.5 files/s, 4800.3 lines/s)
--------------------------------------------------------------------------------
Language files blank comment code
--------------------------------------------------------------------------------
Dart 1652 53877 85427 379872
YAML 56 339 285 2613
Markdown 54 576 0 1467
Groovy 27 215 74 1430
Java 28 245 112 1390
JSON 18 15 0 1259
Bourne Shell 11 135 114 636
Objective C 31 133 60 615
XML 34 65 139 568
CSS 2 43 8 203
Python 3 51 52 196
DOS Batch 2 32 37 148
Swift 2 14 3 99
Bourne Again Shell 1 24 59 94
HTML 8 5 3 80
JavaScript 1 8 11 74
C/C++ Header 15 43 30 69
Dockerfile 1 12 28 65
Ruby 1 6 5 64
PowerShell 1 13 16 50
--------------------------------------------------------------------------------
SUM: 1948 55851 86463 390992
--------------------------------------------------------------------------------
1.0時点のFlutterのdartソースコード量は38万行ほど(テストコード、例題コード等を含む)。
相当に泥臭い実装も必要になるであろう、クロス開発プラットフォームとしては、(裏方にがっつりと各プラットフォーム向けのC++のコードがあるにしても)こんなものなのだろう。他方、ファイル数は1652と多くない。1ファイルあたりの平均コード量は230行ほどだ。多数のクラスが含まれるファイルがあるものと想像される。
おまけで、実装言語のdartについて。私はセミコロンを強要する言語はカスだと決めつける偏った人間なのだが、ほどよくJavaっぽいが簡潔な型ありスクリプト言語であるdartについては、「まぁ、あり」と思っている。
はじめて、dartのことを知って文法を軽くチェックしたのは、C++1xすぎる方が、Dartすごい。マジすごい。美しいと絶賛しておられたのを目にした時だった。そこから7年ほど。多少コンパイル速度を犠牲にしてでもセミコロンを廃止すれば、【dart2さん、マジ天使】とでも書いたのかもしれないが、まぁ、dart2は正常進化しているようだ。私はdartを書くことは当面ないだろうけれど、毎日書く必要があるならば十分受け入れられる簡潔さをもった言語だと思う。Javaをたまに書く身としては、dartの驚きが少ない面は良い。
Flutterソースコードを読むための環境構築
結論としては、ソースコードを読むための環境構築は、普通にAndroidStudioをインストールしてエミュレータでflutterをお試し実行してみるうちに終わっていた。
ということで、委細は、他に譲る。
ポイントは以下の2点。
- Flutterを自分の環境向けに導入する
- これによりflutterのソースコードも手に入る
- インストール先: https://flutter.io/docs/get-started/install
- AndroidStudioにグーグル謹製のFlutterプラグインを入れる
AndroidStudioでの初回実行時には、コードにインデックスが貼られるまでは時間がかかるため、
「Flutter 1.0がリリースされたので概要から、環境構築、実装方法、アーキテクチャ、情報収集方法まで全部書く」など、本アドベントカレンダーの記事、すこし前のスライド「DART/FLUTTER 入門+ 最強の勤怠アプリを作った話 2017/2/27」などから先達の取り組みを学び、Flutterの概要を捉えておこう。
-
React/Fluxになじみのある方は、以下から入るのが良いか:
https://adwd.github.io/dart-flutter-slide/#/8 -
Java/scala等でFuture/Streamになじみのある方は、以下から入るのが良いか:
https://adwd.github.io/dart-flutter-slide/#/7
コールドリーディングの開始箇所
Flutterのコードを読み始めるポイントは、お試し実装のmainが依拠しているStatelessWidgetが良いと思う(FlutterではUI要素のすべてがWidgetオプジェクト)。
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
クロス開発プラットフォームゆえ、このStatelessWidgetはデバイス(iOS/Android)に依存しない作りとなっているはずだ。
ソースコードからコメントを省いて引用する
abstract class StatelessWidget extends Widget {
const StatelessWidget({ Key key }) : super(key: key);
@override
StatelessElement createElement() => StatelessElement(this);
@protected
Widget build(BuildContext context);
}
StatelessWidgetには、メソッドが2つだけ定義されている。MyAppが拡張しているのはbuildメソッドだけ。このシンプルさは良い実装を予感させる。なお、StatelessWidgetが属するframework.dartは、後述のStatefulWidgetはじめ、多くの中核的なクラスを持つ。コメント込みで5000行の実装だ。
StatelessWidgetのcreateElementが返すStatelessElementは、ComponentElementを拡張したもの。Flutterは、Componentを直接さらさない作りとしたらしい。ElementがStatelessWidgetをuseするという作りにも注目だ。
/// An [Element] that uses a [StatelessWidget] as its configuration.
class StatelessElement extends ComponentElement {
/// Creates an element that uses the given widget as its configuration.
StatelessElement(StatelessWidget widget) : super(widget);
@override
StatelessWidget get widget => super.widget;
@override
Widget build() => widget.build(this);
@override
void update(StatelessWidget newWidget) {
super.update(newWidget);
assert(widget == newWidget);
_dirty = true;
rebuild();
}
}
フレームワークの作り手としては、abstract class ComponentElementから先の裏方の実装が主戦場なのだろう。
さて、StatelessWidgetは一意のキーを持つ。
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' show hashValues;
import 'package:meta/meta.dart';
/// A [Key] is an identifier for [Widget]s, [Element]s and [SemanticsNode]s.
///
/// A new widget will only be used to update an existing element if its key is
/// the same as the key of the current widget associated with the element.
///
/// Keys must be unique amongst the [Element]s with the same parent.
///
/// Subclasses of [Key] should either subclass [LocalKey] or [GlobalKey].
///
/// See also the discussion at [Widget.key].
@immutable
abstract class Key {
/// Construct a [ValueKey<String>] with the given [String].
///
/// This is the simplest way to create keys.
const factory Key(String value) = ValueKey<String>;
/// Default constructor, used by subclasses.
///
/// Useful so that subclasses can call us, because the [new Key] factory
/// constructor shadows the implicit constructor.
@protected
const Key.empty();
}
/// A key that is not a [GlobalKey].
///
/// Keys must be unique amongst the [Element]s with the same parent. By
/// contrast, [GlobalKey]s must be unique across the entire app.
///
/// See also the discussion at [Widget.key].
abstract class LocalKey extends Key {
/// Default constructor, used by subclasses.
const LocalKey() : super.empty();
}
/// (略)
class ValueKey<T> extends LocalKey {
/// Creates a key that delegates its [operator==] to the given value.
const ValueKey(this.value);
/// The value to which this key delegates its [operator==]
final T value;
@override
bool operator ==(dynamic other) {
if (other.runtimeType != runtimeType)
return false;
final ValueKey<T> typedOther = other;
return value == typedOther.value;
}
@override
int get hashCode => hashValues(runtimeType, value);
@override
String toString() {
final String valueString = T == String ? '<\'$value\'>' : '<$value>';
// The crazy on the next line is a workaround for
// https://github.com/dart-lang/sdk/issues/33297
if (runtimeType == _TypeLiteral<ValueKey<T>>().type)
return '[$valueString]';
return '[$T $valueString]';
}
}
class _TypeLiteral<T> { Type get type => T; }
適度にコメントが付されたFlutterのdartコードは文法の知識があまりなくとも、それなりに読むことができる。
key.dartを読み流して、これはどういう書き方なのか、と思ったの以下のところだけ。
/// This is the simplest way to create keys.
const factory Key(String value) = ValueKey;
ValueKeyにKeyの名前解決(?)を委譲しているのだろう、くらいまではわかるので、何とかなるけれども。dartの文法速習的なサイトをあとで探してみよう。
先達のスライドを見るに、dartのコンストラクタでは、**名前付き引数{k,j}**を用いた少し独特な記法を行うことと似ている気がした。見た目かなりJavaなdartの中の、dart wayな書き方なのだろうか。
class HelloWorld extends StatelessWidget {
// コンストラクタ
// {}の中は名前付き引数 new HelloWorld(name: 'foo');
HelloWorld({Key key, this.name}): super(key: key);
final String name;
// buildメソッドをoverrideする
@override
Widget build(BuildContext context) {
return new Text('Hello, $name!');
}
}
出典 https://adwd.github.io/dart-flutter-slide/#/10/5
#コードを読むべきは、1000種類以上あるWidgetオブジェクト周りの実装
Flutterは、多様なWidgetを持つ。以下に一覧化されている通り、すでに1000種類を超えているという。
https://flutter.io/docs/reference/widgets
このWidget一覧(Flutter widget index)ページは、flutterのコードリーディングのほどよいとっかかりとなる。
例えば、DataTable widget。継承関係は以下の通りとなる
Object -> Diagnosticable -> DiagnosticableTree -> Widget -> StatelessWidget -> DataTable
https://docs.flutter.io/flutter/material/DataTable-class.html
そう、 DataTableは前述のStatelessWidgetクラスを継承している(DataTable extends StatelessWidget)。
これらWidget界隈がしっかり実装されていることは、Flutterが信頼できるかどうかの試金石となりそうだ。ちなみに、「... extends StatelessWidget」という実装はFlutter内で(テストコード含め)300回以上登場。「... extends StatefulWidget」の方ら340以上と1割多め。
このStatelessWidget/StatefulWidgetは、ドメイン駆動設計の私的用語では、まさしく最重要オブジェクト《ヘビーオブジェクト》の典型例となる。両者がバランス良く実装され続け、iOS/android、そしてwebで安定的に動作し続けることが、クロス開発プラットフォームとしてのFlutterの良しあしを決めるものと思われる。各環境のアップデートに追従しつつ、機能拡張を行うために、幾多の改修とリファクタリングが必要となるはずだ。その中核に位置するStatelessWidget/StatefulWidget周りの実装は、react(native)はじめ、幾多のクロス開発プラットフォームとの差別化要素となる、Google Flutterチームのまさしく決戦兵器だ•̀.̫•́✧
参考
リリファクタリングの《最重要目標》のオブジェクトに分かりやすい名前を与える。
実装としては、当然StateのあるStatefulWidgetの方が複雑と思われる。ただ、当然、他のコンポーネント指向のフレームワークと近いコードの作りになることが予想される。以下の対比は出発点として良かった。
出典 https://adwd.github.io/dart-flutter-slide/#/10/18
StatefulWidgetのクラス定義を見るに自身で管理したいStateをoverrideして定義していくのが流儀らしい。
@override
_MyState createState() => _MyState()
abstract class StatefulWidget extends Widget {
/// Initializes [key] for subclasses.
const StatefulWidget({ Key key }) : super(key: key);
/// Creates a [StatefulElement] to manage this widget's location in the tree.
///
/// It is uncommon for subclasses to override this method.
@override
StatefulElement createElement() => StatefulElement(this);
/// Creates the mutable state for this widget at a given location in the tree.
///
/// Subclasses should override this method to return a newly created
/// instance of their associated [State] subclass:
///
/// ```dart
/// @override
/// _MyState createState() => _MyState();
/// ```
///
/// The framework can call this method multiple times over the lifetime of
/// a [StatefulWidget]. For example, if the widget is inserted into the tree
/// in multiple locations, the framework will create a separate [State] object
/// for each location. Similarly, if the widget is removed from the tree and
/// later inserted into the tree again, the framework will call [createState]
/// again to create a fresh [State] object, simplifying the lifecycle of
/// [State] objects.
@protected
State createState();
}
StatefulWidgetの方も、メソッドは最小限の2つだけなんだね。
##(付記) Reduxのアプローチを採用する場合
なお、UIが複雑なアプリの場合、循環参照の弊害や、テストしやすさの確保の見地から、StatefulWidgetを可能な限り使わないという選択もありうる。
いわゆるReduxのアプローチがその一例。
参考 JS ? ReactNative × Redux ? ダサいよ... Flutter × Reduxにしましょう...
- 「Flutter × Redux」のアプローチについて、1枚絵を引用させてもらおう。
StoreとReducerは単体テストが可能、内部状態を持たないStatelessWidgetは控えめ(humble)でアプリの結合テスト時に不具合はつぶしやすい、と。
Flutterコードリーディングの実践目標
グーグルが推奨するFlutterのreactiveな書き方、**BLoC(Business Logic Component)**を支える仕組みに納得感を得ておくことが(Flutterで書かれたコードを実務で扱うという意味での)実践的なコールドリーディングの目標になると思う。適宜、Widgetクラスとそのサブクラスをチェックしつつ。
私的目標としては、サーバ側のreactiveな作りと対比しつつ捉えておきたいところ。
数年後にFlutterがどのくらいメジャーになっているかは分からないけれど、読むに値するコードと思うよ。
(おまけ) サーバサイドDart
Flutterのサーバ側のお相手は有名なFirebaseが真っ先に候補になるためか、サーバ側のdartのwebフレームワークにあまり有名なのはなさそうだった。
ただ、aqueductは割合としっかり作り込まれているようだった。コード数は約4万行。RESTインターフェースに特化してるという意味では同類と思われるakka-http(scala+java)の8万行の約半分(だからどう、というわけではないが。)。
このフレームワークの作者の方がflutterとaqueductとを組み合わせたtodoアプリの例を公開してくれていた。両者でモデル定義等を共有するsharedあたりは参考になるかもしれない:
https://github.com/stablekernel/aqueduct_examples/tree/592d1e407d2b3fbcc80cee30821e4bf669b8af9d/todo
サーバサイドでdartを使おうとする人は、クライアントもdartベースのもの、特にflutterだろうから、そういうチュートリアルが整備されれば、流行りだすのかもしれない。