Flutter 🇯🇵
Published in

Flutter 🇯🇵

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

Photo by Nick Fewings on Unsplash
environment:
sdk: ">=2.12.0 <3.0.0" // 最低でも2.12.0以上

null safetyの扱いの基本原則

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

Text(
'foo',
style: Theme.of(context).textTheme.headline4.copyWith(
color: Colors.red,
),
),
Text(
'foo',
style: Theme.of(context).textTheme.headline4?.copyWith(
color: Colors.red,
),
),
Text(
'foo',
style: Theme.of(context).textTheme.headline4!.copyWith(
color: Colors.red,
),
),
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(),
);
}
}
void f() {
navigatorKey.currentState?.pushNamed('foo');
}
void f() {
navigatorKey.currentState!.pushNamed('foo');
}

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

void f() {
final navigator = navigatorKey.currentState;
if (navigator == null) {
// エラーログを吐く
return;
}
navigator.pushNamed('foo');
}
void f() {
final navigator = navigatorKey.currentState;
if (navigator == null) {
throw TypeError(); // 厳密にはprivateの `_CastError()`
}
navigator.pushNamed('foo');
}
void f() {
final navigator = navigatorKey.currentState;
if (navigator != null) {
navigator.pushNamed('Foo');
}
}
final text2 = text ?? ''; // textはString?型

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!;
});
},
);
}
}
tristate: trueでvalueがnullの時の見た目
if (value != null) {
isChecked = value;
}

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

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

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

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);
}
}
https://dart.dev/tools/non-promotion-reasons#property-or-this
Widget build(BuildContext context) {
return text == null ? const SizedBox.shrink() : Text(text!);
}
Widget build(BuildContext context) {
final text = this.text;
return text == null ? const SizedBox.shrink() : Text(text);
}
// ここだけ、フィールドアクセスを区別するため thisを付ける
final text = this.text;
// 以降、`text`はローカル変数で、あえて `this.text`とした時のみフィールドアクセスになる(普通はしない)
Widget build(BuildContext context) {
final _text = text;
// 以降気をつけて `_text` の方にアクセスするように書く
return _text == null ? const SizedBox.shrink() : Text(_text);
}
Widget build(BuildContext context) {
// textがnullの時、nullアクセスエラー発生
return text == null ? Text(text!) : const SizedBox.shrink();
}

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の活用

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('🎅'),
);
}
}
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();

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

nullableなFunction型の呼び出し

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();
void f() {
final bar = Bar();
bar.call();
bar();
}

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

late finalでのフィールドの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"
}
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パッケージの利用

void main() {  
final values = <int>[];
// 空配列なのでnoElementエラー発生
print(values.first);
// 同じ処理
print(values[0]);
}
void main() {  
final values = <int>[];
if (values.isNotEmpty) {
print(values.first); // この例の場合は空配列なので実行されない
}
}
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);
}
import 'package:collection/collection.dart';void main() {
final values = <int>[];
// int?型
final value = values.firstOrNull;
if (value != null) {
// int型
print(value);
}
}
// こうではなく、
T get first {}
// こうあるべきでは?
T? get first {}
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!;
}
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!!
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>();
}

Stream/rxdart 事情

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

--

--

Flutterに関する日本語記事を書いていきます🇯🇵

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store