ntaoo blog

主にDart, Flutter, AngularDartについて書いていきます。ときどき長文。日本語のみ。Twitter: @ntaoo

Flutter Webが公開された / Flutter Native アプリをFlutter Webアプリに移植してみた

Flutter Frameworkの上でDartでUIを書いていくので、HTML, CSS, JavaScriptの各APIはそのframeworkに隠蔽されてそれらを直接書く必要がない。

現状の制約

段階としては、まだテクニカルプレビューなので、制約や未実装のAPIがある。


Limitations We intend to completely support all of Flutter's API and functionality across modern browsers. However, during this preview, there are a number of exceptions:

flutter_web does not have a plugin system yet. Temporarily, we provide access to dart:html, dart:js, dart:svg, dart:indexed_db and other web libraries that give you access to the vast majority of browser APIs. However, expect that these libraries will be replaced by a different plugin API. Not all Flutter APIs are implemented on Flutter for web yet. Performance work is only just beginning. The code generated by Flutter for web may run slowly, or demonstrate significant UI "jank". At this time, desktop UI interactions are not fully complete, so a UI built with flutter_web may feel like a mobile app, even when running on a desktop browser. The development workflow is only designed to work with Chrome at the moment.

https://github.com/flutter/flutter_web#

Flutter Web アプリをFlutter Native アプリから移植してみた

Flutter Create コンテスト用にHIITタイマーを1日ででっち上げてたので、それをFlutter Webに移植してみた。

  • Flutter Web用のpackageはFlutterのpackageとは(まだ)別なので、UIにそれぞれのプラットフォームごとにimportで静的な依存関係がある。だからUIコードは今のところコピペが必要になる。UIコードをそのままコピペして動いた。将来はpackage:universal_uiと切り出せるようになるだろう。

  • 一部のAPIは未実装。たとえばGradient APIについては実行時にUnimplemented Errorが出た。

  • Flutter Webでmaterial iconの設定方法をまだ調べていない。なにか方法があるはず。

  • UIにSoundPlayer関係の見苦しいビジネスロジックが散らばっているけど、これはFlutter Createコンテストに提出時に時間がなかったのでこうなっているだけで、本来はSoundPlayerをmodelにDIしてmodel内に閉じ込めるべきロジックです。あとで修正しておきたい。

  • サウンドはWebAudioで実装してHIIT Timer ModelDIしたらちゃんと鳴るはず。

  • Routingはどうやっているのか興味深い。SPAみたいだがいまのところ、History APIを操作しているわけではない?(そりゃそうかも)

  • Hot Reloadがちゃんと動いているようだ。(webdev serve auto=restart) WebでHMR体験。すごい。

もうすこし調べて内部の実装も読んでみる。

キーボードの自作欲

商業ベースのキーボードは、既存のキーボードの規格に縛られてしまい革新を期待できそうにない。 どのような技術革新が起ころうとも、おそらく一生キーボードを使い続けることになるので、キーボードを自作しても割に合う投資になるはず。自作キーボード市場が盛り上がってきているようなので、自分もいつか自作したい。 今はmacOSを前提にして入力環境をかなりカスタマイズしている。他のOSにも簡単に対応するために、そのカスタマイズをキーボード側に寄せていく。

gist.github.com

BLoCパターンにおける、AngularDartでのStreamの扱い方

BLoCパターンでModelを設計するとUIとの通信はStreamとSinkに限定される。StreamをAngularDartのComponentでlistenしてViewを更新するコードについて、迷ったりハマったりするかもしれないところを解説する。

Async Pipeを使用する際の不具合を避ける

BehaviorSubject、StreamTransformer、AsyncPipeの組み合わせで、無限ループ

問題ないケース

// Model
final aBehaviorSubject = BehaviorSubject<String>();
Stream<String> get aStream => aBehaviorSubject.stream;

// Angular Template
<div *ngFor="let element of aStream | async">{{element}}</div>

無限ループが起こるケース

BehaviorSubjectからのObservableまたはStreamStreamTransformertransformしたものを、getterAsyncPipeにパラメーターとして渡すと、無限ループが起こる。

// Model
final aBehaviorSubject = BehaviorSubject<String>();
Stream<String> get aStream => aBehaviorSubject.doOnData(print).stream;

// Angular Template
<div *ngFor="let element of model.aStream | async">{{element}}</div>

上記の例ではdoOnDataでstreamに流れる値をprintしており、これはデバッグ時によく使う手法だが、 (doOnDataに限らず、) Observableのmethodは内部でStreamTransformerで処理されている。

もちろん、package:stream_transformや、自作のStreamTransformerでも同様にこの問題が起こる。

// Model
final aBehaviorSubject = BehaviorSubject<String>();
// Stream<String> get aStream => aBehaviorSubject.stream.transform(tap(print));

// Angular Template
<div *ngFor="let element of model.aStream | async">{{element}}</div>

なぜ無限ループが起こるのか

ChangeDetectionで値を比較する際、StreamTransformerを起動することにより、毎回異なるidentityのstreamgetterで生成してしまうため。StreamTransformerは、同期的なCollection操作と同様に、副作用をおこさないために新しいStreamインスタンスを生成する。(より正確には、BehaviorSubjectからのStreamTransformerは、その内部に独自のBroadcastStreamControllerを持ち、bindによって入力Streamを元にそのBroadcastStreamController参照を保持した新たなBroadcastStreamが生成される。)

注) BehaviorSubjectBroadcastStreamControllerである。

  1. ChangeDetection -> Async Pipe transform
    1. 初回なので、そのままlisten。BehaviorSubjectのstreamなので即座に値が流れてくる
    2. _updateLatestValue -> markForCheckでChangeDetectionを起動させる
  2. ChangeDetection -> Async Pipe transform
    1. getterで新たなstreamを取得するため、以前のstreamをdisposeし、その新たなstreamでAsync Pipe transformを再帰呼び出しする
  3. Async Pipe transform(再帰
    1. getterで新たなBehaviorSubjectのstreamをlistenする。即座に値が流れてくる
    2. _updateLatestValue -> markForCheckでChangeDetectionを起動させる。そして2.1.へ戻る

どうすれば良いか

ChangeDetectionのたびにgetterによりStreamTransformerによる変換を経て新たなStreamが生成されることが問題なので、生成されたstreamをcomponentのインスタンス変数に保持しておくことで防ぐ。

// 可能ならばconstructorで処理し、finalを付ける。
Stream aStream;
ngOnInit() {
  aStream = model.aStream;
}

このほうが無駄なStreamインスタンス再生成を省略できるので、処理効率も良い。しかし、ボイラープレートコードが増えるので退屈。

Viewの更新手法が異なるFlutterのStreamBuilderでは、私が理解している限りではこのような問題は起こらず、同時により効率的な更新手法になっている。

Single Subscription Stream、StreamTransformer、AsyncPipeの組み合わせで、EXCEPTION: Bad state: Stream has already been listened to.

Single Subscription Stream Controllerから生成されたstreamををlistenできるのは一度だけである。複数回listenすると、EXCEPTION: Bad state: Stream has already been listened to.というエラーが起こる。

// Model
final aStreamController = StreamController<String>();
// Stream<String> get aStream => aStreamController.stream.transform(tap(print));

// Angular Template
<div *ngFor="let element of model.aStream | async">{{element}}</div>

なぜAsync Pipeでこのエラーが起こるのか

こちらも、上記の無限ループの挙動のように、 StreamTransformerで新たなSingle Subscription Streamが生成され、その際に入力 StreamがStreamTransformer内部でlistenされているため、上記の無限ループの問題のようにAsync Pipe内部でdispose -> listenすると、新たなStreamTransformerが以前のStreamTransformerによりlisten済みの状態のstreamをlistenしてしまい、このエラーが起こる。

どうすれば良いか

上記無限ループ問題と同様に、StreamTransformerにより変換されたStreamをcomponentのインスタンス変数に保持しておくことで防ぐ。

Component, Templateのコードをどう書くべきか

Async Pipeを使う場合と使わない場合の両方を解説する。

Async Pipeを使う場合

Async Pipeはその内部で、ComponentのonDestroy時にlistenしているstreamのstreamSubscription.cancel()が実行されるので、Componentでstreamをcancelするコードを書く必要がない。

ただ、なんとAngularDartには、*ngIfasync as構文がない。 *ngIf="aStream | async as anObject"と書けない。Objectのfieldが複数ある場合は、残念なことに、以下のような何回もlistenする冗長なコードを書かなければならない。

<div>{{(aStream | async).field1}}</div>
<div>{{(aStream | async).field2}}</div>
<div>{{(aStream | async).field3}}</div>

*ngForではAsync PipeでStreamで流れてきたListのそれぞれの要素を同期的に扱うことができるのだが、*ngForを使わない場合に複数のfieldがあるobjectをハンドリングする際は、Async Pipeを使わないほうが良いかもしれない。

もし良いやり方があれば知りたい。

Async Pipeを使わない場合

Async Pipeを使わない場合は、ComponentのDart側のコードでの対応が必要。コンストラクタやngOnInitで、BLoCのoutput streamをlistenし、そのsubscriptionをListにまとめておく。

var anObject;
final List<StreamSubscription> _subscriptions = [];

ngOnInit() {
  var aSubscription = bloc.aStream.listen((e) {
    anObject = e;
  });
  _subscriptions.add(aSubscription);
}

void ngOnDestroy() {
  print('cancel all subscriptions.');
  for (final s in subscriptions) s.cancel();
}

モリーリークを防ぐために、componentのOnDestroyですべてのStreamSubscriptioncancel()を行う。


BroadcastStreamControllerはその内部で複数のlistenerを保持できる。Listenする側がstreamSubscription.cancel()を忘れると、BroadcastStreamControllerに不要なListenerがいつまでも残ったままになる。つまり、メモリーリークする。長い時間実行される類のアプリケーションならば気にしたほうが良い。 対照的に、SingleSubscriptionStreamControllerにはメモリーリークの心配はない。


StreamはChangeDetectionの対象なので、Streamの新しい値が来てanObjectが書き換わるたびにViewが更新される。

<div>{{(anObject.field1}}</div>
<div>{{(anObject.field2}}</div>
<div>{{(anObject.field3}}</div>

このngOnDestory()でのstreamSubscription.cancel()は頻出するパターンなので、以下のようなMixinを用意すると楽ができる。

mixin CancelSubscriptionsOnDestroy implements OnDestroy {
  @protected
  final List<StreamSubscription> subscriptions = [];
  
  void ngOnDestroy() {
    // cancel all subscriptions.
    for (final s in subscriptions) s.cancel();
  }  
}

使用方法は以下。

class AComponent with CancelSubscriptionsOnDestroy implements OnInit, OnDestroy {
  AComponent(this.bloc);
  
  final ABloc bloc;
  var anObject;
  
  ngOnInit() {
    subscriptions.add(bloc.aStream.listen((e) {
      anObject = e;
    }));
  }
  
  // Override and call super only if the rest process after canceling all stream subscriptions is necessary.
  @override
  void ngOnDestroy() {
    super.ngOnDestroy();
  }
}

Async Pipeのテンプレート構文サポートが不十分なため、このようなボイラープレートコードを書くことが多くなる。また、Async Pipeを使用しても、前述のStreamTransformer使用時の注意点のように、インスタンス変数にStreamを保持するコードが必要になる。


ここまでで説明してきた配慮と使い分けが面倒に感じるならば、いっそAsync Pipeをまったく使わないという判断もありだと思う。

ComponentState Mixin

FlutterのStreamBuilderでは、私が理解している限りは、この解説でAsync Pipeについて見てきたような問題は起こらない。AngularのChangeDetectionほどの黒魔術感もなく、より効率的なView更新メカニズムだと思う。

実は、AngularDartにはComponentStateというMixinが提案されており、これはFlutterのようにsetState()を明示的に呼ぶことでViewを更新する。 ComponentStateはまだExperimental扱いなので実戦投入するべきではないが、これの発展次第ではFlutterのようなより効率的で罠の少ないView更新メカニズムが手に入るのかもしれない。

Stream Pipeを作る

いちどlistenしたstreamが入れ替わることを考慮するユースケースはおそらくないはずなので、いちどlistenしたらcomponentがdestroyされるまでstreamを入れ替えないというpipeがあればいいかもしれない。また、Async PipeはStreamとFutureの両方に対応して内部で処理が分岐しているので、Streamだけに対応したものがあればすこしだけ性能があがる。StreamPipeという名前にして、aStream | listenという構文にする。そうすれば、StreamTransformerで変換したStreamをインスタンス変数に保持するボイラープレートコードが必要なくなる。

FlutterのStreamBuilder WidgetのようなComponentまたはStructual Directiveを作る

Angular TypeScript版のようなasync as構文が無いので、pipeする回数が増えるのは退屈。

FlutterのStreamBuilder WidgetのようなComponentまたはStructural Directiveを作り、Template変数と共に使用すれば、TemplateだけでStreamを扱うことができ、ComponentのDart側のボイラープレートコードをまったく書かなくてすむようになるかもしれない。


両者とも開発コストは高くなさそうだが、今から非公式にそういうものを作るのにかなり躊躇がある。おとなしくいまのままで、ComponentState Mixinの発展やFlutter Webの成功を祈るほうが無難かもしれない。

まとめ

以上のように、Async Pipe使用時の落とし穴と、Streamを取り扱うComponentのパターンについて見てきた。

問題の根本には、Angular TemplateやWeb ComponentのHTMLといったマークアップ用外部DSLプログラミング言語のコードを連携させていく辛みがある。 Angular TypeScript版はTemplate構文が複雑化する宿命。一方、FlutterはViewがXML, HTMLのような外部DSLでなくDartコードによる内部DSLなので問題は起こらないが、Angularとは対照的に、HTML, CSSに慣れたWebデザイナーに再学習を強いる。また、もともとUIを表現することを想定されているわけではないC系の構文でUIを表現するので、Dartのシンプルな構文といえどもXMLなどよりも可読性が低くなりがち。そこで、Dart 2になってnewの記述が任意になったり、2.3でその可読性問題の痛みを緩和するための機能追加が行われた。

Angularの現在のChangeDetectionも十分にこなれており性能も良いので、このままAngularDartを使用しつつ、ComponentState、もしくはFlutterのWeb版の成功を気長に待ちたい。

Streamについての基礎知識を解説するのは負担が重いので避けたが、そういった基礎が不安な人は、以下の記事を読み、dart:asyncソースコードを読むなどしたら理解が深まると思う。

https://www.dartlang.org/articles/libraries/broadcast-streams

RxDartの実装はdart:asyncのStream関連ライブラリのラッパーとなっているので、その内部構造の理解を通じてStreamへの理解を深めていくのも面白いのではないか。

あと、UnmodifiableListView*ngForに渡すとフリーズした件、単なる私の勘違いだったか、Angularのアップデートで直ったのかはわからないが、5.2.0では問題なかった。

AngularDartの、Angular Componentsの使い方

フルスクラッチでUIを構築していくのは現実的ではない。現代のUIに対する要求水準の高さに応えるために決定的に重要な要素として、UIライブラリの質がある。

Angular TypeScript版にAngular Materialライブラリが提供されているように、AngularDartにはpackage:angular_componentsが提供されている。このパッケージは、マテリアルデザインのUIを構築するためのさまざまなUI部品がAngularのComponentとしてまとめられ、またUIテーマやスタイリングのためのSCSSが同梱されている。AngularDartでUIを構築するならば、素直にマテリアルデザインを採用するべきだ。

Angular TypeScript版のAngular Materialのサイトと比較して、AngularDartのpackage:angular_componentsのサイトは地味なので、誤解されることもあるかもしれない。そこで、ここではpackage:angular_componentsの使い方を解説していく。

特徴

使用方法

セットアップ

セットアップ方法については、他のdart packageと同じくREADMEを参照する。 https://pub.dartlang.org/packages/angular_components

SCSSのビルド用にpackage:sass_builderが必要。

表示

たとえば、MaterialButtonの場合は、 import 'package:angular_components/material_button/material_button.dart';とインポートして、 @Componentアノテーションdirectives名前付き引数にリストとしてMaterialButtonComponentを指定する。

import 'package:angular/angular.dart';
import 'package:angular_components/material_button/material_button.dart';

@Component(
  selector: 'buttons',
  templateUrl: 'buttons.html',
  directives: [MaterialButtonComponent],
)
class ButtonsExampleComponent {}

そして、Templateにカスタムタグを書けば表示される。

<material-button>Default</material-button>

ほとんどのタグにはmaterial- prefixがつけられている。 いくつかのComponentでは、directivesへの登録に加えてprovidersへの登録が必要。

使い方が分からなければ、各componentの使用例を参照する。 例えば、MaterialButtonの場合は、 https://dart-lang.github.io/angular_components/#/material_buttonと、そこからリンクされているソースコード https://github.com/dart-lang/angular_components/blob/master/examples/material_button_example/lib/material_button_example.dartを読んで理解する。

レイアウト

アプリのレイアウトをスクラッチで書くのは地味に面倒でバグが発生しやすい。そのため、https://dart-lang.github.io/angular_components/#/app_layoutマテリアルデザインのレイアウトを構築する方法について解説されている。Flutterのようなscaffold widgetだったり<app-layout></app-layout>というコンポーネントがあるわけではない。複数のComponentやscss stylingを組み合わせる形。

ささいなUI部品はComponentでなくSCSSだけで表現

FlutterではすべてがWidgetであったりやTS版AngularのAngularMaterialがささいなUI部品でもComponentで表現しているのとは異なり、package:angular_componentsでは、ComponentにせずSCSSで表現するものもある。たとえば、MaterialCardやDividerといったもの。


MaterialCardについては、以下のようにmdc-cardのscssを指定して表現している。

https://dart-lang.github.io/angular_components/#/material_card https://github.com/dart-lang/angular_components/blob/master/examples/material_card_example/lib/material_card_example.html


Dividerについては、ギャラリーにセクションは設けられていないが、そのようなささいなものはpackage:angular_componentsが提供するColor関係の変数を用いて、

@import 'package:angular_components/css/material/material';

.divider {
  display: flex;
  width: 100%;
  border-bottom: 1px solid $mat-border-light;
}

このように定義するだけでよい。

<div class="divider">

残念ながらFlutterやAngularMaterialほどのコードの統一感はないが、おそらく実行性能を重視した結果だと思う。

スタイリング

マテリアルデザインのためのスタイリング用のSCSS変数やMixinが用意されている。基本的に、以下をimportして変数を使用する。

@import 'package:angular_components/css/material/material';

*注) sass-builderdart-sassに依存している。dart-sassは、Dartのpackage urlをサポートしているため、上記のpackage:angular_components/の指定は有効。

これにより、

// How to use me:
//
// This set of scss files should never output any css unless sass classes are
// extended in the importing file. This allows you to import _material without
// bloating the css files for your components. All variables and components
// within the material stylesheet are prefixed with "mat-" in order to give a
// clear distinction between them and the styles in components that are using
// these material styles.
//
// Example:
//
// small-example.scss
//     @import "package:angular_components/css/material/material";
//
//     .my-app {
//       background-color: $mat-blue-700;
//     }

@import 'color_palette_material';
@import 'color_material';
@import 'core_material';
@import 'elevation_material';
@import 'scrollbar_material';
@import 'transition_material';
@import 'typography_material';

が有効となる。これらでマテリアルデザインガイドラインで定義されたスタイリングは一通り定義されているはずなので、ここから必要となる変数を利用していく。以下、例で説明する。

テーマカラー

例えば、アプリのテーマカラーを指定するには、

$primary-color: map-get($mat-teal-map, $teal-number);
$secondary-color: $mat-light-green-400;

といった具合に書き、各コンポーネントのSCSSで提供されているmixinなどで指定していく。

Elevation

すべてSCSSで定義されており、使用方法もelevation_materialソースコードのコメントに書かれている。

/// Applies the Material Shadow styles to the selected element.
///
/// Use the attributes below to control the shadow.
///
/// - `animated` -- Whether to animate the shadow transition.
/// - `elevation` -- Z-elevation of shadow. Valid Values: 1,2,3,4,5
///
/// Example:
///
///     .shadow-box {
///       @include material-shadow();
///     }
///
///     <div class="shadow-box" animated elevation="3">...</div>

その他の要素

フォントサイズ、リンクスタイル、マージンなど、一通りのスタイルが定義されているので、マテリアルデザインガイドラインに従うためには、marginをアドホックに指定したりアプリ独自に変数を定義するのではなく、上記で説明したように@import "package:angular_components/css/material/material";を用いて定義された変数を探して適用していくことが基本となる。

各UIコンポーネントへのテーマの適用などのカスタマイズ

MaterialButtonなどの各UIコンポーネントには、テーマの適用などのカスタマイズ方法として、SCSSのmixinなどを用意されている。たとえば、MaterialButtonならば、material_button/_mixins.scssなど。

例:

$primary-color: map-get($mat-teal-map, $teal-number);
$secondary-color: $mat-light-green-400;

@mixin button-mixin {
  $selector: 'material-button';
  @include button-color($selector, $primary-color);
}

@mixin input-mixin {
  @include material-input-theme($primary-color);
}

UI Componentごとに説明されている。

サードパーティマテリアルデザインPackage

FlutterのWidgetの充実度と比べると、package:angular_componentsでサポートしているUI部品の種類は相対的に少ない。不足している部品はスクラッチで作ってしまうのも良いが、サードパーティのものを利用することも検討する。例えば、package:skawa_material_components

https://pub.dartlang.org/packages/skawa_material_components

以下の部品が提供されている。

  • <skawa-data-table>
  • <skawa-grid>
  • <skawa-snackbar>

まとめ

Dartソースコードの可読性がとても高いしDoc Commentも充実しているので、PackageのソースコードとDoc Commentを読む習慣があれば上記の情報はすぐに把握できる。しかし、AngularDartに興味をもった段階でその関連ソースコードを読む、というのもハードルが高いので、AngularDartへの理解の促進と誤解を解くための一助として、この解説を書いた。

不明点や疑問点は、以下のチャンネルなどで質問を投げてくれたら解答したい。

公式ガイド

2018年のDart言語の振り返り

2019年初に2018年のDartを振り返ってみる。僭越ながらDart advent calendar 25日目の記事だが、大幅に超過して新年になってしまった。申し訳ない。そして大した内容ではない。個人の回顧です。

Dart 2のリリースと、Dart 1の回顧

Dart 1を回顧するに、以下の特徴をもった言語だったと思う。

  • VMを活かした動的言語であること
  • VM前提の動的言語の表現力、進化を妨げないための、選択換装可能な型システム (言語が型システムに依存するのではなく、型システムが言語に依存する)
  • エディタ、デバッガ(等々)が統合された開発体験などの、Smalltalkを目標とした環境の提供(が目標)
  • ライブラリや開発環境などを整備し、言語のカタログスペックでなく実際のDeveloper Experienceが優れていること
  • 世の中の圧倒的多数を占めるCとJavaプログラマーに学習コストとストレスなく使ってもらうため、C系(Java系)の構文であること
  • 他言語で良いとされている機能を単にカタログスペックを埋めるように採用するようなことはせず、言語仕様の無駄な肥大化を抑えること
  • JavaScriptに無理なくコンパイルできること

Dart 1時代のコアメンバーの経歴や発言、Snapshotの追加や独自統合開発環境(DartEditorやCDE)の提供の試みなどを観察するに、Dart 1によって、Javaの皮を被ったSmalltalkをWebにもたらそうという意思が強く伝わってきていた。そのような野心的な構想に、どうなるのかと興味を惹かれ続けてきたが、その心づもりの言語は結局さまざまな環境要因により、当時のWebコミュニティでは人気を得ることは叶わなかった。これは本当に残念なことだった。

Dartが、Webに加えて他のプラットフォームへの適用の可能性を模索してきた中で結実したもののひとつがFlutterだが、その過程において、iOSなどのプラットフォームの制約事項によりDart言語がVMを前提にできなくなり、AoTコンパイル後のパフォーマンスとスペースを少しでも上げるために、型システムへの方針を変更してAoTコンパイル時に積極的に型情報を利用するものに刷新したものがDart 2であるという理解をしている。 それに伴い、型システムでなくランタイムからのフィードバックに依存する動的言語としての側面はなりをひそめ、AoTコンパイルコンパイルメタプログラミングの技法が重視されるようになった。dart:mirrorは実質的にVM環境でないと使用できないというややもどかしい状況になっている。ただし、Flutterでも開発時や、CLIやサーバーサイドはVMで動作する。

2018年にDart 2がリリースされるまで、宣伝を控えて年単位で大改装が続けられていたが、Dart 2を決意して無事リリースされた現在は、マイナーな機能改善に着手できる段階に達したのだろう。リンクは張らないが、言語チームはいろいろな改善を検討していることがGitHubから観察できる、と書いていたら、うまく日本語でまとめられていたので、リンクを張っておく。

Dartで鋭意検討中の新機能

しばらくは、Flutter中心のフィードバックを経て、ユースケースベースで言語機能の改善が続けられていくのだろう。

Dart 1では、主にScalaやKotlinあたりが優れた言語であると信じるプログラマーから、それらの(というか、Java)言語に表面上似ていても独自のビジョンをもって優先順位を決めていたこの言語に対して、批判がたびたび聞こえてきていたし、現在も、Flutterの人気爆発によりDartが注目されたため、それが再び散見されるようになった。

Dart言語は、評価が定まっていない仕様を軽率に採用してあとから取り除くこともできず、結果として言語仕様やコアなエコシステムが乱雑になる事態を警戒してきたことが観察できる。たとえ、Kotlin、Swift、ScalaJavaScriptなどの他の言語で良いとされて導入されているアイデアであっても、言語仕様や標準ライブラリへの追加には、極めて慎重で注意深い態度を示してきた。

たとえば、言語マニアからまれに批判される、パターンマッチングが無いことなどについても、長い議論の末に、極めて意図的に、あえて非採用、またはペンディング扱いになっていることがGitHub Issueやカンファレンス動画などからうかがえる。

そして、Dart 1にはそのようなマイナーな改善よりもはるかに優先順位が高い事項があったが、悲しいことに、上記のようにその独自の哲学とビジョンはあまり理解されることはなく、推進力を得ることができなかった。

Dart 2時点では、Dart 1で主張していた動的言語としての側面はなりをひそめてしまったので、Java, Scala, Swift, Kotlinといった言語とのスペック比較がやや正当なものになっていくのではないか。

Flutter

2015年(だったか?)のSkyの発表からそれがFlutterになり、観察、学習しつつもまだアルファ版だからなあと静観していたら、昨年あれよあれよと人気が盛り上がっていき、12月に1.0に到達した。

https://ntaoo.hatenablog.com/entry/2018/12/05/091924

Flutter専門のコミュニティが立ち上げられたり、@_mono さんによる、Flutterの解説を専門とした素晴らしいブログなど、日本でもなかなかの盛り上がりを見せている。

FlutterでDartの魅力を知った方が 初心者にこそオススメしたい言語Dart といった記事を書かれたり、先入観なくDart言語とその開発環境の魅力が評価されていく現象が広がっていってたいへん嬉しく思う。

アプリをFlutter Dartで書けても、ネイティブAPIを叩くプラグインはKotlinかJavaで書くしかないので(AndroidでネイティブAPIを叩くときにDartが利用できるという選択肢は当面ないんじゃないか)両者はうまく棲み分けすることで安定しそうだ。(FlutterでUIを書くFuchsiaも控えていることだし。FuchsiaとAndroidの関係はどうなるのだろう?)

Dartは2.0で良くも悪くも手堅い言語になったと思う。これからは、拡張関数などのマイナーな改善が続けられていく段階に到達したようなので、Kotlinファンの不満もなくなっていくだろう。

Dart for Web

JavaScriptの話題が多彩なのでどうしても見逃されがちだが、Web向けの機能もDart 2に伴って地道に改善が続けられているので、JavaScriptに疲れた人には強くお勧めする。フレームワークについては、当面はAngularDartが第一の選択肢になる。

https://ntaoo.hatenablog.com/entry/2018/12/15/173713

2018年12月にWeb版Flutterとして、Hummingbirdの構想が発表された。たとえFlutter Webが未来だとしても、AngularDartは現在もうある非常に信頼できる技術なので、Flutter Webが安定するまでは第一の選択肢としてありがたく利用させていただくことになる。AngularDartとFlutterでModelのコードを共有しておき、Hummingbirdが安定したらゆっくりと置き換えていけばいいんじゃないかと思う。その期間は、数年はあるのではないだろうか。

サーバーサイドDart

サーバーサイドでも、すでにJavaで巨大なサービスを運用して苦闘している膨大な数のJavaエンジニアにとって、学習コストを抑えて両者を併用して書きつつ、場合によってはスムーズに移行できるという特徴は大きな価値を持つだろう。もっとも、まだライブラリ不足なのは否めないが...

https://aqueduct.io/Jaguarなど、コミュニティベースのフレームワークも年齢を重ねてきている。

Dartが、Webも含めたクライアントサイドのシングルコードベースでの大統一開発環境になろうという構想が現実味を帯びてきたので、この調子で今度はGCPの公式サポートが充実してほしいし、FirebaseやgRPCのDart公式サポートなどその兆しは十分ある。

2018年は、Dartはクライアントサイドにフォーカスすると公式に表明されたが、2019年にはサーバーサイドを含めたユニバーサルな言語となる構想が語られてもおかしくないところまで来ていると思う。

個人としては、Firebase、そしてクライアントもサーバーも普通にDartで書いていこうと思う。ライブラリやプラットフォームのサポートが不足している場合はPythonやGoのお世話になると思うが。GAEとか。

ということで

春にDartのミートアップを予定していて、同志や入門者などDartに興味がある方々と情報交換したいなと思う。 https://dartisans-jp.connpass.com/event/109371/

さまざまな言語を触ってきたが、Dartは、言語仕様が思慮深く選定されつづけ、公式ドキュメントがしっかりしているので迷わず(英語だけど)、ScalaJavaScriptのような先行言語に見られるような、歴史的な経緯による少なくない量のBad Partsによる混沌もなく、JavaScriptのように環境構築やコアとなるライブラリ選定などにもつまづくことがない。Dart 2になっても、Dart 1時代からの、扱いやすくパフォーマンスが高く信頼できる実用言語という特徴は維持している。したがって、本質的なプログラミングに集中できる。

実務的には、自分が技術選定できる場合は、Webでもネイティブでも環境を問わず、当分はDartが第一の選択肢になるかなという感じがする。

2019年は、エンジニアリング方面では普通にDartでアプリやパッケージを書いたりVMコンパイラへの理解を深めていくことに集中するとともに、趣味ではSmalltalkを始めいろいろな環境を触ってさまざまな発想を得たい。このブログも実務的な内容を増やしていこうと思う。

AngularDartの魅力

Dartアドベントカレンダー14日めの記事

Flutter経由でDartの魅力に気づいた人が増えてきているので、ここでAngularDartにもすこしでも興味を持ってもらって、使い始める人が増えると嬉しいと思い、この記事を書いた。

網羅的な解説をする余裕がないので、覚え書きの質で書きちらす。また、具体的な機能の特徴をコードを交えて解説するものではない。それに関しては、https://webdev.dartlang.org/を参照されたい。

なお、よく知られたTypeScript版のAngularとの比較が多くなるため、ここでは便宜的にそれをAngularTSと呼称して、AngularDartと区別する。

Hummingbird (Flutter Web)が発表されたけど

Hummingbird (Flutter Web)が発表されたが、まだオープンソースにもなっておらず評価するには早すぎるので、これからはAngularDartでなくHummingbirdだと決めつけてしまうのは年単位で気が早い。

短期的には、Webで要求されるユースケースのすべてを代替できるかについては懐疑的。将来もしHummingbirdが成功するならば、時期を見定めて数ヶ月から数年かけてゆっくりと置き換えていけばいいのではないか。

それまでは、BLoCパターンなどでModelをDartで共有して、WebをAngularDartで、ネイティブをFlutterで書くというパターンが有効。プラットフォームに依存したコード以外の、Model、ライブラリと開発基盤は再利用できるのが強み。

AngularDartの略歴

一昔前にAngularTSからフォークした。それまではAngularTSをDartコンパイルして提供されていたが、よりDartらしくコードを改善し、パフォーマンスと生産性を高めていく目的。Google AdsのWebクライアントで採用されているのが定番の宣伝文句。その他、Youtubeの一部など、Google内部では活発に使われているらしい。

ちなみにさらに歴史を遡ると、AngularJS (Angular 1)のメジャーバージョンアップの構想ためのアイデアの実装としての側面がAngularDart(初代)にはあり、それがAngularJSにバックポートされつつAngular 2の開発が進められ、そのAngular 2ではTS版もDart版もひとつのコードベースで書かれていたが、なんやかんやあって結局AngularはTypeScript版が推されていき、Dart版はフォークして独自のフレームワークになった。

そして、数ヶ月前のDartのversion2.0のリリースによって、Dartの基盤を利用したAngularDartもversion5.0で成熟したとみなせるようになったので、こうして安心して紹介できる。

AngularTSが、Webのドラフト段階のAPIや将来の構想を先取りした巨大なポリフィルとして冒険するエキサイティングな側面があるのに対し、AngularDartは、巨大なミッションクリティカルなアプリで採用されていることもあり、フレームワークの安定性と生産性とパフォーマンスを非常に重視していると感じられる。

AngularTSがWebコミュニティを積極的に巻き込んでGitHub主体でオープンソースで運営している一方、AngularDartはGoogleの内部リポジトリで揉んで決定されたコードがGitHubに同期されてパブリックになる。ただしGitHub Issueなどで意見を募ったりバグ報告やプルリクエストは受け付けてそれがGoogle内部リポジトリに逆に取り込まれたりといったことがまれにある。

エコシステムやコミュニティの広さはAngularTSのほうがはるかに上である。

なぜAngularDartか

Dartで書ける。つまり、Dartのエコシステムに乗ることで、TypeScript (JavaScript) の欠陥や機能不足、乱雑で混沌としたエコシステムやビルド環境に悩まされずにすむ。JavaScriptをハックする必要がない。

  • ビルドエンジニアを用意したり、Webpackと格闘したりする必要がない。基本的に、package:webdevに用意されているコマンドを叩けばよしなにやってくれる。
  • クロスブラウザ対応のためにJSのAPIのポリフィルを導入したりする必要がない。Dartの豊富なライブラリを利用して開発する。デプロイ時はES5にコンパイルしてくれる。
  • JavaScriptのライブラリを利用したくなる場合は、TypeScriptと同様にJSと通信するコードを用意する。package:jsdart:jsで、d.tsのようなものを書く感覚。ただし、Dartのエコシステムがあるので、そのようなコードを書いてJSのライブラリを利用したくなることはあまりない。

また、Flutterでモバイルアプリを書くならば、Web版はAngularDartで開発してModelを共有しておけば、Flutter WebやDesktopが成功して安定した際はスムーズに移行できるだろう。

主にAngularTSと比較してのAngularDartの特徴

網羅的な比較をする余裕はないので、覚え書き。

AngularDartはAngularTSの知識の大部分を流用できるので、TS版を知っているならば学習コストが少ないし、逆にDart版を知ればTS版も苦労せずに応用できる。ZoneChangeDetection、大部分のテンプレート構文、Dependency Injectionといった、中核となる概念を共有している。

AngularTSは、フルスタックフレームワークとして、JavaScriptの混沌としたエコシステムからの技術選定をせずにすみ、統一した開発環境を提供していることが大きな強みだ。AngularDartでは、Dart言語の基盤がそのような役目を担っており、AngularDart自体はその基盤の上でのWebのUI層を司る位置づけになっている。普段の開発でJavaScriptを意識することはほとんどない。

TypeScriptが、良くも悪くもJavaScriptに型システムを追加しただけの方言にすぎず、JavaScriptの作法やエコシステムへの依存度が大きいことに比べ、Dartは完全に別の言語であり、シンプルで堅牢な言語仕様、ビルド基盤、充実した標準ライブラリ、独自のエコシステムを構築している。Flutterやサーバーサイドなど、Web以外の適用範囲が非常に広い。ただし、そのエコシステム自体はまだ発展途上なのが現実。

テンプレート構文

開発チームはAngularTSからの知識の再利用ができる点については気を払っており、たとえば*ngForもTS版と同じノリで*ngFor="let e of list"と書いたりする。Dartなのに。フォーク前から存在する構文に破壊的変更が入るのをなるべく控える方針のようだ。

ただ、TS版と異なりDart版はテンプレート構文の種類が絞り込まれている。TS版での構文の増加に追随しなかったとも言える。これは良し悪しがあって、テンプレート構文が便利になればなるほど、テンプレートに本来はModelに書くべきロジックが入り込む誘惑が強くなるし、テンプレートのコンパイラの複雑さやIDEサポートの困難が増したり、学習コストが増えたりコードレビューが面倒になったりする。一方、テンプレート構文が便利になるとたしかに簡単さを感じるので、Webコンポーネント的なコンポーネントでhtml(テンプレート)とスクリプト部分が分離している設計では、ある意味仕方のない進化なのかとも思う。

複雑なテンプレート構文と付き合うプログラマーのためにIDEサポートが充実しているのもTS版の特徴で、普通に補完がきいたりリファクタリングサポートがちゃんと動くのもよい。WebStormかVSCodeを使っているとその恩恵を強く感じる。Dart版もテンプレートを解析してサポートしてくれるが、TS版ほど充実しているわけではなく、ここははっきりとTS版に劣るところだ。しかし、そもそもテンプレートの解析が必要なほどのロジックを持ち込むなという良い制約になっているという解釈もできなくはない。 ここは対象ユーザー層の量と質の違いがはっきりと現れているのではないか。

私的には、テンプレートには最低限現状の種類のものがあればよく、増えてもメンテナンスが面倒になるだけだと思う。TS版で同僚がさまざまなテンプレート構文を駆使してテンプレートの複雑度が増していくのをあまり抑止できなかったのが残念ポイントだった。Pipeも一見簡単便利なのだが、本来はModelに書くべきものがViewに露出してしまっていることがほとんどなので、使用を禁止したいくらいだ。

AngularDartは、機能を増やすよりも、パフォーマンスと安定性、生成するJSのコードサイズの削減を重視して、あまり使われていない機能を整理、削除することが多い。GitHubのAngularDartリポジトリCHANGELOGをみたらよく分かる。

AngularCLIは?

AngularTSのCLIは、JavaScriptの面倒な部分をだいぶ見ないですむようにしてくれて、生産性の向上に大きく貢献している。AngularDartではその役割はpackage:webdevが担っており、AngularDartに限らないWeb開発のサポート機能が提供されている。package:webdevの各コマンドを叩けばそれでよく、それ以上のことをする必要がもしあるならば、Dartのビルドシステムを学習してプラグインを書く必要がある。

dart:html

AngularTSのようなマルチプラットフォーム対応のための抽象化層は、フォークのあとに廃止された。現在はdart:htmlをそのまま利用する。パフォーマンスの向上とコンパイル後コードサイズの削減のため。

dart:htmlはDOM APIの一部のおかしな挙動(Node, Elementとか)を修正しているので快適。また、XSSを防ぐためのセキュリティ機能を提供しているので安全である。AngularTSにも似た機能はあったはずだが、AngularDartではフレームワークではなくその基盤のdart:htmlAPIを使う。

SSR

抽象化層を廃した代償として、SSR機能がない。そのため、SSRが必須だと判断されたプロジェクトでは選択肢から外れる。 ただし、2019年の環境で、SSRがそのプロジェクトで本当に必要なのかはよく考えたほうがよい。SSRを維持するためのさまざまなコストを支払えるか、PWA+CDNで問題ないのではなど。

https://speakerdeck.com/kazuyaseki/state-of-seo-for-spa-2018

個人的には、SSRは過渡期の技術とみなしていて、もはや積極的に採用することはない。ほぼアンチパターンだとみなしている。

PWA

AngularDart製のアプリが、Lighthouseのスコアでほぼすべて満点を取れている。 PWAは、Angularフレームワークに同梱はされず、Dartらしくdart:htmlや独自パッケージでサポートしている。

StreamとRx

RxJSの知識を再利用したい、またはBehaviorSubjectを利用したいならば、RxDartを採用する。RxDartは標準ライブラリのStreamを継承して書かれている。

私的な好みでは、Rxの大量のオペレーター群を見るのにうんざりしているので、BehaviorSubject以外は、Dartの標準ライブラリのStreampackage:async, package:stream_transformを組み合わせて使っている、チーム開発ではRxDartを使うのが無難かもしれない。

AngularDartはAngularTSとは異なり、RxStreamも必須ではない。

HttpClient

AngularTSではHttpClientフレームワークの一部扱いだが、AngularDartではpackage:httpを使う。Flutterでも利用できるパッケージ。

Module (NgModule)

AngularTSでは、NgModuleはアーキテクチャ上重大な役目を果たすフルスタックフレームワークの要ともいえるものだが、AngularDartでは、単にProviderを合成してまとめておく薄い機能にすぎず、使わなくとも問題ない。特に困っておらず、逆にTS版ではNgModuleに悩まされることが多かった(個人の経験談)。

Test

package:testpackage:mockitoを使う。成熟している。Flutterでもサーバーサイドでも、どのような環境でも利用できる。

AngularTSでは、型システムを生かしていない設計のJavaScriptテストフレームワークjasmineを使うか、マイナーだがmockitoのTS版を使うかという選択肢がある上に、Angularの外に目を向けるとさまざまなテストフレームワークがそれぞれそれなりに使われている混沌とした状況だが、Dartではパッケージの選定に迷うことはなく、さらにWebでもネイティブアプリでも普通に同じパッケージを利用できる。

スタイルガイド

AngularTSでは、独自のスタイルガイドが細かく規定されてLintが提供され、AngularCLIもそれを前提に作られている。 AngularDartでは、Dartのpackage conventionとスタイルガイドに従う。AngularTSのスタイルガイドはAngularDartでは通用しない。

AngularTSのスタイルガイドは、フルスタックフレームワークとして、コミュニティ内でスタイルに迷いがでないようになる利点は確かにあるが、そのスタイル自体はあまり良いものではないと個人的には思う。

package:angular_components

package:angular_componentsでかなり楽にMaterial Design対応できる。

ほぼ一通りのUIコンポーネントは揃っているが、Flutterほど充実して最新の仕様に追随しているわけではない。

付属のSCSSにMaterial Designのスタイルが一通り定義されているので、その変数を利用してスタイリングしていく。独自にmarginやcolorなどを定義していく必要はない。UIコンポーネントのcolor themeの変更についても、UIコンポーネントごとにSCSSのmixinが提供されているので、それを利用する。

たとえば、'package:angular_components/css/material/material'や、 'package:angular_components/app_layout/layout'をimportする。

個別のUIコンポーネントのスタイルのカスタマイズ用SCSSは、 たとえば'package:angular_components/material_button/mixins';にある。

ちょっと困ったところ

まれに落とし穴に落ちてしまう。

  • UnmodifiableListViewを*ngForに渡すとフリーズする。
  • async pipeにstream transformerをかましたstreamを渡すと、無限ループする。streamを適当にキャッシュして、ChangeDetectionのたびにいちいちstream transformerをかまさないように注意する必要がある。もしくはasync pipeを使わない。

需要があれば詳細と回避方法を書く。

入門のための学習リソース

フィードバック

後半になるほど息切れした。抜け、漏れがたくさんあるかもしれない。

フィードバックをいただけると喜んで回答します。

Resultライブラリを使うべきか

あまり知られていないと思うが、実は、package:asyncにResultライブラリが存在する。

https://pub.dartlang.org/documentation/async/latest/async/Result-class.html

Result

Resultのソースコードに端的に表現されている。やっていることはこれだけ。

  /// Creates a `Result` with the result of calling [computation].
  ///
  /// This generates either a [ValueResult] with the value returned by
  /// calling `computation`, or an [ErrorResult] with an error thrown by
  /// the call.
  factory Result(T computation()) {
    try {
      return new ValueResult<T>(computation());
    } catch (e, s) {
      return new ErrorResult(e, s);
    }
  }

計算結果、または計算過程でのエラーをキャッチしてResultオブジェクトでくるむ。

final result = Result(() => someAction());

あとは、通常の制御構文を使用する。

if (result.isValue) {
  final computationResultValue = result.asValue.value;
  // proceed.
} else {
  result.asError.handle((error) {
    // error handling.
  }); 
}

ExceptionやErrorのハンドリングを網羅しているかどうかを静的に検査できるわけではない。 また、try catchにおけるアンチパターンと同様に、計算を雑にResultでくるんで回復不能なエラーまで握りつぶしてしまわないように注意。

非同期処理をキャッチする

Result.captureでFutureをcatchできる。

final result = await Result.capture(someFutureAction());

Result.captureAllでFutureのIterableを、Result.captureStreamStreamをcaptureできる。

標準のtry - catch - finallyとの使い分け

ほとんどの場合は、標準のtry - catch - finallyを使えばよいだろう。

  • Darttryは優秀で、async awaitを組み合わせれば同期処理と非同期処理が混在していてもcatchしてくれる。
  • プログラマーtryでの処理に慣れているため、学習コストがない。
  • Resultは標準ライブラリに含まれていない。

Resultを使えば、以下のメリットがあるだろう。

  • コードレビュー時に、tryよりもResultのほうがエラーハンドリングの不備が見逃されづらくなる。
  • エラーハンドリングのタイミングを遅延させるほうが便利な、特殊な状況がまれにある。

よって、少なくとも、パッケージを作成するときは、パッケージ利用者にResultの使用を強制しないようにデザインするとよい。つまり、ライブラリの公開インターフェースにResultを使用しない。

プライベートなアプリ開発においては、Resultを使用する基準は、開発チームが議論して決めたら良い。

背景に関してのメモ

Dart言語は、評価が定まっていない仕様を軽率に採用してあとから取り除くこともできず、結果として言語仕様やコアなエコシステムが乱雑になる事態を警戒してきたことが観察できる。たとえ、Kotlin、Swift、ScalaJavaScriptなどの他の言語で良いとされて導入されているアイデアであっても、言語仕様や標準ライブラリへの追加には、極めて慎重で注意深い態度を示してきた。

言語マニアからまれに批判される、パターンマッチングが無いことなどについても、長い議論の末に、極めて意図的に、あえて非採用、またはペンディング扱いになっていることがGitHub Issueやカンファレンス動画などからうかがえる。

そして、Dart 1にはそのような言語仕様や標準ライブラリのマイナーな改善よりもはるかに優先順位が高い事項がたくさんあったが、悲しいことに、その独自の哲学とビジョンはあまり理解されることはなく、推進力を得ることができなかった。

方針変更したDart 2では大きな仕事が一段落したようで、比較的マイナーな事項に関する進化を進めているので、気になる人はdartlang/languageリポジトリwatchしたらいいんじゃないかと思う。