Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add experimental MapEffect composable #140

Merged
merged 3 commits into from
Jun 14, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,40 @@ MarkerInfoWindow(
}
```

#### Obtaining Access to the raw GoogleMap (Experimental)

Certain use cases require extending the `GoogleMap` object to decorate / augment
the map. For example, while marker clustering is not yet supported by Maps Compose
(see [Issue #44](https://github.com/googlemaps/android-maps-compose/issues/44)),
it is desirable to use the available [utility library](https://github.com/googlemaps/android-maps-utils)
to perform clustering in the interim. Doing so requires access to the Maps SDK
`GoogleMap` object which you can obtain with the `MapEffect` composable.

```kotlin
GoogleMap(
// ...
) {
val context = LocalContext.current
var clusterManager by remember { mutableStateOf<ClusterManager<MyItem>?>(null) }
MapEffect(items) { map ->
if (clusterManager == null) {
clusterManager = ClusterManager<MyItem>(context, map)
}
clusterManager?.addItems(items)
}
}
arriolac marked this conversation as resolved.
Show resolved Hide resolved
```

Note, however, that `MapEffect` is designed as an escape hatch and has certain
gotchas. The `GoogleMap` composable provided by the Maps Compose library manages
properties while the `GoogleMap` is in composition, and so, setting properties
on the `GoogleMap` instance provided in the `MapEffect` composable may have
unintended consequences. For instance, using the utility library to perform
clustering as shown in the example above will break `onClick` events from
being propagated on `Marker` composables. So, if you are using clustering as
shown above, stick with adding markers through the `ClusterManager` and don't
use `Marker` composables (unless you don't care about `onClick` events).
arriolac marked this conversation as resolved.
Show resolved Hide resolved

## Sample App

This repository includes a [sample app](app).
Expand Down
6 changes: 4 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ android {

kotlinOptions {
jvmTarget = '1.8'
freeCompilerArgs += '-Xopt-in=kotlin.RequiresOptIn'
}
}

Expand All @@ -45,6 +46,7 @@ dependencies {
implementation 'com.google.android.material:material:1.5.0'
implementation 'com.google.maps.android:maps-ktx:3.3.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.google.maps.android:android-maps-utils:2.3.0'

androidTestImplementation "androidx.test:core:$androidx_test_version"
androidTestImplementation "androidx.test:rules:$androidx_test_version"
Expand All @@ -60,9 +62,9 @@ dependencies {
// module.
// However, this should remain uncommented on the `main` branch so that
// the maven declaration of Maps Compose can be used as a snippet.
// implementation project(':maps-compose')
implementation project(':maps-compose')
arriolac marked this conversation as resolved.
Show resolved Hide resolved
// [END_EXCLUDE]
implementation "com.google.maps.android:maps-compose:2.2.0"
//implementation "com.google.maps.android:maps-compose:2.2.0"
implementation 'com.google.android.gms:play-services-maps:18.0.2'
}
// [END maps_android_compose_dependency]
Expand Down
7 changes: 5 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,13 @@
</activity>
<activity
android:name=".BasicMapActivity"
android:exported="true" />
android:exported="false" />
<activity
android:name=".MapInColumnActivity"
android:exported="true"/>
android:exported="false"/>
<activity
android:name=".MapClusteringActivity"
android:exported="false"/>

<!-- Used by createComponentActivity() for unit testing -->
<activity android:name="androidx.activity.ComponentActivity" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ class MainActivity : ComponentActivity() {
}) {
Text(getString(R.string.map_in_column_activity))
}
Spacer(modifier = Modifier.padding(5.dp))
Button(
onClick = {
context.startActivity(Intent(context, MapClusteringActivity::class.java))
}) {
Text(getString(R.string.map_clustering_activity))
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.google.maps.android.compose

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.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
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.ClusterManager
import kotlin.random.Random

private val singapore = LatLng(1.35, 103.87)
private val singapore2 = LatLng(2.50, 103.87)
private val TAG = MapClusteringActivity::class.simpleName

class MapClusteringActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
GoogleMapClustering()
}
}
}

@Composable
fun GoogleMapClustering() {
val items = remember { mutableStateListOf<MyItem>() }
LaunchedEffect(Unit) {
for (i in 1..10) {
val position = LatLng(
singapore2.latitude + Random.nextFloat(),
singapore2.longitude + Random.nextFloat(),
)
items.add(MyItem(position, "Marker", "Snippet"))
}
}
GoogleMapClustering(items = items)
}

@OptIn(MapsComposeExperimentalApi::class)
@Composable
fun GoogleMapClustering(items: List<MyItem>) {
val cameraPositionState = rememberCameraPositionState {
position = CameraPosition.fromLatLngZoom(singapore, 10f)
}
GoogleMap(
modifier = Modifier.fillMaxSize(),
cameraPositionState = cameraPositionState
) {
val context = LocalContext.current
var clusterManager by remember { mutableStateOf<ClusterManager<MyItem>?>(null) }
MapEffect(items) { map ->
if (clusterManager == null) {
clusterManager = ClusterManager<MyItem>(context, map)
}
clusterManager?.addItems(items)
}
LaunchedEffect(key1 = cameraPositionState.isMoving) {
if (!cameraPositionState.isMoving) {
clusterManager?.cluster()
arriolac marked this conversation as resolved.
Show resolved Hide resolved
}
}
MarkerInfoWindow(
state = rememberMarkerState(position = singapore),
onClick = {
// This won't work :(
Log.d(TAG, "I was clicked $it")
arriolac marked this conversation as resolved.
Show resolved Hide resolved
true
}
)
}
}

data class MyItem(
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
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ private val defaultCameraPosition = CameraPosition.fromLatLngZoom(singapore, 11f

class MapInColumnActivity : ComponentActivity() {

@OptIn(ExperimentalComposeUiApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@
<string name="main_activity_title">"Maps Compose Demos \uD83D\uDDFA"</string>
<string name="basic_map_activity">Basic Map Activity</string>
<string name="map_in_column_activity">Map In Column Activity</string>
<string name="map_clustering_activity">Map Clustering</string>
</resources>
1 change: 1 addition & 0 deletions maps-compose/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ android {
kotlinOptions {
jvmTarget = '1.8'
freeCompilerArgs += '-Xexplicit-api=strict'
freeCompilerArgs += '-Xopt-in=kotlin.RequiresOptIn'
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,6 @@ public fun GoogleMap(
mapProperties = currentMapProperties,
mapUiSettings = currentUiSettings,
)

currentContent?.invoke()
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.google.maps.android.compose

import androidx.compose.runtime.Composable
import androidx.compose.runtime.ExperimentalComposeApi
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.currentComposer
import com.google.android.gms.maps.GoogleMap
import kotlinx.coroutines.CoroutineScope

/**
* A side-effect backed by a [LaunchedEffect] which will launch [block] and provide the underlying
* managed [GoogleMap] object into the composition's [CoroutineContext]. This effect will be
* re-launched when a different [key1] is provided.
*
* Note: This effect should be used with caution as the [GoogleMap]'s properties is managed by the
* [_root_ide_package_.com.google.maps.android.compose.GoogleMap()] composable function. However,
* there are use cases when obtaining a raw reference to the map is desirable for extensibility
* (e.g. using the utility library for clustering).
*/
@Composable
@GoogleMapComposable
@MapsComposeExperimentalApi
public fun MapEffect(key1: Any?, block: suspend CoroutineScope.(GoogleMap) -> Unit) {
val map = (currentComposer.applier as MapApplier).map
LaunchedEffect(key1 = key1) {
block(map)
}
}

/**
* A side-effect backed by a [LaunchedEffect] which will launch [block] and provide the underlying
* managed [GoogleMap] object into the composition's [CoroutineContext]. This effect will be
* re-launched when a different [key1] or [key2] is provided.
*
* Note: This effect should be used with caution as the [GoogleMap]'s properties is managed by the
* [_root_ide_package_.com.google.maps.android.compose.GoogleMap()] composable function. However,
* there are use cases when obtaining a raw reference to the map is desirable for extensibility
* (e.g. using the utility library for clustering).
*/
@Composable
@GoogleMapComposable
@MapsComposeExperimentalApi
public fun MapEffect(key1: Any?, key2: Any?, block: suspend CoroutineScope.(GoogleMap) -> Unit) {
val map = (currentComposer.applier as MapApplier).map
LaunchedEffect(key1 = key1, key2 = key2) {
block(map)
}
}

/**
* A side-effect backed by a [LaunchedEffect] which will launch [block] and provide the underlying
* managed [GoogleMap] object into the composition's [CoroutineContext]. This effect will be
* re-launched when a different [key1], [key2], or [key3] is provided.
*
* Note: This effect should be used with caution as the [GoogleMap]'s properties is managed by the
* [_root_ide_package_.com.google.maps.android.compose.GoogleMap()] composable function. However,
* there are use cases when obtaining a raw reference to the map is desirable for extensibility
* (e.g. using the utility library for clustering).
*/
@Composable
@GoogleMapComposable
@MapsComposeExperimentalApi
public fun MapEffect(
key1: Any?,
key2: Any?,
key3: Any?,
block: suspend CoroutineScope.(GoogleMap) -> Unit
) {
val map = (currentComposer.applier as MapApplier).map
LaunchedEffect(key1 = key1, key2 = key2, key3 = key3) {
block(map)
}
}

/**
* A side-effect backed by a [LaunchedEffect] which will launch [block] and provide the underlying
* managed [GoogleMap] object into the composition's [CoroutineContext]. This effect will be
* re-launched with any different [keys].
*
* Note: This effect should be used with caution as the [GoogleMap]'s properties is managed by the
* [_root_ide_package_.com.google.maps.android.compose.GoogleMap()] composable function. However,
* there are use cases when obtaining a raw reference to the map is desirable for extensibility
* (e.g. using the utility library for clustering).
*/
@Composable
@GoogleMapComposable
@MapsComposeExperimentalApi
public fun MapEffect(
vararg keys: Any?,
block: suspend CoroutineScope.(GoogleMap) -> Unit
) {
val map = (currentComposer.applier as MapApplier).map
LaunchedEffect(keys = keys) {
block(map)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.google.maps.android.compose

/**
* Marks declarations that are still **experimental**.
*
*/
@MustBeDocumented
@Retention(value = AnnotationRetention.BINARY)
@RequiresOptIn(
level = RequiresOptIn.Level.WARNING,
message = "Targets marked by this annotation may contain breaking changes in the future as their design is still incubating."
)
public annotation class MapsComposeExperimentalApi