はじめに
FlutterのDartファイルからAPI通信をしてJsonをさばいてみました。
環境
Android Studio
Flutter1.0
API通信先
今回は駅データさんの都道府県APIを使用させていただきました。
Jsonをデコードする前段階
今回はAPI通信でJsonを扱うため、Jsonのライブラリを使用します。
色々試したのですが、公式でも紹介されているjson_serializableを使用することにします。
ライブラリを使用するためpubspec.yaml
に必要なモジュールを追加します。
dependencies:
json_annotation: ^2.0.0
dev_dependencies:
build_runner: ^1.0.0
json_serializable: ^2.0.0
また、rootフォルダにbuild.yaml
ファイル作成して以下を追加します。
targets:
$default:
builders:
json_serializable:
options:
# Options configure how source code is generated for every
# `@JsonSerializable`-annotated class in the package.
#
# The default value for each is listed.
#
# For usage information, reference the corresponding field in
# `JsonSerializableGenerator`.
any_map: false
checked: false
create_factory: true
create_to_json: true
disallow_unrecognized_keys: false
explicit_to_json: false
field_rename: none
generate_to_json_function: true
include_if_null: true
nullable: true
use_wrappers: false
追加したらまずはデコード先のデータクラスを作成しておきます。
import 'package:json_annotation/json_annotation.dart';
part 'station.g.dart';
@JsonSerializable(nullable: false)
class Station {
@JsonKey(name: 'line', nullable: false)
final List<Line> line;
Station(this.line);
factory Station.fromJson(Map<String, dynamic> json) => _$StationFromJson(json);
Map<String, dynamic> toJson() => _$StationToJson(this);
}
@JsonSerializable(nullable: false)
class Line {
@JsonKey(name: 'line_cd', nullable: false)
final int lineCd;
@JsonKey(name : 'line_name', nullable: false)
final String lineName;
Line(this.lineCd, this.lineName);
factory Line.fromJson(Map<String, dynamic> json) => _$LineFromJson(json);
Map<String, dynamic> toJson() => _$LineToJson(this);
}
ここで作成するクラスは駅データさんのAPI仕様にのっとった形です。
line
の下にline_cd
とline_name
がリストがある形です。
ここで重要なのはpart 'station.g.dart';
を追加するのと、各クラス内にfromJson
とtoJson
を追加することです。
まだエラーにはなってしまいますが、後々必要なため例のような感じでクラス名の部分だけ変えて追加しておいてください。
ここまで出来たらAndroid StudioのTerminal
から以下のコマンドを打ち込みます。
flutter packages pub run build_runner build
これはbuild_runner
を実行するコマンドになっており、今回はアノテーションを解析後にJsonファイルをデコードとエンコードするメソッドを自動生成してstation.g.dart
を生成してくれます。
この作業は該当となるファイルを変更したら毎回実行する必要がある点にご注意ください。
ファイル変更を検知するwatch
機能もあるようですが、自分はなるべく実行癖をつけるために今は使用してません。
これにて前段階の準備は完了です。
main.dartを実装してAPIを通信を実施する。
ここではいくつかに分けて実装を見ていきます。
まずはimport部分です。
必要なパッケージとデータクラスを読み込みます。
http
がなければpubspec.yaml
にhttp
を追加しておいてください。
dependencies:
http: ^0.12.0+1
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'station.dart';
次にWidget
の共通部です。
特に言うことはありません。
void main(){
runApp(RootWidget());
}
class RootWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomeWidget(),
);
}
}
class HomeWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return ListState();
}
}
最後にAPI通信を行なってAPI通信のレスポンスをリスト表示するState
部分です。
今回はPull to refreshで県データを更新する(最大値は考慮しない)ようにもしています。
実際にAPI通信をする部分についてはrequestStationData()
に実装しています。
通信は非同期となるのと、Pull to refshするのはいいけど、初期表示する際にも通信してデータ持ってこないと困るので、別メソッドに切り出しています。
なので、Pull to refreshを実現するための_refresh()
はそのメソッドを呼ぶだけのラッパーになっています。
通信して状態変更している部分のコードを抜き出すとこんな感じです。
Json戻ってくる中に余計な文字があってので、substring
するといういけてないコードが混じっているのはご勘弁ください。
Jsonデータのデコードは簡単でdart:convert
内にあるjsonDecode(String)
をコールして出来たMap<String, dynamic>
をデコード先のクラスに作ったfromJson
するだけです。
昔はもうちょっと色々やらないとダメだったようですが、今は仕組みさえ分かれば簡単に出来ちゃいます。
http.get(url).then((response) {
var body = response.body.substring(50, response.body.length - 58);
var mapStation = jsonDecode(body);
setState(() {
station = Station.fromJson(mapStation);
});
});
また、上記で初期でも通信と書きましたが、Widget
生成時に処理を追加したい場合はinitState()
をoverride
して実装してあげてください。
これはsuper.initState()
をコールすることが必須となっているため、それプラスして自分のやりたい処理を追加します。
@override
void initState() {
super.initState();
_refresh();
}
あと、通信が非同期のため何も対策を講じなければnull参照で落ちてしまいます。
なので、通信結果を入れる変数がnullだった場合に処理中であることが分かるようにCircularProgressIndicator
を用意しておきます。
通信完了後はListView
を表示するよう分岐させます。
Widget _getCardChild() {
if(station == null) {
return Center(
child: CircularProgressIndicator(),
);
} else {
return ListView.builder(
itemBuilder: (BuildContext context, int index) {
return Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Colors.black38),
),
),
child: ListTile(
title: Text(station.line[index].lineCd.toString()),
subtitle: Text(station.line[index].lineName),
),
);},
itemCount: station.line.length,
);
}
}
あとはbuild
メソッドを実装して出来た全量がこちらになります。
class ListState extends State<HomeWidget> {
var index = 0;
Station station;
Future<void> _refresh() async {
await Future.sync(() {
requestStationData();
});
}
void requestStationData() {
index++;
var url = 'http://www.ekidata.jp/api/p/' + index.toString() + '.json';
http.get(url).then((response) {
var body = response.body.substring(50, response.body.length - 58);
var mapStation = jsonDecode(body);
setState(() {
station = Station.fromJson(mapStation);
});
});
}
@override
void initState() {
super.initState();
_refresh();
}
Widget _getCardChild() {
if(station == null) {
return Center(
child: CircularProgressIndicator(),
);
} else {
return ListView.builder(
itemBuilder: (BuildContext context, int index) {
return Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Colors.black38),
),
),
child: ListTile(
title: Text(station.line[index].lineCd.toString()),
subtitle: Text(station.line[index].lineName),
),
);},
itemCount: station.line.length,
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('List Test')),
body: RefreshIndicator(
child: _getCardChild(),
onRefresh: _refresh,
),
);
}
}
実行したら無事表示されて更新も出来ました。

おわりに
Dartの仕様が分からず苦戦しましたが、分かってみると簡単に通信した結果を表示することが出来ました。こんな感じでがりがりと書いていけばアプリが出来てしまうので、分かってくると楽しいですね。