Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

Kotlinのアレ、Dartでどう書くの? ~ 1人 FlutterKaigi unconference ~

こんにちは、モバイルクライアントグループの中島です。 ついこの間、約300~400年に一度のイベントがありましたね。 皆既月食+惑星食、道ゆく人が皆空を見上げていたのが印象的でした。 自分は残念ながら天体観測用の環境やカメラなどがなかったため、スマホのカメラでそれらしい写真を撮影するに留まりました。

www.youtube.com

さて、今回はFlutterのお話をします。 11/16~18に行われる FlutterKaigi 2022 、自分もプロポーザルを提出したのですが、残念ながら落選してしまいました。

fortee.jp

これから話すのは、採択されていたら話そうと思っていた話の一部です。 この記事が、Androidエンジニアの方々のFlutter開発新規参入に少しでも役立てれば幸いです。

data class

まず何はともあれdata class。 その名の通り、データを保持するだけのエンティティクラス作成の際に重宝しますね。

JavaではJava 14から搭載されるRecord Classが近い機能を持っています。

docs.oracle.com

data classの機能の内、特によく使うのはcopyメソッドやequalsメソッドではないでしょうか。

翻訳元として簡単にコードを書いておきます。

/** Kotlin */

data class Person(
    val firstName: String,
    val lastName: String,
    val age: Int,
)

Dartでは?

Dartでは残念ながら言語レベルで同様のサポートはありません。 そのため、自力で同機能を各クラスに書くか、外部パッケージを利用することになります。

freezed annotation

Studyplusではfreezedを採用しています。

pub.dev

freezedbuild_runnerというcode generatorを利用したパッケージです。 build_runnerの細かな説明や設定などはここでは省きます。*1

一言で言えば、Android開発で言うところのAnnotationProcessorのようにdata class相当のコードを生成できます。

では、先ほど書いたdata class Person を、freezedを用いてDartで書いてみます。 なお、このサンプルコードはfreezedのドキュメントから一部抜粋しています。

/// Dart

/// Required: ~~(この記述があるファイル名).freezed.dart ファイルにコードが生成される
part 'person.freezed.dart';

/// Optional: Json の Serializable 処理が必要の場合追記
part 'person.g.dart';

@freezed
class Person with _$Person {
  const factory Person({
    required String firstName,
    required String lastName,
    required Int age,
  }) = _Person;

  /// Optional: Json の Serializable 処理が必要の場合追記
  factory Person.fromJson(Map<String, dynamic> json) =>
          _$PersonFromJson(json);
}

こう記述した後にbuild_runnerを走らせると、data class相当のコードが生成されてKotlinと遜色ない機能を利用できるようになります。

/// Dart

/// 誕生日を迎えました :tada:
final newPerson = person.copyWith(
  age: person.age + 1,
);

data classの話題からは少しズレますが、freezedsealed class互換の機能も適応できます。

/// Dart

@freezed
class Person with _$Person {
  const factory Person({
    required String firstName,
    required String lastName,
    required int age,
  }) = _Person;

  /// 動物好きな人
  const factory Person.animalLover({
    required String firstName,
    required String lastName,
    required int age,
    @Default('Cat') String loveAnimal,
  }) = _PersonAnimalLover;
}

このように名前付きconstructorを用いることで、sealed classのような分岐に対応できます。

/// Dart

void person({
  required Person person,
}) {
  person.when(
    (
      firstName,
      lastName,
      age,
    ) {
      /// 通常のPerson インスタンスの場合の処理
    },

    animalLover: (
      firstName,
      lastName,
      age,
      loveAnimal,
    ) {
      /// animalLover インスタンスの場合の処理
    }
  );
}

ちなみに、お分かりかと思いますが自分は猫派です。

build_runnerによるコード自動生成への工夫

build_runnerによって生成されたコードは別ファイルに記述されます。 freezedではperson.freezed.dart(とperson.g.dart)です。 この仕様により、ファイルが増えてしまう点が懸念として挙げられます。 これについては、File Nesting機能があるIDEであればファイルツリー上の問題は軽減できます。

dev.to

www.jetbrains.com

built_value

Studyplusでは利用していないので簡単な紹介と比較までに留めます。 同様にbuild_runnerを利用してクラス機能を拡張するパッケージとして、built_valueもあります。 こちらもequalshashtoStringおよびSerializeなどに対応していますが、copyに対応していません。

pub.dev

Dart Data Class

同じく紹介までですが、AndroidStudio/IntelliJのプラグインとしてdata class相当のコードを自動生成するものもあります。

plugins.jetbrains.com

プロジェクトへのパッケージインストールが必要ない分、IDEへの依存や実コード内に生成される視認性などの影響とのトレードオフでしょうか。 最終的にはチーム合意に基づく技術選択となりそうですが、筆者個人としては個人の環境に影響されないfreezedの方が多人数による開発上便利ではないかという認識です。

Collection

Android開発でJavaからKotlinへ移行した際に、充実したCollection処理に感動した覚えがあります。

Dartでは?

Collection処理も、Dartのデフォルトではあまり充実しているとは言えません。 具体的に言うと map { } はあるけど flatMap { } flatten() に該当するものがなかったりします。 しかし、Dartの公式パッケージとしてcollectionが存在していますので、これを用いることで概ね補完できます。 Collectionを便利に扱うなら最初から入れておくことをお勧めします。

pub.dev

これで、先ほどの例で言えば flatten() に該当する処理を flattend で行えます。

collectionを導入してもまだ足りないと感じる場合はquiverも参照すると良さそうです。 *2

pub.dev

それでは、筆者の独断と偏見により「よく使いそう」なものをいくつか紹介します。 Kotlinと同じ名前/同じ使い方をできるものを紹介してもなんですので、違う名前だったり互換メソッドが存在しないものから選んでみました。

なおコードを見ていただくための予備知識ですが、Dartは [a, b, c] でList、{a, b, c} でSetの新規インスタンスをそれぞれ作成できます。

フィルタリング/filter

ある条件を満たす要素でフィルタリングする処理は Iterable<E> where(bool test(E element)) です。

/** Kotlin */

val list = listOf(1, 2, 3, 4)

/** 奇数だけのフィルタリング */
val result = list.filter { it % 2 != 0 }
/** result = [1, 3] */
/// Dart

final list = [1, 2, 3, 4];

/// 奇数だけのフィルタリング
/// スプレッド演算子 `...` は要素をバラして結合できます
/// where()はIterableを返すので、スプレッドで要素にバラした上でListとしてインスタンスを生成しています
final result = [...list.where((element) => element.isOdd)];
/// result = [1, 3]

Dartも静的型付け言語である以上、型でフィルタリングしたい時もあります。 当然と言うべきか、型でフィルタリングするIterable<T> whereType<T>()も存在します。

/** Kotlin */

val list = listOf(1, "2", 3, "4")

/** Intだけのフィルタリング */
val result = list.filterIsInstance<Int>()
/** result = [1, 3] */
/// Dart

final list = [1, '2', 3, '4'];

/// intだけのフィルタリング
final result = [...list.whereType<int>()];
/// result = [1, 3]

フィルタリング処理に限らず、DartのCollection処理はCollectionを返す際、基本的にListやSetを保持せずIterable型で返ってきます。 スプレッドを用いてListなりで再生成して受けておくと取り回しがいいでしょう。

(単発)検索/find, first(OrNull), last(OrNull)

要素を1つ検索する処理は E firstWhere(bool test(E element), {E orElse()?}) などになります。 Kotlinと同様 first last や、見つからなければnullを返せる OrNull 版もそれぞれ持っています。

また、OrNull でない場合は見つからなかった場合のフォールバックを orElse として自分で定義できます。 OrNull でない、かつ orElse を定義しない場合はIterableElementError.noElement()をthrowする仕組みになっています。

/** Kotlin */

val list = listOf(1, 2, 3, 4)

/** 初めに見つかったnの倍数を返す、見つからなければ-1 */
val result1 = list.firstOrNull { it % 3 == 0 } ?: -1
/** result1 = 3 */

val result2 = list.firstOrNull { it % 5 == 0 } ?: -1
/** result2 = -1 */
/// Dart

final list = [1, 2, 3, 4];

/// 見つからなければ orElse
final result1 = list.firstWhere(
  (element) => element % 3 == 0,
  orElse: () => -1,
);
/// result1 = 3

/// `??` オペレータはエルビス演算子のように扱えます(完全互換ではないです)
/// Androidでもデータバインディングなどの時にxml内で使いますね
final result2 = list.firstWhereOrNull(
  (element) => element % 5 == 0,
) ?? -1;
/// result2 = -1
重複削除/distinct

要素の重複削除はcollectionパッケージを導入しても該当するものがないので、一旦Set化するのが早いようです。 Setは重複不可のIterableなので、Listからの変換時に重複要素が削除されます。

/** Kotlin */

val list = listOf(1, 2, 3, 4)
val duplicationList = listOf(1).plus(list)
/** duplicationList = [1, 1, 2, 3, 4] */

/** 重複削除 */
val result = duplicationList.distinct()
/** result = [1, 2, 3, 4] */
/// Dart

final list = [1, 2, 3, 4];
/// 下記は [1, 1, 2, 3, 4]と同値です
final duplicationList = [1, ...list];

/// Set になった時点で重複が削除されますので List に戻せば完了です
final set = {...duplicationList};
final distinctList1 = [...set];

/// あるIterableを List にするだけなら toList() もあります(toSet()ももちろんあります)
final distinctList2 = set.toList();

まとめると、実践的にはこんな感じになります。

/// Dart

/// この程度ならto~~()との選択は見やすい形でお好みでしょう
final result = {...duplicationList}.toList;
/// result = [1, 2, 3, 4]

なお、残念ながら要素のパラメータで重複判定するdistinctBy{ it.id }に該当する処理は手で書くしかなさそうです。

終わりに

今回は、AndroidエンジニアがFlutterに参入した経験から、Kotlinで書いたあれやこれやの処理をDartのコードで紹介してみました。 紹介しきれなかったものもいくらでもあるのですが、今回はここまでとさせていただきます。 Flutterを書いてみたくなった方がいらっしゃれば、この記事は役目を果たせたと言えるでしょう。

FlutterKaigi 2022、楽しみですね。 なお、スタディプラスはブロンズスポンサーとして協賛しています。 冒頭でも言った通り筆者は落選してしまいましたが、弊社からはテックリードの若宮(id:D_R_1009)が登壇いたします。 詳しくはこちらをご参照ください。

tech.studyplus.co.jp

*1:簡単に説明すると、コマンドを叩くことでbuild.yamlファイルの設定を基にコードを自動生成するパッケージです。

*2:例えばLruCacheはquiverを導入すると互換機能を扱えます。