🏣

【Flutter】郵便番号ガチャアプリを5日で作る

2024/12/25に公開

はじめに

東海オンエアの『【まさかの東北】第1回!日本0円帰宅!!』をみたときに、ランダムで郵便番号を出すアプリ作りたいなぁ〜、となんとなく思っていました。
https://youtu.be/b7iWY4-5KoQ?si=zh3L_MPMDqEp8swL
https://youtu.be/LJkEiQ26Ii4?si=sKs6Mee58na3xnOA

ということで、重い腰を上げて作ってみたいと思います。

使用技術とデータ

  • Flutter
    • freezed
    • csv ← 初使用
  • Google Maps Platform ← 初使用
    • Places API
  • 郵便番号データ(1レコード1行、UTF-8形式) by 日本郵便

選んだ理由(と個人的感想)

Flutter

Flutterは2021年の10月(多分)ぐらいから使っていたため、今回も引き続き採用しました。1年ぐらいあまり触れていなかったので、Flutter(Dart)の進化を感じました。

Google Maps Platform

郵便番号をランダムで出したからには、地図を表示したいなぁということで採用しました。
郵便番号 to 座標データは今回の中では山場でしたが、Google Maps Platformは一発で解決してくれるみたいです。

郵便番号データ

郵便番号データは、日本郵便が配布しているCSV形式のデータをダウンロードして使いました。
https://www.post.japanpost.jp/zipcode/download.html

作ったアプリ

まず、実際に作ったアプリはこちらです。
ランダムな郵便番号を表示するホーム画面と郵便番号を検索する検索画面があります。
以下からダウンロードできますので、使ってみてください。
https://apps.apple.com/jp/app/郵便番号ガチャ/id6739616914
https://play.google.com/store/apps/details?id=com.cavezonex.postal_code

どうつくった?

ここからは、実際にアプリを開発した流れを軽く記録します。

0. アプリの要件を決める

まず、イメージレベルでアプリの要件を考えました。(適当です)

  1. 郵便番号データはCSV形式(オフライン)
  2. 郵便番号はドラムロール形式で表示
  3. 住所、マップはボタンを押してから表示
  4. 広告は入れない(いらない)

郵便番号データはCSV形式(オフライン)

日本郵便で提供されているデータをなるべくそのまま使いたい、という願望からCSV形式のデータをバンドルすることにしました。
クラウドのデータベースに格納することも考えましたが、頻繁に更新されるものではないこと、最新のデータである必要性があまりないことから、そのままアプリにつっこみました。アプリアップデートのついでに、データを更新する方針で運用したいと思います。

郵便番号はドラムロール形式で表示

ワクワク感がほしいので、郵便番号は後ろから順々に表示していこうと決めました。

住所、マップはボタンを押してから表示

いきなり住所やマップが表示されると「どこだ!?」の余韻がないかなということで、ワンアクション挟みました。

広告は入れない(いらない)

広告、いらないですよね。特に、使う人にメリットは、、、ないですね。
この規模だと、開発側にもメリットないです(数円程)。過去に作ったアプリでは広告入れていましたが、それも練習でという気持ちでした(過去作アプリでも順次広告機能を削除します)。

1. 郵便番号データを整える

次に、ダウンロードした郵便番号データを整えます。
ダウンロードしたデータは以下のような列で構成されています。

この郵便番号データファイルでは、以下の順に配列しています。

  1. 全国地方公共団体コード(JIS X0401、X0402)……… 半角数字
  2. (旧)郵便番号(5桁)……………………………………… 半角数字
  3. 郵便番号(7桁)……………………………………… 半角数字
  4. 都道府県名 ………… 全角カタカナ(コード順に掲載) (※1)
  5. 市区町村名 ………… 全角カタカナ(コード順に掲載) (※1)
  6. 町域名 ……………… 全角カタカナ(五十音順に掲載) (※1)
  7. 都道府県名 ………… 漢字(コード順に掲載) (※1,2)
  8. 市区町村名 ………… 漢字(コード順に掲載) (※1,2)
  9. 町域名 ……………… 漢字(五十音順に掲載) (※1,2)
  10. 一町域が二以上の郵便番号で表される場合の表示 (※3) (「1」は該当、「0」は該当せず)
  11. 小字毎に番地が起番されている町域の表示 (※4) (「1」は該当、「0」は該当せず)
  12. 丁目を有する町域の場合の表示 (「1」は該当、「0」は該当せず)
  13. 一つの郵便番号で二以上の町域を表す場合の表示 (※5) (「1」は該当、「0」は該当せず)
  14. 更新の表示(※6)(「0」は変更なし、「1」は変更あり、「2」廃止(廃止データのみ使用))
  15. 変更理由 (「0」は変更なし、「1」市政・区政・町政・分区・政令指定都市施行、「2」住居表示の実施、「3」区画整理、「4」郵便区調整等、「5」訂正、「6」廃止(廃止データのみ使用))

郵便番号データ(1レコード1行、UTF-8形式)の説明より

今回は、最小限の「3, 4, 5, 6」のデータのみを抽出したCSVをPythonで生成しました。
後述する、FlutterでのCSV読み込み問題もここで解決しています。

データ整形python
import csv
import pandas as pd

csv_header = ['area_code','old_postal_code','postal_code','prefecture_kana','city_kana','town_kana','prefecture','city','town','multi_code','koaza','chou','multi_area','update','update_reason']

# csvファイルを読みこむ(str型で読み込む)
df = pd.read_csv('utf_ken_all.csv', dtype=str, names=csv_header)

# 最新のデータのみを抽出
df_latest = df[df['update'] == '0']

# カラムを絞る
df_latest = df_latest.drop(columns=[
    'area_code',
    'old_postal_code',
    'prefecture_kana',
    'city_kana',
    'town_kana',
    'multi_code',
    'koaza',
    'chou',
    'multi_area',
    'update',
    'update_reason',
    ])

df_latest.to_csv('postal_code.csv', encoding='UTF-8',lineterminator='¥n',header=False,quoting=csv.QUOTE_ALL)

2. Flutterプロジェクトを作成

次に、Flutterでの開発に入ります。
このあたりは、よくある工程なので、サクッと書きます。

2-1. プロジェクト構成

プロジェクトの構成は以下のようにしました。今までの自分のプロジェクトを踏襲した感じです。

lib以下
lib
 ├ consts -- 定数とか
 ├ freezed -- freezed関連のファイル
 ├ pages -- 各ページのファイル
 ├ routes -- ルーティング関連(画面遷移)
 ├ utils -- 共通機能
 ├ widgets -- 共通部品ウィジェット
 ┗ main.dart

共通ウィジェットとかはかなり流用しました。(AppBar, drawerなど)

2-2. CSV関連(郵便番号)機能の実装

前提として、郵便番号データのクラスをfreezedで生成しています。

  1. csvパッケージをインストール(pub add派)
flutter pub add csv
  1. CSVの読み込み
load_csv.dart
// CSVの読み込み
final input = await rootBundle.loadString('assets/data.csv');
List<List<dynamic>> postalCodeData =
    const CsvToListConverter().convert(input, eol: '¥n');

ポイントは、rootBundleassetsファイルを読み込むという点、CSV読み込み時の引数で改行コードを指定する(eol: '¥n')点です。特に後者は、デフォルトでは全レコードを1行として読み込んでしまい躓きポイントでした。Pythonでのデータ生成時とFlutterでのデータ読み込み時の両方で、改行コードを指定することで解決しました。

  1. ランダムの郵便番号を抽出
    単純にCSVデータの長さを、Random().nextInt(length)の引数に渡して抽出しています。

2-3. GoogleMap機能の実装

  1. GoogleMap, geocoding パッケージをインストール(pub add派)
flutter pub add google_maps_flutter
flutter pub add geocoding
  1. 郵便番号から座標を取得
final locationList = await locationFromAddress(zipCode);
final location = locationList.first;

geocodingのlocationFromAddressでLocation型のリストを取得できます。今回は一番最初の候補のみ採用します。
3. マップの表示部

// マップコントローラ
final Completer<GoogleMapController> _controller =
    Completer<GoogleMapController>();
// マップウィジェット
GoogleMap(
    mapType: MapType.normal,
    markers: {Marker(markerId: MarkerId('point'), position: LatLng(35.697486343031, 139.76158283538))},
    initialCameraPosition: CameraPosition(
    zoom: 8,
    target: LatLng(35.697486343031, 139.76158283538),
    ),
    onMapCreated: (GoogleMapController controller) {
    _controller.complete(controller);
    },
);

リファレンス通りの実装だと思います。なお、GoogleMapウィジェットはExpandedとかに入れないとだめっぽいです。

実装時のポイントとしてはこのぐらいでした。他にも気づき次第追記したいと思います。

まとめ

今回は、妥協仕様で制作しましたが、考えることや躓きポイントが少なくて良かったです。今後の個人開発も、このぐらいの感覚でやっていければと思います。
一応定石通りにビルド、AppStoreとGooglePlayへのリリースまで行いましたので、触ってみてご意見などいただけるとありがたいです。
課題としては、Android版のGoogleMapがうまく動いてません。ちょっとずつ、いつか直します。
https://apps.apple.com/jp/app/郵便番号ガチャ/id6739616914
https://play.google.com/store/apps/details?id=com.cavezonex.postal_code

GitHubで編集を提案

Discussion