Dart/Flutterのnull safety対応ベストプラクティス

Flutter Advent Calendar 2021 の12月24日枠

mono 
Flutter 🇯🇵

--

Photo by Nick Fewings on Unsplash

Dartは、2021年3月リリースのバージョン2.12にてnull safety対応されました。同時期にFlutter 2.0もリリースされ、それにDart 2.12が同梱されました(2021年12月現在の最新はFlutter 2.8.1/Dart 2.15.1)。

そこからしばらく経ちましたが、自分が書いてきた経験や巷のコード眺めて感じたことを踏まえて、ベターなnull safetyの扱いについて書いていきます。

前提として、null safety を有効にするには、 pubspec.yaml にて以下のように指定する必要があります。

environment:
sdk: ">=2.12.0 <3.0.0" // 最低でも2.12.0以上

下限は、可能な範囲上げておくと最新の言語機能を使えて良いです。例えばFlutter stable最新を使うポリシーの場合、2021年12月現在ではFlutter 2.8.1に同梱されているDartバージョンが2.15系( flutter doctor -v で確認可能)なので >=2.15.0 あたりにするのがお勧めです。以下などから最新版の機能をキャッチアップしてどんどん活用していきましょう🎯

null safetyの扱いの基本原則

まず大事なのは、! (非nullであることのassert)を、過不足なく使うことです。無闇に乱用するのも、逆に過度に避けすぎるのもどちらも良くないです。この記事では、これについて具体例を交えながら丁寧に説明していきます。

nullでは無いことが確信的な場合は積極的に `!` する

まず、リリース前に必ず修正する前提でのプログラミングミスでしかnullになり得ない想定の場合は、基本的には分岐対応などせずに ! だけで済ませる方がむしろ良いです。

例えば、次の headline4 の型は TextStyle? なので、それに対して、 .copyWith() は呼べずコンパイルエラーになります。

Text(
'foo',
style: Theme.of(context).textTheme.headline4.copyWith(
color: Colors.red,
),
),

よく見かける「安全」な対応は以下のように ?. とすることで、コンパイルが通るようにして、さらにそれがnullだった場合でもエラーにならずに済むようになる、というものです。

Text(
'foo',
style: Theme.of(context).textTheme.headline4?.copyWith(
color: Colors.red,
),
),

しかし、普通に扱っていれば headline4 は必ず非nullである類のものですし、仮にnullだった場合に、Textの style がnullであるのは通常意図してないはずです。もし仮にThemeの設定ミスなどでnullになってしまった時にエラーが発生して気付けるような以下の書き方が健全です。

Text(
'foo',
style: Theme.of(context).textTheme.headline4!.copyWith(
color: Colors.red,
),
),

また、上記コードはnull safety前の Theme.of(context).textTheme.headline4.copyWith() と全く同じ動作になります。この時も厳密にはnullになり得ましたが、 ?. アクセスなどせずに当たり前のように . でアクセスしていたはずです。つまり、ここでの !. は以前と比べて危険なnullアクセスが増えたわけではなく、過度に避ける必要は全くありません。逆に、 ?. などの濫用は例外処理の握り潰しと同様の行為です。

似たような別の例としては、以下のような navigatorKey がある時、

final navigatorKey = GlobalKey<NavigatorState>();class App extends StatelessWidget {
const App({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: navigatorKey,
home: const HomePage(),
);
}
}

次のように ?. してしまうと、 navigatorKey の設定ミス(MaterialApp に渡し忘れなど)で本当にnullになってしまった時に何も起こらなくなり、エラーなども起こらないためその原因も分かりにくくなります。

void f() {
navigatorKey.currentState?.pushNamed('foo');
}

これも、基本的には同様に以下のように ! アクセスするのが良いです。

void f() {
navigatorKey.currentState!.pushNamed('foo');
}

もしnullだった場合はエラーが発生して、原因箇所がすぐに分かります。先ほどの例と同様、これもnull safety前に普通に書いてたコードの挙動と一緒で、この場合 .? アクセスした方が本当に適したケースの方が稀です。

Flutterでは未キャッチのエラーが発生してもクラッシュしないこともポイント

上のコード例ではnullアクセスエラーは基本的にはリリース前に気付ける類のもの(プログラミングミス)ですが、稀に気付けずにそのミスを含んだ状態でリリースしてしまうこともあり得ます。Flutterではその場合でも、クラッシュはせずエラーが発生するだけです(グローバルエラーハンドリングでfirebase_crashlyticsのrecordErrorメソッドを呼ぶなどしておけば、そのミスに気付いて次のアップデートで速やかな修正がしやすくなります)。

nullアクセスでクラッシュしてしまうフレームワークの場合、次のように絶対にそうならないようにすることが合理的な面もあります(クラッシュせずに動き続けることが必ずしも正義ではないがなるべく動き続けてほしい前提の場合)。

void f() {
final navigator = navigatorKey.currentState;
if (navigator == null) {
// エラーログを吐く
return;
}
navigator.pushNamed('foo');
}

しかし、Flutterの場合は未キャッチのエラーが発生してもクラッシュしない(exit(1)を明示的に呼べばクラッシュするようにもできる)ので、わざわざ上のような冗長なコードを書かずとも ! アクセスで済ませるだけで同等です。

また、 navigatorKey.currentState!.pushNamed('Foo'); は以下相当です。

void f() {
final navigator = navigatorKey.currentState;
if (navigator == null) {
throw TypeError(); // 厳密にはprivateの `_CastError()`
}
navigator.pushNamed('foo');
}

ユーザー体験的にも、nullアクセスエラーが発生して裏側でエラーが起こった場合も、 ?.if 分岐などで弾いた場合でも、「クラッシュせずそこで処理が中断されて何も起こらない」ということで一緒です。コードや内部動作的には ! の方がnullアクセスエラーが発生してくれて分かりやすく、単純にその分優位です。

また、 ?. アクセスは以下のようなコードに相当します。nullの時に本当に何も起こらないべきという明確な意志があるならこれで良いですが、状況によってnullになることを想定しているのでなければミスに気付きにくくなる弊害だけ抱えてしまうことになります。

void f() {
final navigator = navigatorKey.currentState;
if (navigator != null) {
navigator.pushNamed('Foo');
}
}

?. を使うのは基本的に、nullの場合が普通に想定できて、かつわざわざ分岐せずにどちらの場合も同じように扱った方が都合が良い時に限定した方が良いです。

?? も同様で、例えば String? text が渡ってきた時に、以下のように空文字で済ませてしまうのは本当にそれがベストかどうかの判断が必要です。その文脈ではnullが来ない想定なら text!; としてしまった方が良いこともよくあります。

final text2 = text ?? ''; // textはString?型

Checkbox Widgetの例

もう1つ実用的な例として、Checkbox Widgetを扱う場合について触れます。

class _CheckBox extends StatefulWidget {
const _CheckBox({Key? key}) : super(key: key);

@override
State<_CheckBox> createState() => _CheckBoxState();
}

class _CheckBoxState extends State<_CheckBox> {
bool isChecked = false;

@override
Widget build(BuildContext context) {
return Checkbox(
value: isChecked,
onChanged: (bool? value) {
setState(() {
isChecked = value!;
});
},
);
}
}

チェックボックス状態が変わる時に onChanged が呼ばれますが、なぜかその値が bool? 型です。上記の場合は、 value! とするのが正解です。なぜなら、nullが来るのは tristate が trueの時のみ(もちろんドキュメントに記載してある)で、それがデフォルトのfalseになっている上記の場合はnullが来ることはあり得ないからです。

tristate: trueでvalueがnullの時の見た目

万が一、誤解やバグなどでnullが来たとしても、いずれにせよ想定してないnullが来た時に正常動作するように前もって書く術は無いのでどうしようもありません。また、Checkbox Widgetドキュメント記載のサンプルも ! アクセスで済ませています。

この例の場合において、念のため「丁寧に」以下のように書いても何の恩恵もないですし、繰り返しになりますが万が一想定外のnull値が来た場合にそれが無視されて原因究明の邪魔になります( ! アクセスしている場合はエラーが発生して容易に気付けます)。

if (value != null) {
isChecked = value;
}

ここまでで、 ! もそれが適した場面なら積極的に使って良いということを述べましたが、次は逆に使い過ぎないようにという観点で述べていきます。

`!` を使うのは最小限にとどめる

! アクセスは、「型的にはnullであることがあり得るが、この文脈ではnullなことはあり得ないとみなす(万が一nullだったらエラーになって後続の処理が実行されないことを許容)」という表明です。

同じアクセスに対して ! などが付いていると、その表明が重複していることになりノイズで、以下の冗談なようなコードと同等とも言えます。

void f(String? text) {
if (text != null) {
// 念のため、もう一度チェック
if (text != null) {
print('length: ${text.length}');
}
}
}

まず、DartではType promotionによって、nullチェックや ! アクセスがあった後のスコープでは非nullに自動的に変わります。

そのため、上記の二重のnullチェックも実際には次の警告が出て、警告に気を付けていればこのような無駄な記述を防げます。

The operand can’t be null, so the condition is always true.

次のようなコードを書いた場合も同様で、 String?型の text! アクセスした時点でそれ以降のスコープではString型であることが保証されます(もしnullだった場合はエラーが発生して後続の処理にたどり着かないので)。

void f(String? text) {
print('length: ${text!.length}'); // textはString?
print('length: ${text!.length}'); // textはString型
}

2回目の text! アクセスで次のような警告が発生します。

The ‘!’ will have no effect because the receiver can’t be null

このように、(警告に気付いて適切に書き換えれば)言語仕様的に無駄な ! アクセスがないようになってそうに一見見えますが、普通に書いててそうではないシーンが実はよくあります。

Type promotionが効かない場合の対処法

次のようなコードは、一見真っ当そうなnull safetyの扱いに見えます。

class MyText extends StatelessWidget {
const MyText({
Key? key,
required this.text,
}) : super(key: key);

final String? text;

@override
Widget build(BuildContext context) {
return text == null ? const SizedBox.shrink() : Text(text);
}
}

しかし、非nullチェックされたスコープであるはずの Text(text) の箇所で次のようなコンパイルエラーが発生します。

The argument type ‘String?’ can’t be assigned to the parameter type ‘String’. (Documentation)
‘text’ refers to a property so it couldn’t be promoted. See http://dart.dev/go/non-promo-property (program.dart:40).

String? 型は String 型のパラメーター(Text Widgetの引数)に渡せない、とのことです。nullチェック後なので、非nullのStringにtype promotionしているはずでは?と思うところですが、2文目に「’text’はプロパティを参照しているので、type promotion対象外です。こちらを参照ください: http://dart.dev/go/non-promo-property 」と書いてあります。最高に親切なエラーメッセージですね( ´・‿・`)👍

https://dart.dev/tools/non-promotion-reasons#property-or-this

他にもいくつか type promotionが効かない例が書かれていますが、今回の例の場合が大半のはずです。

コンパイルエラーの対処として2通りの対処法が書いてあって、1つ目は以下のように ! を付けるものです。せっかくnullチェックした後にまた ! を付ける、一見不自然なコードです。

Widget build(BuildContext context) {
return text == null ? const SizedBox.shrink() : Text(text!);
}

2つ目は、次のように一旦ローカルフィールドに設定し直して、それに対してnullチェック分岐をする、というものです。

Widget build(BuildContext context) {
final text = this.text;
return text == null ? const SizedBox.shrink() : Text(text);
}

なぜクラスフィールドではtype promotionが効かないかの理由は次の回答が分かりやすく、継承関係によってはnullチェック後のアクセスでも容易にnullになるような状況を作り出せてしまうからです。

一旦ローカル変数として保持すればこの抜け穴を回避でき、nullチェック後に非nullであることを保証できるようになります。

多少面倒に見えるかもしれませんが、Dartはフィールド名と同名のローカル変数を宣言でき(シャドーイング)、一旦ローカル変数に置くという記述を無理なくできるため、ちょっと1行挟むだけで他に歪みが出ずに済みます。

// ここだけ、フィールドアクセスを区別するため thisを付ける
final text = this.text;
// 以降、`text`はローカル変数で、あえて `this.text`とした時のみフィールドアクセスになる(普通はしない)

もし同名のローカル変数を宣言できない仕様だったら、例えば次のような書き方にしたりなど悩ましかったですが、そうではなくて良かったです。

Widget build(BuildContext context) {
final _text = text;
// 以降気をつけて `_text` の方にアクセスするように書く
return _text == null ? const SizedBox.shrink() : Text(_text);
}

ドキュメントでは、一応2通りの解がありましたが、堅牢なコードとするには後者のようにローカル変数に当て直す解一択だと思います。

null safetyの恩恵の1つは、真っ当な書き方をしていれば言語サポートによりうっかり意図しないnullアクセスを防げることですが、前者の ! を付けてしまう解はその恩恵を捨てて言語サポートではなく自力で解決するものです。

言語サポートを捨てたコーディングは、例えば次のようにうっかり逆にしてしまうミスが容易に起こり得ます。

Widget build(BuildContext context) {
// textがnullの時、nullアクセスエラー発生
return text == null ? Text(text!) : const SizedBox.shrink();
}

この場合、すぐに気付いて修正してHot Reloadすれば済みますが、例えば既存コードを大きくリファクタリングした際などは間違ったスコープでnullアクセスするような書き換えミスをしてデグレを引き起こしてマイナーな画面では気付くのが遅れてしまうなどのリスクが大きくなります。言語サポートの恩恵を捨てれば捨てるほどコードは壊れやすくなります。たった1行ローカル変数に当て直す処理を書くだけで少しでも堅牢にできるならそうするメリットの方が明らかに上回ると思います。

Type promotionのnull以外の例

ちなみにType promotionはnullに限らず使えて、例えば次のように別の型へのキャスト処理などにも同様に効きます。成功した場合にしかそのスコープに入らないことが保証されるためです。

void f(A a) {
a as B;
// 次のような、別の変数への割り当ては不要
// final b = a as B;

// 以降、aはB型
a.b();
}
void f2(A a) {
if (a is B) {
// このスコープではaはB型
a.b();

}
}
class A {}

class B extends A {
void b() {}
}

late finalの活用

次に late でnullableな型を減らすテクニックです。例えば、AnimationControllerを使ったコードをnull safety以前の書き方のまま書き換えると次のようになります。

class MyAnimation extends StatefulWidget {
const MyAnimation({Key? key}) : super(key: key);
@override
_MyAnimationState createState() => _MyAnimationState();
}

class _MyAnimationState extends State<MyAnimation>
with SingleTickerProviderStateMixin {
AnimationController? _animationController;

@override
void initState() {
super.initState();

_animationController = AnimationController(
vsync: this,
duration: const Duration(seconds: 3),
)..forward();

}

@override
void dispose() {
_animationController!.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _animationController!,
child: Text('🎅'),
);
}
}

初めに呼ばれるinitStateでの代入以降ずっと非nullな_animationControllerが、初期値はnullにせざるを得ないためにnullable型にしたことによって、 !? アクセスなど必要になるのがイマイチですね。

こういう場合、 late final を使うと非nullにできて、さらに意図せず_animationControllerの再代入があった場合はLateInitializationErrorで気づけるようになります。 late を付けると、クラスの生成時に初期化されるのではなく初めて参照されたタイミングで算出されてキャッシュされ、2回目以降のアクセスではその算出済みの値が返るようになります。

class _MyAnimationState extends State<MyAnimation>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController;

@override
void initState() {
super.initState();

_animationController = AnimationController(
vsync: this,
duration: const Duration(seconds: 3),
)..forward();

}
// 略...
}

late については以下のコード例を見ると分かりやすいかと思います。

void main() {
final foo = Foo();
print(foo.time);
print(foo.time); // 上と同じ

final foo2 = Foo2();
print(foo2.time);
print(foo2.time); // 上と同じ
}

class Foo {
late final time = DateTime.now();
}

// late final相当の処理を自前で書いたコード例
class Foo2 {
DateTime? _time;
DateTime get time => _time ??= DateTime.now();
// `??=` は以下と同等
DateTime get time2 {
var time = _time ?? DateTime.now();
_time = time;
return time;
}
}

さらに、次のようにフィールドの宣言にまとめることもできます。そもそも、なぜ元々このように書けなかったというと vsyncthis を渡すためで、クラスのインスタンス生成と同時にはそれができないからです。 late はインスタンス生成後、他から参照されたタイミングで呼ばれてその時には this 参照が可能なのでこのような書き方が可能となります。

class _MyAnimationState extends State<MyAnimation>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController = AnimationController(
vsync: this,
duration: const Duration(seconds: 3),
)..forward();
// 略...
}

個人的には、この方がスッキリしていてかつ堅牢にできて好みです(例えばリファクタリングで移動した際などに値セットする処理を欠損してしまうミスなどがなくなる)が、「他から初めて参照されたタイミングで処理が走る」という挙動に注意です。

例えば、次のようにその変数への参照が無いと全く処理が走らないです。この場合は initState でセットするのが自然な書き方です。使い分けするか、どちらの場合でも問題なく動く initState でセットする方に統一するかは好み次第だと思います(僕は使い分けを好みます)。

class _MyAnimationState extends State<MyAnimation>
with SingleTickerProviderStateMixin {
var _opacity = 0.0;

late final AnimationController _animationController = AnimationController(
vsync: this,
duration: const Duration(seconds: 3),
)

..forward()
..addListener(() {
setState(() {
_opacity = _animationController.value;
});
});

@override
Widget build(BuildContext context) {
// _animationControllerへのアクセスが無い
return Opacity(
opacity: _opacity,
child: Text('🎅'),
);
}
}

nullのスムーズな扱いTips集

個人的にはnull safetyの扱いはSwift・TypeScriptなどで充分慣れていたこともあって、Dartのnull safety対応が来た時も、上述のクラスフィールドのtype promotionが効かないことに初め戸惑った以外は初見ですぐスムーズに扱えました。

ただ、しばらく書いていると細かいところで、やはりDart特有のnullの扱いのコツがいるなと思うことがちょくちょくありました。それらを紹介していきます。

nullableなFunction型の呼び出し

例えば、次のような時に、 onPressed がnullなら何も起こらず何かセットされていたら呼びたいとします。

class Foo extends StatelessWidget {
const Foo({
Key? key,
this.onPressed,
}) : super(key: key);

final VoidCallback? onPressed;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
onPressed?(); // この書き方だとコンパイルエラー
// 他の処理書いたり
},
child: Text('🎅'),
);
}
}

この時の書き方に少し迷いますが、次のように書けます👌

onPressed?.call();

callメソッドはDartクラスで特別な意味合いを持ち、次のようにも使えて、これを稀に利用することもあります(freezedのcopyWithはこのテクニックを応用して実装されています)。

void f() {
final bar = Bar();
bar.call();
bar();
}

class Bar {
void call() {
print('bar');
}
}

late finalでのフィールドのnullableの取り扱いのコツ

late キーワードは、クラス生成時には値未割り当てで、参照タイミングで処理が走るため他のフィールドへのアクセスが可能です。ただ、nullableなフィールドにアクセスするような処理を普通に書くと次のようなコードになりがちです。

class Foo {
Foo({
required this.a,
required this.b,
});
String? a;
String? b;

late final abLength =
a != null && b != null ? '${a!.length}-${b!.length}' : null;

}

void main() {
print(Foo(a: 'merry', b: 'christmas').abLength); // "5-9"
}

Type promotionが効かないパターンで、言語サポートではなく自前判断での脆い ! アクセスの妥協するコードになっていますが、次のように即時関数を使いながら上述のシャドーイング方式で書くとnull安全に書けてお勧めです👍

class Foo {
String? a;
String? b;

late final abLength = () {
final a = this.a;
final b = this.b;
return a != null && b != null ? '${a.length}-${b.length}' : null;
}();
}

collectionパッケージの利用

DartのListの first は、要素数がゼロの時はnoElementエラーが発生してしまいます。

void main() {  
final values = <int>[];
// 空配列なのでnoElementエラー発生
print(values.first);
// 同じ処理
print(values[0]);
}

firstアクセスでエラーが起こらないようにするにはその前にisNotEmptyメソッドなどでチェックする必要があったりしてうっかり意図せずエラーを起こしがちです(先述の通り、もしそうなってもFlutterアプリはクラッシュしないので一部の機能不全程度で済みますが)。

void main() {  
final values = <int>[];
if (values.isNotEmpty) {
print(values.first); // この例の場合は空配列なので実行されない
}
}

そこで、Dartのnull safety対応の一環で、collectionパッケージにはnullable型を扱うのに便利な拡張メソッドが色々追加されました

firstOrNullはその代表例です。

import 'package:collection/collection.dart';void main() {
final values = [1, 2];
print(values.firstOrNull); // 1(int?型)
// values.firstOrNull無しで同等のコード記述
print(values.isNotEmpty ? values.first : null);
print(values.first); // 1(int型)


values.clear();
print(values.firstOrNull); // null(int?型)
// 空配列なのでnoElementエラー発生
print(values.first);
}

また、isNotEmpty後でチェック後にfirstアクセスする場合は本当にチェックされたスコープか気を付けて書く必要がありましたが、 firstOrNull で得たnullable型に対してnullチェックすればそのスコープ内では非nullableにtype promotionされるのでミスを確実に防げるようになります。こういう言語サポートの恩恵を得られるような書き方を心がけるのがベターです👌

import 'package:collection/collection.dart';void main() {
final values = <int>[];
// int?型
final value = values.firstOrNull;
if (value != null) {
// int型
print(value);
}
}

firstOrNull! とするのが適したような場面(要素が空でないことが確信的な文脈)でのみ first を使うのが良いです。

また、以下のようにfirstがnullableを返してfirstOrNull無しが自然なのでは?と疑問が浮かぶかもしれませんが、firstは要素が無かった時にエラーになるようなDartの言語仕様だったので、それを後から変えた場合の破壊的変更の影響は甚大なのでそれはそのまま維持しつつ firstOrNull を足すことで対処したのだと思います。

// こうではなく、
T get first {}
// こうあるべきでは?
T? get first {}

また、Dartのnullable型はその入れ子の表現( int?? のようなもの)がないため、firstとfirstOrNullの2つ用意されているのも合理的な気もします。

import 'package:collection/collection.dart';void main() {
final values = [null, 1];
// 空なのかnull要素なのか不明
final int? value1 = values.firstOrNull;
// null要素があることが分かる
final int? value2 = values.first;
// firstOrNullに`!`を付けるとnullアクセスエラー発生
// firstOrNull単体では空なのかnull要素なのかの区別不可能
final int value3 = values.firstOrNull!;
}

Swiftの場合にはOptionalの入れ子があるためnullableを返す first 1つだけでDartのfirst/fisrtOrNullと同等の使い分けが可能なことと比較すると分かりやすいかもしれません。

let values = [nil, 1]
// 空なのかnull要素なのか不明
let value1: Int?? = values.first // nil
// `!` を付けるとnull要素があることが分かる
let value2: Int? = values.first! // nil
// `!` を2つ付けると要素自体をアンラップ(この場合nullアクセスエラー)
let value3: Int = values.first!!

話は戻って、つまりDartの場合は空の可能性もある文脈でうっかり first と書いてしまうミスは気を付けることで対処するしかないです(もちろん単体テスト書くなどでより堅牢にできます)。

他にnull safetyの一環で追加された拡張メソッド例としては、 List<T?> からnullを除去しつつ List<T> 型に変換してくれるwhereNotNullもとても便利です。

void main() {
final List<int?> values = [1, null, 2];
final Iterable<int> values2 = values.whereNotNull();
final List<int> values3 = values.whereNotNull().toList();
// whereNotNullを使わない場合
// 冗長かつ気を付けながら `!` アクセスすることになりミスしたり壊れやすい
final List<int> values4 =
values.where((e) => e != null).map((e) => e!).toList();
// whereType<T>でもほぼ同様にできるがnullの除去目的ならwhereNotNullの方が適している
final Iterable<int> values5 = values.whereType<int>();
}

これ系のパッケージの選定はきちんとしたものを選ぶのが大事ですが、これはDartチーム製なので大丈夫です👌

また、一般的にcollectionパッケージのような純粋なDart実装のパッケージ(ネイティブ実装を含むプラグインではないもの)では、ビルド時間・アプリサイズ増などのトレードオフも少なく、そういう心配もほとんど不要です(低品質なパッケージを乱用しないような注意は必要ですが)。さらに、collectionパッケージはFlutterフレームワークにも含まれて元々間接的に依存されている公式パッケージ(僕は準標準パッケージと呼んだりしてます)なので、利用するデメリットはゼロに近いです。

そのため、もし他の同等の機能のパッケージ使ってたり自前の拡張メソッド生やしている場合は、collectionパッケージに乗り換えることをお勧めします。

Stream/rxdart 事情

[追記 2022/05/30]
rxdart 0.27.4whereNotNull が追加されました🎉
(以下の記述はしばらくしたら更新します)

List的な概念を非同期処理に当てはめたStreamにも、必然的に同様の拡張メソッドが欲しくなる場面があるものの、標準Stream APIにもrxdartにもwhereNotNullなどは今のところないです。

whereNotNull追加のプルリクエストやそれを実装したrxdart_extはあって一応利用はできますが 👌
個人的にはrxdart_ext使うほどでもないと思って、rxdartwhereType を活用して次のような拡張メソッド生やして済ませています(標準Stream API/rxdartだけで済むようになったら乗り換え予定)。

import 'package:rxdart/rxdart.dart';extension StreamX<T extends Object> on Stream<T?> {
Stream<T> whereNotNull() => whereType();
}

また、null safety関連の最近の変更としては、 rxdartValueStream に対して バージョン0.27.0にて以下のようなnull safety対応の破壊的変更がでなされました。Iterable/Listのfirst/firstOrNullなどのインターフェースに揃えられた感じですね。

  • value: Streamの値が1つも流れていなかったらエラー(以前はnullだった)
  • valueOrNull: Streamの値が1つも流れていなかったらnullが返る(以前はこのgetterは無かった)

以上、つらつらとDart/Flutterのnull safety関連の扱いについて触れました。もう少しTipsなどあった気もするので、思い出したら追記していきます🐶

--

--