Skip to content

Commit bcdf6b0

Browse files
committed
feat(navigation-app): Add Navigation support to Maps Compose
Adds support for the Navigation SDK to Maps Compose. It just allows the user to replace a standard MapView with a NavigationView within a GoogleMap composable. The following changes were made: Added a new NavigationViewDelegate class to handle the integration between NavigationView and Maps Compose. Added a new NavigationScreen composable function to display the navigation view. Added a new MovableMarker composable function to display a draggable marker on the map. Updated the GoogleMap composable function to support the use of NavigationView. Added a new NavigationApplication class to initialize the Places SDK. Added a new ApiKeyProvider class to provide API keys for the Maps and Places SDKs. Added a new LocationProvider class to provide location data. Added a new PermissionChecker class to check for location permissions. Updated the build.gradle.kts files to include the necessary dependencies. Updated the local.defaults.properties file to include the Places API key.
1 parent 5424095 commit bcdf6b0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1104
-23
lines changed

build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ plugins {
1919
alias(libs.plugins.dokka) apply true
2020
alias(libs.plugins.compose.compiler) apply false
2121
id("com.autonomousapps.dependency-analysis") version "2.0.0"
22+
alias(libs.plugins.android.application) apply false
23+
alias(libs.plugins.kotlin.android) apply false
2224

2325
}
2426

gradle/libs.versions.toml

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,35 @@
11
[versions]
2+
accompanistPermissions = "0.37.0"
23
activitycompose = "1.9.3"
34
agp = "8.7.2"
4-
androidxtest = "1.6.2"
5+
agpVersion = "8.7.2"
56
androidCore = "1.6.1"
7+
androidx-core = "1.15.0"
8+
androidxtest = "1.6.2"
69
compose-bom = "2024.11.00"
710
dokka = "1.9.20"
811
espresso = "3.6.1"
912
jacoco-plugin = "0.2.1"
10-
junitktx = "1.2.1"
1113
junit = "4.13.2"
14+
junitVersion = "1.2.1"
15+
junitktx = "1.2.1"
1216
kotlin = "2.0.21"
1317
kotlinxCoroutines = "1.9.0"
14-
mapsktx = "5.1.1"
18+
lifecycleRuntimeKtx = "2.8.7"
1519
mapsecrets = "2.0.1"
20+
mapsktx = "5.1.1"
21+
navigation = "6.0.0"
1622
org-jacoco-core = "0.8.11"
17-
androidx-core = "1.15.0"
23+
places = "4.1.0"
24+
playServicesLocation = "21.3.0"
25+
robolectric = "4.14.1"
1826
screenshot = "0.0.1-alpha08"
27+
secretsGradlePlugin = "2.0.1"
28+
truth = "1.4.4"
1929

2030
[libraries]
31+
# robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
32+
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
2133
android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "agp" }
2234
androidx-compose-activity = { module = "androidx.activity:activity-compose", version.ref = "activitycompose" }
2335
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" }
@@ -27,25 +39,39 @@ androidx-compose-ui = { module = "androidx.compose.ui:ui" }
2739
androidx-compose-ui-preview-tooling = { module = "androidx.compose.ui:ui-tooling-preview" }
2840
androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
2941
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
42+
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
43+
androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycleRuntimeKtx" }
44+
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
45+
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" }
46+
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
3047
androidx-test-compose-ui = { module = "androidx.compose.ui:ui-test-junit4" }
3148
androidx-test-core = { module = "androidx.test:core", version.ref = "androidCore" }
3249
androidx-test-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" }
3350
androidx-test-junit-ktx = { module = "androidx.test.ext:junit-ktx", version.ref = "junitktx" }
3451
androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidCore" }
3552
androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidxtest" }
53+
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
54+
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
3655
dokka-plugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" }
3756
jacoco-android-plugin = { module = "com.mxalbert.gradle:jacoco-android", version.ref = "jacoco-plugin", version.require = "0.2.1" }
3857
kotlin = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk7", version.ref = "kotlin" }
3958
kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
40-
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" }
4159
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" }
60+
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" }
4261
maps-ktx-std = { module = "com.google.maps.android:maps-ktx", version.ref = "mapsktx" }
4362
maps-ktx-utils = { module = "com.google.maps.android:maps-utils-ktx", version.ref = "mapsktx" }
4463
maps-secrets-plugin = { module = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin", version.ref = "mapsecrets" }
64+
navigation = { module = "com.google.android.libraries.navigation:navigation", version.ref = "navigation" }
4565
org-jacoco-core = { module = "org.jacoco:org.jacoco.core", version.ref = "org-jacoco-core" }
66+
places = { group = "com.google.android.libraries.places", name = "places", version.ref = "places" }
67+
play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "playServicesLocation" }
4668
test-junit = { module = "junit:junit", version.ref = "junit" }
69+
truth = { module = "com.google.truth:truth", version.ref = "truth" }
4770

4871
[plugins]
49-
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
72+
android-application = { id = "com.android.application", version.ref = "agpVersion" }
5073
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
51-
screenshot = { id = "com.android.compose.screenshot", version.ref = "screenshot"}
74+
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
75+
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
76+
screenshot = { id = "com.android.compose.screenshot", version.ref = "screenshot"}
77+
secrets-gradle-plugin = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secretsGradlePlugin" }

local.defaults.properties

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@
77
# Location of the SDK. This is only used by Gradle.
88
# For customization when using a Version Control System, please read the
99
# header note.
10-
MAPS_API_KEY=YOUR_API_KEY
10+
MAPS_API_KEY=YOUR_API_KEY
11+
PLACES_API_KEY=DEFAULT_API_KEY

maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import androidx.compose.runtime.rememberCoroutineScope
3636
import androidx.compose.runtime.rememberUpdatedState
3737
import androidx.compose.runtime.setValue
3838
import androidx.compose.ui.Modifier
39+
import androidx.compose.ui.platform.AbstractComposeView
3940
import androidx.compose.ui.platform.ComposeView
4041
import androidx.compose.ui.platform.LocalInspectionMode
4142
import androidx.compose.ui.viewinterop.AndroidView
@@ -149,6 +150,11 @@ public fun GoogleMap(
149150
var subcompositionJob by remember { mutableStateOf<Job?>(null) }
150151
val parentCompositionScope = rememberCoroutineScope()
151152

153+
var delegate by remember {
154+
// TODO: this could leak the view?
155+
mutableStateOf<AbstractMapViewDelegate<*>?>(null)
156+
}
157+
152158
AndroidView(
153159
modifier = modifier,
154160
factory = { context ->
@@ -157,6 +163,7 @@ public fun GoogleMap(
157163
} else {
158164
MapViewDelegate(MapView(context, googleMapOptionsFactory()))
159165
}.also { mapViewDelegate: AbstractMapViewDelegate<*> ->
166+
delegate = mapViewDelegate
160167
val mapView = mapViewDelegate.mapView
161168

162169
val componentCallbacks = object : ComponentCallbacks2 {
@@ -193,12 +200,11 @@ public fun GoogleMap(
193200
},
194201
onReset = { /* View is detached. */ },
195202
onRelease = { mapView ->
196-
mapView.toDelegate()
197-
198-
val (componentCallbacks, lifecycleObserver) = when {
199-
mapView is MapView -> mapView.tagData
200-
else -> TODO("not yet implemented!")
201-
}
203+
val (componentCallbacks, lifecycleObserver) = delegate!!.tagData
204+
// val (componentCallbacks, lifecycleObserver) = when {
205+
// mapView is MapView -> mapView.tagData
206+
// else -> TODO("not yet implemented!")
207+
// }
202208
mapView.context.unregisterComponentCallbacks(componentCallbacks)
203209
lifecycleObserver.moveToDestroyedState()
204210
mapView.tag = null
@@ -208,7 +214,7 @@ public fun GoogleMap(
208214
subcompositionJob = parentCompositionScope.launchSubcomposition(
209215
mapUpdaterState,
210216
parentComposition,
211-
mapView.toDelegate(),
217+
delegate!!, // TODO: not sure about this. Maybe just remember a factory method?
212218
mapClickListeners,
213219
currentContent,
214220
)
@@ -339,11 +345,23 @@ public interface AbstractMapViewDelegate<T : View> {
339345
public fun onLowMemory()
340346
public fun onDestroy()
341347
public suspend fun awaitMap(): GoogleMap
342-
public fun renderComposeViewOnce(view: ComposeView, parentContext: CompositionContext)
348+
public fun renderComposeViewOnce(
349+
view: AbstractComposeView,
350+
parentContext: CompositionContext,
351+
onAddedToWindow: ((View) -> Unit)? = null,
352+
)
353+
354+
public fun startRenderingComposeView(
355+
view: AbstractComposeView,
356+
parentContext: CompositionContext,
357+
): ComposeUiViewRenderer.RenderHandle
343358

344359
public val mapView: T
345360
}
346361

362+
private val <T : View> AbstractMapViewDelegate<T>.tagData: MapTagData
363+
get() = mapView.tag as MapTagData
364+
347365
public class MapViewDelegate(override val mapView: MapView) : AbstractMapViewDelegate<MapView> {
348366
override fun onCreate(savedInstanceState: Bundle?): Unit = mapView.onCreate(savedInstanceState)
349367
override fun onStart(): Unit = mapView.onStart()
@@ -353,8 +371,26 @@ public class MapViewDelegate(override val mapView: MapView) : AbstractMapViewDel
353371
override fun onLowMemory(): Unit = mapView.onLowMemory()
354372
override fun onDestroy(): Unit = mapView.onDestroy()
355373
override suspend fun awaitMap(): GoogleMap = mapView.awaitMap()
356-
override fun renderComposeViewOnce(view: ComposeView, parentContext: CompositionContext) {
357-
mapView.renderComposeViewOnce(view, parentContext = parentContext)
374+
override fun renderComposeViewOnce(
375+
view: AbstractComposeView,
376+
parentContext: CompositionContext,
377+
onAddedToWindow: ((View) -> Unit)?
378+
) {
379+
mapView.renderComposeViewOnce(
380+
view = view,
381+
parentContext = parentContext,
382+
onAddedToWindow = onAddedToWindow
383+
)
384+
}
385+
386+
override fun startRenderingComposeView(
387+
view: AbstractComposeView,
388+
parentContext: CompositionContext
389+
): ComposeUiViewRenderer.RenderHandle {
390+
return mapView.startRenderingComposeView(
391+
view = view,
392+
parentContext = parentContext,
393+
)
358394
}
359395
}
360396

maps-compose/src/main/java/com/google/maps/android/compose/MapComposeViewRender.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import java.io.Closeable
2222
* to a window. [onAddedToWindow] is called in place, and then [view] is removed from the window
2323
* before returning.
2424
*/
25-
internal fun MapView.renderComposeViewOnce(
25+
public fun MapView.renderComposeViewOnce(
2626
view: AbstractComposeView,
2727
onAddedToWindow: ((View) -> Unit)? = null,
2828
parentContext: CompositionContext,
@@ -38,7 +38,7 @@ internal fun MapView.renderComposeViewOnce(
3838
* to a window. A [ComposeUiViewRenderer.RenderHandle] is returned, which must be disposed after
3939
* this view no longer needs to render. Disposing removes [view] from the [MapView].
4040
*/
41-
internal fun MapView.startRenderingComposeView(
41+
public fun MapView.startRenderingComposeView(
4242
view: AbstractComposeView,
4343
parentContext: CompositionContext,
4444
): ComposeUiViewRenderer.RenderHandle {
@@ -69,7 +69,7 @@ private fun MapView.ensureContainerView(): NoDrawContainerView {
6969
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
7070
@Composable
7171
public fun rememberComposeUiViewRenderer(): ComposeUiViewRenderer {
72-
val mapView = (currentComposer.applier as MapApplier).mapView
72+
val mapView = (currentComposer.applier as MapApplier).mapViewDelegate
7373
val compositionContext = rememberCompositionContext()
7474

7575
return remember(compositionContext) {

maps-compose/src/main/java/com/google/maps/android/compose/MapUpdater.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public val DefaultMapContentPadding: PaddingValues = PaddingValues()
100100
@Composable
101101
internal inline fun MapUpdater(mapUpdaterState: MapUpdaterState) = with(mapUpdaterState) {
102102
val map = (currentComposer.applier as MapApplier).map
103-
val mapView = (currentComposer.applier as MapApplier).mapView
103+
val mapView = (currentComposer.applier as MapApplier).mapViewDelegate.mapView
104104
if (mergeDescendants) {
105105
mapView.importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
106106
}

navigation-app/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

navigation-app/build.gradle.kts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
plugins {
2+
alias(libs.plugins.android.application)
3+
alias(libs.plugins.kotlin.android)
4+
alias(libs.plugins.compose.compiler)
5+
id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
6+
}
7+
8+
android {
9+
namespace = "com.google.maps.android.compose.navigation"
10+
compileSdk = 35
11+
12+
defaultConfig {
13+
applicationId = "com.google.maps.android.compose.navigation"
14+
minSdk = 24
15+
targetSdk = 35
16+
versionCode = 1
17+
versionName = "1.0"
18+
19+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
20+
}
21+
22+
buildTypes {
23+
release {
24+
isMinifyEnabled = false
25+
proguardFiles(
26+
getDefaultProguardFile("proguard-android-optimize.txt"),
27+
"proguard-rules.pro"
28+
)
29+
}
30+
}
31+
32+
compileOptions {
33+
sourceCompatibility = JavaVersion.VERSION_1_8
34+
targetCompatibility = JavaVersion.VERSION_1_8
35+
}
36+
37+
kotlinOptions {
38+
jvmTarget = "1.8"
39+
freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
40+
}
41+
42+
buildFeatures {
43+
buildConfig = true
44+
compose = true
45+
}
46+
}
47+
48+
configurations.all {
49+
resolutionStrategy {
50+
exclude(group = "com.google.android.gms", module = "play-services-maps")
51+
}
52+
}
53+
54+
dependencies {
55+
56+
implementation(libs.androidx.core)
57+
implementation(libs.androidx.lifecycle.runtime.ktx)
58+
implementation(libs.androidx.compose.activity)
59+
implementation(platform(libs.androidx.compose.bom))
60+
implementation(libs.androidx.compose.ui)
61+
implementation(libs.androidx.ui.graphics)
62+
implementation(libs.androidx.compose.ui.preview.tooling)
63+
implementation(libs.androidx.material3)
64+
testImplementation(libs.test.junit)
65+
androidTestImplementation(libs.androidx.junit)
66+
androidTestImplementation(libs.androidx.test.espresso)
67+
androidTestImplementation(platform(libs.androidx.compose.bom))
68+
androidTestImplementation(libs.androidx.test.compose.ui)
69+
debugImplementation(libs.androidx.compose.ui.tooling)
70+
debugImplementation(libs.androidx.ui.test.manifest)
71+
72+
// Instead of the lines below, regular apps would load these libraries from Maven according to
73+
// the README installation instructions
74+
implementation(project(":maps-compose"))
75+
implementation(project(":maps-compose-widgets"))
76+
implementation(project(":maps-compose-utils"))
77+
78+
implementation(libs.maps.ktx.std)
79+
implementation(libs.maps.ktx.utils)
80+
81+
// Use the navigation SDK which includes the maps SDK
82+
implementation(libs.navigation)
83+
84+
implementation(libs.play.services.location)
85+
86+
// testImplementation(libs.robolectric)
87+
testImplementation(libs.androidx.core)
88+
testImplementation(libs.truth)
89+
90+
implementation(libs.androidx.lifecycle.viewmodel.compose)
91+
implementation(libs.androidx.lifecycle.runtime.compose)
92+
93+
implementation(libs.places)
94+
95+
// Accompanist permission helper
96+
implementation(libs.accompanist.permissions)
97+
98+
}
99+
100+
secrets {
101+
// To add your Maps API key to this project:
102+
// 1. If the secrets.properties file does not exist, create it in the same folder as the local.properties file.
103+
// 2. Add this line, where YOUR_API_KEY is your API key:
104+
// MAPS_API_KEY=YOUR_API_KEY
105+
propertiesFileName = "secrets.properties"
106+
107+
// A properties file containing default secret values. This file can be
108+
// checked in version control.
109+
defaultPropertiesFileName = "local.defaults.properties"
110+
}

navigation-app/proguard-rules.pro

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Add project specific ProGuard rules here.
2+
# You can control the set of applied configuration files using the
3+
# proguardFiles setting in build.gradle.
4+
#
5+
# For more details, see
6+
# http://developer.android.com/guide/developing/tools/proguard.html
7+
8+
# If your project uses WebView with JS, uncomment the following
9+
# and specify the fully qualified class name to the JavaScript interface
10+
# class:
11+
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12+
# public *;
13+
#}
14+
15+
# Uncomment this to preserve the line number information for
16+
# debugging stack traces.
17+
#-keepattributes SourceFile,LineNumberTable
18+
19+
# If you keep the line number information, uncomment this to
20+
# hide the original source file name.
21+
#-renamesourcefileattribute SourceFile

0 commit comments

Comments
 (0)