【Flutter】郵便番号ガチャアプリを5日で作る
はじめに
東海オンエアの『【まさかの東北】第1回!日本0円帰宅!!』をみたときに、ランダムで郵便番号を出すアプリ作りたいなぁ〜、となんとなく思っていました。
ということで、重い腰を上げて作ってみたいと思います。
使用技術とデータ
- 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形式のデータをダウンロードして使いました。
作ったアプリ
まず、実際に作ったアプリはこちらです。
ランダムな郵便番号を表示するホーム画面と郵便番号を検索する検索画面があります。
以下からダウンロードできますので、使ってみてください。
どうつくった?
ここからは、実際にアプリを開発した流れを軽く記録します。
0. アプリの要件を決める
まず、イメージレベルでアプリの要件を考えました。(適当です)
- 郵便番号データはCSV形式(オフライン)
- 郵便番号はドラムロール形式で表示
- 住所、マップはボタンを押してから表示
- 広告は入れない(いらない)
郵便番号データはCSV形式(オフライン)
日本郵便で提供されているデータをなるべくそのまま使いたい、という願望からCSV形式のデータをバンドルすることにしました。
クラウドのデータベースに格納することも考えましたが、頻繁に更新されるものではないこと、最新のデータである必要性があまりないことから、そのままアプリにつっこみました。アプリアップデートのついでに、データを更新する方針で運用したいと思います。
郵便番号はドラムロール形式で表示
ワクワク感がほしいので、郵便番号は後ろから順々に表示していこうと決めました。
住所、マップはボタンを押してから表示
いきなり住所やマップが表示されると「どこだ!?」の余韻がないかなということで、ワンアクション挟みました。
広告は入れない(いらない)
広告、いらないですよね。特に、使う人にメリットは、、、ないですね。
この規模だと、開発側にもメリットないです(数円程)。過去に作ったアプリでは広告入れていましたが、それも練習でという気持ちでした(過去作アプリでも順次広告機能を削除します)。
1. 郵便番号データを整える
次に、ダウンロードした郵便番号データを整えます。
ダウンロードしたデータは以下のような列で構成されています。
この郵便番号データファイルでは、以下の順に配列しています。
- 全国地方公共団体コード(JIS X0401、X0402)……… 半角数字
- (旧)郵便番号(5桁)……………………………………… 半角数字
- 郵便番号(7桁)……………………………………… 半角数字
- 都道府県名 ………… 全角カタカナ(コード順に掲載) (※1)
- 市区町村名 ………… 全角カタカナ(コード順に掲載) (※1)
- 町域名 ……………… 全角カタカナ(五十音順に掲載) (※1)
- 都道府県名 ………… 漢字(コード順に掲載) (※1,2)
- 市区町村名 ………… 漢字(コード順に掲載) (※1,2)
- 町域名 ……………… 漢字(五十音順に掲載) (※1,2)
- 一町域が二以上の郵便番号で表される場合の表示 (※3) (「1」は該当、「0」は該当せず)
- 小字毎に番地が起番されている町域の表示 (※4) (「1」は該当、「0」は該当せず)
- 丁目を有する町域の場合の表示 (「1」は該当、「0」は該当せず)
- 一つの郵便番号で二以上の町域を表す場合の表示 (※5) (「1」は該当、「0」は該当せず)
- 更新の表示(※6)(「0」は変更なし、「1」は変更あり、「2」廃止(廃止データのみ使用))
- 変更理由 (「0」は変更なし、「1」市政・区政・町政・分区・政令指定都市施行、「2」住居表示の実施、「3」区画整理、「4」郵便区調整等、「5」訂正、「6」廃止(廃止データのみ使用))
今回は、最小限の「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
├ consts -- 定数とか
├ freezed -- freezed関連のファイル
├ pages -- 各ページのファイル
├ routes -- ルーティング関連(画面遷移)
├ utils -- 共通機能
├ widgets -- 共通部品ウィジェット
┗ main.dart
共通ウィジェットとかはかなり流用しました。(AppBar, drawerなど)
2-2. CSV関連(郵便番号)機能の実装
前提として、郵便番号データのクラスをfreezedで生成しています。
- csvパッケージをインストール(
pub add
派)
flutter pub add csv
- CSVの読み込み
// CSVの読み込み
final input = await rootBundle.loadString('assets/data.csv');
List<List<dynamic>> postalCodeData =
const CsvToListConverter().convert(input, eol: '¥n');
ポイントは、rootBundle
でassets
ファイルを読み込むという点、CSV読み込み時の引数で改行コードを指定する(eol: '¥n'
)点です。特に後者は、デフォルトでは全レコードを1行として読み込んでしまい躓きポイントでした。Pythonでのデータ生成時とFlutterでのデータ読み込み時の両方で、改行コードを指定することで解決しました。
- ランダムの郵便番号を抽出
単純にCSVデータの長さを、Random().nextInt(length)
の引数に渡して抽出しています。
2-3. GoogleMap機能の実装
- GoogleMap, geocoding パッケージをインストール(
pub add
派)
flutter pub add google_maps_flutter
flutter pub add geocoding
- 郵便番号から座標を取得
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がうまく動いてません。ちょっとずつ、いつか直します。
Discussion