dart2 null-safety対応でやったこと

こんにちは、 id:numanuma08 です。コベリンでは新しいアプリfennecをflutterで開発中です。

twitter.com

2021年初頭のホットな fullter 関連の話題といえば、dartのnull-safety対応でしょう。

dart.dev

fennecも今後の開発を継続するためnull-safety対応を行ったので、その簡単な記録をまとめます。

Null-safety対応をいつするか

dart 2.12以降を利用していればNull-safetyが有効になりますがアプリのコードだけでなく依存しているライブラリもNull-safetyに対応していなければなりません。dart pub outdated --mode=null-safetyを実行するとNull-safetyに対応していてアップデート可能なライブラリ一覧が表示されます。依存しているライブラリは色々ありますが、私達はアプリの根幹となるfreezedやjson_serializable、state_notifierのNull-safety対応を待つこととしました。

Null-safety対応以前

とりあえず現状を把握するため、心を無にしてdartおよびfullterのバージョンを上げてどれくらいエラーが発生するのか調べます。

330件のエラーが発生しました。dartはマイグレーションツールも用意してくれていて、dart migrationを実行するとWEBブラウザ上で動作するツールが起動します。しかし、結果的に今回このツールは使いませんでした。実は既存のコードは将来Null-safety対応が発生することを見越して、修正が簡単に済むように作られていました。その工夫を紹介します。

freezedの機能を使う

freezedは利用されている方も多いと思いますが、コードの自動生成ライブラリで、クラスの情報の一部コピーやパターンマッチなどに関するコードを生成してくれます。

fennecは内部で保持している状態やモデルクラスのほとんどにfreezedで自動生成したコードを利用していました。freezedは以前からnullを許容したいプロパティには@nullableを、初期値を設定したいプロパティに対しては@Defaultというアノテーションをつけるよう機能が提供されていました。これらの機能は@nullable?をつけたプロパティとすることで、そしてデフォルト値の無いプロパティについてはrequiredをつけることでdart2.12対応がだいたい完了しました。

assertを使う

自分たちで作ったWidgetやメソッドのパラメータがnullを想定して無い場合、わりとassert(hoge != null)というアサーションを設定していました。この対応自体は完璧ではなかったですが、それでも多くのコードでこの対応を行っていたおかげでnull許容型かどうかの判断が可能でした。

どうやってNull-safetyに対応したか

ここからはコード中に何箇所か出現したNull-safety対応のための変更部分を紹介します。

lateキーワード

クラスのプロパティがインスタンス生成時点で初期化されていないけどnull非許容型で表現したいときに、lateキーワードをつけたプロパティで宣言します。fennecで多かったのはlistenしたStreamをキャンセルするために使うStreamSubscriptionでinitStateで初期化、disposeでキャンセルされるけれどnullとして扱いたくないのでlateキーワードを付けて宣言しました。コード例は以下です。

late StreamSubscription _subscription

void initState() {
  _subscription = ...
}

void dispose() {
  _subscription.cancel()
}

Nullになる場所の処理を考える

APIの都合などで、だいたい非nullなのにプロパティやメソッドの返り値がnull許容型で宣言されている場合があります。その場合、強制unwrapを使うことになりますが、ただ!をつけるより必要に応じてラッパーや例外をスローすることで、unwrap失敗時の対応がやりやすくなります。

例としてpath_providerのgetExternalStorageDirectory()というメソッドを紹介します。

pub.dev

これはAndroidアプリの書込み可能なアプリ外ディレクトリのパスを返すメソッドですが、Androidで動作させている限りは(だいたいの場合)値が返ってきます。なので、このAPIを使うプラットフォーム依存のコードは次のようになります。

// プラットフォームがAndroidのときだけ実行される
final path = await getExternalStorageDirectory();
if (path == null) {
  throw Exception('外部ディレクトリが無効になっています');
  return;
}

強制unwrapや早期リターン、?:による代わりの値を入れるなどしてアプリを動かすより適切に例外をスローしてアプリをクラッシュするなりログを出力した方が異常な状態になっているとすぐわかって便利です。特に早期リターンや?:は「アプリは動くけど画面がなにかおかしい」という発見しにくいバグを生み出しがちなので注意が必要です。

Null-safety未対応ライブラリの扱い

幸いなことにfennecが依存しているライブラリはほとんどNull-safety対応が行われました。しかし、一部ライブラリはNull-safety対応が行われていなかったため、以下の対応をしました。

  • PRが出ていればそのPRのハッシュを、出ていなければ自分でPRを出してそのハッシュを依存先に指定
  • コードはNull-safety対応していたが、pub.devにデプロイされていないものは最新のコミットハッシュを依存先に指定

このブログを書いている時点で3つのライブラリがNull-safety対応版がリリースされておらず、上記対応となっています。

まとめ

fennecをNull-safety対応させるためにやっていたこと、やったことをまとめました。実際、Null-safety対応は私一人のタスクでだいたい1週間くらいで他のタスクもこなしつつ終わったのでめちゃくちゃ大変だったという印象はありません。Null-safety導入による破壊的変更は事前にアナウンスされていたため事なきを得たという感想です。