null safety article
Contents
Fetch data from the internet
ほとんどのアプリでは、インターネットからデータを取得する必要があります。幸いなことに、DartとFlutterは、このタイプの作業用のパッケージとしてhttpパッケージなどのツールを提供しています 。
このレシピでは、次の手順を使用します。
http
パッケージを追加します。http
パッケージを使用してネットワークリクエストを行います。- レスポンス(応答)をカスタムDartオブジェクトに変換します。
- Flutterでデータを取得して表示します。
1. Add the http
package
このhttp
パッケージは、インターネットからデータをフェッチする最も簡単な方法を提供します。
http
パッケージをインストールするには、pubspec.yaml
ファイルの依存関係セクションにパッケージを追加します。http
パッケージの最新バージョンは pub.devにあります。
dependencies:
http: <latest_version>
httpパッケージをインポートします。
//main.dartなど
import 'package:http/http.dart' as http;
さらに、AndroidManifest.xmlファイルにインターネット権限を追加します。
<!-- Required to fetch data from the internet. -->
<uses-permission android:name="android.permission.INTERNET" />
2. Make a network request
このレシピでは、http.get()メソッドを使用してJSONPlaceholderからサンプルアルバムをフェッチする方法について説明します。
Future<http.Response> fetchAlbum() {
return http.get(Uri.https('jsonplaceholder.typicode.com', 'albums/1'));
}
http.get()メソッドはFuture<http.Response>型の返り値を返します。
Future
は、非同期操作を操作するためのコアDartクラスです。Futureオブジェクトは、将来のある時点で利用可能になる可能性のある値またはエラーを表します。- この
http.Response
クラスには、成功したhttp呼び出し(call)から受信したデータが含まれています。
3. Convert the response into a custom Dart object
ネットワークリクエストを行うのは簡単ですが、受け取ったFuture<http.Response>
をそのまま使用するのはあまり便利ではありません。手間を少なくするためhttp.Response
型をDartオブジェクト(モデルクラス)に変換します。
Create an Album
class
まず、ネットワークリクエストからのデータを受け取るためのAlbumクラスを作成します。これには、JSONからAlbumインスタンスを作成するファクトリ(factory)コンストラクターが含まれています。
JSONを手動で変換することは1つのオプションにすぎません。
詳細については、JSON & serializationに関する記事全文を参照してください 。
class Album { final int userId; final int id; final String title; Album({ required this.userId, required this.id, required this.title, }); factory Album.fromJson(Map<String, dynamic> json) { return Album( userId: json['userId'], id: json['id'], title: json['title'], ); } }
Convert the http.Response
to an Album
ここで、次の手順を使用してfetchAlbum()
関数を修正して、Future<Album>
を返すようにします。
dart:convertパッケージを使って、レスポンスのbody(ボディ)をJSON Mapに変換します。
サーバーがステータスコード200のOKレスポンスを返してきた場合(通信が成功してデータ(JSON Map)を受け取ることができた場合)、JSON Mapから、fromJson()(factoryコンストラクタ)を使ってAlbum型インスタンスを生成します。
サーバーがステータスコード200のOK応答を返さない場合は、例外をスローします。「404NotFound」サーバー応答の場合でも、例外をスローします。
nullを返さないでください。これは、この後説明するsnapshotでデータを調べる時に重要なことです。
import 'dart:convert';
Future<Album> fetchAlbum() async {
//↓リクエストを送ってレスポンスを受け取る非同期メソッドhttp.get()
//awaitキーワードを使って非同期処理が完了するまで(あるいはエラーが来るまで)待つ。
final response = await http.get('https://jsonplaceholder.typicode.com/albums/1');
if (response.statusCode == 200) {
// If the server did return a 200 OK response,
// then parse the JSON.
//↓サーバーが200 OK レスポンスを返してきた場合は、JSONをパースする。
return Album.fromJson(jsonDecode(response.body));
} else {
// If the server did not return a 200 OK response,
// then throw an exception.
//↓200 OK レスポンスが返ってこなかった場合は例外をスローする。
throw Exception('Failed to load album');
}
}
やりましたね。インターネットからアルバムをfetch(フェッチ)する関数を定義することができました。
4. Fetch the data
fetchAlbum()メソッドをinitState()メソッド、あるいはdidChangeDependencies()メソッド内のどちらかで呼び出します(実行します)。
このinitState()
メソッドは一度だけ呼び出され、その後は二度と呼び出されません。
InheritedWidget
が保持する状態変数の変更に応じてAPIをリロードするオプションが必要な場合は 、didChangeDependencies()
メソッド内でfetchAlbum()メソッドを呼び出します。
さらに詳しい情報はStateクラスをご覧ください。
class _MyAppState extends State<MyApp> {
Future<Album> futureAlbum;
@override
void initState() {
super.initState();
futureAlbum = fetchAlbum();
}
5. Display the data
画面にデータを表示するには、FutureBuilder
ウィジェットを使用します。
FutureBuilder
ウィジェットはFlutterフレームワークに用意されているウィジェットで、非同期のデータソースを簡単に扱うことができます。
FutureBuilderコンストラクタには二つの引数を渡す必要があります。
- 扱いたいFutureインスタンス。今回はfetchAlbum()メソッドの返り値のfuture。
- Flutterに対して何をレンダーすべきかを示すbuilder関数。builder関数内でFutureから受け取る値を使います。
snapshotがnullでない値を持っている場合のみ、snapshot.hasDataはtrueを返します。
(= 404の時にfetchAlbumメソッドがnullを返すようにしてしまうと、snapshot.hasDataはfalseを返す。その時に一つ下のサンプルのような条件分岐にすると、404の時いつまでもスピナーが回り続けることになる。だから一つ前のサンプルコードで、404の時もfetchAlbumメソッドが例外をスローするようにしている。)
Because fetchAlbum
can only return non-null values, the function should throw an exception even in the case of a “404 Not Found” server response.
fetchAlbumメソッドはnullでない値のみを返します。ですから、fetchAlbumメソッドは、サーバーレスポンスが“404 Not Found”の時も例外をスローする必要があります。(そのように実装した。)
Throwing an exception sets the snapshot.hasError
to true
which can be used to display an error message.
例外がスローされた時は、snapshot.hasErrorにtrueがセットされます。その結果(画面に)エラーメッセージが表示されます。
Otherwise, the spinner will be displayed.
それ以外の場合はスピナー(CircularProgressIndicator)が表示されます。
サーバーレスポンスが”404 Not Found”の場合も例外をスローすべき理由がこれです。(2021/8/19、削除されていることを確認)
(レスポンスが404の時)fetchAlbumがnullを返した場合、スピナー(プログレスインジケータ)はいつまでも回り続けることになります。(2021/8/19、削除されていることを確認)
上記の説明の通り、”404 Not Found”の時にfetchAlbumメソッドがnullを返した場合、snapshot.hasDataがfalseになり、snapshot.hasErrorもfalseになるので、ifブロック、if-elseブロックは無視され、スピナーが表示される(いつまでも)。
FutureBuilder<Album>(
future: futureAlbum,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data!.title);
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
// By default, show a loading spinner.
return CircularProgressIndicator();
},
);
Why is fetchAlbum() called in initState()?
buildメソッド内でAPIコールを呼び出す(fetchAlbumメソッドを呼び出す)ことは便利ですが、お勧め出来ません。
Flutterは、ビュー内(画面表示)の何かを変更する必要があるたびにbuild()メソッドを呼び出しますが、これは驚くほど頻繁に発生します。
fetchメソッドをbuild()メソッド内に置くと、不要なAPIの呼び出しで溢れ、アプリの速度が低下します。
Testing
今回のこの機能をテストする方法については、次のレシピを参照してください。
Complete example
import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; Future<Album> fetchAlbum() async { final response = await http .get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1')); if (response.statusCode == 200) { // If the server did return a 200 OK response, // then parse the JSON. return Album.fromJson(jsonDecode(response.body)); } else { // If the server did not return a 200 OK response, // then throw an exception. throw Exception('Failed to load album'); } } class Album { final int userId; final int id; final String title; Album({ required this.userId, required this.id, required this.title, }); factory Album.fromJson(Map<String, dynamic> json) { return Album( userId: json['userId'], id: json['id'], title: json['title'], ); } } void main() => runApp(const MyApp()); class MyApp extends StatefulWidget { const MyApp({Key? key}) : super(key: key); @override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State<MyApp> { late Future<Album> futureAlbum; @override void initState() { super.initState(); futureAlbum = fetchAlbum(); } @override Widget build(BuildContext context) { return MaterialApp( title: 'Fetch Data Example', theme: ThemeData( primarySwatch: Colors.blue, ), home: Scaffold( appBar: AppBar( title: const Text('Fetch Data Example'), ), body: Center( child: FutureBuilder<Album>( future: futureAlbum, builder: (context, snapshot) { if (snapshot.hasData) { return Text(snapshot.data!.title); } else if (snapshot.hasError) { return Text('${snapshot.error}'); } // By default, show a loading spinner. return const CircularProgressIndicator(); }, ), ), ), ); } }
fetchAlbumメソッド内にprint文を追加して、型変換部分の型を確認する。
Future<Album> fetchAlbum() async { final response = await http.get(Uri.https('jsonplaceholder.typicode.com', 'albums/1')); print(response); print(response.runtimeType); print(response.body.runtimeType); print(response.body); print(jsonDecode(response.body).runtimeType); if (response.statusCode == 200) { return Album.fromJson(jsonDecode(response.body)); } else { // If the server did not return a 200 OK response, // then throw an exception. throw Exception('Failed to load album'); } }
//結果 flutter: Instance of 'Response' ←print(response); flutter: Response ←print(response.runtimeType); flutter: String ←print(response.body.runtimeType); flutter: { ←print(response.body); "userId": 1, "id": 1, "title": "quidem molestiae enim" } flutter: _InternalLinkedHashMap<String, dynamic> ↑print(jsonDecode(response.body).runtimeType);
参考