Slide 1

Slide 1 text

Flutter で Mapbox と Supabase(PostGIS)を 使ってみた MIERUNE Meetup mini #01 2021/12/16 まつひさ(hmatsu47)

Slide 2

Slide 2 text

自己紹介 松久裕保(@hmatsu47) ● https://qiita.com/hmatsu47 ● 現在のステータス: ○ 名古屋で Web インフラのお守り係をしています ○ Flutter は個人的に触り始めたところです ○ GIS も詳しくないです ■ ようやく「丸い地球」を扱うことができるようになった MySQL 8.0 で少し 触った程度 2

Slide 3

Slide 3 text

Flutter とは? ● Google 製の UI フレームワーク ○ 使う言語は Dart ○ 当初はクロスプラットフォームモバイルアプリ開発用 ○ その後、Web や Windows / macOS / Linux も対象に 3

Slide 4

Slide 4 text

Flutter を試そうと思ったきっかけ ● 以前 Qiita でバズったこのサイト ○ https://korette.jp/ 4

Slide 5

Slide 5 text

Flutter を試そうと思ったきっかけ ● 以前 Qiita でバズったこのサイト ○ https://korette.jp/ ○ サポーターズの一員として大量にクイズ投稿 ○ その後、コロナ禍で観光地の状況が一変 ○ コロナが落ち着いた隙をみながら問題メンテナンスの旅へ ○ 旅のお供として、情報収集・整理のためのアプリが欲しい ○ 作ることにした 5

Slide 6

Slide 6 text

余談ですが ● ご本人は起業されて・・・ ○ https://ambirise.jp/ 6

Slide 7

Slide 7 text

Flutter でモバイル地図アプリ ● 主な選択肢(LIKES 順) ○ google_maps_flutter(Google マップ) ○ flutter_map(Leaflet / Azure Maps・OpenStreetMap など) ○ mapbox_gl(Mapbox) ○ flutter_osm_plugin(OpenStreetMap) 7

Slide 8

Slide 8 text

Flutter でモバイル地図アプリ ● 主な選択肢(LIKES 順) ○ google_maps_flutter(Google マップ) ○ flutter_map(Leaflet / Azure Maps・OpenStreetMap など) ○ mapbox_gl(Mapbox) ○ flutter_osm_plugin(OpenStreetMap) ● 選んだ理由:Google マップ以外でたまたま見つけた ○ 他の 2 つは後で知った 8

Slide 9

Slide 9 text

作っているアプリ(maptool) ● https://github.com/hmatsu47/maptool ○ 状態管理ライブラリは使わず StatefulWidget だけでどこまでいけるかチャレンジ中 ● 実装済みの主な機能 ○ 訪問(予定)地へのピン立て(登録) ○ 登録ピンと関連づけて写真撮影 ○ 登録ピンの検索 ○ 地図スタイル切り替え ○ 文化財などの近隣スポット検索 9

Slide 10

Slide 10 text

作っているアプリ(maptool) ● 訪問(予定)地へのピン立て(登録) 10 注:画面は少し古めのバージョン です(以降同じ)

Slide 11

Slide 11 text

作っているアプリ(maptool) ● 登録ピンと関連づけて写真撮影 11

Slide 12

Slide 12 text

作っているアプリ(maptool) 12 ● 登録ピンの検索

Slide 13

Slide 13 text

作っているアプリ(maptool) ● 地図スタイル切り替え 13

Slide 14

Slide 14 text

作っているアプリ(maptool) 14 ● 文化財などの近隣スポット検索(Supabase / PostGIS)

Slide 15

Slide 15 text

Flutter mapbox_gl で困ったこと ● いくつかトラブルが発生 ○ Android で地図スタイルを切り替えるとエラーで落ちる ○ mapbox_gl 0.14.0 で MapboxMap クラスに実装されている onStyleLoadedCallback の動きが変化 ■ 地図スタイル切り替え直後の言語指定・ピン再表示に困る ○ 他にも細かいトラブル発生 ■ プラットフォームによる差異(対応/非対応)など(詳細は省略) 15

Slide 16

Slide 16 text

Flutter mapbox_gl で困ったこと ● Android で地図スタイルを切り替えるとエラーで落ちる ○ ○ mapbox_gl 0.13.0 では時々、0.14.0 では必ずエラー発生 ■ ただし機種によって発生したりしなかったりする可能性も ○ ハードウェアアクセラレーション無効化はダメ ■ 地図描画で使っているため ○ Android では地図スタイル切り替えを無効にした 16 signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x8 Cause: null pointer dereference

Slide 17

Slide 17 text

Flutter mapbox_gl で困ったこと ● mapbox_gl 0.14.0 で MapboxMap クラスに実装されて いる onStyleLoadedCallback の動きが変化 ■ 地図スタイル切り替え直後の言語指定・ピン再表示に利用していた ○ 0.13.0 ではこれを使って言語の指定とピン再表示が可能だった ○ 0.14.0 で動かなくなる ○ 仕方がないので地図ウィジェット再表示の後に別途指定&再表示 ■ ときどきピン再表示に失敗(未解決) 17

Slide 18

Slide 18 text

Flutter から Supabase の PostGIS を使う ● こちらの記事を参照 Flutter から Supabase の PostgreSQL with PostGIS を使ってみる https://qiita.com/hmatsu47/items/c3f9cafb499aedaca1f1 ○ Supabase を設定 ■ PostgreSQL で PostGIS を有効化 ■ テーブル作成 ■ ストアドファンクション作成 ○ Flutter から RPC でストアドファンクションを呼び出す 18

Slide 19

Slide 19 text

テーブル CREATE TABLE category ( id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, categoryname text NOT NULL ); CREATE TABLE spot_opendata ( id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, category_id int REFERENCES category (id) NOT NULL, title text NOT NULL, describe text NOT NULL, location geometry(point, 4326) NOT NULL, prefecture text NOT NULL, municipality text NOT NULL, pref_muni text GENERATED ALWAYS AS (prefecture || municipality) STORED, created_at timestamp with time zone DEFAULT timezone('utc'::text, now()) NOT NULL, updated_at timestamp with time zone DEFAULT timezone('utc'::text, now()) NOT NULL ); CREATE INDEX spot_location_idx ON spot_opendata USING GIST (location); CREATE INDEX spot_pref_idx ON spot_opendata (prefecture); CREATE INDEX spot_muni_idx ON spot_opendata (municipality); CREATE INDEX spot_pref_muni_idx ON spot_opendata (pref_muni); 19

Slide 20

Slide 20 text

ストアドファンクション 20 CREATE OR REPLACE FUNCTION get_spots(point_latitude double precision, point_longitude double precision, dist_limit int, category_id_number int) RETURNS TABLE ( distance double precision, category_name text, title text, describe text, latitude double precision, longitude double precision, prefecture text, municipality text ) AS $$ BEGIN RETURN QUERY

Slide 21

Slide 21 text

ストアドファンクション 21 SELECT ((ST_POINT(point_longitude, point_latitude)::geography <-> spot_opendata.location::geography) / 1000) AS distance, category.category_name, spot_opendata.title, spot_opendata.describe, ST_Y(spot_opendata.location), ST_X(spot_opendata.location), spot_opendata.prefecture, spot_opendata.municipality FROM spot_opendata INNER JOIN category ON spot_opendata.category_id = category.id WHERE (ST_POINT(point_longitude, point_latitude)::geography <-> spot_opendata.location::geography) <= dist_limit AND (CASE WHEN category_id_number = -1 THEN true ELSE category.id = category_id_number END) ORDER BY distance; END; $$ LANGUAGE plpgsql;

Slide 22

Slide 22 text

Flutter コード import 'package:mapbox_gl/mapbox_gl.dart'; import 'package:supabase/supabase.dart'; import 'class_definition.dart'; SupabaseClient getSupabaseClient(String supabaseUrl, String supabaseKey) { return SupabaseClient(supabaseUrl, supabaseKey); } Future> searchSpotCategory(SupabaseClient client) async { final PostgrestResponse selectResponse = await client .from('category') .select() .order('id', ascending: true) .execute(); final List items = selectResponse.data; final List resultList = []; for (dynamic item in items) { final SpotCategory category = SpotCategory(item['id'] as int, item['category_name'] as String); resultList.add(category); } return resultList; } 22

Slide 23

Slide 23 text

Flutter コード Future> searchNearSpot(SupabaseClient client, LatLng latLng, int distLimit, int? categoryId) async { final PostgrestResponse selectResponse = await client.rpc('get_spots', params: { 'point_latitude': latLng.latitude, 'point_longitude': latLng.longitude, 'dist_limit': distLimit, 'category_id_number': (categoryId ?? -1) }).execute(); final List items = selectResponse.data; final List resultList = []; for (dynamic item in items) { final SpotData spotData = SpotData( item['distance'] as num, item['category_name'] as String, item['title'] as String, item['describe'] as String, LatLng((item['latitude'] as num).toDouble(), (item['longitude'] as num).toDouble()), PrefMuni(item['prefecture'] as String, item['municipality'] as String)); resultList.add(spotData); } return resultList; } 23

Slide 24

Slide 24 text

まとめ ● Flutter でモバイル地図アプリ製作 ○ 細かいことを気にしなければ比較的簡単に作れる ○ Mapbox の場合は Android で苦労することが多そう ■ iOS だけ非対応の機能もいくつかあるが、全体的に(個人の印象) ● Flutter から Supabase の PostGIS を使う ○ クエリビルダで(GIS)関数が使えないのでストアドファンク ションを RPC で呼び出す 24

Slide 25

Slide 25 text

参考情報 25 ● 関連ブログ記事 ○ https://qiita.com/hmatsu47/items/b98ef4c1a87cc0ec415d ○ https://zenn.dev/hmatsu47/articles/846c3186f5b4fe ○ https://zenn.dev/hmatsu47/articles/9102fb79a99a98 ○ https://zenn.dev/hmatsu47/articles/e81bf3c2bf00f8 ○ https://qiita.com/hmatsu47/items/e4f7e310e88376d54009 ○ https://qiita.com/hmatsu47/items/86a9c028bb5b3beeebdd ○ https://qiita.com/hmatsu47/items/53ea68769c4fc2d76450 ○ https://qiita.com/hmatsu47/items/c3f9cafb499aedaca1f1