Upgrade to Pro — share decks privately, control downloads, hide ads and more …

FlutterでMapboxとSupabase(PostGIS)を使ってみた

hmatsu47
PRO
December 14, 2021

 FlutterでMapboxとSupabase(PostGIS)を使ってみた

MIERUNE Meetup mini #01 2021/12/16 LT 資料

hmatsu47
PRO

December 14, 2021
Tweet

More Decks by hmatsu47

Other Decks in Technology

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  16. 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

    View Slide

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

    View Slide

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

    View Slide

  19. テーブル
    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

    View Slide

  20. ストアドファンクション
    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

    View Slide

  21. ストアドファンクション
    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;

    View Slide

  22. 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

    View Slide

  23. 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

    View Slide

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

    View Slide

  25. 参考情報
    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

    View Slide