paint-brush
Jetpack Compose でモバイル マップを変革: 開発者向け Google I/O からの洞察@darrylbayliss
新しい歴史

Jetpack Compose でモバイル マップを変革: 開発者向け Google I/O からの洞察

Darryl Bayliss22m2024/07/09
Read on Terminal Reader

長すぎる; 読むには

Jetpack Compose for Maps は、最新の Android 開発を変革し、モバイル マッピングをより簡単に、より効率的にします。Google I/O Extended 2023 で、Google マップを Jetpack Compose プロジェクトに統合する方法に関する重要なテクニックとヒントを紹介します。
featured image - Jetpack Compose でモバイル マップを変革: 開発者向け Google I/O からの洞察
Darryl Bayliss HackerNoon profile picture
0-item



この投稿は、2023 年 7 月に Google Developers Group London のGoogle I/O Extended で行われた講演に基づいています。講演のスライドは、 こちらからご覧いただけます。

マップ用 Jetpack Compose

Jetpack Compose 1.0 が2021 年 7 月にリリースされたとは想像しがたいことです。2 年が経過し、 Google Play のトップ 1000 アプリの 24% が Compose を採用していることを考えると、その理由は簡単に理解できます。



こうした盛り上がりの中、モダン Android 開発であまり注目されていないと感じているのが、Google マップです。SDK を使用するのは久しぶりでしたが、Google マップが時代の流れに追いつき、独自のCompose ライブラリをリリースしたことには、うれしい驚きを感じました。


これは、マッピング分野で働く企業やエンジニアにとって朗報となるでしょう。モバイル マッピングは 355 億ドル規模の産業であり、2028 年までに 19.83% の年平均成長率 (CAGR) で 877 億ドルにまで成長すると予測されています。 出典


成長市場グラフ


なぜこれが重要なのでしょうか?市場が拡大すれば、企業がモバイル マッピングのアプリケーションから収益を得る機会が増えることになります。その範囲は、通常の使用例から、食品、食料品の配達、配車サービスまで多岐にわたります。しかし、深く掘り下げてみると、すぐにはわからないアプリケーションもあります。


以下は、簡単に検索して見つけた例です。


成長市場分野モバイル マップはスマート シティに最適で、都市の鼓動を管理し、課題をよりよく理解して対処できるようにデータを視覚化するのに役立ちます。都市計画者、緊急対応組織、または一般住民にとって便利です。


資源管理もマッピング ソリューションの恩恵を受けます。農業から漁業、鉱業から林業に至るまで、マップはこれらの分野の人々に、持続可能な方法で材料を収穫するための適切な決定を下すための視点を提供します。


交通はマッピング技術に大きく依存しています。Google マップや Uber などの消費者向けアプリだけでなく、企業の車両群の位置を把握するといったビジネスレベルの機能もマッピング技術に依存しています。交通機関も、交通を管理し、交通の流れをスムーズにするためにどこに交通を誘導するかを決定するためにマップを使用しています。


最後に、気候変動と天候がますます予測不可能になる中、地図により、気象機関、緊急対応部隊、野生生物保護活動家は、世界がどのように変化しているかを理解し、変化を軽減するために前向きな措置を講じるために何ができるかを理解できるようになります。

出典:Mordor IntelligenceGMInsightsAllied Market ResearchEMR ResearchGoogle Earth OutreachResearch & Markets


世界がますます多くのデータを提供するようになった今、そのデータを地図上に表示する方法を学ぶには良い時期です。それを実行してから、コードに戻りましょう。


Compose に Google マップを使用する

Google Maps for Compose は次の依存関係に依存します。

 dependencies { implementation "com.google.maps.android:maps-compose:2.11.4" implementation "com.google.android.gms:play-services-maps:18.1.0" // Optional Util Library implementation "com.google.maps.android:maps-compose-utils:2.11.4" implementation 'com.google.maps.android:maps-compose-widgets:2.11.4' // Optional Accompanist permissions to request permissions in compose implementation "com.google.accompanist:accompanist-permissions:0.31.5-beta" }


Google Maps for Compose は Google Maps SDK 上に構築されているため、Compose ライブラリと Maps SDK をインポートする必要があります。Compose ライブラリはこれらのほとんどを Composable でラップするため、Google Maps SDK のほとんどのオブジェクトを使用する必要はありません。


utils ライブラリと widgets ライブラリはオプションの依存関係です。utils ライブラリはマップ上のマーカーをクラスター化する機能を提供し、widgets は追加の UI コンポーネントを提供します。これらは後で使用して確認します。


この投稿では、マップでよく使用される位置情報の許可をリクエストする方法を示すために、Accompanist のリクエスト許可ライブラリを組み込みました。Accompanist は、Google が Jetpack Compose にまだ含まれていない機能を試してフィードバックを収集するための実験的なライブラリです。


最後に、 Google Developer Consoleにアクセスし、Google Maps SDK API キーを登録して、プロジェクトに追加する必要があります。これを行う方法については、Google Maps Developer Docsのガイドを参照してください。


セキュリティのヒント: Google Developer Console で API キーをロックダウンし、自分のアプリケーションでのみ機能するようにします。これにより、不正使用を回避できます。


地図を表示する

地図を表示するのは以下のように簡単です。


 setContent { val hydePark = LatLng(51.508610, -0.163611) val cameraPositionState = rememberCameraPositionState { position = CameraPosition.fromLatLngZoom(hydePark, 10f) } GoogleMap( modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState) { Marker( state = MarkerState(position = hydePark), title = "Hyde Park", snippet = "Marker in Hyde Park" ) } }


エリアの位置を含むLatLngオブジェクトを作成し、それをrememberCameraPositionStateと組み合わせて使用して、カメラの初期位置を設定します。このメソッドは、手やプログラムを使用して移動するときに、マップの位置を記憶します。このメソッドがないと、Compose は状態が変化するたびにマップを初期位置に再計算します。


次に、 GoogleMapコンポーズを作成し、選択した修飾子とカメラの状態を渡します。GoogleMap GoogleMap 、追加のコンポーザブルを渡すための Slot API も提供します。これらのコンポーザブルは、マップ上に描画するものです。


Markerコンポーザブルを追加し、内部のマーカーの位置を含むMarkerStateを追加します。最後に、マーカーのタイトルと説明を追加します。


これを実行すると、ハイド パークのマーカーとともに西ロンドンの美しい空中写真が表示されます。


ハイドパークの標識

マーカーウィンドウのカスタマイズ

MarkerInfoWindowContent Composable を使用してマーカーのウィンドウをカスタマイズできます。これにはスロットベースの API もあるため、コンポーザブルを渡してウィンドウにカスタム UI をレンダリングできます。


 setContent { val hydePark = LatLng(51.508610, -0.163611) val cameraPositionState = rememberCameraPositionState { position = CameraPosition.fromLatLngZoom(hydePark, 10f) } GoogleMap( modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState) { MarkerInfoWindowContent( state = MarkerState(position = hydePark), title = "Hyde Park", snippet = "Marker in Hyde Park" ) { marker -> Column(horizontalAlignment = Alignment.CenterHorizontally) { Text( modifier = Modifier.padding(top = 6.dp), text = marker.title ?: "", fontWeight = FontWeight.Bold ) Text("Hyde Park is a Grade I-listed parked in Westminster") Image( modifier = Modifier .padding(top = 6.dp) .border( BorderStroke(3.dp, color = Color.Gray), shape = RectangleShape ), painter = painterResource(id = R.drawable.hyde_park), contentDescription = "A picture of hyde park" ) } } } }


これを実行すると、マーカーをタップしたときにその上にカスタム ウィンドウが表示されます。


ハイドパークのカスタムウィンドウマーカー

複数のマーカーを表示

複数のマーカーを表示するのは、必要な数だけ追加するだけです。西ロンドンのいくつかの異なる公園にマーカーを追加してみましょう。


 setContent { val hydePark = LatLng(51.508610, -0.163611) val regentsPark = LatLng(51.531143, -0.159893) val primroseHill = LatLng(51.539556, -0.16076088) val cameraPositionState = rememberCameraPositionState { position = CameraPosition.fromLatLngZoom(hydePark, 10f) } GoogleMap( modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState) { // Marker 1 Marker( state = MarkerState(position = hydePark), title = "Hyde Park", snippet = "Marker in Hyde Park" ) // Marker 2 Marker( state = MarkerState(position = regentsPark), title = "Regents Park", snippet = "Marker in Regents Park" ) // Marker 3 Marker( state = MarkerState(position = primroseHill), title = "Primrose Hill", snippet = "Marker in Primrose Hill" ) } }


コードを実行すると、マーカーがマップ上に表示されます。


複数の公園マーカー


クラスタリングマーカー

地図は短時間で混雑することがあります。300 個のマーカーを表示しようとすると、何が起こっているのかをユーザーが視覚的に理解するのが難しくなります。Google マップとデバイスも、マーカー1 つ 1 つレンダリングする必要があり、パフォーマンスとバッテリー寿命に影響を与えるため、これを歓迎しません。


この問題を解決するのが、クラスタリングです。これは、互いに近いマーカーを 1 つのマーカーにグループ化する手法です。このクラスタリングは、ズーム レベルに基づいて行われます。マップをズーム アウトすると、マーカーは 1 つのクラスターにグループ化され、ズーム インすると、クラスターは個別のマーカーに分割されます。


Google Maps for Compose では、 Clusteringコンポーザブルを介してこれをすぐに使用できます。クラスタリングを実行するために複雑な並べ替えやフィルタリングを記述する必要はありません。


 setContent { val hydePark = LatLng(51.508610, -0.163611) val regentsPark = LatLng(51.531143, -0.159893) val primroseHill = LatLng(51.539556, -0.16076088) val crystalPalacePark = LatLng(51.42153, -0.05749) val greenwichPark = LatLng(51.476688, 0.000130) val lloydPark = LatLng(51.364188, -0.080703) val cameraPositionState = rememberCameraPositionState { position = CameraPosition.fromLatLngZoom(hydePark, 10f) } GoogleMap( modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState) { val parkMarkers = remember { mutableStateListOf( ParkItem(hydepark, "Hyde Park", "Marker in hyde Park"), ParkItem(regentspark, "Regents Park", "Marker in Regents Park"), ParkItem(primroseHill, "Primrose Hill", "Marker in Primrose Hill"), ParkItem(crystalPalacePark, "Crystal Palace", "Marker in Crystal Palace"), ParkItem(greenwichPark, "Greenwich Park", "Marker in Greenwich Park"), ParkItem(lloydPark, "Lloyd park", "Marker in Lloyd Park"), ) } Clustering(items = parkMarkers, onClusterClick = { // Handle when the cluster is tapped }, onClusterItemClick = { marker -> // Handle when a marker in the cluster is tapped }) } } data class ParkItem( val itemPosition: LatLng, val itemTitle: String, val itemSnippet: String) : ClusterItem { override fun getPosition(): LatLng = itemPosition override fun getTitle(): String = itemTitle override fun getSnippet(): String = itemSnippet }


ParkItemデータ クラスが追加されていることに注目してください。これは、 Clusteringコンポーザブルに渡されるアイテムがClusterItemインターフェースに準拠する必要があるために必要です。インターフェースは、各マーカーの位置、タイトル、スニペットを Cluster に提供します。

ズームインおよびズームアウトすると、クラスタリングが実際に行われているのがわかります。


位置情報の許可を取得する

地図とユーザーの位置は密接に関連していることが多いため、一部のマッピング アプリがユーザーの位置情報の許可を求めるのは理にかなっています。

これを実行する場合は、ユーザーの権限を尊重してください。位置情報の許可は、ユーザーから取得する権限の中で最も機密性の高い権限の 1 つです。


この権限が必要な理由をユーザーに必ず伝え、権限を付与するメリットを積極的に示してください。権限をまったく必要とせずにアプリが部分的に機能する場合は、ボーナスポイントが加算されます。


Google は、ユーザーの位置情報の取り扱い方に関する優れたガイドを提供しているほか、バックグラウンドで位置情報データにアクセスするための別のガイドも提供しています。


十分な調査を行った結果、場所にアクセスするにはユーザーの許可が必要であると判断しました。Accompanist の権限ライブラリを使用して、次のように実行します。


 // Don't forget to add the permissions to AndroidManifest.xml val allLocationPermissionState = rememberMultiplePermissionsState( listOf(android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION) ) // Check if we have location permissions if (!allLocationPermissionsState.allPermissionsGranted) { // Show a component to request permission from the user Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, modifier = Modifier .padding(horizontal = 36.dp) .clip(RoundedCornerShape(16.dp)) .background(Color.white) ) { Text( modifier = Modifier.padding(top = 6.dp), textAlign = TextAlign.Center, text = "This app functions 150% times better with percise location enabled" ) Button(modifier = Modifier.padding(top = 12.dp), onClick = { allLocationPermissionsState.launchMultiplePermissionsRequest() }) { Text(text = "Grant Permission") } } }


accompanist を介して、アプリがACCESS_FINE_LOCATION権限、つまり英語での高レベルの GPS 精度にアクセスできるかどうかを確認しています。要求された権限を Android マニフェストに含めることが重要です。そうしないと、権限を要求できなくなります。


Android システムと Google Play ストアもマニフェストを使用してアプリの動作を理解し、ユーザーに通知します。

許可が与えられない場合は、許可が必要であることを説明する小さなダイアログ コンポーザブルと、システム経由で許可要求を開始するためのボタンが表示されます。



マップをアニメーション化する

ほとんどのマップ アプリではユーザーがタッチ操作でマップを移動する必要がありますが、Google Maps for Compose ではプログラムでマップを移動するための API が提供されています。これは、イベントに応じて特定のエリアに移動する場合に便利です。


この例では、マーカーのコレクションを通じてアプリをゆっくりとナビゲートします。


 Box(contentAlignment = Alignment.Center) { GoogleMap( modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState ) { Clustering(items = parkMarkers, onClusterClick = { // Handle when the click is tapped false }, onClusterItemClick = { marker -> // Handle when the marker is tapped }) LaunchedEffect(key1 = "Animation") { for (marker in parkMarkers) { cameraPositionState.animate( CameraUpdateFactory.newLatLngZoom( marker.itemPosition, // LatLng 16.0f), // Zoom level 2000 // Animation duration in millis ), delay(4000L) // Delay in millis } } } }


ここで重要な部分は、 LaunchedEffect内のコードです。アプリはマーカーごとに、マーカーに移動するためのcameraPositionState.animate()呼び出しを設定します。カメラは、 CameraUpdateFactory.newLatLngZoom()を使用して作成されたカメラの更新を通じてこの情報を受け取ります。


このメソッドは、マップのズーム レベルを示す float のLatLngと、アニメーションの継続時間を設定する long を受け取ります。

最後に、アニメーションの間隔を空けるために、 delay()を使用して各アニメーションの間に 4 秒間の一時停止を追加します。


ストリートビューを表示

Google Maps for Compose がサポートするのは航空写真だけではありません。アプリにStreet Viewへのアクセスを許可して、場所の 360 度ビューを表示することもできます。これを行うには、 StreetViewコンポーザブルを使用します。


 var selectedMarker: ParkItem? by remember { mutableStateOf(null) } if (selectedMarker != null) { StreetView(Modifier.fillMaxSize(), streetViewPanoramaOptionsFactory = { StreetViewPanoramaOptions().position(selectedMarker!!.position) }) } else { Box(contentAlignment = Alignment.Center) { GoogleMap( modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState ) { Clustering(items = parkMarkers, onClusterClick = { // Handle when the cluster is clicked false }, onClusterItemClick = { marker -> // Handle when a marker in the cluster is clicked selectedMarker = marker false }) } } }


この例では、マーカーがタップされるたびにselectedMarker変数を設定します。マーカーが選択されると、マーカーの位置を渡してストリート ビューを表示します。



図形を描く

地図上に独自の図形や注釈を描画したい場合があります。Google Maps for Compose には、これを行うためのコンポーザブルが多数用意されています。この投稿では、 Circleコンポーザブルを使用します。


アプリがジオフェンスを使用してユーザーの位置情報の変化に反応する場合は、円が適した形状です。円は、ジオフェンスがアクティブな領域を表すことができます。


 Box(contentAlignment = Alignment.Center) { GoogleMap( modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState ) { Clustering(items = parkMarkers, onClusterClick = { // Handle when the cluster is clicked false }, onClusterItemClick = { marker -> // Handle when a marker in the cluster is clicked selectedMarker = marker false }) } } parkMarkers.forEach { Circle( center = it.position, radius = 120.0, fillColor = Color.Green, strokeColor = Color.Green ) }


ここでは、マーカーごとに円を設定します。円を作成するには、円の位置と半径のサイズを渡します。また、2 つのオプション パラメータを使用して、円の境界線の色を設定し、円の色を塗りつぶします。



スケールバーの表示

優れた地図には、地図上の空間の測定値が距離に換算して何に相当するかを示す凡例や図表が付いていることがよくあります。すべての地図が同じ測定形式を使用しているわけではないので、これによって地図に含まれる空間の概要がわかります。


ズームインやズームアウトが可能なデジタル マップの場合、表示される距離が動的に変化する可能性があるため、複雑さが増します。幸い、Google Maps for Compose がこれをカバーします。


Widgets ライブラリを使用すると、 DisappearingScaleBarおよびScaleBarコンポーザブルにアクセスできます。これらは、マップの上部に配置される UI コンポーネントで、ズーム レベルに応じて変化する距離の測定値をユーザーに提供します。


 Box(contentAlignment = Alignment.Center) { GoogleMap( modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState ) { // You can also use ScaleBar DisappearingScaleBar( modifier = Modifier .padding(top = 5.dp, end = 15.dp) .align(Alignment.TopStart), cameraPositionState = cameraPositionState ) Clustering(items = parkMarkers, onClusterClick = { // Handle when the cluster is clicked false }, onClusterItemClick = { marker -> // Handle when a marker in the cluster is clicked selectedMarker = marker false }) } } parkMarkers.forEach { Circle( center = it.position, radius = 120.0, fillColor = Color.Green, strokeColor = Color.Green ) }


コンポーザブルを追加すると、マップの上部のズーム レベルに応じて変化する ScaleBar が表示されます。




ヘルプとサポート

Google Maps for Compose は Google マップを操作するのに最適な方法ですが、学ぶべきことはまだまだたくさんあります。ヘルプが必要な場合は、次のサイトをお勧めします。

  • Google Maps for Compose リポジトリ: ライブラリのソースコードを含むリポジトリ。ライブラリの使用方法に関するコードサンプルが含まれており、バグレポートや貢献を送信することもできます。
  • Android 版 Google マップの Web サイト: Android 版 Google マップの背後にある概念を学習できる場所です。これらは高レベルで、Compose ライブラリは使用しませんが、バックグラウンドで使用されるため、知っておくことが重要です。
  • Google マップ プラットフォーム Discord Google マップの公式 Discord サーバーです。ここでは、複数のプラットフォームの Google マップについて話し合ったり、質問したり、助けを提供したり、自分の作品を披露したりする人々が見つかります。