Flutterをバージョンアップしてnull safetyな世界へ

モバイルアプリ開発を担当している工藤です。

アンドパッドでは現在2種類のFlutterアプリを提供しています。7月にその両方のアプリがFlutter1系から2系にバージョンアップしました。

Flutterの2系ではデフォルトの型にはnullをセットできなくなり、もし代入したい場合は明示的に ? がついたOptional型で宣言する必要があります。

型によってnullの有無を保証できるので想定外のnull変数を参照して表示エラーになることが減り、最低限のチェック処理を記述するだけで制御が可能になります。

今回バージョン2.2.1にアップデートを行ったところ、いくつかのトラブルに遭遇しました。

その中から、知っていたらすぐ解消できそうなものを3つ紹介したいと思います。今後マイグレーションする方の参考になれば嬉しいです。

マイグレーションツール実行エラー

アップデートを円滑にすすめるために、公式サイトからマイグレーションツールが提供されています。 ツールを実行することで、構文が変更されたり参照元から判断して適宜Optional型に変換される...

はずでしたが、僕の手元で実行したところ以下のようなエラーが大量に出力されました。

$ dart migrate

error • The argument type 'Null' can't be assigned to the parameter type 'int' at hogehoge.dart:9999:99 • (argument_type_not_assignable)

類似の事象が報告されていて、このissueでは最新版のツールで実行することが推奨されています。

github.com

一時的にstableからdevチャンネルに切り替え実行したところ公式サイトで紹介されているようなURLが発行され、ブラウザ経由でコードの変更案を確認することができました。

なお調査の過程でリポジトリをRevertしてマイグレーションを数回試行したところ、マイグレーションが成功するときと処理が行われず終了するときがありました。もし処理が空振っているなと感じたら、一度最初からやり直すというのも手かもしれません。

Optional型の大量発生

Dartの引数の扱い

Dartの引数は、以下のように必須引数名前付き引数optional位置引数の三種類の中から選択できます(注: それぞれ正確な日本語名称は異なるかもしれません)。

void foo(
  int a /* 必須引数 */, {
  int b = 0 /* 名前付き引数 */,
}){
  print("hello!")
}

void bar([
  int c = 0 /* optional位置引数 */,
]){
  print("hello!")
}

void main(){
  foo(1, b: 1);
  bar(1);
}

今回対応したプロダクトでは元々、optional位置引数よりも可読性があるという理由から名前付き引数を多用していました。一方名前付き引数はrequiredを設定しても引数未設定でビルドエラーになるわけでもなく(※Lint設定が理由かもしれません)、引数の変更や記載漏れの検知がしにくいという課題もありました。そのため最近は、必須引数を利用する頻度が多くなっていました。

全体のルールとして強いコーディング規約を設けていなかったため、2種類の記載方法が混在しているような状態でした。また、nullが設定される可能性は記載方法の判断材料としていませんでした。

マイグレーションツールの解釈

マイグレーションツールでは、前後のコンテキストで判断して変更を提案されます。

例えば最初から初期値が設定されている変数は非Optional型と解釈されますが、論理的に必ず値が設定されるはずでも初期値が未設定であればOptional型と判定され、更新対象として提案されます。

先程紹介した引数のうち必須引数以外は省略可能でnullになるため、多くの場合ツールによってOptional型として解釈されてしまいます。

変更箇所が少ない場合は目視で確認して適切に対応していけば良いのですが、それなりの規模のリポジトリだったこともあり変更の提案箇所も多く、(僕の心が折れて)全てツールの提案どおりにSubmitしてしまいました。

更新後のイケてなさと改善

マイグレーション後動作確認とバグ対応を進めたのですが、思いの外Optional引数が多く、修正するにつれ大量の if(value != null)を記述していることに気が付きました。少しでもnullの考慮を減らすためにマイグレーションしたのにも関わらず、多くの引数をツールの提案通りOptional型に更新したのが原因です。

対応方法としてデフォルト値を設定して回避することを考えましたが、contextのように定義しづらい値も多くあります。

いくつか改善案は考えたもののどれも一長一短だということがわかったため、基本に立ち返り引数の設定方法を整理しルール化して、正しく利用するという方針で改善していくことになりました。

数が多すぎると混乱しそうだったので、ルールは3つに絞りました。シンプルなルールにしたことで修正箇所は少し増えてしまいましたが、全体的に意図が明確になり理解しやすくなりました。

  1. DIを意図した引数のうち、通常はデフォルト引数で十分でテストコード内でのみ設定が必要なものは名前付き引数を利用する
  2. 明らかにオプションとして引数を渡したりそうでなかったりするケースでは名前付き引数を利用する
  3. それ以外は全て必須引数を利用する

上記ルールを最初から設定してマイグレーション前にきれいな状態にしておけば、もっとスムーズに作業できたはずだと思っています。

あるいは、段階的なマイグレーションを積極的に検討すべきだったかもしれません。

dart.dev

APK生成が失敗する

社内開発環境向けのアプリのAndroid版はapkの形式でFirebase App Distributionから配布しています(aabにしていないのは単純にまだ移行していないだけです。ストア配信版はaabに対応済み)。

レビューを通過しCIが実行されたところで、apkのビルドエラーが発覚しました。

当初エラーログのメッセージには有用なものが出力されなかったため、ライブラリのバージョン・Kotlinバージョン・Gradleバージョンを順に上げて様子を見ることにしました。Gradleバージョンのアップデートを行ったところでエラーメッセージが変わり、調査した結果appのbuild.gradle変更で対応できることがわかりました。

lintOptions {
    checkReleaseBuilds false
}

stackoverflow.com

apk形式を利用しているプロダクトがどれだけ残っているかわかりませんが、正直これの対応が一番辛かったため印象に残っています。

アップデートを終えて

苦しみながらも無事リリースも終え、現在は次の大規模改修に向けたリファクタリングとテストコードの拡充を進めています。

型の保証が強化されたことで実装に集中しやすくなり、Flutterの魅力の一つである開発体験が以前にも増して良くなったと感じています。何より開発環境を最新に保っているのはエンジニアとしても精神衛生的に良いことで、見えにくいけれど仕事の楽しさに直結する重要な部分だと思っています。

今回はFlutter2発表から少し時間がかかってしまいましたが、今後はチームで協力してより早い段階で新しい環境に追従できるようにしていきたいです。

まとめ

Flutter2系へのアップデート中に遭遇したトラブルと、その際実施したワークアラウンドを紹介しました。

どれもわかっていれば簡単な内容ですが、アップデートのトラブルを題材にした情報が少なかったために見た目より時間をかけて対応しています。

もし皆さんも同様の事象に遭遇したら、ぜひ試してみてください。

最後に

アンドパッドでは一緒に働く仲間を募集中です。

アプリに限らず皆さんが活躍できる場が数多く用意されています。ぜひ一度、採用ページを覗いていってください。

engineer.andpad.co.jp