every Tech Blog

株式会社エブリーのTech Blogです。

Flutter2から3に上げた際のNull Safety対応

Flutter2から3に上げた際のNull Safety対応

はじめに

リテールハブ開発部のネットスーパー事業向き合いで開発を行っている野口です。 今回は、弊社で開発しているFlutterアプリのバージョンを2.10.5から3.24.3に上げた際にNull Safety対応を行ったのでそれについて書いていきます。

Null Safetyとは

Null Safetyは、変数が null を持つことによって発生するバグを防ぐための仕組みです。 Flutter2以前では変数はnullableになっているため、nullを考慮したコードが必要でした。 Flutter3以降では、変数がnon-nullableになるため、より堅牢なコードが書けるようになります。詳しくは以下をご覧ください。

https://dart.dev/null-safety/understanding-null-safety

Null Safety対応の手順

実際にFlutter 3.24.3にアップグレードした際、800件近いエラーが発生しました。(このプロジェクトの総Dart行数は20430行です) これらを効率的に解決するため、段階的に対応しました。 まず、データの基盤であるモデル層から対応を開始し、次にリポジトリ層とユースケース層を経由して、最終的にUI層に至る順で対応しました。こうすることで、データが正しく上流から下流に流れることを確認しつつ、エラーを段階的に解消することができました。

具体的な手順としては、まずモデル層で null の許容や必須を明確にし、次にリポジトリ層やユースケース層でデータ取得やビジネスロジックに対する null 処理を適切に行い、最後にUI層で画面表示の際に null を考慮した処理を実装していくという流れです。

このように各層で順を追って対応することで、エラーの混乱を最小限に抑えることができました。

パターンごとのアプローチ

ここからは実際に発生したエラーの具体例とそれに対する対応方法を紹介します。

パターン1 : 初期値のエラー

DateTime date;

エラー内容:

Non-nullable instance field 'date' must be initialized. Try adding an initializer expression, or a generative constructor that initializes it, or mark it 'late'.

対応方法

初期値が定義されていないことで起きています。 対応方法としては以下の3つがあります。

  1. nullableにする
DateTime? date;

?を付けて変数をnullableにすると、nullを許容するようになります。これによって、変数に初期値を定義しなくてもエラーは発生しません。 ただし、?を使用する際にはnullかどうかを考慮したコードを書く必要があります。

2.lateをつける

late DateTime date;

date = DateTime.now(); // どこかで代入する必要がある

lateを使うことでnon-nullableにすることができます。 変数が後で代入されることが確実であれば、lateを使用した方がnullableを使用した場合のようにnullを考慮したコードを書かなくて良くなります。 しかし、変数が後で代入されなければnullエラーが出るので確実に代入される場面で使用しましょう。

3.初期値を設定する

DateTime date = DateTime.now();

特定のデフォルト値(日付など)が決まっている場合には、初期値を設定します。 デフォルト値入れていいか判断しずらい場合は思わぬバグを起こさないために、デフォルト値を安易に入れないほうが良いかなと思います。

パターン2: モデルのエラー

class User {
  final String id;
  final String name;
   
  User({this.id, this.name});
}

エラー内容:

The parameter 'id' can't have a value of 'null' because of its type, but the implicit default value is 'null'.

対応方法

変数のidやnameはnon-nullableとして定義されているが、デフォルトでnullが入るようになっているためエラーが出ています。 対応方法としては以下の2つがあります。

  1. idのように必須の値はrequiredキーワードを付けて、必須パラメータにします。
  2. nameのようにnullになる可能性があるフィールドには、?を付けてnullableにします。
class User {
  final String id;    // Nullを許容しない
  final String? name; // Nullを許容する
   
  User({required this.id, this.name});
}

パターン3: nullを返す可能性がある

static String getUrl() {
  switch (environment) {
    case "production":
      return "production.example.com";
    case "staging":
      return "staging.example.com";
    case "development":
      return "development.example.com";
    default:
      return null;
  }
}

エラー内容:

A value of type 'Null' can't be returned from the method 'getUrl' because it has a return type of 'String'

対応方法

返却値がStringと定義されているがnullを返却する可能性があるためエラーが出ています。 対応方法としては以下の2つがあります。

1.デフォルト値を設定する null を返す代わりにデフォルト値を設定します。

static String getUrl() {
  switch (environment) {
    case "production":
      return "production.example.com";
    case "staging":
      return "staging.example.com";
    case "development":
      return "development.example.com";
    default:
      return "development.example.com";
  }
}

2.受け取り側で null チェックを行う デフォルト値を設定できない場合は、呼び出し元で null チェックを行います。

String? url = getUrl();
if (url == null) {
  throw Exception("Invalid environment");
}

パターン4:FutureBuildersnapshotの受け取り

FutureBuilder<ResultSampleData>(
    future: fetchData(),
    builder: (BuildContext context, AsyncSnapshot<ResultSampleData> snapshot) {
      if (snapshot.hasData) {
        List<String> dataList = snapshot.data.dataList; // エラー箇所
      }
    },
);

エラー内容:

The property 'dataList' can't be unconditionally accessed because the receiver can be 'null'.

対応方法

snapshot.dataがnullの可能性があるためエラーが出ています。 FutureBuilderhasDatadataのnullチェックをしているため、data!を使ってnullを除外します。

if (snapshot.hasData) {
  List<String> dataList = snapshot.data!.dataList;
}

パターン5:ModalRouteでの引数の受け取り

final String args = ModalRoute.of(context).settings.arguments;

エラー内容:

A value of type 'Object?' can't be assigned to a variable of type 'String'.

対応方法

ModalRoute.of(context).settings.argumentsObject?型であり、それが実際にStringであるかどうかが保証されないため、型不一致のエラーが出ています。

as Stringを使ってキャストし、この値はString型として扱って良いことを明示してあげることでコンパイラが型を正しく認証でき、エラーが解消できます。

final String args =
        ModalRoute.of(context)?.settings.arguments as String;

Null Safety対応を行って感じたこと

膨大なエラー数であったが、モデル、ポジトリ層、ユースケース層、UI層で段階分けすることと、エラーのパターン分けをすることで、混乱を最小限に抑えて作業できた点が良かったです。

既存のコードにはnullを適切に処理している部分もありましたが、ほとんどの箇所でnull処理が不十分であり、全体的にnullが入りやすい設計になっていたことを再認識しました。

おそらく、Flutter2でもnullチェックを意識してコードを書くことは大切だと思うので、そもそも既存のコードの書き方にも問題がありそうだなと感じました。

まとめ

今回はFlutter3でのNull Safety対応についてまとめました。 初めての移行作業ではありましたが、段階分けとエラーパターンの分類を行うことで、効率的かつ統一感のある対応ができました。 個々のエラーは一見シンプルではあるものの、パターン化して整理することで、どのように対応すべきか迷うことが少なくなり、結果的に作業がスムーズに進んだと感じています。 Null Safetyの対応は、手間がかかるものの、コードの信頼性や堅牢性が向上し、バグの予防に大きく寄与します。今回の記事が、Null Safety対応やFlutterのバージョンアップを迷っている方にとって、少しでも参考になれば幸いです。

ご覧いただきありがとうございました。