diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index d61d190f2..7c5e4b808 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -79,6 +79,7 @@ maps-utils-core = { module = "com.google.maps.android:android-maps-utils-core",
compose-multiplatform-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "composeMultiplatform" }
compose-multiplatform-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "composeMultiplatform" }
compose-multiplatform-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "composeMultiplatform" }
+maps-utils-kmp-core = { module = "com.google.maps.android:android-maps-utils-core", version.ref = "mapsutilsKmp" }
maps-utils-kmp-clustering = { module = "com.google.maps.android:android-maps-utils-clustering", version.ref = "mapsutilsKmp" }
maps-utils-kmp-maps-model = { module = "com.google.maps.android:android-maps-utils-maps-model", version.ref = "mapsutilsKmp" }
maps-utils = { module = "com.google.maps.android:android-maps-utils", version.ref = "mapsutils" }
diff --git a/iosApp/create_project.rb b/iosApp/create_project.rb
index b116dc228..71a224e0f 100644
--- a/iosApp/create_project.rb
+++ b/iosApp/create_project.rb
@@ -45,7 +45,10 @@
API_KEY="YOUR_API_KEY"
if [ -f "$SECRETS_PATH" ]; then
- EXTRACTED_KEY=$(grep -E "^MAPS_API_KEY=" "$SECRETS_PATH" | cut -d'=' -f2 | tr -d '"' | tr -d "'")
+ EXTRACTED_KEY=$(grep -E "^IOS_MAPS_API_KEY=" "$SECRETS_PATH" | cut -d'=' -f2 | tr -d '"' | tr -d "'")
+ if [ -z "$EXTRACTED_KEY" ]; then
+ EXTRACTED_KEY=$(grep -E "^MAPS_API_KEY=" "$SECRETS_PATH" | cut -d'=' -f2 | tr -d '"' | tr -d "'")
+ fi
if [ ! -z "$EXTRACTED_KEY" ]; then
API_KEY="$EXTRACTED_KEY"
fi
diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist
index 37d0d4e6c..cd135cb65 100644
--- a/iosApp/iosApp/Info.plist
+++ b/iosApp/iosApp/Info.plist
@@ -18,6 +18,8 @@
1.0
CFBundleVersion
1
+ NSLocationWhenInUseUsageDescription
+ Demonstrates multiplatform LocationSource Flow on Google Maps.
LSRequiresIPhoneOS
UILaunchScreen
diff --git a/maps-compose-multiplatform-demo/build.gradle.kts b/maps-compose-multiplatform-demo/build.gradle.kts
index 549f03992..be4450b07 100644
--- a/maps-compose-multiplatform-demo/build.gradle.kts
+++ b/maps-compose-multiplatform-demo/build.gradle.kts
@@ -56,5 +56,10 @@ kotlin {
implementation(libs.compose.multiplatform.ui)
}
}
+ androidMain {
+ dependencies {
+ implementation(libs.androidx.activity.compose)
+ }
+ }
}
}
diff --git a/maps-compose-multiplatform-demo/src/androidMain/AndroidManifest.xml b/maps-compose-multiplatform-demo/src/androidMain/AndroidManifest.xml
new file mode 100644
index 000000000..c6f8711f0
--- /dev/null
+++ b/maps-compose-multiplatform-demo/src/androidMain/AndroidManifest.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
diff --git a/maps-compose-multiplatform-demo/src/androidMain/kotlin/com/google/maps/android/compose/multiplatform/demo/PlatformLocationSource.android.kt b/maps-compose-multiplatform-demo/src/androidMain/kotlin/com/google/maps/android/compose/multiplatform/demo/PlatformLocationSource.android.kt
new file mode 100644
index 000000000..1cfd9ec2a
--- /dev/null
+++ b/maps-compose-multiplatform-demo/src/androidMain/kotlin/com/google/maps/android/compose/multiplatform/demo/PlatformLocationSource.android.kt
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.maps.android.compose.multiplatform.demo
+
+import android.Manifest
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.Context
+import android.content.ContextWrapper
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.location.LocationManager
+import android.net.Uri
+import android.provider.Settings
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.LocalContext
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import com.google.maps.android.location.LocationPriority
+import com.google.maps.android.location.LocationSource
+import com.google.maps.android.location.asLocationSource
+import com.google.maps.android.location.coarseLocationEvents
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.channelFlow
+import kotlinx.coroutines.flow.merge
+
+private val LOCATION_PERMISSIONS = arrayOf(
+ Manifest.permission.ACCESS_FINE_LOCATION,
+ Manifest.permission.ACCESS_COARSE_LOCATION,
+)
+
+private fun Context.hasAnyLocationPermission(): Boolean =
+ checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
+ checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
+
+private fun Context.hasFineLocationPermission(): Boolean =
+ checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
+
+private fun Activity.shouldShowLocationRationale(): Boolean =
+ shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) ||
+ shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_COARSE_LOCATION)
+
+private tailrec fun Context.findActivity(): Activity? = when (this) {
+ is Activity -> this
+ is ContextWrapper -> baseContext.findActivity()
+ else -> null
+}
+
+@SuppressLint("MissingPermission")
+@Composable
+internal actual fun rememberPlatformLocationController(): PlatformLocationController {
+ val context = LocalContext.current
+ val appContext = context.applicationContext
+ val activity = remember(context) { context.findActivity() }
+ val lifecycleOwner = LocalLifecycleOwner.current
+ var hasPermission by remember { mutableStateOf(appContext.hasAnyLocationPermission()) }
+
+ // Re-check location permission whenever the host lifecycle resumes (covering both the
+ // system permission dialog closing and returning from Android App Settings).
+ DisposableEffect(lifecycleOwner, appContext) {
+ val observer = LifecycleEventObserver { _, event ->
+ if (event == Lifecycle.Event.ON_RESUME) {
+ hasPermission = appContext.hasAnyLocationPermission()
+ }
+ }
+ lifecycleOwner.lifecycle.addObserver(observer)
+ onDispose {
+ lifecycleOwner.lifecycle.removeObserver(observer)
+ }
+ }
+
+ // TODO(https://github.com/googlemaps/android-maps-utils/issues/1818): Once
+ // LocationManager.asLocationSource() handles cached lastKnownLocation emission and
+ // coarse-permission/indoor fallback directly in android-maps-utils, simplify this to
+ // `remember(manager) { manager.asLocationSource() }` matching PlatformLocationSource.ios.kt.
+ val locationSource = remember(appContext) {
+ val manager = appContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
+ LocationSource { intervalMs, minUpdateDistanceM, priority ->
+ channelFlow {
+ if (!appContext.hasAnyLocationPermission()) {
+ return@channelFlow
+ }
+
+ // Safely query cached locations across providers; wrap each call in runCatching
+ // so that coarse-only ("Approximate") permission or a mid-flight revocation
+ // never throws an unhandled SecurityException.
+ val lastKnown = runCatching {
+ manager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
+ }.getOrNull()
+ ?: runCatching {
+ manager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)
+ }.getOrNull()
+ ?: runCatching {
+ manager.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER)
+ }.getOrNull()
+
+ if (lastKnown != null) {
+ send(lastKnown)
+ }
+
+ val stream = if (appContext.hasFineLocationPermission() &&
+ priority == LocationPriority.HIGH_ACCURACY
+ ) {
+ merge(
+ manager.asLocationSource().locationEvents(
+ intervalMs = intervalMs,
+ minUpdateDistanceM = minUpdateDistanceM,
+ priority = LocationPriority.HIGH_ACCURACY,
+ ).catch { if (it !is SecurityException) throw it },
+ manager.coarseLocationEvents(
+ minTimeMs = intervalMs,
+ minDistanceM = minUpdateDistanceM,
+ ).catch { if (it !is SecurityException) throw it },
+ )
+ } else {
+ manager.coarseLocationEvents(
+ minTimeMs = intervalMs,
+ minDistanceM = minUpdateDistanceM,
+ ).catch { if (it !is SecurityException) throw it }
+ }
+
+ stream.collect { send(it) }
+ }
+ }
+ }
+
+ val openAppSettings: () -> Unit = remember(appContext) {
+ {
+ val intent = Intent(
+ Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
+ Uri.fromParts("package", appContext.packageName, null),
+ ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ appContext.startActivity(intent)
+ }
+ }
+
+ var rationaleBeforeRequest by remember { mutableStateOf(false) }
+
+ val permissionLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.RequestMultiplePermissions(),
+ ) { permissions ->
+ val granted = permissions.values.any { it } || appContext.hasAnyLocationPermission()
+ hasPermission = granted
+ if (!granted) {
+ val rationaleAfterRequest = activity?.shouldShowLocationRationale() ?: false
+ // If shouldShowRequestPermissionRationale was false before the request and remains
+ // false after the request was denied, Android suppressed the permission dialog
+ // because the permission is permanently denied ("Don't ask again" / USER_FIXED).
+ if (!rationaleBeforeRequest && !rationaleAfterRequest) {
+ openAppSettings()
+ }
+ }
+ }
+
+ val requestPermission: () -> Unit = remember(activity, permissionLauncher, openAppSettings) {
+ {
+ if (activity == null) {
+ openAppSettings()
+ } else {
+ rationaleBeforeRequest = activity.shouldShowLocationRationale()
+ permissionLauncher.launch(LOCATION_PERMISSIONS)
+ }
+ }
+ }
+
+ return remember(locationSource, hasPermission, requestPermission, openAppSettings) {
+ PlatformLocationController(
+ locationSource = locationSource,
+ hasPermission = hasPermission,
+ requestPermission = requestPermission,
+ openAppSettings = openAppSettings,
+ )
+ }
+}
diff --git a/maps-compose-multiplatform-demo/src/commonMain/kotlin/com/google/maps/android/compose/multiplatform/demo/DemoApp.kt b/maps-compose-multiplatform-demo/src/commonMain/kotlin/com/google/maps/android/compose/multiplatform/demo/DemoApp.kt
index 625e1f956..80d365b74 100644
--- a/maps-compose-multiplatform-demo/src/commonMain/kotlin/com/google/maps/android/compose/multiplatform/demo/DemoApp.kt
+++ b/maps-compose-multiplatform-demo/src/commonMain/kotlin/com/google/maps/android/compose/multiplatform/demo/DemoApp.kt
@@ -73,6 +73,9 @@ public val multiplatformDemos: List = listOf(
MultiplatformDemo("Map clicks", "Tap to drop a marker, long press to clear them") {
MapClicksDemo()
},
+ MultiplatformDemo("Location Flow", "Live device GPS streamed via KMP LocationSource Flow") {
+ LocationFlowDemo()
+ },
)
/**
diff --git a/maps-compose-multiplatform-demo/src/commonMain/kotlin/com/google/maps/android/compose/multiplatform/demo/Demos.kt b/maps-compose-multiplatform-demo/src/commonMain/kotlin/com/google/maps/android/compose/multiplatform/demo/Demos.kt
index 2345a4320..ef2450782 100644
--- a/maps-compose-multiplatform-demo/src/commonMain/kotlin/com/google/maps/android/compose/multiplatform/demo/Demos.kt
+++ b/maps-compose-multiplatform-demo/src/commonMain/kotlin/com/google/maps/android/compose/multiplatform/demo/Demos.kt
@@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
@@ -47,6 +48,13 @@ import com.google.maps.android.compose.multiplatform.Polyline
import com.google.maps.android.compose.multiplatform.cameraPosition
import com.google.maps.android.compose.multiplatform.rememberCameraPositionState
import com.google.maps.android.compose.multiplatform.rememberMarkerState
+import com.google.maps.android.location.Location
+import com.google.maps.android.location.LocationPriority
+import com.google.maps.android.location.accuracyMeters
+import com.google.maps.android.location.latLng
+import com.google.maps.android.location.latitude
+import com.google.maps.android.location.locationEvents
+import com.google.maps.android.location.longitude
import com.google.maps.android.model.LatLng
import com.google.maps.android.model.latitude
import com.google.maps.android.model.longitude
@@ -272,3 +280,97 @@ internal fun MapClicksDemo() {
private fun Double.format(): String = (round(this * 10_000) / 10_000).toString()
private fun Boolean.onOff(): String = if (this) "on" else "off"
+
+
+private const val MAX_TRAIL_POINTS = 500
+
+@Composable
+internal fun LocationFlowDemo() {
+ val controller = rememberPlatformLocationController()
+ val camera = rememberCameraPositionState { position = cameraPosition(SanFrancisco, 14f) }
+ val markerState = rememberMarkerState(SanFrancisco)
+ var latestLocation by remember { mutableStateOf(null) }
+ var fixCount by remember { mutableIntStateOf(0) }
+ var priority by remember { mutableStateOf(LocationPriority.HIGH_ACCURACY) }
+ val trail = remember { mutableStateListOf() }
+
+ LaunchedEffect(controller.locationSource, priority, controller.hasPermission) {
+ if (!controller.hasPermission) {
+ latestLocation = null
+ return@LaunchedEffect
+ }
+ controller.locationSource.locationEvents(
+ intervalMs = 1_000L,
+ minUpdateDistanceM = 0f,
+ priority = priority,
+ ).collect { location ->
+ latestLocation = location
+ fixCount++
+ val target = location.latLng
+ if (trail.lastOrNull() != target) {
+ if (trail.size >= MAX_TRAIL_POINTS) {
+ trail.removeAt(0)
+ }
+ trail += target
+ }
+ markerState.position = target
+ camera.animate(cameraPosition(target, 15f))
+ }
+ }
+
+ Box(Modifier.fillMaxSize()) {
+ GoogleMap(Modifier.fillMaxSize(), cameraPositionState = camera) {
+ val loc = latestLocation
+ if (loc != null && controller.hasPermission) {
+ Marker(
+ state = markerState,
+ title = "Live Location (KMP Flow)",
+ snippet = "Fix #$fixCount (${loc.latitude.format()}, ${loc.longitude.format()})",
+ )
+ val accuracyRadius = loc.accuracyMeters.toDouble().coerceAtLeast(25.0)
+ Circle(
+ center = loc.latLng,
+ radius = accuracyRadius,
+ fillColor = Color(0x441A73E8),
+ strokeColor = Accent,
+ strokeWidth = 3f,
+ )
+ if (trail.size >= 2) {
+ Polyline(
+ points = trail.toList(),
+ color = Accent,
+ width = 8f,
+ )
+ }
+ }
+ }
+ DemoControls(Modifier.align(Alignment.TopStart)) {
+ if (!controller.hasPermission) {
+ DemoLabel(
+ "Location permission required.\n" +
+ "Grant permission to stream live LocationSource updates.",
+ )
+ Spacer(Modifier.height(8.dp))
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ DemoButton("Grant Permission") { controller.requestPermission() }
+ DemoButton("App Settings") { controller.openAppSettings() }
+ }
+ } else {
+ val loc = latestLocation
+ if (loc == null) {
+ DemoLabel("Waiting for LocationSource.locationEvents()...")
+ } else {
+ DemoLabel(
+ "Flow Fix #$fixCount: ${loc.latitude.format()}, ${loc.longitude.format()}\n" +
+ "Accuracy: ${loc.accuracyMeters.toDouble().format()}m ยท Mode: ${priority.name}",
+ )
+ }
+ Spacer(Modifier.height(8.dp))
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ DemoButton("High Accuracy") { priority = LocationPriority.HIGH_ACCURACY }
+ DemoButton("Balanced") { priority = LocationPriority.BALANCED_POWER_ACCURACY }
+ }
+ }
+ }
+ }
+}
diff --git a/maps-compose-multiplatform-demo/src/commonMain/kotlin/com/google/maps/android/compose/multiplatform/demo/PlatformLocationSource.kt b/maps-compose-multiplatform-demo/src/commonMain/kotlin/com/google/maps/android/compose/multiplatform/demo/PlatformLocationSource.kt
new file mode 100644
index 000000000..7f24694f5
--- /dev/null
+++ b/maps-compose-multiplatform-demo/src/commonMain/kotlin/com/google/maps/android/compose/multiplatform/demo/PlatformLocationSource.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.maps.android.compose.multiplatform.demo
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import com.google.maps.android.location.LocationSource
+
+/**
+ * Holds a multiplatform [LocationSource] alongside observable runtime permission state and
+ * platform callbacks to request permission or open system application settings.
+ *
+ * Exposing permission state alongside [locationSource] allows shared `commonMain` UI to react
+ * gracefully when the user denies or revokes location permission at runtime, rather than crashing
+ * or hanging indefinitely.
+ */
+@Stable
+internal class PlatformLocationController(
+ val locationSource: LocationSource,
+ val hasPermission: Boolean,
+ val requestPermission: () -> Unit,
+ val openAppSettings: () -> Unit,
+)
+
+/**
+ * Remembers a platform-backed [PlatformLocationController] (`LocationManager` on Android,
+ * `CLLocationManager` on iOS) that tracks runtime location permissions and streams device GPS
+ * fixes into shared `commonMain` UI via [LocationSource.locationEvents].
+ */
+@Composable
+internal expect fun rememberPlatformLocationController(): PlatformLocationController
diff --git a/maps-compose-multiplatform-demo/src/iosMain/kotlin/com/google/maps/android/compose/multiplatform/demo/PlatformLocationSource.ios.kt b/maps-compose-multiplatform-demo/src/iosMain/kotlin/com/google/maps/android/compose/multiplatform/demo/PlatformLocationSource.ios.kt
new file mode 100644
index 000000000..db969be90
--- /dev/null
+++ b/maps-compose-multiplatform-demo/src/iosMain/kotlin/com/google/maps/android/compose/multiplatform/demo/PlatformLocationSource.ios.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.maps.android.compose.multiplatform.demo
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import com.google.maps.android.location.asLocationSource
+import platform.CoreLocation.CLAuthorizationStatus
+import platform.CoreLocation.CLLocationManager
+import platform.CoreLocation.CLLocationManagerDelegateProtocol
+import platform.CoreLocation.kCLAuthorizationStatusAuthorizedAlways
+import platform.CoreLocation.kCLAuthorizationStatusAuthorizedWhenInUse
+import platform.Foundation.NSNotificationCenter
+import platform.Foundation.NSURL
+import platform.UIKit.UIApplication
+import platform.UIKit.UIApplicationDidBecomeActiveNotification
+import platform.UIKit.UIApplicationOpenSettingsURLString
+import platform.darwin.NSObject
+
+private fun CLAuthorizationStatus.isAuthorized(): Boolean =
+ this == kCLAuthorizationStatusAuthorizedWhenInUse ||
+ this == kCLAuthorizationStatusAuthorizedAlways
+
+@Composable
+internal actual fun rememberPlatformLocationController(): PlatformLocationController {
+ val manager = remember { CLLocationManager() }
+ var hasPermission by remember { mutableStateOf(manager.authorizationStatus.isAuthorized()) }
+
+ // Observe CoreLocation authorization changes and app foreground activation so returning
+ // from the system permission dialog or iOS Settings updates hasPermission without polling.
+ DisposableEffect(manager) {
+ val authObserver = CLLocationManager()
+ val authDelegate = object : NSObject(), CLLocationManagerDelegateProtocol {
+ override fun locationManagerDidChangeAuthorization(manager: CLLocationManager) {
+ hasPermission = manager.authorizationStatus.isAuthorized()
+ }
+ }
+ authObserver.delegate = authDelegate
+ val notificationObserver = NSNotificationCenter.defaultCenter.addObserverForName(
+ name = UIApplicationDidBecomeActiveNotification,
+ `object` = null,
+ queue = null,
+ ) { _ ->
+ hasPermission = manager.authorizationStatus.isAuthorized()
+ }
+ onDispose {
+ // Keep authDelegate referenced until onDispose because CLLocationManager.delegate is weak.
+ if (authObserver.delegate === authDelegate) {
+ authObserver.delegate = null
+ }
+ NSNotificationCenter.defaultCenter.removeObserver(notificationObserver)
+ }
+ }
+
+ val locationSource = remember(manager) { manager.asLocationSource() }
+
+ val requestPermission: () -> Unit = remember(manager) {
+ { manager.requestWhenInUseAuthorization() }
+ }
+
+ val openAppSettings: () -> Unit = remember {
+ {
+ val url = NSURL.URLWithString(UIApplicationOpenSettingsURLString)
+ if (url != null) {
+ UIApplication.sharedApplication.openURL(
+ url,
+ options = emptyMap(),
+ completionHandler = null,
+ )
+ }
+ }
+ }
+
+ return remember(locationSource, hasPermission, requestPermission, openAppSettings) {
+ PlatformLocationController(
+ locationSource = locationSource,
+ hasPermission = hasPermission,
+ requestPermission = requestPermission,
+ openAppSettings = openAppSettings,
+ )
+ }
+}
diff --git a/maps-compose-multiplatform/build.gradle.kts b/maps-compose-multiplatform/build.gradle.kts
index b8b5ebc2b..98063e37a 100644
--- a/maps-compose-multiplatform/build.gradle.kts
+++ b/maps-compose-multiplatform/build.gradle.kts
@@ -72,6 +72,7 @@ kotlin {
// a snapshot (see settings.gradle.kts). It shares its artifactId with the AAR that
// maps-compose-utils pulls from Maven Central, so the two resolve to one module.
api(libs.maps.utils.kmp.maps.model)
+ api(libs.maps.utils.kmp.core)
api(libs.maps.utils.kmp.clustering)
}
}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 172204816..c498fd014 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -19,7 +19,11 @@ dependencyResolutionManagement {
repositories {
if (providers.gradleProperty("useMavenLocal").orNull == "true" ||
providers.environmentVariable("USE_MAVEN_LOCAL").orNull == "true") {
- mavenLocal()
+ mavenLocal {
+ mavenContent {
+ includeGroup("com.google.maps.android")
+ }
+ }
}
google()
mavenCentral()