FlutterでAPI通信してレスポンス結果のJsonをデコードして結果を表示させたよ

# はじめに
FlutterのDartファイルからAPI通信をしてJsonをさばいてみました。

# 環境
Android Studio
Flutter1.0

# API通信先
今回は駅データさんの都道府県APIを使用させていただきました。

http://www.ekidata.jp/api/api_pref.php

# Jsonをデコードする前段階
今回はAPI通信でJsonを扱うため、Jsonのライブラリを使用します。
色々試したのですが、公式でも紹介されている[json_serializable](https://pub.dartlang.org/packages/json_serializable)を使用することにします。

ライブラリを使用するため`pubspec.yaml`に必要なモジュールを追加します。

``` yaml
dependencies:
  json_annotation: ^2.0.0
dev_dependencies:
  build_runner: ^1.0.0
  json_serializable: ^2.0.0
```

また、rootフォルダに`build.yaml`ファイル作成して以下を追加します。

``` 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
```

追加したらまずはデコード先のデータクラスを作成しておきます。

``` Dart
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`から以下のコマンドを打ち込みます。

``` sh
flutter packages pub run build_runner build
```

これは`build_runner`を実行するコマンドになっており、今回はアノテーションを解析後にJsonファイルをデコードとエンコードするメソッドを自動生成して`station.g.dart`を生成してくれます。
この作業は該当となるファイルを変更したら毎回実行する必要がある点にご注意ください。
ファイル変更を検知する`watch`機能もあるようですが、自分はなるべく実行癖をつけるために今は使用してません。

これにて前段階の準備は完了です。

# main.dartを実装してAPIを通信を実施する。
ここではいくつかに分けて実装を見ていきます。

まずはimport部分です。
必要なパッケージとデータクラスを読み込みます。

`http`がなければ`pubspec.yaml`に`http`を追加しておいてください。

``` yaml
dependencies:
  http: ^0.12.0+1
```

``` Dart
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

import 'station.dart';
```

次に`Widget`の共通部です。
特に言うことはありません。

``` Dart
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`するだけです。
昔はもうちょっと色々やらないとダメだったようですが、今は仕組みさえ分かれば簡単に出来ちゃいます。


``` Dart
    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()`をコールすることが必須となっているため、それプラスして自分のやりたい処理を追加します。

``` Dart
  @override
  void initState() {
    super.initState();
    _refresh();
  }
```

あと、通信が非同期のため何も対策を講じなければnull参照で落ちてしまいます。
なので、通信結果を入れる変数がnullだった場合に処理中であることが分かるように`CircularProgressIndicator`を用意しておきます。
通信完了後は`ListView`を表示するよう分岐させます。

``` Dart
  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`メソッドを実装して出来た全量がこちらになります。

``` Dart
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,
      ),
    );
  }
}
```

実行したら無事表示されて更新も出来ました。

<img width="287" alt="スクリーンショット 2019-02-07 22.02.10.png" src="https://qiita-image-store.s3.amazonaws.com/0/71288/24ea139d-8b1d-fdb5-ab6d-00e168106208.png">

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