GoogleMapにBitmapのカスタムマーカーを沢山表示させつつ滑らかな操作を実現したい!
自己紹介と概要
初めまして!
株式会社ジーニーのooyamaと申します。現在はGeniee SFA/CRMのAndroidアプリを担当しています
今回は、Jetpack ComposeでGoogleMapを表示し、その中に300個程度のカスタムマーカーを動的に表示させた時のお話です。
マーカーには全てタイトルを表示し、カメラ位置が変わると表示マーカーを新しく取得し直すということを実現しています。その際にタイトル表示やパフォーマンスについて苦労しましたので、実現に至った経緯をご紹介したいと思います
同じようなご苦労をされている方のご参考になれば幸いです
要望の内容
以下の要件を実現したいという要望がPdMよりありました
- 日本全国に数十万件の企業情報がある
- その内、アプリでは現在地を中心に300企業まで表示させる
- マーカーはカスタムアイコンを使用し、色指定もそれぞれ異なる
- マーカーにはそれぞれ企業名をタイトルとして表示する
- 企業名は表示させることも消すこともできる
- 選択したマーカーは1.5倍に大きく表示する
この話を聞いた時に思ったことは「うわぁ・・」でした。特に「マーカーにはそれぞれ企業名をタイトルとして表示する」は、その時のGoogleMap純正の機能では実現が難しいものでした
※なお、企業名や座標情報、色指定はAPIより取得します。そちらもDBの構成上苦労したのですが(バックエンドチームが)、API詳細やバックエンドの苦労話は割愛します
また、取得したマーカー情報をRoomなどローカルDBで保持することで高速化を図ることは可能ですが、これはデータ取得の高速化という文脈になりますので、今回はスコープ外とします
使用するライブラリ
SFA/CRMアプリは元々Fragment + レイアウトファイル(xml)で作成していたのですが、2023年からアプリのリニューアルを開始し、JetpackComposeの導入を決定しています
今回のマップ画面もMaps Composeを使用しました
MapCompose導入や基本的な実装については公式含め良い記事が沢山ありますので省略します
実装において困ったことと対策
マーカーのタイトルの設定(困ったこと1)
デフォルトのマーカーについて
APIで取得した企業情報をマップのデフォルトマーカーで表示するのは特に困ることはありません
MapComposeのMarkerを利用して表示するだけです
以下のgif動画では描画がもたついているように見えますが、APIの応答待ち時間です。API呼び出しはcoroutineのflowを使用した非同期処理で、その間のマップの移動も特に問題ありません
カスタムマーカーをBitmapにて生成する
ただし、デフォルトのマーカーでは「マーカーはカスタムアイコンを使用し、色指定もそれぞれ異なる」のうち、カスタムアイコンが実現できません。そこで、SVGファイルからBitmapDescripterを作成してアイコンに指定することで表示することができるようにします(Bitmapの生成方法は割愛します)
この際、APIから取得したカラーコードを適用しています
それっぽい感じになりましたが、タイトルを表示しなくてはなりません
タイトルの表示
ComposeMapのMarkerにはtitleというプロパティがありますが、ここに設定した場合はユーザがタップしたマーカーのみ表示が行われるもので、今回の要望である「マーカーにはそれぞれ企業名をタイトルとして表示する」には合いません
ですので、タイトルについても今回はBitmapDescripterで生成しました
タイトル表示のON/OFFやマーカー選択時の拡大表示という要件についてもBitmap再生成で実現できました
なお、現在はどうも高度なマーカーのMarkerComposableというプロパティで同様のことが実現できるようです。ベース実装完了の3ヶ月後くらいに発表された機能なので、結構ショッキングでした。このプロパティについてはまた時間を取って試してみるつもりです
カクカクした操作の改善(困ったこと2)
さて、これで取り急ぎ要望についてはクリアできました。
どうすか!?とPdMに見せてみたところ、「何かもっさりしてないですか?」と言われました
開発時の環境では表示マーカーの数がそれほど多くなかったのですが、検証用の環境は企業の数が300件以上あり、最大件数表示させた場合に操作->表示レスポンスの遅延が発生していました
以下の動画で、スワイプの動作にマップ描画が遅れたり反応しなかったりすることが確認できます
これではとてもユーザーに出すことはできませんね。何とか解決する必要があります
[仮説1]Bitmap生成が遅い
こういう場合は、Profile取得して細かく解析するべきなのですが、それ以前にBitmap生成はコストがかかるためまずこの部分の処理確認と対応を行いました
その時の実装では、マップ移動時にAPIより取得した新しい企業のアイコンを生成し、表示外となったアイコン情報は破棄していました。さらにこの処理をMainスレッドで行なっていました
このため移動の度にBitmap生成を待つという挙動になっており、明らかによろしくないです
よって以下の対応を行い、Bitmap生成によるUIの遅れを解消しました
- 生成したBitmapは600個ほどメモリ保持、既に作成したアイコンはリユースする
- Bitmap生成はバックグラウンドで非同期化
これで体感的には少しマシになったのですが、どうもまだカクカク感が残っています
[仮説2]GoogleMapのカスタムマーカー描画が遅い?
さてどうしたものか・・と思いつつ、ふとマーカーのアイコンをデフォルトに戻してみると、企業数が多い環境でも結構サクサク動作することに気づきました
この時に思ったことが、「ああ、マーカー描画はインスタンス再利用するのだな」でした
少しアーキテクチャの話になるのですが、リファクタ前のソースコードはほとんどの処理がFragmentに書かれていいる恐ろしいものでした。
今回のリファクタにあたり、クリーンアーキテクチャへの移行を目指しており、
View-Presenter(ViewModel)-UseCase-Repository-DataSource
と責務を分散する構成に徐々に変更していっております
UseCaseで取得したマーカーのリスト情報はアイコンのBitmapも含めてViewModelでListを保持、View側に変更通知します。カメラ移動でマーカー情報変更された時は、.copy()メソッドで新しいListを作り直していました。View側では変更されたリストからアイコンデータをMapsComposeライブラリに渡してマーカー描画させています
Listはもちろんイミュータブルであり、変更の度にインスタンスが変わるという作りになっていました
仮説として、GoogleMapのマーカー描画が毎回1から行われるためにコストがかかるのではないか、と想定しました
[仮説2の対応]イミュータブルをやめてみると・・
上記の仮説を検証するには、マーカーの情報mutableListを使って保持し、リストの内容を変更する形になるでしょうか。ただし、この場合リスト自体のインスタンスが変わらないので、別のフラグで伝えるような仕組みを作ってみました
すると、何ということでしょう
これまで全然追従できていなかったマップのスワイプ操作が、滑らかに動くようになりました
検証の効果が確認できたため、最終的にはMutableStateFlowを使用してリストを保持し、Viewに変更を通知する形で実装を行いました
現在はマップ機能もリリース済みであり、幸いなことに運用頂いているお客様にもご好評頂けています
マーカー+タイトル表示はやり過ぎ感がありますね。。今後の課題は適切なクラスタ化です
まとめ
参照される状態は1つ、副作用を発生させたくないのでStateはイミュータブルであるべきという思想で実装を行ったことで発生した現象でした
状況によっては、あるべき論を捨てて要望の実現を最優先する、これもProfessionalWorkなのではないかと思えた事例でしたので、今回ご紹介させて頂きました
恐らくもっともっと良い設計があると思います。また、MapsComposeのソースコードも追えておらず、幾分消化不良気味で現在に至る、というのが現在の正直なところです
引き続き良い方法は追求していきたいと考えています
また、上司からは、仮説の検証よりも先にプロファイル解析などによる事実確認を行うべきだと指摘頂きました。これはご尤もであり、今回はたまたま上手くいきましたが、仮説の元となる情報が無い限り、思いつきに過ぎないということは肝に銘じる必要があります
Discussion