FlutterのNullSafetyとmigrationについてまとめてみた



Flutterの記事を整理し本にしました

  • 本稿の記事を含む様々な記事を体系的に整理し本にまとめました
  • 今後はこちらを最新化するため、最新情報はこちらをご確認くださ
  • 10万文字を超える超大作になっています(笑)

はじめに

Flutter2になって、Non-nullable by default(NNBD)になったんだよ。良かったよ。みたいな記事を見かけるのですが、何がいいのかさっぱりわからなかったので、自分なりにまとめてみました。

結論。良さがわかった気がする

まとめ

Nullの問題点

Nullというのはなにもないという状態を表すのに一見便利そうではありますが、一度使い始めると様々な場所でNullチェックを行う必要性がうまれ、バグの温床になりがちです。

NullPointerException/Null参照エラーを見たことがないエンジニアはいないと思います。

Null参照を発明したアントニー・ホーアは、後にこの発明を10億ドルの損失だったとさえ発言しています。

Null-aware演算子

まず、Flutter(Dart)には、Nullを考慮したいくつかの演算子が準備されています。

?.演算子

オブジェクトのnullチェックをしてからアクセスします

NullAware1.dart
x = nullableInstance?.method();

// 等価な処理
if(nullableInstance != null){
    x = nullableInstance.method();
}

?? 演算子

代入する変数がnullでないならばその値を使い、nullなら右辺値を使います。

NullAware2.dart

x = nullableValue ?? 0;

// 等価な処理
if(nullableValue != null){
    x = nullablevalue;
}else{
    x = 0;
}

??=演算子

変数がnullでないならば値を変えず、nullなら右辺値を使う
値がない場合の初期化などによく使われます。

NullAware3.dart
x ??= 0;

// 等価な処理
x =  x ?? 0; 

// 等価な処理
if(x != null){
    x = x; //元から値があるならそのままの意味
}else{
    x = 0; 
}

NullSafety

Null-aware演算子によって、Nullをある程度コントロールできるものの、Nullを使わなくて良いならば、元から入らないように制御するほうが、より根本的な対策になります。

Flutter2からはNon-nullable by default(NNBD)の考え方が導入されています。
int xと記載するとxにnullが入ることは認められません。
nullを許容する場合は明示的に、int? xのように、?を足してわかるように宣言する必要があります。

NullSafetyの効果

NullSafetyには、大きく2つの効果があります。

1つ目は開発者にNullの検討しなくてよい環境を提供することです。
Nullというのは通常予期せぬ場合に起きます。この予期せぬ状態を細部まで想定し、ハンドリングしていくことは開発者に大きなストレスを与えます。
また、実際にNPEが発生し最上位まで伝播してしまうと、アプリがクラッシュし、エンドユーザやGooglePlay/AppStoreに対して、おおきなマイナス要因を与えてしまいます。
NullSafetyではこれらの問題から開放されます。

2つ目はコンパイラが最適化を行えるため、性能が改善するということです。
Nullの可能性があるということは、Nullチェック及びNullが来た時の準備をして置かなければなりません。しかし、これらが一切除去されているときは、Nullが絶対に来ないという前提のものに処理を最適化することができます。
これは、非常に大きく有益な前提条件となり、アプリが大きくなればなるほど効果を発揮します。

NullSafetyのサンプルコード

それでは、実際のソースコードを見ていきます。

  • NullSafetyの確認を行うNullSampleクラスを準備し、動作確認用のメソッドmethod1,method2を準備しています。
  • getNullableは乱数を使って、整数かnullをランダムで返すメソッドです。
NullSample.dart
import "dart:math" as math;
class NullSample {
  method1() {
    int x = 10; // nullの可能性はない
    int? y = getNullable(); // nullの可能性がある

    //x = y; // Non-NullableにNullableを入れるのはコンパイルエラー
    //y = x; // NullableにNon-Nullableを入れるのはOK

    if (y != null) {
      x = y; //nullチェックの後のためコンパイルOK
    }

  }
  //ランダムでnullか1を返すメソッド
  getNullable() {
    var rand = new math.Random();
    if (rand.nextInt(2) == 0) {
      return null;
    }
    return 1;
  }
}
result1.sh
I/flutter ( 7785): x: 1 y: 1 (1が返る場合)
I/flutter ( 7785): x: 10 y: null (Nullが返る場合)

xは整数だけ、yは整数かnullなので、yのほうが集合として大きいため、y=xはできますがその逆はできません。
ただし、nullチェックの後であれば、nullの可能性が除外されているため、x=yを行うことができます。

NullSample.dart
import "dart:math" as math;
class NullSample {
  method2() {
    int x = 20; // nullではない
    int? y = getNullable();; // Nullの可能性がある
    // NonNullableにキャストして代入する。
    // ただし、yにnullが入った状態で行うとエラーになるので注意
    x = y!;
    print("x: " + x.toString() + " y: " + y.toString());
  }
  getNullable() {
    var rand = new math.Random();
    if (rand.nextInt(2) == 0) {
      return null;
    }
    return 1;
  }
}
result2.sh
I/flutter ( 7785): x: 1 y: 1(1が返る場合)

════════ Exception caught by gesture library ═══════════════════════════════════
type 'Null' is not a subtype of type 'String' of 'function result'
════════════════════════════════════════════════════════════════════════════════
 (Nullが返る場合)

NullSafetyへの移行

Flutter1のNon-NullsafetyからFlutter2のNullSafetyに移行するためのコマンドが準備されています。

◆◆注意◆◆
移行作業で依存パッケージやソースコードが大幅に変わる可能性があります。
必ず前の状態に戻せるようにしてから作業を開始してください。
◆◆◆◆◆◆

まずは、パッケージを更新します

cmd.sh
dart pub upgrade --null-safety 
dart pub get.

続いて、マイグレーションのコマンドを実行します

cmd.sh
dart migrate

利用するパッケージが完全にNullSafeになっていない場合は、--skip-import-checkを付け対象から外す必要があります。

cmd.ch
$ dart migrate --skip-import-check
Migrating /Users/kazutxt/flutter_project/hello_world
See https://dart.dev/go/null-safety-migration for a migration guide.
Analyzing project...
[-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------/]Warning: package has unmigrated dependencies.
Continuing due to the presence of `--skip-import-check`. To see a complete
list of the unmigrated dependencies, re-run without the `--skip-import-check`
flag.
No analysis issues found.
Generating migration suggestions...
[------------------------------------------------------------------------------------------------------------------------------------------------------------------------------]
Compiling instrumentation information...
[------------------------------------------------------------------------------------------------------------------------------------------------------------------------------]
View the migration suggestions by visiting:
 http://127.0.0.1:54015/Users/kazutxt/flutter_project/hello_world?authToken=CrAcMcPavdg%3D
Use this interactive web view to review, improve, or apply the results.
When finished with the preview, hit ctrl-c to terminate this process.
If you make edits outside of the web view (in your IDE), use the 'Rerun from
sources' action.
Applying migration suggestions to disk...
Migrated 8 files:
  test_driver/integration_test.dart
  test/widget_test.dart
  lib/calc.dart
  lib/main.dart
  lib/generated_plugin_registrant.dart
  lib/nullsample.dart
  pubspec.yaml
  .dart_tool/package_config.json

表示されているURLに接続します。

Webページを開くと、どこをどのように直すべきかが表示されています。

チェックをした上で、問題がなければ、右上の「APPLY MIGRATION」をするとソースコードを修正してくれます。

NullSafetyはSDKを2.12以上にする必要がありますが、migrateを実行するとpubspec.ymlの該当箇所も自動で修正されます