From 4279199c6f4fedc3e734ddb15d52de1ca041909a Mon Sep 17 00:00:00 2001 From: Philip Date: Fri, 13 Oct 2023 11:21:02 +0200 Subject: [PATCH 001/103] Modify GoogleMap to use AndroidView overload with onReset lambda This will make sure that the underlying MapView is re-used in a LazyColumn (and elsewhere) which will significantly improve scrolling performance in LazyColumn. If this overload isn't used the MapView will be destroyed every time it leaves composition. The MapView will still be destroyed when the parent node leaves the composition. --- .../google/maps/android/compose/GoogleMap.kt | 45 +++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index da27b122..3f45e735 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -33,6 +33,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCompositionContext import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode @@ -97,11 +98,25 @@ public fun GoogleMap( return } - val context = LocalContext.current - val mapView = remember { MapView(context, googleMapOptionsFactory()) } + // Will either be set to a re-used or a new MapView + var mapViewOrNull: MapView? by remember { mutableStateOf(null) } + var isMapViewReused by remember { mutableStateOf(true) } + + AndroidView( + modifier = modifier, + factory = { context -> + isMapViewReused = false + MapView(context, googleMapOptionsFactory()) + }, + onReset = { }, + onRelease = { it.destroyAndRemoveAllViews()}, + update = { mapViewOrNull = it } + ) + + // Wait until we have a MapView + val mapView = mapViewOrNull ?: return - AndroidView(modifier = modifier, factory = { mapView }) - MapLifecycle(mapView) + MapLifecycle(mapView, isMapViewReused) // rememberUpdatedState and friends are used here to make these values observable to // the subcomposition without providing a new content function each recomposition @@ -177,14 +192,24 @@ private suspend inline fun MapView.newComposition( } } +private fun MapView.destroyAndRemoveAllViews() { + onDestroy() + removeAllViews() +} + /** * Registers lifecycle observers to the local [MapView]. */ @Composable -private fun MapLifecycle(mapView: MapView) { +private fun MapLifecycle(mapView: MapView, isMapViewReused: Boolean) { val context = LocalContext.current val lifecycle = LocalLifecycleOwner.current.lifecycle - val previousState = remember { mutableStateOf(Lifecycle.Event.ON_CREATE) } + val previousState = remember { + // If mapView is re-used then ON_CREATE should not be invoked on it again + val initialState = if(isMapViewReused) Lifecycle.Event.ON_STOP else Lifecycle.Event.ON_CREATE + mutableStateOf(initialState) + } + DisposableEffect(context, lifecycle, mapView) { val mapLifecycleObserver = mapView.lifecycleObserver(previousState) val callbacks = mapView.componentCallbacks() @@ -197,12 +222,6 @@ private fun MapLifecycle(mapView: MapView) { context.unregisterComponentCallbacks(callbacks) } } - DisposableEffect(mapView) { - onDispose { - mapView.onDestroy() - mapView.removeAllViews() - } - } } private fun MapView.lifecycleObserver(previousState: MutableState): LifecycleEventObserver = @@ -222,7 +241,7 @@ private fun MapView.lifecycleObserver(previousState: MutableState this.onPause() Lifecycle.Event.ON_STOP -> this.onStop() Lifecycle.Event.ON_DESTROY -> { - //handled in onDispose + // Handled in AndroidView onRelease } else -> throw IllegalStateException() } From 284f93a61fa1447dda4816a53c6b00c06b7e2559 Mon Sep 17 00:00:00 2001 From: Philip Date: Fri, 13 Oct 2023 11:22:36 +0200 Subject: [PATCH 002/103] Update androidx.compose.ui:ui library to fix AndroidView bug Issue: https://issuetracker.google.com/issues/267642562 --- gradle/libs.versions.toml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 80f80357..390a5455 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,13 +16,18 @@ material = "1.9.0" mapsktx = "4.0.0" mapsecrets = "2.0.1" +# We have to override this version because the current release has a +# bug related to AndroidView being re-used in LazyColumn. +# Related issue: https://issuetracker.google.com/issues/267642562 +compose-ui = "1.6.0-alpha07" + [libraries] android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "agp" } androidx-compose-activity = { module = "androidx.activity:activity-compose", version.ref = "activitycompose" } androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" } androidx-compose-foundation = { module = "androidx.compose.foundation:foundation" } androidx-compose-material = { module = "androidx.compose.material:material" } -androidx-compose-ui = { module = "androidx.compose.ui:ui" } +androidx-compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose-ui" } androidx-compose-ui-preview-tooling = { module = "androidx.compose.ui:ui-tooling-preview" } androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } androidx-core = { module = "androidx.core:core-ktx", version.require = "1.12.0" } From 6968d1e24a14d379c5bbb2ef4ca3ca8aa9affca1 Mon Sep 17 00:00:00 2001 From: Philip Date: Fri, 13 Oct 2023 11:56:11 +0200 Subject: [PATCH 003/103] Add MapsInLazyColumnActivity --- app/src/main/AndroidManifest.xml | 3 + .../maps/android/compose/MainActivity.kt | 7 ++ .../compose/MapsInLazyColumnActivity.kt | 80 +++++++++++++++++++ app/src/main/res/values/strings.xml | 1 + 4 files changed, 91 insertions(+) create mode 100644 app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 69d8deea..e008f6e4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -45,6 +45,9 @@ + 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 ff8e0bdc..35a77816 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 @@ -119,6 +119,13 @@ class MainActivity : ComponentActivity() { }) { Text(getString(R.string.custom_location_button)) } + Spacer(modifier = Modifier.padding(5.dp)) + Button( + onClick = { + context.startActivity(Intent(context, MapsInLazyColumnActivity::class.java)) + }) { + Text(getString(R.string.maps_in_lazy_column_activity)) + } } } } diff --git a/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt b/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt new file mode 100644 index 00000000..4e7f95cc --- /dev/null +++ b/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt @@ -0,0 +1,80 @@ +package com.google.maps.android.compose + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.Card +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.google.android.gms.maps.CameraUpdateFactory +import com.google.android.gms.maps.model.CameraPosition +import kotlinx.coroutines.delay + +private const val TAG = "MapsInLazyColumnActivity" + +class MapsInLazyColumnActivity: ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + MapsInLazyColumn() + } + } +} + +@Composable +private fun MapsInLazyColumn() { + LazyColumn(verticalArrangement = Arrangement.spacedBy(32.dp)) { + items(100) { index -> + Box( + Modifier + .fillMaxWidth() + .height(200.dp), + contentAlignment = Alignment.Center + ) { + MapCard(index) + } + } + } +} + +@Composable +private fun MapCard(index: Int) { + Card( + Modifier + .padding(16.dp) + .height(300.dp), elevation = 4.dp) { + Box { + MyMap(Modifier.fillMaxSize()) + Text("$index", modifier = Modifier + .align(Alignment.TopStart) + .padding(8.dp)) + } + } +} + +@Composable +private fun MyMap(modifier: Modifier) { + val cameraPositionState = rememberCameraPositionState(init = { position = defaultCameraPosition }) + + GoogleMap( + modifier = modifier, + cameraPositionState = cameraPositionState + ) +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dd10f260..276bd38d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -24,4 +24,5 @@ Scale Bar Street View Custom Location Button + Maps in LazyColumn \ No newline at end of file From edc75c46ec41e12fde3234fcd229a609020fddb4 Mon Sep 17 00:00:00 2001 From: Philip Date: Fri, 13 Oct 2023 13:23:42 +0200 Subject: [PATCH 004/103] Hoist list items state --- .../compose/MapsInLazyColumnActivity.kt | 105 ++++++++++++++---- .../google/maps/android/compose/GoogleMap.kt | 2 +- 2 files changed, 84 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt b/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt index 4e7f95cc..38d23476 100644 --- a/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt @@ -3,78 +3,139 @@ package com.google.maps.android.compose import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material.Card +import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Text import androidx.compose.runtime.Composable 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.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModel +import com.google.android.gms.maps.CameraUpdate import com.google.android.gms.maps.CameraUpdateFactory +import com.google.android.gms.maps.model.BitmapDescriptorFactory import com.google.android.gms.maps.model.CameraPosition +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.LatLngBounds import kotlinx.coroutines.delay +import kotlin.random.Random -private const val TAG = "MapsInLazyColumnActivity" +private val mapListItems = List(100) { index -> + val title = "Item #$index" + val centerLat = Random.nextDouble(-50.0, 75.0) + val centerLng = Random.nextDouble(-180.0, 180.0) + val zoom = Random.nextDouble(0.0, 21.0).toFloat() + MapListItem(title, LatLng(centerLat, centerLng), zoom) +} class MapsInLazyColumnActivity: ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { - MapsInLazyColumn() + MapsInLazyColumn(mapListItems) } } } +private data class MapListItem( + val title: String, + val location: LatLng, + val zoom: Float +) + @Composable -private fun MapsInLazyColumn() { - LazyColumn(verticalArrangement = Arrangement.spacedBy(32.dp)) { - items(100) { index -> +private fun MapsInLazyColumn(items: List) { + LazyColumn { + items(items) { item -> Box( Modifier .fillMaxWidth() - .height(200.dp), + .height(300.dp), contentAlignment = Alignment.Center ) { - MapCard(index) + MapCard(item) } } } } @Composable -private fun MapCard(index: Int) { +private fun MapCard(item: MapListItem) { Card( Modifier - .padding(16.dp) - .height(300.dp), elevation = 4.dp) { - Box { - MyMap(Modifier.fillMaxSize()) - Text("$index", modifier = Modifier - .align(Alignment.TopStart) - .padding(8.dp)) + .padding(16.dp), + elevation = 4.dp + ) { + Column { + Box { + MyMap( + Modifier.fillMaxSize(), + item + ) + Text( + item.title, + modifier = Modifier + .align(Alignment.TopStart) + .padding(8.dp)) + } } } } @Composable -private fun MyMap(modifier: Modifier) { +private fun MyMap( + modifier: Modifier, + mapItem: MapListItem +) { val cameraPositionState = rememberCameraPositionState(init = { position = defaultCameraPosition }) + var mapLoaded by remember { mutableStateOf(false) } + + LaunchedEffect(mapItem, mapLoaded) { + if(!mapLoaded) return@LaunchedEffect + + val cameraUpdate = CameraUpdateFactory.newLatLngZoom(mapItem.location, mapItem.zoom) + cameraPositionState.move(cameraUpdate) + } - GoogleMap( - modifier = modifier, - cameraPositionState = cameraPositionState - ) + Box { + GoogleMap( + modifier = modifier, + cameraPositionState = cameraPositionState, + onMapLoaded = { mapLoaded = true } + ) + AnimatedVisibility(!mapLoaded, enter = fadeIn(), exit = fadeOut()) { + Box( + Modifier + .fillMaxSize() + .background(Color.White), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + } } diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 3f45e735..63ef9b01 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -109,7 +109,7 @@ public fun GoogleMap( MapView(context, googleMapOptionsFactory()) }, onReset = { }, - onRelease = { it.destroyAndRemoveAllViews()}, + onRelease = { it.destroyAndRemoveAllViews() }, update = { mapViewOrNull = it } ) From 7c62ce7103f1fca539c35a195170349f40be2f69 Mon Sep 17 00:00:00 2001 From: Philip Date: Sat, 28 Oct 2023 22:54:37 +0200 Subject: [PATCH 005/103] Bump compose-ui dependency --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 390a5455..16a7ddf7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,7 +19,7 @@ mapsecrets = "2.0.1" # We have to override this version because the current release has a # bug related to AndroidView being re-used in LazyColumn. # Related issue: https://issuetracker.google.com/issues/267642562 -compose-ui = "1.6.0-alpha07" +compose-ui = "1.6.0-alpha08" [libraries] android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "agp" } From c1f13b0e02efb1d0e024b3025a0fa4794847e08e Mon Sep 17 00:00:00 2001 From: Philip Date: Sun, 29 Oct 2023 00:11:00 +0200 Subject: [PATCH 006/103] Update libs.versions.toml --- gradle/libs.versions.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 16a7ddf7..1a61fc08 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,8 +18,9 @@ mapsecrets = "2.0.1" # We have to override this version because the current release has a # bug related to AndroidView being re-used in LazyColumn. +# 1.6.0-alpha08 doesn't work. # Related issue: https://issuetracker.google.com/issues/267642562 -compose-ui = "1.6.0-alpha08" +compose-ui = "1.6.0-alpha07" [libraries] android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "agp" } From 6b85dab930745d80ffae1e4382d283e94b93243d Mon Sep 17 00:00:00 2001 From: Philip Date: Sun, 29 Oct 2023 13:40:29 +0100 Subject: [PATCH 007/103] Show list of countries in MapsInLazyColumnActivity --- .../compose/MapsInLazyColumnActivity.kt | 91 ++++++++++++------- 1 file changed, 57 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt b/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt index 38d23476..7c04b6d9 100644 --- a/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt @@ -1,17 +1,15 @@ 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.activity.viewModels import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -26,30 +24,19 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import androidx.lifecycle.ViewModel -import com.google.android.gms.maps.CameraUpdate import com.google.android.gms.maps.CameraUpdateFactory -import com.google.android.gms.maps.model.BitmapDescriptorFactory import com.google.android.gms.maps.model.CameraPosition import com.google.android.gms.maps.model.LatLng -import com.google.android.gms.maps.model.LatLngBounds import kotlinx.coroutines.delay import kotlin.random.Random -private val mapListItems = List(100) { index -> - val title = "Item #$index" - val centerLat = Random.nextDouble(-50.0, 75.0) - val centerLng = Random.nextDouble(-180.0, 180.0) - val zoom = Random.nextDouble(0.0, 21.0).toFloat() - MapListItem(title, LatLng(centerLat, centerLng), zoom) -} +data class CountryLocation(val name: String, val latLng: LatLng, val zoom: Float) class MapsInLazyColumnActivity: ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -61,16 +48,10 @@ class MapsInLazyColumnActivity: ComponentActivity() { } } -private data class MapListItem( - val title: String, - val location: LatLng, - val zoom: Float -) - @Composable -private fun MapsInLazyColumn(items: List) { +private fun MapsInLazyColumn(mapListItems: List) { LazyColumn { - items(items) { item -> + items(mapListItems) { item -> Box( Modifier .fillMaxWidth() @@ -100,7 +81,9 @@ private fun MapCard(item: MapListItem) { item.title, modifier = Modifier .align(Alignment.TopStart) - .padding(8.dp)) + .padding(8.dp) + .background(Color.White.copy(0.8f)) + ) } } } @@ -111,22 +94,21 @@ private fun MyMap( modifier: Modifier, mapItem: MapListItem ) { - val cameraPositionState = rememberCameraPositionState(init = { position = defaultCameraPosition }) var mapLoaded by remember { mutableStateOf(false) } - - LaunchedEffect(mapItem, mapLoaded) { - if(!mapLoaded) return@LaunchedEffect - - val cameraUpdate = CameraUpdateFactory.newLatLngZoom(mapItem.location, mapItem.zoom) - cameraPositionState.move(cameraUpdate) - } + val cameraPositionState = rememberCameraPositionState( + key = mapItem.id, + init = { position = CameraPosition.fromLatLngZoom(mapItem.location, mapItem.zoom) } + ) Box { GoogleMap( modifier = modifier, cameraPositionState = cameraPositionState, onMapLoaded = { mapLoaded = true } - ) + ) { + Marker(rememberMarkerState(position = mapItem.location)) + } + AnimatedVisibility(!mapLoaded, enter = fadeIn(), exit = fadeOut()) { Box( Modifier @@ -139,3 +121,44 @@ private fun MyMap( } } } + +private data class MapListItem( + val title: String, + val location: LatLng, + val zoom: Float, + val id: String +) + +// From https://developers.google.com/public-data/docs/canonical/countries_csv +private val countries = listOf( + CountryLocation("Hong Kong", LatLng(22.396428, 114.109497), 5f), + CountryLocation("Bolivia", LatLng(-16.290154, -63.588653), 5f), + CountryLocation("Ecuador", LatLng(-1.831239, -78.183406), 5f), + CountryLocation("Sweden", LatLng(60.128161, 18.643501), 5f), + CountryLocation("Eritrea", LatLng(15.179384, 39.782334), 5f), + CountryLocation("Portugal", LatLng(39.399872, -8.224454), 5f), + CountryLocation("Belgium", LatLng(50.503887, 4.469936), 5f), + CountryLocation("Slovakia", LatLng(48.669026, 19.699024), 5f), + CountryLocation("El Salvador", LatLng(13.794185, -88.89653), 5f), + CountryLocation("Bhutan", LatLng(27.514162, 90.433601), 5f), + CountryLocation("Saint Lucia", LatLng(13.909444, -60.978893), 5f), + CountryLocation("Uganda", LatLng(1.373333, 32.290275), 5f), + CountryLocation("South Africa", LatLng(-30.559482, 22.937506), 5f), + CountryLocation("Spain", LatLng(40.463667, -3.74922), 5f), + CountryLocation("Georgia", LatLng(42.315407, 43.356892), 5f), + CountryLocation("Burundi", LatLng(-3.373056, 29.918886), 5f), + CountryLocation("Christmas Island", LatLng(-10.447525, 105.690449), 5f), + CountryLocation("Vanuatu", LatLng(-15.376706, 166.959158), 5f), + CountryLocation("Jersey", LatLng(49.214439, -2.13125), 5f), + CountryLocation("Svalbard and Jan Mayen", LatLng(77.553604, 23.670272), 5f), + CountryLocation("American Samoa", LatLng(-14.270972, -170.132217), 5f), + CountryLocation("Moldova", LatLng(47.411631, 28.369885), 5f), + CountryLocation("Bouvet Island", LatLng(-54.423199, 3.413194), 5f), + CountryLocation("Puerto Rico", LatLng(18.220833, -66.590149), 5f), + CountryLocation("Colombia", LatLng(4.570868, -74.297333), 5f), +) + +private val mapListItems = countries + .mapIndexed { index, country -> + MapListItem(country.name, country.latLng, country.zoom, "MapInLazyColumn#$index") +} \ No newline at end of file From 9c4dd298087cb4e025f3c90450ae5ee970cdb832 Mon Sep 17 00:00:00 2001 From: Philip Date: Sun, 29 Oct 2023 13:52:14 +0100 Subject: [PATCH 008/103] Cleanup --- .../compose/MapsInLazyColumnActivity.kt | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt b/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt index 7c04b6d9..0df27978 100644 --- a/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt @@ -1,7 +1,6 @@ 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.animation.AnimatedVisibility @@ -20,36 +19,29 @@ import androidx.compose.material.Card import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import com.google.android.gms.maps.CameraUpdateFactory import com.google.android.gms.maps.model.CameraPosition import com.google.android.gms.maps.model.LatLng -import kotlinx.coroutines.delay -import kotlin.random.Random - -data class CountryLocation(val name: String, val latLng: LatLng, val zoom: Float) class MapsInLazyColumnActivity: ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { - MapsInLazyColumn(mapListItems) + MapsInLazyColumn() } } } @Composable -private fun MapsInLazyColumn(mapListItems: List) { +private fun MapsInLazyColumn() { LazyColumn { items(mapListItems) { item -> Box( @@ -73,7 +65,7 @@ private fun MapCard(item: MapListItem) { ) { Column { Box { - MyMap( + CardMap( Modifier.fillMaxSize(), item ) @@ -90,7 +82,7 @@ private fun MapCard(item: MapListItem) { } @Composable -private fun MyMap( +private fun CardMap( modifier: Modifier, mapItem: MapListItem ) { @@ -161,4 +153,6 @@ private val countries = listOf( private val mapListItems = countries .mapIndexed { index, country -> MapListItem(country.name, country.latLng, country.zoom, "MapInLazyColumn#$index") -} \ No newline at end of file +} + +private data class CountryLocation(val name: String, val latLng: LatLng, val zoom: Float) \ No newline at end of file From 2f5f0b639ad1429c54d640de31a63bcc9bec5227 Mon Sep 17 00:00:00 2001 From: Philip Date: Thu, 1 Feb 2024 17:16:57 +0100 Subject: [PATCH 009/103] Remove compose-ui dependency version override --- gradle/libs.versions.toml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b455bf2a..57a8f35a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,12 +16,6 @@ material = "1.9.0" mapsktx = "5.0.0" mapsecrets = "2.0.1" -# We have to override this version because the current release has a -# bug related to AndroidView being re-used in LazyColumn. -# 1.6.0-alpha08 doesn't work. -# Related issue: https://issuetracker.google.com/issues/267642562 -compose-ui = "1.6.0-alpha07" - [libraries] android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "agp" } androidx-compose-activity = { module = "androidx.activity:activity-compose", version.ref = "activitycompose" } From f38a3a13e5569dff5d8f6b2cd60673a26b5e79e9 Mon Sep 17 00:00:00 2001 From: Philip Date: Thu, 1 Feb 2024 17:26:38 +0100 Subject: [PATCH 010/103] Update libs.versions.toml --- gradle/libs.versions.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 57a8f35a..7f35f8c3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,6 @@ androidx-compose-activity = { module = "androidx.activity:activity-compose", ver androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" } androidx-compose-foundation = { module = "androidx.compose.foundation:foundation" } androidx-compose-material = { module = "androidx.compose.material:material" } -androidx-compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose-ui" } androidx-compose-ui-preview-tooling = { module = "androidx.compose.ui:ui-tooling-preview" } androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } androidx-core = { module = "androidx.core:core-ktx", version.require = "1.12.0" } From 44c524fb2d92b468cf1de88caf515e17d10493e9 Mon Sep 17 00:00:00 2001 From: Philip Date: Thu, 1 Feb 2024 17:28:44 +0100 Subject: [PATCH 011/103] Add parameter to GoogleMap to allow user to re-use the underlying MapView --- .../google/maps/android/compose/GoogleMap.kt | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index aaa6d252..0276fc4d 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -69,6 +69,8 @@ import kotlinx.coroutines.awaitCancellation * @param onPOIClick lambda invoked when a POI is clicked * @param contentPadding the padding values used to signal that portions of the map around the edges * may be obscured. The map will move the Google logo, etc. to avoid overlapping the padding. + * @param reuseMapView whether the underlying MapView will be reused. Optimized for lazy layouts. + * Can have a very slight impact on initial initialization time if enabled. * @param content the content of the map */ @Composable @@ -88,6 +90,7 @@ public fun GoogleMap( onMyLocationClick: ((Location) -> Unit)? = null, onPOIClick: ((PointOfInterest) -> Unit)? = null, contentPadding: PaddingValues = NoPadding, + reuseMapView: Boolean = false, content: (@Composable @GoogleMapComposable () -> Unit)? = null, ) { // When in preview, early return a Box with the received modifier preserving layout @@ -96,6 +99,64 @@ public fun GoogleMap( return } + if(reuseMapView) { + ReusableGoogleMap( + cameraPositionState = cameraPositionState, + contentDescription = contentDescription, + googleMapOptionsFactory = googleMapOptionsFactory, + properties = properties, + locationSource = locationSource, + uiSettings = uiSettings, + indoorStateChangeListener = indoorStateChangeListener, + onMapClick = onMapClick, + onMapLongClick = onMapLongClick, + onMapLoaded = onMapLoaded, + onMyLocationButtonClick = onMyLocationButtonClick, + onMyLocationClick = onMyLocationClick, + onPOIClick = onPOIClick, + contentPadding = contentPadding, + content = content, + ) + } else { + FastGoogleMap( + cameraPositionState = cameraPositionState, + contentDescription = contentDescription, + googleMapOptionsFactory = googleMapOptionsFactory, + properties = properties, + locationSource = locationSource, + uiSettings = uiSettings, + indoorStateChangeListener = indoorStateChangeListener, + onMapClick = onMapClick, + onMapLongClick = onMapLongClick, + onMapLoaded = onMapLoaded, + onMyLocationButtonClick = onMyLocationButtonClick, + onMyLocationClick = onMyLocationClick, + onPOIClick = onPOIClick, + contentPadding = contentPadding, + content = content, + ) + } +} + +@Composable +private fun ReusableGoogleMap( + modifier: Modifier = Modifier, + cameraPositionState: CameraPositionState = rememberCameraPositionState(), + contentDescription: String? = null, + googleMapOptionsFactory: () -> GoogleMapOptions = { GoogleMapOptions() }, + properties: MapProperties = DefaultMapProperties, + locationSource: LocationSource? = null, + uiSettings: MapUiSettings = DefaultMapUiSettings, + indoorStateChangeListener: IndoorStateChangeListener = DefaultIndoorStateChangeListener, + onMapClick: ((LatLng) -> Unit)? = null, + onMapLongClick: ((LatLng) -> Unit)? = null, + onMapLoaded: (() -> Unit)? = null, + onMyLocationButtonClick: (() -> Boolean)? = null, + onMyLocationClick: ((Location) -> Unit)? = null, + onPOIClick: ((PointOfInterest) -> Unit)? = null, + contentPadding: PaddingValues = NoPadding, + content: (@Composable @GoogleMapComposable () -> Unit)? = null, +) { // Will either be set to a re-used or a new MapView var mapViewOrNull: MapView? by remember { mutableStateOf(null) } var isMapViewReused by remember { mutableStateOf(true) } @@ -116,6 +177,92 @@ public fun GoogleMap( MapLifecycle(mapView, isMapViewReused) + ApplyMapConfiguration( + mapView, + cameraPositionState, + contentDescription, + properties, + locationSource, + uiSettings, + indoorStateChangeListener, + onMapClick, + onMapLongClick, + onMapLoaded, + onMyLocationButtonClick, + onMyLocationClick, + onPOIClick, + contentPadding, + content + ) +} + +/** + * This [GoogleMap] overload doesn't re-use the underlying MapView between composables but + * could be slightly faster than using [ReusableGoogleMap] + * */ +@Composable +private fun FastGoogleMap( + modifier: Modifier = Modifier, + cameraPositionState: CameraPositionState = rememberCameraPositionState(), + contentDescription: String? = null, + googleMapOptionsFactory: () -> GoogleMapOptions = { GoogleMapOptions() }, + properties: MapProperties = DefaultMapProperties, + locationSource: LocationSource? = null, + uiSettings: MapUiSettings = DefaultMapUiSettings, + indoorStateChangeListener: IndoorStateChangeListener = DefaultIndoorStateChangeListener, + onMapClick: ((LatLng) -> Unit)? = null, + onMapLongClick: ((LatLng) -> Unit)? = null, + onMapLoaded: (() -> Unit)? = null, + onMyLocationButtonClick: (() -> Boolean)? = null, + onMyLocationClick: ((Location) -> Unit)? = null, + onPOIClick: ((PointOfInterest) -> Unit)? = null, + contentPadding: PaddingValues = NoPadding, + content: (@Composable @GoogleMapComposable () -> Unit)? = null, +) { + val context = LocalContext.current + val mapView = remember { MapView(context, googleMapOptionsFactory()) } + + AndroidView(modifier = modifier, factory = { mapView }) + + MapLifecycle(mapView = mapView, isMapViewReused = false) + + ApplyMapConfiguration( + mapView, + cameraPositionState, + contentDescription, + properties, + locationSource, + uiSettings, + indoorStateChangeListener, + onMapClick, + onMapLongClick, + onMapLoaded, + onMyLocationButtonClick, + onMyLocationClick, + onPOIClick, + contentPadding, + content + ) +} + +@Composable +private fun ApplyMapConfiguration( + mapView: MapView, + cameraPositionState: CameraPositionState = rememberCameraPositionState(), + contentDescription: String? = null, + properties: MapProperties = DefaultMapProperties, + locationSource: LocationSource? = null, + uiSettings: MapUiSettings = DefaultMapUiSettings, + indoorStateChangeListener: IndoorStateChangeListener = DefaultIndoorStateChangeListener, + onMapClick: ((LatLng) -> Unit)? = null, + onMapLongClick: ((LatLng) -> Unit)? = null, + onMapLoaded: (() -> Unit)? = null, + onMyLocationButtonClick: (() -> Boolean)? = null, + onMyLocationClick: ((Location) -> Unit)? = null, + onPOIClick: ((PointOfInterest) -> Unit)? = null, + contentPadding: PaddingValues = NoPadding, + content: (@Composable @GoogleMapComposable () -> Unit)? = null, +) { // rememberUpdatedState and friends are used here to make these values observable to // the subcomposition without providing a new content function each recomposition val mapClickListeners = remember { MapClickListeners() }.also { From 5be5a18737b0d08c0cc22b81c31177c84bb6de1a Mon Sep 17 00:00:00 2001 From: Philip Date: Thu, 1 Feb 2024 18:51:38 +0100 Subject: [PATCH 012/103] Update libs.versions.toml --- gradle/libs.versions.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4e1209fa..fb1bceb9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,6 +22,7 @@ androidx-compose-activity = { module = "androidx.activity:activity-compose", ver androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" } androidx-compose-foundation = { module = "androidx.compose.foundation:foundation" } androidx-compose-material = { module = "androidx.compose.material:material" } +androidx-compose-ui = { module = "androidx.compose.ui:ui" } androidx-compose-ui-preview-tooling = { module = "androidx.compose.ui:ui-tooling-preview" } androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } androidx-core = { module = "androidx.core:core-ktx", version.require = "1.12.0" } From 3b1f79e69f99018f8cbb34facf3697542539885c Mon Sep 17 00:00:00 2001 From: Philip Date: Thu, 1 Feb 2024 18:51:53 +0100 Subject: [PATCH 013/103] Update MapsInLazyColumnActivity.kt --- .../com/google/maps/android/compose/MapsInLazyColumnActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt b/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt index 0df27978..821b02b8 100644 --- a/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt @@ -98,7 +98,7 @@ private fun CardMap( cameraPositionState = cameraPositionState, onMapLoaded = { mapLoaded = true } ) { - Marker(rememberMarkerState(position = mapItem.location)) + Marker(state = rememberMarkerState(position = mapItem.location)) } AnimatedVisibility(!mapLoaded, enter = fadeIn(), exit = fadeOut()) { From eb41c7219788bf1f3454d0d41dded80afd74fed1 Mon Sep 17 00:00:00 2001 From: Philip Date: Thu, 1 Feb 2024 18:52:14 +0100 Subject: [PATCH 014/103] Fix merge issues --- .../google/maps/android/compose/GoogleMap.kt | 73 +++++++++++-------- 1 file changed, 41 insertions(+), 32 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 95c2e2c3..fadcb65a 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -103,6 +103,8 @@ public fun GoogleMap( if(reuseMapView) { ReusableGoogleMap( + modifier = modifier, + mergeDescendants = mergeDescendants, cameraPositionState = cameraPositionState, contentDescription = contentDescription, googleMapOptionsFactory = googleMapOptionsFactory, @@ -121,6 +123,8 @@ public fun GoogleMap( ) } else { FastGoogleMap( + modifier = modifier, + mergeDescendants = mergeDescendants, cameraPositionState = cameraPositionState, contentDescription = contentDescription, googleMapOptionsFactory = googleMapOptionsFactory, @@ -142,22 +146,23 @@ public fun GoogleMap( @Composable private fun ReusableGoogleMap( - modifier: Modifier = Modifier, - cameraPositionState: CameraPositionState = rememberCameraPositionState(), - contentDescription: String? = null, - googleMapOptionsFactory: () -> GoogleMapOptions = { GoogleMapOptions() }, - properties: MapProperties = DefaultMapProperties, - locationSource: LocationSource? = null, - uiSettings: MapUiSettings = DefaultMapUiSettings, - indoorStateChangeListener: IndoorStateChangeListener = DefaultIndoorStateChangeListener, - onMapClick: ((LatLng) -> Unit)? = null, - onMapLongClick: ((LatLng) -> Unit)? = null, - onMapLoaded: (() -> Unit)? = null, - onMyLocationButtonClick: (() -> Boolean)? = null, - onMyLocationClick: ((Location) -> Unit)? = null, - onPOIClick: ((PointOfInterest) -> Unit)? = null, - contentPadding: PaddingValues = NoPadding, - content: (@Composable @GoogleMapComposable () -> Unit)? = null, + modifier: Modifier, + mergeDescendants: Boolean, + cameraPositionState: CameraPositionState, + contentDescription: String?, + googleMapOptionsFactory: () -> GoogleMapOptions, + properties: MapProperties, + locationSource: LocationSource?, + uiSettings: MapUiSettings, + indoorStateChangeListener: IndoorStateChangeListener, + onMapClick: ((LatLng) -> Unit)?, + onMapLongClick: ((LatLng) -> Unit)?, + onMapLoaded: (() -> Unit)?, + onMyLocationButtonClick: (() -> Boolean)?, + onMyLocationClick: ((Location) -> Unit)?, + onPOIClick: ((PointOfInterest) -> Unit)?, + contentPadding: PaddingValues, + content: (@Composable @GoogleMapComposable () -> Unit)?, ) { // Will either be set to a re-used or a new MapView var mapViewOrNull: MapView? by remember { mutableStateOf(null) } @@ -180,6 +185,7 @@ private fun ReusableGoogleMap( MapLifecycle(mapView, isMapViewReused) ApplyMapConfiguration( + mergeDescendants, mapView, cameraPositionState, contentDescription, @@ -204,22 +210,23 @@ private fun ReusableGoogleMap( * */ @Composable private fun FastGoogleMap( - modifier: Modifier = Modifier, - cameraPositionState: CameraPositionState = rememberCameraPositionState(), - contentDescription: String? = null, - googleMapOptionsFactory: () -> GoogleMapOptions = { GoogleMapOptions() }, - properties: MapProperties = DefaultMapProperties, - locationSource: LocationSource? = null, - uiSettings: MapUiSettings = DefaultMapUiSettings, - indoorStateChangeListener: IndoorStateChangeListener = DefaultIndoorStateChangeListener, - onMapClick: ((LatLng) -> Unit)? = null, - onMapLongClick: ((LatLng) -> Unit)? = null, - onMapLoaded: (() -> Unit)? = null, - onMyLocationButtonClick: (() -> Boolean)? = null, - onMyLocationClick: ((Location) -> Unit)? = null, - onPOIClick: ((PointOfInterest) -> Unit)? = null, - contentPadding: PaddingValues = NoPadding, - content: (@Composable @GoogleMapComposable () -> Unit)? = null, + mergeDescendants: Boolean, + modifier: Modifier, + cameraPositionState: CameraPositionState, + contentDescription: String?, + googleMapOptionsFactory: () -> GoogleMapOptions, + properties: MapProperties, + locationSource: LocationSource?, + uiSettings: MapUiSettings, + indoorStateChangeListener: IndoorStateChangeListener, + onMapClick: ((LatLng) -> Unit)?, + onMapLongClick: ((LatLng) -> Unit)?, + onMapLoaded: (() -> Unit)?, + onMyLocationButtonClick: (() -> Boolean)?, + onMyLocationClick: ((Location) -> Unit)?, + onPOIClick: ((PointOfInterest) -> Unit)?, + contentPadding: PaddingValues, + content: (@Composable @GoogleMapComposable () -> Unit)?, ) { val context = LocalContext.current val mapView = remember { MapView(context, googleMapOptionsFactory()) } @@ -229,6 +236,7 @@ private fun FastGoogleMap( MapLifecycle(mapView = mapView, isMapViewReused = false) ApplyMapConfiguration( + mergeDescendants, mapView, cameraPositionState, contentDescription, @@ -249,6 +257,7 @@ private fun FastGoogleMap( @Composable private fun ApplyMapConfiguration( + mergeDescendants: Boolean, mapView: MapView, cameraPositionState: CameraPositionState = rememberCameraPositionState(), contentDescription: String? = null, From c82853cf5e8fe4f43111391aae8303bb830d9c3b Mon Sep 17 00:00:00 2001 From: Philip Date: Sun, 4 Feb 2024 17:29:09 +0100 Subject: [PATCH 015/103] Refactor the GoogleMap composable to support reuse of underlying map+ remove LaunchedEffect block --- .../compose/MapsInLazyColumnActivity.kt | 2 +- .../google/maps/android/compose/GoogleMap.kt | 299 +++++------------- 2 files changed, 81 insertions(+), 220 deletions(-) diff --git a/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt b/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt index 821b02b8..83f707a2 100644 --- a/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt @@ -43,7 +43,7 @@ class MapsInLazyColumnActivity: ComponentActivity() { @Composable private fun MapsInLazyColumn() { LazyColumn { - items(mapListItems) { item -> + items(mapListItems, key = { it.id }) { item -> Box( Modifier .fillMaxWidth() diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index fadcb65a..7ee7e105 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -24,13 +24,11 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Composition import androidx.compose.runtime.CompositionContext import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCompositionContext +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier @@ -46,7 +44,9 @@ import com.google.android.gms.maps.MapView import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.PointOfInterest import com.google.maps.android.ktx.awaitMap +import kotlinx.coroutines.Job import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.launch /** * A compose container for a [MapView]. @@ -70,8 +70,6 @@ import kotlinx.coroutines.awaitCancellation * @param onPOIClick lambda invoked when a POI is clicked * @param contentPadding the padding values used to signal that portions of the map around the edges * may be obscured. The map will move the Google logo, etc. to avoid overlapping the padding. - * @param reuseMapView whether the underlying MapView will be reused. Optimized for lazy layouts. - * Can have a very slight impact on initial initialization time if enabled. * @param content the content of the map */ @Composable @@ -92,7 +90,6 @@ public fun GoogleMap( onMyLocationClick: ((Location) -> Unit)? = null, onPOIClick: ((PointOfInterest) -> Unit)? = null, contentPadding: PaddingValues = NoPadding, - reuseMapView: Boolean = false, content: (@Composable @GoogleMapComposable () -> Unit)? = null, ) { // When in preview, early return a Box with the received modifier preserving layout @@ -101,179 +98,6 @@ public fun GoogleMap( return } - if(reuseMapView) { - ReusableGoogleMap( - modifier = modifier, - mergeDescendants = mergeDescendants, - cameraPositionState = cameraPositionState, - contentDescription = contentDescription, - googleMapOptionsFactory = googleMapOptionsFactory, - properties = properties, - locationSource = locationSource, - uiSettings = uiSettings, - indoorStateChangeListener = indoorStateChangeListener, - onMapClick = onMapClick, - onMapLongClick = onMapLongClick, - onMapLoaded = onMapLoaded, - onMyLocationButtonClick = onMyLocationButtonClick, - onMyLocationClick = onMyLocationClick, - onPOIClick = onPOIClick, - contentPadding = contentPadding, - content = content, - ) - } else { - FastGoogleMap( - modifier = modifier, - mergeDescendants = mergeDescendants, - cameraPositionState = cameraPositionState, - contentDescription = contentDescription, - googleMapOptionsFactory = googleMapOptionsFactory, - properties = properties, - locationSource = locationSource, - uiSettings = uiSettings, - indoorStateChangeListener = indoorStateChangeListener, - onMapClick = onMapClick, - onMapLongClick = onMapLongClick, - onMapLoaded = onMapLoaded, - onMyLocationButtonClick = onMyLocationButtonClick, - onMyLocationClick = onMyLocationClick, - onPOIClick = onPOIClick, - contentPadding = contentPadding, - content = content, - ) - } -} - -@Composable -private fun ReusableGoogleMap( - modifier: Modifier, - mergeDescendants: Boolean, - cameraPositionState: CameraPositionState, - contentDescription: String?, - googleMapOptionsFactory: () -> GoogleMapOptions, - properties: MapProperties, - locationSource: LocationSource?, - uiSettings: MapUiSettings, - indoorStateChangeListener: IndoorStateChangeListener, - onMapClick: ((LatLng) -> Unit)?, - onMapLongClick: ((LatLng) -> Unit)?, - onMapLoaded: (() -> Unit)?, - onMyLocationButtonClick: (() -> Boolean)?, - onMyLocationClick: ((Location) -> Unit)?, - onPOIClick: ((PointOfInterest) -> Unit)?, - contentPadding: PaddingValues, - content: (@Composable @GoogleMapComposable () -> Unit)?, -) { - // Will either be set to a re-used or a new MapView - var mapViewOrNull: MapView? by remember { mutableStateOf(null) } - var isMapViewReused by remember { mutableStateOf(true) } - - AndroidView( - modifier = modifier, - factory = { context -> - isMapViewReused = false - MapView(context, googleMapOptionsFactory()) - }, - onReset = { }, - onRelease = { it.destroyAndRemoveAllViews() }, - update = { mapViewOrNull = it } - ) - - // Wait until we have a MapView - val mapView = mapViewOrNull ?: return - - MapLifecycle(mapView, isMapViewReused) - - ApplyMapConfiguration( - mergeDescendants, - mapView, - cameraPositionState, - contentDescription, - properties, - locationSource, - uiSettings, - indoorStateChangeListener, - onMapClick, - onMapLongClick, - onMapLoaded, - onMyLocationButtonClick, - onMyLocationClick, - onPOIClick, - contentPadding, - content - ) -} - -/** - * This [GoogleMap] overload doesn't re-use the underlying MapView between composables but - * could be slightly faster than using [ReusableGoogleMap] - * */ -@Composable -private fun FastGoogleMap( - mergeDescendants: Boolean, - modifier: Modifier, - cameraPositionState: CameraPositionState, - contentDescription: String?, - googleMapOptionsFactory: () -> GoogleMapOptions, - properties: MapProperties, - locationSource: LocationSource?, - uiSettings: MapUiSettings, - indoorStateChangeListener: IndoorStateChangeListener, - onMapClick: ((LatLng) -> Unit)?, - onMapLongClick: ((LatLng) -> Unit)?, - onMapLoaded: (() -> Unit)?, - onMyLocationButtonClick: (() -> Boolean)?, - onMyLocationClick: ((Location) -> Unit)?, - onPOIClick: ((PointOfInterest) -> Unit)?, - contentPadding: PaddingValues, - content: (@Composable @GoogleMapComposable () -> Unit)?, -) { - val context = LocalContext.current - val mapView = remember { MapView(context, googleMapOptionsFactory()) } - - AndroidView(modifier = modifier, factory = { mapView }) - - MapLifecycle(mapView = mapView, isMapViewReused = false) - - ApplyMapConfiguration( - mergeDescendants, - mapView, - cameraPositionState, - contentDescription, - properties, - locationSource, - uiSettings, - indoorStateChangeListener, - onMapClick, - onMapLongClick, - onMapLoaded, - onMyLocationButtonClick, - onMyLocationClick, - onPOIClick, - contentPadding, - content - ) -} - -@Composable -private fun ApplyMapConfiguration( - mergeDescendants: Boolean, - mapView: MapView, - cameraPositionState: CameraPositionState = rememberCameraPositionState(), - contentDescription: String? = null, - properties: MapProperties = DefaultMapProperties, - locationSource: LocationSource? = null, - uiSettings: MapUiSettings = DefaultMapUiSettings, - indoorStateChangeListener: IndoorStateChangeListener = DefaultIndoorStateChangeListener, - onMapClick: ((LatLng) -> Unit)? = null, - onMapLongClick: ((LatLng) -> Unit)? = null, - onMapLoaded: (() -> Unit)? = null, - onMyLocationButtonClick: (() -> Boolean)? = null, - onMyLocationClick: ((Location) -> Unit)? = null, - onPOIClick: ((PointOfInterest) -> Unit)? = null, - contentPadding: PaddingValues = NoPadding, - content: (@Composable @GoogleMapComposable () -> Unit)? = null, -) { // rememberUpdatedState and friends are used here to make these values observable to // the subcomposition without providing a new content function each recomposition val mapClickListeners = remember { MapClickListeners() }.also { @@ -294,7 +118,9 @@ private fun ApplyMapConfiguration( val parentComposition = rememberCompositionContext() val currentContent by rememberUpdatedState(content) - LaunchedEffect(Unit) { + val mapUpdaterScope = rememberCoroutineScope() + + fun launchMapUpdaterJob(mapView: MapView) = mapUpdaterScope.launch { disposingComposition { mapView.newComposition(parentComposition, mapClickListeners) { MapUpdater( @@ -317,6 +143,52 @@ private fun ApplyMapConfiguration( } } } + + val lifecycle = LocalLifecycleOwner.current.lifecycle + val context = LocalContext.current + var mapLifecycleController: MapViewLifecycleController? by remember { mutableStateOf(null) } + var componentCallbacks: ComponentCallbacks? by remember { mutableStateOf(null) } + var mapUpdaterJob: Job? by remember { mutableStateOf(null) } + + AndroidView( + modifier = modifier, + factory = { + MapView(context, googleMapOptionsFactory()).also { mapView -> + mapLifecycleController = MapViewLifecycleController( + mapView = mapView, + isViewReused = false + ) + componentCallbacks = mapView.componentCallbacks() + } + }, + onReset = { + mapLifecycleController!!.lifecycle = null + context.unregisterComponentCallbacks(componentCallbacks) + }, + onRelease = { mapView -> + // MapView will never be used again and should be destroyed. + mapLifecycleController!!.onDestroy() + mapView.removeAllViews() + context.unregisterComponentCallbacks(componentCallbacks) + }, + update = { mapView -> + if (mapLifecycleController == null) { + mapLifecycleController = MapViewLifecycleController( + mapView = mapView, + isViewReused = true + ) + } + + if (componentCallbacks == null) { + componentCallbacks = mapView.componentCallbacks() + } + + mapLifecycleController!!.lifecycle = lifecycle + + if (mapUpdaterJob == null) + mapUpdaterJob = launchMapUpdaterJob(mapView) + } + ) } internal suspend inline fun disposingComposition(factory: () -> Composition) { @@ -341,64 +213,53 @@ private suspend inline fun MapView.newComposition( } } -private fun MapView.destroyAndRemoveAllViews() { - onDestroy() - removeAllViews() -} - -/** - * Registers lifecycle observers to the local [MapView]. - */ -@Composable -private fun MapLifecycle(mapView: MapView, isMapViewReused: Boolean) { - val context = LocalContext.current - val lifecycle = LocalLifecycleOwner.current.lifecycle - val previousState = remember { - // If mapView is re-used then ON_CREATE should not be invoked on it again - val initialState = if(isMapViewReused) Lifecycle.Event.ON_STOP else Lifecycle.Event.ON_CREATE - mutableStateOf(initialState) - } - - DisposableEffect(context, lifecycle, mapView) { - val mapLifecycleObserver = mapView.lifecycleObserver(previousState) - val callbacks = mapView.componentCallbacks() - - lifecycle.addObserver(mapLifecycleObserver) - context.registerComponentCallbacks(callbacks) +private class MapViewLifecycleController( + private val mapView: MapView, + isViewReused: Boolean +) { + private var previousState = if (isViewReused) + Lifecycle.Event.ON_STOP else + Lifecycle.Event.ON_CREATE - onDispose { - lifecycle.removeObserver(mapLifecycleObserver) - context.unregisterComponentCallbacks(callbacks) - } + fun onDestroy() { + lifecycle = null + mapView.onDestroy() } -} -private fun MapView.lifecycleObserver(previousState: MutableState): LifecycleEventObserver = - LifecycleEventObserver { _, event -> - event.targetState + private val observer = LifecycleEventObserver { _, event -> when (event) { Lifecycle.Event.ON_CREATE -> { // Skip calling mapView.onCreate if the lifecycle did not go through onDestroy - in // this case the GoogleMap composable also doesn't leave the composition. So, // recreating the map does not restore state properly which must be avoided. - if (previousState.value != Lifecycle.Event.ON_STOP) { - this.onCreate(Bundle()) + if (previousState != Lifecycle.Event.ON_STOP) { + mapView.onCreate(Bundle()) } } - Lifecycle.Event.ON_START -> this.onStart() - Lifecycle.Event.ON_RESUME -> this.onResume() - Lifecycle.Event.ON_PAUSE -> this.onPause() - Lifecycle.Event.ON_STOP -> this.onStop() + Lifecycle.Event.ON_START -> mapView.onStart() + Lifecycle.Event.ON_RESUME -> mapView.onResume() + Lifecycle.Event.ON_PAUSE -> mapView.onPause() + Lifecycle.Event.ON_STOP -> mapView.onStop() Lifecycle.Event.ON_DESTROY -> { // Handled in AndroidView onRelease } else -> throw IllegalStateException() } - previousState.value = event + previousState = event } + var lifecycle: Lifecycle? = null + set(value) { + if (field !== value) { + field?.removeObserver(observer) + value?.addObserver(observer) + field = value + } + } +} + private fun MapView.componentCallbacks(): ComponentCallbacks = object : ComponentCallbacks { override fun onConfigurationChanged(config: Configuration) {} From 267e62b53b38df767e8f5d6595c3c58bec4690c6 Mon Sep 17 00:00:00 2001 From: Philip Date: Tue, 6 Feb 2024 21:14:00 +0100 Subject: [PATCH 016/103] Replace ComposeNode usages with ReusableComposeNode --- .../src/main/java/com/google/maps/android/compose/Circle.kt | 4 ++-- .../java/com/google/maps/android/compose/GroundOverlay.kt | 6 +++--- .../java/com/google/maps/android/compose/InputHandler.kt | 6 +++--- .../main/java/com/google/maps/android/compose/MapUpdater.kt | 4 ++-- .../src/main/java/com/google/maps/android/compose/Marker.kt | 6 +++--- .../main/java/com/google/maps/android/compose/Polygon.kt | 4 ++-- .../main/java/com/google/maps/android/compose/Polyline.kt | 4 ++-- .../java/com/google/maps/android/compose/TileOverlay.kt | 4 ++-- .../android/compose/streetview/StreetViewPanoramaUpdater.kt | 4 ++-- 9 files changed, 21 insertions(+), 21 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/Circle.kt b/maps-compose/src/main/java/com/google/maps/android/compose/Circle.kt index 237af045..dedd5878 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/Circle.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/Circle.kt @@ -15,7 +15,7 @@ package com.google.maps.android.compose import androidx.compose.runtime.Composable -import androidx.compose.runtime.ComposeNode +import androidx.compose.runtime.ReusableComposeNode import androidx.compose.runtime.currentComposer import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb @@ -65,7 +65,7 @@ public fun Circle( onClick: (Circle) -> Unit = {}, ) { val mapApplier = currentComposer.applier as? MapApplier - ComposeNode( + ReusableComposeNode( factory = { val circle = mapApplier?.map?.addCircle { center(center) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GroundOverlay.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GroundOverlay.kt index 89828218..6bd83b3a 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GroundOverlay.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GroundOverlay.kt @@ -15,7 +15,7 @@ package com.google.maps.android.compose import androidx.compose.runtime.Composable -import androidx.compose.runtime.ComposeNode +import androidx.compose.runtime.ReusableComposeNode import androidx.compose.runtime.currentComposer import androidx.compose.ui.geometry.Offset import com.google.android.gms.maps.model.BitmapDescriptor @@ -23,8 +23,8 @@ import com.google.android.gms.maps.model.GroundOverlay import com.google.android.gms.maps.model.GroundOverlayOptions import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.LatLngBounds +import com.google.maps.android.compose.GroundOverlayPosition.Companion.create import com.google.maps.android.ktx.addGroundOverlay -import kotlin.IllegalStateException internal class GroundOverlayNode( val groundOverlay: GroundOverlay, @@ -90,7 +90,7 @@ public fun GroundOverlay( onClick: (GroundOverlay) -> Unit = {}, ) { val mapApplier = currentComposer.applier as? MapApplier - ComposeNode( + ReusableComposeNode( factory = { val groundOverlay = mapApplier?.map?.addGroundOverlay { anchor(anchor.x, anchor.y) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/InputHandler.kt b/maps-compose/src/main/java/com/google/maps/android/compose/InputHandler.kt index a6bbda2b..8dacb35d 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/InputHandler.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/InputHandler.kt @@ -2,15 +2,15 @@ package com.google.maps.android.compose import androidx.annotation.RestrictTo import androidx.compose.runtime.Composable -import androidx.compose.runtime.ComposeNode +import androidx.compose.runtime.ReusableComposeNode import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import com.google.android.gms.maps.GoogleMap.OnMarkerClickListener import com.google.android.gms.maps.model.Circle import com.google.android.gms.maps.model.GroundOverlay import com.google.android.gms.maps.model.Marker import com.google.android.gms.maps.model.Polygon -import com.google.android.gms.maps.GoogleMap.OnMarkerClickListener import com.google.android.gms.maps.model.Polyline /** @@ -34,7 +34,7 @@ public fun InputHandler( onMarkerDragEnd: ((Marker) -> Unit)? = null, onMarkerDragStart: ((Marker) -> Unit)? = null, ) { - ComposeNode( + ReusableComposeNode( factory = { InputHandlerNode( onCircleClick, diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/MapUpdater.kt b/maps-compose/src/main/java/com/google/maps/android/compose/MapUpdater.kt index f27a000d..2cb69f86 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/MapUpdater.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/MapUpdater.kt @@ -18,7 +18,7 @@ import android.annotation.SuppressLint import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable -import androidx.compose.runtime.ComposeNode +import androidx.compose.runtime.ReusableComposeNode import androidx.compose.runtime.currentComposer import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection @@ -109,7 +109,7 @@ internal inline fun MapUpdater( } val density = LocalDensity.current val layoutDirection = LocalLayoutDirection.current - ComposeNode( + ReusableComposeNode( factory = { MapPropertiesNode( map = map, 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 1904abc6..67166581 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 @@ -16,10 +16,10 @@ package com.google.maps.android.compose import android.view.View import androidx.compose.runtime.Composable -import androidx.compose.runtime.ComposeNode import androidx.compose.runtime.CompositionContext import androidx.compose.runtime.Immutable import androidx.compose.runtime.MutableState +import androidx.compose.runtime.ReusableComposeNode import androidx.compose.runtime.currentComposer import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -459,7 +459,7 @@ private fun MarkerImpl( ) { val mapApplier = currentComposer.applier as? MapApplier val compositionContext = rememberCompositionContext() - ComposeNode( + ReusableComposeNode( factory = { val marker = mapApplier?.map?.addMarker { contentDescription(contentDescription) @@ -664,7 +664,7 @@ private fun AdvancedMarkerImpl( advancedMarkerOptions.icon(BitmapDescriptorFactory.fromPinConfig(pinConfig)) } - ComposeNode( + ReusableComposeNode( factory = { val marker = mapApplier?.map?.addMarker(advancedMarkerOptions) ?: error("Error adding marker") diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/Polygon.kt b/maps-compose/src/main/java/com/google/maps/android/compose/Polygon.kt index 9848f1dc..36774b74 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/Polygon.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/Polygon.kt @@ -15,7 +15,7 @@ package com.google.maps.android.compose import androidx.compose.runtime.Composable -import androidx.compose.runtime.ComposeNode +import androidx.compose.runtime.ReusableComposeNode import androidx.compose.runtime.currentComposer import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb @@ -69,7 +69,7 @@ public fun Polygon( onClick: (Polygon) -> Unit = {} ) { val mapApplier = currentComposer.applier as MapApplier? - ComposeNode( + ReusableComposeNode( factory = { val polygon = mapApplier?.map?.addPolygon { addAll(points) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/Polyline.kt b/maps-compose/src/main/java/com/google/maps/android/compose/Polyline.kt index 242f0862..cc2b233d 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/Polyline.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/Polyline.kt @@ -15,7 +15,7 @@ package com.google.maps.android.compose import androidx.compose.runtime.Composable -import androidx.compose.runtime.ComposeNode +import androidx.compose.runtime.ReusableComposeNode import androidx.compose.runtime.currentComposer import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb @@ -71,7 +71,7 @@ public fun Polyline( onClick: (Polyline) -> Unit = {} ) { val mapApplier = currentComposer.applier as MapApplier? - ComposeNode( + ReusableComposeNode( factory = { val polyline = mapApplier?.map?.addPolyline { addAll(points) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/TileOverlay.kt b/maps-compose/src/main/java/com/google/maps/android/compose/TileOverlay.kt index 18679b9b..1f53e34d 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/TileOverlay.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/TileOverlay.kt @@ -15,7 +15,7 @@ package com.google.maps.android.compose import androidx.compose.runtime.Composable -import androidx.compose.runtime.ComposeNode +import androidx.compose.runtime.ReusableComposeNode import androidx.compose.runtime.currentComposer import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -84,7 +84,7 @@ public fun TileOverlay( onClick: (TileOverlay) -> Unit = {}, ) { val mapApplier = currentComposer.applier as MapApplier? - ComposeNode( + ReusableComposeNode( factory = { val tileOverlay = mapApplier?.map?.addTileOverlay { tileProvider(tileProvider) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetViewPanoramaUpdater.kt b/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetViewPanoramaUpdater.kt index 2898ec8f..a52ba730 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetViewPanoramaUpdater.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetViewPanoramaUpdater.kt @@ -1,7 +1,7 @@ package com.google.maps.android.compose.streetview import androidx.compose.runtime.Composable -import androidx.compose.runtime.ComposeNode +import androidx.compose.runtime.ReusableComposeNode import androidx.compose.runtime.currentComposer import com.google.android.gms.maps.StreetViewPanorama import com.google.maps.android.compose.MapNode @@ -55,7 +55,7 @@ internal inline fun StreetViewUpdater( ) { val streetViewPanorama = (currentComposer.applier as StreetViewPanoramaApplier).streetViewPanorama - ComposeNode( + ReusableComposeNode( factory = { StreetViewPanoramaPropertiesNode( cameraPositionState = cameraPositionState, From 706fe3a56a5b0f217784510052de45404c2a91bc Mon Sep 17 00:00:00 2001 From: Philip Date: Tue, 6 Feb 2024 21:28:07 +0100 Subject: [PATCH 017/103] Refactor GoogleMap to save Composition + MapClickListeners + other related things in MapView using setTag() This also includes some additional things for debugging/testing/demonstrations purposes which will be removed before merging --- .../google/maps/android/compose/GoogleMap.kt | 260 ++++++++++++------ .../maps/android/compose/MapClickListeners.kt | 20 +- maps-compose/src/main/res/values/ids.xml | 5 + 3 files changed, 197 insertions(+), 88 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 7ee7e105..e5cab81c 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -18,12 +18,17 @@ import android.content.ComponentCallbacks import android.content.res.Configuration import android.location.Location import android.os.Bundle +import android.util.Log +import android.view.View import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.Composition -import androidx.compose.runtime.CompositionContext import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ReusableComposition import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -31,10 +36,12 @@ import androidx.compose.runtime.rememberCompositionContext import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver @@ -44,10 +51,14 @@ import com.google.android.gms.maps.MapView import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.PointOfInterest import com.google.maps.android.ktx.awaitMap -import kotlinx.coroutines.Job import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.launch +internal const val TAG = "GoogleMap" + +private var compositionCounter = 0 +private var androidViewCounter = 0 + /** * A compose container for a [MapView]. * @@ -100,7 +111,8 @@ public fun GoogleMap( // rememberUpdatedState and friends are used here to make these values observable to // the subcomposition without providing a new content function each recomposition - val mapClickListeners = remember { MapClickListeners() }.also { + var mapClickListeners by remember { mutableStateOf(null) } + mapClickListeners?.also { it.indoorStateChangeListener = indoorStateChangeListener it.onMapClick = onMapClick it.onMapLongClick = onMapLongClick @@ -109,6 +121,7 @@ public fun GoogleMap( it.onMyLocationClick = onMyLocationClick it.onPOIClick = onPOIClick } + val currentContentDescription by rememberUpdatedState(contentDescription) val currentLocationSource by rememberUpdatedState(locationSource) val currentCameraPositionState by rememberUpdatedState(cameraPositionState) @@ -120,75 +133,162 @@ public fun GoogleMap( val currentContent by rememberUpdatedState(content) val mapUpdaterScope = rememberCoroutineScope() - fun launchMapUpdaterJob(mapView: MapView) = mapUpdaterScope.launch { - disposingComposition { - mapView.newComposition(parentComposition, mapClickListeners) { - MapUpdater( - mergeDescendants = mergeDescendants, - contentDescription = currentContentDescription, - cameraPositionState = currentCameraPositionState, - contentPadding = currentContentPadding, - locationSource = currentLocationSource, - mapProperties = currentMapProperties, - mapUiSettings = currentUiSettings, - ) - - MapClickListenerUpdater() - - CompositionLocalProvider( - LocalCameraPositionState provides currentCameraPositionState, - ) { - currentContent?.invoke() - } - } - } + fun View.log(msg: String) { + Log.d(TAG, "[AndroidView/${this.getTag(R.id.maps_compose_map_view_tag_debug_id)}] $msg") } + var composition by remember { mutableStateOf(null) } + val lifecycle = LocalLifecycleOwner.current.lifecycle val context = LocalContext.current var mapLifecycleController: MapViewLifecycleController? by remember { mutableStateOf(null) } var componentCallbacks: ComponentCallbacks? by remember { mutableStateOf(null) } - var mapUpdaterJob: Job? by remember { mutableStateOf(null) } - - AndroidView( - modifier = modifier, - factory = { - MapView(context, googleMapOptionsFactory()).also { mapView -> - mapLifecycleController = MapViewLifecycleController( - mapView = mapView, - isViewReused = false - ) - componentCallbacks = mapView.componentCallbacks() + var isCompositionSet by remember { mutableStateOf(false) } + + // Debug stuff + var debugMapReused: Boolean? by remember { mutableStateOf(null) } + val debugCompositionId = remember { compositionCounter++ } + var debugMapId: Int? by remember { mutableStateOf(null) } + + suspend fun MapView.getOrCreateComposition( + mapClickListeners: MapClickListeners + ): Pair { + val current = getTag(R.id.maps_compose_map_view_tag_composition) as? ReusableComposition + + if(current == null) { + setTag(R.id.maps_compose_map_view_tag_debug_id, androidViewCounter++) + } + + return if(current == null || current.isDisposed) { + val map = awaitMap() + ReusableComposition( + MapApplier(map, this, mapClickListeners), parentComposition + ).also { composition -> + setTag(R.id.maps_compose_map_view_tag_composition, composition) + } to false + } else { + current to true + }.also { (_, reused) -> + log("getOrCreateComposition. Reused composition: $reused.") + } + } + + fun MapView.getOrCreateMapListenersTag(): MapClickListeners { + return getTag(R.id.maps_compose_map_view_tag_click_listeners) as? MapClickListeners ?: MapClickListeners() + .also { clickListeners -> + setTag(R.id.maps_compose_map_view_tag_click_listeners, clickListeners) } - }, - onReset = { - mapLifecycleController!!.lifecycle = null - context.unregisterComponentCallbacks(componentCallbacks) - }, - onRelease = { mapView -> - // MapView will never be used again and should be destroyed. - mapLifecycleController!!.onDestroy() - mapView.removeAllViews() - context.unregisterComponentCallbacks(componentCallbacks) - }, - update = { mapView -> - if (mapLifecycleController == null) { - mapLifecycleController = MapViewLifecycleController( - mapView = mapView, - isViewReused = true - ) + } + + /** Apply the [content] compositions to the map. */ + suspend fun setComposition(mapView: MapView) { + mapView.log("setComposition") + val clickListeners = mapView.getOrCreateMapListenersTag() + val (currentComposition, mapReused) = mapView.getOrCreateComposition(clickListeners) + + composition = currentComposition + + val mapCompositionContent: @Composable () -> Unit = { + MapUpdater( + mergeDescendants = mergeDescendants, + contentDescription = currentContentDescription, + cameraPositionState = currentCameraPositionState, + contentPadding = currentContentPadding, + locationSource = currentLocationSource, + mapProperties = currentMapProperties, + mapUiSettings = currentUiSettings, + ) + + MapClickListenerUpdater() + + CompositionLocalProvider( + LocalCameraPositionState provides currentCameraPositionState, + ) { + currentContent?.invoke() } + } - if (componentCallbacks == null) { - componentCallbacks = mapView.componentCallbacks() + currentComposition.apply { + if(mapReused) { + setContentWithReuse(mapCompositionContent) + } else { + setContent(mapCompositionContent) } + } + + // Set this after composition is started. + mapClickListeners = clickListeners + } - mapLifecycleController!!.lifecycle = lifecycle + Box { + AndroidView( + modifier = modifier, + factory = { + Log.d(TAG, "Factory") + debugMapReused = false + MapView(context, googleMapOptionsFactory()).also { mapView -> + mapLifecycleController = MapViewLifecycleController( + isMapReused = false, + mapView = mapView + ) + componentCallbacks = mapView.componentCallbacks() + } + }, + onReset = { mapView -> + mapView.log("onReset") + // Deactivate composition to save resources + context.unregisterComponentCallbacks(componentCallbacks) + mapLifecycleController!!.onLifecycleDetached() + composition?.deactivate() + // Call onStop/onPause or something? Set map type to None to save resources? Because the MapView is detached. + }, + onRelease = { mapView -> + mapView.log("onRelease") + context.unregisterComponentCallbacks(componentCallbacks) + // Dispose composition + composition?.dispose() + // Invoke onDestroy + remove lifecycle callbacks for the MapView + mapLifecycleController!!.onDestroy() + // Clean up MapView. + mapView.removeAllViews() + }, + update = { mapView -> + mapView.log("update") + if (mapLifecycleController == null) { + debugMapReused = true + mapLifecycleController = MapViewLifecycleController( + isMapReused = true, + mapView = mapView + ) + } + + if (componentCallbacks == null) { + componentCallbacks = mapView.componentCallbacks() + } - if (mapUpdaterJob == null) - mapUpdaterJob = launchMapUpdaterJob(mapView) + mapLifecycleController!!.lifecycle = lifecycle + + // Create Composition + if(!isCompositionSet) { + isCompositionSet = true + mapUpdaterScope.launch { + setComposition(mapView) + debugMapId = mapView.getTag(R.id.maps_compose_map_view_tag_debug_id) as? Int + } + } + } + ) + + Column( + modifier = Modifier + .align(Alignment.TopStart) + .padding(8.dp) + ) { + Text("Map reused: $debugMapReused") + Text("Composition ID: $debugCompositionId") + Text("MapView ID: $debugMapId") } - ) + } } internal suspend inline fun disposingComposition(factory: () -> Composition) { @@ -200,32 +300,33 @@ internal suspend inline fun disposingComposition(factory: () -> Composition) { } } -private suspend inline fun MapView.newComposition( - parent: CompositionContext, - mapClickListeners: MapClickListeners, - noinline content: @Composable () -> Unit -): Composition { - val map = awaitMap() - return Composition( - MapApplier(map, this, mapClickListeners), parent - ).apply { - setContent(content) - } -} - private class MapViewLifecycleController( - private val mapView: MapView, - isViewReused: Boolean + isMapReused: Boolean, + private val mapView: MapView ) { - private var previousState = if (isViewReused) + private var previousState = if (isMapReused) Lifecycle.Event.ON_STOP else Lifecycle.Event.ON_CREATE + var lifecycle: Lifecycle? = null + set(value) { + if (field !== value) { + field?.removeObserver(observer) + value?.addObserver(observer) + field = value + } + } + fun onDestroy() { lifecycle = null mapView.onDestroy() } + fun onLifecycleDetached() { + lifecycle = null + mapView.onStop() + } + private val observer = LifecycleEventObserver { _, event -> when (event) { Lifecycle.Event.ON_CREATE -> { @@ -249,15 +350,6 @@ private class MapViewLifecycleController( } previousState = event } - - var lifecycle: Lifecycle? = null - set(value) { - if (field !== value) { - field?.removeObserver(observer) - value?.addObserver(observer) - field = value - } - } } private fun MapView.componentCallbacks(): ComponentCallbacks = diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/MapClickListeners.kt b/maps-compose/src/main/java/com/google/maps/android/compose/MapClickListeners.kt index 42b3ee6e..ebdcd18b 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/MapClickListeners.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/MapClickListeners.kt @@ -16,8 +16,8 @@ package com.google.maps.android.compose import android.location.Location import androidx.compose.runtime.Composable -import androidx.compose.runtime.ComposeNode import androidx.compose.runtime.NonRestartableComposable +import androidx.compose.runtime.ReusableComposeNode import androidx.compose.runtime.currentComposer import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -88,7 +88,9 @@ internal class MapClickListenerNode( @Composable internal fun MapClickListenerUpdater() { // The mapClickListeners container object is not allowed to ever change - val mapClickListeners = (currentComposer.applier as MapApplier).mapClickListeners + val applier = (currentComposer.applier as MapApplier) + val mapClickListeners = applier.mapClickListeners + val mapView = applier.mapView with(mapClickListeners) { ::indoorStateChangeListener.let { callback -> @@ -125,8 +127,18 @@ internal fun MapClickListenerUpdater() { MapClickListenerComposeNode( callback, GoogleMap::setOnMapLoadedCallback, - OnMapLoadedCallback { callback()?.invoke() } + OnMapLoadedCallback { + // Save map loaded state in MapView tag. + mapView.setTag(R.id.maps_compose_map_view_tag_map_loaded, true) + callback()?.invoke() + } ) + + // Check if map is already loaded from MapView tag and if so, invoke the onMapLoaded callback. + // This is relevant if map is reused in a lazy layout. + if(mapView.getTag(R.id.maps_compose_map_view_tag_map_loaded) == true) { + callback()?.invoke() + } } ::onMyLocationButtonClick.let { callback -> @@ -192,5 +204,5 @@ private fun MapClickListenerComposeNode( // when callbacks recompose rapidly; setting GoogleMap listeners could potentially be // expensive due to synchronization, etc. GoogleMap listeners are not designed with a // use case of rapid recomposition in mind. - if (callback() != null) ComposeNode, MapApplier>(factory) {} + if (callback() != null) ReusableComposeNode, MapApplier>(factory) {} } diff --git a/maps-compose/src/main/res/values/ids.xml b/maps-compose/src/main/res/values/ids.xml index 10f586d6..5f19476e 100644 --- a/maps-compose/src/main/res/values/ids.xml +++ b/maps-compose/src/main/res/values/ids.xml @@ -1,4 +1,9 @@ + + + + + From 10427626178b6f332ce6e407775f03106a2e4eea Mon Sep 17 00:00:00 2001 From: Philip Date: Tue, 6 Feb 2024 21:28:36 +0100 Subject: [PATCH 018/103] Add compose material library to build.gradle temporarily for demo purposes --- maps-compose/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/maps-compose/build.gradle b/maps-compose/build.gradle index e6a8343a..e99ec7c3 100644 --- a/maps-compose/build.gradle +++ b/maps-compose/build.gradle @@ -38,6 +38,7 @@ dependencies { implementation platform(libs.androidx.compose.bom) implementation libs.androidx.core implementation libs.androidx.compose.foundation + implementation libs.androidx.compose.material implementation libs.kotlin api libs.maps.ktx.std From 01dc74ddd6e7fcfc1ab247fc07718b474c688fff Mon Sep 17 00:00:00 2001 From: Philip Date: Wed, 7 Feb 2024 11:19:25 +0100 Subject: [PATCH 019/103] Implement ComposeNodeLifecycleCallback for MapClickListenerNode + set/remove listener on reuse/deactivation --- .../maps/android/compose/MapClickListeners.kt | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/MapClickListeners.kt b/maps-compose/src/main/java/com/google/maps/android/compose/MapClickListeners.kt index ebdcd18b..9fd6ce6d 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/MapClickListeners.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/MapClickListeners.kt @@ -16,6 +16,7 @@ package com.google.maps.android.compose import android.location.Location import androidx.compose.runtime.Composable +import androidx.compose.runtime.ComposeNodeLifecycleCallback import androidx.compose.runtime.NonRestartableComposable import androidx.compose.runtime.ReusableComposeNode import androidx.compose.runtime.currentComposer @@ -76,11 +77,15 @@ internal class MapClickListenerNode( private val map: GoogleMap, private val setter: GoogleMap.(L?) -> Unit, private val listener: L -) : MapNode { +) : MapNode, ComposeNodeLifecycleCallback { override fun onAttached() = setListener(listener) override fun onRemoved() = setListener(null) override fun onCleared() = setListener(null) + override fun onReuse() = setListener(listener) + override fun onDeactivate() = setListener(null) + override fun onRelease() = setListener(null) + private fun setListener(listenerOrNull: L?) = map.setter(listenerOrNull) } @@ -127,18 +132,8 @@ internal fun MapClickListenerUpdater() { MapClickListenerComposeNode( callback, GoogleMap::setOnMapLoadedCallback, - OnMapLoadedCallback { - // Save map loaded state in MapView tag. - mapView.setTag(R.id.maps_compose_map_view_tag_map_loaded, true) - callback()?.invoke() - } + OnMapLoadedCallback { callback()?.invoke() } ) - - // Check if map is already loaded from MapView tag and if so, invoke the onMapLoaded callback. - // This is relevant if map is reused in a lazy layout. - if(mapView.getTag(R.id.maps_compose_map_view_tag_map_loaded) == true) { - callback()?.invoke() - } } ::onMyLocationButtonClick.let { callback -> From 4a3130d902029890779aca41078722f9cc86f66a Mon Sep 17 00:00:00 2001 From: Philip Date: Wed, 7 Feb 2024 11:19:54 +0100 Subject: [PATCH 020/103] Update GoogleMap.kt --- .../java/com/google/maps/android/compose/GoogleMap.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index e5cab81c..a80337bd 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -133,10 +133,6 @@ public fun GoogleMap( val currentContent by rememberUpdatedState(content) val mapUpdaterScope = rememberCoroutineScope() - fun View.log(msg: String) { - Log.d(TAG, "[AndroidView/${this.getTag(R.id.maps_compose_map_view_tag_debug_id)}] $msg") - } - var composition by remember { mutableStateOf(null) } val lifecycle = LocalLifecycleOwner.current.lifecycle @@ -291,6 +287,10 @@ public fun GoogleMap( } } +internal fun MapView.log(msg: String) { + Log.d(TAG, "[AndroidView/${this.getTag(R.id.maps_compose_map_view_tag_debug_id)}] $msg") +} + internal suspend inline fun disposingComposition(factory: () -> Composition) { val composition = factory() try { From 56971f11da4e132d2bd57e0d6f70916ebfd3d2d2 Mon Sep 17 00:00:00 2001 From: Philip Date: Wed, 7 Feb 2024 11:22:11 +0100 Subject: [PATCH 021/103] Update MapsInLazyColumnActivity.kt --- .../compose/MapsInLazyColumnActivity.kt | 93 ++++++++++++++----- 1 file changed, 71 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt b/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt index 83f707a2..225adb19 100644 --- a/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt @@ -19,15 +19,21 @@ import androidx.compose.material.Card import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.google.android.gms.maps.GoogleMap import com.google.android.gms.maps.model.CameraPosition +import com.google.android.gms.maps.model.IndoorBuilding import com.google.android.gms.maps.model.LatLng class MapsInLazyColumnActivity: ComponentActivity() { @@ -69,48 +75,99 @@ private fun MapCard(item: MapListItem) { Modifier.fillMaxSize(), item ) - Text( - item.title, - modifier = Modifier - .align(Alignment.TopStart) - .padding(8.dp) - .background(Color.White.copy(0.8f)) - ) } } } } +@OptIn(MapsComposeExperimentalApi::class) @Composable private fun CardMap( modifier: Modifier, mapItem: MapListItem ) { var mapLoaded by remember { mutableStateOf(false) } + var buildingFocused: Boolean? by remember { mutableStateOf(null) } + var focusedBuildingInvocationCount by remember { mutableIntStateOf(0) } + var activatedIndoorLevel: String? by remember { mutableStateOf(null) } + var activatedIndoorLevelInvocationCount by remember { mutableIntStateOf(0) } + var onMapClickCount by remember { mutableIntStateOf(0) } + val cameraPositionState = rememberCameraPositionState( key = mapItem.id, init = { position = CameraPosition.fromLatLngZoom(mapItem.location, mapItem.zoom) } ) + var map: GoogleMap? by remember { mutableStateOf(null) } + + fun updateIndoorLevel() { + activatedIndoorLevel = map!!.focusedBuilding?.run { levels.getOrNull(activeLevelIndex)?.name } + } + Box { GoogleMap( + onMapClick = { + onMapClickCount++ + }, modifier = modifier, + properties = remember { + MapProperties( + isBuildingEnabled = true, + isIndoorEnabled = true + ) + }, cameraPositionState = cameraPositionState, - onMapLoaded = { mapLoaded = true } + onMapLoaded = { mapLoaded = true }, + indoorStateChangeListener = object: IndoorStateChangeListener { + override fun onIndoorBuildingFocused() { + super.onIndoorBuildingFocused() + focusedBuildingInvocationCount++ + buildingFocused = (map!!.focusedBuilding != null) + updateIndoorLevel() + } + + override fun onIndoorLevelActivated(building: IndoorBuilding) { + super.onIndoorLevelActivated(building) + activatedIndoorLevelInvocationCount++ + updateIndoorLevel() + } + } ) { - Marker(state = rememberMarkerState(position = mapItem.location)) + MapEffect(Unit) { + map = it + } } AnimatedVisibility(!mapLoaded, enter = fadeIn(), exit = fadeOut()) { Box( - Modifier - .fillMaxSize() - .background(Color.White), + Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { CircularProgressIndicator() } } + + @Composable + fun TextWithBackground(text: String, fontWeight: FontWeight = FontWeight.Medium) { + Text( + modifier = Modifier.background(Color.White.copy(0.7f)), + text = text, + fontWeight = fontWeight, + fontSize = 10.sp + ) + } + + Column( + modifier = Modifier.align(Alignment.BottomStart) + ) { + TextWithBackground(mapItem.title, fontWeight = FontWeight.Bold) + TextWithBackground("Map loaded: $mapLoaded") + TextWithBackground("Map click count: $onMapClickCount") + TextWithBackground("Building focused: $buildingFocused") + TextWithBackground("Building focused invocation count: $focusedBuildingInvocationCount") + TextWithBackground("Indoor level: $activatedIndoorLevel") + TextWithBackground("Indoor level invocation count: $activatedIndoorLevelInvocationCount") + } } } @@ -124,6 +181,7 @@ private data class MapListItem( // From https://developers.google.com/public-data/docs/canonical/countries_csv private val countries = listOf( CountryLocation("Hong Kong", LatLng(22.396428, 114.109497), 5f), + CountryLocation("Madison Square Garden (has indoor mode)", LatLng(40.7504656,-73.9937246), 19.33f), CountryLocation("Bolivia", LatLng(-16.290154, -63.588653), 5f), CountryLocation("Ecuador", LatLng(-1.831239, -78.183406), 5f), CountryLocation("Sweden", LatLng(60.128161, 18.643501), 5f), @@ -138,16 +196,7 @@ private val countries = listOf( CountryLocation("South Africa", LatLng(-30.559482, 22.937506), 5f), CountryLocation("Spain", LatLng(40.463667, -3.74922), 5f), CountryLocation("Georgia", LatLng(42.315407, 43.356892), 5f), - CountryLocation("Burundi", LatLng(-3.373056, 29.918886), 5f), - CountryLocation("Christmas Island", LatLng(-10.447525, 105.690449), 5f), - CountryLocation("Vanuatu", LatLng(-15.376706, 166.959158), 5f), - CountryLocation("Jersey", LatLng(49.214439, -2.13125), 5f), - CountryLocation("Svalbard and Jan Mayen", LatLng(77.553604, 23.670272), 5f), - CountryLocation("American Samoa", LatLng(-14.270972, -170.132217), 5f), - CountryLocation("Moldova", LatLng(47.411631, 28.369885), 5f), - CountryLocation("Bouvet Island", LatLng(-54.423199, 3.413194), 5f), - CountryLocation("Puerto Rico", LatLng(18.220833, -66.590149), 5f), - CountryLocation("Colombia", LatLng(4.570868, -74.297333), 5f), + CountryLocation("Burundi", LatLng(-3.373056, 29.918886), 5f) ) private val mapListItems = countries From 7c7ed3a45612fb95ba2afc54d7c60b76d9292033 Mon Sep 17 00:00:00 2001 From: Philip Date: Wed, 7 Feb 2024 11:23:35 +0100 Subject: [PATCH 022/103] Update ids.xml --- maps-compose/src/main/res/values/ids.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maps-compose/src/main/res/values/ids.xml b/maps-compose/src/main/res/values/ids.xml index 5f19476e..f326b4de 100644 --- a/maps-compose/src/main/res/values/ids.xml +++ b/maps-compose/src/main/res/values/ids.xml @@ -4,6 +4,6 @@ + - From 07ceeedb183c09c36900b997e4924012f32bd547 Mon Sep 17 00:00:00 2001 From: Philip Date: Wed, 7 Feb 2024 11:26:48 +0100 Subject: [PATCH 023/103] Update GoogleMap.kt --- .../google/maps/android/compose/GoogleMap.kt | 106 +++++++++--------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index a80337bd..e035aaac 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -216,65 +216,65 @@ public fun GoogleMap( mapClickListeners = clickListeners } - Box { - AndroidView( - modifier = modifier, - factory = { - Log.d(TAG, "Factory") - debugMapReused = false - MapView(context, googleMapOptionsFactory()).also { mapView -> - mapLifecycleController = MapViewLifecycleController( - isMapReused = false, - mapView = mapView - ) - componentCallbacks = mapView.componentCallbacks() - } - }, - onReset = { mapView -> - mapView.log("onReset") - // Deactivate composition to save resources - context.unregisterComponentCallbacks(componentCallbacks) - mapLifecycleController!!.onLifecycleDetached() - composition?.deactivate() - // Call onStop/onPause or something? Set map type to None to save resources? Because the MapView is detached. - }, - onRelease = { mapView -> - mapView.log("onRelease") - context.unregisterComponentCallbacks(componentCallbacks) - // Dispose composition - composition?.dispose() - // Invoke onDestroy + remove lifecycle callbacks for the MapView - mapLifecycleController!!.onDestroy() - // Clean up MapView. - mapView.removeAllViews() - }, - update = { mapView -> - mapView.log("update") - if (mapLifecycleController == null) { - debugMapReused = true - mapLifecycleController = MapViewLifecycleController( - isMapReused = true, - mapView = mapView - ) - } + AndroidView( + modifier = modifier, + factory = { + Log.d(TAG, "Factory") + debugMapReused = false + MapView(context, googleMapOptionsFactory()).also { mapView -> + mapLifecycleController = MapViewLifecycleController( + isMapReused = false, + mapView = mapView + ) + componentCallbacks = mapView.componentCallbacks() + } + }, + onReset = { mapView -> + mapView.log("onReset") + // Deactivate composition to save resources + context.unregisterComponentCallbacks(componentCallbacks) + mapLifecycleController!!.onLifecycleDetached() + composition?.deactivate() + // Call onStop/onPause or something? Set map type to None to save resources? Because the MapView is detached. + }, + onRelease = { mapView -> + mapView.log("onRelease") + context.unregisterComponentCallbacks(componentCallbacks) + // Dispose composition + composition?.dispose() + // Invoke onDestroy + remove lifecycle callbacks for the MapView + mapLifecycleController!!.onDestroy() + // Clean up MapView. + mapView.removeAllViews() + }, + update = { mapView -> + mapView.log("update") + if (mapLifecycleController == null) { + debugMapReused = true + mapLifecycleController = MapViewLifecycleController( + isMapReused = true, + mapView = mapView + ) + } - if (componentCallbacks == null) { - componentCallbacks = mapView.componentCallbacks() - } + if (componentCallbacks == null) { + componentCallbacks = mapView.componentCallbacks() + } - mapLifecycleController!!.lifecycle = lifecycle + mapLifecycleController!!.lifecycle = lifecycle - // Create Composition - if(!isCompositionSet) { - isCompositionSet = true - mapUpdaterScope.launch { - setComposition(mapView) - debugMapId = mapView.getTag(R.id.maps_compose_map_view_tag_debug_id) as? Int - } + // Create Composition + if(!isCompositionSet) { + isCompositionSet = true + mapUpdaterScope.launch { + setComposition(mapView) + debugMapId = mapView.getTag(R.id.maps_compose_map_view_tag_debug_id) as? Int } } - ) + } + ) + Box(modifier = modifier) { Column( modifier = Modifier .align(Alignment.TopStart) From 19060dbe6b993ce44dd67c96ae5b36822d620455 Mon Sep 17 00:00:00 2001 From: Philip Date: Wed, 7 Feb 2024 11:30:01 +0100 Subject: [PATCH 024/103] Update MapClickListeners.kt --- .../java/com/google/maps/android/compose/MapClickListeners.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/MapClickListeners.kt b/maps-compose/src/main/java/com/google/maps/android/compose/MapClickListeners.kt index 9fd6ce6d..120c1d77 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/MapClickListeners.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/MapClickListeners.kt @@ -95,7 +95,6 @@ internal fun MapClickListenerUpdater() { // The mapClickListeners container object is not allowed to ever change val applier = (currentComposer.applier as MapApplier) val mapClickListeners = applier.mapClickListeners - val mapView = applier.mapView with(mapClickListeners) { ::indoorStateChangeListener.let { callback -> From 2d9a1585a04cd2a17bb4aec2a4804cf10ba07b30 Mon Sep 17 00:00:00 2001 From: Philip Date: Wed, 7 Feb 2024 13:24:56 +0100 Subject: [PATCH 025/103] Update GoogleMap.kt --- .../src/main/java/com/google/maps/android/compose/GoogleMap.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index e035aaac..ab26a1b6 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -19,7 +19,6 @@ import android.content.res.Configuration import android.location.Location import android.os.Bundle import android.util.Log -import android.view.View import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues From 51f16d16e1016ac9901cb9c7b69eb60b8434d192 Mon Sep 17 00:00:00 2001 From: Philip Date: Wed, 21 Feb 2024 15:16:33 +0100 Subject: [PATCH 026/103] Make so that MapView lifecycle state "moves" through lifecycle states instead of setting the state directly. --- .../google/maps/android/compose/GoogleMap.kt | 110 ++++++++++++++++-- 1 file changed, 99 insertions(+), 11 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index ab26a1b6..03f6f2ed 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -150,10 +150,6 @@ public fun GoogleMap( ): Pair { val current = getTag(R.id.maps_compose_map_view_tag_composition) as? ReusableComposition - if(current == null) { - setTag(R.id.maps_compose_map_view_tag_debug_id, androidViewCounter++) - } - return if(current == null || current.isDisposed) { val map = awaitMap() ReusableComposition( @@ -221,6 +217,7 @@ public fun GoogleMap( Log.d(TAG, "Factory") debugMapReused = false MapView(context, googleMapOptionsFactory()).also { mapView -> + mapView.setTag(R.id.maps_compose_map_view_tag_debug_id, androidViewCounter++) mapLifecycleController = MapViewLifecycleController( isMapReused = false, mapView = mapView @@ -234,7 +231,6 @@ public fun GoogleMap( context.unregisterComponentCallbacks(componentCallbacks) mapLifecycleController!!.onLifecycleDetached() composition?.deactivate() - // Call onStop/onPause or something? Set map type to None to save resources? Because the MapView is detached. }, onRelease = { mapView -> mapView.log("onRelease") @@ -303,7 +299,49 @@ private class MapViewLifecycleController( isMapReused: Boolean, private val mapView: MapView ) { - private var previousState = if (isMapReused) + private val lcTag = run { + val mapViewId = mapView.getTag(R.id.maps_compose_map_view_tag_debug_id) + "MVLC/$mapViewId" + } + + private companion object { + /** + * Used to navigate up/down through lifecycle state. + * [up] and [down] are nullable suppliers to avoid circular references. + * + * Up means if a lifecycle goes "upwards", ie onCreate -> onResume. + * Down means the opposite, ie onResume -> onDestroy. + * + * https://developer.android.com/guide/components/activities/activity-lifecycle#alc + * */ + sealed class LifecycleStateNavigation( + val lifecycle: Lifecycle.Event, + val up: (() -> LifecycleStateNavigation)?, + val down: (() -> LifecycleStateNavigation)? + ) { + companion object { + private val instances = listOf(OnDestroy, OnStop, OnPause, OnCreate, OnStart, OnResume) + fun forLifecycleEvent(lifecycleEvent: Lifecycle.Event) = instances.first { it.lifecycle == lifecycleEvent } + } + } + + val lifecycleUp = listOf( + Lifecycle.Event.ON_CREATE, + Lifecycle.Event.ON_START, + Lifecycle.Event.ON_RESUME + ) + + data object OnDestroy: LifecycleStateNavigation(Lifecycle.Event.ON_DESTROY, null, null) + data object OnStop: LifecycleStateNavigation(Lifecycle.Event.ON_STOP, { OnStart }, { OnDestroy }) + data object OnPause: LifecycleStateNavigation(Lifecycle.Event.ON_PAUSE, { OnResume }, { OnStop }) + data object OnCreate: LifecycleStateNavigation(Lifecycle.Event.ON_CREATE, { OnStart }, null) + data object OnStart: LifecycleStateNavigation(Lifecycle.Event.ON_START, { OnResume }, null) + data object OnResume: LifecycleStateNavigation(Lifecycle.Event.ON_RESUME, null, { OnPause }) + } + + private var created = isMapReused + + private var previousLifecycleState = if (isMapReused) Lifecycle.Event.ON_STOP else Lifecycle.Event.ON_CREATE @@ -318,21 +356,55 @@ private class MapViewLifecycleController( fun onDestroy() { lifecycle = null - mapView.onDestroy() + moveToLifecycleEvent(Lifecycle.Event.ON_DESTROY) } fun onLifecycleDetached() { lifecycle = null - mapView.onStop() + moveToLifecycleEvent(Lifecycle.Event.ON_STOP) } - private val observer = LifecycleEventObserver { _, event -> + /** + * Move to the lifecycle event instead of setting it directly. + * + * + * */ + private fun moveToLifecycleEvent(targetLifecycle: Lifecycle.Event) { + if(targetLifecycle == previousLifecycleState) return + + val moveUp = targetLifecycle in lifecycleUp + val strDirection = if(moveUp) "UP" else "DOWN" + var currentLifecycleNavigator = LifecycleStateNavigation.forLifecycleEvent(previousLifecycleState) + Log.d(lcTag, "Moving $strDirection from ${currentLifecycleNavigator.lifecycle} to $targetLifecycle.") + + while(currentLifecycleNavigator.lifecycle != targetLifecycle) { + buildString { + appendLine("==========================================") + appendLine("Current navigator: ${currentLifecycleNavigator.lifecycle}") + appendLine("up: ${currentLifecycleNavigator.up?.invoke()?.lifecycle}") + appendLine("down: ${currentLifecycleNavigator.down?.invoke()?.lifecycle}") + appendLine("Moving: $strDirection") + appendLine("------------------------------------------") + }.also { + Log.d(lcTag, it) + } + currentLifecycleNavigator = if(moveUp) + currentLifecycleNavigator.up!!.invoke() else + currentLifecycleNavigator.down!!.invoke() + + setLifecycleEvent(currentLifecycleNavigator.lifecycle) + } + } + + private fun setLifecycleEvent(event: Lifecycle.Event) { + Log.d(lcTag, "Invoking: $event!") + when (event) { Lifecycle.Event.ON_CREATE -> { // Skip calling mapView.onCreate if the lifecycle did not go through onDestroy - in // this case the GoogleMap composable also doesn't leave the composition. So, // recreating the map does not restore state properly which must be avoided. - if (previousState != Lifecycle.Event.ON_STOP) { + if (previousLifecycleState != Lifecycle.Event.ON_STOP) { mapView.onCreate(Bundle()) } } @@ -347,7 +419,23 @@ private class MapViewLifecycleController( else -> throw IllegalStateException() } - previousState = event + previousLifecycleState = event + } + + private val observer = LifecycleEventObserver { _, event -> + Log.d(lcTag, "---===[ LEO: Lifecycle event received from LifecycleEventObserver: $event. ]===---") + + if(!created) { + Log.d(lcTag, "LEO: Invoking initial ON_CREATE.") + created = true + setLifecycleEvent(Lifecycle.Event.ON_CREATE) + } else if(event == Lifecycle.Event.ON_CREATE) { + Log.d(lcTag, "LEO: ON_CREATE lifecycle event was received but view is already created.") + } + + if(event != Lifecycle.Event.ON_CREATE) { + moveToLifecycleEvent(event) + } } } From 86e940c81af7c9412b461b84931623843caa082c Mon Sep 17 00:00:00 2001 From: Philip Date: Wed, 21 Feb 2024 15:17:54 +0100 Subject: [PATCH 027/103] Move MapViewLifecycleController into its own file --- .../google/maps/android/compose/GoogleMap.kt | 147 ----------------- .../compose/MapViewLifecycleController.kt | 151 ++++++++++++++++++ 2 files changed, 151 insertions(+), 147 deletions(-) create mode 100644 maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 03f6f2ed..e3996460 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -17,7 +17,6 @@ package com.google.maps.android.compose import android.content.ComponentCallbacks import android.content.res.Configuration import android.location.Location -import android.os.Bundle import android.util.Log import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -42,8 +41,6 @@ import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver import com.google.android.gms.maps.GoogleMapOptions import com.google.android.gms.maps.LocationSource import com.google.android.gms.maps.MapView @@ -295,150 +292,6 @@ internal suspend inline fun disposingComposition(factory: () -> Composition) { } } -private class MapViewLifecycleController( - isMapReused: Boolean, - private val mapView: MapView -) { - private val lcTag = run { - val mapViewId = mapView.getTag(R.id.maps_compose_map_view_tag_debug_id) - "MVLC/$mapViewId" - } - - private companion object { - /** - * Used to navigate up/down through lifecycle state. - * [up] and [down] are nullable suppliers to avoid circular references. - * - * Up means if a lifecycle goes "upwards", ie onCreate -> onResume. - * Down means the opposite, ie onResume -> onDestroy. - * - * https://developer.android.com/guide/components/activities/activity-lifecycle#alc - * */ - sealed class LifecycleStateNavigation( - val lifecycle: Lifecycle.Event, - val up: (() -> LifecycleStateNavigation)?, - val down: (() -> LifecycleStateNavigation)? - ) { - companion object { - private val instances = listOf(OnDestroy, OnStop, OnPause, OnCreate, OnStart, OnResume) - fun forLifecycleEvent(lifecycleEvent: Lifecycle.Event) = instances.first { it.lifecycle == lifecycleEvent } - } - } - - val lifecycleUp = listOf( - Lifecycle.Event.ON_CREATE, - Lifecycle.Event.ON_START, - Lifecycle.Event.ON_RESUME - ) - - data object OnDestroy: LifecycleStateNavigation(Lifecycle.Event.ON_DESTROY, null, null) - data object OnStop: LifecycleStateNavigation(Lifecycle.Event.ON_STOP, { OnStart }, { OnDestroy }) - data object OnPause: LifecycleStateNavigation(Lifecycle.Event.ON_PAUSE, { OnResume }, { OnStop }) - data object OnCreate: LifecycleStateNavigation(Lifecycle.Event.ON_CREATE, { OnStart }, null) - data object OnStart: LifecycleStateNavigation(Lifecycle.Event.ON_START, { OnResume }, null) - data object OnResume: LifecycleStateNavigation(Lifecycle.Event.ON_RESUME, null, { OnPause }) - } - - private var created = isMapReused - - private var previousLifecycleState = if (isMapReused) - Lifecycle.Event.ON_STOP else - Lifecycle.Event.ON_CREATE - - var lifecycle: Lifecycle? = null - set(value) { - if (field !== value) { - field?.removeObserver(observer) - value?.addObserver(observer) - field = value - } - } - - fun onDestroy() { - lifecycle = null - moveToLifecycleEvent(Lifecycle.Event.ON_DESTROY) - } - - fun onLifecycleDetached() { - lifecycle = null - moveToLifecycleEvent(Lifecycle.Event.ON_STOP) - } - - /** - * Move to the lifecycle event instead of setting it directly. - * - * - * */ - private fun moveToLifecycleEvent(targetLifecycle: Lifecycle.Event) { - if(targetLifecycle == previousLifecycleState) return - - val moveUp = targetLifecycle in lifecycleUp - val strDirection = if(moveUp) "UP" else "DOWN" - var currentLifecycleNavigator = LifecycleStateNavigation.forLifecycleEvent(previousLifecycleState) - Log.d(lcTag, "Moving $strDirection from ${currentLifecycleNavigator.lifecycle} to $targetLifecycle.") - - while(currentLifecycleNavigator.lifecycle != targetLifecycle) { - buildString { - appendLine("==========================================") - appendLine("Current navigator: ${currentLifecycleNavigator.lifecycle}") - appendLine("up: ${currentLifecycleNavigator.up?.invoke()?.lifecycle}") - appendLine("down: ${currentLifecycleNavigator.down?.invoke()?.lifecycle}") - appendLine("Moving: $strDirection") - appendLine("------------------------------------------") - }.also { - Log.d(lcTag, it) - } - currentLifecycleNavigator = if(moveUp) - currentLifecycleNavigator.up!!.invoke() else - currentLifecycleNavigator.down!!.invoke() - - setLifecycleEvent(currentLifecycleNavigator.lifecycle) - } - } - - private fun setLifecycleEvent(event: Lifecycle.Event) { - Log.d(lcTag, "Invoking: $event!") - - when (event) { - Lifecycle.Event.ON_CREATE -> { - // Skip calling mapView.onCreate if the lifecycle did not go through onDestroy - in - // this case the GoogleMap composable also doesn't leave the composition. So, - // recreating the map does not restore state properly which must be avoided. - if (previousLifecycleState != Lifecycle.Event.ON_STOP) { - mapView.onCreate(Bundle()) - } - } - - Lifecycle.Event.ON_START -> mapView.onStart() - Lifecycle.Event.ON_RESUME -> mapView.onResume() - Lifecycle.Event.ON_PAUSE -> mapView.onPause() - Lifecycle.Event.ON_STOP -> mapView.onStop() - Lifecycle.Event.ON_DESTROY -> { - // Handled in AndroidView onRelease - } - - else -> throw IllegalStateException() - } - previousLifecycleState = event - } - - private val observer = LifecycleEventObserver { _, event -> - Log.d(lcTag, "---===[ LEO: Lifecycle event received from LifecycleEventObserver: $event. ]===---") - - if(!created) { - Log.d(lcTag, "LEO: Invoking initial ON_CREATE.") - created = true - setLifecycleEvent(Lifecycle.Event.ON_CREATE) - } else if(event == Lifecycle.Event.ON_CREATE) { - Log.d(lcTag, "LEO: ON_CREATE lifecycle event was received but view is already created.") - } - - if(event != Lifecycle.Event.ON_CREATE) { - moveToLifecycleEvent(event) - } - } -} - private fun MapView.componentCallbacks(): ComponentCallbacks = object : ComponentCallbacks { override fun onConfigurationChanged(config: Configuration) {} diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt b/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt new file mode 100644 index 00000000..d2d39765 --- /dev/null +++ b/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt @@ -0,0 +1,151 @@ +package com.google.maps.android.compose + +import android.os.Bundle +import android.util.Log +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import com.google.android.gms.maps.MapView + +internal class MapViewLifecycleController( + isMapReused: Boolean, + private val mapView: MapView +) { + private val lcTag = run { + val mapViewId = mapView.getTag(R.id.maps_compose_map_view_tag_debug_id) + "MVLC/$mapViewId" + } + + private companion object { + /** + * Used to navigate up/down through lifecycle state. + * [up] and [down] are nullable suppliers to avoid circular references. + * + * Up means if a lifecycle goes "upwards", ie onCreate -> onResume. + * Down means the opposite, ie onResume -> onDestroy. + * + * https://developer.android.com/guide/components/activities/activity-lifecycle#alc + * */ + sealed class LifecycleStateNavigation( + val lifecycle: Lifecycle.Event, + val up: (() -> LifecycleStateNavigation)?, + val down: (() -> LifecycleStateNavigation)? + ) { + companion object { + private val instances = listOf(OnDestroy, OnStop, OnPause, OnCreate, OnStart, OnResume) + fun forLifecycleEvent(lifecycleEvent: Lifecycle.Event) = instances.first { it.lifecycle == lifecycleEvent } + } + } + + val lifecycleUp = listOf( + Lifecycle.Event.ON_CREATE, + Lifecycle.Event.ON_START, + Lifecycle.Event.ON_RESUME + ) + + data object OnDestroy: LifecycleStateNavigation(Lifecycle.Event.ON_DESTROY, null, null) + data object OnStop: LifecycleStateNavigation(Lifecycle.Event.ON_STOP, { OnStart }, { OnDestroy }) + data object OnPause: LifecycleStateNavigation(Lifecycle.Event.ON_PAUSE, { OnResume }, { OnStop }) + data object OnCreate: LifecycleStateNavigation(Lifecycle.Event.ON_CREATE, { OnStart }, null) + data object OnStart: LifecycleStateNavigation(Lifecycle.Event.ON_START, { OnResume }, null) + data object OnResume: LifecycleStateNavigation(Lifecycle.Event.ON_RESUME, null, { OnPause }) + } + + private var created = isMapReused + + private var previousLifecycleState = if (isMapReused) + Lifecycle.Event.ON_STOP else + Lifecycle.Event.ON_CREATE + + var lifecycle: Lifecycle? = null + set(value) { + if (field !== value) { + field?.removeObserver(observer) + value?.addObserver(observer) + field = value + } + } + + fun onDestroy() { + lifecycle = null + moveToLifecycleEvent(Lifecycle.Event.ON_DESTROY) + } + + fun onLifecycleDetached() { + lifecycle = null + moveToLifecycleEvent(Lifecycle.Event.ON_STOP) + } + + /** + * Move to the lifecycle event instead of setting it directly. + * + * + * */ + private fun moveToLifecycleEvent(targetLifecycle: Lifecycle.Event) { + if(targetLifecycle == previousLifecycleState) return + + val moveUp = targetLifecycle in lifecycleUp + val strDirection = if(moveUp) "UP" else "DOWN" + var currentLifecycleNavigator = LifecycleStateNavigation.forLifecycleEvent(previousLifecycleState) + Log.d(lcTag, "Moving $strDirection from ${currentLifecycleNavigator.lifecycle} to $targetLifecycle.") + + while(currentLifecycleNavigator.lifecycle != targetLifecycle) { + buildString { + appendLine("==========================================") + appendLine("Current navigator: ${currentLifecycleNavigator.lifecycle}") + appendLine("up: ${currentLifecycleNavigator.up?.invoke()?.lifecycle}") + appendLine("down: ${currentLifecycleNavigator.down?.invoke()?.lifecycle}") + appendLine("Moving: $strDirection") + appendLine("------------------------------------------") + }.also { + Log.d(lcTag, it) + } + currentLifecycleNavigator = if(moveUp) + currentLifecycleNavigator.up!!.invoke() else + currentLifecycleNavigator.down!!.invoke() + + setLifecycleEvent(currentLifecycleNavigator.lifecycle) + } + } + + private fun setLifecycleEvent(event: Lifecycle.Event) { + Log.d(lcTag, "Invoking: $event!") + + when (event) { + Lifecycle.Event.ON_CREATE -> { + // Skip calling mapView.onCreate if the lifecycle did not go through onDestroy - in + // this case the GoogleMap composable also doesn't leave the composition. So, + // recreating the map does not restore state properly which must be avoided. + if (previousLifecycleState != Lifecycle.Event.ON_STOP) { + mapView.onCreate(Bundle()) + } + } + + Lifecycle.Event.ON_START -> mapView.onStart() + Lifecycle.Event.ON_RESUME -> mapView.onResume() + Lifecycle.Event.ON_PAUSE -> mapView.onPause() + Lifecycle.Event.ON_STOP -> mapView.onStop() + Lifecycle.Event.ON_DESTROY -> { + // Handled in AndroidView onRelease + } + + else -> throw IllegalStateException() + } + previousLifecycleState = event + } + + private val observer = LifecycleEventObserver { _, event -> + Log.d(lcTag, "---===[ LEO: Lifecycle event received from LifecycleEventObserver: $event. ]===---") + + if(!created) { + Log.d(lcTag, "LEO: Invoking initial ON_CREATE.") + created = true + setLifecycleEvent(Lifecycle.Event.ON_CREATE) + } else if(event == Lifecycle.Event.ON_CREATE) { + Log.d(lcTag, "LEO: ON_CREATE lifecycle event was received but view is already created.") + } + + if(event != Lifecycle.Event.ON_CREATE) { + moveToLifecycleEvent(event) + } + } +} From e0dc55c31e35894cdd32abf28bf659e26bbfa874 Mon Sep 17 00:00:00 2001 From: Philip Date: Wed, 21 Feb 2024 15:22:10 +0100 Subject: [PATCH 028/103] Update MapViewLifecycleController.kt --- .../google/maps/android/compose/MapViewLifecycleController.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt b/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt index d2d39765..cfd57b64 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt @@ -23,7 +23,7 @@ internal class MapViewLifecycleController( * Up means if a lifecycle goes "upwards", ie onCreate -> onResume. * Down means the opposite, ie onResume -> onDestroy. * - * https://developer.android.com/guide/components/activities/activity-lifecycle#alc + * [developer.android.com/...](https://developer.android.com/guide/components/activities/activity-lifecycle) * */ sealed class LifecycleStateNavigation( val lifecycle: Lifecycle.Event, From 59be315b6351568b9eab45ba5f62969d82445b54 Mon Sep 17 00:00:00 2001 From: Philip Date: Thu, 22 Feb 2024 13:09:29 +0100 Subject: [PATCH 029/103] Remove composition reuse logic as it's moved to another PR --- .../com/google/maps/android/compose/Circle.kt | 4 +- .../google/maps/android/compose/GoogleMap.kt | 108 ++++++++---------- .../maps/android/compose/GroundOverlay.kt | 4 +- .../maps/android/compose/InputHandler.kt | 4 +- .../maps/android/compose/MapClickListeners.kt | 10 +- .../google/maps/android/compose/MapUpdater.kt | 4 +- .../com/google/maps/android/compose/Marker.kt | 6 +- .../google/maps/android/compose/Polygon.kt | 4 +- .../google/maps/android/compose/Polyline.kt | 4 +- .../maps/android/compose/TileOverlay.kt | 4 +- .../streetview/StreetViewPanoramaUpdater.kt | 4 +- 11 files changed, 71 insertions(+), 85 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/Circle.kt b/maps-compose/src/main/java/com/google/maps/android/compose/Circle.kt index dedd5878..237af045 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/Circle.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/Circle.kt @@ -15,7 +15,7 @@ package com.google.maps.android.compose import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReusableComposeNode +import androidx.compose.runtime.ComposeNode import androidx.compose.runtime.currentComposer import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb @@ -65,7 +65,7 @@ public fun Circle( onClick: (Circle) -> Unit = {}, ) { val mapApplier = currentComposer.applier as? MapApplier - ReusableComposeNode( + ComposeNode( factory = { val circle = mapApplier?.map?.addCircle { center(center) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index e3996460..b2e340e0 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -25,8 +25,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.Composition +import androidx.compose.runtime.CompositionContext import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.ReusableComposition import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -48,7 +48,9 @@ import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.PointOfInterest import com.google.maps.android.ktx.awaitMap import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlin.time.Duration internal const val TAG = "GoogleMap" @@ -107,8 +109,7 @@ public fun GoogleMap( // rememberUpdatedState and friends are used here to make these values observable to // the subcomposition without providing a new content function each recomposition - var mapClickListeners by remember { mutableStateOf(null) } - mapClickListeners?.also { + val mapClickListeners = remember { MapClickListeners() }.also { it.indoorStateChangeListener = indoorStateChangeListener it.onMapClick = onMapClick it.onMapLongClick = onMapLongClick @@ -117,7 +118,6 @@ public fun GoogleMap( it.onMyLocationClick = onMyLocationClick it.onPOIClick = onPOIClick } - val currentContentDescription by rememberUpdatedState(contentDescription) val currentLocationSource by rememberUpdatedState(locationSource) val currentCameraPositionState by rememberUpdatedState(cameraPositionState) @@ -127,54 +127,26 @@ public fun GoogleMap( val parentComposition = rememberCompositionContext() val currentContent by rememberUpdatedState(content) - val mapUpdaterScope = rememberCoroutineScope() - - var composition by remember { mutableStateOf(null) } val lifecycle = LocalLifecycleOwner.current.lifecycle val context = LocalContext.current var mapLifecycleController: MapViewLifecycleController? by remember { mutableStateOf(null) } var componentCallbacks: ComponentCallbacks? by remember { mutableStateOf(null) } - var isCompositionSet by remember { mutableStateOf(false) } // Debug stuff - var debugMapReused: Boolean? by remember { mutableStateOf(null) } val debugCompositionId = remember { compositionCounter++ } var debugMapId: Int? by remember { mutableStateOf(null) } - suspend fun MapView.getOrCreateComposition( - mapClickListeners: MapClickListeners - ): Pair { - val current = getTag(R.id.maps_compose_map_view_tag_composition) as? ReusableComposition - - return if(current == null || current.isDisposed) { - val map = awaitMap() - ReusableComposition( - MapApplier(map, this, mapClickListeners), parentComposition - ).also { composition -> - setTag(R.id.maps_compose_map_view_tag_composition, composition) - } to false - } else { - current to true - }.also { (_, reused) -> - log("getOrCreateComposition. Reused composition: $reused.") - } - } - - fun MapView.getOrCreateMapListenersTag(): MapClickListeners { - return getTag(R.id.maps_compose_map_view_tag_click_listeners) as? MapClickListeners ?: MapClickListeners() - .also { clickListeners -> - setTag(R.id.maps_compose_map_view_tag_click_listeners, clickListeners) - } - } - - /** Apply the [content] compositions to the map. */ - suspend fun setComposition(mapView: MapView) { - mapView.log("setComposition") - val clickListeners = mapView.getOrCreateMapListenersTag() - val (currentComposition, mapReused) = mapView.getOrCreateComposition(clickListeners) + val mapUpdaterScope = rememberCoroutineScope() - composition = currentComposition + /** + * Create and apply the [content] compositions to the map + + * dispose the [Composition] when the parent composable is disposed. + * */ + fun setCompositionAsync( + mapView: MapView + ) { + mapView.log("Creating composition...") val mapCompositionContent: @Composable () -> Unit = { MapUpdater( @@ -196,29 +168,41 @@ public fun GoogleMap( } } - currentComposition.apply { - if(mapReused) { - setContentWithReuse(mapCompositionContent) - } else { + mapUpdaterScope.launch { + val composition = mapView.createComposition(mapClickListeners, parentComposition).apply { setContent(mapCompositionContent) } + + // Dispose composition when mapUpdaterScope is cancelled. + launch { + delay(Duration.INFINITE) + }.invokeOnCompletion { + mapView.log("Disposing composition...") + composition.dispose() + } } - // Set this after composition is started. - mapClickListeners = clickListeners } + var isCompositionSet by remember { mutableStateOf(false) } + var debugIsMapReused: Boolean? by remember { mutableStateOf(null) } + AndroidView( modifier = modifier, factory = { Log.d(TAG, "Factory") - debugMapReused = false + debugIsMapReused = false MapView(context, googleMapOptionsFactory()).also { mapView -> - mapView.setTag(R.id.maps_compose_map_view_tag_debug_id, androidViewCounter++) + (androidViewCounter++).also { mapViewId -> + mapView.setTag(R.id.maps_compose_map_view_tag_debug_id, mapViewId) + debugMapId = mapViewId + } + mapLifecycleController = MapViewLifecycleController( isMapReused = false, mapView = mapView ) + componentCallbacks = mapView.componentCallbacks() } }, @@ -227,13 +211,10 @@ public fun GoogleMap( // Deactivate composition to save resources context.unregisterComponentCallbacks(componentCallbacks) mapLifecycleController!!.onLifecycleDetached() - composition?.deactivate() }, onRelease = { mapView -> mapView.log("onRelease") context.unregisterComponentCallbacks(componentCallbacks) - // Dispose composition - composition?.dispose() // Invoke onDestroy + remove lifecycle callbacks for the MapView mapLifecycleController!!.onDestroy() // Clean up MapView. @@ -242,7 +223,7 @@ public fun GoogleMap( update = { mapView -> mapView.log("update") if (mapLifecycleController == null) { - debugMapReused = true + debugIsMapReused = true mapLifecycleController = MapViewLifecycleController( isMapReused = true, mapView = mapView @@ -251,6 +232,7 @@ public fun GoogleMap( if (componentCallbacks == null) { componentCallbacks = mapView.componentCallbacks() + context.registerComponentCallbacks(componentCallbacks) } mapLifecycleController!!.lifecycle = lifecycle @@ -258,10 +240,7 @@ public fun GoogleMap( // Create Composition if(!isCompositionSet) { isCompositionSet = true - mapUpdaterScope.launch { - setComposition(mapView) - debugMapId = mapView.getTag(R.id.maps_compose_map_view_tag_debug_id) as? Int - } + setCompositionAsync(mapView) } } ) @@ -272,15 +251,26 @@ public fun GoogleMap( .align(Alignment.TopStart) .padding(8.dp) ) { - Text("Map reused: $debugMapReused") + Text("Map reused: $debugIsMapReused") Text("Composition ID: $debugCompositionId") Text("MapView ID: $debugMapId") } } } +private suspend fun MapView.createComposition( + mapClickListeners: MapClickListeners, + parentComposition: CompositionContext +): Composition { + val map = awaitMap() + return Composition( + applier = MapApplier(map, this, mapClickListeners), + parent = parentComposition + ) +} + internal fun MapView.log(msg: String) { - Log.d(TAG, "[AndroidView/${this.getTag(R.id.maps_compose_map_view_tag_debug_id)}] $msg") + Log.d(TAG, "[MapView/${this.getTag(R.id.maps_compose_map_view_tag_debug_id)}] $msg") } internal suspend inline fun disposingComposition(factory: () -> Composition) { diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GroundOverlay.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GroundOverlay.kt index 6bd83b3a..67132145 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GroundOverlay.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GroundOverlay.kt @@ -15,7 +15,7 @@ package com.google.maps.android.compose import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReusableComposeNode +import androidx.compose.runtime.ComposeNode import androidx.compose.runtime.currentComposer import androidx.compose.ui.geometry.Offset import com.google.android.gms.maps.model.BitmapDescriptor @@ -90,7 +90,7 @@ public fun GroundOverlay( onClick: (GroundOverlay) -> Unit = {}, ) { val mapApplier = currentComposer.applier as? MapApplier - ReusableComposeNode( + ComposeNode( factory = { val groundOverlay = mapApplier?.map?.addGroundOverlay { anchor(anchor.x, anchor.y) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/InputHandler.kt b/maps-compose/src/main/java/com/google/maps/android/compose/InputHandler.kt index 8dacb35d..cac37198 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/InputHandler.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/InputHandler.kt @@ -2,7 +2,7 @@ package com.google.maps.android.compose import androidx.annotation.RestrictTo import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReusableComposeNode +import androidx.compose.runtime.ComposeNode import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -34,7 +34,7 @@ public fun InputHandler( onMarkerDragEnd: ((Marker) -> Unit)? = null, onMarkerDragStart: ((Marker) -> Unit)? = null, ) { - ReusableComposeNode( + ComposeNode( factory = { InputHandlerNode( onCircleClick, diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/MapClickListeners.kt b/maps-compose/src/main/java/com/google/maps/android/compose/MapClickListeners.kt index 120c1d77..1be391c9 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/MapClickListeners.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/MapClickListeners.kt @@ -16,9 +16,9 @@ package com.google.maps.android.compose import android.location.Location import androidx.compose.runtime.Composable +import androidx.compose.runtime.ComposeNode import androidx.compose.runtime.ComposeNodeLifecycleCallback import androidx.compose.runtime.NonRestartableComposable -import androidx.compose.runtime.ReusableComposeNode import androidx.compose.runtime.currentComposer import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -77,15 +77,11 @@ internal class MapClickListenerNode( private val map: GoogleMap, private val setter: GoogleMap.(L?) -> Unit, private val listener: L -) : MapNode, ComposeNodeLifecycleCallback { +) : MapNode { override fun onAttached() = setListener(listener) override fun onRemoved() = setListener(null) override fun onCleared() = setListener(null) - override fun onReuse() = setListener(listener) - override fun onDeactivate() = setListener(null) - override fun onRelease() = setListener(null) - private fun setListener(listenerOrNull: L?) = map.setter(listenerOrNull) } @@ -198,5 +194,5 @@ private fun MapClickListenerComposeNode( // when callbacks recompose rapidly; setting GoogleMap listeners could potentially be // expensive due to synchronization, etc. GoogleMap listeners are not designed with a // use case of rapid recomposition in mind. - if (callback() != null) ReusableComposeNode, MapApplier>(factory) {} + if (callback() != null) ComposeNode, MapApplier>(factory) {} } diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/MapUpdater.kt b/maps-compose/src/main/java/com/google/maps/android/compose/MapUpdater.kt index 2cb69f86..f27a000d 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/MapUpdater.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/MapUpdater.kt @@ -18,7 +18,7 @@ import android.annotation.SuppressLint import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReusableComposeNode +import androidx.compose.runtime.ComposeNode import androidx.compose.runtime.currentComposer import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection @@ -109,7 +109,7 @@ internal inline fun MapUpdater( } val density = LocalDensity.current val layoutDirection = LocalLayoutDirection.current - ReusableComposeNode( + ComposeNode( factory = { MapPropertiesNode( map = map, 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 67166581..1904abc6 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 @@ -16,10 +16,10 @@ package com.google.maps.android.compose import android.view.View import androidx.compose.runtime.Composable +import androidx.compose.runtime.ComposeNode import androidx.compose.runtime.CompositionContext import androidx.compose.runtime.Immutable import androidx.compose.runtime.MutableState -import androidx.compose.runtime.ReusableComposeNode import androidx.compose.runtime.currentComposer import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -459,7 +459,7 @@ private fun MarkerImpl( ) { val mapApplier = currentComposer.applier as? MapApplier val compositionContext = rememberCompositionContext() - ReusableComposeNode( + ComposeNode( factory = { val marker = mapApplier?.map?.addMarker { contentDescription(contentDescription) @@ -664,7 +664,7 @@ private fun AdvancedMarkerImpl( advancedMarkerOptions.icon(BitmapDescriptorFactory.fromPinConfig(pinConfig)) } - ReusableComposeNode( + ComposeNode( factory = { val marker = mapApplier?.map?.addMarker(advancedMarkerOptions) ?: error("Error adding marker") diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/Polygon.kt b/maps-compose/src/main/java/com/google/maps/android/compose/Polygon.kt index 36774b74..9848f1dc 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/Polygon.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/Polygon.kt @@ -15,7 +15,7 @@ package com.google.maps.android.compose import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReusableComposeNode +import androidx.compose.runtime.ComposeNode import androidx.compose.runtime.currentComposer import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb @@ -69,7 +69,7 @@ public fun Polygon( onClick: (Polygon) -> Unit = {} ) { val mapApplier = currentComposer.applier as MapApplier? - ReusableComposeNode( + ComposeNode( factory = { val polygon = mapApplier?.map?.addPolygon { addAll(points) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/Polyline.kt b/maps-compose/src/main/java/com/google/maps/android/compose/Polyline.kt index cc2b233d..242f0862 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/Polyline.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/Polyline.kt @@ -15,7 +15,7 @@ package com.google.maps.android.compose import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReusableComposeNode +import androidx.compose.runtime.ComposeNode import androidx.compose.runtime.currentComposer import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb @@ -71,7 +71,7 @@ public fun Polyline( onClick: (Polyline) -> Unit = {} ) { val mapApplier = currentComposer.applier as MapApplier? - ReusableComposeNode( + ComposeNode( factory = { val polyline = mapApplier?.map?.addPolyline { addAll(points) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/TileOverlay.kt b/maps-compose/src/main/java/com/google/maps/android/compose/TileOverlay.kt index 1f53e34d..18679b9b 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/TileOverlay.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/TileOverlay.kt @@ -15,7 +15,7 @@ package com.google.maps.android.compose import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReusableComposeNode +import androidx.compose.runtime.ComposeNode import androidx.compose.runtime.currentComposer import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -84,7 +84,7 @@ public fun TileOverlay( onClick: (TileOverlay) -> Unit = {}, ) { val mapApplier = currentComposer.applier as MapApplier? - ReusableComposeNode( + ComposeNode( factory = { val tileOverlay = mapApplier?.map?.addTileOverlay { tileProvider(tileProvider) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetViewPanoramaUpdater.kt b/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetViewPanoramaUpdater.kt index a52ba730..2898ec8f 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetViewPanoramaUpdater.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetViewPanoramaUpdater.kt @@ -1,7 +1,7 @@ package com.google.maps.android.compose.streetview import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReusableComposeNode +import androidx.compose.runtime.ComposeNode import androidx.compose.runtime.currentComposer import com.google.android.gms.maps.StreetViewPanorama import com.google.maps.android.compose.MapNode @@ -55,7 +55,7 @@ internal inline fun StreetViewUpdater( ) { val streetViewPanorama = (currentComposer.applier as StreetViewPanoramaApplier).streetViewPanorama - ReusableComposeNode( + ComposeNode( factory = { StreetViewPanoramaPropertiesNode( cameraPositionState = cameraPositionState, From fcdc8336f114a49704cfc5d08f2f7e26bc7a501d Mon Sep 17 00:00:00 2001 From: Philip Date: Thu, 22 Feb 2024 13:10:26 +0100 Subject: [PATCH 030/103] Simplify MapViewLifecycleController logic --- .../compose/MapViewLifecycleController.kt | 129 ++++++++---------- 1 file changed, 60 insertions(+), 69 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt b/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt index cfd57b64..cb5d18fb 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt @@ -3,8 +3,10 @@ package com.google.maps.android.compose import android.os.Bundle import android.util.Log import androidx.lifecycle.Lifecycle +import androidx.lifecycle.Lifecycle.Event import androidx.lifecycle.LifecycleEventObserver import com.google.android.gms.maps.MapView +import com.google.maps.android.compose.MapViewLifecycleController.LifecycleDirection.* internal class MapViewLifecycleController( isMapReused: Boolean, @@ -16,45 +18,20 @@ internal class MapViewLifecycleController( } private companion object { - /** - * Used to navigate up/down through lifecycle state. - * [up] and [down] are nullable suppliers to avoid circular references. - * - * Up means if a lifecycle goes "upwards", ie onCreate -> onResume. - * Down means the opposite, ie onResume -> onDestroy. - * - * [developer.android.com/...](https://developer.android.com/guide/components/activities/activity-lifecycle) - * */ - sealed class LifecycleStateNavigation( - val lifecycle: Lifecycle.Event, - val up: (() -> LifecycleStateNavigation)?, - val down: (() -> LifecycleStateNavigation)? - ) { - companion object { - private val instances = listOf(OnDestroy, OnStop, OnPause, OnCreate, OnStart, OnResume) - fun forLifecycleEvent(lifecycleEvent: Lifecycle.Event) = instances.first { it.lifecycle == lifecycleEvent } - } - } - val lifecycleUp = listOf( - Lifecycle.Event.ON_CREATE, - Lifecycle.Event.ON_START, - Lifecycle.Event.ON_RESUME + Event.ON_CREATE, + Event.ON_START, + Event.ON_RESUME ) - - data object OnDestroy: LifecycleStateNavigation(Lifecycle.Event.ON_DESTROY, null, null) - data object OnStop: LifecycleStateNavigation(Lifecycle.Event.ON_STOP, { OnStart }, { OnDestroy }) - data object OnPause: LifecycleStateNavigation(Lifecycle.Event.ON_PAUSE, { OnResume }, { OnStop }) - data object OnCreate: LifecycleStateNavigation(Lifecycle.Event.ON_CREATE, { OnStart }, null) - data object OnStart: LifecycleStateNavigation(Lifecycle.Event.ON_START, { OnResume }, null) - data object OnResume: LifecycleStateNavigation(Lifecycle.Event.ON_RESUME, null, { OnPause }) } - private var created = isMapReused + private enum class LifecycleDirection { + Up, Down + } private var previousLifecycleState = if (isMapReused) - Lifecycle.Event.ON_STOP else - Lifecycle.Event.ON_CREATE + Event.ON_STOP else + Event.ON_CREATE var lifecycle: Lifecycle? = null set(value) { @@ -67,64 +44,76 @@ internal class MapViewLifecycleController( fun onDestroy() { lifecycle = null - moveToLifecycleEvent(Lifecycle.Event.ON_DESTROY) + moveToLifecycleEvent(Event.ON_DESTROY) } fun onLifecycleDetached() { lifecycle = null - moveToLifecycleEvent(Lifecycle.Event.ON_STOP) + moveToLifecycleEvent(Event.ON_STOP) } /** * Move to the lifecycle event instead of setting it directly. - * - * * */ - private fun moveToLifecycleEvent(targetLifecycle: Lifecycle.Event) { + private fun moveToLifecycleEvent(targetLifecycle: Event) { if(targetLifecycle == previousLifecycleState) return - val moveUp = targetLifecycle in lifecycleUp - val strDirection = if(moveUp) "UP" else "DOWN" - var currentLifecycleNavigator = LifecycleStateNavigation.forLifecycleEvent(previousLifecycleState) - Log.d(lcTag, "Moving $strDirection from ${currentLifecycleNavigator.lifecycle} to $targetLifecycle.") - - while(currentLifecycleNavigator.lifecycle != targetLifecycle) { - buildString { - appendLine("==========================================") - appendLine("Current navigator: ${currentLifecycleNavigator.lifecycle}") - appendLine("up: ${currentLifecycleNavigator.up?.invoke()?.lifecycle}") - appendLine("down: ${currentLifecycleNavigator.down?.invoke()?.lifecycle}") - appendLine("Moving: $strDirection") - appendLine("------------------------------------------") - }.also { - Log.d(lcTag, it) - } - currentLifecycleNavigator = if(moveUp) - currentLifecycleNavigator.up!!.invoke() else - currentLifecycleNavigator.down!!.invoke() + val lifecycleDirection = if(targetLifecycle in lifecycleUp) Up else Down + + (if(lifecycleDirection == Up) "UP" else "DOWN").let { strDirection -> + Log.d(lcTag, "Moving $strDirection from $previousLifecycleState to $targetLifecycle.") + } - setLifecycleEvent(currentLifecycleNavigator.lifecycle) + do { + val nextLifecycleState = nextLifecycleEvent(lifecycleDirection) + setLifecycleEvent(nextLifecycleState) + } while(nextLifecycleState != targetLifecycle) + } + + private fun nextLifecycleEvent(direction: LifecycleDirection) = when(previousLifecycleState) { + Event.ON_CREATE -> when(direction) { + Up -> Event.ON_START + Down -> error("No lifecycle event below ON_CREATE.") + } + Event.ON_START -> when(direction) { + Up -> Event.ON_RESUME + Down -> error("No lifecycle event below ON_START.") + } + Event.ON_RESUME -> when(direction) { + Up -> error("No lifecycle event above ON_RESUME.") + Down -> Event.ON_PAUSE + } + Event.ON_PAUSE -> when(direction) { + Up -> Event.ON_RESUME + Down -> Event.ON_STOP } + Event.ON_STOP -> when(direction) { + Up -> Event.ON_START + Down -> Event.ON_DESTROY + } + Event.ON_DESTROY -> error("No lifecycle event above ON_DESTROY") + Event.ON_ANY -> error("Unsupported operation") } - private fun setLifecycleEvent(event: Lifecycle.Event) { + + private fun setLifecycleEvent(event: Event) { Log.d(lcTag, "Invoking: $event!") when (event) { - Lifecycle.Event.ON_CREATE -> { + Event.ON_CREATE -> { // Skip calling mapView.onCreate if the lifecycle did not go through onDestroy - in // this case the GoogleMap composable also doesn't leave the composition. So, // recreating the map does not restore state properly which must be avoided. - if (previousLifecycleState != Lifecycle.Event.ON_STOP) { + if (previousLifecycleState != Event.ON_STOP) { mapView.onCreate(Bundle()) } } - Lifecycle.Event.ON_START -> mapView.onStart() - Lifecycle.Event.ON_RESUME -> mapView.onResume() - Lifecycle.Event.ON_PAUSE -> mapView.onPause() - Lifecycle.Event.ON_STOP -> mapView.onStop() - Lifecycle.Event.ON_DESTROY -> { + Event.ON_START -> mapView.onStart() + Event.ON_RESUME -> mapView.onResume() + Event.ON_PAUSE -> mapView.onPause() + Event.ON_STOP -> mapView.onStop() + Event.ON_DESTROY -> { // Handled in AndroidView onRelease } @@ -133,18 +122,20 @@ internal class MapViewLifecycleController( previousLifecycleState = event } + private var created = isMapReused + private val observer = LifecycleEventObserver { _, event -> Log.d(lcTag, "---===[ LEO: Lifecycle event received from LifecycleEventObserver: $event. ]===---") if(!created) { Log.d(lcTag, "LEO: Invoking initial ON_CREATE.") created = true - setLifecycleEvent(Lifecycle.Event.ON_CREATE) - } else if(event == Lifecycle.Event.ON_CREATE) { + setLifecycleEvent(Event.ON_CREATE) + } else if(event == Event.ON_CREATE) { Log.d(lcTag, "LEO: ON_CREATE lifecycle event was received but view is already created.") } - if(event != Lifecycle.Event.ON_CREATE) { + if(event != Event.ON_CREATE) { moveToLifecycleEvent(event) } } From 1a7ca37645374878fc3f45d9dfca1c165b9c36bd Mon Sep 17 00:00:00 2001 From: Philip Date: Mon, 26 Feb 2024 11:12:23 +0100 Subject: [PATCH 031/103] Re-register componentCallbacks if context has changed --- .../google/maps/android/compose/GoogleMap.kt | 55 ++++++++++++++++--- maps-compose/src/main/res/values/ids.xml | 3 +- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index b2e340e0..adb9e277 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -15,6 +15,7 @@ package com.google.maps.android.compose import android.content.ComponentCallbacks +import android.content.Context import android.content.res.Configuration import android.location.Location import android.util.Log @@ -131,7 +132,6 @@ public fun GoogleMap( val lifecycle = LocalLifecycleOwner.current.lifecycle val context = LocalContext.current var mapLifecycleController: MapViewLifecycleController? by remember { mutableStateOf(null) } - var componentCallbacks: ComponentCallbacks? by remember { mutableStateOf(null) } // Debug stuff val debugCompositionId = remember { compositionCounter++ } @@ -185,6 +185,7 @@ public fun GoogleMap( } var isCompositionSet by remember { mutableStateOf(false) } + var componentCallbacksUpdated by remember { mutableStateOf(false) } var debugIsMapReused: Boolean? by remember { mutableStateOf(null) } AndroidView( @@ -203,18 +204,23 @@ public fun GoogleMap( mapView = mapView ) - componentCallbacks = mapView.componentCallbacks() + mapView.registerAndSaveNewComponentCallbacks(context) } }, onReset = { mapView -> mapView.log("onReset") - // Deactivate composition to save resources - context.unregisterComponentCallbacks(componentCallbacks) + mapView[ComponentCallbacksTag]?.let { componentCallbacks -> + mapView[ComponentCallbacksContextTag]?.unregisterComponentCallbacks(componentCallbacks) + mapView[ComponentCallbacksContextTag] = null + } mapLifecycleController!!.onLifecycleDetached() }, onRelease = { mapView -> mapView.log("onRelease") - context.unregisterComponentCallbacks(componentCallbacks) + mapView[ComponentCallbacksTag]?.let { componentCallbacks -> + mapView[ComponentCallbacksContextTag]?.unregisterComponentCallbacks(componentCallbacks) + mapView[ComponentCallbacksContextTag] = null + } // Invoke onDestroy + remove lifecycle callbacks for the MapView mapLifecycleController!!.onDestroy() // Clean up MapView. @@ -230,9 +236,10 @@ public fun GoogleMap( ) } - if (componentCallbacks == null) { - componentCallbacks = mapView.componentCallbacks() - context.registerComponentCallbacks(componentCallbacks) + // componentCallbacksUpdated will be reset upon reuse so we need to check one time on each reuse. + if(!componentCallbacksUpdated) { + componentCallbacksUpdated = true + mapView.reRegisterComponentCallbacksIfChanged(context) } mapLifecycleController!!.lifecycle = lifecycle @@ -258,6 +265,38 @@ public fun GoogleMap( } } +private fun MapView.reRegisterComponentCallbacksIfChanged(context: Context) { + // If componentCallbacks haven't been updated since initial composition, re-register to new context if necessary. + // Should never be null here because it's set in [factory] + val componentCallbacksContext = this[ComponentCallbacksContextTag]!! + if(componentCallbacksContext != context) { + // New context. Unregister previous componentCallbacks and re-register on new context. + val callbacks = this[ComponentCallbacksTag]!! + componentCallbacksContext.unregisterComponentCallbacks(callbacks) + this.registerAndSaveNewComponentCallbacks(context) + } +} + +private fun MapView.registerAndSaveNewComponentCallbacks(context: Context) { + val componentCallbacks = this.componentCallbacks() + this[ComponentCallbacksTag] = componentCallbacks + this[ComponentCallbacksContextTag] = context + context.registerComponentCallbacks(componentCallbacks) +} + +private sealed class MapViewTag(val resourceId: Int) +private data object ComponentCallbacksContextTag: MapViewTag(R.id.maps_compose_map_view_tag_context) +private data object ComponentCallbacksTag: MapViewTag(R.id.maps_compose_map_view_tag_component_callbacks) +private data object DebugTag: MapViewTag(R.id.maps_compose_map_view_tag_debug_id) +private data object ClickListenersTag: MapViewTag(R.id.maps_compose_map_view_tag_click_listeners) + +private operator fun MapView.set(key: MapViewTag, value: T?) { + setTag(key.resourceId, value) +} + +@Suppress("UNCHECKED_CAST") +private operator fun MapView.get(key: MapViewTag): T? = getTag(key.resourceId) as? T + private suspend fun MapView.createComposition( mapClickListeners: MapClickListeners, parentComposition: CompositionContext diff --git a/maps-compose/src/main/res/values/ids.xml b/maps-compose/src/main/res/values/ids.xml index f326b4de..802f7392 100644 --- a/maps-compose/src/main/res/values/ids.xml +++ b/maps-compose/src/main/res/values/ids.xml @@ -2,8 +2,9 @@ - + + From bc7de0eae0e492c27bafaec3b6615b0b194fd1af Mon Sep 17 00:00:00 2001 From: Philip Date: Mon, 26 Feb 2024 13:14:14 +0100 Subject: [PATCH 032/103] Store map tag data in default tag instead of in separate tags by resource ids --- .../google/maps/android/compose/GoogleMap.kt | 65 +++++++++---------- .../maps/android/compose/MapClickListeners.kt | 1 - .../compose/MapViewLifecycleController.kt | 3 +- maps-compose/src/main/res/values/ids.xml | 6 -- 4 files changed, 34 insertions(+), 41 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index adb9e277..b6fbc7ff 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -56,7 +56,6 @@ import kotlin.time.Duration internal const val TAG = "GoogleMap" private var compositionCounter = 0 -private var androidViewCounter = 0 /** * A compose container for a [MapView]. @@ -129,7 +128,7 @@ public fun GoogleMap( val parentComposition = rememberCompositionContext() val currentContent by rememberUpdatedState(content) - val lifecycle = LocalLifecycleOwner.current.lifecycle + val lifecycleOwner = LocalLifecycleOwner.current val context = LocalContext.current var mapLifecycleController: MapViewLifecycleController? by remember { mutableStateOf(null) } @@ -194,11 +193,6 @@ public fun GoogleMap( Log.d(TAG, "Factory") debugIsMapReused = false MapView(context, googleMapOptionsFactory()).also { mapView -> - (androidViewCounter++).also { mapViewId -> - mapView.setTag(R.id.maps_compose_map_view_tag_debug_id, mapViewId) - debugMapId = mapViewId - } - mapLifecycleController = MapViewLifecycleController( isMapReused = false, mapView = mapView @@ -209,17 +203,15 @@ public fun GoogleMap( }, onReset = { mapView -> mapView.log("onReset") - mapView[ComponentCallbacksTag]?.let { componentCallbacks -> - mapView[ComponentCallbacksContextTag]?.unregisterComponentCallbacks(componentCallbacks) - mapView[ComponentCallbacksContextTag] = null - } mapLifecycleController!!.onLifecycleDetached() }, onRelease = { mapView -> mapView.log("onRelease") - mapView[ComponentCallbacksTag]?.let { componentCallbacks -> - mapView[ComponentCallbacksContextTag]?.unregisterComponentCallbacks(componentCallbacks) - mapView[ComponentCallbacksContextTag] = null + val tagData = mapView.tagData() + tagData.componentCallbacks?.let { componentCallbacks -> + tagData.componentCallbacksContext?.unregisterComponentCallbacks(componentCallbacks) + tagData.componentCallbacks = null + tagData.componentCallbacksContext = null } // Invoke onDestroy + remove lifecycle callbacks for the MapView mapLifecycleController!!.onDestroy() @@ -238,11 +230,12 @@ public fun GoogleMap( // componentCallbacksUpdated will be reset upon reuse so we need to check one time on each reuse. if(!componentCallbacksUpdated) { + debugMapId = mapView.tagData().debugId componentCallbacksUpdated = true mapView.reRegisterComponentCallbacksIfChanged(context) } - mapLifecycleController!!.lifecycle = lifecycle + mapLifecycleController!!.lifecycle = lifecycleOwner.lifecycle // Create Composition if(!isCompositionSet) { @@ -268,34 +261,40 @@ public fun GoogleMap( private fun MapView.reRegisterComponentCallbacksIfChanged(context: Context) { // If componentCallbacks haven't been updated since initial composition, re-register to new context if necessary. // Should never be null here because it's set in [factory] - val componentCallbacksContext = this[ComponentCallbacksContextTag]!! + val tagData = tagData() + val componentCallbacksContext = tagData.componentCallbacksContext!! if(componentCallbacksContext != context) { // New context. Unregister previous componentCallbacks and re-register on new context. - val callbacks = this[ComponentCallbacksTag]!! - componentCallbacksContext.unregisterComponentCallbacks(callbacks) + val currentCallbacks = tagData.componentCallbacks!! + componentCallbacksContext.unregisterComponentCallbacks(currentCallbacks) this.registerAndSaveNewComponentCallbacks(context) } } private fun MapView.registerAndSaveNewComponentCallbacks(context: Context) { - val componentCallbacks = this.componentCallbacks() - this[ComponentCallbacksTag] = componentCallbacks - this[ComponentCallbacksContextTag] = context - context.registerComponentCallbacks(componentCallbacks) + val newComponentCallbacks = this.componentCallbacks() + val tagData = tagData() + tagData.componentCallbacks = newComponentCallbacks + tagData.componentCallbacksContext = context + context.registerComponentCallbacks(newComponentCallbacks) } -private sealed class MapViewTag(val resourceId: Int) -private data object ComponentCallbacksContextTag: MapViewTag(R.id.maps_compose_map_view_tag_context) -private data object ComponentCallbacksTag: MapViewTag(R.id.maps_compose_map_view_tag_component_callbacks) -private data object DebugTag: MapViewTag(R.id.maps_compose_map_view_tag_debug_id) -private data object ClickListenersTag: MapViewTag(R.id.maps_compose_map_view_tag_click_listeners) - -private operator fun MapView.set(key: MapViewTag, value: T?) { - setTag(key.resourceId, value) +internal data class MapTagData( + var componentCallbacks: ComponentCallbacks?, + var componentCallbacksContext: Context?, + val debugId: Int = nextId +) { + companion object { + private var nextId = 0 + get() = field++ + } } -@Suppress("UNCHECKED_CAST") -private operator fun MapView.get(key: MapViewTag): T? = getTag(key.resourceId) as? T +internal fun MapView.tagData(): MapTagData = tag as? MapTagData ?: run { + MapTagData(null, null).also { newTag -> + tag = newTag + } +} private suspend fun MapView.createComposition( mapClickListeners: MapClickListeners, @@ -309,7 +308,7 @@ private suspend fun MapView.createComposition( } internal fun MapView.log(msg: String) { - Log.d(TAG, "[MapView/${this.getTag(R.id.maps_compose_map_view_tag_debug_id)}] $msg") + Log.d(TAG, "[MapView/${ tagData().debugId }] $msg") } internal suspend inline fun disposingComposition(factory: () -> Composition) { diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/MapClickListeners.kt b/maps-compose/src/main/java/com/google/maps/android/compose/MapClickListeners.kt index 1be391c9..62771089 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/MapClickListeners.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/MapClickListeners.kt @@ -17,7 +17,6 @@ package com.google.maps.android.compose import android.location.Location import androidx.compose.runtime.Composable import androidx.compose.runtime.ComposeNode -import androidx.compose.runtime.ComposeNodeLifecycleCallback import androidx.compose.runtime.NonRestartableComposable import androidx.compose.runtime.currentComposer import androidx.compose.runtime.getValue diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt b/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt index cb5d18fb..985bcd3b 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt @@ -2,6 +2,7 @@ package com.google.maps.android.compose import android.os.Bundle import android.util.Log +import androidx.core.view.get import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle.Event import androidx.lifecycle.LifecycleEventObserver @@ -13,7 +14,7 @@ internal class MapViewLifecycleController( private val mapView: MapView ) { private val lcTag = run { - val mapViewId = mapView.getTag(R.id.maps_compose_map_view_tag_debug_id) + val mapViewId = mapView.tagData().debugId "MVLC/$mapViewId" } diff --git a/maps-compose/src/main/res/values/ids.xml b/maps-compose/src/main/res/values/ids.xml index 802f7392..10f586d6 100644 --- a/maps-compose/src/main/res/values/ids.xml +++ b/maps-compose/src/main/res/values/ids.xml @@ -1,10 +1,4 @@ - - - - - - From 9ac909e227a030568b1db51f55d8ba48038c3404 Mon Sep 17 00:00:00 2001 From: Philip Date: Mon, 26 Feb 2024 13:21:08 +0100 Subject: [PATCH 033/103] Use CoroutineStart.UNDISPATCHED for creating composition --- .../src/main/java/com/google/maps/android/compose/GoogleMap.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index b6fbc7ff..34f6e467 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -48,6 +48,7 @@ import com.google.android.gms.maps.MapView import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.PointOfInterest import com.google.maps.android.ktx.awaitMap +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -167,7 +168,7 @@ public fun GoogleMap( } } - mapUpdaterScope.launch { + mapUpdaterScope.launch(start = CoroutineStart.UNDISPATCHED) { val composition = mapView.createComposition(mapClickListeners, parentComposition).apply { setContent(mapCompositionContent) } From 6d6095a9bc92dddccbc24795a7e4dd2df1c365fd Mon Sep 17 00:00:00 2001 From: Philip Date: Mon, 26 Feb 2024 13:42:58 +0100 Subject: [PATCH 034/103] Don't remove lifecycle from MapViewLifecycleController on detach to observe onDestroy event --- .../com/google/maps/android/compose/GoogleMap.kt | 9 ++------- .../android/compose/MapViewLifecycleController.kt | 14 +------------- 2 files changed, 3 insertions(+), 20 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 34f6e467..627aa5eb 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -202,9 +202,8 @@ public fun GoogleMap( mapView.registerAndSaveNewComponentCallbacks(context) } }, - onReset = { mapView -> - mapView.log("onReset") - mapLifecycleController!!.onLifecycleDetached() + onReset = { + // View is detached. }, onRelease = { mapView -> mapView.log("onRelease") @@ -214,10 +213,6 @@ public fun GoogleMap( tagData.componentCallbacks = null tagData.componentCallbacksContext = null } - // Invoke onDestroy + remove lifecycle callbacks for the MapView - mapLifecycleController!!.onDestroy() - // Clean up MapView. - mapView.removeAllViews() }, update = { mapView -> mapView.log("update") diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt b/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt index 985bcd3b..4a8d3625 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt @@ -43,16 +43,6 @@ internal class MapViewLifecycleController( } } - fun onDestroy() { - lifecycle = null - moveToLifecycleEvent(Event.ON_DESTROY) - } - - fun onLifecycleDetached() { - lifecycle = null - moveToLifecycleEvent(Event.ON_STOP) - } - /** * Move to the lifecycle event instead of setting it directly. * */ @@ -114,9 +104,7 @@ internal class MapViewLifecycleController( Event.ON_RESUME -> mapView.onResume() Event.ON_PAUSE -> mapView.onPause() Event.ON_STOP -> mapView.onStop() - Event.ON_DESTROY -> { - // Handled in AndroidView onRelease - } + Event.ON_DESTROY -> mapView.onDestroy() else -> throw IllegalStateException() } From 66a71cd97fd675acbd90300e6616e5966bd1efe6 Mon Sep 17 00:00:00 2001 From: Philip Date: Mon, 26 Feb 2024 13:59:44 +0100 Subject: [PATCH 035/103] Reformat code --- .../compose/MapsInLazyColumnActivity.kt | 10 +++--- .../google/maps/android/compose/GoogleMap.kt | 8 ++--- .../compose/MapViewLifecycleController.kt | 35 +++++++++++-------- 3 files changed, 29 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt b/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt index 225adb19..07a9f19c 100644 --- a/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt @@ -36,7 +36,7 @@ import com.google.android.gms.maps.model.CameraPosition import com.google.android.gms.maps.model.IndoorBuilding import com.google.android.gms.maps.model.LatLng -class MapsInLazyColumnActivity: ComponentActivity() { +class MapsInLazyColumnActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -118,7 +118,7 @@ private fun CardMap( }, cameraPositionState = cameraPositionState, onMapLoaded = { mapLoaded = true }, - indoorStateChangeListener = object: IndoorStateChangeListener { + indoorStateChangeListener = object : IndoorStateChangeListener { override fun onIndoorBuildingFocused() { super.onIndoorBuildingFocused() focusedBuildingInvocationCount++ @@ -181,7 +181,7 @@ private data class MapListItem( // From https://developers.google.com/public-data/docs/canonical/countries_csv private val countries = listOf( CountryLocation("Hong Kong", LatLng(22.396428, 114.109497), 5f), - CountryLocation("Madison Square Garden (has indoor mode)", LatLng(40.7504656,-73.9937246), 19.33f), + CountryLocation("Madison Square Garden (has indoor mode)", LatLng(40.7504656, -73.9937246), 19.33f), CountryLocation("Bolivia", LatLng(-16.290154, -63.588653), 5f), CountryLocation("Ecuador", LatLng(-1.831239, -78.183406), 5f), CountryLocation("Sweden", LatLng(60.128161, 18.643501), 5f), @@ -201,7 +201,7 @@ private val countries = listOf( private val mapListItems = countries .mapIndexed { index, country -> - MapListItem(country.name, country.latLng, country.zoom, "MapInLazyColumn#$index") -} + MapListItem(country.name, country.latLng, country.zoom, "MapInLazyColumn#$index") + } private data class CountryLocation(val name: String, val latLng: LatLng, val zoom: Float) \ No newline at end of file diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 627aa5eb..3b9c075d 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -225,7 +225,7 @@ public fun GoogleMap( } // componentCallbacksUpdated will be reset upon reuse so we need to check one time on each reuse. - if(!componentCallbacksUpdated) { + if (!componentCallbacksUpdated) { debugMapId = mapView.tagData().debugId componentCallbacksUpdated = true mapView.reRegisterComponentCallbacksIfChanged(context) @@ -234,7 +234,7 @@ public fun GoogleMap( mapLifecycleController!!.lifecycle = lifecycleOwner.lifecycle // Create Composition - if(!isCompositionSet) { + if (!isCompositionSet) { isCompositionSet = true setCompositionAsync(mapView) } @@ -259,7 +259,7 @@ private fun MapView.reRegisterComponentCallbacksIfChanged(context: Context) { // Should never be null here because it's set in [factory] val tagData = tagData() val componentCallbacksContext = tagData.componentCallbacksContext!! - if(componentCallbacksContext != context) { + if (componentCallbacksContext != context) { // New context. Unregister previous componentCallbacks and re-register on new context. val currentCallbacks = tagData.componentCallbacks!! componentCallbacksContext.unregisterComponentCallbacks(currentCallbacks) @@ -304,7 +304,7 @@ private suspend fun MapView.createComposition( } internal fun MapView.log(msg: String) { - Log.d(TAG, "[MapView/${ tagData().debugId }] $msg") + Log.d(TAG, "[MapView/${tagData().debugId}] $msg") } internal suspend inline fun disposingComposition(factory: () -> Composition) { diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt b/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt index 4a8d3625..e5109aa8 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt @@ -2,12 +2,12 @@ package com.google.maps.android.compose import android.os.Bundle import android.util.Log -import androidx.core.view.get import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle.Event import androidx.lifecycle.LifecycleEventObserver import com.google.android.gms.maps.MapView -import com.google.maps.android.compose.MapViewLifecycleController.LifecycleDirection.* +import com.google.maps.android.compose.MapViewLifecycleController.LifecycleDirection.Down +import com.google.maps.android.compose.MapViewLifecycleController.LifecycleDirection.Up internal class MapViewLifecycleController( isMapReused: Boolean, @@ -47,41 +47,46 @@ internal class MapViewLifecycleController( * Move to the lifecycle event instead of setting it directly. * */ private fun moveToLifecycleEvent(targetLifecycle: Event) { - if(targetLifecycle == previousLifecycleState) return + if (targetLifecycle == previousLifecycleState) return - val lifecycleDirection = if(targetLifecycle in lifecycleUp) Up else Down + val lifecycleDirection = if (targetLifecycle in lifecycleUp) Up else Down - (if(lifecycleDirection == Up) "UP" else "DOWN").let { strDirection -> + (if (lifecycleDirection == Up) "UP" else "DOWN").let { strDirection -> Log.d(lcTag, "Moving $strDirection from $previousLifecycleState to $targetLifecycle.") } do { val nextLifecycleState = nextLifecycleEvent(lifecycleDirection) setLifecycleEvent(nextLifecycleState) - } while(nextLifecycleState != targetLifecycle) + } while (nextLifecycleState != targetLifecycle) } - private fun nextLifecycleEvent(direction: LifecycleDirection) = when(previousLifecycleState) { - Event.ON_CREATE -> when(direction) { + private fun nextLifecycleEvent(direction: LifecycleDirection) = when (previousLifecycleState) { + Event.ON_CREATE -> when (direction) { Up -> Event.ON_START Down -> error("No lifecycle event below ON_CREATE.") } - Event.ON_START -> when(direction) { + + Event.ON_START -> when (direction) { Up -> Event.ON_RESUME Down -> error("No lifecycle event below ON_START.") } - Event.ON_RESUME -> when(direction) { + + Event.ON_RESUME -> when (direction) { Up -> error("No lifecycle event above ON_RESUME.") Down -> Event.ON_PAUSE } - Event.ON_PAUSE -> when(direction) { + + Event.ON_PAUSE -> when (direction) { Up -> Event.ON_RESUME Down -> Event.ON_STOP } - Event.ON_STOP -> when(direction) { + + Event.ON_STOP -> when (direction) { Up -> Event.ON_START Down -> Event.ON_DESTROY } + Event.ON_DESTROY -> error("No lifecycle event above ON_DESTROY") Event.ON_ANY -> error("Unsupported operation") } @@ -116,15 +121,15 @@ internal class MapViewLifecycleController( private val observer = LifecycleEventObserver { _, event -> Log.d(lcTag, "---===[ LEO: Lifecycle event received from LifecycleEventObserver: $event. ]===---") - if(!created) { + if (!created) { Log.d(lcTag, "LEO: Invoking initial ON_CREATE.") created = true setLifecycleEvent(Event.ON_CREATE) - } else if(event == Event.ON_CREATE) { + } else if (event == Event.ON_CREATE) { Log.d(lcTag, "LEO: ON_CREATE lifecycle event was received but view is already created.") } - if(event != Event.ON_CREATE) { + if (event != Event.ON_CREATE) { moveToLifecycleEvent(event) } } From 72c745c827f7a472afb0f16d4cf6ec18e67cc010 Mon Sep 17 00:00:00 2001 From: Philip S Date: Tue, 16 Apr 2024 09:44:03 +0200 Subject: [PATCH 036/103] Move disposingComposition function to StreetView.kt as it's no longer used in GoogleMap.kt --- .../java/com/google/maps/android/compose/GoogleMap.kt | 9 --------- .../maps/android/compose/streetview/StreetView.kt | 11 ++++++++++- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 3b9c075d..c614254b 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -307,15 +307,6 @@ internal fun MapView.log(msg: String) { Log.d(TAG, "[MapView/${tagData().debugId}] $msg") } -internal suspend inline fun disposingComposition(factory: () -> Composition) { - val composition = factory() - try { - awaitCancellation() - } finally { - composition.dispose() - } -} - private fun MapView.componentCallbacks(): ComponentCallbacks = object : ComponentCallbacks { override fun onConfigurationChanged(config: Configuration) {} diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetView.kt b/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetView.kt index e73c7d53..0e8da01c 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetView.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetView.kt @@ -38,9 +38,9 @@ import androidx.lifecycle.LifecycleEventObserver import com.google.android.gms.maps.StreetViewPanoramaOptions import com.google.android.gms.maps.StreetViewPanoramaView import com.google.android.gms.maps.model.StreetViewPanoramaOrientation -import com.google.maps.android.compose.disposingComposition import com.google.maps.android.ktx.MapsExperimentalFeature import com.google.maps.android.ktx.awaitStreetViewPanorama +import kotlinx.coroutines.awaitCancellation /** * A composable for displaying a Street View for a given location. A location might not be available for a given @@ -131,6 +131,15 @@ private fun StreetViewLifecycle(streetView: StreetViewPanoramaView) { } } +internal suspend inline fun disposingComposition(factory: () -> Composition) { + val composition = factory() + try { + awaitCancellation() + } finally { + composition.dispose() + } +} + private suspend inline fun StreetViewPanoramaView.newComposition( parent: CompositionContext, noinline content: @Composable () -> Unit From 8c2991d678c05b95c1e95f7b846b46b8e0596e31 Mon Sep 17 00:00:00 2001 From: Philip S Date: Tue, 16 Apr 2024 09:44:33 +0200 Subject: [PATCH 037/103] Use awaitCancellation instead of infinite delay --- .../main/java/com/google/maps/android/compose/GoogleMap.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index c614254b..f86d6d5d 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -174,9 +174,9 @@ public fun GoogleMap( } // Dispose composition when mapUpdaterScope is cancelled. - launch { - delay(Duration.INFINITE) - }.invokeOnCompletion { + try { + awaitCancellation() + } finally { mapView.log("Disposing composition...") composition.dispose() } From 49c9de727f6496b8b5721c7a914e76bec41a700c Mon Sep 17 00:00:00 2001 From: Philip S Date: Tue, 16 Apr 2024 10:39:56 +0200 Subject: [PATCH 038/103] Fix lifecycle event error message --- .../maps/android/compose/MapViewLifecycleController.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt b/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt index e5109aa8..3be19bca 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt @@ -64,16 +64,16 @@ internal class MapViewLifecycleController( private fun nextLifecycleEvent(direction: LifecycleDirection) = when (previousLifecycleState) { Event.ON_CREATE -> when (direction) { Up -> Event.ON_START - Down -> error("No lifecycle event below ON_CREATE.") + Down -> error("No lifecycle state below created.") } Event.ON_START -> when (direction) { Up -> Event.ON_RESUME - Down -> error("No lifecycle event below ON_START.") + Down -> error("No lifecycle state below started.") } Event.ON_RESUME -> when (direction) { - Up -> error("No lifecycle event above ON_RESUME.") + Up -> error("No lifecycle state above resumed.") Down -> Event.ON_PAUSE } @@ -87,8 +87,8 @@ internal class MapViewLifecycleController( Down -> Event.ON_DESTROY } - Event.ON_DESTROY -> error("No lifecycle event above ON_DESTROY") Event.ON_ANY -> error("Unsupported operation") + Event.ON_DESTROY -> error("No lifecycle state above destroyed") } From a8725b9f3067fccfd597036af06e2d391472acd1 Mon Sep 17 00:00:00 2001 From: Philip S Date: Tue, 16 Apr 2024 10:40:24 +0200 Subject: [PATCH 039/103] Use else branch instead of ON_ANY in nextLifecycleEvent --- .../google/maps/android/compose/MapViewLifecycleController.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt b/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt index 3be19bca..a69b7f7f 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt @@ -87,8 +87,8 @@ internal class MapViewLifecycleController( Down -> Event.ON_DESTROY } - Event.ON_ANY -> error("Unsupported operation") Event.ON_DESTROY -> error("No lifecycle state above destroyed") + else -> error("Unsupported operation") } From a554a01c9f578fd2499c115f6f3baabce3b8327a Mon Sep 17 00:00:00 2001 From: Philip S Date: Tue, 16 Apr 2024 10:41:35 +0200 Subject: [PATCH 040/103] Change name of setCompositionAsync to launchComposition and make it more idiomatic --- .../com/google/maps/android/compose/GoogleMap.kt | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index f86d6d5d..6798c613 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -48,11 +48,11 @@ import com.google.android.gms.maps.MapView import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.PointOfInterest import com.google.maps.android.ktx.awaitMap +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Job import kotlinx.coroutines.awaitCancellation -import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import kotlin.time.Duration internal const val TAG = "GoogleMap" @@ -143,9 +143,7 @@ public fun GoogleMap( * Create and apply the [content] compositions to the map + * dispose the [Composition] when the parent composable is disposed. * */ - fun setCompositionAsync( - mapView: MapView - ) { + fun CoroutineScope.launchComposition(mapView: MapView): Job { mapView.log("Creating composition...") val mapCompositionContent: @Composable () -> Unit = { @@ -168,7 +166,7 @@ public fun GoogleMap( } } - mapUpdaterScope.launch(start = CoroutineStart.UNDISPATCHED) { + return launch(start = CoroutineStart.UNDISPATCHED) { val composition = mapView.createComposition(mapClickListeners, parentComposition).apply { setContent(mapCompositionContent) } @@ -181,7 +179,6 @@ public fun GoogleMap( composition.dispose() } } - } var isCompositionSet by remember { mutableStateOf(false) } @@ -236,7 +233,7 @@ public fun GoogleMap( // Create Composition if (!isCompositionSet) { isCompositionSet = true - setCompositionAsync(mapView) + mapUpdaterScope.launchComposition(mapView) } } ) From 47894887bc029b951c8e48508e352e8bbf437eec Mon Sep 17 00:00:00 2001 From: Philip S Date: Wed, 17 Apr 2024 12:01:16 +0200 Subject: [PATCH 041/103] Undo import changes --- .../main/java/com/google/maps/android/compose/GroundOverlay.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GroundOverlay.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GroundOverlay.kt index 67132145..89828218 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GroundOverlay.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GroundOverlay.kt @@ -23,8 +23,8 @@ import com.google.android.gms.maps.model.GroundOverlay import com.google.android.gms.maps.model.GroundOverlayOptions import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.LatLngBounds -import com.google.maps.android.compose.GroundOverlayPosition.Companion.create import com.google.maps.android.ktx.addGroundOverlay +import kotlin.IllegalStateException internal class GroundOverlayNode( val groundOverlay: GroundOverlay, From 2077915d20a86a528673a5f19ab9ad1e3d620c6d Mon Sep 17 00:00:00 2001 From: Philip S Date: Wed, 17 Apr 2024 12:02:22 +0200 Subject: [PATCH 042/103] Undo unrelated import changes --- .../main/java/com/google/maps/android/compose/InputHandler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/InputHandler.kt b/maps-compose/src/main/java/com/google/maps/android/compose/InputHandler.kt index cac37198..a6bbda2b 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/InputHandler.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/InputHandler.kt @@ -6,11 +6,11 @@ import androidx.compose.runtime.ComposeNode import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import com.google.android.gms.maps.GoogleMap.OnMarkerClickListener import com.google.android.gms.maps.model.Circle import com.google.android.gms.maps.model.GroundOverlay import com.google.android.gms.maps.model.Marker import com.google.android.gms.maps.model.Polygon +import com.google.android.gms.maps.GoogleMap.OnMarkerClickListener import com.google.android.gms.maps.model.Polyline /** From 13247ce7501e8100ec270458a2f0ed4c1592f205 Mon Sep 17 00:00:00 2001 From: Philip S Date: Wed, 17 Apr 2024 12:03:45 +0200 Subject: [PATCH 043/103] Undo unrelated refactoring --- .../java/com/google/maps/android/compose/MapClickListeners.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/MapClickListeners.kt b/maps-compose/src/main/java/com/google/maps/android/compose/MapClickListeners.kt index 62771089..42b3ee6e 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/MapClickListeners.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/MapClickListeners.kt @@ -88,8 +88,7 @@ internal class MapClickListenerNode( @Composable internal fun MapClickListenerUpdater() { // The mapClickListeners container object is not allowed to ever change - val applier = (currentComposer.applier as MapApplier) - val mapClickListeners = applier.mapClickListeners + val mapClickListeners = (currentComposer.applier as MapApplier).mapClickListeners with(mapClickListeners) { ::indoorStateChangeListener.let { callback -> From 756ee072883a86938bd87946a9bd17f3bb399b8e Mon Sep 17 00:00:00 2001 From: Philip S Date: Thu, 18 Apr 2024 10:36:35 +0200 Subject: [PATCH 044/103] Clarify that dependency is temporary --- maps-compose/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/maps-compose/build.gradle b/maps-compose/build.gradle index e99ec7c3..5bebae4f 100644 --- a/maps-compose/build.gradle +++ b/maps-compose/build.gradle @@ -38,6 +38,7 @@ dependencies { implementation platform(libs.androidx.compose.bom) implementation libs.androidx.core implementation libs.androidx.compose.foundation + // TODO - remove before release. Just for debugging purposes. implementation libs.androidx.compose.material implementation libs.kotlin api libs.maps.ktx.std From bfe1e824917c1b098e50d1bc1e1fc202ea3a6e0b Mon Sep 17 00:00:00 2001 From: Philip S Date: Thu, 18 Apr 2024 10:43:20 +0200 Subject: [PATCH 045/103] Create IncrementalLifecycleApplier This class is simpler and uses better practices than MapViewLifecycleController. Instead of modifying a MapView directly it invokes events on an internface. Also it utilizes Lifecycle.State which is more clear and idiomatic than only managing `Lifecycle.Event`s. Also, instead of relying on custom lifecycle logic, standard Lifecycle.Event utils are used. --- .../compose/IncrementalLifecycleApplier.kt | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 maps-compose/src/main/java/com/google/maps/android/compose/IncrementalLifecycleApplier.kt diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/IncrementalLifecycleApplier.kt b/maps-compose/src/main/java/com/google/maps/android/compose/IncrementalLifecycleApplier.kt new file mode 100644 index 00000000..6b6e3305 --- /dev/null +++ b/maps-compose/src/main/java/com/google/maps/android/compose/IncrementalLifecycleApplier.kt @@ -0,0 +1,106 @@ +package com.google.maps.android.compose + +import android.os.Bundle +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import com.google.android.gms.maps.MapView + +/** Invokes lifecycle events on the [lifecycleApplier] based on the current [lifecycle]. */ +internal class IncrementalLifecycleApplier( + private val lifecycle: Lifecycle, + private var currentLifecycleState: Lifecycle.State, + private val lifecycleApplier: LifecycleApplier +) { + private lateinit var lifecycleEventObserver: LifecycleEventObserver + + init { + observeLifecycleEvents() + } + + private fun observeLifecycleEvents() { + lifecycleEventObserver = LifecycleEventObserver { _, event -> onLifecycleEvent(event) } + lifecycle.addObserver(lifecycleEventObserver) + } + + private fun onLifecycleEvent(lifecycleEvent: Lifecycle.Event) { + val targetState = lifecycleEvent.targetState + + if (targetState == currentLifecycleState) return + + val isValidTargetLifecycleEvent = isValidTargetLifecycleEvent( + sourceLifecycleState = currentLifecycleState, + targetLifecycleEvent = lifecycleEvent + ) + if (!isValidTargetLifecycleEvent) return + + moveToLifecycleEvent(lifecycleEvent) + } + + /** @return if there is a valid path from [sourceLifecycleState] to [targetLifecycleEvent]. */ + private fun isValidTargetLifecycleEvent( + sourceLifecycleState: Lifecycle.State, + targetLifecycleEvent: Lifecycle.Event + ): Boolean { + val direction = getLifecycleDirection(targetLifecycleEvent) + var nextLifecycleEvent: Lifecycle.Event? = + getNextLifecycleEvent(sourceLifecycleState, direction) + + while (true) { + if (nextLifecycleEvent == null) return false + if (nextLifecycleEvent == targetLifecycleEvent) return true + nextLifecycleEvent = getNextLifecycleEvent(nextLifecycleEvent.targetState, direction) + } + } + + private fun getLifecycleDirection(targetLifecycleEvent: Lifecycle.Event) = when { + targetLifecycleEvent.targetState.isAtLeast(currentLifecycleState) -> LifecycleEventDirection.UP + else -> LifecycleEventDirection.DOWN + } + + private fun getNextLifecycleEvent(from: Lifecycle.State, direction: LifecycleEventDirection) = + when (direction) { + LifecycleEventDirection.UP -> Lifecycle.Event.upFrom(from) + LifecycleEventDirection.DOWN -> Lifecycle.Event.downFrom(from) + } + + private fun moveToLifecycleEvent(targetLifecycleEvent: Lifecycle.Event) { + val direction = getLifecycleDirection(targetLifecycleEvent) + + do { + val nextEvent = getNextLifecycleEvent(currentLifecycleState, direction)!! + invokeLifecycleEvent(nextEvent) + } while (nextEvent != targetLifecycleEvent) + } + + private fun invokeLifecycleEvent(lifecycleEvent: Lifecycle.Event) { + lifecycleApplier.invokeEvent(lifecycleEvent) + currentLifecycleState = lifecycleEvent.targetState + } + + fun destroyAndDispose() { + onLifecycleEvent(Lifecycle.Event.ON_DESTROY) + lifecycle.removeObserver(lifecycleEventObserver) + } +} + +private enum class LifecycleEventDirection { UP, DOWN } + +internal interface LifecycleApplier { + fun invokeEvent(event: Lifecycle.Event) +} + +internal class MapViewLifecycleApplier(private val mapView: MapView) : LifecycleApplier { + override fun invokeEvent(event: Lifecycle.Event) { + when (event) { + Lifecycle.Event.ON_CREATE -> mapView.onCreate(Bundle()) + Lifecycle.Event.ON_START -> mapView.onStart() + Lifecycle.Event.ON_RESUME -> mapView.onResume() + Lifecycle.Event.ON_PAUSE -> mapView.onPause() + Lifecycle.Event.ON_STOP -> mapView.onStop() + Lifecycle.Event.ON_DESTROY -> mapView.onDestroy() + Lifecycle.Event.ON_ANY -> error("Cannot invoke lifecycle event ON_ANY on mapView.") + } + + mapView.tagData().lifecycleState = event.targetState + } +} From 61335faf5f44b299e2df3aecb3a357db0e85ac24 Mon Sep 17 00:00:00 2001 From: Philip S Date: Thu, 18 Apr 2024 10:46:30 +0200 Subject: [PATCH 046/103] Delete MapViewLifecycleController and replace with IncrementalLifecycleApplier --- .../google/maps/android/compose/GoogleMap.kt | 41 +++--- .../compose/MapViewLifecycleController.kt | 136 ------------------ 2 files changed, 22 insertions(+), 155 deletions(-) delete mode 100644 maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 6798c613..7787ac52 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -42,6 +42,7 @@ import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView +import androidx.lifecycle.Lifecycle import com.google.android.gms.maps.GoogleMapOptions import com.google.android.gms.maps.LocationSource import com.google.android.gms.maps.MapView @@ -131,7 +132,7 @@ public fun GoogleMap( val lifecycleOwner = LocalLifecycleOwner.current val context = LocalContext.current - var mapLifecycleController: MapViewLifecycleController? by remember { mutableStateOf(null) } + var mapLifecycleApplier: IncrementalLifecycleApplier? by remember { mutableStateOf(null) } // Debug stuff val debugCompositionId = remember { compositionCounter++ } @@ -191,12 +192,18 @@ public fun GoogleMap( Log.d(TAG, "Factory") debugIsMapReused = false MapView(context, googleMapOptionsFactory()).also { mapView -> - mapLifecycleController = MapViewLifecycleController( - isMapReused = false, - mapView = mapView + mapView.registerAndSaveNewComponentCallbacks(context) + + mapLifecycleApplier = IncrementalLifecycleApplier( + lifecycleOwner.lifecycle, + currentLifecycleState = Lifecycle.State.INITIALIZED, + lifecycleApplier = MapViewLifecycleApplier(mapView) ) mapView.registerAndSaveNewComponentCallbacks(context) + // This mapLifecycleApplier also has to be active while the MapView is detached from + // the UI. Therefore we store it i the MapView's tag so that it can be retrieved in the future. + mapView.tagData().lifecycleApplier = mapLifecycleApplier } }, onReset = { @@ -204,21 +211,17 @@ public fun GoogleMap( }, onRelease = { mapView -> mapView.log("onRelease") - val tagData = mapView.tagData() - tagData.componentCallbacks?.let { componentCallbacks -> - tagData.componentCallbacksContext?.unregisterComponentCallbacks(componentCallbacks) - tagData.componentCallbacks = null - tagData.componentCallbacksContext = null + mapView.tagData().let { tagData -> + tagData.lifecycleApplier!!.destroyAndDispose() + tagData.componentCallbacks?.let { componentCallbacks -> + tagData.mapViewContext?.unregisterComponentCallbacks(componentCallbacks) + } } }, update = { mapView -> mapView.log("update") - if (mapLifecycleController == null) { - debugIsMapReused = true - mapLifecycleController = MapViewLifecycleController( - isMapReused = true, - mapView = mapView - ) + if (mapLifecycleApplier == null) { + mapLifecycleApplier = mapView.tagData().lifecycleApplier!! } // componentCallbacksUpdated will be reset upon reuse so we need to check one time on each reuse. @@ -228,8 +231,6 @@ public fun GoogleMap( mapView.reRegisterComponentCallbacksIfChanged(context) } - mapLifecycleController!!.lifecycle = lifecycleOwner.lifecycle - // Create Composition if (!isCompositionSet) { isCompositionSet = true @@ -274,7 +275,9 @@ private fun MapView.registerAndSaveNewComponentCallbacks(context: Context) { internal data class MapTagData( var componentCallbacks: ComponentCallbacks?, - var componentCallbacksContext: Context?, + var mapViewContext: Context?, + var lifecycleState: Lifecycle.State?, + var lifecycleApplier: IncrementalLifecycleApplier?, val debugId: Int = nextId ) { companion object { @@ -284,7 +287,7 @@ internal data class MapTagData( } internal fun MapView.tagData(): MapTagData = tag as? MapTagData ?: run { - MapTagData(null, null).also { newTag -> + MapTagData(null, null, null, null).also { newTag -> tag = newTag } } diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt b/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt deleted file mode 100644 index a69b7f7f..00000000 --- a/maps-compose/src/main/java/com/google/maps/android/compose/MapViewLifecycleController.kt +++ /dev/null @@ -1,136 +0,0 @@ -package com.google.maps.android.compose - -import android.os.Bundle -import android.util.Log -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.Lifecycle.Event -import androidx.lifecycle.LifecycleEventObserver -import com.google.android.gms.maps.MapView -import com.google.maps.android.compose.MapViewLifecycleController.LifecycleDirection.Down -import com.google.maps.android.compose.MapViewLifecycleController.LifecycleDirection.Up - -internal class MapViewLifecycleController( - isMapReused: Boolean, - private val mapView: MapView -) { - private val lcTag = run { - val mapViewId = mapView.tagData().debugId - "MVLC/$mapViewId" - } - - private companion object { - val lifecycleUp = listOf( - Event.ON_CREATE, - Event.ON_START, - Event.ON_RESUME - ) - } - - private enum class LifecycleDirection { - Up, Down - } - - private var previousLifecycleState = if (isMapReused) - Event.ON_STOP else - Event.ON_CREATE - - var lifecycle: Lifecycle? = null - set(value) { - if (field !== value) { - field?.removeObserver(observer) - value?.addObserver(observer) - field = value - } - } - - /** - * Move to the lifecycle event instead of setting it directly. - * */ - private fun moveToLifecycleEvent(targetLifecycle: Event) { - if (targetLifecycle == previousLifecycleState) return - - val lifecycleDirection = if (targetLifecycle in lifecycleUp) Up else Down - - (if (lifecycleDirection == Up) "UP" else "DOWN").let { strDirection -> - Log.d(lcTag, "Moving $strDirection from $previousLifecycleState to $targetLifecycle.") - } - - do { - val nextLifecycleState = nextLifecycleEvent(lifecycleDirection) - setLifecycleEvent(nextLifecycleState) - } while (nextLifecycleState != targetLifecycle) - } - - private fun nextLifecycleEvent(direction: LifecycleDirection) = when (previousLifecycleState) { - Event.ON_CREATE -> when (direction) { - Up -> Event.ON_START - Down -> error("No lifecycle state below created.") - } - - Event.ON_START -> when (direction) { - Up -> Event.ON_RESUME - Down -> error("No lifecycle state below started.") - } - - Event.ON_RESUME -> when (direction) { - Up -> error("No lifecycle state above resumed.") - Down -> Event.ON_PAUSE - } - - Event.ON_PAUSE -> when (direction) { - Up -> Event.ON_RESUME - Down -> Event.ON_STOP - } - - Event.ON_STOP -> when (direction) { - Up -> Event.ON_START - Down -> Event.ON_DESTROY - } - - Event.ON_DESTROY -> error("No lifecycle state above destroyed") - else -> error("Unsupported operation") - } - - - private fun setLifecycleEvent(event: Event) { - Log.d(lcTag, "Invoking: $event!") - - when (event) { - Event.ON_CREATE -> { - // Skip calling mapView.onCreate if the lifecycle did not go through onDestroy - in - // this case the GoogleMap composable also doesn't leave the composition. So, - // recreating the map does not restore state properly which must be avoided. - if (previousLifecycleState != Event.ON_STOP) { - mapView.onCreate(Bundle()) - } - } - - Event.ON_START -> mapView.onStart() - Event.ON_RESUME -> mapView.onResume() - Event.ON_PAUSE -> mapView.onPause() - Event.ON_STOP -> mapView.onStop() - Event.ON_DESTROY -> mapView.onDestroy() - - else -> throw IllegalStateException() - } - previousLifecycleState = event - } - - private var created = isMapReused - - private val observer = LifecycleEventObserver { _, event -> - Log.d(lcTag, "---===[ LEO: Lifecycle event received from LifecycleEventObserver: $event. ]===---") - - if (!created) { - Log.d(lcTag, "LEO: Invoking initial ON_CREATE.") - created = true - setLifecycleEvent(Event.ON_CREATE) - } else if (event == Event.ON_CREATE) { - Log.d(lcTag, "LEO: ON_CREATE lifecycle event was received but view is already created.") - } - - if (event != Event.ON_CREATE) { - moveToLifecycleEvent(event) - } - } -} From 0cb4a5d357b65e5ae219ffeff8415e9216b0194a Mon Sep 17 00:00:00 2001 From: Philip S Date: Thu, 18 Apr 2024 11:00:34 +0200 Subject: [PATCH 047/103] Resolve requirements --- .../google/maps/android/compose/GoogleMap.kt | 37 ++++--------------- .../android/compose/streetview/StreetView.kt | 2 +- 2 files changed, 8 insertions(+), 31 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 7787ac52..6e710626 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -137,8 +137,7 @@ public fun GoogleMap( // Debug stuff val debugCompositionId = remember { compositionCounter++ } var debugMapId: Int? by remember { mutableStateOf(null) } - - val mapUpdaterScope = rememberCoroutineScope() + var debugIsMapReused: Boolean? by remember { mutableStateOf(null) } /** * Create and apply the [content] compositions to the map + @@ -172,7 +171,6 @@ public fun GoogleMap( setContent(mapCompositionContent) } - // Dispose composition when mapUpdaterScope is cancelled. try { awaitCancellation() } finally { @@ -183,8 +181,7 @@ public fun GoogleMap( } var isCompositionSet by remember { mutableStateOf(false) } - var componentCallbacksUpdated by remember { mutableStateOf(false) } - var debugIsMapReused: Boolean? by remember { mutableStateOf(null) } + val mapUpdaterScope = rememberCoroutineScope() AndroidView( modifier = modifier, @@ -200,7 +197,6 @@ public fun GoogleMap( lifecycleApplier = MapViewLifecycleApplier(mapView) ) - mapView.registerAndSaveNewComponentCallbacks(context) // This mapLifecycleApplier also has to be active while the MapView is detached from // the UI. Therefore we store it i the MapView's tag so that it can be retrieved in the future. mapView.tagData().lifecycleApplier = mapLifecycleApplier @@ -217,23 +213,17 @@ public fun GoogleMap( tagData.mapViewContext?.unregisterComponentCallbacks(componentCallbacks) } } + mapView.tag = null }, update = { mapView -> - mapView.log("update") if (mapLifecycleApplier == null) { mapLifecycleApplier = mapView.tagData().lifecycleApplier!! } - // componentCallbacksUpdated will be reset upon reuse so we need to check one time on each reuse. - if (!componentCallbacksUpdated) { - debugMapId = mapView.tagData().debugId - componentCallbacksUpdated = true - mapView.reRegisterComponentCallbacksIfChanged(context) - } - // Create Composition if (!isCompositionSet) { isCompositionSet = true + debugMapId = mapView.tagData().debugId mapUpdaterScope.launchComposition(mapView) } } @@ -252,24 +242,11 @@ public fun GoogleMap( } } -private fun MapView.reRegisterComponentCallbacksIfChanged(context: Context) { - // If componentCallbacks haven't been updated since initial composition, re-register to new context if necessary. - // Should never be null here because it's set in [factory] - val tagData = tagData() - val componentCallbacksContext = tagData.componentCallbacksContext!! - if (componentCallbacksContext != context) { - // New context. Unregister previous componentCallbacks and re-register on new context. - val currentCallbacks = tagData.componentCallbacks!! - componentCallbacksContext.unregisterComponentCallbacks(currentCallbacks) - this.registerAndSaveNewComponentCallbacks(context) - } -} - private fun MapView.registerAndSaveNewComponentCallbacks(context: Context) { val newComponentCallbacks = this.componentCallbacks() val tagData = tagData() tagData.componentCallbacks = newComponentCallbacks - tagData.componentCallbacksContext = context + tagData.mapViewContext = context context.registerComponentCallbacks(newComponentCallbacks) } @@ -303,8 +280,8 @@ private suspend fun MapView.createComposition( ) } -internal fun MapView.log(msg: String) { - Log.d(TAG, "[MapView/${tagData().debugId}] $msg") +internal fun MapView.log(msg: String, tag: String = TAG) { + Log.d(tag, "[MapView/${tagData().debugId}] $msg") } private fun MapView.componentCallbacks(): ComponentCallbacks = diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetView.kt b/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetView.kt index 0e8da01c..98227a24 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetView.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetView.kt @@ -131,7 +131,7 @@ private fun StreetViewLifecycle(streetView: StreetViewPanoramaView) { } } -internal suspend inline fun disposingComposition(factory: () -> Composition) { +private suspend inline fun disposingComposition(factory: () -> Composition) { val composition = factory() try { awaitCancellation() From 3c5acc9b6d2a61ff525f0edbc34885e0da5bb8bd Mon Sep 17 00:00:00 2001 From: Philip S Date: Thu, 18 Apr 2024 11:00:49 +0200 Subject: [PATCH 048/103] Add some logs for IncrementalLifecycleApplier --- .../maps/android/compose/IncrementalLifecycleApplier.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/IncrementalLifecycleApplier.kt b/maps-compose/src/main/java/com/google/maps/android/compose/IncrementalLifecycleApplier.kt index 6b6e3305..6b68e12d 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/IncrementalLifecycleApplier.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/IncrementalLifecycleApplier.kt @@ -1,6 +1,7 @@ package com.google.maps.android.compose import android.os.Bundle +import android.util.Log import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import com.google.android.gms.maps.MapView @@ -78,6 +79,7 @@ internal class IncrementalLifecycleApplier( } fun destroyAndDispose() { + Log.d(TAG, "destroyAndDispose()") onLifecycleEvent(Lifecycle.Event.ON_DESTROY) lifecycle.removeObserver(lifecycleEventObserver) } @@ -101,6 +103,8 @@ internal class MapViewLifecycleApplier(private val mapView: MapView) : Lifecycle Lifecycle.Event.ON_ANY -> error("Cannot invoke lifecycle event ON_ANY on mapView.") } + Log.d("MapViewLifecycleApplier", "[MapView#${ mapView.tagData().debugId }]Invoking $event") + mapView.tagData().lifecycleState = event.targetState } } From c2a0f7319db3b5358022c54fa8d7c2b629cc7389 Mon Sep 17 00:00:00 2001 From: Philip S Date: Thu, 18 Apr 2024 11:04:58 +0200 Subject: [PATCH 049/103] Remove `currentLifecycleState` parameter for IncrementalLifecycleApplier --- .../src/main/java/com/google/maps/android/compose/GoogleMap.kt | 1 - .../google/maps/android/compose/IncrementalLifecycleApplier.kt | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 6e710626..a79ea981 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -193,7 +193,6 @@ public fun GoogleMap( mapLifecycleApplier = IncrementalLifecycleApplier( lifecycleOwner.lifecycle, - currentLifecycleState = Lifecycle.State.INITIALIZED, lifecycleApplier = MapViewLifecycleApplier(mapView) ) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/IncrementalLifecycleApplier.kt b/maps-compose/src/main/java/com/google/maps/android/compose/IncrementalLifecycleApplier.kt index 6b68e12d..3b82fe93 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/IncrementalLifecycleApplier.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/IncrementalLifecycleApplier.kt @@ -9,9 +9,9 @@ import com.google.android.gms.maps.MapView /** Invokes lifecycle events on the [lifecycleApplier] based on the current [lifecycle]. */ internal class IncrementalLifecycleApplier( private val lifecycle: Lifecycle, - private var currentLifecycleState: Lifecycle.State, private val lifecycleApplier: LifecycleApplier ) { + private var currentLifecycleState = Lifecycle.State.INITIALIZED private lateinit var lifecycleEventObserver: LifecycleEventObserver init { From aa7a1763fe2f03c414c5a806f48ee347af676895 Mon Sep 17 00:00:00 2001 From: Philip S Date: Thu, 18 Apr 2024 11:10:57 +0200 Subject: [PATCH 050/103] Make some functions in IncrementalLifecycleApplier pure and move to companion object --- .../compose/IncrementalLifecycleApplier.kt | 60 ++++++++++--------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/IncrementalLifecycleApplier.kt b/maps-compose/src/main/java/com/google/maps/android/compose/IncrementalLifecycleApplier.kt index 3b82fe93..2b5366f4 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/IncrementalLifecycleApplier.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/IncrementalLifecycleApplier.kt @@ -37,35 +37,8 @@ internal class IncrementalLifecycleApplier( moveToLifecycleEvent(lifecycleEvent) } - /** @return if there is a valid path from [sourceLifecycleState] to [targetLifecycleEvent]. */ - private fun isValidTargetLifecycleEvent( - sourceLifecycleState: Lifecycle.State, - targetLifecycleEvent: Lifecycle.Event - ): Boolean { - val direction = getLifecycleDirection(targetLifecycleEvent) - var nextLifecycleEvent: Lifecycle.Event? = - getNextLifecycleEvent(sourceLifecycleState, direction) - - while (true) { - if (nextLifecycleEvent == null) return false - if (nextLifecycleEvent == targetLifecycleEvent) return true - nextLifecycleEvent = getNextLifecycleEvent(nextLifecycleEvent.targetState, direction) - } - } - - private fun getLifecycleDirection(targetLifecycleEvent: Lifecycle.Event) = when { - targetLifecycleEvent.targetState.isAtLeast(currentLifecycleState) -> LifecycleEventDirection.UP - else -> LifecycleEventDirection.DOWN - } - - private fun getNextLifecycleEvent(from: Lifecycle.State, direction: LifecycleEventDirection) = - when (direction) { - LifecycleEventDirection.UP -> Lifecycle.Event.upFrom(from) - LifecycleEventDirection.DOWN -> Lifecycle.Event.downFrom(from) - } - private fun moveToLifecycleEvent(targetLifecycleEvent: Lifecycle.Event) { - val direction = getLifecycleDirection(targetLifecycleEvent) + val direction = getLifecycleDirection(currentLifecycleState, targetLifecycleEvent) do { val nextEvent = getNextLifecycleEvent(currentLifecycleState, direction)!! @@ -83,6 +56,37 @@ internal class IncrementalLifecycleApplier( onLifecycleEvent(Lifecycle.Event.ON_DESTROY) lifecycle.removeObserver(lifecycleEventObserver) } + + private companion object { + + /** @return if there is a valid path from [sourceLifecycleState] to [targetLifecycleEvent]. */ + private fun isValidTargetLifecycleEvent( + sourceLifecycleState: Lifecycle.State, + targetLifecycleEvent: Lifecycle.Event + ): Boolean { + val direction = getLifecycleDirection(sourceLifecycleState, targetLifecycleEvent) + var nextLifecycleEvent: Lifecycle.Event? = getNextLifecycleEvent(sourceLifecycleState, direction) + + while (true) { + if (nextLifecycleEvent == null) return false + if (nextLifecycleEvent == targetLifecycleEvent) return true + nextLifecycleEvent = getNextLifecycleEvent(nextLifecycleEvent.targetState, direction) + } + } + + fun getLifecycleDirection( + sourceLifecycleState: Lifecycle.State, + targetLifecycleEvent: Lifecycle.Event + ) = when { + targetLifecycleEvent.targetState.isAtLeast(sourceLifecycleState) -> LifecycleEventDirection.UP + else -> LifecycleEventDirection.DOWN + } + + fun getNextLifecycleEvent(from: Lifecycle.State, direction: LifecycleEventDirection) = when (direction) { + LifecycleEventDirection.UP -> Lifecycle.Event.upFrom(from) + LifecycleEventDirection.DOWN -> Lifecycle.Event.downFrom(from) + } + } } private enum class LifecycleEventDirection { UP, DOWN } From c67f1ed306de87685b4caa404d8bb9f25df2cf57 Mon Sep 17 00:00:00 2001 From: Philip S Date: Thu, 18 Apr 2024 12:39:22 +0200 Subject: [PATCH 051/103] Add support to override lifecycle state Used to set lifecycle state to Created when mapView is detached + restore once reattached --- .../google/maps/android/compose/GoogleMap.kt | 17 ++-- .../compose/IncrementalLifecycleApplier.kt | 82 +++++++++++++++---- 2 files changed, 75 insertions(+), 24 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index a79ea981..54df9e8f 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -132,7 +132,7 @@ public fun GoogleMap( val lifecycleOwner = LocalLifecycleOwner.current val context = LocalContext.current - var mapLifecycleApplier: IncrementalLifecycleApplier? by remember { mutableStateOf(null) } + var mapLifecycleApplier: IncrementalMapLifecycleApplier? by remember { mutableStateOf(null) } // Debug stuff val debugCompositionId = remember { compositionCounter++ } @@ -191,18 +191,16 @@ public fun GoogleMap( MapView(context, googleMapOptionsFactory()).also { mapView -> mapView.registerAndSaveNewComponentCallbacks(context) - mapLifecycleApplier = IncrementalLifecycleApplier( - lifecycleOwner.lifecycle, - lifecycleApplier = MapViewLifecycleApplier(mapView) - ) + mapLifecycleApplier = IncrementalMapLifecycleApplier(lifecycleOwner.lifecycle, mapView) // This mapLifecycleApplier also has to be active while the MapView is detached from // the UI. Therefore we store it i the MapView's tag so that it can be retrieved in the future. mapView.tagData().lifecycleApplier = mapLifecycleApplier } }, - onReset = { + onReset = { mapView -> // View is detached. + mapView.tagData().lifecycleApplier!!.setTemporaryLifecycleState(Lifecycle.State.CREATED) }, onRelease = { mapView -> mapView.log("onRelease") @@ -222,7 +220,10 @@ public fun GoogleMap( // Create Composition if (!isCompositionSet) { isCompositionSet = true - debugMapId = mapView.tagData().debugId + mapView.tagData().let { tagData -> + tagData.lifecycleApplier!!.clearTemporaryLifecycleState() + debugMapId = tagData.debugId + } mapUpdaterScope.launchComposition(mapView) } } @@ -253,7 +254,7 @@ internal data class MapTagData( var componentCallbacks: ComponentCallbacks?, var mapViewContext: Context?, var lifecycleState: Lifecycle.State?, - var lifecycleApplier: IncrementalLifecycleApplier?, + var lifecycleApplier: IncrementalMapLifecycleApplier?, val debugId: Int = nextId ) { companion object { diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/IncrementalLifecycleApplier.kt b/maps-compose/src/main/java/com/google/maps/android/compose/IncrementalLifecycleApplier.kt index 2b5366f4..ecee5c51 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/IncrementalLifecycleApplier.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/IncrementalLifecycleApplier.kt @@ -7,11 +7,14 @@ import androidx.lifecycle.LifecycleEventObserver import com.google.android.gms.maps.MapView /** Invokes lifecycle events on the [lifecycleApplier] based on the current [lifecycle]. */ -internal class IncrementalLifecycleApplier( +internal open class IncrementalLifecycleApplier( private val lifecycle: Lifecycle, private val lifecycleApplier: LifecycleApplier ) { - private var currentLifecycleState = Lifecycle.State.INITIALIZED + /** The [Lifecycle.State] which was last applied to [lifecycleApplier] */ + protected var appliedLifecycleState = Lifecycle.State.INITIALIZED + /** The [Lifecycle.Event] which was last received by [lifecycleEventObserver] */ + protected var lastObservedLifecycleEvent: Lifecycle.Event? = null private lateinit var lifecycleEventObserver: LifecycleEventObserver init { @@ -19,17 +22,20 @@ internal class IncrementalLifecycleApplier( } private fun observeLifecycleEvents() { - lifecycleEventObserver = LifecycleEventObserver { _, event -> onLifecycleEvent(event) } + lifecycleEventObserver = LifecycleEventObserver { _, event -> + lastObservedLifecycleEvent = event + onLifecycleEvent(event) + } lifecycle.addObserver(lifecycleEventObserver) } - private fun onLifecycleEvent(lifecycleEvent: Lifecycle.Event) { + protected fun onLifecycleEvent(lifecycleEvent: Lifecycle.Event) { val targetState = lifecycleEvent.targetState - if (targetState == currentLifecycleState) return + if (targetState == appliedLifecycleState) return val isValidTargetLifecycleEvent = isValidTargetLifecycleEvent( - sourceLifecycleState = currentLifecycleState, + sourceLifecycleState = appliedLifecycleState, targetLifecycleEvent = lifecycleEvent ) if (!isValidTargetLifecycleEvent) return @@ -38,17 +44,17 @@ internal class IncrementalLifecycleApplier( } private fun moveToLifecycleEvent(targetLifecycleEvent: Lifecycle.Event) { - val direction = getLifecycleDirection(currentLifecycleState, targetLifecycleEvent) + val direction = getLifecycleDirection(appliedLifecycleState, targetLifecycleEvent.targetState) do { - val nextEvent = getNextLifecycleEvent(currentLifecycleState, direction)!! + val nextEvent = getNextLifecycleEvent(appliedLifecycleState, direction)!! invokeLifecycleEvent(nextEvent) } while (nextEvent != targetLifecycleEvent) } private fun invokeLifecycleEvent(lifecycleEvent: Lifecycle.Event) { lifecycleApplier.invokeEvent(lifecycleEvent) - currentLifecycleState = lifecycleEvent.targetState + appliedLifecycleState = lifecycleEvent.targetState } fun destroyAndDispose() { @@ -57,14 +63,14 @@ internal class IncrementalLifecycleApplier( lifecycle.removeObserver(lifecycleEventObserver) } - private companion object { + protected companion object { /** @return if there is a valid path from [sourceLifecycleState] to [targetLifecycleEvent]. */ - private fun isValidTargetLifecycleEvent( + fun isValidTargetLifecycleEvent( sourceLifecycleState: Lifecycle.State, targetLifecycleEvent: Lifecycle.Event ): Boolean { - val direction = getLifecycleDirection(sourceLifecycleState, targetLifecycleEvent) + val direction = getLifecycleDirection(sourceLifecycleState, targetLifecycleEvent.targetState) var nextLifecycleEvent: Lifecycle.Event? = getNextLifecycleEvent(sourceLifecycleState, direction) while (true) { @@ -76,9 +82,9 @@ internal class IncrementalLifecycleApplier( fun getLifecycleDirection( sourceLifecycleState: Lifecycle.State, - targetLifecycleEvent: Lifecycle.Event + targetLifecycleState: Lifecycle.State ) = when { - targetLifecycleEvent.targetState.isAtLeast(sourceLifecycleState) -> LifecycleEventDirection.UP + targetLifecycleState.isAtLeast(sourceLifecycleState) -> LifecycleEventDirection.UP else -> LifecycleEventDirection.DOWN } @@ -87,15 +93,59 @@ internal class IncrementalLifecycleApplier( LifecycleEventDirection.DOWN -> Lifecycle.Event.downFrom(from) } } + + protected enum class LifecycleEventDirection { UP, DOWN } } -private enum class LifecycleEventDirection { UP, DOWN } +/** Controls a MapView's lifecycle + allows override of actual [lifecycle]'s value */ +internal class IncrementalMapLifecycleApplier( + lifecycle: Lifecycle, + mapView: MapView +) : IncrementalLifecycleApplier( + lifecycle = lifecycle, + lifecycleApplier = MapViewLifecycleApplier(mapView) +) { + /** Currently overwritten lifecycle state */ + private var temporaryLifecycleState: Lifecycle.State? = null + + fun setTemporaryLifecycleState(targetLifecycleState: Lifecycle.State) { + if(targetLifecycleState in listOf(Lifecycle.State.DESTROYED, Lifecycle.State.INITIALIZED)) + error("Invalid temporary lifecycle state: $targetLifecycleState") + + val targetLifecycleEvent = getTargetLifecycleEventForState( + appliedLifecycleState, targetLifecycleState + ) ?: return + + temporaryLifecycleState = targetLifecycleState + onLifecycleEvent(targetLifecycleEvent) + } + + fun clearTemporaryLifecycleState() { + temporaryLifecycleState ?: return + lastObservedLifecycleEvent?.let { onLifecycleEvent(it) } + temporaryLifecycleState = null + } + + private companion object { + fun getTargetLifecycleEventForState( + currentLifecycleState: Lifecycle.State, + targetLifecycleState: Lifecycle.State + ): Lifecycle.Event? { + val direction = getLifecycleDirection(currentLifecycleState, targetLifecycleState) + + return when(direction) { + LifecycleEventDirection.UP -> Lifecycle.Event.upTo(targetLifecycleState) + LifecycleEventDirection.DOWN -> Lifecycle.Event.downTo(targetLifecycleState) + } + } + } +} internal interface LifecycleApplier { fun invokeEvent(event: Lifecycle.Event) } -internal class MapViewLifecycleApplier(private val mapView: MapView) : LifecycleApplier { +private class MapViewLifecycleApplier(private val mapView: MapView) : LifecycleApplier { override fun invokeEvent(event: Lifecycle.Event) { when (event) { Lifecycle.Event.ON_CREATE -> mapView.onCreate(Bundle()) From 7ee881c00bdaa4de3850ba9f93248d97455d43d0 Mon Sep 17 00:00:00 2001 From: Philip S Date: Fri, 19 Apr 2024 09:34:16 +0200 Subject: [PATCH 052/103] Remove LifecycleApplier stuff from GoogleMap --- .../google/maps/android/compose/GoogleMap.kt | 21 ++----------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 54df9e8f..392688c7 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -132,7 +132,6 @@ public fun GoogleMap( val lifecycleOwner = LocalLifecycleOwner.current val context = LocalContext.current - var mapLifecycleApplier: IncrementalMapLifecycleApplier? by remember { mutableStateOf(null) } // Debug stuff val debugCompositionId = remember { compositionCounter++ } @@ -190,22 +189,14 @@ public fun GoogleMap( debugIsMapReused = false MapView(context, googleMapOptionsFactory()).also { mapView -> mapView.registerAndSaveNewComponentCallbacks(context) - - mapLifecycleApplier = IncrementalMapLifecycleApplier(lifecycleOwner.lifecycle, mapView) - - // This mapLifecycleApplier also has to be active while the MapView is detached from - // the UI. Therefore we store it i the MapView's tag so that it can be retrieved in the future. - mapView.tagData().lifecycleApplier = mapLifecycleApplier } }, onReset = { mapView -> // View is detached. - mapView.tagData().lifecycleApplier!!.setTemporaryLifecycleState(Lifecycle.State.CREATED) }, onRelease = { mapView -> mapView.log("onRelease") mapView.tagData().let { tagData -> - tagData.lifecycleApplier!!.destroyAndDispose() tagData.componentCallbacks?.let { componentCallbacks -> tagData.mapViewContext?.unregisterComponentCallbacks(componentCallbacks) } @@ -213,17 +204,10 @@ public fun GoogleMap( mapView.tag = null }, update = { mapView -> - if (mapLifecycleApplier == null) { - mapLifecycleApplier = mapView.tagData().lifecycleApplier!! - } - // Create Composition if (!isCompositionSet) { isCompositionSet = true - mapView.tagData().let { tagData -> - tagData.lifecycleApplier!!.clearTemporaryLifecycleState() - debugMapId = tagData.debugId - } + debugMapId = mapView.tagData().debugId mapUpdaterScope.launchComposition(mapView) } } @@ -254,7 +238,6 @@ internal data class MapTagData( var componentCallbacks: ComponentCallbacks?, var mapViewContext: Context?, var lifecycleState: Lifecycle.State?, - var lifecycleApplier: IncrementalMapLifecycleApplier?, val debugId: Int = nextId ) { companion object { @@ -264,7 +247,7 @@ internal data class MapTagData( } internal fun MapView.tagData(): MapTagData = tag as? MapTagData ?: run { - MapTagData(null, null, null, null).also { newTag -> + MapTagData(null, null, null).also { newTag -> tag = newTag } } From 133e7517a0e93676122b4c12490771effc7711c2 Mon Sep 17 00:00:00 2001 From: Philip S Date: Fri, 19 Apr 2024 17:09:17 +0200 Subject: [PATCH 053/103] Add show/hide button on MapsInLazyColumnActivity --- .../android/compose/MapsInLazyColumnActivity.kt | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt b/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt index 07a9f19c..a8d522c2 100644 --- a/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt @@ -7,14 +7,17 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material.Button import androidx.compose.material.Card import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Text @@ -41,7 +44,18 @@ class MapsInLazyColumnActivity : ComponentActivity() { super.onCreate(savedInstanceState) setContent { - MapsInLazyColumn() + var showMaps by remember { mutableStateOf(true) } + + Column { + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { + Button(onClick = { showMaps = !showMaps }) { + Text(text = if(showMaps) "Hide maps" else "Show maps") + } + } + if(showMaps) { + MapsInLazyColumn() + } + } } } } From b253bafbc2857aaf71041e404899001dba9c0f7c Mon Sep 17 00:00:00 2001 From: Philip S Date: Fri, 19 Apr 2024 17:10:57 +0200 Subject: [PATCH 054/103] Use LifecycleRegistry solution instead of custom solution --- .../google/maps/android/compose/GoogleMap.kt | 80 ++++++++++++++++++- 1 file changed, 77 insertions(+), 3 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 392688c7..73079b03 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -12,12 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. +@file:Suppress("RemoveRedundantQualifierName") + package com.google.maps.android.compose import android.content.ComponentCallbacks import android.content.Context import android.content.res.Configuration import android.location.Location +import android.os.Bundle import android.util.Log import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -43,6 +46,9 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry import com.google.android.gms.maps.GoogleMapOptions import com.google.android.gms.maps.LocationSource import com.google.android.gms.maps.MapView @@ -130,7 +136,6 @@ public fun GoogleMap( val parentComposition = rememberCompositionContext() val currentContent by rememberUpdatedState(content) - val lifecycleOwner = LocalLifecycleOwner.current val context = LocalContext.current // Debug stuff @@ -179,28 +184,52 @@ public fun GoogleMap( } } + val lifecycleOwner = LocalLifecycleOwner.current var isCompositionSet by remember { mutableStateOf(false) } val mapUpdaterScope = rememberCoroutineScope() AndroidView( modifier = modifier, factory = { - Log.d(TAG, "Factory") debugIsMapReused = false MapView(context, googleMapOptionsFactory()).also { mapView -> + mapView.log("Creating MapView") mapView.registerAndSaveNewComponentCallbacks(context) + + // Used to observe lifecycle owner's lifecycle state and + // to gradually move between states. + mapView.tagData().lifecycleRegistry = DerivedLifecycleRegistry(lifecycleOwner) { _, event -> + mapView.log("Invoking $event") + when (event) { + Lifecycle.Event.ON_CREATE -> mapView.onCreate(Bundle()) + Lifecycle.Event.ON_START -> mapView.onStart() + Lifecycle.Event.ON_RESUME -> mapView.onResume() + Lifecycle.Event.ON_PAUSE -> mapView.onPause() + Lifecycle.Event.ON_STOP -> mapView.onStop() + Lifecycle.Event.ON_DESTROY -> { + mapView.onDestroy() + mapView.tagData().lifecycleRegistry!!.destroy() + } + else -> error("Unsupported Lifecycle.Event: $event") + } + } } }, onReset = { mapView -> // View is detached. + mapView.tagData().lifecycleRegistry!!.overrideLifecycleState(Lifecycle.State.CREATED) }, onRelease = { mapView -> mapView.log("onRelease") + mapView.tagData().let { tagData -> tagData.componentCallbacks?.let { componentCallbacks -> tagData.mapViewContext?.unregisterComponentCallbacks(componentCallbacks) } + + tagData.lifecycleRegistry!!.destroy() } + mapView.tag = null }, update = { mapView -> @@ -210,6 +239,8 @@ public fun GoogleMap( debugMapId = mapView.tagData().debugId mapUpdaterScope.launchComposition(mapView) } + + mapView.tagData().lifecycleRegistry!!.clearOverwrittenLifecycleState() } ) @@ -226,6 +257,48 @@ public fun GoogleMap( } } +// TODO is there a better name? +/** + * LifecycleRegistry which observes [lifecycleOwner] and dispatches incoming events. + * Also supports overriding current state. + * */ +internal class DerivedLifecycleRegistry( + private val lifecycleOwner: LifecycleOwner, + private val lifecycleEventObserver: LifecycleEventObserver +): LifecycleRegistry(lifecycleOwner) { + private var lifecycleOwnerState = Lifecycle.State.INITIALIZED + private var overwrittenLifecycleState: Lifecycle.State? = null + + private val lifecycleObserver = LifecycleEventObserver { _, event -> + if(overwrittenLifecycleState == null) { + handleLifecycleEvent(event) + } + lifecycleOwnerState = event.targetState + } + + init { + addObserver(lifecycleEventObserver) + lifecycleOwner.lifecycle.addObserver(lifecycleObserver) + } + + fun overrideLifecycleState(state: Lifecycle.State) { + overwrittenLifecycleState = state + currentState = state + } + + fun clearOverwrittenLifecycleState() { + overwrittenLifecycleState ?: return + overwrittenLifecycleState = null + currentState = lifecycleOwnerState + } + + fun destroy() { + lifecycleOwner.lifecycle.removeObserver(lifecycleObserver) + currentState = Lifecycle.State.DESTROYED + removeObserver(lifecycleEventObserver) + } +} + private fun MapView.registerAndSaveNewComponentCallbacks(context: Context) { val newComponentCallbacks = this.componentCallbacks() val tagData = tagData() @@ -237,7 +310,7 @@ private fun MapView.registerAndSaveNewComponentCallbacks(context: Context) { internal data class MapTagData( var componentCallbacks: ComponentCallbacks?, var mapViewContext: Context?, - var lifecycleState: Lifecycle.State?, + var lifecycleRegistry: DerivedLifecycleRegistry?, val debugId: Int = nextId ) { companion object { @@ -246,6 +319,7 @@ internal data class MapTagData( } } +// TODO make private internal fun MapView.tagData(): MapTagData = tag as? MapTagData ?: run { MapTagData(null, null, null).also { newTag -> tag = newTag From e6210c1ba5253a0b05b6bb9b4357a7064ddf7824 Mon Sep 17 00:00:00 2001 From: Philip S Date: Fri, 19 Apr 2024 17:11:04 +0200 Subject: [PATCH 055/103] Delete IncrementalLifecycleApplier.kt --- .../compose/IncrementalLifecycleApplier.kt | 164 ------------------ 1 file changed, 164 deletions(-) delete mode 100644 maps-compose/src/main/java/com/google/maps/android/compose/IncrementalLifecycleApplier.kt diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/IncrementalLifecycleApplier.kt b/maps-compose/src/main/java/com/google/maps/android/compose/IncrementalLifecycleApplier.kt deleted file mode 100644 index ecee5c51..00000000 --- a/maps-compose/src/main/java/com/google/maps/android/compose/IncrementalLifecycleApplier.kt +++ /dev/null @@ -1,164 +0,0 @@ -package com.google.maps.android.compose - -import android.os.Bundle -import android.util.Log -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver -import com.google.android.gms.maps.MapView - -/** Invokes lifecycle events on the [lifecycleApplier] based on the current [lifecycle]. */ -internal open class IncrementalLifecycleApplier( - private val lifecycle: Lifecycle, - private val lifecycleApplier: LifecycleApplier -) { - /** The [Lifecycle.State] which was last applied to [lifecycleApplier] */ - protected var appliedLifecycleState = Lifecycle.State.INITIALIZED - /** The [Lifecycle.Event] which was last received by [lifecycleEventObserver] */ - protected var lastObservedLifecycleEvent: Lifecycle.Event? = null - private lateinit var lifecycleEventObserver: LifecycleEventObserver - - init { - observeLifecycleEvents() - } - - private fun observeLifecycleEvents() { - lifecycleEventObserver = LifecycleEventObserver { _, event -> - lastObservedLifecycleEvent = event - onLifecycleEvent(event) - } - lifecycle.addObserver(lifecycleEventObserver) - } - - protected fun onLifecycleEvent(lifecycleEvent: Lifecycle.Event) { - val targetState = lifecycleEvent.targetState - - if (targetState == appliedLifecycleState) return - - val isValidTargetLifecycleEvent = isValidTargetLifecycleEvent( - sourceLifecycleState = appliedLifecycleState, - targetLifecycleEvent = lifecycleEvent - ) - if (!isValidTargetLifecycleEvent) return - - moveToLifecycleEvent(lifecycleEvent) - } - - private fun moveToLifecycleEvent(targetLifecycleEvent: Lifecycle.Event) { - val direction = getLifecycleDirection(appliedLifecycleState, targetLifecycleEvent.targetState) - - do { - val nextEvent = getNextLifecycleEvent(appliedLifecycleState, direction)!! - invokeLifecycleEvent(nextEvent) - } while (nextEvent != targetLifecycleEvent) - } - - private fun invokeLifecycleEvent(lifecycleEvent: Lifecycle.Event) { - lifecycleApplier.invokeEvent(lifecycleEvent) - appliedLifecycleState = lifecycleEvent.targetState - } - - fun destroyAndDispose() { - Log.d(TAG, "destroyAndDispose()") - onLifecycleEvent(Lifecycle.Event.ON_DESTROY) - lifecycle.removeObserver(lifecycleEventObserver) - } - - protected companion object { - - /** @return if there is a valid path from [sourceLifecycleState] to [targetLifecycleEvent]. */ - fun isValidTargetLifecycleEvent( - sourceLifecycleState: Lifecycle.State, - targetLifecycleEvent: Lifecycle.Event - ): Boolean { - val direction = getLifecycleDirection(sourceLifecycleState, targetLifecycleEvent.targetState) - var nextLifecycleEvent: Lifecycle.Event? = getNextLifecycleEvent(sourceLifecycleState, direction) - - while (true) { - if (nextLifecycleEvent == null) return false - if (nextLifecycleEvent == targetLifecycleEvent) return true - nextLifecycleEvent = getNextLifecycleEvent(nextLifecycleEvent.targetState, direction) - } - } - - fun getLifecycleDirection( - sourceLifecycleState: Lifecycle.State, - targetLifecycleState: Lifecycle.State - ) = when { - targetLifecycleState.isAtLeast(sourceLifecycleState) -> LifecycleEventDirection.UP - else -> LifecycleEventDirection.DOWN - } - - fun getNextLifecycleEvent(from: Lifecycle.State, direction: LifecycleEventDirection) = when (direction) { - LifecycleEventDirection.UP -> Lifecycle.Event.upFrom(from) - LifecycleEventDirection.DOWN -> Lifecycle.Event.downFrom(from) - } - } - - protected enum class LifecycleEventDirection { UP, DOWN } -} - -/** Controls a MapView's lifecycle + allows override of actual [lifecycle]'s value */ -internal class IncrementalMapLifecycleApplier( - lifecycle: Lifecycle, - mapView: MapView -) : IncrementalLifecycleApplier( - lifecycle = lifecycle, - lifecycleApplier = MapViewLifecycleApplier(mapView) -) { - /** Currently overwritten lifecycle state */ - private var temporaryLifecycleState: Lifecycle.State? = null - - fun setTemporaryLifecycleState(targetLifecycleState: Lifecycle.State) { - if(targetLifecycleState in listOf(Lifecycle.State.DESTROYED, Lifecycle.State.INITIALIZED)) - error("Invalid temporary lifecycle state: $targetLifecycleState") - - val targetLifecycleEvent = getTargetLifecycleEventForState( - appliedLifecycleState, targetLifecycleState - ) ?: return - - temporaryLifecycleState = targetLifecycleState - onLifecycleEvent(targetLifecycleEvent) - } - - fun clearTemporaryLifecycleState() { - temporaryLifecycleState ?: return - lastObservedLifecycleEvent?.let { onLifecycleEvent(it) } - temporaryLifecycleState = null - } - - private companion object { - fun getTargetLifecycleEventForState( - currentLifecycleState: Lifecycle.State, - targetLifecycleState: Lifecycle.State - ): Lifecycle.Event? { - val direction = getLifecycleDirection(currentLifecycleState, targetLifecycleState) - - return when(direction) { - LifecycleEventDirection.UP -> Lifecycle.Event.upTo(targetLifecycleState) - LifecycleEventDirection.DOWN -> Lifecycle.Event.downTo(targetLifecycleState) - } - } - } -} - -internal interface LifecycleApplier { - fun invokeEvent(event: Lifecycle.Event) -} - -private class MapViewLifecycleApplier(private val mapView: MapView) : LifecycleApplier { - override fun invokeEvent(event: Lifecycle.Event) { - when (event) { - Lifecycle.Event.ON_CREATE -> mapView.onCreate(Bundle()) - Lifecycle.Event.ON_START -> mapView.onStart() - Lifecycle.Event.ON_RESUME -> mapView.onResume() - Lifecycle.Event.ON_PAUSE -> mapView.onPause() - Lifecycle.Event.ON_STOP -> mapView.onStop() - Lifecycle.Event.ON_DESTROY -> mapView.onDestroy() - Lifecycle.Event.ON_ANY -> error("Cannot invoke lifecycle event ON_ANY on mapView.") - } - - Log.d("MapViewLifecycleApplier", "[MapView#${ mapView.tagData().debugId }]Invoking $event") - - mapView.tagData().lifecycleState = event.targetState - } -} From 955cb4c771453746bb3cd3d297896584e42988a9 Mon Sep 17 00:00:00 2001 From: Philip S Date: Mon, 22 Apr 2024 14:08:53 +0200 Subject: [PATCH 056/103] Update GoogleMap.kt --- .../google/maps/android/compose/GoogleMap.kt | 93 ++++++++++++++----- 1 file changed, 69 insertions(+), 24 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 73079b03..3caaab1f 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -49,6 +49,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry +import androidx.lifecycle.findViewTreeLifecycleOwner import com.google.android.gms.maps.GoogleMapOptions import com.google.android.gms.maps.LocationSource import com.google.android.gms.maps.MapView @@ -198,24 +199,11 @@ public fun GoogleMap( // Used to observe lifecycle owner's lifecycle state and // to gradually move between states. - mapView.tagData().lifecycleRegistry = DerivedLifecycleRegistry(lifecycleOwner) { _, event -> - mapView.log("Invoking $event") - when (event) { - Lifecycle.Event.ON_CREATE -> mapView.onCreate(Bundle()) - Lifecycle.Event.ON_START -> mapView.onStart() - Lifecycle.Event.ON_RESUME -> mapView.onResume() - Lifecycle.Event.ON_PAUSE -> mapView.onPause() - Lifecycle.Event.ON_STOP -> mapView.onStop() - Lifecycle.Event.ON_DESTROY -> { - mapView.onDestroy() - mapView.tagData().lifecycleRegistry!!.destroy() - } - else -> error("Unsupported Lifecycle.Event: $event") - } - } + mapView.tagData().lifecycleRegistry = MapViewDerivedLifecycleRegistry(lifecycleOwner, mapView) } }, onReset = { mapView -> + mapView.log("ON RESET!") // View is detached. mapView.tagData().lifecycleRegistry!!.overrideLifecycleState(Lifecycle.State.CREATED) }, @@ -240,7 +228,10 @@ public fun GoogleMap( mapUpdaterScope.launchComposition(mapView) } - mapView.tagData().lifecycleRegistry!!.clearOverwrittenLifecycleState() + mapView.tagData().lifecycleRegistry!!.run { + tryUpdateLifecycleOwner(mapView.findViewTreeLifecycleOwner()!!) + clearOverwrittenLifecycleState() + } } ) @@ -262,23 +253,54 @@ public fun GoogleMap( * LifecycleRegistry which observes [lifecycleOwner] and dispatches incoming events. * Also supports overriding current state. * */ -internal class DerivedLifecycleRegistry( - private val lifecycleOwner: LifecycleOwner, - private val lifecycleEventObserver: LifecycleEventObserver +internal abstract class DerivedLifecycleRegistry( + lifecycleOwner: LifecycleOwner ): LifecycleRegistry(lifecycleOwner) { private var lifecycleOwnerState = Lifecycle.State.INITIALIZED private var overwrittenLifecycleState: Lifecycle.State? = null - private val lifecycleObserver = LifecycleEventObserver { _, event -> - if(overwrittenLifecycleState == null) { + private var currentLifecycleOwner = lifecycleOwner + + private val lifecycleOwnerObserver = LifecycleEventObserver { _, event -> + if (event == Event.ON_DESTROY) { + // Parent lifecycle reached DESTROYED state. Unregister LifecycleObserver. + removeCurrentLifecycleOwnerObserver() + } + if (overwrittenLifecycleState == null) { handleLifecycleEvent(event) } lifecycleOwnerState = event.targetState } - init { + private val lifecycleEventObserver = object : LifecycleEventObserver { + override fun onStateChanged(source: LifecycleOwner, event: Event) { + if (event == Event.ON_DESTROY) { + // This listener has received Event.ON_DESTROY. Won't receive more lifecycle events. + this@DerivedLifecycleRegistry.removeObserver(this) + } + onLifecycleEvent(event) + } + } + + private fun removeCurrentLifecycleOwnerObserver() { + currentLifecycleOwner.lifecycle.removeObserver(lifecycleOwnerObserver) + } + + @Synchronized + fun tryUpdateLifecycleOwner(lifecycleOwner: LifecycleOwner) { + if(lifecycleOwner == currentLifecycleOwner) return + + // There is a new LifecycleOwner. Let's change it. + currentLifecycleOwner.lifecycle.removeObserver(lifecycleOwnerObserver) + currentLifecycleOwner = lifecycleOwner + lifecycleOwner.lifecycle.addObserver(lifecycleOwnerObserver) + } + + abstract fun onLifecycleEvent(event: Lifecycle.Event) + + fun initObserver() { addObserver(lifecycleEventObserver) - lifecycleOwner.lifecycle.addObserver(lifecycleObserver) + currentLifecycleOwner.lifecycle.addObserver(lifecycleOwnerObserver) } fun overrideLifecycleState(state: Lifecycle.State) { @@ -293,12 +315,35 @@ internal class DerivedLifecycleRegistry( } fun destroy() { - lifecycleOwner.lifecycle.removeObserver(lifecycleObserver) + currentLifecycleOwner.lifecycle.removeObserver(lifecycleOwnerObserver) currentState = Lifecycle.State.DESTROYED removeObserver(lifecycleEventObserver) } } +internal class MapViewDerivedLifecycleRegistry( + lifecycleOwner: LifecycleOwner, + private val mapView: MapView +) : DerivedLifecycleRegistry(lifecycleOwner) { + + init { + super.initObserver() + } + + override fun onLifecycleEvent(event: Lifecycle.Event) { + mapView.log("Invoking $event") + when (event) { + Lifecycle.Event.ON_CREATE -> mapView.onCreate(Bundle()) + Lifecycle.Event.ON_START -> mapView.onStart() + Lifecycle.Event.ON_RESUME -> mapView.onResume() + Lifecycle.Event.ON_PAUSE -> mapView.onPause() + Lifecycle.Event.ON_STOP -> mapView.onStop() + Lifecycle.Event.ON_DESTROY -> mapView.onDestroy() + else -> error("Unsupported Lifecycle.Event: $event") + } + } +} + private fun MapView.registerAndSaveNewComponentCallbacks(context: Context) { val newComponentCallbacks = this.componentCallbacks() val tagData = tagData() From 79f6f9358b1a071199650101deeb8b7f855cf647 Mon Sep 17 00:00:00 2001 From: Philip S Date: Mon, 22 Apr 2024 15:07:51 +0200 Subject: [PATCH 057/103] Move between lifecycle states using custom approach inspired by LifecycleRegistry#sync() --- .../google/maps/android/compose/GoogleMap.kt | 195 ++++++++---------- 1 file changed, 82 insertions(+), 113 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 3caaab1f..bd18ea49 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -22,6 +22,7 @@ import android.content.res.Configuration import android.location.Location import android.os.Bundle import android.util.Log +import android.view.View import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -42,7 +43,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.Lifecycle @@ -61,6 +61,7 @@ import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Job import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.launch +import kotlin.properties.Delegates internal const val TAG = "GoogleMap" @@ -185,7 +186,6 @@ public fun GoogleMap( } } - val lifecycleOwner = LocalLifecycleOwner.current var isCompositionSet by remember { mutableStateOf(false) } val mapUpdaterScope = rememberCoroutineScope() @@ -197,16 +197,86 @@ public fun GoogleMap( mapView.log("Creating MapView") mapView.registerAndSaveNewComponentCallbacks(context) - // Used to observe lifecycle owner's lifecycle state and - // to gradually move between states. - mapView.tagData().lifecycleRegistry = MapViewDerivedLifecycleRegistry(lifecycleOwner, mapView) + fun log(msg: String) = mapView.log(msg) + + val lifecycleObserver = object : LifecycleEventObserver { + private var currentLifecycleState: Lifecycle.State = Lifecycle.State.INITIALIZED + + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + if(event == Lifecycle.Event.ON_DESTROY) { + // Only destroy from onRelease. Move down to CREATED state. + moveToLifecycleState(Lifecycle.State.CREATED) + return + } + moveToLifecycleState(event.targetState) + } + + @Synchronized + fun moveToLifecycleState(targetState: Lifecycle.State) { + while(currentLifecycleState != targetState) { + if(targetState < currentLifecycleState) moveBackward() + else moveForward() + } + } + + private fun moveBackward() { + val event = Lifecycle.Event.downFrom(currentLifecycleState) + ?: error("no event down from $currentLifecycleState") + invokeEvent(event) + } + + private fun moveForward() { + val event = Lifecycle.Event.upFrom(currentLifecycleState) + ?: error("no event up from $currentLifecycleState") + invokeEvent(event) + } + + private fun invokeEvent(event: Lifecycle.Event) { + log("Invoking Lifecycle event $event") + when(event) { + Lifecycle.Event.ON_CREATE -> mapView.onCreate(Bundle()) + Lifecycle.Event.ON_START -> mapView.onStart() + Lifecycle.Event.ON_RESUME -> mapView.onResume() + Lifecycle.Event.ON_PAUSE -> mapView.onPause() + Lifecycle.Event.ON_STOP -> mapView.onStop() + Lifecycle.Event.ON_DESTROY -> mapView.onDestroy() + else -> error("Unsupported lifecycle event: $event") + } + currentLifecycleState = event.targetState + } + } + + var lifecycleOwner by Delegates.notNull() + + fun unregisterLifecycleObserver() { + log("Unregistering lifecycle observer") + lifecycleOwner.lifecycle.removeObserver(lifecycleObserver) + } + + val attachStateListener = object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) { + log("View attached!") + lifecycleOwner = v.findViewTreeLifecycleOwner()!! + lifecycleOwner.lifecycle.addObserver(lifecycleObserver) + } + + override fun onViewDetachedFromWindow(v: View) { + log("View detached!") + unregisterLifecycleObserver() + lifecycleObserver.moveToLifecycleState(Lifecycle.State.CREATED) + } + } + + mapView.addOnAttachStateChangeListener(attachStateListener) + + mapView.tagData().onRelease = { + unregisterLifecycleObserver() + mapView.removeOnAttachStateChangeListener(attachStateListener) + lifecycleObserver.moveToLifecycleState(Lifecycle.State.DESTROYED) + } } }, - onReset = { mapView -> - mapView.log("ON RESET!") - // View is detached. - mapView.tagData().lifecycleRegistry!!.overrideLifecycleState(Lifecycle.State.CREATED) - }, + onReset = { /* View is detached. */ }, onRelease = { mapView -> mapView.log("onRelease") @@ -215,7 +285,7 @@ public fun GoogleMap( tagData.mapViewContext?.unregisterComponentCallbacks(componentCallbacks) } - tagData.lifecycleRegistry!!.destroy() + tagData.onRelease!!.invoke() } mapView.tag = null @@ -227,11 +297,6 @@ public fun GoogleMap( debugMapId = mapView.tagData().debugId mapUpdaterScope.launchComposition(mapView) } - - mapView.tagData().lifecycleRegistry!!.run { - tryUpdateLifecycleOwner(mapView.findViewTreeLifecycleOwner()!!) - clearOverwrittenLifecycleState() - } } ) @@ -248,102 +313,6 @@ public fun GoogleMap( } } -// TODO is there a better name? -/** - * LifecycleRegistry which observes [lifecycleOwner] and dispatches incoming events. - * Also supports overriding current state. - * */ -internal abstract class DerivedLifecycleRegistry( - lifecycleOwner: LifecycleOwner -): LifecycleRegistry(lifecycleOwner) { - private var lifecycleOwnerState = Lifecycle.State.INITIALIZED - private var overwrittenLifecycleState: Lifecycle.State? = null - - private var currentLifecycleOwner = lifecycleOwner - - private val lifecycleOwnerObserver = LifecycleEventObserver { _, event -> - if (event == Event.ON_DESTROY) { - // Parent lifecycle reached DESTROYED state. Unregister LifecycleObserver. - removeCurrentLifecycleOwnerObserver() - } - if (overwrittenLifecycleState == null) { - handleLifecycleEvent(event) - } - lifecycleOwnerState = event.targetState - } - - private val lifecycleEventObserver = object : LifecycleEventObserver { - override fun onStateChanged(source: LifecycleOwner, event: Event) { - if (event == Event.ON_DESTROY) { - // This listener has received Event.ON_DESTROY. Won't receive more lifecycle events. - this@DerivedLifecycleRegistry.removeObserver(this) - } - onLifecycleEvent(event) - } - } - - private fun removeCurrentLifecycleOwnerObserver() { - currentLifecycleOwner.lifecycle.removeObserver(lifecycleOwnerObserver) - } - - @Synchronized - fun tryUpdateLifecycleOwner(lifecycleOwner: LifecycleOwner) { - if(lifecycleOwner == currentLifecycleOwner) return - - // There is a new LifecycleOwner. Let's change it. - currentLifecycleOwner.lifecycle.removeObserver(lifecycleOwnerObserver) - currentLifecycleOwner = lifecycleOwner - lifecycleOwner.lifecycle.addObserver(lifecycleOwnerObserver) - } - - abstract fun onLifecycleEvent(event: Lifecycle.Event) - - fun initObserver() { - addObserver(lifecycleEventObserver) - currentLifecycleOwner.lifecycle.addObserver(lifecycleOwnerObserver) - } - - fun overrideLifecycleState(state: Lifecycle.State) { - overwrittenLifecycleState = state - currentState = state - } - - fun clearOverwrittenLifecycleState() { - overwrittenLifecycleState ?: return - overwrittenLifecycleState = null - currentState = lifecycleOwnerState - } - - fun destroy() { - currentLifecycleOwner.lifecycle.removeObserver(lifecycleOwnerObserver) - currentState = Lifecycle.State.DESTROYED - removeObserver(lifecycleEventObserver) - } -} - -internal class MapViewDerivedLifecycleRegistry( - lifecycleOwner: LifecycleOwner, - private val mapView: MapView -) : DerivedLifecycleRegistry(lifecycleOwner) { - - init { - super.initObserver() - } - - override fun onLifecycleEvent(event: Lifecycle.Event) { - mapView.log("Invoking $event") - when (event) { - Lifecycle.Event.ON_CREATE -> mapView.onCreate(Bundle()) - Lifecycle.Event.ON_START -> mapView.onStart() - Lifecycle.Event.ON_RESUME -> mapView.onResume() - Lifecycle.Event.ON_PAUSE -> mapView.onPause() - Lifecycle.Event.ON_STOP -> mapView.onStop() - Lifecycle.Event.ON_DESTROY -> mapView.onDestroy() - else -> error("Unsupported Lifecycle.Event: $event") - } - } -} - private fun MapView.registerAndSaveNewComponentCallbacks(context: Context) { val newComponentCallbacks = this.componentCallbacks() val tagData = tagData() @@ -355,7 +324,7 @@ private fun MapView.registerAndSaveNewComponentCallbacks(context: Context) { internal data class MapTagData( var componentCallbacks: ComponentCallbacks?, var mapViewContext: Context?, - var lifecycleRegistry: DerivedLifecycleRegistry?, + var onRelease: (() -> Unit)?, val debugId: Int = nextId ) { companion object { From 93f709a3d3fea47890cafe81db4609dc37e9cadd Mon Sep 17 00:00:00 2001 From: Philip S Date: Tue, 23 Apr 2024 09:29:18 +0200 Subject: [PATCH 058/103] Move LifecycleEventObserver into its own class --- .../google/maps/android/compose/GoogleMap.kt | 94 +++++++++---------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index bd18ea49..1de4eca3 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -48,7 +48,6 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.findViewTreeLifecycleOwner import com.google.android.gms.maps.GoogleMapOptions import com.google.android.gms.maps.LocationSource @@ -199,52 +198,7 @@ public fun GoogleMap( fun log(msg: String) = mapView.log(msg) - val lifecycleObserver = object : LifecycleEventObserver { - private var currentLifecycleState: Lifecycle.State = Lifecycle.State.INITIALIZED - - override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { - if(event == Lifecycle.Event.ON_DESTROY) { - // Only destroy from onRelease. Move down to CREATED state. - moveToLifecycleState(Lifecycle.State.CREATED) - return - } - moveToLifecycleState(event.targetState) - } - - @Synchronized - fun moveToLifecycleState(targetState: Lifecycle.State) { - while(currentLifecycleState != targetState) { - if(targetState < currentLifecycleState) moveBackward() - else moveForward() - } - } - - private fun moveBackward() { - val event = Lifecycle.Event.downFrom(currentLifecycleState) - ?: error("no event down from $currentLifecycleState") - invokeEvent(event) - } - - private fun moveForward() { - val event = Lifecycle.Event.upFrom(currentLifecycleState) - ?: error("no event up from $currentLifecycleState") - invokeEvent(event) - } - - private fun invokeEvent(event: Lifecycle.Event) { - log("Invoking Lifecycle event $event") - when(event) { - Lifecycle.Event.ON_CREATE -> mapView.onCreate(Bundle()) - Lifecycle.Event.ON_START -> mapView.onStart() - Lifecycle.Event.ON_RESUME -> mapView.onResume() - Lifecycle.Event.ON_PAUSE -> mapView.onPause() - Lifecycle.Event.ON_STOP -> mapView.onStop() - Lifecycle.Event.ON_DESTROY -> mapView.onDestroy() - else -> error("Unsupported lifecycle event: $event") - } - currentLifecycleState = event.targetState - } - } + val lifecycleObserver = MapLifecycleEventObserver(mapView) var lifecycleOwner by Delegates.notNull() @@ -403,3 +357,49 @@ public fun googleMapFactory( } } +private class MapLifecycleEventObserver(private val mapView: MapView) : LifecycleEventObserver { + private var currentLifecycleState: Lifecycle.State = Lifecycle.State.INITIALIZED + + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + if(event == Lifecycle.Event.ON_DESTROY) { + // Only destroy from onRelease. Move down to CREATED state. + moveToLifecycleState(Lifecycle.State.CREATED) + return + } + moveToLifecycleState(event.targetState) + } + + @Synchronized + fun moveToLifecycleState(targetState: Lifecycle.State) { + while(currentLifecycleState != targetState) { + if(targetState < currentLifecycleState) moveBackward() + else moveForward() + } + } + + private fun moveBackward() { + val event = Lifecycle.Event.downFrom(currentLifecycleState) + ?: error("no event down from $currentLifecycleState") + invokeEvent(event) + } + + private fun moveForward() { + val event = Lifecycle.Event.upFrom(currentLifecycleState) + ?: error("no event up from $currentLifecycleState") + invokeEvent(event) + } + + private fun invokeEvent(event: Lifecycle.Event) { + mapView.log("Invoking Lifecycle event $event") + when(event) { + Lifecycle.Event.ON_CREATE -> mapView.onCreate(Bundle()) + Lifecycle.Event.ON_START -> mapView.onStart() + Lifecycle.Event.ON_RESUME -> mapView.onResume() + Lifecycle.Event.ON_PAUSE -> mapView.onPause() + Lifecycle.Event.ON_STOP -> mapView.onStop() + Lifecycle.Event.ON_DESTROY -> mapView.onDestroy() + else -> error("Unsupported lifecycle event: $event") + } + currentLifecycleState = event.targetState + } +} \ No newline at end of file From c58f1beae840d849c99de614c8fed66e06884a5d Mon Sep 17 00:00:00 2001 From: Philip S Date: Tue, 23 Apr 2024 09:35:50 +0200 Subject: [PATCH 059/103] Rename moveForward/Backward to moveUp/Down --- .../main/java/com/google/maps/android/compose/GoogleMap.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 1de4eca3..473242b4 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -377,13 +377,13 @@ private class MapLifecycleEventObserver(private val mapView: MapView) : Lifecycl } } - private fun moveBackward() { + private fun moveDown() { val event = Lifecycle.Event.downFrom(currentLifecycleState) ?: error("no event down from $currentLifecycleState") invokeEvent(event) } - private fun moveForward() { + private fun moveUp() { val event = Lifecycle.Event.upFrom(currentLifecycleState) ?: error("no event up from $currentLifecycleState") invokeEvent(event) From 863b994b913e3000ce17322e62ad9caf76517d7d Mon Sep 17 00:00:00 2001 From: Philip S Date: Tue, 23 Apr 2024 09:39:12 +0200 Subject: [PATCH 060/103] Make moveToLifecycleState more clear --- .../main/java/com/google/maps/android/compose/GoogleMap.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 473242b4..fe38f199 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -372,8 +372,10 @@ private class MapLifecycleEventObserver(private val mapView: MapView) : Lifecycl @Synchronized fun moveToLifecycleState(targetState: Lifecycle.State) { while(currentLifecycleState != targetState) { - if(targetState < currentLifecycleState) moveBackward() - else moveForward() + when { + currentLifecycleState < targetState -> moveUp() + currentLifecycleState > targetState -> moveDown() + } } } From c70638cd45868f547b04da1e655622b66b69c8e8 Mon Sep 17 00:00:00 2001 From: Philip S Date: Tue, 23 Apr 2024 09:53:25 +0200 Subject: [PATCH 061/103] Make lifecycleOwner nullable and nullify on detach --- .../com/google/maps/android/compose/GoogleMap.kt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index fe38f199..8866c1fe 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -60,7 +60,6 @@ import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Job import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.launch -import kotlin.properties.Delegates internal const val TAG = "GoogleMap" @@ -200,18 +199,20 @@ public fun GoogleMap( val lifecycleObserver = MapLifecycleEventObserver(mapView) - var lifecycleOwner by Delegates.notNull() + var lifecycleOwner: LifecycleOwner? = null fun unregisterLifecycleObserver() { log("Unregistering lifecycle observer") - lifecycleOwner.lifecycle.removeObserver(lifecycleObserver) + lifecycleOwner?.lifecycle?.removeObserver(lifecycleObserver) + lifecycleOwner = null } val attachStateListener = object : View.OnAttachStateChangeListener { - override fun onViewAttachedToWindow(v: View) { + override fun onViewAttachedToWindow(mapView: View) { log("View attached!") - lifecycleOwner = v.findViewTreeLifecycleOwner()!! - lifecycleOwner.lifecycle.addObserver(lifecycleObserver) + lifecycleOwner = mapView.findViewTreeLifecycleOwner()!!.also { + it.lifecycle.addObserver(lifecycleObserver) + } } override fun onViewDetachedFromWindow(v: View) { From f485e2101a34043c9ca664c103d254aed7d63e09 Mon Sep 17 00:00:00 2001 From: Philip S Date: Tue, 23 Apr 2024 09:54:49 +0200 Subject: [PATCH 062/103] Rename attachStateListener to onAttachStateListener for clarity --- .../main/java/com/google/maps/android/compose/GoogleMap.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 8866c1fe..16a2c299 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -207,7 +207,7 @@ public fun GoogleMap( lifecycleOwner = null } - val attachStateListener = object : View.OnAttachStateChangeListener { + val onAttachStateListener = object : View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(mapView: View) { log("View attached!") lifecycleOwner = mapView.findViewTreeLifecycleOwner()!!.also { @@ -222,11 +222,11 @@ public fun GoogleMap( } } - mapView.addOnAttachStateChangeListener(attachStateListener) + mapView.addOnAttachStateChangeListener(onAttachStateListener) mapView.tagData().onRelease = { unregisterLifecycleObserver() - mapView.removeOnAttachStateChangeListener(attachStateListener) + mapView.removeOnAttachStateChangeListener(onAttachStateListener) lifecycleObserver.moveToLifecycleState(Lifecycle.State.DESTROYED) } } From d194481168969346445fcfb8c6b7d24e708bed3c Mon Sep 17 00:00:00 2001 From: Philip S Date: Tue, 23 Apr 2024 10:10:10 +0200 Subject: [PATCH 063/103] Add moveToBaseState method to avoid reaching Lifecycle.State.CREATED prematurely --- .../google/maps/android/compose/GoogleMap.kt | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 16a2c299..ff7a3c73 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -218,7 +218,7 @@ public fun GoogleMap( override fun onViewDetachedFromWindow(v: View) { log("View detached!") unregisterLifecycleObserver() - lifecycleObserver.moveToLifecycleState(Lifecycle.State.CREATED) + lifecycleObserver.moveToBaseState() } } @@ -362,12 +362,21 @@ private class MapLifecycleEventObserver(private val mapView: MapView) : Lifecycl private var currentLifecycleState: Lifecycle.State = Lifecycle.State.INITIALIZED override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { - if(event == Lifecycle.Event.ON_DESTROY) { - // Only destroy from onRelease. Move down to CREATED state. + when (event) { + // [mapView.onDestroy] is only invoked from AndroidView->onRelease. + Lifecycle.Event.ON_DESTROY -> moveToBaseState() + else -> moveToLifecycleState(event.targetState) + } + } + + /** + * Move down to [Lifecycle.State.CREATED] but only if [currentLifecycleState] is actually above that. + * It's theoretically possible that [currentLifecycleState] is still in [Lifecycle.State.INITIALIZED] state. + * */ + fun moveToBaseState() { + if(currentLifecycleState > Lifecycle.State.CREATED) { moveToLifecycleState(Lifecycle.State.CREATED) - return } - moveToLifecycleState(event.targetState) } @Synchronized From 62b866016fd4ccc470d8964ccc5062a98e55c2e9 Mon Sep 17 00:00:00 2001 From: Philip S Date: Tue, 23 Apr 2024 10:32:20 +0200 Subject: [PATCH 064/103] Replace mapView.tag onRelease lambda with lifecycleObserver reference + add moveToDestroyedState method --- .../google/maps/android/compose/GoogleMap.kt | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index ff7a3c73..35c7d4fa 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -197,7 +197,9 @@ public fun GoogleMap( fun log(msg: String) = mapView.log(msg) - val lifecycleObserver = MapLifecycleEventObserver(mapView) + val lifecycleObserver = MapLifecycleEventObserver(mapView).also { + mapView.tagData().lifecycleObserver = it + } var lifecycleOwner: LifecycleOwner? = null @@ -223,12 +225,6 @@ public fun GoogleMap( } mapView.addOnAttachStateChangeListener(onAttachStateListener) - - mapView.tagData().onRelease = { - unregisterLifecycleObserver() - mapView.removeOnAttachStateChangeListener(onAttachStateListener) - lifecycleObserver.moveToLifecycleState(Lifecycle.State.DESTROYED) - } } }, onReset = { /* View is detached. */ }, @@ -240,7 +236,7 @@ public fun GoogleMap( tagData.mapViewContext?.unregisterComponentCallbacks(componentCallbacks) } - tagData.onRelease!!.invoke() + tagData.lifecycleObserver!!.moveToDestroyedState() } mapView.tag = null @@ -279,7 +275,7 @@ private fun MapView.registerAndSaveNewComponentCallbacks(context: Context) { internal data class MapTagData( var componentCallbacks: ComponentCallbacks?, var mapViewContext: Context?, - var onRelease: (() -> Unit)?, + var lifecycleObserver: MapLifecycleEventObserver?, val debugId: Int = nextId ) { companion object { @@ -358,7 +354,8 @@ public fun googleMapFactory( } } -private class MapLifecycleEventObserver(private val mapView: MapView) : LifecycleEventObserver { +// TODO make private +internal class MapLifecycleEventObserver(private val mapView: MapView) : LifecycleEventObserver { private var currentLifecycleState: Lifecycle.State = Lifecycle.State.INITIALIZED override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { @@ -379,8 +376,14 @@ private class MapLifecycleEventObserver(private val mapView: MapView) : Lifecycl } } + fun moveToDestroyedState() { + if(currentLifecycleState > Lifecycle.State.INITIALIZED) { + moveToLifecycleState(Lifecycle.State.DESTROYED) + } + } + @Synchronized - fun moveToLifecycleState(targetState: Lifecycle.State) { + private fun moveToLifecycleState(targetState: Lifecycle.State) { while(currentLifecycleState != targetState) { when { currentLifecycleState < targetState -> moveUp() From 3efa55e14e8f4b7e1140963a7c7a717bd1870436 Mon Sep 17 00:00:00 2001 From: Philip S Date: Tue, 23 Apr 2024 10:35:21 +0200 Subject: [PATCH 065/103] Remove @Synchronized annotation --- .../src/main/java/com/google/maps/android/compose/GoogleMap.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 35c7d4fa..060dfc8e 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -382,7 +382,6 @@ internal class MapLifecycleEventObserver(private val mapView: MapView) : Lifecyc } } - @Synchronized private fun moveToLifecycleState(targetState: Lifecycle.State) { while(currentLifecycleState != targetState) { when { From 5e58385a5bf911f13d14ae9727fa7705360ef0ae Mon Sep 17 00:00:00 2001 From: Philip S Date: Tue, 23 Apr 2024 10:58:46 +0200 Subject: [PATCH 066/103] Make tagData() method a bit more clear --- .../main/java/com/google/maps/android/compose/GoogleMap.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 060dfc8e..d2cd151b 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -285,9 +285,9 @@ internal data class MapTagData( } // TODO make private -internal fun MapView.tagData(): MapTagData = tag as? MapTagData ?: run { +internal fun MapView.tagData(): MapTagData = (tag as? MapTagData) ?: let { mapView -> MapTagData(null, null, null).also { newTag -> - tag = newTag + mapView.tag = newTag } } From 801b9ee62590868d307e15b274b271a6e87d3e30 Mon Sep 17 00:00:00 2001 From: Philip S Date: Wed, 24 Apr 2024 09:35:22 +0200 Subject: [PATCH 067/103] Remove LocalContext.current + mapViewContext --- .../com/google/maps/android/compose/GoogleMap.kt | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index d2cd151b..85a3eb2d 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -17,7 +17,6 @@ package com.google.maps.android.compose import android.content.ComponentCallbacks -import android.content.Context import android.content.res.Configuration import android.location.Location import android.os.Bundle @@ -136,8 +135,6 @@ public fun GoogleMap( val parentComposition = rememberCompositionContext() val currentContent by rememberUpdatedState(content) - val context = LocalContext.current - // Debug stuff val debugCompositionId = remember { compositionCounter++ } var debugMapId: Int? by remember { mutableStateOf(null) } @@ -189,11 +186,11 @@ public fun GoogleMap( AndroidView( modifier = modifier, - factory = { + factory = { context -> debugIsMapReused = false MapView(context, googleMapOptionsFactory()).also { mapView -> mapView.log("Creating MapView") - mapView.registerAndSaveNewComponentCallbacks(context) + mapView.registerAndSaveNewComponentCallbacks() fun log(msg: String) = mapView.log(msg) @@ -233,7 +230,7 @@ public fun GoogleMap( mapView.tagData().let { tagData -> tagData.componentCallbacks?.let { componentCallbacks -> - tagData.mapViewContext?.unregisterComponentCallbacks(componentCallbacks) + mapView.context.unregisterComponentCallbacks(componentCallbacks) } tagData.lifecycleObserver!!.moveToDestroyedState() @@ -264,17 +261,15 @@ public fun GoogleMap( } } -private fun MapView.registerAndSaveNewComponentCallbacks(context: Context) { +private fun MapView.registerAndSaveNewComponentCallbacks() { val newComponentCallbacks = this.componentCallbacks() val tagData = tagData() tagData.componentCallbacks = newComponentCallbacks - tagData.mapViewContext = context context.registerComponentCallbacks(newComponentCallbacks) } internal data class MapTagData( var componentCallbacks: ComponentCallbacks?, - var mapViewContext: Context?, var lifecycleObserver: MapLifecycleEventObserver?, val debugId: Int = nextId ) { @@ -287,6 +282,7 @@ internal data class MapTagData( // TODO make private internal fun MapView.tagData(): MapTagData = (tag as? MapTagData) ?: let { mapView -> MapTagData(null, null, null).also { newTag -> + MapTagData(null, null).also { newTag -> mapView.tag = newTag } } From 2e12e1841a3823710cd2f566247ebc31e40f6903 Mon Sep 17 00:00:00 2001 From: Philip S Date: Wed, 24 Apr 2024 09:35:44 +0200 Subject: [PATCH 068/103] Make MapTagData + MapLifecycleEventObserver private --- .../java/com/google/maps/android/compose/GoogleMap.kt | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 85a3eb2d..b82e0227 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -268,7 +268,7 @@ private fun MapView.registerAndSaveNewComponentCallbacks() { context.registerComponentCallbacks(newComponentCallbacks) } -internal data class MapTagData( +private data class MapTagData( var componentCallbacks: ComponentCallbacks?, var lifecycleObserver: MapLifecycleEventObserver?, val debugId: Int = nextId @@ -279,9 +279,7 @@ internal data class MapTagData( } } -// TODO make private -internal fun MapView.tagData(): MapTagData = (tag as? MapTagData) ?: let { mapView -> - MapTagData(null, null, null).also { newTag -> +private fun MapView.tagData(): MapTagData = (tag as? MapTagData) ?: let { mapView -> MapTagData(null, null).also { newTag -> mapView.tag = newTag } @@ -350,8 +348,7 @@ public fun googleMapFactory( } } -// TODO make private -internal class MapLifecycleEventObserver(private val mapView: MapView) : LifecycleEventObserver { +private class MapLifecycleEventObserver(private val mapView: MapView) : LifecycleEventObserver { private var currentLifecycleState: Lifecycle.State = Lifecycle.State.INITIALIZED override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { From 99c0466f35b6e7f133176f52b2fb9e617cba48e6 Mon Sep 17 00:00:00 2001 From: Philip S Date: Wed, 24 Apr 2024 09:39:16 +0200 Subject: [PATCH 069/103] Update GoogleMap.kt --- .../src/main/java/com/google/maps/android/compose/GoogleMap.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index b82e0227..3c749f94 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -40,7 +40,6 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView From e7d6dd4bf34b850b7176dd4f1f2b61baaf71894c Mon Sep 17 00:00:00 2001 From: Philip S Date: Wed, 24 Apr 2024 09:40:27 +0200 Subject: [PATCH 070/103] Merge componentCallbacks into registerAndSaveNewComponentCallbacks --- .../google/maps/android/compose/GoogleMap.kt | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 3c749f94..39f28ce8 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -261,7 +261,13 @@ public fun GoogleMap( } private fun MapView.registerAndSaveNewComponentCallbacks() { - val newComponentCallbacks = this.componentCallbacks() + val newComponentCallbacks = object : ComponentCallbacks { + override fun onConfigurationChanged(config: Configuration) {} + + override fun onLowMemory() { + this@registerAndSaveNewComponentCallbacks.onLowMemory() + } + } val tagData = tagData() tagData.componentCallbacks = newComponentCallbacks context.registerComponentCallbacks(newComponentCallbacks) @@ -299,15 +305,6 @@ internal fun MapView.log(msg: String, tag: String = TAG) { Log.d(tag, "[MapView/${tagData().debugId}] $msg") } -private fun MapView.componentCallbacks(): ComponentCallbacks = - object : ComponentCallbacks { - override fun onConfigurationChanged(config: Configuration) {} - - override fun onLowMemory() { - this@componentCallbacks.onLowMemory() - } - } - public typealias GoogleMapFactory = @Composable () -> Unit /** From 91c1065786de280a5d5f79fd019350448fb80482 Mon Sep 17 00:00:00 2001 From: Philip S Date: Wed, 24 Apr 2024 09:50:36 +0200 Subject: [PATCH 071/103] inline MapView.createComposition (move code to call site) --- .../google/maps/android/compose/GoogleMap.kt | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 39f28ce8..c0df66b1 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -167,9 +167,12 @@ public fun GoogleMap( } return launch(start = CoroutineStart.UNDISPATCHED) { - val composition = mapView.createComposition(mapClickListeners, parentComposition).apply { - setContent(mapCompositionContent) - } + val map = mapView.awaitMap() + val composition = Composition( + applier = MapApplier(map, mapView, mapClickListeners), + parent = parentComposition + ) + composition.setContent(mapCompositionContent) try { awaitCancellation() @@ -290,17 +293,6 @@ private fun MapView.tagData(): MapTagData = (tag as? MapTagData) ?: let { mapVie } } -private suspend fun MapView.createComposition( - mapClickListeners: MapClickListeners, - parentComposition: CompositionContext -): Composition { - val map = awaitMap() - return Composition( - applier = MapApplier(map, this, mapClickListeners), - parent = parentComposition - ) -} - internal fun MapView.log(msg: String, tag: String = TAG) { Log.d(tag, "[MapView/${tagData().debugId}] $msg") } From 43a4be8c12c5540305a8a96de076f3d06159071a Mon Sep 17 00:00:00 2001 From: Philip S Date: Wed, 24 Apr 2024 09:50:50 +0200 Subject: [PATCH 072/103] Inline unregisterLifecycleObserver --- .../java/com/google/maps/android/compose/GoogleMap.kt | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index c0df66b1..1d559e6f 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -202,12 +202,6 @@ public fun GoogleMap( var lifecycleOwner: LifecycleOwner? = null - fun unregisterLifecycleObserver() { - log("Unregistering lifecycle observer") - lifecycleOwner?.lifecycle?.removeObserver(lifecycleObserver) - lifecycleOwner = null - } - val onAttachStateListener = object : View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(mapView: View) { log("View attached!") @@ -218,7 +212,9 @@ public fun GoogleMap( override fun onViewDetachedFromWindow(v: View) { log("View detached!") - unregisterLifecycleObserver() + log("Unregistering lifecycle observer") + lifecycleOwner?.lifecycle?.removeObserver(lifecycleObserver) + lifecycleOwner = null lifecycleObserver.moveToBaseState() } } From b3d0c455a611414918c3825cc48b247174409e48 Mon Sep 17 00:00:00 2001 From: Philip S Date: Wed, 24 Apr 2024 09:56:59 +0200 Subject: [PATCH 073/103] Replace `isCompositionSet` with nullable `subcompositionJob` --- .../java/com/google/maps/android/compose/GoogleMap.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 1d559e6f..16514052 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -29,7 +29,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.Composition -import androidx.compose.runtime.CompositionContext import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -183,7 +182,7 @@ public fun GoogleMap( } } - var isCompositionSet by remember { mutableStateOf(false) } + var subcompositionJob by remember { mutableStateOf(null) } val mapUpdaterScope = rememberCoroutineScope() AndroidView( @@ -238,10 +237,9 @@ public fun GoogleMap( }, update = { mapView -> // Create Composition - if (!isCompositionSet) { - isCompositionSet = true + if (subcompositionJob == null) { debugMapId = mapView.tagData().debugId - mapUpdaterScope.launchComposition(mapView) + subcompositionJob = mapUpdaterScope.launchComposition(mapView) } } ) From 4ab3bc1444cb2ec75e30c83d25b9384d3c7e8919 Mon Sep 17 00:00:00 2001 From: Philip S Date: Wed, 24 Apr 2024 09:59:43 +0200 Subject: [PATCH 074/103] Rename `mapUpdaterScope` to `parentCompositionScope` --- .../main/java/com/google/maps/android/compose/GoogleMap.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 16514052..cf9b36e5 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -183,7 +183,7 @@ public fun GoogleMap( } var subcompositionJob by remember { mutableStateOf(null) } - val mapUpdaterScope = rememberCoroutineScope() + val parentCompositionScope = rememberCoroutineScope() AndroidView( modifier = modifier, @@ -239,7 +239,7 @@ public fun GoogleMap( // Create Composition if (subcompositionJob == null) { debugMapId = mapView.tagData().debugId - subcompositionJob = mapUpdaterScope.launchComposition(mapView) + subcompositionJob = parentCompositionScope.launchComposition(mapView) } } ) From b9b9d6a7d221003697e54c1a9623950b2ced72e1 Mon Sep 17 00:00:00 2001 From: Philip S Date: Wed, 24 Apr 2024 10:19:20 +0200 Subject: [PATCH 075/103] Make MapTagData fully immutable + remove all logs + some related refactoring --- .../google/maps/android/compose/GoogleMap.kt | 67 +++++-------------- 1 file changed, 15 insertions(+), 52 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index cf9b36e5..bbae1fbb 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -20,7 +20,6 @@ import android.content.ComponentCallbacks import android.content.res.Configuration import android.location.Location import android.os.Bundle -import android.util.Log import android.view.View import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -135,7 +134,6 @@ public fun GoogleMap( // Debug stuff val debugCompositionId = remember { compositionCounter++ } - var debugMapId: Int? by remember { mutableStateOf(null) } var debugIsMapReused: Boolean? by remember { mutableStateOf(null) } /** @@ -143,8 +141,6 @@ public fun GoogleMap( * dispose the [Composition] when the parent composable is disposed. * */ fun CoroutineScope.launchComposition(mapView: MapView): Job { - mapView.log("Creating composition...") - val mapCompositionContent: @Composable () -> Unit = { MapUpdater( mergeDescendants = mergeDescendants, @@ -176,7 +172,6 @@ public fun GoogleMap( try { awaitCancellation() } finally { - mapView.log("Disposing composition...") composition.dispose() } } @@ -190,28 +185,21 @@ public fun GoogleMap( factory = { context -> debugIsMapReused = false MapView(context, googleMapOptionsFactory()).also { mapView -> - mapView.log("Creating MapView") - mapView.registerAndSaveNewComponentCallbacks() - - fun log(msg: String) = mapView.log(msg) + val componentCallbacks = mapView.registerComponentCallbacks() + val lifecycleObserver = MapLifecycleEventObserver(mapView) - val lifecycleObserver = MapLifecycleEventObserver(mapView).also { - mapView.tagData().lifecycleObserver = it - } + mapView.tag = MapTagData(componentCallbacks, lifecycleObserver) var lifecycleOwner: LifecycleOwner? = null val onAttachStateListener = object : View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(mapView: View) { - log("View attached!") lifecycleOwner = mapView.findViewTreeLifecycleOwner()!!.also { it.lifecycle.addObserver(lifecycleObserver) } } override fun onViewDetachedFromWindow(v: View) { - log("View detached!") - log("Unregistering lifecycle observer") lifecycleOwner?.lifecycle?.removeObserver(lifecycleObserver) lifecycleOwner = null lifecycleObserver.moveToBaseState() @@ -223,22 +211,14 @@ public fun GoogleMap( }, onReset = { /* View is detached. */ }, onRelease = { mapView -> - mapView.log("onRelease") - - mapView.tagData().let { tagData -> - tagData.componentCallbacks?.let { componentCallbacks -> - mapView.context.unregisterComponentCallbacks(componentCallbacks) - } - - tagData.lifecycleObserver!!.moveToDestroyedState() - } - + val (componentCallbacks, lifecycleObserver) = mapView.tagData() + mapView.context.unregisterComponentCallbacks(componentCallbacks) + lifecycleObserver.moveToDestroyedState() mapView.tag = null }, update = { mapView -> // Create Composition if (subcompositionJob == null) { - debugMapId = mapView.tagData().debugId subcompositionJob = parentCompositionScope.launchComposition(mapView) } } @@ -252,44 +232,28 @@ public fun GoogleMap( ) { Text("Map reused: $debugIsMapReused") Text("Composition ID: $debugCompositionId") - Text("MapView ID: $debugMapId") } } } -private fun MapView.registerAndSaveNewComponentCallbacks() { - val newComponentCallbacks = object : ComponentCallbacks { +private fun MapView.registerComponentCallbacks(): ComponentCallbacks { + val componentCallbacks = object : ComponentCallbacks { override fun onConfigurationChanged(config: Configuration) {} override fun onLowMemory() { - this@registerAndSaveNewComponentCallbacks.onLowMemory() + this@registerComponentCallbacks.onLowMemory() } } - val tagData = tagData() - tagData.componentCallbacks = newComponentCallbacks - context.registerComponentCallbacks(newComponentCallbacks) + context.registerComponentCallbacks(componentCallbacks) + return componentCallbacks } private data class MapTagData( - var componentCallbacks: ComponentCallbacks?, - var lifecycleObserver: MapLifecycleEventObserver?, - val debugId: Int = nextId -) { - companion object { - private var nextId = 0 - get() = field++ - } -} + val componentCallbacks: ComponentCallbacks, + val lifecycleObserver: MapLifecycleEventObserver +) -private fun MapView.tagData(): MapTagData = (tag as? MapTagData) ?: let { mapView -> - MapTagData(null, null).also { newTag -> - mapView.tag = newTag - } -} - -internal fun MapView.log(msg: String, tag: String = TAG) { - Log.d(tag, "[MapView/${tagData().debugId}] $msg") -} +private fun MapView.tagData(): MapTagData = tag as MapTagData public typealias GoogleMapFactory = @Composable () -> Unit @@ -379,7 +343,6 @@ private class MapLifecycleEventObserver(private val mapView: MapView) : Lifecycl } private fun invokeEvent(event: Lifecycle.Event) { - mapView.log("Invoking Lifecycle event $event") when(event) { Lifecycle.Event.ON_CREATE -> mapView.onCreate(Bundle()) Lifecycle.Event.ON_START -> mapView.onStart() From 3615f29a5c8f43cf98181ef8025aee0ffdb62376 Mon Sep 17 00:00:00 2001 From: Philip S Date: Wed, 24 Apr 2024 10:27:15 +0200 Subject: [PATCH 076/103] Remove debugging stuff --- .../google/maps/android/compose/GoogleMap.kt | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index bbae1fbb..05aa8acd 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -22,10 +22,7 @@ import android.location.Location import android.os.Bundle import android.view.View import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.Composition import androidx.compose.runtime.CompositionLocalProvider @@ -36,10 +33,8 @@ import androidx.compose.runtime.rememberCompositionContext import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver @@ -59,8 +54,6 @@ import kotlinx.coroutines.launch internal const val TAG = "GoogleMap" -private var compositionCounter = 0 - /** * A compose container for a [MapView]. * @@ -132,10 +125,6 @@ public fun GoogleMap( val parentComposition = rememberCompositionContext() val currentContent by rememberUpdatedState(content) - // Debug stuff - val debugCompositionId = remember { compositionCounter++ } - var debugIsMapReused: Boolean? by remember { mutableStateOf(null) } - /** * Create and apply the [content] compositions to the map + * dispose the [Composition] when the parent composable is disposed. @@ -183,7 +172,6 @@ public fun GoogleMap( AndroidView( modifier = modifier, factory = { context -> - debugIsMapReused = false MapView(context, googleMapOptionsFactory()).also { mapView -> val componentCallbacks = mapView.registerComponentCallbacks() val lifecycleObserver = MapLifecycleEventObserver(mapView) @@ -223,17 +211,6 @@ public fun GoogleMap( } } ) - - Box(modifier = modifier) { - Column( - modifier = Modifier - .align(Alignment.TopStart) - .padding(8.dp) - ) { - Text("Map reused: $debugIsMapReused") - Text("Composition ID: $debugCompositionId") - } - } } private fun MapView.registerComponentCallbacks(): ComponentCallbacks { From 1193cbf075c51f041b0118b75db6d5a12e8e3cf8 Mon Sep 17 00:00:00 2001 From: Philip S Date: Wed, 24 Apr 2024 10:30:08 +0200 Subject: [PATCH 077/103] Add some comments in AndroidView->factory --- .../main/java/com/google/maps/android/compose/GoogleMap.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 05aa8acd..4264829a 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -173,13 +173,16 @@ public fun GoogleMap( modifier = modifier, factory = { context -> MapView(context, googleMapOptionsFactory()).also { mapView -> + // Register ComponentCallbacks so that MapView is notified on low memory val componentCallbacks = mapView.registerComponentCallbacks() + // Forward parent and custom lifecycle events to MapView val lifecycleObserver = MapLifecycleEventObserver(mapView) - + // Store things in the tag which must be retrievable across recompositions mapView.tag = MapTagData(componentCallbacks, lifecycleObserver) - + // MapView's parent LifecycleOwner, such as Activity/Fragment var lifecycleOwner: LifecycleOwner? = null + // Only register for [lifecycleObserver]'s lifecycle events when MapView is attached val onAttachStateListener = object : View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(mapView: View) { lifecycleOwner = mapView.findViewTreeLifecycleOwner()!!.also { From 899fe204d81fad9fce6a7f58d5a88d2766f4aa9c Mon Sep 17 00:00:00 2001 From: Philip S Date: Wed, 24 Apr 2024 10:33:12 +0200 Subject: [PATCH 078/103] Remove "Create Composition" comment --- .../src/main/java/com/google/maps/android/compose/GoogleMap.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 4264829a..4556278b 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -208,7 +208,6 @@ public fun GoogleMap( mapView.tag = null }, update = { mapView -> - // Create Composition if (subcompositionJob == null) { subcompositionJob = parentCompositionScope.launchComposition(mapView) } From e0d725c1fda4a8019f3deeccc8f41c20eefef9ad Mon Sep 17 00:00:00 2001 From: Philip S Date: Wed, 24 Apr 2024 11:10:03 +0200 Subject: [PATCH 079/103] Remove compose material dependency for maps-compose module --- maps-compose/build.gradle | 2 -- 1 file changed, 2 deletions(-) diff --git a/maps-compose/build.gradle b/maps-compose/build.gradle index 1906b2c8..d8dfc009 100644 --- a/maps-compose/build.gradle +++ b/maps-compose/build.gradle @@ -59,8 +59,6 @@ dependencies { implementation platform(libs.androidx.compose.bom) implementation libs.androidx.core implementation libs.androidx.compose.foundation - // TODO - remove before release. Just for debugging purposes. - implementation libs.androidx.compose.material implementation libs.kotlin api libs.maps.ktx.std From b1dd8484fe8b4adfe1698ddd9c47d8a0fdbc0b73 Mon Sep 17 00:00:00 2001 From: Philip S Date: Wed, 24 Apr 2024 12:23:17 +0200 Subject: [PATCH 080/103] Some improvements to MapsInLazyColumnActivity + add more debugging controls --- .../compose/MapsInLazyColumnActivity.kt | 276 +++++++++--------- 1 file changed, 143 insertions(+), 133 deletions(-) diff --git a/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt b/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt index a8d522c2..b1f3711f 100644 --- a/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt @@ -7,6 +7,8 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -17,16 +19,17 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.Button +import androidx.compose.foundation.rememberScrollState import androidx.compose.material.Card import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Text +import androidx.compose.material.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -39,21 +42,79 @@ import com.google.android.gms.maps.model.CameraPosition import com.google.android.gms.maps.model.IndoorBuilding import com.google.android.gms.maps.model.LatLng +private data class CountryLocation(val name: String, val latLng: LatLng, val zoom: Float) + +// From https://developers.google.com/public-data/docs/canonical/countries_csv +private val countries = listOf( + CountryLocation("Hong Kong", LatLng(22.396428, 114.109497), 5f), + CountryLocation("Madison Square Garden (has indoor mode)", LatLng(40.7504656, -73.9937246), 19.33f), + CountryLocation("Bolivia", LatLng(-16.290154, -63.588653), 5f), + CountryLocation("Ecuador", LatLng(-1.831239, -78.183406), 5f), + CountryLocation("Sweden", LatLng(60.128161, 18.643501), 5f), + CountryLocation("Eritrea", LatLng(15.179384, 39.782334), 5f), + CountryLocation("Portugal", LatLng(39.399872, -8.224454), 5f), + CountryLocation("Belgium", LatLng(50.503887, 4.469936), 5f), + CountryLocation("Slovakia", LatLng(48.669026, 19.699024), 5f), + CountryLocation("El Salvador", LatLng(13.794185, -88.89653), 5f), + CountryLocation("Bhutan", LatLng(27.514162, 90.433601), 5f), + CountryLocation("Saint Lucia", LatLng(13.909444, -60.978893), 5f), + CountryLocation("Uganda", LatLng(1.373333, 32.290275), 5f), + CountryLocation("South Africa", LatLng(-30.559482, 22.937506), 5f), + CountryLocation("Spain", LatLng(40.463667, -3.74922), 5f), + CountryLocation("Georgia", LatLng(42.315407, 43.356892), 5f), + CountryLocation("Burundi", LatLng(-3.373056, 29.918886), 5f) +) + +private data class MapListItem( + val title: String, + val location: LatLng, + val zoom: Float, + val id: String +) + +private val allItems = countries.mapIndexed { index, country -> + MapListItem(country.name, country.latLng, country.zoom, "MapInLazyColumn#$index") +} + class MapsInLazyColumnActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { - var showMaps by remember { mutableStateOf(true) } + var showLazyColumn by rememberSaveable { mutableStateOf(true) } + var visibleItems by rememberSaveable { mutableStateOf(allItems) } + + fun setItemCount(count: Int) { + visibleItems = allItems.take(count.coerceIn(0, allItems.size)) + } Column { - Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { - Button(onClick = { showMaps = !showMaps }) { - Text(text = if(showMaps) "Hide maps" else "Show maps") + Row( + Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + TextButton(onClick = { setItemCount(0) }) { + Text(text = "Clear") + } + TextButton(onClick = { setItemCount(visibleItems.size - 1) }) { + Text(text = "Remove") + } + TextButton(onClick = { showLazyColumn = !showLazyColumn }) { + Text(text = if(showLazyColumn) "Hide" else "Show") + } + TextButton(onClick = { setItemCount(visibleItems.size + 1) }) { + Text(text = "Add") + } + TextButton(onClick = { setItemCount(allItems.size) }) { + Text(text = "Fill") } } - if(showMaps) { - MapsInLazyColumn() + if(showLazyColumn) { + Box(Modifier.border(1.dp, Color.LightGray.copy(0.5f))) { + MapsInLazyColumn(visibleItems) + } } } } @@ -61,9 +122,9 @@ class MapsInLazyColumnActivity : ComponentActivity() { } @Composable -private fun MapsInLazyColumn() { +private fun MapsInLazyColumn(mapItems: List) { LazyColumn { - items(mapListItems, key = { it.id }) { item -> + items(mapItems, key = { it.id }) { item -> Box( Modifier .fillMaxWidth() @@ -76,146 +137,95 @@ private fun MapsInLazyColumn() { } } +@OptIn(MapsComposeExperimentalApi::class) @Composable private fun MapCard(item: MapListItem) { Card( - Modifier - .padding(16.dp), + Modifier.padding(16.dp), elevation = 4.dp ) { - Column { - Box { - CardMap( - Modifier.fillMaxSize(), - item - ) - } + var mapLoaded by remember { mutableStateOf(false) } + var buildingFocused: Boolean? by remember { mutableStateOf(null) } + var focusedBuildingInvocationCount by remember { mutableIntStateOf(0) } + var activatedIndoorLevel: String? by remember { mutableStateOf(null) } + var activatedIndoorLevelInvocationCount by remember { mutableIntStateOf(0) } + var onMapClickCount by remember { mutableIntStateOf(0) } + + val cameraPositionState = rememberCameraPositionState( + key = item.id, + init = { position = CameraPosition.fromLatLngZoom(item.location, item.zoom) } + ) + + var map: GoogleMap? by remember { mutableStateOf(null) } + + fun updateIndoorLevel() { + activatedIndoorLevel = map!!.focusedBuilding?.run { levels.getOrNull(activeLevelIndex)?.name } } - } -} -@OptIn(MapsComposeExperimentalApi::class) -@Composable -private fun CardMap( - modifier: Modifier, - mapItem: MapListItem -) { - var mapLoaded by remember { mutableStateOf(false) } - var buildingFocused: Boolean? by remember { mutableStateOf(null) } - var focusedBuildingInvocationCount by remember { mutableIntStateOf(0) } - var activatedIndoorLevel: String? by remember { mutableStateOf(null) } - var activatedIndoorLevelInvocationCount by remember { mutableIntStateOf(0) } - var onMapClickCount by remember { mutableIntStateOf(0) } - - val cameraPositionState = rememberCameraPositionState( - key = mapItem.id, - init = { position = CameraPosition.fromLatLngZoom(mapItem.location, mapItem.zoom) } - ) - - var map: GoogleMap? by remember { mutableStateOf(null) } - - fun updateIndoorLevel() { - activatedIndoorLevel = map!!.focusedBuilding?.run { levels.getOrNull(activeLevelIndex)?.name } - } + Box { + GoogleMap( + onMapClick = { + onMapClickCount++ + }, + properties = remember { + MapProperties( + isBuildingEnabled = true, + isIndoorEnabled = true + ) + }, + cameraPositionState = cameraPositionState, + onMapLoaded = { mapLoaded = true }, + indoorStateChangeListener = object : IndoorStateChangeListener { + override fun onIndoorBuildingFocused() { + super.onIndoorBuildingFocused() + focusedBuildingInvocationCount++ + buildingFocused = (map!!.focusedBuilding != null) + updateIndoorLevel() + } - Box { - GoogleMap( - onMapClick = { - onMapClickCount++ - }, - modifier = modifier, - properties = remember { - MapProperties( - isBuildingEnabled = true, - isIndoorEnabled = true - ) - }, - cameraPositionState = cameraPositionState, - onMapLoaded = { mapLoaded = true }, - indoorStateChangeListener = object : IndoorStateChangeListener { - override fun onIndoorBuildingFocused() { - super.onIndoorBuildingFocused() - focusedBuildingInvocationCount++ - buildingFocused = (map!!.focusedBuilding != null) + override fun onIndoorLevelActivated(building: IndoorBuilding) { + super.onIndoorLevelActivated(building) + activatedIndoorLevelInvocationCount++ + updateIndoorLevel() + } + } + ) { + MapEffect(Unit) { + map = it updateIndoorLevel() } + } - override fun onIndoorLevelActivated(building: IndoorBuilding) { - super.onIndoorLevelActivated(building) - activatedIndoorLevelInvocationCount++ - updateIndoorLevel() + AnimatedVisibility(!mapLoaded, enter = fadeIn(), exit = fadeOut()) { + Box( + Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() } } - ) { - MapEffect(Unit) { - map = it + + @Composable + fun TextWithBackground(text: String, fontWeight: FontWeight = FontWeight.Medium) { + Text( + modifier = Modifier.background(Color.White.copy(0.7f)), + text = text, + fontWeight = fontWeight, + fontSize = 10.sp + ) } - } - AnimatedVisibility(!mapLoaded, enter = fadeIn(), exit = fadeOut()) { - Box( - Modifier.fillMaxSize(), - contentAlignment = Alignment.Center + Column( + modifier = Modifier.align(Alignment.BottomStart) ) { - CircularProgressIndicator() + TextWithBackground(item.title, fontWeight = FontWeight.Bold) + TextWithBackground("Map loaded: $mapLoaded") + TextWithBackground("Map click count: $onMapClickCount") + TextWithBackground("Building focused: $buildingFocused") + TextWithBackground("Building focused invocation count: $focusedBuildingInvocationCount") + TextWithBackground("Indoor level: $activatedIndoorLevel") + TextWithBackground("Indoor level invocation count: $activatedIndoorLevelInvocationCount") } } - - @Composable - fun TextWithBackground(text: String, fontWeight: FontWeight = FontWeight.Medium) { - Text( - modifier = Modifier.background(Color.White.copy(0.7f)), - text = text, - fontWeight = fontWeight, - fontSize = 10.sp - ) - } - - Column( - modifier = Modifier.align(Alignment.BottomStart) - ) { - TextWithBackground(mapItem.title, fontWeight = FontWeight.Bold) - TextWithBackground("Map loaded: $mapLoaded") - TextWithBackground("Map click count: $onMapClickCount") - TextWithBackground("Building focused: $buildingFocused") - TextWithBackground("Building focused invocation count: $focusedBuildingInvocationCount") - TextWithBackground("Indoor level: $activatedIndoorLevel") - TextWithBackground("Indoor level invocation count: $activatedIndoorLevelInvocationCount") - } } } - -private data class MapListItem( - val title: String, - val location: LatLng, - val zoom: Float, - val id: String -) - -// From https://developers.google.com/public-data/docs/canonical/countries_csv -private val countries = listOf( - CountryLocation("Hong Kong", LatLng(22.396428, 114.109497), 5f), - CountryLocation("Madison Square Garden (has indoor mode)", LatLng(40.7504656, -73.9937246), 19.33f), - CountryLocation("Bolivia", LatLng(-16.290154, -63.588653), 5f), - CountryLocation("Ecuador", LatLng(-1.831239, -78.183406), 5f), - CountryLocation("Sweden", LatLng(60.128161, 18.643501), 5f), - CountryLocation("Eritrea", LatLng(15.179384, 39.782334), 5f), - CountryLocation("Portugal", LatLng(39.399872, -8.224454), 5f), - CountryLocation("Belgium", LatLng(50.503887, 4.469936), 5f), - CountryLocation("Slovakia", LatLng(48.669026, 19.699024), 5f), - CountryLocation("El Salvador", LatLng(13.794185, -88.89653), 5f), - CountryLocation("Bhutan", LatLng(27.514162, 90.433601), 5f), - CountryLocation("Saint Lucia", LatLng(13.909444, -60.978893), 5f), - CountryLocation("Uganda", LatLng(1.373333, 32.290275), 5f), - CountryLocation("South Africa", LatLng(-30.559482, 22.937506), 5f), - CountryLocation("Spain", LatLng(40.463667, -3.74922), 5f), - CountryLocation("Georgia", LatLng(42.315407, 43.356892), 5f), - CountryLocation("Burundi", LatLng(-3.373056, 29.918886), 5f) -) - -private val mapListItems = countries - .mapIndexed { index, country -> - MapListItem(country.name, country.latLng, country.zoom, "MapInLazyColumn#$index") - } - -private data class CountryLocation(val name: String, val latLng: LatLng, val zoom: Float) \ No newline at end of file From a07f093b5b2b7c06445c80c1f96f13714349b00b Mon Sep 17 00:00:00 2001 From: Philip S Date: Wed, 24 Apr 2024 14:54:38 +0200 Subject: [PATCH 081/103] Convert tagData() to extension property --- .../main/java/com/google/maps/android/compose/GoogleMap.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 4556278b..b7d31073 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -202,7 +202,7 @@ public fun GoogleMap( }, onReset = { /* View is detached. */ }, onRelease = { mapView -> - val (componentCallbacks, lifecycleObserver) = mapView.tagData() + val (componentCallbacks, lifecycleObserver) = mapView.tagData mapView.context.unregisterComponentCallbacks(componentCallbacks) lifecycleObserver.moveToDestroyedState() mapView.tag = null @@ -232,7 +232,8 @@ private data class MapTagData( val lifecycleObserver: MapLifecycleEventObserver ) -private fun MapView.tagData(): MapTagData = tag as MapTagData +private val MapView.tagData: MapTagData + get() = tag as MapTagData public typealias GoogleMapFactory = @Composable () -> Unit From f239db0ec14296726067b0fb52508c401078da08 Mon Sep 17 00:00:00 2001 From: Philip S Date: Wed, 24 Apr 2024 16:20:18 +0200 Subject: [PATCH 082/103] Move `composition` inside `try` block --- .../src/main/java/com/google/maps/android/compose/GoogleMap.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index b7d31073..6c2de791 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -156,9 +156,9 @@ public fun GoogleMap( applier = MapApplier(map, mapView, mapClickListeners), parent = parentComposition ) - composition.setContent(mapCompositionContent) try { + composition.setContent(mapCompositionContent) awaitCancellation() } finally { composition.dispose() From f076922d9e6ccf52de350ef20943208fca995b17 Mon Sep 17 00:00:00 2001 From: Philip S Date: Wed, 24 Apr 2024 16:26:49 +0200 Subject: [PATCH 083/103] Formatting --- .../maps/android/compose/MapsInLazyColumnActivity.kt | 4 ++-- .../java/com/google/maps/android/compose/GoogleMap.kt | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt b/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt index b1f3711f..685c40af 100644 --- a/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt @@ -102,7 +102,7 @@ class MapsInLazyColumnActivity : ComponentActivity() { Text(text = "Remove") } TextButton(onClick = { showLazyColumn = !showLazyColumn }) { - Text(text = if(showLazyColumn) "Hide" else "Show") + Text(text = if (showLazyColumn) "Hide" else "Show") } TextButton(onClick = { setItemCount(visibleItems.size + 1) }) { Text(text = "Add") @@ -111,7 +111,7 @@ class MapsInLazyColumnActivity : ComponentActivity() { Text(text = "Fill") } } - if(showLazyColumn) { + if (showLazyColumn) { Box(Modifier.border(1.dp, Color.LightGray.copy(0.5f))) { MapsInLazyColumn(visibleItems) } diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 6c2de791..5a524dab 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -290,19 +290,19 @@ private class MapLifecycleEventObserver(private val mapView: MapView) : Lifecycl * It's theoretically possible that [currentLifecycleState] is still in [Lifecycle.State.INITIALIZED] state. * */ fun moveToBaseState() { - if(currentLifecycleState > Lifecycle.State.CREATED) { + if (currentLifecycleState > Lifecycle.State.CREATED) { moveToLifecycleState(Lifecycle.State.CREATED) } } fun moveToDestroyedState() { - if(currentLifecycleState > Lifecycle.State.INITIALIZED) { + if (currentLifecycleState > Lifecycle.State.INITIALIZED) { moveToLifecycleState(Lifecycle.State.DESTROYED) } } private fun moveToLifecycleState(targetState: Lifecycle.State) { - while(currentLifecycleState != targetState) { + while (currentLifecycleState != targetState) { when { currentLifecycleState < targetState -> moveUp() currentLifecycleState > targetState -> moveDown() @@ -323,7 +323,7 @@ private class MapLifecycleEventObserver(private val mapView: MapView) : Lifecycl } private fun invokeEvent(event: Lifecycle.Event) { - when(event) { + when (event) { Lifecycle.Event.ON_CREATE -> mapView.onCreate(Bundle()) Lifecycle.Event.ON_START -> mapView.onStart() Lifecycle.Event.ON_RESUME -> mapView.onResume() From 137746f29b830f3d7c72b136f1a1f668f9a4e22b Mon Sep 17 00:00:00 2001 From: Philip S Date: Wed, 24 Apr 2024 16:28:45 +0200 Subject: [PATCH 084/103] Add comment on CoroutineStart.UNDISPATCHED --- .../src/main/java/com/google/maps/android/compose/GoogleMap.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 5a524dab..da01cedf 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -150,6 +150,7 @@ public fun GoogleMap( } } + // Use [CoroutineStart.UNDISPATCHED] to kick off composition immediately return launch(start = CoroutineStart.UNDISPATCHED) { val map = mapView.awaitMap() val composition = Composition( From 2d26a6a6c840508724fc637e7c20bf966c4a556a Mon Sep 17 00:00:00 2001 From: Philip S Date: Wed, 24 Apr 2024 16:31:48 +0200 Subject: [PATCH 085/103] Add KDoc to MapTagData --- .../src/main/java/com/google/maps/android/compose/GoogleMap.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index da01cedf..a7a944a4 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -178,7 +178,6 @@ public fun GoogleMap( val componentCallbacks = mapView.registerComponentCallbacks() // Forward parent and custom lifecycle events to MapView val lifecycleObserver = MapLifecycleEventObserver(mapView) - // Store things in the tag which must be retrievable across recompositions mapView.tag = MapTagData(componentCallbacks, lifecycleObserver) // MapView's parent LifecycleOwner, such as Activity/Fragment var lifecycleOwner: LifecycleOwner? = null @@ -228,6 +227,7 @@ private fun MapView.registerComponentCallbacks(): ComponentCallbacks { return componentCallbacks } +/** Used to stored things in the tag which must be retrievable across recompositions */ private data class MapTagData( val componentCallbacks: ComponentCallbacks, val lifecycleObserver: MapLifecycleEventObserver From bf4289f4cee561ad0b133988a290e8adc158c7b9 Mon Sep 17 00:00:00 2001 From: Philip S Date: Wed, 24 Apr 2024 16:33:58 +0200 Subject: [PATCH 086/103] Remove Suppress annotation --- .../src/main/java/com/google/maps/android/compose/GoogleMap.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index a7a944a4..412169ee 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -@file:Suppress("RemoveRedundantQualifierName") - package com.google.maps.android.compose import android.content.ComponentCallbacks From 34b1044ce3cbac76531cdbb8f97eb649bb3fb8fa Mon Sep 17 00:00:00 2001 From: Philip S Date: Wed, 24 Apr 2024 16:35:03 +0200 Subject: [PATCH 087/103] Rename launchComposition to launchSubcomposition --- .../main/java/com/google/maps/android/compose/GoogleMap.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 412169ee..8fc223f5 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -127,7 +127,7 @@ public fun GoogleMap( * Create and apply the [content] compositions to the map + * dispose the [Composition] when the parent composable is disposed. * */ - fun CoroutineScope.launchComposition(mapView: MapView): Job { + fun CoroutineScope.launchSubcomposition(mapView: MapView): Job { val mapCompositionContent: @Composable () -> Unit = { MapUpdater( mergeDescendants = mergeDescendants, @@ -207,7 +207,7 @@ public fun GoogleMap( }, update = { mapView -> if (subcompositionJob == null) { - subcompositionJob = parentCompositionScope.launchComposition(mapView) + subcompositionJob = parentCompositionScope.launchSubcomposition(mapView) } } ) From 14a59643489c12c96ee00e05c0627edefe2c3e84 Mon Sep 17 00:00:00 2001 From: Philip S Date: Wed, 24 Apr 2024 16:36:17 +0200 Subject: [PATCH 088/103] Remove obvious comments --- .../main/java/com/google/maps/android/compose/GoogleMap.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 8fc223f5..370a4ea5 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -172,12 +172,10 @@ public fun GoogleMap( modifier = modifier, factory = { context -> MapView(context, googleMapOptionsFactory()).also { mapView -> - // Register ComponentCallbacks so that MapView is notified on low memory val componentCallbacks = mapView.registerComponentCallbacks() - // Forward parent and custom lifecycle events to MapView val lifecycleObserver = MapLifecycleEventObserver(mapView) mapView.tag = MapTagData(componentCallbacks, lifecycleObserver) - // MapView's parent LifecycleOwner, such as Activity/Fragment + var lifecycleOwner: LifecycleOwner? = null // Only register for [lifecycleObserver]'s lifecycle events when MapView is attached From c562f3b28d9f2d7a1b4fde496a35a0f5af3239f6 Mon Sep 17 00:00:00 2001 From: Philip S Date: Wed, 24 Apr 2024 16:39:38 +0200 Subject: [PATCH 089/103] Move `lifecycleOwner` inside `object : View.OnAttachStateChangeListener` --- .../main/java/com/google/maps/android/compose/GoogleMap.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 370a4ea5..374a29fc 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -176,10 +176,10 @@ public fun GoogleMap( val lifecycleObserver = MapLifecycleEventObserver(mapView) mapView.tag = MapTagData(componentCallbacks, lifecycleObserver) - var lifecycleOwner: LifecycleOwner? = null - // Only register for [lifecycleObserver]'s lifecycle events when MapView is attached val onAttachStateListener = object : View.OnAttachStateChangeListener { + private var lifecycleOwner: LifecycleOwner? = null + override fun onViewAttachedToWindow(mapView: View) { lifecycleOwner = mapView.findViewTreeLifecycleOwner()!!.also { it.lifecycle.addObserver(lifecycleObserver) From 3bbb774bb2a015fc62e352803e249700488e79a7 Mon Sep 17 00:00:00 2001 From: Philip S Date: Wed, 24 Apr 2024 16:41:22 +0200 Subject: [PATCH 090/103] Fix comment --- .../src/main/java/com/google/maps/android/compose/GoogleMap.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 374a29fc..88f2ae9f 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -176,7 +176,7 @@ public fun GoogleMap( val lifecycleObserver = MapLifecycleEventObserver(mapView) mapView.tag = MapTagData(componentCallbacks, lifecycleObserver) - // Only register for [lifecycleObserver]'s lifecycle events when MapView is attached + // Only register for [lifecycleOwner]'s lifecycle events while MapView is attached val onAttachStateListener = object : View.OnAttachStateChangeListener { private var lifecycleOwner: LifecycleOwner? = null From bbc6ac1dbbac4ec32ec94989dc88985c9978e067 Mon Sep 17 00:00:00 2001 From: Philip S Date: Wed, 24 Apr 2024 16:48:04 +0200 Subject: [PATCH 091/103] Convert registerComponentCallbacks to a top-level class --- .../google/maps/android/compose/GoogleMap.kt | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 88f2ae9f..37619dd0 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -172,8 +172,11 @@ public fun GoogleMap( modifier = modifier, factory = { context -> MapView(context, googleMapOptionsFactory()).also { mapView -> - val componentCallbacks = mapView.registerComponentCallbacks() + val componentCallbacks = MapViewComponentCallbacks(mapView) + context.registerComponentCallbacks(componentCallbacks) + val lifecycleObserver = MapLifecycleEventObserver(mapView) + mapView.tag = MapTagData(componentCallbacks, lifecycleObserver) // Only register for [lifecycleOwner]'s lifecycle events while MapView is attached @@ -211,16 +214,11 @@ public fun GoogleMap( ) } -private fun MapView.registerComponentCallbacks(): ComponentCallbacks { - val componentCallbacks = object : ComponentCallbacks { - override fun onConfigurationChanged(config: Configuration) {} - - override fun onLowMemory() { - this@registerComponentCallbacks.onLowMemory() - } +private class MapViewComponentCallbacks(private val mapView: MapView) : ComponentCallbacks { + override fun onConfigurationChanged(newConfig: Configuration) {} + override fun onLowMemory() { + mapView.onLowMemory() } - context.registerComponentCallbacks(componentCallbacks) - return componentCallbacks } /** Used to stored things in the tag which must be retrievable across recompositions */ From 4af88f9906aada10384420b1dc6e15a33265c149 Mon Sep 17 00:00:00 2001 From: Philip S Date: Wed, 24 Apr 2024 16:54:23 +0200 Subject: [PATCH 092/103] Inline componentCallbacks class --- .../com/google/maps/android/compose/GoogleMap.kt | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 37619dd0..0e31d704 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -172,7 +172,10 @@ public fun GoogleMap( modifier = modifier, factory = { context -> MapView(context, googleMapOptionsFactory()).also { mapView -> - val componentCallbacks = MapViewComponentCallbacks(mapView) + val componentCallbacks = object : ComponentCallbacks { + override fun onConfigurationChanged(newConfig: Configuration) {} + override fun onLowMemory() { mapView.onLowMemory() } + } context.registerComponentCallbacks(componentCallbacks) val lifecycleObserver = MapLifecycleEventObserver(mapView) @@ -214,13 +217,6 @@ public fun GoogleMap( ) } -private class MapViewComponentCallbacks(private val mapView: MapView) : ComponentCallbacks { - override fun onConfigurationChanged(newConfig: Configuration) {} - override fun onLowMemory() { - mapView.onLowMemory() - } -} - /** Used to stored things in the tag which must be retrievable across recompositions */ private data class MapTagData( val componentCallbacks: ComponentCallbacks, From 348658f0e49d380cc199150778932556a63a2443 Mon Sep 17 00:00:00 2001 From: Philip S Date: Wed, 24 Apr 2024 16:57:10 +0200 Subject: [PATCH 093/103] Store reference to `Lifecycle` instead of `LifecycleOwner` --- .../java/com/google/maps/android/compose/GoogleMap.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 0e31d704..27d39a24 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -184,17 +184,17 @@ public fun GoogleMap( // Only register for [lifecycleOwner]'s lifecycle events while MapView is attached val onAttachStateListener = object : View.OnAttachStateChangeListener { - private var lifecycleOwner: LifecycleOwner? = null + private var lifecycle: Lifecycle? = null override fun onViewAttachedToWindow(mapView: View) { - lifecycleOwner = mapView.findViewTreeLifecycleOwner()!!.also { - it.lifecycle.addObserver(lifecycleObserver) + lifecycle = mapView.findViewTreeLifecycleOwner()!!.lifecycle.also { + it.addObserver(lifecycleObserver) } } override fun onViewDetachedFromWindow(v: View) { - lifecycleOwner?.lifecycle?.removeObserver(lifecycleObserver) - lifecycleOwner = null + lifecycle?.removeObserver(lifecycleObserver) + lifecycle = null lifecycleObserver.moveToBaseState() } } From a4cdd8b77f2a63ed2ffeb281a41f03e48874c28b Mon Sep 17 00:00:00 2001 From: Philip S Date: Wed, 24 Apr 2024 18:11:32 +0200 Subject: [PATCH 094/103] Update comment --- .../src/main/java/com/google/maps/android/compose/GoogleMap.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 27d39a24..7803f952 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -148,7 +148,7 @@ public fun GoogleMap( } } - // Use [CoroutineStart.UNDISPATCHED] to kick off composition immediately + // Use [CoroutineStart.UNDISPATCHED] to kick off GoogleMap loading immediately return launch(start = CoroutineStart.UNDISPATCHED) { val map = mapView.awaitMap() val composition = Composition( From b26f55dd45b19bfa5873f229962fd8f45b8041fe Mon Sep 17 00:00:00 2001 From: Philip S Date: Wed, 24 Apr 2024 18:29:30 +0200 Subject: [PATCH 095/103] Fix typo --- .../src/main/java/com/google/maps/android/compose/GoogleMap.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 7803f952..dd3414b2 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -217,7 +217,7 @@ public fun GoogleMap( ) } -/** Used to stored things in the tag which must be retrievable across recompositions */ +/** Used to store things in the tag which must be retrievable across recompositions */ private data class MapTagData( val componentCallbacks: ComponentCallbacks, val lifecycleObserver: MapLifecycleEventObserver From 653c8570bdeb95d1d9bd7286d9052cd03ed73444 Mon Sep 17 00:00:00 2001 From: Philip S Date: Tue, 7 May 2024 11:07:32 +0200 Subject: [PATCH 096/103] Remove unused TAG constant --- .../src/main/java/com/google/maps/android/compose/GoogleMap.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index dd3414b2..de02e785 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -50,8 +50,6 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.launch -internal const val TAG = "GoogleMap" - /** * A compose container for a [MapView]. * From 0a3a0e574bcc57652b8e0cfcf018aa3d2380f524 Mon Sep 17 00:00:00 2001 From: Philip S Date: Thu, 9 May 2024 12:27:48 +0200 Subject: [PATCH 097/103] Demo - Set buildingFocused state on initial composition --- .../google/maps/android/compose/MapsInLazyColumnActivity.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt b/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt index 685c40af..bc74428a 100644 --- a/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt @@ -190,9 +190,10 @@ private fun MapCard(item: MapListItem) { } } ) { - MapEffect(Unit) { - map = it + MapEffect(Unit) { googleMap -> + map = googleMap updateIndoorLevel() + buildingFocused = (googleMap.focusedBuilding != null) } } From 99810a7a1254deebc73b9a22f538ca6fa17bafd6 Mon Sep 17 00:00:00 2001 From: Philip S Date: Thu, 9 May 2024 17:07:10 +0200 Subject: [PATCH 098/103] Make maps in MapsInLazyColumnActivity pannable --- .../compose/MapsInLazyColumnActivity.kt | 60 ++++++++++++++----- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt b/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt index bc74428a..c53d3b6b 100644 --- a/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/MapsInLazyColumnActivity.kt @@ -19,12 +19,16 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.material.Card import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.LocalTextStyle +import androidx.compose.material.ProvideTextStyle import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -34,6 +38,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -44,6 +49,8 @@ import com.google.android.gms.maps.model.LatLng private data class CountryLocation(val name: String, val latLng: LatLng, val zoom: Float) +private typealias MapItemId = String + // From https://developers.google.com/public-data/docs/canonical/countries_csv private val countries = listOf( CountryLocation("Hong Kong", LatLng(22.396428, 114.109497), 5f), @@ -69,7 +76,7 @@ private data class MapListItem( val title: String, val location: LatLng, val zoom: Float, - val id: String + val id: MapItemId ) private val allItems = countries.mapIndexed { index, country -> @@ -123,15 +130,41 @@ class MapsInLazyColumnActivity : ComponentActivity() { @Composable private fun MapsInLazyColumn(mapItems: List) { - LazyColumn { - items(mapItems, key = { it.id }) { item -> - Box( - Modifier - .fillMaxWidth() - .height(300.dp), - contentAlignment = Alignment.Center - ) { - MapCard(item) + val lazyListState = rememberLazyListState() + + val cameraPositionStates = mapItems.associate { item -> + item.id to rememberCameraPositionState( + key = item.id, + init = { position = CameraPosition.fromLatLngZoom(item.location, item.zoom) } + ) + } + val visibleItemIds by remember(lazyListState) { + derivedStateOf { + lazyListState.layoutInfo.visibleItemsInfo.map { it.key as MapItemId } + } + } + val anyMapMoving by remember(cameraPositionStates) { + derivedStateOf { + visibleItemIds.any { cameraPositionStates[it]?.isMoving == true } + } + } + + Box { + LazyColumn( + state = lazyListState, + userScrollEnabled = !anyMapMoving + ) { + items(mapItems, key = { it.id }) { item -> + val cameraPositionState = cameraPositionStates[item.id]!! + + Box( + Modifier + .fillMaxWidth() + .height(300.dp), + contentAlignment = Alignment.Center + ) { + MapCard(item, cameraPositionState) + } } } } @@ -139,7 +172,7 @@ private fun MapsInLazyColumn(mapItems: List) { @OptIn(MapsComposeExperimentalApi::class) @Composable -private fun MapCard(item: MapListItem) { +private fun MapCard(item: MapListItem, cameraPositionState: CameraPositionState) { Card( Modifier.padding(16.dp), elevation = 4.dp @@ -151,11 +184,6 @@ private fun MapCard(item: MapListItem) { var activatedIndoorLevelInvocationCount by remember { mutableIntStateOf(0) } var onMapClickCount by remember { mutableIntStateOf(0) } - val cameraPositionState = rememberCameraPositionState( - key = item.id, - init = { position = CameraPosition.fromLatLngZoom(item.location, item.zoom) } - ) - var map: GoogleMap? by remember { mutableStateOf(null) } fun updateIndoorLevel() { From b4f5c2986b955a6dfedfbb358d6e7f91275bdaac Mon Sep 17 00:00:00 2001 From: Philip S Date: Thu, 9 May 2024 18:06:39 +0200 Subject: [PATCH 099/103] Incorporate changes from #522 + remove MapClickListeners property from MapApplier [see desc.] - Wrap MapUpdater parameters inside a MapUpdaterState data class - Remove MapClickListeners property from MapApplier - Pass MapClickListeners directly to MapClickListenerUpdater --- .../google/maps/android/compose/GoogleMap.kt | 136 +++++++++++------- .../google/maps/android/compose/MapApplier.kt | 1 - .../maps/android/compose/MapClickListeners.kt | 5 +- .../google/maps/android/compose/MapUpdater.kt | 10 +- 4 files changed, 88 insertions(+), 64 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index de02e785..c50779ba 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -23,7 +23,9 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable import androidx.compose.runtime.Composition +import androidx.compose.runtime.CompositionContext import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -111,58 +113,29 @@ public fun GoogleMap( it.onMyLocationClick = onMyLocationClick it.onPOIClick = onPOIClick } - val currentContentDescription by rememberUpdatedState(contentDescription) - val currentLocationSource by rememberUpdatedState(locationSource) - val currentCameraPositionState by rememberUpdatedState(cameraPositionState) - val currentContentPadding by rememberUpdatedState(contentPadding) - val currentUiSettings by rememberUpdatedState(uiSettings) - val currentMapProperties by rememberUpdatedState(properties) - val parentComposition = rememberCompositionContext() - val currentContent by rememberUpdatedState(content) - - /** - * Create and apply the [content] compositions to the map + - * dispose the [Composition] when the parent composable is disposed. - * */ - fun CoroutineScope.launchSubcomposition(mapView: MapView): Job { - val mapCompositionContent: @Composable () -> Unit = { - MapUpdater( - mergeDescendants = mergeDescendants, - contentDescription = currentContentDescription, - cameraPositionState = currentCameraPositionState, - contentPadding = currentContentPadding, - locationSource = currentLocationSource, - mapProperties = currentMapProperties, - mapUiSettings = currentUiSettings, - ) - - MapClickListenerUpdater() - - CompositionLocalProvider( - LocalCameraPositionState provides currentCameraPositionState, - ) { - currentContent?.invoke() - } - } - - // Use [CoroutineStart.UNDISPATCHED] to kick off GoogleMap loading immediately - return launch(start = CoroutineStart.UNDISPATCHED) { - val map = mapView.awaitMap() - val composition = Composition( - applier = MapApplier(map, mapView, mapClickListeners), - parent = parentComposition - ) - - try { - composition.setContent(mapCompositionContent) - awaitCancellation() - } finally { - composition.dispose() - } - } + val mapUpdaterState = remember { + MapUpdaterState( + mergeDescendants, + contentDescription, + cameraPositionState, + contentPadding, + locationSource, + properties, + uiSettings + ) + }.also { + it.mergeDescendants = mergeDescendants + it.contentDescription = contentDescription + it.cameraPositionState = cameraPositionState + it.contentPadding = contentPadding + it.locationSource = locationSource + it.mapProperties = properties + it.mapUiSettings = uiSettings } + val parentComposition = rememberCompositionContext() + val currentContent by rememberUpdatedState(content) var subcompositionJob by remember { mutableStateOf(null) } val parentCompositionScope = rememberCoroutineScope() @@ -209,12 +182,75 @@ public fun GoogleMap( }, update = { mapView -> if (subcompositionJob == null) { - subcompositionJob = parentCompositionScope.launchSubcomposition(mapView) + subcompositionJob = parentCompositionScope.launchSubcomposition( + mapUpdaterState, + parentComposition, + mapClickListeners, + currentContent, + mapView, + ) } } ) } +/** + * Create and apply the [content] compositions to the map + + * dispose the [Composition] when the parent composable is disposed. + * */ +private fun CoroutineScope.launchSubcomposition( + mapUpdaterState: MapUpdaterState, + parentComposition: CompositionContext, + clickListeners: MapClickListeners, + content: (@Composable @GoogleMapComposable () -> Unit)?, + mapView: MapView +): Job { + // Use [CoroutineStart.UNDISPATCHED] to kick off GoogleMap loading immediately + return launch(start = CoroutineStart.UNDISPATCHED) { + val map = mapView.awaitMap() + val composition = Composition( + applier = MapApplier(map, mapView), + parent = parentComposition + ) + + try { + composition.setContent { + MapUpdater(mapUpdaterState) + + MapClickListenerUpdater(clickListeners) + + CompositionLocalProvider( + LocalCameraPositionState provides currentCameraPositionState, + ) { + content?.invoke() + } + } + awaitCancellation() + } finally { + composition.dispose() + } + } +} + +@Stable +internal class MapUpdaterState( + mergeDescendants: Boolean, + contentDescription: String?, + cameraPositionState: CameraPositionState, + contentPadding: PaddingValues, + locationSource: LocationSource?, + mapProperties: MapProperties, + mapUiSettings: MapUiSettings +) { + var mergeDescendants by mutableStateOf(mergeDescendants) + var contentDescription by mutableStateOf(contentDescription) + var cameraPositionState by mutableStateOf(cameraPositionState) + var contentPadding by mutableStateOf(contentPadding) + var locationSource by mutableStateOf(locationSource) + var mapProperties by mutableStateOf(mapProperties) + var mapUiSettings by mutableStateOf(mapUiSettings) +} + /** Used to store things in the tag which must be retrievable across recompositions */ private data class MapTagData( val componentCallbacks: ComponentCallbacks, 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..b7256cdd 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 @@ -39,7 +39,6 @@ private object MapNodeRoot : MapNode internal class MapApplier( val map: GoogleMap, internal val mapView: MapView, - val mapClickListeners: MapClickListeners, ) : AbstractApplier(MapNodeRoot) { private val decorations = mutableListOf() diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/MapClickListeners.kt b/maps-compose/src/main/java/com/google/maps/android/compose/MapClickListeners.kt index 42b3ee6e..6ac1b860 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/MapClickListeners.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/MapClickListeners.kt @@ -86,10 +86,7 @@ internal class MapClickListenerNode( @Suppress("ComplexRedundantLet") @Composable -internal fun MapClickListenerUpdater() { - // The mapClickListeners container object is not allowed to ever change - val mapClickListeners = (currentComposer.applier as MapApplier).mapClickListeners - +internal fun MapClickListenerUpdater(mapClickListeners: MapClickListeners) { with(mapClickListeners) { ::indoorStateChangeListener.let { callback -> MapClickListenerComposeNode( diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/MapUpdater.kt b/maps-compose/src/main/java/com/google/maps/android/compose/MapUpdater.kt index f27a000d..869de256 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/MapUpdater.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/MapUpdater.kt @@ -93,15 +93,7 @@ internal val NoPadding = PaddingValues() @SuppressLint("MissingPermission") @Suppress("NOTHING_TO_INLINE") @Composable -internal inline fun MapUpdater( - mergeDescendants: Boolean = false, - contentDescription: String?, - cameraPositionState: CameraPositionState, - contentPadding: PaddingValues = NoPadding, - locationSource: LocationSource?, - mapProperties: MapProperties, - mapUiSettings: MapUiSettings, -) { +internal inline fun MapUpdater(mapUpdaterState: MapUpdaterState) = with(mapUpdaterState) { val map = (currentComposer.applier as MapApplier).map val mapView = (currentComposer.applier as MapApplier).mapView if (mergeDescendants) { From cff3b49d9e1857436b44f61ba74c81a5c3ff9270 Mon Sep 17 00:00:00 2001 From: Philip S Date: Fri, 10 May 2024 13:31:12 +0200 Subject: [PATCH 100/103] Revert MapClickListeners change + fix parameter order convention --- .../google/maps/android/compose/GoogleMap.kt | 19 +++++++++---------- .../google/maps/android/compose/MapApplier.kt | 1 + .../maps/android/compose/MapClickListeners.kt | 5 ++++- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 529fa735..89d3a3f0 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -185,9 +185,9 @@ public fun GoogleMap( subcompositionJob = parentCompositionScope.launchSubcomposition( mapUpdaterState, parentComposition, + mapView, mapClickListeners, currentContent, - mapView, ) } } @@ -201,15 +201,15 @@ public fun GoogleMap( private fun CoroutineScope.launchSubcomposition( mapUpdaterState: MapUpdaterState, parentComposition: CompositionContext, - clickListeners: MapClickListeners, - content: (@Composable @GoogleMapComposable () -> Unit)?, - mapView: MapView + mapView: MapView, + mapClickListeners: MapClickListeners, + content: @Composable @GoogleMapComposable () -> Unit, ): Job { // Use [CoroutineStart.UNDISPATCHED] to kick off GoogleMap loading immediately return launch(start = CoroutineStart.UNDISPATCHED) { val map = mapView.awaitMap() val composition = Composition( - applier = MapApplier(map, mapView), + applier = MapApplier(map, mapView, mapClickListeners), parent = parentComposition ) @@ -217,13 +217,12 @@ private fun CoroutineScope.launchSubcomposition( composition.setContent { MapUpdater(mapUpdaterState) - MapClickListenerUpdater(clickListeners) + MapClickListenerUpdater() CompositionLocalProvider( - LocalCameraPositionState provides currentCameraPositionState - ) { - content?.invoke() - } + LocalCameraPositionState provides currentCameraPositionState, + content + ) } awaitCancellation() } finally { 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 b7256cdd..36e488b0 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 @@ -39,6 +39,7 @@ private object MapNodeRoot : MapNode internal class MapApplier( val map: GoogleMap, internal val mapView: MapView, + val mapClickListeners: MapClickListeners, ) : AbstractApplier(MapNodeRoot) { private val decorations = mutableListOf() diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/MapClickListeners.kt b/maps-compose/src/main/java/com/google/maps/android/compose/MapClickListeners.kt index 6ac1b860..42b3ee6e 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/MapClickListeners.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/MapClickListeners.kt @@ -86,7 +86,10 @@ internal class MapClickListenerNode( @Suppress("ComplexRedundantLet") @Composable -internal fun MapClickListenerUpdater(mapClickListeners: MapClickListeners) { +internal fun MapClickListenerUpdater() { + // The mapClickListeners container object is not allowed to ever change + val mapClickListeners = (currentComposer.applier as MapApplier).mapClickListeners + with(mapClickListeners) { ::indoorStateChangeListener.let { callback -> MapClickListenerComposeNode( From a917ed0e10fe411977d0676a8859fd930d7a4ff1 Mon Sep 17 00:00:00 2001 From: Philip S Date: Fri, 10 May 2024 13:33:59 +0200 Subject: [PATCH 101/103] Use `mapUpdaterState.cameraPositionState` instead of `currentCameraPositionState` --- .../src/main/java/com/google/maps/android/compose/GoogleMap.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 89d3a3f0..f29fac15 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -220,7 +220,7 @@ private fun CoroutineScope.launchSubcomposition( MapClickListenerUpdater() CompositionLocalProvider( - LocalCameraPositionState provides currentCameraPositionState, + LocalCameraPositionState provides mapUpdaterState.cameraPositionState, content ) } From e8bf202c85e0692d2db4dc75b574d7c368999865 Mon Sep 17 00:00:00 2001 From: Philip Date: Fri, 5 Jul 2024 00:19:28 +0200 Subject: [PATCH 102/103] Minor fixes --- app/src/main/AndroidManifest.xml | 6 +++--- .../main/java/com/google/maps/android/compose/GoogleMap.kt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e87653e5..925e2f3e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -49,10 +49,10 @@ android:name=".MapInColumnActivity" android:exported="false"/> + android:name=".MapsInLazyColumnActivity" + android:exported="false"/> Date: Tue, 9 Jul 2024 17:25:32 +0200 Subject: [PATCH 103/103] Update app/src/main/AndroidManifest.xml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Enrique López Mañas --- app/src/main/AndroidManifest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 925e2f3e..0063f72d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -52,7 +52,7 @@ android:name=".MapsInLazyColumnActivity" android:exported="false"/>