Chapter 06

Step5: データモデルのCityクラスを作成

ツルオカ
ツルオカ
2023.12.01に更新

本章のトピック

Step4では非同期処理で RESAS から取得したデータを jsonDecode を用いて Json パースし、その値をアプリで表示していました。本章では City というモデルを作成し、取得したデータを構造化し扱いやすい形に書き換えます。

  • Classes | Dart を使って市区町村データを構造化する
  • enum を使って市区町村の種類を列挙

Cityモデルの作成

Dart におけるモデルは単なるクラスでデータ構造を表現するのに役立ちます。クラスについては以下をご参照ください。
https://dart.dev/language/classes

こちらもわかりやすいです。
https://medium.com/@deepak.devflutter/model-class-in-flutter-for-beginners-924317c56a17

例えば、市区町村の一覧を取得する API では以下がデータとして取得できますが、これらを City クラスとしてまとめて定義しておくイメージです。モデル定義がなされていないと、データを取り出す時に毎回"cityName"というプロパティ名を指定して JSON から取得する必要がありまた、本来1つの塊として扱われるデータなので、塊として定義しておくのが望ましいです。

{
    "prefCode": 1, # 都道府県コード
    "cityCode": "01100", # 市区町村コード
    "cityName": "札幌市", # 市区町村名
    "bigCityFlag": "2" # 特別区・行政区フラグ(0:一般の市区町村、1:政令指定都市の区、2:政令指定都市の市、3:東京都23区)
},

定義は簡単です。まず、city フォルダ直下に city.dart を作成し、以下を記述します。細かい解説は割愛しますが、上記で言及した RESAS API で取得できるデータの Key をそのままフィールド名として定義しています(prefCode など)。

class City {
  City({
    required this.prefCode,
    required this.cityCode,
    required this.cityName,
    required this.bigCityFlag,
  });

  // 今回の肝で、JSONを引数に取り中身をそれぞれ展開しCityクラスに変換して返却しています。
  factory City.fromJson(Map<String, dynamic> json) {
    return City(
      prefCode: json['prefCode'] as int,
      cityCode: json['cityCode'] as String,
      cityName: json['cityName'] as String,
      bigCityFlag: json['bigCityFlag'] as String,
    );
  }

  int prefCode;
  String cityCode;
  String cityName;
  String bigCityFlag;
}

次に list_page.dart の jsonDecode 部分に以下を追記し、cities で書き換えます。

list_page.dart
    final json = jsonDecode(snapshot.data!)['result'] as List;
    final items = json.cast<Map<String, dynamic>>();
+    final cities = items.map(City.fromJson).toList();
    return ListView.builder(
-      itemCount: items.length,
+      itemCount: cities.length,
      itemBuilder: (context, index) {
-        final item = items[index];
+        final city = cities[index];
        return ListTile(
-          title: Text(item['cityName'] as String),
+          title: Text(city.cityName),
          subtitle: const Text('政令指定都市'),
          trailing: const Icon(Icons.navigate_next),
          onTap: () {
            Navigator.of(context).push<void>(
              MaterialPageRoute(
                builder: (context) => CityDetailPage(
-                  city: item['cityName'] as String,
+                  city: city.cityName,
                ),
              ),
            );
          },
        );
      },
    );

City モデルの cityName は String(文字列)で扱うことがすでに定義されているので、as String のような変換をわざわざする必要がなくなります。同様に、subtitle に bigCityFlag を指定してみましょう。

list_page.dart
      return ListTile(
          title: Text(city.cityName),
-          subtitle: const Text('政令指定都市'),
+          subtitle: Text(city.bigCityFlag),
          trailing: const Icon(Icons.navigate_next),
        );

このように City モデルであらかじめ定義したフィールドはピリオドで繋いで取得できます。ここまでできると、以下のような画面となるはずです。

Cityモデルでパースした表示

enumを使って市区町村の種類を管理する

前のステップで、subtitle に city.bigCityFlag を指定した影響で、政令指定都市で固定されていたテキストが数字に変わってしまいました。改めてこの bigCityFlag を RESAS API のドキュメントで確認してみましょう。

ドキュメントに記載の通り、この値はフラグとなっており数値と特別区/行政区が1対1で管理されているようです。例えば、"0"の場合は一般市区町村、といった具合です。

この数値を元に if 文や switch 文を使って分岐してもよいのですが、こういった網羅性が必要な値を管理するには enum を利用するのがおすすめです。
https://dart.dev/language/enums

city フォルダ直下に city_type.dart を作成し以下を書きます。

city/city_type.dart
enum CityType {
  /// 一般の市区町村
  general('一般の市区町村'),

  /// 政令指定都市の区
  designatedWard('政令指定都市の区'),

  /// 政令指定都市の市
  designatedCity('政令指定都市の市'),

  /// 東京都23区
  designatedTokyoWard('東京都23区'),
  ;

  const CityType(this.label);
  final String label;
}

これで CityType という型で"general"は一般の市区町村といった具合の対応関係を作ることができました。

次に city.dart を書き換えます。bigCityFlag は数字しか情報をもっていなかったですが、先ほど enum で定義した CityType に帰ることで、対応関係を持つ型にできました。あわせて命名も適切な形で cityType に変更しました。

city.dart
+ import 'city_type.dart';

class City {
  City({
    required this.prefCode,
    required this.cityCode,
    required this.cityName,
-    required this.bigCityFlag,
+    required this.cityType,
  });

  factory City.fromJson(Map<String, dynamic> json) {
    return City(
      prefCode: json['prefCode'] as int,
      cityCode: json['cityCode'] as String,
      cityName: json['cityName'] as String,
-      bigCityFlag: json['bigCityFlag'] as String,
+      cityType: CityType.values[int.parse(json['bigCityFlag'] as String)],
    );
  }

  int prefCode;
  String cityCode;
  String cityName;
-  String bigCityFlag;
+  CityType cityType;


  // ref. https://dart.dev/language/operators
  
  bool operator ==(Object other) {
    return other is City &&
        other.prefCode == prefCode &&
        other.cityCode == cityCode &&
        other.cityName == cityName &&
-        other.bigCityFlag == bigCityFlag;
+        other.cityType == cityType;
  }

  
  int get hashCode;
}

最後に、list_page.dart の subtitle を修正します。

city/list_page.dart
    ListTile(
      title: Text(city.cityName),
-      subtitle: Text(city.bigCityFlag),
+      subtitle: Text(city.cityType.label),
      trailing: const Icon(Icons.navigate_next),
    );

以下の表示となるはずです。

CityTypeを使って行政区を適切化

良いですね。元々"政令指定都市"とベタ書きだったものが、RESAS のデータに差し替わって数値となり、今回の実装で適切な文字列として表示されました。市区町村の名前と subtitle で指定した特別区・行政区の表示がしっかりあっていることがわかります。

これで市区町村の一覧画面の実装は完了です。

本章は以上です。