diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 146e0c53..c3b5aa63 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -43,13 +43,13 @@
android:name=".BasicMapActivity"
android:exported="false" />
+
+
+
+
+
diff --git a/app/src/main/java/com/google/maps/android/compose/BasicMapActivity.kt b/app/src/main/java/com/google/maps/android/compose/BasicMapActivity.kt
index 79cb11c5..47eb2515 100644
--- a/app/src/main/java/com/google/maps/android/compose/BasicMapActivity.kt
+++ b/app/src/main/java/com/google/maps/android/compose/BasicMapActivity.kt
@@ -141,7 +141,7 @@ fun GoogleMapView(
val singapore4State = rememberMarkerState(position = singapore4)
var circleCenter by remember { mutableStateOf(singapore) }
- if (singaporeState.dragState == DragState.END) {
+ if (!singaporeState.isDragging) {
circleCenter = singaporeState.position
}
@@ -380,7 +380,7 @@ private fun DebugView(
Text(text = "Camera position is ${cameraPositionState.position}")
Spacer(modifier = Modifier.height(4.dp))
val dragging =
- if (markerState.dragState == DragState.DRAG) "dragging" else "not dragging"
+ if (markerState.isDragging) "dragging" else "not dragging"
Text(text = "Marker is $dragging")
Text(text = "Marker position is ${markerState.position}")
}
diff --git a/app/src/main/java/com/google/maps/android/compose/MainActivity.kt b/app/src/main/java/com/google/maps/android/compose/MainActivity.kt
index f7fadb44..42ef0c0e 100644
--- a/app/src/main/java/com/google/maps/android/compose/MainActivity.kt
+++ b/app/src/main/java/com/google/maps/android/compose/MainActivity.kt
@@ -32,6 +32,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
+import com.google.maps.android.compose.markerexamples.AdvancedMarkersActivity
+import com.google.maps.android.compose.markerexamples.MarkerClusteringActivity
+import com.google.maps.android.compose.markerexamples.draggablemarkerscollectionwithpolygon.DraggableMarkersCollectionWithPolygonActivity
+import com.google.maps.android.compose.markerexamples.markerdragevents.MarkerDragEventsActivity
+import com.google.maps.android.compose.markerexamples.markerscollection.MarkersCollectionActivity
+import com.google.maps.android.compose.markerexamples.syncingdraggablemarkerwithdatamodel.SyncingDraggableMarkerWithDataModelActivity
+import com.google.maps.android.compose.markerexamples.updatingnodragmarkerwithdatamodel.UpdatingNoDragMarkerWithDataModelActivity
import com.google.maps.android.compose.theme.MapsComposeSampleTheme
class MainActivity : ComponentActivity() {
@@ -141,6 +148,41 @@ class MainActivity : ComponentActivity() {
}) {
Text(getString(R.string.recomposition_activity))
}
+ Spacer(modifier = Modifier.padding(5.dp))
+ Button(
+ onClick = {
+ context.startActivity(Intent(context, MarkerDragEventsActivity::class.java))
+ }) {
+ Text(getString(R.string.marker_drag_events_activity))
+ }
+ Spacer(modifier = Modifier.padding(5.dp))
+ Button(
+ onClick = {
+ context.startActivity(Intent(context, MarkersCollectionActivity::class.java))
+ }) {
+ Text(getString(R.string.markers_collection_activity))
+ }
+ Spacer(modifier = Modifier.padding(5.dp))
+ Button(
+ onClick = {
+ context.startActivity(Intent(context, SyncingDraggableMarkerWithDataModelActivity::class.java))
+ }) {
+ Text(getString(R.string.syncing_draggable_marker_with_data_model))
+ }
+ Spacer(modifier = Modifier.padding(5.dp))
+ Button(
+ onClick = {
+ context.startActivity(Intent(context, UpdatingNoDragMarkerWithDataModelActivity::class.java))
+ }) {
+ Text(getString(R.string.updating_non_draggable_marker_with_data_model))
+ }
+ Spacer(modifier = Modifier.padding(5.dp))
+ Button(
+ onClick = {
+ context.startActivity(Intent(context, DraggableMarkersCollectionWithPolygonActivity::class.java))
+ }) {
+ Text(getString(R.string.draggable_markers_collection_with_polygon))
+ }
}
}
}
diff --git a/app/src/main/java/com/google/maps/android/compose/AdvancedMarkersActivity.kt b/app/src/main/java/com/google/maps/android/compose/markerexamples/AdvancedMarkersActivity.kt
similarity index 94%
rename from app/src/main/java/com/google/maps/android/compose/AdvancedMarkersActivity.kt
rename to app/src/main/java/com/google/maps/android/compose/markerexamples/AdvancedMarkersActivity.kt
index 48cf92e8..e32b08a1 100644
--- a/app/src/main/java/com/google/maps/android/compose/AdvancedMarkersActivity.kt
+++ b/app/src/main/java/com/google/maps/android/compose/markerexamples/AdvancedMarkersActivity.kt
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.maps.android.compose
+package com.google.maps.android.compose.markerexamples
import android.R.drawable.ic_menu_myplaces
@@ -36,6 +36,12 @@ import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.Marker
import com.google.android.gms.maps.model.PinConfig
+import com.google.maps.android.compose.AdvancedMarker
+import com.google.maps.android.compose.GoogleMap
+import com.google.maps.android.compose.MapProperties
+import com.google.maps.android.compose.MapType
+import com.google.maps.android.compose.rememberCameraPositionState
+import com.google.maps.android.compose.rememberMarkerState
private const val TAG = "AdvancedMarkersActivity"
diff --git a/app/src/main/java/com/google/maps/android/compose/MarkerClusteringActivity.kt b/app/src/main/java/com/google/maps/android/compose/markerexamples/MarkerClusteringActivity.kt
similarity index 95%
rename from app/src/main/java/com/google/maps/android/compose/MarkerClusteringActivity.kt
rename to app/src/main/java/com/google/maps/android/compose/markerexamples/MarkerClusteringActivity.kt
index 3f025e62..8301b006 100644
--- a/app/src/main/java/com/google/maps/android/compose/MarkerClusteringActivity.kt
+++ b/app/src/main/java/com/google/maps/android/compose/markerexamples/MarkerClusteringActivity.kt
@@ -1,4 +1,4 @@
-package com.google.maps.android.compose
+package com.google.maps.android.compose.markerexamples
import android.os.Bundle
import android.util.Log
@@ -40,9 +40,16 @@ import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.LatLng
import com.google.maps.android.clustering.ClusterItem
import com.google.maps.android.clustering.algo.NonHierarchicalViewBasedAlgorithm
+import com.google.maps.android.compose.GoogleMap
+import com.google.maps.android.compose.MapsComposeExperimentalApi
+import com.google.maps.android.compose.MarkerInfoWindow
import com.google.maps.android.compose.clustering.Clustering
import com.google.maps.android.compose.clustering.rememberClusterManager
import com.google.maps.android.compose.clustering.rememberClusterRenderer
+import com.google.maps.android.compose.rememberCameraPositionState
+import com.google.maps.android.compose.rememberMarkerState
+import com.google.maps.android.compose.singapore
+import com.google.maps.android.compose.singapore2
import kotlin.random.Random
private val TAG = MarkerClusteringActivity::class.simpleName
diff --git a/app/src/main/java/com/google/maps/android/compose/markerexamples/draggablemarkerscollectionwithpolygon/DraggableMarkersCollectionWithPolygonActivity.kt b/app/src/main/java/com/google/maps/android/compose/markerexamples/draggablemarkerscollectionwithpolygon/DraggableMarkersCollectionWithPolygonActivity.kt
new file mode 100644
index 00000000..8bea519c
--- /dev/null
+++ b/app/src/main/java/com/google/maps/android/compose/markerexamples/draggablemarkerscollectionwithpolygon/DraggableMarkersCollectionWithPolygonActivity.kt
@@ -0,0 +1,192 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.maps.android.compose.markerexamples.draggablemarkerscollectionwithpolygon
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableStateMapOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.SnapshotStateMap
+import androidx.compose.ui.Modifier
+import com.google.android.gms.maps.model.LatLng
+import com.google.maps.android.compose.GoogleMap
+import com.google.maps.android.compose.Marker
+import com.google.maps.android.compose.MarkerState
+import com.google.maps.android.compose.Polygon
+import com.google.maps.android.compose.defaultCameraPosition
+import com.google.maps.android.compose.rememberCameraPositionState
+import com.google.maps.android.compose.theme.MapsComposeSampleTheme
+
+/**
+ * Data type representing a location.
+ *
+ * This only stores location position, for illustration,
+ * but could hold other data related to the location.
+ */
+@Immutable
+private data class LocationData(val position: LatLng)
+
+/**
+ * Unique, stable key for location
+ */
+private class LocationKey
+
+/**
+ * Encapsulates mapping from data model to MarkerStates. Part of view model.
+ * MarkerStates are relegated to an implementation detail.
+ * Use new [DraggableMarkersModel] instance if data model is updated externally:
+ * MarkerStates are source of truth after initialization from data model.
+ */
+@Stable
+private class DraggableMarkersModel(dataModel: Map) {
+ // This initializes MarkerState from our model once (model is initial source of truth)
+ // and never updates it from the model afterwards.
+ // See SyncingDraggableMarkerWithDataModelActivity for rationale.
+ private val markerDataMap: SnapshotStateMap = mutableStateMapOf(
+ *dataModel.entries.map { (locationKey, locationData) ->
+ locationKey to MarkerState(locationData.position)
+ }.toTypedArray()
+ )
+
+ /** Add new marker location to model */
+ fun addLocation(locationData: LocationData) {
+ markerDataMap += LocationKey() to MarkerState(locationData.position)
+ }
+
+ /** Delete marker location from model */
+ private fun deleteLocation(locationKey: LocationKey) {
+ markerDataMap -= locationKey
+ }
+
+ /**
+ * Render Markers from model
+ */
+ @Composable
+ fun Markers() = markerDataMap.forEach { (locationKey, markerState) ->
+ key(locationKey) {
+ LocationMarker(
+ markerState,
+ onClick = { deleteLocation(locationKey) }
+ )
+ }
+ }
+
+ /**
+ * List of functions providing current positions of Markers.
+ *
+ * Calling from composition will trigger recomposition when Markers and their positions
+ * change.
+ */
+ val markerPositionsModel: List<() -> LatLng>
+ get() = markerDataMap.values.map { { it.position } }
+}
+
+/**
+ * Demonstrates how to sync a data model with a changing collection of
+ * draggable markers using keys, while keeping a Polygon of the marker positions in sync with
+ * the current marker position.
+ *
+ * The user can add a location marker to the model by clicking the map and delete a location from
+ * the model by clicking a marker.
+ *
+ * This example builds on top of ideas from MarkersCollectionActivity and
+ * SyncingDraggableMarkerWithDataModelActivity.
+ */
+class DraggableMarkersCollectionWithPolygonActivity : ComponentActivity() {
+ // Simplistic data model from repository being set from outside (should be part of view model);
+ // Only stores [LocationData], for illustration, but could hold additional data.
+ private var dataModel: Map = mapOf()
+ set(value) {
+ field = value
+ markersModel = DraggableMarkersModel(value)
+ }
+
+ private var markersModel by mutableStateOf(DraggableMarkersModel(dataModel))
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContent {
+ MapsComposeSampleTheme {
+ GoogleMapWithLocations(
+ markersModel,
+ modifier = Modifier.fillMaxSize()
+ )
+ }
+ }
+ }
+}
+
+/**
+ * A GoogleMap with locations represented by markers
+ */
+@Composable
+private fun GoogleMapWithLocations(
+ markersModel: DraggableMarkersModel,
+ modifier: Modifier = Modifier
+) {
+ val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition }
+
+ GoogleMap(
+ modifier = modifier,
+ cameraPositionState = cameraPositionState,
+ onMapClick = { position -> markersModel.addLocation(LocationData(position)) }
+ ) {
+ markersModel.Markers()
+
+ Polygon(markersModel::markerPositionsModel)
+ }
+}
+
+/**
+ * A draggable GoogleMap Marker representing a location on the map
+ */
+@Composable
+private fun LocationMarker(
+ markerState: MarkerState,
+ onClick: () -> Unit
+) {
+ Marker(
+ state = markerState,
+ draggable = true,
+ onClick = {
+ onClick()
+
+ true
+ }
+ )
+}
+
+/**
+ * A Polygon. Helps isolate recompositions while a Marker is being dragged.
+ */
+@Composable
+private fun Polygon(markerPositionsModel: () -> List<() -> LatLng>) {
+ val movingMarkerPositions = markerPositionsModel()
+
+ if (movingMarkerPositions.isNotEmpty()) {
+ val markerPositions = movingMarkerPositions.map { it() }
+
+ Polygon(markerPositions)
+ }
+}
diff --git a/app/src/main/java/com/google/maps/android/compose/markerexamples/markerdragevents/MarkerDragEventsActivity.kt b/app/src/main/java/com/google/maps/android/compose/markerexamples/markerdragevents/MarkerDragEventsActivity.kt
new file mode 100644
index 00000000..5862ce60
--- /dev/null
+++ b/app/src/main/java/com/google/maps/android/compose/markerexamples/markerdragevents/MarkerDragEventsActivity.kt
@@ -0,0 +1,127 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.maps.android.compose.markerexamples.markerdragevents
+
+import android.os.Bundle
+import android.util.Log
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Modifier
+import com.google.android.gms.maps.model.LatLng
+import com.google.maps.android.compose.GoogleMap
+import com.google.maps.android.compose.Marker
+import com.google.maps.android.compose.defaultCameraPosition
+import com.google.maps.android.compose.rememberCameraPositionState
+import com.google.maps.android.compose.rememberMarkerState
+import com.google.maps.android.compose.singapore
+import com.google.maps.android.compose.theme.MapsComposeSampleTheme
+import kotlinx.coroutines.flow.dropWhile
+
+private val TAG = MarkerDragEventsActivity::class.simpleName
+
+/**
+ * Demonstrates how to reliably generate a sequence of Marker drag START-DRAG-END events as in the
+ * original GoogleMap Marker listener.
+ */
+class MarkerDragEventsActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContent {
+ MapsComposeSampleTheme {
+ GoogleMapWithMarker(
+ modifier = Modifier.fillMaxSize()
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun GoogleMapWithMarker(
+ modifier: Modifier = Modifier,
+) {
+ val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition }
+
+ GoogleMap(
+ modifier = modifier,
+ cameraPositionState = cameraPositionState,
+ ) {
+ DraggableMarker(
+ onDragStart = { Log.i(TAG, "onDragStart") },
+ onDragEnd = { Log.i(TAG, "onDragEnd") },
+ onDrag = { position -> Log.i(TAG, "onDrag: $position") }
+ )
+ }
+}
+
+/**
+ * A draggable GoogleMap Marker.
+ *
+ * @param onDragStart called when marker dragging starts
+ * @param onDrag called with an update for the marker's current position during dragging
+ * @param onDragEnd called when marker dragging ends
+ */
+@Composable
+private fun DraggableMarker(
+ onDragStart: () -> Unit = {},
+ onDrag: (LatLng) -> Unit = {},
+ onDragEnd: () -> Unit = {}
+) {
+ val markerState = rememberMarkerState(position = singapore)
+
+ Marker(
+ state = markerState,
+ draggable = true
+ )
+
+ LaunchedEffect(Unit) {
+ var inDrag = false
+ var priorPosition: LatLng? = singapore
+
+ snapshotFlow { markerState.isDragging to markerState.position }
+ .dropWhile { (isDragging, position) ->
+ !isDragging && position == priorPosition // ignore initial value
+ }
+ .collect { (isDragging, position) ->
+ // Do not even bother to check isDragging state here:
+ // it is possible to miss a sequence of states
+ // where isDragging == true, then isDragging == false;
+ // in this case we would only see a change in position.
+ // (Hypothetically we could even miss a change in position
+ // if the Marker ended up in its original position at the
+ // end of the drag. But then nothing changed at all,
+ // so we should be ok to ignore this case altogether.)
+ if (!inDrag) {
+ inDrag = true
+ onDragStart()
+ }
+
+ if (position != priorPosition) {
+ onDrag(position)
+ priorPosition = position
+ }
+
+ if (!isDragging) {
+ inDrag = false
+ onDragEnd()
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/google/maps/android/compose/markerexamples/markerscollection/MarkersCollectionActivity.kt b/app/src/main/java/com/google/maps/android/compose/markerexamples/markerscollection/MarkersCollectionActivity.kt
new file mode 100644
index 00000000..dacb43ac
--- /dev/null
+++ b/app/src/main/java/com/google/maps/android/compose/markerexamples/markerscollection/MarkersCollectionActivity.kt
@@ -0,0 +1,154 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.maps.android.compose.markerexamples.markerscollection
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableStateMapOf
+import androidx.compose.ui.Modifier
+import com.google.android.gms.maps.model.LatLng
+import com.google.maps.android.compose.GoogleMap
+import com.google.maps.android.compose.MarkerState
+import com.google.maps.android.compose.defaultCameraPosition
+import com.google.maps.android.compose.markerexamples.updatingnodragmarkerwithdatamodel.Marker
+import com.google.maps.android.compose.rememberCameraPositionState
+import com.google.maps.android.compose.theme.MapsComposeSampleTheme
+
+/**
+ * Simplistic app data model intended for persistent storage.
+ *
+ * This only stores [LocationData], for demonstration purposes, but could hold an entire app's data.
+ */
+private class DataModel {
+ /**
+ * Location data.
+ */
+ val locationDataMap = mutableStateMapOf()
+}
+
+/**
+ * Data type representing a location.
+ *
+ * This only stores location position, for demonstration purposes,
+ * but could hold other data related to the location.
+ */
+@Immutable
+private data class LocationData(val position: LatLng)
+
+/**
+ * Unique, stable key for location
+ */
+private class LocationKey
+
+private typealias KeyedLocationData = Pair
+
+/**
+ * Demonstrates how to sync a data model with a changing collection of
+ * location markers using keys.
+ *
+ * The user can add a location marker to the model by clicking the map and delete a location from
+ * the model by clicking a marker.
+ *
+ * This example reuses the simple non-draggable Marker approach from the
+ * `UpdatingNoDragMarkerWithDataModelActivity` example, which encapsulates
+ * [MarkerState] to provide a cleaner API surface.
+ */
+class MarkersCollectionActivity : ComponentActivity() {
+ private val dataModel = DataModel()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContent {
+ MapsComposeSampleTheme {
+ Screen(
+ dataModel = dataModel,
+ modifier = Modifier.fillMaxSize()
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun Screen(
+ dataModel: DataModel,
+ modifier: Modifier = Modifier
+) = GoogleMapWithLocations(
+ modifier = modifier,
+ keyedLocationData = dataModel.locationDataMap.toList(),
+ onAddLocation = { locationData ->
+ dataModel.locationDataMap += LocationKey() to locationData
+ },
+ onDeleteLocation = { key ->
+ dataModel.locationDataMap -= key
+ }
+)
+
+/**
+ * A GoogleMap with locations represented by markers
+ *
+ * @param keyedLocationData model data for location markers with unique keys.
+ * Uses a [Collection] type to keep it independent of our data model.
+ * @param onAddLocation location addition events for updating data model
+ * @param onDeleteLocation location deletion events for updating data model
+ */
+@Composable
+private fun GoogleMapWithLocations(
+ keyedLocationData: Collection,
+ modifier: Modifier = Modifier,
+ onAddLocation: (LocationData) -> Unit,
+ onDeleteLocation: (LocationKey) -> Unit
+) {
+ val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition }
+
+ GoogleMap(
+ modifier = modifier,
+ cameraPositionState = cameraPositionState,
+ onMapClick = { position -> onAddLocation(LocationData(position)) }
+ ) {
+ Locations(
+ keyedLocationData = keyedLocationData,
+ onLocationClick = onDeleteLocation
+ )
+ }
+}
+
+/**
+ * Renders locations on a GoogleMap
+ *
+ * @param keyedLocationData model data for location markers with unique keys.
+ * @param onLocationClick location click events
+ */
+@Composable
+private fun Locations(
+ keyedLocationData: Collection,
+ onLocationClick: (LocationKey) -> Unit
+) = keyedLocationData.forEach { (key, locationData) ->
+ key(key) {
+ Marker(
+ position = locationData.position,
+ onClick = {
+ onLocationClick(key)
+ true // consume click event to prevent camera move to marker
+ }
+ )
+ }
+}
diff --git a/app/src/main/java/com/google/maps/android/compose/markerexamples/syncingdraggablemarkerwithdatamodel/SyncingDraggableMarkerWithDataModelActivity.kt b/app/src/main/java/com/google/maps/android/compose/markerexamples/syncingdraggablemarkerwithdatamodel/SyncingDraggableMarkerWithDataModelActivity.kt
new file mode 100644
index 00000000..1aa48ae9
--- /dev/null
+++ b/app/src/main/java/com/google/maps/android/compose/markerexamples/syncingdraggablemarkerwithdatamodel/SyncingDraggableMarkerWithDataModelActivity.kt
@@ -0,0 +1,168 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.maps.android.compose.markerexamples.syncingdraggablemarkerwithdatamodel
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Modifier
+import com.google.android.gms.maps.model.LatLng
+import com.google.maps.android.compose.GoogleMap
+import com.google.maps.android.compose.Marker
+import com.google.maps.android.compose.MarkerState
+import com.google.maps.android.compose.defaultCameraPosition
+import com.google.maps.android.compose.rememberCameraPositionState
+import com.google.maps.android.compose.singapore
+import com.google.maps.android.compose.theme.MapsComposeSampleTheme
+
+/**
+ * Simplistic app data model intended for persistent storage.
+ *
+ * This only stores [LocationData], for demonstration purposes, but could hold an entire app's data.
+ */
+private class DataModel {
+ /**
+ * Location data
+ */
+ var locationData by mutableStateOf(LocationData(singapore))
+}
+
+/**
+ * Data type representing a location.
+ *
+ * This only stores location position, for demonstration purposes,
+ * but could hold other data related to the location.
+ */
+@Immutable
+private data class LocationData(val position: LatLng)
+
+/**
+ * Demonstrates how to avoid data races when keeping a data model in sync
+ * with location derived from a draggable marker. The model is the initial source of truth for the
+ * marker's position; markers are draggable, so MarkerState becomes the source of truth after
+ * initialization.
+ *
+ * This addresses difficulties caused by having source of truth for position baked into
+ * com.google.android.gms.maps.model.Marker, and consequently MarkerState.
+ */
+class SyncingDraggableMarkerWithDataModelActivity : ComponentActivity() {
+ private val dataModel = DataModel()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContent {
+ MapsComposeSampleTheme {
+ Screen(
+ dataModel = dataModel,
+ modifier = Modifier.fillMaxSize()
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun Screen(
+ dataModel: DataModel,
+ modifier: Modifier = Modifier
+) {
+ GoogleMapWithLocation(
+ modifier = modifier,
+ locationData = dataModel.locationData,
+ onUpdateLocation = { locationData ->
+ dataModel.locationData = locationData
+ }
+ )
+}
+
+/**
+ * A GoogleMap with a location represented by a marker
+ *
+ * @param locationData model data for location marker. The UI becomes the source of truth for
+ * marker position after initial composition; the model's position is ignored on recomposition.
+ * @param onUpdateLocation location update events for updating data model
+ */
+@Composable
+private fun GoogleMapWithLocation(
+ locationData: LocationData,
+ modifier: Modifier = Modifier,
+ onUpdateLocation: (LocationData) -> Unit
+) {
+ val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition }
+
+ GoogleMap(
+ modifier = modifier,
+ cameraPositionState = cameraPositionState,
+ ) {
+ LocationMarker(
+ locationData = locationData,
+ onLocationUpdate = onUpdateLocation
+ )
+ }
+}
+
+/**
+ * A draggable GoogleMap Marker representing a location on the map.
+ *
+ * @param locationData model data for location marker. The UI becomes the source of truth for
+ * marker position after initial composition; the model's position is ignored on recomposition.
+ * @param onLocationUpdate marker update events with updated [LocationData]
+ */
+@Composable
+private fun LocationMarker(
+ locationData: LocationData,
+ onLocationUpdate: (LocationData) -> Unit
+) {
+ // This sets the MarkerData from our model once (model is initial source of truth)
+ // and never updates it from the model afterwards,
+ // because MarkerState/GoogleMap is the source of truth after initialization
+ // and we want to avoid multiple competing sources of truth
+ // to prevent potential data races.
+ // This achieves a clean separation of sources of truth at the cost of
+ // no longer having state flow down.
+ // It is the price we pay for having source of truth baked into
+ // com.google.android.gms.maps.model.Marker, and consequently MarkerState.
+ //
+ // Do not use rememberMarkerState() here, because it uses rememberSaveable();
+ // we want to save the position to persistent storage as part of our data model
+ // instead - rememberSaveable() would add a conflicting source of truth.
+ val markerState = remember { MarkerState(locationData.position) }
+
+ Marker(
+ state = markerState,
+ draggable = true
+ )
+
+ LaunchedEffect(Unit) {
+ snapshotFlow { markerState.position }
+ .collect { position ->
+ // build LocationData update from marker update
+ val update = LocationData(position = position)
+
+ // send update event
+ onLocationUpdate(update)
+ }
+ }
+}
diff --git a/app/src/main/java/com/google/maps/android/compose/markerexamples/updatingnodragmarkerwithdatamodel/UpdatingNoDragMarkerWithDataModelActivity.kt b/app/src/main/java/com/google/maps/android/compose/markerexamples/updatingnodragmarkerwithdatamodel/UpdatingNoDragMarkerWithDataModelActivity.kt
new file mode 100644
index 00000000..ecd58208
--- /dev/null
+++ b/app/src/main/java/com/google/maps/android/compose/markerexamples/updatingnodragmarkerwithdatamodel/UpdatingNoDragMarkerWithDataModelActivity.kt
@@ -0,0 +1,154 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.maps.android.compose.markerexamples.updatingnodragmarkerwithdatamodel
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.lifecycle.lifecycleScope
+import com.google.android.gms.maps.model.LatLng
+import com.google.maps.android.compose.GoogleMap
+import com.google.maps.android.compose.Marker
+import com.google.maps.android.compose.MarkerState
+import com.google.maps.android.compose.defaultCameraPosition
+import com.google.maps.android.compose.rememberCameraPositionState
+import com.google.maps.android.compose.rememberMarkerState
+import com.google.maps.android.compose.singapore
+import com.google.maps.android.compose.singapore2
+import com.google.maps.android.compose.singapore3
+import com.google.maps.android.compose.theme.MapsComposeSampleTheme
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlin.random.Random
+
+/**
+ * Simplistic app data model intended for persistent storage.
+ *
+ * This only stores [LocationData], for demonstration purposes, but could hold an entire app's data.
+ */
+private class DataModel {
+ /**
+ * Location data
+ */
+ var locationData by mutableStateOf(LocationData(singapore))
+}
+
+/**
+ * Data type representing a location.
+ *
+ * This only stores location position, for demonstration purposes,
+ * but could hold other data related to the location.
+ */
+@Immutable
+private data class LocationData(val position: LatLng)
+
+/**
+ * Demonstrates how to easily initialize and update position for a non-draggable
+ * Marker from a data model.
+ */
+class UpdatingNoDragMarkerWithDataModelActivity : ComponentActivity() {
+ private val dataModel = DataModel()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ lifecycleScope.launch {
+ // Simulate remote updates to data model
+ while (true) {
+ delay(3_000)
+
+ val newPosition = when (Random.nextInt(3)) {
+ 0 -> singapore
+ 1 -> singapore2
+ 2 -> singapore3
+ else -> singapore
+ }
+
+ dataModel.locationData = LocationData(newPosition)
+ }
+ }
+
+ setContent {
+ MapsComposeSampleTheme {
+ GoogleMapWithSimpleMarker(
+ locationData = dataModel.locationData,
+ modifier = Modifier.fillMaxSize()
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun GoogleMapWithSimpleMarker(
+ locationData: LocationData,
+ modifier: Modifier = Modifier,
+) {
+ val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition }
+
+ GoogleMap(
+ modifier = modifier,
+ cameraPositionState = cameraPositionState,
+ ) {
+ Marker(position = locationData.position)
+ }
+}
+
+/**
+ * Standard API pattern for a non-draggable Marker.
+ *
+ * The caller does not have to deal with MarkerState,
+ * and can update Marker [position] via recomposition.
+ */
+@Composable
+fun Marker(
+ position: LatLng,
+ onClick: () -> Boolean = { false },
+) {
+ val markerState = rememberUpdatedMarkerState(position)
+
+ Marker(
+ state = markerState,
+ onClick = { onClick() }
+ )
+}
+
+/**
+ * Standard API pattern for remembering, initializing, and updating MarkerState for a
+ * non-draggable Marker, where [position] comes from a data model.
+ *
+ * Implementation modeled after `rememberUpdatedState`.
+ *
+ * This one uses [remember] behind the scenes, not [rememberMarkerState], which uses
+ * `rememberSaveable`. Our data model is the source of truth - `rememberSaveable` would
+ * create a conflicting source of truth.
+ */
+@Composable
+fun rememberUpdatedMarkerState(position: LatLng): MarkerState =
+ // This pattern is equivalent to what rememberUpdatedState() does:
+ // rememberUpdatedState() uses MutableState, we use MarkerState.
+ // This is more efficient than updating position in an effect,
+ // as we avoid an additional recomposition.
+ remember { MarkerState(position = position) }.also {
+ it.position = position
+ }
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 5b74c4b9..6638f6ee 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -21,8 +21,13 @@
Advanced Markers
Map In Column
Marker Clustering
+ Marker Drag Events
+ Markers Collection
Location Tracking
Scale Bar
+ Syncing Draggable Marker With Model
+ Updating Non-Draggable Marker With Model
+ Polygon around draggable markers
Recomposition Map
Street View
Custom Location Button
diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/MapApplier.kt b/maps-compose/src/main/java/com/google/maps/android/compose/MapApplier.kt
index 36e488b0..f1793069 100644
--- a/maps-compose/src/main/java/com/google/maps/android/compose/MapApplier.kt
+++ b/maps-compose/src/main/java/com/google/maps/android/compose/MapApplier.kt
@@ -142,45 +142,70 @@ internal class MapApplier(
)
}
map.setOnMarkerDragListener(object : GoogleMap.OnMarkerDragListener {
- override fun onMarkerDrag(marker: Marker) {
+ // We update MarkerState isDragging & position properties in a specific well-defined
+ // order: MarkerState.position is never updated by us unless
+ // MarkerState.isDragging == true. This avoids using Snapshots, which can fail to apply;
+ // they would not be meaningful here, because we are not the actual source of truth.
+
+ override fun onMarkerDragStart(marker: Marker) {
decorations.findInputCallback(
nodeMatchPredicate = { it.marker == marker },
+ marker = marker,
nodeInputCallback = {
{
- markerState.position = it.position
- markerState.dragState = DragState.DRAG
+ val position = it.position
+
+ markerState.isDragging = true
+ // update position after enabling isDragging
+ markerState.position = position
+
+ @Suppress("DEPRECATION")
+ markerState.dragState = DragState.START
}
},
- marker = marker,
- inputHandlerCallback = { onMarkerDrag }
+ inputHandlerCallback = { onMarkerDragStart }
)
}
- override fun onMarkerDragEnd(marker: Marker) {
+ override fun onMarkerDrag(marker: Marker) {
decorations.findInputCallback(
nodeMatchPredicate = { it.marker == marker },
- marker = marker,
nodeInputCallback = {
{
- markerState.position = it.position
- markerState.dragState = DragState.END
+ val position = it.position
+
+ markerState.isDragging = true // just in case, should be set already
+ // update position after enabling isDragging
+ markerState.position = position
+
+ @Suppress("DEPRECATION")
+ markerState.dragState = DragState.DRAG
}
},
- inputHandlerCallback = { onMarkerDragEnd }
+ marker = marker,
+ inputHandlerCallback = { onMarkerDrag }
)
}
- override fun onMarkerDragStart(marker: Marker) {
+ override fun onMarkerDragEnd(marker: Marker) {
decorations.findInputCallback(
nodeMatchPredicate = { it.marker == marker },
marker = marker,
nodeInputCallback = {
{
- markerState.position = it.position
- markerState.dragState = DragState.START
+ val position = it.position
+
+ markerState.isDragging = true // just in case, should be set already
+ // update position after enabling isDragging
+ markerState.position = position
+ // disable isDragging after updating position
+ markerState.isDragging = false
+
+ @Suppress("DEPRECATION")
+ markerState.dragState = DragState.END
}
},
- inputHandlerCallback = { onMarkerDragStart }
+ inputHandlerCallback = { onMarkerDragEnd }
)
}
})
diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/Marker.kt b/maps-compose/src/main/java/com/google/maps/android/compose/Marker.kt
index 796ce3a3..25ddab7e 100644
--- a/maps-compose/src/main/java/com/google/maps/android/compose/Marker.kt
+++ b/maps-compose/src/main/java/com/google/maps/android/compose/Marker.kt
@@ -63,6 +63,7 @@ internal class MarkerNode(
}
@Immutable
+@Deprecated("START, DRAG, END are events, not states. Avoid usage.")
public enum class DragState {
START, DRAG, END
}
@@ -70,6 +71,8 @@ public enum class DragState {
/**
* A state object that can be hoisted to control and observe the marker state.
*
+ * This cannot be used to preserve marker info window visibility across configuration changes.
+ *
* @param position the initial marker position
*/
public class MarkerState(
@@ -77,12 +80,35 @@ public class MarkerState(
) {
/**
* Current position of the marker.
+ *
+ * This property is backed by Compose state.
+ * It can be updated by the API user and by the API itself:
+ * it has two potentially competing sources of truth.
+ *
+ * The API will not update the property unless [isDragging] `== true`,
+ * which will happen if and only if a Marker is draggable and the end user
+ * is currently dragging it.
*/
public var position: LatLng by mutableStateOf(position)
+ /**
+ * Reflects whether the end user is currently dragging the marker.
+ * Dragging can happen only if a Marker is draggable.
+ *
+ * This property is backed by Compose state.
+ */
+ public var isDragging: Boolean by mutableStateOf(false)
+ internal set
+
/**
* Current [DragState] of the marker.
*/
+ @Deprecated(
+ "Use isDragging instead - dragState is not appropriate for representing \"state\";" +
+ " it is a lossy representation of drag \"events\", promoting invalid usage.",
+ level = DeprecationLevel.WARNING
+ )
+ @Suppress("DEPRECATION")
public var dragState: DragState by mutableStateOf(DragState.END)
internal set
@@ -99,14 +125,28 @@ public class MarkerState(
}
/**
- * Shows the info window for the underlying marker
+ * Shows the info window for the underlying marker.
+ *
+ * Not backed by Compose state to accommodate
+ * [com.google.android.gms.maps.GoogleMap] special semantics:
+ * only a single info window can be visible for the entire GoogleMap.
+ *
+ * Only use from Compose Effect APIs, never directly from composition, to avoid exceptions and
+ * unexpected behavior from cancelled compositions.
*/
public fun showInfoWindow() {
marker?.showInfoWindow()
}
/**
- * Hides the info window for the underlying marker
+ * Hides the info window for the underlying marker.
+ *
+ * Not backed by observable Compose state to accommodate
+ * [com.google.android.gms.maps.GoogleMap] special semantics:
+ * only a single info window can be visible for the entire GoogleMap.
+ *
+ * Only use from Compose Effect APIs, never directly from composition, to avoid
+ * unexpected behavior from cancelled compositions.
*/
public fun hideInfoWindow() {
marker?.hideInfoWindow()
@@ -115,6 +155,9 @@ public class MarkerState(
public companion object {
/**
* The default saver implementation for [MarkerState]
+ *
+ * This cannot be used to preserve marker info window visibility across
+ * configuration changes.
*/
public val Saver: Saver = Saver(
save = { it.position },
@@ -123,6 +166,14 @@ public class MarkerState(
}
}
+/**
+ * Uses [rememberSaveable] to retain [MarkerState.position] across configuration changes,
+ * for simple use cases.
+ *
+ * Other use cases may be better served syncing [MarkerState.position] with a data model.
+ *
+ * This cannot be used to preserve info window visibility across configuration changes.
+ */
@Composable
public fun rememberMarkerState(
key: String? = null,