すぐにFlutterを始めたい人のためのDart入門(前編)

この記事はFlutter 全部俺 Advent Calendar 3日目の記事です。

このアドベントカレンダーについて

このアドベントカレンダーは @itome が全て書いています。

基本的にFlutterの公式ドキュメントとソースコードを参照しながら書いていきます。誤植や編集依頼はTwitterにお願いします。

Dartについて

DartはGoogleがJavascriptの持っている根本的な問題を解決するために作った言語です。 もともとは完全にJavascriptを置き換える目的で、専用のDartVM上で動く言語として作られていました。

“We have decided not to integrate the Dart VM into Chrome.” http://news.dartlang.org/2015/03/dart-for-entire-web.html

しかし、結果的にその試みはうまく行かず、ChromeにDartVMが統合されことはありませんでした。 その後は他のAltJS言語と同様に、Javascriptへトランスパイルする方針に切り替えられました。

FlutterではAndroid/iOS版ではDartVMが、Web版ではJavascriptへのトランスパイラが使われています。

Flutterを書くためのDart言語入門

すぐにFlutterを始めるためのDartのチュートリアルです。Flutterで頻出の文法を中心に紹介します。

関数

CやJavaと同じように返り値の型を先に書きます。

Widget build(BuildContext context) {
  return ...
}

名前付き引数

Widget calculate({int a, int b}) {
  return ...
}

calculate(a: 1, b: 2);

引数のリストを {} で囲むことで、名前付き引数を定義できます。通常の引数と名前付き引数の併用も可能です。

名前付き引数のみオプショナルです。 また、名前付き引数は実引数の順番を気にせずに呼び出すことができ、デフォルト引数を指定することもできます。

名前付き引数を名前を指定せずに呼び出したり、通常の引数を名前付きで呼び出したりすることはできません。

void func(
  int hoge,  // 通常の引数。呼び出し元は必ず第一引数に指定する。
  {
    int fuga, // 名前付き引数。
    @required int foo // 名前付き引数。指定しなければ警告が出る。
    int bar = 2, //名前付き引数。指定されなければ2が入る。
  }
) {
  ...
}

func(1, foo: 2); // fugaは指定しなくてもいい

通常の引数と名前付き引数の使い分けを表にするとこのような感じになります。

名前をつけずに呼び出し 名前をつけて呼び出し 呼び出し側の指定 デフォルト引数
通常の引数 必須
名前付き引数 オプショナル

すべての型は Object を継承していて、KotlinやSwiftのような nullable/nonnul の区別はありません。 似たような概念に dynamic がありますが、これは「なんの型なのかわからない」という状態を表すものです。

数値型は intdouble のみです。以前は double の引数に int を代入することはできなかったのですが、 Dart2.1以降は暗黙的に変換されるようになりました。 そのためFlutterのサンプルやテンプレートには実引数に .0 をつけていることが多いですが、 2.1以降のバージョンを使っている人は int でそのまま指定できます。

  child: SizedBox(
    width: 24.0,
    height: 24.0,
    child: Container(...),
  )

↓ Dart2.1以降

  child: SizedBox(
    width: 24,
    height: 24,
    child: Container(...),
  )

高階関数

Flutterではコールバックの定義などで高階関数を多用します。 BuildContext を受け取って Widget を返す関数は以下のように定義します。

Widget Function(BuildContext) builder = (context) => Container();

これでもコードとしては正しいですが、返り値前置の構文のせいでで型が見づらいのでFlutterのソースコードでは事前に typedef されていることがほとんどです。

typedef WidgetBuilder = Widget Function(BuildContext context);

WidgetBuilder builder = (context) => Container();

呼び出し側はJavascriptと同じような構文です。

builder: (context) {
  return Container();
},

返り値のみの関数であればアロー関数も使うことができます

builder: (context) => Container(),

変数の宣言

型推論してくれるので初期値から型が明らかなときは var で宣言できます。

var a = 1; // int a = 1; と同じ。

final

再代入不可能な変数の宣言には final を使います。 final は参照先の変更はできるため、 List<T>final で宣言しても要素の追加や削除はできるので注意が必要です。

final int a = 1; // 型推論を使って final a = 1; とも書ける。 

final list = [1];
list.add(1); // 参照先の内部の変更は可能

const

コンパイル時定数の宣言には const を使います。 final のように再代入不可能であると同時に参照先の内部の変更もできません。 また、ある型のインスタンスを const で宣言するためには、その型のコンストラクタに const が指定してある必要があります。 そのため final よりも制約がつよいですが、 Flutterでは const 修飾子がついているWidgetに関しては再描画の処理をしないという最適化が行われているため、 つけられるときは積極的につけたほうがいいです。 2日目の記事の dartanalyzer を使うことで、 const をつけるべきところを指摘してくれるので有効活用しましょう。

// FlutterのCenterWidgetの定義、コンストラクタがconst指定してある
class Center extends Align {
  /// Creates a widget that centers its child.
  const Center({ Key key, double widthFactor, double heightFactor, Widget child })
    : super(key: key, widthFactor: widthFactor, heightFactor: heightFactor, child: child);
}
...
  // canst指定して初期化することで無駄な再描画を減らす
  child: const Center(
    child: Text(...),
  ),
...

getter/setter

以下のように書くことで、setter/getterを定義することができます。

int _a = 1;

int get a => _a;

int set a(value) {
  _a = value;
}

クラス

クラスは他の言語と同じようにフィールド、メソッドの定義ができます。インスタンス化は new をつけてもつけなくても同じ結果が得られます。 ( new をつけないコンストラクタはDart2以降に追加された機能です。古いサンプルなどは new を使っているところも多いですが、新しく書く場合は new をつけないほうが推奨されています。)

class Rectangle {
  const Rectangle(
    this.height,
    this.width,
    this.x,
    this.y,
  )
  
  final int height;
  final int width;
  final int x;
  final int y;
}

final rect = Rectangle(height: 100, width: 100, x: 0, y: 0);

明示的なインターフェース宣言はなく、クラスの定義のときに暗黙的にインターフェースが作成されます。

コンストラクタ

コンストラクタはJavaと同じようにクラス名と同名の関数です。

class Dog {
  String name;

  Dog(String name) {
    this.name = name;
  }
}

フィールドの初期化は省略して以下のように書くことができます。

class Dog {
  String name;

  Dog(this.name);
}

finalなフィールドはコンストラクタのbodyで初期化できないため、Initializerという機能を使います。 コロンに続いて、コンマ区切りの初期化処理を書くことができます。

class Dog {
  String name;
  final Owner owner;

  Dog(this.name): owner = Owner('John');
}

フィールドがすべてfinalであれば、 const constructor が作成できます。これによって、呼び出し側がconst宣言できるようになります。

class Dog {
  final String name;
  final Owner owner;

  const Dog(this.name): owner = Owner('John');
}

名前付きのコンストラクタも作ることができます。

class Dog {
  String name;
  final Owner owner;

  Dog(this.name): this.owner = Owner('John');
  
  Dog.pochi(): this.name = 'Pochi',
               this.owner = Owner('John');
}

別のコンストラクタにリダイレクトするには、Initializerの最後に呼び出します。

class Dog {
  String name;
  final Owner owner;

  Dog(this.name): this.owner = Owner('John');
  
  Dog.pochi(): this.name = 'Pochi',
               this.owner = Owner('John');
               
  Dog.taro(String name): this(name);
}

factory

Dartはファクトリデザインパターンを言語レベルでサポートしています。factoryを使って以下のようにSingletonを表現できます。

class Application {
  static Application _app;
  
  factory Application(){
    _app ??= Application._(); // 後述のnull-aware演算子。_appがnullなときだけ右辺が代入される。
    return _app;
  }
  
  Application._();
}

継承

クラスの継承は extends 、インターフェースの継承は implements で行います。コンストラクタは継承されません。 Dartのクラスは暗黙的なインターフェースも作成するので、 classから extendsimplements も行うことができます。

class A {
  int foo = 0;
  void bar() {
  }
}

class B extends A { // Aの実装を引き継ぐ
}

class C implements A { // Aのインターフェースしか引き継がない。superクラスの呼び出しは不可。
}

Mixin

Dartの継承はMixin-basedです。Mixinはフィールドやメソッドなどの機能のまとまりをclassに適用して拡張する仕組みです。

Mixinを使える言語はそんなに多くないので、概念に馴染みがない人が多いと思うので、図を使って説明します。

下の図は生物の分類を継承関係で表したものです、生物の中には泳げるものもいますが、 泳げるという性質は魚だけでなく、イルカ(Dolphin)のような一部の哺乳類(Mammalian)にも当てはまります。

Creature mixin

この場合、泳ぐという機能を継承関係で共通化しようとすると、共通のsuperクラスであるCreature(生物)に swim を実装することになりますが、 もちろん泳ぐことができないのCreatureもいるので、このアプローチは不適切です。

ここでMixinの出番です。 swim というメソッドを実装した Swimmer mixinを定義してDolphinとSharkにMixinします。 このとき重要なのは、Mixinはsuperクラスに対して行われることです。 つまり、「DolphinにSwimmerという機能を追加する」わけではなく、「Swimmerという機能を追加した『泳げる哺乳類』をDolphinが継承する」ということです。

以上のことをDartのコードで表現すると以下のようになります。

mixin Swimmer {
  void swim() { ... }
}

class Creature {}

class Mammal extends Creature {}

class Fish extends Creature {}

class Dolphin extends Mammal with Swimmer {}

class Shark extends Fish with Swimmer {}

Flutterアプリを作っていて、mixinを活用することはあまりありませんが、Animationの vsync を実装してくれる SingleTickerProviderStateMixin などで見ることがあります。

Enum

Enumは他の言語に比べてほとんど機能を持っていません。メソッドもフィールドもない純粋な列挙型です。

enum Color {
  Blue, Red, Black,
}

4日目の記事につづきます。

2日目: Flutterのツールを使いこなして開発効率を上げる :

https://itome.team/blog/2019/12/flutter-advent-calendar-day2

4日目: すぐにFlutterを始めたい人のためのDart入門(後編) : https://itome.team/blog/2019/12/flutter-advent-calendar-day4