Flutter×Supabaseで作った位置情報共有アプリの中身
記事の概要
この記事では、FlutterとSupabaseを利用して作成した位置情報共有アプリの実装方針、考慮事項に関して機能別に簡単にまとめています。
作成したアプリ
IDを渡せば誰とでも位置情報を共有でき、時間が経つと自動で共有が解除されるため、簡単な待ち合わせなどに利用できます。
各機能
ログイン/認証
ログインにはEmailを利用しています。Supabaseでは、デフォルトの設定でアカウント登録時に認証メールが送信され本人確認ができるようになっています。メール内の認証リンクを押下することでアカウントが認証済みステータスになりアプリにログインできるようになります。
また、DeepLinkの設定をすることで認証リンクを押下後、アプリに戻ることができ、アプリ側ではonAuthStateChangedのリスナーを張ることで認証後の画面遷移処理などを実行することができます。
// 認証リスナー
_authSubscription = supabase.auth.onAuthStateChange.listen((data) async {
final AuthChangeEvent event = data.event;
debugPrint(event.toString());
final session = data.session;
if (session != null) {
// 認証後の処理
}
}
}, onError: (error) {
debugPrint(error);
});
アイコン画像
各ユーザのプロフィールに表示されるアイコン画像はSupabase上のStorageに送信して保持するようにしています。
Storageの使い方は以前まとめたこちらも参考にしてみてください。
SupabaseのStorageを無料プランで利用する場合、ダウンロードのデータ量が2GBまでとなっています。そのため、この帯域幅を節約するために今回は以下のように対応しました。
- 扱う画像のデータサイズを小さくする
image_pickerで写真フォルダから画像データを取得する際に、imageQualityを0に指定することで画像データのサイズを小さくすることができます。
final pickedFile = await imagePicker?.pickImage(
// imageQualityは0~100まで指定でき0が最小サイズ
source: ImageSource.gallery, imageQuality: 0);
Storageで扱うデータサイズを小さくすることで、ダウンロード時のデータ量を抑えることができます。さらに抑える必要がある場合は、データの圧縮や変換を検討する必要がありそうです。
- キャッシュしてダウンロード回数を減らす
cached_network_imageを利用することでキャッシュすることができます。
cached_network_imageではネットワークから取得した画像を端末にキャッシュしてくれるため、余計なダウンロードが発生せず帯域幅を節約できます。
チャット
チャット機能はSupabaseのリアルタイム機能とFlutterのStreamBuilderを利用して実装しています。
まず、Supabase上にメッセージを保持するMessagesテーブルを用意し、Messagesテーブルのリアルタイム機能をオンにします。
アプリ側では、以下のように表示したいメッセージデータをストリームで取得し、それを利用してStreamBuilderでビルドすることで、チャット機能が実装できます。
stream = supabase
.from('messages')
.stream(primaryKey: ['seq'])
// 同じチャットグループのメッセージデータを抽出
.eq('group_id', groupIdAndExpState.groupId)
.order('created_at');
参考
FlutterとSupabaseでチャットアプリを作る
supabase_flutter - Realtime data as Stream
マップ
GoogleMap
地図機能にはMap BoxやMap Kitなどの選択肢もありますが、今回は特に地図をカスタマイズする必要もなかったため簡単に使えるGoogle Mapを利用しました。Google Mapは一定の無料枠はあるもののリクエスト数に応じて料金が発生するようになっています。ただ、モバイル向けのMap SDK for iOS, Maps SDK for Androidに関してはこちらにある通り、現時点では制限なく無料で利用できます。
カスタムマーカー
今回利用したgoogle_maps_flutterでは、markersプロパティでマーカーの設定をすることができます。マーカーはデフォルトで赤いピン画像が利用されます。
GoogleMap(
// 省略
markers: [
Marker(
markerId: MarkerId(),
position: LatLng(//省略),
icon: //BitmapDescriptorで指定
),
Marker(
markerId: MarkerId(),
position: LatLng(//省略),
icon: //BitmapDescriptorで指定
),
]
)
実際のアプリでは、赤いピンではなく、ユーザの設定したアイコン画像を円で囲んだカスタムマーカー(Widget)を表示する必要がありました。
この時、上記のiconプロパティにWidgetを指定することはできず、BitmapDescriptorをセットする必要があるため、一手間かける必要がありました。具体的にはWidgetを画像(Byte)に変換し、それをBitmapDescriptorに変換し、iconプロパティに渡すという方法です。
簡潔に書くと以下のような処理を実装する必要があります。
- 表示したいカスタムマーカーのWidgetをビルドする
※Widget自体は表示する必要がないため画面の外などにビルドする - Widgetのビルド完了後のコールバック関数(addPostFrameCallback)内でWidget>画像>BitmapDescriptorと変換する
※Widgetのビルド完了前に変換するとエラーになるためコールバック関数を利用する - GoogleMapウィジェットのMarker.iconプロパティにBitmapDescriptorを渡す
これで、カスタムマーカーをMap上に表示することができます。
参考
GoogleMapで複数の Widget をマーカーとして表示する
バックグラウンドで位置情報を取得
位置情報を共有している間は、アプリが閉じられていてもリアルタイムで更新されるよう、バックグラウンドで位置情報を端末から取得し、Supabase上に送信するという処理を実行する必要がありました。
また、バックグラウンドでの実行には位置情報の利用を「常に許可」に設定してもらう必要があります。「常に許可」を設定するには「一度だけ許可/アプリの使用中は許可/許可しない」>「使用中のみ許可のままにする/常に許可に変更する」の二段階でユーザに許可してもらう必要があり、これを考慮して画面設計する必要があります。
今回は端末の位置情報と許可設定を扱うためにlocation+permission_handlerを利用しました。
locationだけでは「常に許可」の権限の確認や要求ができないため、permission_handlerと組み合わせて利用する必要がありました。
permission_handlerでは権限のステータス確認や、許可要求を実施することができます。
実際のアプリの画面は以下のように実装しています。
- まず「アプリの使用中は許可」を設定してもらうようにします。
- 次に「常に許可」を設定してもらうようにします。この時手順1で許可されていることが必要になります。
- 手順1.2で許可設定されなかった場合や、アプリの使用中に設定が変更された場合は、以下の画面に遷移しユーザ自身で設定アプリから「常に許可」を設定してもらうようにします。
まとめ
今回はFlutterとSupabaseで作成した位置情報共有アプリのポイントとなる機能の実装方針に関してまとめました。Supabaseを利用したアプリの作成は今回が初めてでしたが、モバイルのバックエンドサービスとして非常に使いやすく、やはりRDBが使えるという点でFirebaseよりも学習コストが低く、個人で作るアプリはSupabaseの一択になりそうかなという所感です。
具体的な実装方法など詳細に関しては連絡していただければお答えします。何か参考になるものがあれば幸いです。
Discussion