(当頁はdocs.flutter.dev(https://docs.flutter.dev/cookbook/networking/background-parsing)のリファレンスブログです。)
1.概要
(1)Dart アプリのデフォルト動作
Dart アプリはデフォルトで シングルスレッド で動作します。
多くの場合、シンプルなモデルであり、十分高速であるため、アプリのパフォーマンスが低下したり、アニメーションがカクついたりすること(俗に “jank” と呼ばれる)はありません。
(2)問題の発生
ただし、非常に大きな JSON ドキュメントを解析する など、計算コストが高い作業が必要な場合があります。
もしこの作業が 16ミリ秒以上 かかると、アニメーションや画面のスムーズな動作が妨げられ、ユーザーに “jank” を感じさせることになります。
(3)解決方法
“jank” を防ぐためには、このような高負荷な計算を バックグラウンド で行う必要があります。
(Android では、この作業を別スレッドにスケジュールします。)
Flutter では、Isolate を使用して、別のプロセスで計算を行います。
(4)このレシピでの手順
(ⅰ)http パッケージを追加
ネットワークリクエストを処理するために http パッケージを利用します。
(ⅱ)ネットワークリクエストを送信
http パッケージを使ってサーバから JSON データを取得します。
(ⅲ)レスポンスを写真リストに変換
サーバから受け取った JSON データを解析し、カスタムモデル(この例では写真のリスト)に変換します。
(ⅳ)バックグラウンド処理に移動
この重い処理を Isolate に移動し、メインスレッドの負担を軽減します。
(5)ポイント
- Flutter の Isolate を使うことで、計算の重いタスクをメインスレッドから切り離して実行可能。
- これにより、スムーズな UI パフォーマンスを維持できます。
詳細なコード例や手順に関しては、以下の各ステップで具体的な実装方法が解説されています。
このアプローチを使用することで、アプリのスムーズな操作感を確保しながら、高負荷なタスクを処理できます。
2.http パッケージの追加
flutter pub add http
3.ネットワークリクエストを行う
次の例では、JSONPlaceholder REST API を利用して、5千枚の写真を含む大規模な JSONドキュメントを取得します。
//fetchPhotos 関数定義:
//( 非同期に HTTP GET リクエスト を送信し、レスポンスを返します。
// 引数: http.Client ( 関数の引数として受け取ります。)
// ( このようにすることで、次の利点があります:
// ・テストの容易性 ( テスト環境では、http.Client をモックに差し替えることで、
// ネットワークに依存しないテストが可能。)
// ・柔軟性 ( 他の環境(例:開発用サーバーや本番環境)でも、同じ関数を使い回せます。))
// 戻り値: client.get() の結果である http.Response を返します。
Future<http.Response> fetchPhotos(http.Client client) async {
return client.get(
Uri.parse('https://jsonplaceholder.typicode.com/photos')
);
}
(重要なポイント)
1.柔軟性の向上:
関数が http.Client を受け取ることで、実際の HTTP クライアントやモッククライアントを使う等、異なる環境で簡単に使用できます。
2.テスト性の向上:
モッククライアントを使えば、実際にネットワークに接続せずに関数をテスト可能。
4.JSONを解析して写真のリストに変換する
HTTP レスポンスで得られた JSON データ を Dart オブジェクトのリスト に変換することで、データを扱い易くする。
(1)写真クラスの作成
(Photo クラスの役割)
- 各写真データ(albumId、id、title、url、thumbnailUrl)を格納する。
- JSON オブジェクトから Photo インスタンスを簡単に生成できる fromJson ファクトリメソッド を持つ。
class Photo {
final int albumId; //アルバム ID
final int id; //写真 ID
final String title; //写真タイトル
final String url; //写真の URL
final String thumbnailUrl; //サムネイルの URL
const Photo({
required this.albumId,
required this.id,
required this.title,
required this.url,
required this.thumbnailUrl,
});
//Photo.fromJson() ファクトリメソッド:
//JSON オブジェクトを Photo インスタンスに変換するファクトリメソッド
factory Photo.fromJson(Map<String, dynamic> json) {
return Photo(
albumId: json['albumId'] as int,
id: json['id'] as int,
title: json['title'] as String,
url: json['url'] as String,
thumbnailUrl: json['thumbnailUrl'] as String,
);
}
}
(2)レスポンス(JSON)を写真のリストに変換する
HTTP レスポンスの JSON 本文(responseBody)を Dart のリスト形式に変換します。
List<Photo> parsePhotos(String responseBody) {
//JSON を解析して List<Map<String, dynamic>> にキャストする
final parsed = (
jsonDecode(responseBody) as List
).cast<Map<String, dynamic>>();
//各 JSON オブジェクトを Photo インスタンスに変換
return parsed.map<Photo>((json) => Photo.fromJson(json)).toList();
}
(3)fetchPhotos 関数を更新する
(fetchPhotos 関数)
- http.get() を使用して写真データを取得。
- parsePhotos 関数 を使って JSON を Dart のリスト(List)に変換。
Future<List<Photo>> fetchPhotos(http.Client client) async {
//http.Client.get():
//( 写真データを取得。
// HTTP GET リクエストを送信してレスポンスを取得 )
final response = await client.get(
Uri.parse('https://jsonplaceholder.typicode.com/photos'),
);
//parsePhotos 関数:
//( JSON を Dart のリスト(List<Photo>)に変換。
// レスポンスの本文を parsePhotos 関数で解析して写真リストを返す。)
return parsePhotos(response.body);
}
(コードの流れ)
- ネットワークリクエスト:
- fetchPhotos 関数が http.get を使用して JSONPlaceholder API からデータを取得。
- fetchPhotos 関数が http.get を使用して JSONPlaceholder API からデータを取得。
- JSON の解析:
- parsePhotos 関数 でレスポンスボディを解析し、JSON オブジェクトを Photo インスタンス に変換。
- parsePhotos 関数 でレスポンスボディを解析し、JSON オブジェクトを Photo インスタンス に変換。
- 写真リストの返却:
- fetchPhotos 関数 は List を返す。
上記迄の処理により、取得した大規模 JSON データも効率的に解析し、アプリで簡単に扱える形式に整えられるようになります。
5.上記処理を別の Isolate に移動
(背景)
- fetchPhotos() 関数は大量の JSON データを取得して解析します。
- 低速なデバイスで実行すると、JSON の解析と Dart オブジェクトへの変換に時間がかかり、アプリが一時的にフリーズ(「ジャンク」)する場合があります。
(解決策)
compute() 関数 を使用して、この重い処理をバックグラウンドの Isolate(スレッドのようなもの)に移動します。
(Isolate と compute() 関数)
- Isolate:
- Dart は単一スレッドで動作する仕組みを採用しています。
- Isolate は Dart における軽量スレッドのようなもの。
(メインスレッドとは独立した処理を行い、メインスレッドに影響を与えません。)
- compute() 関数:
- 高コストな関数をバックグラウンドの Isolate で実行し、処理結果をメインスレッドに返すための Flutter が提供する便利な関数です。
- 引数:
- 実行したい関数(parsePhotos など)。
- その関数に渡すデータ(response.body など)。
(コードの変更)
以下のコードは、compute() を使用して JSON の解析と変換をバックグラウンドで行う例です。
import 'package:flutter/foundation.dart'; //compute() 関数を使うために必要
//(1)メインスレッドで fetchPhotos を呼び出す。
Future<List<Photo>> fetchPhotos(http.Client client) async {
final response = await client.get(
Uri.parse('https://jsonplaceholder.typicode.com/photos'),
);
//(2)compute():
//( JSON 解析をバックグラウンドの Isolate で実行。
// compute(parsePhotos, response.body) を実行すると、
// Flutter は別の Isolate を起動する。)
return compute(parsePhotos, response.body);
}
//(3)parsePhotos 関数:
//( JSON 解析と Photo オブジェクトへの変換を行う。
// バックグラウンド Isolateで parsePhotos 関数が実行され、
// JSON を解析して List<Photo> を生成。)
List<Photo> parsePhotos(String responseBody) {
final parsed =
(jsonDecode(responseBody) as List).cast<Map<String, dynamic>>();
//(4)処理が完了すると、結果の List<Photo> がメインスレッドに返される。
return parsed.map<Photo>((json) => Photo.fromJson(json)).toList();
}
(利点)
- パフォーマンス向上
- JSON の解析と Dart オブジェクトへの変換がバックグラウンドで実行されるため、メインスレッドで UI の描画がブロックされません。
- これにより、アプリの動作がスムーズになり、ユーザー体験が向上します。
- 簡単な実装
- Flutter の compute() 関数を利用するだけで、複雑なスレッド管理をせずに並列処理を実現できます。
(注意点)
- 引数と戻り値の制限
- compute() に渡す引数と戻り値は、シリアライズ可能(JSON に変換可能な型)である必要があります。
- 例えば、String や Map などは問題ありませんが、File やソケットなどの非シリアライズ型は使用できません。
- バックグラウンド処理のオーバーヘッド:
- Isolate を起動するには少し時間がかかるため、処理が軽い場合はかえって遅くなる可能性があります。
- このアプローチは、大規模な JSON データや計算量が多い処理に適しています。
6.Isolate を使用する際の注意点
(1)Isolate のメッセージ通信
- Isolate は完全に独立しており、共有メモリを持ちません。(その為、メッセージのやり取りによってデータを共有します。)
- メッセージとして渡せるデータには制限があります。
(2)渡せるデータ
以下の様なシリアライズ可能(JSON に変換可能)なプリミティブ型や簡単なデータ構造を使用できます。
- プリミティブ型
- null
- num(整数や浮動小数点数)
- bool(真偽値)
- String(文字列)
- 単純なオブジェクト
- (例:このコードの List の様な、シリアライズ可能な型を持つリスト。)
(3)渡せないデータ
次の様な複雑なオブジェクトを渡そうとすると、エラーが発生する可能性があります。
(これらは Dart のシリアライザが対応していないため、エラーの原因になります。)
- 非シリアライズ型のオブジェクト
- Future(非同期処理)
- http.Response(HTTPレスポンスオブジェクト)
(4)代替ソリューション
もし Isolate を使うことが複雑すぎたり、制約が大きい場合、以下のパッケージを検討して下さい。
(a)worker_manager パッケージ
- バックグラウンドでの処理を簡単に管理できます。
- 長時間実行するタスクや複数の並列タスクに適しています。
(b)workmanager パッケージ
- 定期的なバックグラウンドタスクをスケジュールするために使用されます。
- 特に Android と iOS のネイティブタスクスケジューリングに対応しています。
(5)まとめ
- Isolate を使う際には、渡すデータがシリアライズ可能かどうかに注意してください。
- もし制限が厳しい場合や、より便利な方法を探している場合は、worker_manager や workmanager パッケージの利用を検討して下さい。これらは、バックグラウンド処理をより簡単に実現する手段を提供します。
7.サンプルコード
docs.flutter.dev掲載のサンプルコードを実行すると、次の(問題)が発生しました。
(問題)
デバッグモードで実行後、初期画面は表示され、スクロールすると、複数の画像が、順次表示されたのですが、
2回ほど、スクロールした後、画面が反応しなくなりました。
(対応)
dispose(メモリ解放)を追加してあります。
import 'dart:async';
import 'dart:convert'; //JSON データを扱うためのライブラリ
import 'package:flutter/foundation.dart'; //compute 関数を使用するために必要
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; //HTTP リクエスト用ライブラリ
//fetchPhotos 関数:
//( ネットワークから JSON データを取得して解析する関数
// (サーバーから JSON データを取得し、compute を使って別の Isolate でデータを解析します。
// JSON データを Dart の Photo リストに変換します。))
Future<List<Photo>> fetchPhotos(http.Client client) async {
final response = await client
.get(Uri.parse('https://jsonplaceholder.typicode.com/photos'));
//compute 関数:
//( JSON の解析処理を別の Isolate(スレッド)で実行。
// (JSON 解析をバックグラウンドで処理することで、アプリのメインスレッドでの「カクつき」を防ぎます。))
return compute(parsePhotos, response.body);
}
//JSON データを Dart のオブジェクト(List<Photo>)に変換する関数
List<Photo> parsePhotos(String responseBody) {
//JSON データを List<Map<String, dynamic>> 型に変換
final parsed =
(jsonDecode(responseBody) as List).cast<Map<String, dynamic>>();
//各 JSON オブジェクトを Photo インスタンスに変換
return parsed.map<Photo>((json) => Photo.fromJson(json)).toList();
}
//Photo クラス:
//( JSON データをマッピングするモデルクラス
// (JSON データの構造を表現するためのモデルクラスです。
// fromJson を使って JSON を Dart オブジェクトに変換します。))
class Photo {
final int albumId; //アルバムID
final int id; //写真ID
final String title; //写真タイトル
final String url; //写真URL
final String thumbnailUrl; //サムネイルURL
//コンストラクタ
const Photo({
required this.albumId,
required this.id,
required this.title,
required this.url,
required this.thumbnailUrl,
});
//JSON データを Dart オブジェクトに変換するファクトリコンストラクタ
factory Photo.fromJson(Map<String, dynamic> json) {
return Photo(
albumId: json['albumId'] as int,
id: json['id'] as int,
title: json['title'] as String,
url: json['url'] as String,
thumbnailUrl: json['thumbnailUrl'] as String,
);
}
}
//アプリのエントリーポイント
void main() => runApp(const MyApp());
//アプリのルートウィジェット
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
const appTitle = 'Isolate Demo'; //アプリのタイトル
return const MaterialApp(
title: appTitle,
home: MyHomePage(title: appTitle), //ホーム画面に遷移
);
}
}
//ホーム画面のステートフルウィジェット
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
//ホーム画面の状態を管理するクラス
class _MyHomePageState extends State<MyHomePage> {
//非同期で取得する写真のリスト
late Future<List<Photo>> futurePhotos;
//追加) http.Client をフィールドで管理
final http.Client client = http.Client();
@override
void initState() {
super.initState();
//アプリ起動時に写真データを取得
futurePhotos = fetchPhotos(http.Client());
}
//追加) HTTP クライアントのクリーンアップ
@override
void dispose() {
client.close(); //HTTP クライアントのクリーンアップ
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title), //アプリバーにタイトルを表示
),
//FutureBuilder ウィジェット:
//( 非同期データの取得状態に応じて画面を更新します。
// データ取得中はローディングスピナーを表示し、エラー時やデータ取得成功時にそれぞれの画面を表示します。)
body: FutureBuilder<List<Photo>>(
future: futurePhotos, //写真データの取得状態を監視
builder: (context, snapshot) {
if (snapshot.hasError) {
//エラーが発生した場合の処理
return const Center(
child: Text('An error has occurred!'),
);
} else if (snapshot.hasData) {
//データが正常に取得された場合の処理
return PhotosList(photos: snapshot.data!);
} else {
//データ取得中はローディングスピナーを表示
return const Center(
child: CircularProgressIndicator(),
);
}
},
),
);
}
}
//PhotosList ウィジェット:
//( 写真リストを表示する StatelessWidget
// (取得した写真をグリッドレイアウトで表示します。
// サムネイル画像をネットワーク経由で取得して表示します。))
class PhotosList extends StatelessWidget {
const PhotosList({super.key, required this.photos});
final List<Photo> photos; //表示する写真のリスト
@override
Widget build(BuildContext context) {
return GridView.builder(
//グリッド表示を設定
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, //列数を2列に設定
),
itemCount: photos.length, //写真の数
itemBuilder: (context, index) {
//各写真をサムネイルとして表示
return Image.network(photos[index].thumbnailUrl);
},
);
}
}
(スクロール途中で固まらなくなりました)