LoginSignup
22
29

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-02-07

はじめに

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_cdline_nameがリストがある形です。

ここで重要なのはpart 'station.g.dart';を追加するのと、各クラス内にfromJsontoJsonを追加することです。
まだエラーにはなってしまいますが、後々必要なため例のような感じでクラス名の部分だけ変えて追加しておいてください。

ここまで出来たらAndroid StudioのTerminalから以下のコマンドを打ち込みます。

flutter packages pub run build_runner build

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

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

main.dartを実装してAPIを通信を実施する。

ここではいくつかに分けて実装を見ていきます。

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

httpがなければpubspec.yamlhttpを追加しておいてください。

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

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

スクリーンショット 2019-02-07 22.02.10.png

おわりに

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

22
29
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
29