Java
Kotlin
Dart

KotlinとJavaができる人向けDart速習

この記事について

この資料は株式会社Diverseの社内で使用しているDartを速習したい人向けの教材(2018/08/09作成)を公開したものです。

社内でのFlutter採用が本決まりになったため関係エンジニア向けに作成したものですが、多くの人に役立ててもらえる可能性があるため公開と相成りました。

Diverseは今後もコミュニティに貢献する活動を続けてゆきます。
気になった方は @kikuchy または弊社社員へお気軽にご連絡ください。

まえがき

Kotlinより断然Javaに近い言語なので、Javaベースで話した方が早い箇所が多そう。

以下の前提の人がFlutter開発で不便しない程度にDartを読み書きできるようになることを目標としています。

  • 解説するのはDart2
  • Java/KotlinをAndroidアプリ開発で一通り使ったことがある
  • JavaScriptやRubyもかじったことがある

関数/変数スコープ

Kotlinと同じくトップレベルに関数宣言、変数宣言することができます。
mainの宣言もKotlinと同じくトップレベル。

不定型

Javaの Object 、Kotlinの Any と同じく、「何型であってもよい」型は dynamic と表記しますできます。

Effective Dartによると、「本当に何型であっても良い」ことを示す際には Object を、「型Aと型Bのうちどちらか、のような、期待する型が決まってはいるがDartの型システムで表現できない型表現」を dynamic でするべき、らしいです。

/// [arg]を真偽値型に変換する。[arg]はStringかboolじゃないとダメ。
bool convertToBool(dynamic arg) {
  if (arg is bool) return arg;
  if (arg is String) return arg == 'true';
  throw ArgumentError('Cannot convert $arg to a bool.');
}

可視性

_ から始まるメンバ関数/変数が、Javaで private キーワードをつけたメソッド/フィールドに相当します。
クラスも _ から始める名前にできます。

パッケージプライベートはなく、ライブラリプライベートしか存在しないようです。
また、同じライブラリ内であれば _ で始まるフィールドもクラス外から触れてしまうので、無法者が現れた場合を考慮するならlinterなどで治安維持するしかないかも… 😢

型推論

変数の型とか関数の引数とかあちこち推論してくれるみたい。
Kotlinと同程度は推論してくれるとみて良さそう。

var hoge = "huga";
[1, 2, 3].forEach((x) => print(x*2));

これで hogeString 型、 xint 型と推論されます。
型を指定する場合は var の前とか final の後とかに型名を書きます。
(ただし var は省略が可能。省略すると、Javaの変数定義と同じ書き方になる)

変数のデフォルト値

null です。なんてこった。
全ての変数は null を代入できるので、nullチェックは自分で行わないといけません。なんてこった。

ちなみに assert() メソッドはproduction環境だとスルーされるようなので、「ある変数や引数にnullが入っていることを期待しない」系はassertで弾くと良いかと思います。

再代入不能変数と定数と不変オブジェクト

Javaの final と同じような機能が似たような感じで使えます。

final hoge = "fuga";

var の代わりに final を使うと再代入不能変数になります。

ただし、厳密にJavaの final (一度代入したら、その後は再代入不可)と同じわけではなく、初期化できるタイミングが限られています。
クラスフィールドに final を使った場合には、後述の初期化子リストで初期化しないといけません。
特殊なときじゃないとセッターにアクセスできないフィールドみたいなイメージ?

定数は const で宣言します。これも型推論してくれます。
こちらはKotlinの val と同じく、宣言時に値が決まっていないといけません。

const CONSTANT = "VALUE";

const はリスト系リテラルや値クラスにも使えます。
const 宣言された値はメンバを変更できません。(と言っても変更すると例外が飛ぶだけでコンパイル時エラーにはしてくれない。なんてこった)

// 変数は再代入可能だが、中のリストは変更不可
var hoge = const [1, 2, 3];

組み込み型の説明

数値型

整数型 int, BigInt と浮動小数点型 double しかないっぽいです。
intdoublenum の小クラスのようですが、numdart:math パッケージを使うときくらいしか意識することないと思います。
また、 int は64ビット、2の補数表現なので、それ以上の範囲の数値を扱う場合は BigIntを使うことになるかと思います。

リテラルの数値はコンパイル時に解決されるので、 const 初期化子は計算式(定数式)でも良いっぽいです。

const A = 3;
const B = 9;
const C = A * B;

文字列

String var str = "こんなふうに"
                        '改行で続けても'
                        "一つの文字列に結合されます。";
str = "JSよろしく" + "加算演算子でも結合が可能です";
str = "Kotlinと同様に $exprName で変数の内容を文字列中に展開できます";
str = """
ダブルクォートもしくはシングルクォートを3つ続けると
複数行文字列リテラルになります。
改行も反映されます。
""";

真偽値

Javaのプリミティブ boolean と同じような bool があります。以上。

リスト

Dartに配列はありません。リストがあるだけです。
APIはJavaのListとほぼ同じっぽいですが、Dartにはリストを作るリテラルがあります。

// これで List<int> になる
final list = [1, 3, 5, 7, 9];

print(list[1]);    // 3

マップ

APIはJavaのMapとほぼ同じっぽいですが、Dartにはマップを作るリテラルがあります。

var map = {
    "ほげ": 1,
    "ふが": 2,   // 最後のkey-valueペアの後のカンマはあってもなくても良い
};

print(map["ほげ"]);   // 1
print(map["もげ"]);   // null

字形(Rune)

文字列リテラル中の \uXXXX (XXXXは4桁の16進数)、もしくは \u{XXXXXX} ({} 中は桁数は問わない16進数)で任意のUTF-32コードポイントを表すことができます。
(とA Tour of the Dart Languageには書かれている)

var clapping = '\u{1f44f}';
print(clapping);   // 👏

シンボル

JavaにもKotlinにも無い概念。

プログラム中の演算だか同定だかに使用するもので、RubyのSymbolに近いもののようですが、なんか普段使うことがないので気にしなくて良いらしいです。
API作る人とかはもしかしたら使うことがあるのかも。

#hogehoge のように # で始めるとシンボルになるっぽいです。

関数

記法

名前付き関数の記法はJavaと一緒。

String greet(String name) {
    return  "Hello, $name";
}

/*
戻り値型を省略することで戻り値型を推論してくれるらしいですが、
可読性が悪いので省略しないことを勧める、ってEffective Dartに書いてあるそうです
greet(String name) {
    return  "Hello, $name";
}
*/

引数だとかで関数型を求められるような場所にはラムダ式を書けます。
変数にラムダ式で宣言した関数を放り込むことも可能。

var l = [1,2,3];

l.forEach((i) { print(i); });
// 中身が1行だけなら省略形も可能。
//  l.forEach((i) => print(i));

ちなみにファットアロー記法 () => exp() { return exp; } のシンタックスシュガーらしいです。ラムダ式以外にも、クラスのメソッドの宣言時にも使用できます。
ちゃんと値も返してくれます。

名前付き任意パラメータ(Optional named parameter)と順序付き任意パラメータ(Optional positional parameter)

Javaにはない概念です。
Kotlinのデフォルト値付き引数で良いじゃん、って気持ちになる。

Dartの関数は必須パラメータと任意パラメータの二種類をとることができます。
任意パラメータは、関数呼び出し時に値を渡されなくても大丈夫なパラメータです。
そして任意パラメータには、名前付き任意パラメータと順序付き任意パラメータの二種類があります。めんどくさい。

必須パラメータは任意パラメータより先に書かねばならず、任意パラメータは {} (名前付き任意パラメータ)または [] (順序付き任意パラメータ)でくくる必要があります。

名前付き任意パラメータは、関数呼び出し時に引数ラベルを書かねばならない(代わりに記述順序は任意になっている)引数です。値を渡さない場合はラベルを書かなくても構いません。
値が渡されなかった仮引数には null が入ります。

void enableFlags({bool bold, bool hidden}) {...}

// boldにはnull、hiddenにはtrueが渡される
enableFlags(hidden: true);

ちなみに、名前付き任意パラメータの仮引数に @required アノテーションを付けると、その仮引数はラベル付きの必須パラメータになります。
@required はmetaパッケージに含まれているため、使う際には package:meta/meta.dart をimportする必要があります。
ややこしい。

// childに値を渡さないとエラーになる
const Scrollbar({Key key, @required Widget child})

順序付き任意パラメータは、特定の位置以降の引数を省略可能にします。
こちらも値が渡されなかった仮引数には null が入ります。

String say(String from, String msg,
    [String device, String mood]) {
  var result = '$from says $msg';
  if (device != null) {
    result = '$result with a $device';
  }
  if (mood != null) {
    result = '$result (in a $mood mood)';
  }
  return result;
}

// 第三引数以降は省略可
assert(say('Bob', 'Howdy') == 'Bob says Howdy');

// 引数ラベルは書く必要がない
// 第四引数だけの省略も可能
assert(say('Bob', 'Howdy', 'smoke signal') ==
    'Bob says Howdy with a smoke signal');

デフォルト値

任意パラメータはデフォルト値を持つことができます。
基本Kotlinと同じですが、「デフォルト値をもたせた引数以後の引数にもデフォルト値設定しないといけない」的な縛りはないです。

String say(String from, String msg,
    [String device = 'carrier pigeon', String mood]) {
  var result = '$from says $msg';
  if (device != null) {
    result = '$result with a $device';
  }
  if (mood != null) {
    result = '$result (in a $mood mood)';
  }
  return result;
}

assert(say('Bob', 'Howdy') ==
    'Bob says Howdy with a carrier pigeon');

デフォルト値として与える値はコンパイル時定数であれば何でも良いです。なので、 const [...] とか const {...} とかも設定可能。

スコープ

JavaやKotlinと同じだと考えておけば良さそう。

演算子

Javaにはないものだけ解説。

// Kotlinと同様に ?. で「nullでなかったらメンバ呼び出し」が可能
List<dynamic> list = null;
list?.add(3);    // addは呼び出されないのでエラーにならない

// Javaの / 相当の除算は ~/
int div = 4 ~/ 2;      // 2
div = 4 / 2;           // / だけだとdoubleを返すので、これはコンパイルエラー

// instanceof 相当の型確認演算子は is
bool isHoge = h is Hoge;
bool isNotHoge = h is! Hoge;   // == !(h is Hoge)

// 実はKotlinのSmart Cast相当の事ができる
if (p is Person) {
    print(p.firstName);
}

// Kotlinの ?: 相当は ??
var b = a ?? "hoge";

// 変数がnullだったら代入する ??= 演算子がある
a ??= "hoge";

カスケード記法

A Tour of the Dart Languageではなぜか演算子の節にまとめられていたけれども、注記に「厳密にはDartの文法機能だよ」と書かれていた。
Conditional expressionsNote:のところ)

インスタンスの後に .. を続けることで、メンバにアクセスした上で、その後も .. を続けて書くことができます。
これを使えばKotlinのapplyスコープ関数相当の事ができます。

querySelector('#confirm') // Get an object.
  ..text = 'Confirm' // Use its members.
  ..classes.add('important')
  ..onClick.listen((e) => window.alert('Confirmed!'));
/*
上記は以下のコードに相当する
var button = querySelector('#confirm');
button.text = 'Confirm';
button.classes.add('important');
button.onClick.listen((e) => window.alert('Confirmed!'));
*/

// ()で結合順序を明示する必要があるものの、ネストも可能
final addressBook = (AddressBookBuilder()
      ..name = 'jenny'
      ..email = 'jenny@example.com'
      ..phone = (PhoneNumberBuilder()
            ..number = '415-555-0100'
            ..label = 'home')
          .build())
    .build();

制御構文

基本は全部Javaと一緒。for-inとかも一緒。
でもIterable舐めるためだったら普通にforEach使ったほうが良い。

switch-case文だけちょっと違いがある。

  • すべてのインスタンスをswitchできる?(比較は == でなされる)
    • 少なくともint, String, 列挙型は可能
    • == をoverrideしているクラスは対応しないっぽい
  • case節には必ず break もしくは continue を書かないといけない
    • default節(必ず最後に書く必要あり)はbreak書かなくて良い
  • contine label; で、label: をつけたcase節に飛ぶことができる(Cのswitch-caseでbreak省略すると次のcase節の内容が実行されるのと似たような挙動再現のための機能)
var command = 'CLOSED';
switch (command) {
  case 'CLOSED':
    executeClosed();
    continue nowClosed;
  // Continues executing at the nowClosed label.

  nowClosed:
  case 'NOW_CLOSED':
    // Runs for both CLOSED and NOW_CLOSED.
    executeNowClosed();
    break;
}

例外処理

Javaと似てる。

try {
  throw Exception("\レイガイダヨ/");
  // なんと任意のインスタンスを例外として投げることが可能
  // 当然型による例外型による条件分類がでめんどくさくなるのでやめたほうが良いと書かれている
  // ならなぜそんな実装にしたんだ…
  // throw "\レイガイダヨ/";
} on Exception catch(e) {
  // 捕まえる型を指定するには on ~~ catch を使う
  // eはException型

  // retrhowでtry-catch-finallyブロックの外に例外を投げ直す事ができる
  rethrow;
} catch (e, s) {
  // 型を指定しないcatchは、何型かわからない例外全部キャッチする
  // catchに仮引数を2つ指定すると、2つ目はStackTraceオブジェクトが入る
} finally {
  // finallyブロックは省略可。
}

クラス

クラス宣言の記法とか、すべてのクラスはObjectの子クラスである、という点とかはJavaそっくり。

なお this は通常省略するのがDart流らしいです。
(この辺どうするかはチームのコーディングルールによるでしょう)

細かく違うところをざっくり。

  • Javaの instance.class 相当は instance.runtimeTypeType 型のインスタンスが返る
  • インスタンス化する際の new はDart2から書いても書かなくても良い

コンストラクタでフィールドに値を代入する

Kotlinのコンストラクタ引数に val/var を付けるの下位互換っぽいの。

class Person {
  final String firstName;
  final String lastName;

  // これだけでフィールドに値を代入できる
  // コンストラクタのbody内ではすでに代入された状態で使用できる
  // this省略しちゃうと別の仮引数として扱われちゃうっぽい
  Person(this.firstName, this.lastName) {
    ...
  }
}

名前付きコンストラクタ

Dartのクラスはデフォルト(名前無し)コンストラクタと名前付きコンストラクタを持つことができます。
名前付きコンストラクタはファクトリメソッドの代わりに便利。
そもそもDartはメソッドオーバーロードができないので、複数コンストラクタを持つクラスを作る方法はこれしかない模様。

class Person {
  final String firstName;
  final String lastName;

  Person(this.firstName, this.lastName);

  Person.anonymous() {
    this.firstName = "Anoymous";
    this.lastName = "";
  }
}

var p = Person.anoymous();

コンストラクタ継承

Javaと同じく、Dartのコンストラクタは自動的に継承されないので、親クラスのコンストラクタを使いたい場合は明示的に呼び出す必要があります。
でもって、それ専用の記法があります。

class Mutant extends Person {
  Mutant(String firstName, String lastName) : super(firstName, lastName);

  Mutant.anonymous() : super.anonymous() {
    print("anonymous mutant");
  }
}

親クラスがデフォルトコンストラクタ(名前無し、かつ、引数なしのコンストラクタ)を持っていない場合は、子クラスは何かしら親クラスのコンストラクタを継承しないといけない決まりがあります。

ちなみに super ではなく this で自クラスのコンストラクタにリダイレクトすることもできます。

初期化子リスト

親クラスコンストラクタを呼び出す記法を使って、メンバの初期化や引数の条件確認などを、コンストラクタのbodyが実行される前に行うことができます。
面白いとは思うけど、正直これに意味はあるのだろうか… final なフィールドを初期化するときは初期化子リスト内で初期値を代入する必要があります(コンストラクタのbody内で初期化しようとするとコンパイルエラーになる)。

class Hoge {
  final String fuga;
  Hoge(String moge)
          : assert(moge != null),
          fuga = moge.toUpperCase() {
    print(fuga);
  }
}

/*
これは fugaなんていうセッターはない と怒られてコンパイルエラーになる
class Dame {
  final String fuga;
  Hoge(String moge) {
    fuga = moge.toUpperCase();
  }
}
*/

不変コンストラクタ

フィールドが全部 final 宣言されていて、コンストラクタの頭に const がついていれば、その不変コンストラクタから初期化したインスタンスはコンパイル時定数になれます。

class Point {
    final int x;
    final int y;

    const Point(this.x, this.y);
}

const p = Point(1, 2);

ファクトリコンストラクタ

factory をコンストラクタにつけると、ファクトリコンストラクタになります。
ファクトリコンストラクタはJavaのファクトリメソッドとほぼ同じで、中で明示的にインスタンスを返さねばなりません。
シングルトンパターンをするときに便利。

class Logger {
  final String name;
  bool mute = false;

  // _cache is library-private, thanks to
  // the _ in front of its name.
  static final Map<String, Logger> _cache =
      <String, Logger>{};

  factory Logger(String name) {
    if (_cache.containsKey(name)) {
      return _cache[name];
    } else {
      final logger = Logger._internal(name);
      _cache[name] = logger;
      return logger;
    }
  }

  Logger._internal(this.name);

  void log(String msg) {
    if (!mute) print(msg);
  }
}

メソッド

クラス内に関数書けばいい。
this で自インスタンスを指せる以外は何も変わらない。

なお、Javaとは違い、メソッドオーバーロードはできません。
基本的に任意パラメータがあればオーバーロードできなくても困ることはないと思いますが。

ゲッタ/セッタ

メソッドの特殊系。
Kotlinのカスタムgetter/setterの記法と考え方が似てるっちゃ似てるが、どうやら field みたいに値をしまうバッキングフィールドまでは用意してくれないみたい。
なので、バッキングフィールドが欲しかったら自分でprivateなフィールドを宣言しておいてやる必要がある。
なんてこったい。

class Rect {
  final Point point;
  final Size size;

  num get area => size.width * size.height;


  // バッキングフィールドは自動生成されないので自分で作ってやる必要がある
  Color _color;

  Color get color => _color;
  set color(Color newColor) {
    _color = newColor;
    notifyColorChanged();
  }
}

抽象クラスと抽象メソッド

実装しないと怒られるメソッド宣言を持つクラスを宣言できます。
Javaと一緒。

abstract class Animal {
    void hello();
}

class Cat extends Animal {
  void hello() {
    print("meow");
  }
}

暗黙的に作られるインターフェイス

Dartには interface キーワードが存在しませんが、クラスを宣言した時点でそのクラスと同じAPIのinterfaceが勝手に作られています。
インターフェイスは実装を持ちません。

Javaと同じく implements キーワードで実装可能です。

// Personクラスの宣言であり、greet()メソッドを持ったPersonインターフェイスの宣言でもある。
class Person {
  // privateなものはインターフェイスには含まれない
  final _name;

  // コンストラクタもインターフェイスには含まれない
  Person(this._name);

  // このメソッドのシグネチャはインターフェイスに含まれる
  String greet(String who) => 'Hello, $who. I am $_name.';
}

// Personインターフェイスを実装する
class Impostor implements Person {
  get _name => '';

  // greetを実装しないと怒られる
  String greet(String who) => 'Hi $who. Do you know who I am?';
}

String greetBob(Person person) => person.greet('Bob');

void main() {
  print(greetBob(Person('Kathy')));
  print(greetBob(Impostor()));
}

「すべてのクラスはクラスであると当時にインターフェイスであり、extendsするかimplementsするかで性質が違う」と捉えても良いのかもしれない?

継承

Java同様の継承もできます。Javaと同じでDartは単一のクラスの継承のみ可。
キーワードとかも親クラスの呼び出し方も全部いっしょ。

メソッドオーバーライド

Javaと同様@override アノテーションを付けることが可能です。任意です。強制にしてくれればいいのに。

オーバーライドするメソッド引数の型を元のメソッドの型より狭めたい場合は、 covariant キーワードを付ければ型を変えつつオーバーライドできます。

class Animal {
  void chase(Animal x) { ... }
}

class Mouse extends Animal { ... }

class Cat extends Animal {
  void chase(covariant Mouse x) { ... }
}

演算子オーバーライド

C++やGroovyなどと同様に演算子の上書きが可能です。

上書き可能な演算子については公式のページ参照

ただし == を上書きする場合は hashCode() メソッドも一緒に上書きする必要があります。
Javaの equalshashCode 同時にいじらないとよくない、というのと似てる(両方上書きしないとHashMapとかに入れたとき挙動がおかしくなるので)。

class Vector {
  final int x, y;

  Vector(this.x, this.y);

  Vector operator +(Vector v) => Vector(x + v.x, y + v.y);
  Vector operator -(Vector v) => Vector(x - v.x, y - v.y);

  // Operator == and hashCode not shown. For details, see note below.
  // ···
}

noSuchMethod()

Rubyの method_missing みたいなメソッドとして noSuchMethod なるメソッドがあります。
宣言されていないメソッドをinvokeした際に呼び出されるメソッドです。
これを許しちゃうとタイプセーフとは言えなくなるのでは…? と思える曲者ですが、どうやら昔の名残で残っている様子。
noSuchMethodが呼び出されるには以下の条件があって、どれか一つに該当する必要があります。

  • レシーバーが dynamic 型である
    • こちらのパターンの場合、実行時に警告が出る Warning: '(メソッド名)' is used reflectively but not in MirrorsUsed. This will break minified code.
    • @MirrorsUsedを使うと、事前に指定したメンバの呼び出しについては警告が出なくなる。
  • レシーバーの型のメソッドが実装を持っておらず、かつ noSuchMethod がオーバーライドされて Object#noSuchMethod と実装が異なっている
class A {
  @override
  void noSuchMethod(Invocation invocation) => print("method ${invocation.memberName} is missing");
}

class B {
  // なんとabstract classでなくても抽象メソッドは持てる
  void hoge();

  @override
  void noSuchMethod(Invocation invocation) => print("method ${invocation.memberName} is missing");
}


void main() {
  dynamic a = A();
  a.hoge();

  // なんとabstract classでなければ抽象メソッドを実装しないままインスタンス化できる。嘘だろおい
  B b = B();
  b.hoge();
}

列挙型

Javaとほぼ同じ。

列挙子は宣言された順にインデックス(0始まり)が割り振られていて、 index で参照できる。

enum Color { red, green, blue }

assert(Color.green.index == 1);

switch-case文でも使用可能。

enumを継承できなかったり(mixinにも使えない)、enumのインスタンスを自前で生成できない(定数のみしか使えない)というのもJavaのenumと同じ。

ただし実装を持つことができないのがJavaのenumと違うところ。

ミックスイン

継承時、コンストラクタがないクラスを with キーワードでミックスイン(継承せずに実装を取り込む)できます。
(Dart 2.1.0-dev.1.0.flutter-ccb16f7282時点。バージョンが上がるとともに緩和されていく傾向にあるようですし、構文も変わる提案がなされているようなので、将来的に変わる可能性が十分あります)

継承ではないため、複数のクラスをmixinできます。

class AndroidEngineer {
  void implementAndroidApp() {
    print("got it!");
  }
}

class IOSEngineer {
  void implementIOSApp() {
    print("Jobs!!");
  }
}

class Person {
}

class FlutterEngineer extends Person with AndroidEngineer, IOSEngineer {
}

void main() {
  final e = FlutterEngineer();
  e.implementAndroidApp();
  e.implementIOSApp();
}

withextends キーワードとセットでないと使えないので、特に何かを継承せず機能だけ取り込みたい場合は extends Object with 〜〜 としておくと良いかも。

ただし、親クラスが実装しているメソッドはabstract扱いされるようなので、ミックスインするクラス or ミックスインされるクラスで実装し直す必要があります。

class Engineer {
  void say(String s) {
    print("Hello world, $s");
  }
}

class AndroidEngineer extends Engineer {
  void implementAndroidApp() {
    print("got it!");
  }

  // ここ、もしくはFlutterEngineer内でsay()を実装し直さないとコンパイルエラー
  @override
  void say(String s) {
    print("Hello world, $s");
  }
}

class Person {
}

class FlutterEngineer extends Person with AndroidEngineer {

}

クラス変数とクラスメソッド

Javaと全く同じです。フィールド宣言やメソッド宣言に static を付けるだけ。

static メソットはコンパイル時定数扱いなので、constantコンストラクタの引数に渡すことができます。

ジェネリクス

Javaと基本的に同じ。若干違うところもある。

型変数の型を保持する期間

Javaの型変数の状態はコンパイル時にのみ使われていて、実行時には保持されていません。
Dartでは実行時まで保持されているので、実行時に型変数の型を気にするようなコードも書けます。

final strList = <String>[
  "hoge", "fuga", "moge"
];
assert(strList is List<String>);

型変数に制限を加える

Javaと同様に <T extends S> で「TはSまたはSの小クラスである型」とすることができます。

が、Javaと同様に <T extends A & B & C> のようにアンパサンドでつないで複数の条件を設けることはできないようです。
(文法エラーになる)

メソッドのジェネリクス

Javaとは文法が違うので注意。

// 型変数の定義を書く位置がメソッド名と仮引数リストの間になっている
// (Javaではアクセス修飾子の戻り値型の間だった)
T doHoge<T>(T value){ ... }

昔はメソッドでしかジェネリクス使えなかったっぽいですが、Dart 2では通常の関数でもジェネリクスできるとのこと。

ライブラリ

ライブラリを使う

import '(ライブラリURI)' でライブラリを読み込めばOK。

ライブラリのURI

ライブラリは全部URIで表されるようになってます。

接頭辞はジャンルによって違うので、使い分けに注意。

  • Dart標準ライブラリ… dart: 以下
  • サードパーティ製ライブラリ(パッケージマネージャで導入されたライブラリ)… package: 以下
    • Flutterのライブラリもサードパーティにあたる
  • ネイティブ実装のライブラリ… dart-ext: 以下

相対パス指定でパッケージライブラリ化していないソースも参照できます。

ライブラリ名に別名を付ける

Kotlinのimportと同じ。

import "package:mylib/awesome_functions.dart" as awesome;

ライブラリの一部の機能だけ使う/除外する

// Import only foo.
import 'package:lib1/lib1.dart' show foo;

// Import all names EXCEPT foo.
import 'package:lib2/lib2.dart' hide foo;

ライブラリの遅延ロード

なんと遅延ロードが可能です。
使いどころとしては以下のような時があるようです。

  • アプリケーションの起動を早くしたい場合
  • A/Bテストなどの用途で、都合によって実装を切り替える場合
  • 稀にしか出さないような画面やダイアログなど、普段は使わない機能を読み込まず軽くしておきたい場合

遅延ロードするには deferred as で別名を付けておく必要があります。

import "package:rarely_used/lib.dart" deferred as rare;

loadLibrary() を呼び出すと読み込みが行われますが、Future を返すメソッドなので await と使わないときついと思います。

Future doSomething() async {
  await rare.loadLibrary();
  rare.awesomeFunction();
}

注意事項とかもいくつかあります。

  • 遅延ロードされるライブラリ内でconst宣言されている定数は、ロードを要求する側では定数として扱えません。
    • コンパイル時にその値が存在してないんだから当然といえば当然。
  • 遅延ロードするライブラリで宣言されているクラスは使用できません
    • コンパイル時にそのクラスが無いんだから当然といえば当然。
  • deferred as (namespace) のnamespaceに勝手に Future loadLibrary() メソッドが追加されるので注意
    • 同シグネチャのメソッドを置いておいたりしない限り大丈夫

ただ、どうやらFlutterのDartではまだ実装できていないらしいです。残念。
設計を考えておかないと使い所が難しい印象。

ライブラリを作る

pubspec.yaml を置いたり一定のディレクトリ構造を守る必要があったりとか、いろいろ決まり事があります。

外に公開するライブラリを作る機会はまだなさそうなので、必要になったらドキュメントを読みましょう -> Create Library Packages

lib/ にアプリケーションコードを配置させてたりするので、FlutterアプリケーションもDart的にはライブラリなんですね。

非同期サポート

C#やJavaScriptの async/await と似たような感じで、Dartも構文で非同期処理を手続き的に書けるようになっています。

Future型とasync/await

JavaScriptのPromiseと似たようなものです。
単一の値やエラーがいつか落ちてくるので、落ちてきたときに非同期に処理を行います。
詳しいAPIはFutureのAPIドキュメントを見てください。

Futureを返すメソッド内であれば、他のFutureを返すメソッドを手続き的な記法で呼び出して結果を待つことができます。

Future checkVersion() async {
  var version = await lookUpVersion();
  // Do something with version
}

await (expression) のexpressionはFutureであるのが普通だけれども、もしFuture以外の型だったら勝手にFutureでラップしたことにしてくれるらしい。

main関数もasyncにできる。その場合もちゃんと戻り値は Future<void> にしておかないといけない。

Future main() async {
  checkVersion();
  print('In main: version is ${await lookUpVersion()}');
}

async関数の戻り値は勝手にFutureでラップされる。

async キーワードを書くのは引数リストの後。
ラムダ式もasyncにできる。

Future<String> lookUpVersion() async => '1.0.0';

try-catch文の中でもawait可能。Futureがエラーを伝えてきた時点で例外が飛んでcatchに移行されます。

Stream型とasync/await

RxJavaやってる人ならお馴染みのStreamです。
listenしてると複数の値やエラーが落ちてくるので、落ちてきたときに非同期に処理を行います。

listenで見張っていてもよいですが、Streamの値を単純なループと同じような文法で記述する await for 文があります。

Future main() async {
  // ...
  await for (var request in requestServer) {
    handleRequest(request);
  }
  // ...
}

通常のfor文と同様に breakreturn で抜けると、Streamの購読を停止できます。

await forasync 関数内で使う必要があります。

await for は基本的に 終りがある Streamで使うべきで、UIイベントのストリームなどのエンドレスストリームでは使うべきではありません。
(ループの外から購読を停止するAPIが無いため)
listen() で購読すれば StreamSubscriptionのインスタンスが手に入るので、 cancel を叩くことでエンドレスなストリームであっても購読を停止できます。

ジェネレーター

IterableStream の値を生成する関数のことです。
Iterable の非同期版が Stream だと思えばよろしい。

Itrable のジェネレーターはこう書きます。

// body前に sync* が必要
Iterable<int> naturalsTo(int n) sync* {
  int k = 0;
  // yieldで値を返しつつ関数を中断できる
  while (k < n) yield k++;
}

Stream はこう。async* に変わるだけです。

Stream<int> asynchronousNaturalsTo(int n) async* {
  int k = 0;
  while (k < n) yield k++;
}

中身が再帰関数になっている場合は yield* で中断するようにします。
多分、末尾再帰最適化とかいろいろやる都合で人間がコンパイラにおしえておいてやらないといけないのでしょう。

Iterable<int> naturalsDownFrom(int n) sync* {
  if (n > 0) {
    yield n;
    yield* naturalsDownFrom(n - 1);
  }
}

呼び出し可能なクラス

Kotlinの invoke() と同じです。
call() を実装しているクラスのインスタンスは関数同様に () を後置すると call() が呼び出されます。

class WannabeFunction {
  call(String a, String b, String c) => '$a $b $c!';
}

main() {
  var wf = new WannabeFunction();
  var out = wf("Hi","there,","gang");
}

call()を実装してると関数型としても扱えるっぽいです。

Isolate

Dartにはスレッドはありません。
すべてのコードはIsolateという単位で動いています。

各Isolateが独立したメモリヒープを持っていて、(特別なことをしなければ)あるIsolateの挙動が他のIsolateの挙動を変えてしまったりする事はありません。

並列処理をしたいときなどは新しいIsolateを立ち上げて、そこで並行処理をおこなわせたりします。

Isolate間でやりとりする場合はPortを開けてそこにデータを流します。

import "dart:isolate";
import "dart:async";

void w(sendPort) {
    for(int i = 0; i< 100000; i ++) {
      print("waiting: $i");
    }
    sendPort.send("finished!");
}

main() async {
  final receivePort = ReceivePort();
  final sendPort = receivePort.sendPort;
  await Isolate.spawn(w, sendPort);
  receivePort.listen((msg){
    print("message from another Isolate: $msg");
  });
}

相互にやり取りする場合はもうちょっと面倒。

Isolate.spawnUri() を使えば main() 関数が書かれた別ファイルをIsolateとして立ち上げることも可能で、Flutterアプリもこの方法で立ち上げられてます。

Typedef

Kotlinのtypealiasを関数型に限定したようなやつ。

typedef キーワードで関数型の別名を作ることが可能です。

typedef GestureTapDownCallback = void Function(TapDownDetails details);

なんとジェネリクスにすることも可能。

typedef Compare<T> = int Function(T a, T b);

int sort(int a, int b) => a - b;

void main() {
  assert(sort is Compare<int>); // True!
}

メタデータ

DartはJava風のアノテーションでコードにメタデータを付与する事ができます。

言語組み込みのアノテーションはannotations.dartを見るとわかります。よく使うのは @override@deprecated なんじゃないですかねえ。

デフォルトコンストラクタがconstコンストラクタであるクラスはアノテーションとして使用可能なようです。
クラスやらメンバやらにつけたアノテーションは、リフレクションを使えば実行時に取得可能です。

import 'dart:mirrors';

class Todo {
  final String comment;
  const Todo(this.comment);
}

@Todo("いつかつくる")
class Hoge {}

main() {
  final h = Hoge();
  final mirror = reflectClass(Hoge);
  final todoRef = mirror.metadata.first;
  print(todoRef.reflectee.comment);
}

コメント

大体Javaと同じ

一行コメント

final hoge = 0;    // 行末までがコメント

複数行コメント

/*
    何行でも
    コメント
    できちゃうよ
*/

ドキュメントコメント

dartdocでHTMLのAPIドキュメント吐いてくれるっぽいです。
多分IDEの補完時とかにドキュメント出してくれるだろうと思うので積極的に書いていって問題ないと思います。

/// 単一行コメントを
/// 複数つなげても
/// ひとつのドキュメントになります。
class Hoge {}

/**
    Javadoc民はこっちの方が慣れてる気がする

    Kotlinと同じく [Hoge] とかするとHogeクラスへ参照を張れる
*/
class Fuga {}

わからないことがあったらこれを読んでください。
https://www.dartlang.org/guides/language/language-tour