Skip to content

chore(kmp-demo): add shared multiplatform Location Flow demo - #1038

Open
dkhawk wants to merge 16 commits into
feat/experimental-kmp-modulefrom
feat/kmp-location-flow-demo
Open

dkhawk wants to merge 16 commits into
feat/experimental-kmp-modulefrom
feat/kmp-location-flow-demo

Conversation

@dkhawk

@dkhawk dkhawk commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a shared Location Flow demo (LocationFlowDemo) to :maps-compose-multiplatform-demo exercising the multiplatform Location, LocationSource, and LocationPriority APIs from googlemaps/android-maps-utils#1817 across Android and iOS:

  • Dependency Wiring (gradle/libs.versions.toml, maps-compose-multiplatform/build.gradle.kts, settings.gradle.kts):
    • Adds maps-utils-kmp-core (com.google.maps.android:android-maps-utils-core) and exposes it via api(libs.maps.utils.kmp.core) in :maps-compose-multiplatform commonMain.
    • Scopes mavenLocal() in settings.gradle.kts (when useMavenLocal=true) to includeGroup("com.google.maps.android") so local KMP snapshot testing does not conflict with external dependencies.
  • Platform Location & Permission Controller (PlatformLocationSource.kt, PlatformLocationSource.android.kt, PlatformLocationSource.ios.kt):
    • Implements rememberPlatformLocationController() backed by LocationManager on Android and CLLocationManager.asLocationSource() on iOS, tracking runtime location permission state and providing permission request / system settings actions.
  • Shared LocationFlowDemo (DemoApp.kt, Demos.kt):
    • Collects LocationSource.locationEvents() in commonMain to animate the camera, update a live Marker, render a horizontal accuracy Circle, and draw a breadcrumb Polyline trail, with interactive High Accuracy / Balanced priority switching and runtime permission recovery controls.
  • App Manifests & Xcode Project Generation (maps-app/src/main/AndroidManifest.xml, iosApp/iosApp/Info.plist, iosApp/create_project.rb):
    • Declares ACCESS_COARSE_LOCATION and ACCESS_FINE_LOCATION in maps-app and NSLocationWhenInUseUsageDescription in iosApp.
    • Updates iosApp/create_project.rb to check IOS_MAPS_API_KEY in secrets.properties before falling back to MAPS_API_KEY.

Note: Depends on googlemaps/android-maps-utils#1817 being merged and published to 6.1.0-SNAPSHOT for CI resolution.

Testing

  • Verified live on physical Android device (Pixel 6) and physical iOS device (iPhone 14) as well as iOS Simulator.

kikoso and others added 13 commits October 1, 2026 14:49
…anch

Adds common-code clustering to maps-compose-multiplatform via the
experimental KMP modules of android-maps-utils (maps-model, clustering):
clusterMarkers() runs NonHierarchicalDistanceBasedAlgorithm in
commonMain and ClusteredGoogleMap renders the resulting clusters, so
Android and iOS produce identical clusters from shared code.

Consumption paths (see settings.gradle.kts): publishToMavenLocal from
../android-maps-utils plus -PuseMavenLocal=true (full build works, this
is the default), or -PuseLocalMapsUtils=true for a composite build
(platform compilations work; shared-metadata compilation is skipped by
Kotlin's granular-metadata transform across included builds and fails).

Note: linkPodDebugFrameworkIosArm64 fails on this branch with or without
this change (Kotlin/Native cache failure on compose-ui 1.7.3
uikitarm64); simulator link and all target compilations pass.

Claude-Session: https://claude.ai/code/session_01225X6MnAqkyCF7Xones6WY
The maps-compose-multiplatform module now depends on
com.google.maps.android:maps-model and :clustering from the
feat/experimental-kmp-clustering branch of android-maps-utils, which are
not published to any remote repository. Check out that branch and
publish the two modules to the runner's mavenLocal in the lint, test and
instrumentation workflows, and set USE_MAVEN_LOCAL so the compose build
resolves them from there.

Claude-Session: https://claude.ai/code/session_01225X6MnAqkyCF7Xones6WY
com.google.maps.android:clustering duplicated the classes of the
android-maps-utils-clustering AAR that maps-compose-utils pulls from
Maven Central, failing maps-app's checkDebugDuplicateClasses. Depend on
android-maps-utils-clustering / android-maps-utils-maps-model instead
(matching the renamed KMP publications) so Gradle resolves both paths to
one module; mavenLocal shadows the Central AAR with the API-compatible
KMP artifact.

Claude-Session: https://claude.ai/code/session_01225X6MnAqkyCF7Xones6WY
…OS CI

- KmpPublishingConventionPlugin: explicit API mode, iosArm64 and
  iosSimulatorArm64, lint, Kover and Maven Central publishing as
  maps-compose-multiplatform
- Public API checks for the iOS klibs (Kotlin ABI validation) and the
  Android target (binary-compatibility-validator), in api/
- Publishing a release fails while it depends on a maps-utils snapshot
- New macOS workflow builds the iOS targets and the iOS demo app
- The publish workflow runs on macOS, which the iOS klibs need
…ment the KMP build

- commonTest for clusterMarkers, run as Android host tests and on the
  iOS simulator
- iosApp/README.md replaces BUILD_KMP.md; the Xcode project is generated
  by create_project.rb and no longer checked in
- AGENTS.md describes the multiplatform module, its API checks and the
  snapshot dependency
…-multiplatform

Replaces the prototype API with one that follows maps-compose:

- GoogleMap(cameraPositionState, properties, uiSettings, onMapClick,
  onMapLongClick, onMapLoaded) { content }
- CameraPositionState follows the map both ways and can animate
- Marker with MarkerState (draggable, tappable, tinted pin), Polyline,
  Polygon with holes and Circle; each keeps one native object per call
  instead of clearing the map on every update
- Clustering reclusters as the camera zooms and zooms in on cluster taps
- Line widths are density-independent on both platforms

Demos move to maps-compose-multiplatform-demo, shared by maps-app and the
iOS app, which now hosts them instead of a Swift list. Tests cover the
camera state, content registration and clustering on the JVM and iOS.
@dkhawk
dkhawk requested a review from kikoso October 9, 2026 23:02
@github-actions

Copy link
Copy Markdown

Coverage (unit tests)

Compared against 85a56aa on feat/experimental-kmp-module (#1034).

Module Line % Change Branch % Change
maps-compose 0.00% no change 0.00% no change
maps-compose-multiplatform 61.63% new 26.40% new
maps-compose-utils 2.04% no change 0.49% no change
maps-compose-widgets 0.00% no change 0.00% no change
TOTAL 8.54% 🔺 +8.13 3.17% 🔺 +3.08

Line and branch coverage from unit test reports. History is recorded on the coverage-history branch after each merge to feat/experimental-kmp-module.

@googlemaps-bot

Copy link
Copy Markdown
Contributor

Code Coverage

Overall Project 44.37% 🍏

There is no coverage information present for the Files changed

@kikoso kikoso left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice demo, @dkhawk ! A few comments, mostly about the permission handling. Also, since this only touches the demo app, let's mark it as a chore instead of feat, to avoid releasing a new version.

Comment on lines +71 to +76
LaunchedEffect(appContext) {
while (true) {
hasPermission = appContext.hasAnyLocationPermission()
delay(500L)
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Polling the permission every 500ms works, but it keeps a loop running for as long as the demo is on screen. Could we re-check it on ON_RESUME with a LifecycleEventObserver instead? That covers both the system dialog and coming back from Settings. What do you think?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call! Replaced the 500ms polling loop in PlatformLocationSource.android.kt with a LifecycleEventObserver in DisposableEffect(lifecycleOwner, appContext) that re-checks permission on Lifecycle.Event.ON_RESUME (and also replaced the polling loop on iOS with CLLocationManagerDelegateProtocol.locationManagerDidChangeAuthorization + UIApplicationDidBecomeActiveNotification).

Comment on lines +160 to +164
Handler(Looper.getMainLooper()).postDelayed({
if (!appContext.hasAnyLocationPermission() && activity.hasWindowFocus()) {
openAppSettings()
}
}, 300L)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am thinking out loud if the 300ms window focus heuristic could misfire on slower devices, where the permission dialog takes longer to appear and we would open Settings on top of it. Would it make sense to use shouldShowRequestPermissionRationale (or an ActivityResultLauncher) to detect the permanently denied case instead?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed—switched to rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) combined with checking shouldShowRequestPermissionRationale before and after the request (!rationaleBeforeRequest && !rationaleAfterRequest) so permanently denied permissions are detected deterministically in the result callback without any timer or window-focus heuristic.

Comment thread maps-app/src/main/AndroidManifest.xml Outdated
Comment on lines +20 to +21
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to understand it: do we need the location permissions in maps-app, or is this demo only launched from the multiplatform demo module? If maps-app does not use location, we could keep these permissions out of its manifest.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Android, :maps-compose-multiplatform-demo is an Android KMP library module hosted by KmpMapActivity inside :maps-app. I reverted maps-app/src/main/AndroidManifest.xml and moved the <uses-permission> declarations into maps-compose-multiplatform-demo/src/androidMain/AndroidManifest.xml so maps-app's own manifest stays untouched while AGP's manifest merger still contributes the permissions for KmpMapActivity.

Comment on lines +308 to +310
if (trail.lastOrNull() != target) {
trail += target
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the trail grows without limit while the demo is open. For a demo it is probably fine, but should we cap it (e.g. last 500 points) to avoid an ever growing polyline?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done—added MAX_TRAIL_POINTS = 500 in Demos.kt to cap the trail list at the most recent 500 points.

@kikoso
kikoso force-pushed the feat/experimental-kmp-module branch from cda70c2 to 54c5bb8 Compare October 12, 2026 04:36
@kikoso
kikoso requested a review from a team as a code owner October 12, 2026 04:36
@dkhawk dkhawk changed the title feat(kmp-demo): add shared multiplatform Location Flow demo chore(kmp-demo): add shared multiplatform Location Flow demo Oct 13, 2026
@googlemaps-bot

Copy link
Copy Markdown
Contributor

Coverage (instrumentation tests)

Overall Project 44.41% 🍏

There is no coverage information present for the Files changed

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants