こんにちは、 id:numanuma08 です。コベリンでは新しいアプリfennecをflutterで開発中です。
twitter.com【ベータテスター募集中】
— fennec_app (@fennec_app) 2021年3月23日
コベリンの新しいニュースアプリの開発に協力してくれませんか?
タブをカスタマイズして自分専用のニュースを作ってみましょう!
iOSはこちら👇https://t.co/lSdAGAb0Qy
Androidはこちら👇https://t.co/ysla62UWLV pic.twitter.com/QvsQCC6oQq
2021年初頭のホットな fullter 関連の話題といえば、dartのnull-safety対応でしょう。
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のバージョンを上げてどれくらいエラーが発生するのか調べます。
弊社で開発中の新アプリ fennec @fennec_app はflutterで実装していますが、ようやくflutter2に対応させるべくmigrationを開始しました。何もしてない状態でエラーが300個以上。やるぞー 💪('ω'💪) pic.twitter.com/Qnw6oUz3oB
— ぬま (@numa08) 2021年3月29日
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()
というメソッドを紹介します。
これは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導入による破壊的変更は事前にアナウンスされていたため事なきを得たという感想です。