Flutterエンジニアが知っておくべき「Zone」で出来る3つのこと
はじめに
こんにちは、株式会社メンバーズクロスアプリケーションカンパニーの阿瀬川です。
皆さんは、DartにおけるZone(ゾーン)という概念を知っていますか?
Zoneという概念は、書籍や技術記事に取り上げられることが少ないため、知らない人が多いですが、使いこなすことができればFlutterエンジニアにとって非常に強力な武器となります。
本記事では、Zoneを使うことで出来る代表的な3つのことを、Dart初心者にも理解しやすいように解説します。
Zoneは「安全な空間」のイメージ
Zoneとは「非同期呼び出し全体で安定した状態を維持する環境」であると、Flutter公式ドキュメントにおけるZone classで記載されています。
以下の図のように、安全な空間(Zone)を展開して、内部で非同期処理を動かすイメージを持ってください。
Zoneを使うことで出来る3つのこと
Zoneで出来ることは様々ですが、以下の3つに分類することができます。
どんなエラーが発生してもハンドリングできる
Dartの基盤となる特定の機能を上書きできる
引数指定しなくてもアクセスできる共有データを作れる
次の章よりコードを交えて説明していきます。
Zoneを実装するには、runZonedGuardedとrunZoned
の関数を使います。実装する処理によって、どちらかの関数を選択して使います。
runZonedGuarded(() {
// 非同期処理を呼び出す
});
runZoned(() {
// 非同期処理を呼び出す
});どんなエラーが発生してもハンドリングできる
Zoneで発生したエラーは必ずハンドリングすることができ、Zoneの外部に影響することはありません。そのため、アプリの予期せぬエラーでクラッシュすることを防ぐことができます。
こちらのコードを見てください。例えば、noErrorHandling()という第三者が作った関数があるとします。
この関数は、本来であればエラーハンドリングしてあるべきですが、第三者は忘れています。
しかし、noErrorHandling()の呼び出しをZoneで行うことで、エラーを必ずハンドリングすることができます。
runZonedGuarded(() {
// 第三者が作ったメソッドを呼び出す場合
noErrorHandling();
}, (error, stackTrace) {
// エラーは、最終的にここに流れ着く
print("Zoneで捕捉: $error");
});
Future<void> noErrorHandling() {
throw Exception("エラー");
// エラーハンドリング忘れてた!
}この処理であれば、try-catch文で実装可能だと考えるかもしれませんが、非同期処理を扱う場合、それは不可能です。以下のコードを動かしてみてください。
runZonedGuarded(() {
try {
// 処理の予約だけして即座にブロックを抜ける
Future.delayed(Duration(seconds: 1), () {
throw Exception("1秒後にエラー");
});
} catch (e) {
// この処理は行われない
// エラー発生時には、既にこのtry-catchブロックは終了している
}
}, (error, stackTrace) {
// エラーは、最終的にこのブロックに流れ着く
print("Zoneで捕捉: $error");
});try-catch文の中で、非同期処理であるFuture.delayedが呼び出されています。
この処理は、1秒後にExceptionを投げるため、処理の予約だけして即座にtryブロックを抜けます。
その結果catchブロックには辿り着けません。これが、非同期処理でZoneが必要な理由です。
次に実際の現場で用いられる使われ方を紹介します。
第二引数のエラーハンドリングを行う箇所で、errorとstackTrace(エラーが発生するまでの関数の呼び出し履歴)を受け取ることができます。
現場ではこれらをAPM(アプリケーションパフォーマンス管理)に渡し活用します。
APMとは、アプリをリリースした後にユーザーが遭遇したエラーを開発者に自動で通報するシステムです。
Zoneを使い、予期しないエラーを一箇所で一元管理することで、APMという便利なツールを使うことができます。
void main() {
runZonedGuarded(() {
// 非同期でエラーが発生
}, (error, stackTrace) {
// APMにerrorとstackTraceを渡す処理を記述する
});
}Dartの基盤となる特定の機能を上書きできる
Zone内で使われる、Dartの基盤となる特定の機能(printやTimer)を上書きすることができます。
例えば、全ての非同期処理における標準出力のprint()を上書きして、「何も処理をしない」という挙動にすることができます。本番環境のアプリでは標準出力を使わないため、何も処理をしないことで動作を軽くすることができます。
実装には、runZonedとZoneSpecification
を使用します。
ZoneSpecificationの引数にて、上書き内容を記述することができます。以下のコードを見てください。6行目で何も処理を書かないことで、print()を無効化することができます。
5行目に複数の引数が登場していますが、これは複雑な上書き処理を記述をするためにあります。本記事では、詳細を割愛します。
runZoned(() {
print("ユーザーのログインに失敗しました。");
},
zoneSpecification: ZoneSpecification(
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
// 何もしない
},
),
);引数指定しなくてもアクセスできる共有データを作れる
Zoneでは、引数指定しなくてもアクセスできる共有データを作ることができます。
実装には、runZonedとzoneValuesを使用します。zoneValuesを使うことで、各Zone内だけで使えるMap型の値を保存できます。
以下のコードを見てください。user_idに引数なしでアクセスすることができます。
これにより、関数の中で関数を呼び出し、引数のやり取りを何度も行うバケツリレーの問題を解消することができます。
runZoned(() {
printUserId();
}, zoneValues: {#user_id: "taro_tanaka_012345"});
void printUserId() {
// 引数なしでデータにアクセスできる
final String userId = Zone.current[#user_id];
print("現在のユーザーは$userIdです");
}ただ、zoneValuesに関してはアプリ開発で直接書くことは少ないかもしれません。しかし、外部パッケージやフレームワークの裏側では頻繁に使われているため、知っておくことは高度な設計をする上で大切になります。
まとめ
DartにおけるZoneで出来ることを、3つに分類して紹介しました。
正直な話、1つ目のエラーハンドリングは見かけたり実装することがあるかもしれませんが、Dartの基盤となる特定の機能の上書きと共有データの作成は使う機会が少ないかもしれません。
しかし、あなたが毎日使っているパッケージの裏側では、必ずと言っていい程、このZoneが「安全」と「便利」を支えています。
本記事はZoneの入門を目的としています。この機会にZoneについて深く調べて、より堅牢で簡潔なアプリ開発を目指しましょう。
What is BEMA!?
Be Engineer, More Agile


