Skip to content

Commit

Permalink
feat(navigation-app): Add Navigation support to Maps Compose
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dkhawk committed Jan 8, 2025
1 parent 5424095 commit bcdf6b0
Show file tree
Hide file tree
Showing 44 changed files with 1,104 additions and 23 deletions.
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ plugins {
alias(libs.plugins.dokka) apply true
alias(libs.plugins.compose.compiler) apply false
id("com.autonomousapps.dependency-analysis") version "2.0.0"
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false

}

Expand Down
40 changes: 33 additions & 7 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,23 +1,35 @@
[versions]
accompanistPermissions = "0.37.0"
activitycompose = "1.9.3"
agp = "8.7.2"
androidxtest = "1.6.2"
agpVersion = "8.7.2"
androidCore = "1.6.1"
androidx-core = "1.15.0"
androidxtest = "1.6.2"
compose-bom = "2024.11.00"
dokka = "1.9.20"
espresso = "3.6.1"
jacoco-plugin = "0.2.1"
junitktx = "1.2.1"
junit = "4.13.2"
junitVersion = "1.2.1"
junitktx = "1.2.1"
kotlin = "2.0.21"
kotlinxCoroutines = "1.9.0"
mapsktx = "5.1.1"
lifecycleRuntimeKtx = "2.8.7"
mapsecrets = "2.0.1"
mapsktx = "5.1.1"
navigation = "6.0.0"
org-jacoco-core = "0.8.11"
androidx-core = "1.15.0"
places = "4.1.0"
playServicesLocation = "21.3.0"
robolectric = "4.14.1"
screenshot = "0.0.1-alpha08"
secretsGradlePlugin = "2.0.1"
truth = "1.4.4"

[libraries]
# robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
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" }
Expand All @@ -27,25 +39,39 @@ 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.ref = "androidx-core" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycleRuntimeKtx" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-test-compose-ui = { module = "androidx.compose.ui:ui-test-junit4" }
androidx-test-core = { module = "androidx.test:core", version.ref = "androidCore" }
androidx-test-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" }
androidx-test-junit-ktx = { module = "androidx.test.ext:junit-ktx", version.ref = "junitktx" }
androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidCore" }
androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidxtest" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
dokka-plugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" }
jacoco-android-plugin = { module = "com.mxalbert.gradle:jacoco-android", version.ref = "jacoco-plugin", version.require = "0.2.1" }
kotlin = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk7", version.ref = "kotlin" }
kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" }
maps-ktx-std = { module = "com.google.maps.android:maps-ktx", version.ref = "mapsktx" }
maps-ktx-utils = { module = "com.google.maps.android:maps-utils-ktx", version.ref = "mapsktx" }
maps-secrets-plugin = { module = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin", version.ref = "mapsecrets" }
navigation = { module = "com.google.android.libraries.navigation:navigation", version.ref = "navigation" }
org-jacoco-core = { module = "org.jacoco:org.jacoco.core", version.ref = "org-jacoco-core" }
places = { group = "com.google.android.libraries.places", name = "places", version.ref = "places" }
play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "playServicesLocation" }
test-junit = { module = "junit:junit", version.ref = "junit" }
truth = { module = "com.google.truth:truth", version.ref = "truth" }

[plugins]
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
android-application = { id = "com.android.application", version.ref = "agpVersion" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
screenshot = { id = "com.android.compose.screenshot", version.ref = "screenshot"}
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
screenshot = { id = "com.android.compose.screenshot", version.ref = "screenshot"}
secrets-gradle-plugin = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secretsGradlePlugin" }
3 changes: 2 additions & 1 deletion local.defaults.properties
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the
# header note.
MAPS_API_KEY=YOUR_API_KEY
MAPS_API_KEY=YOUR_API_KEY
PLACES_API_KEY=DEFAULT_API_KEY
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.AbstractComposeView
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.viewinterop.AndroidView
Expand Down Expand Up @@ -149,6 +150,11 @@ public fun GoogleMap(
var subcompositionJob by remember { mutableStateOf<Job?>(null) }
val parentCompositionScope = rememberCoroutineScope()

var delegate by remember {
// TODO: this could leak the view?
mutableStateOf<AbstractMapViewDelegate<*>?>(null)
}

AndroidView(
modifier = modifier,
factory = { context ->
Expand All @@ -157,6 +163,7 @@ public fun GoogleMap(
} else {
MapViewDelegate(MapView(context, googleMapOptionsFactory()))
}.also { mapViewDelegate: AbstractMapViewDelegate<*> ->
delegate = mapViewDelegate
val mapView = mapViewDelegate.mapView

val componentCallbacks = object : ComponentCallbacks2 {
Expand Down Expand Up @@ -193,12 +200,11 @@ public fun GoogleMap(
},
onReset = { /* View is detached. */ },
onRelease = { mapView ->
mapView.toDelegate()

val (componentCallbacks, lifecycleObserver) = when {
mapView is MapView -> mapView.tagData
else -> TODO("not yet implemented!")
}
val (componentCallbacks, lifecycleObserver) = delegate!!.tagData
// val (componentCallbacks, lifecycleObserver) = when {
// mapView is MapView -> mapView.tagData
// else -> TODO("not yet implemented!")
// }
mapView.context.unregisterComponentCallbacks(componentCallbacks)
lifecycleObserver.moveToDestroyedState()
mapView.tag = null
Expand All @@ -208,7 +214,7 @@ public fun GoogleMap(
subcompositionJob = parentCompositionScope.launchSubcomposition(
mapUpdaterState,
parentComposition,
mapView.toDelegate(),
delegate!!, // TODO: not sure about this. Maybe just remember a factory method?
mapClickListeners,
currentContent,
)
Expand Down Expand Up @@ -339,11 +345,23 @@ public interface AbstractMapViewDelegate<T : View> {
public fun onLowMemory()
public fun onDestroy()
public suspend fun awaitMap(): GoogleMap
public fun renderComposeViewOnce(view: ComposeView, parentContext: CompositionContext)
public fun renderComposeViewOnce(
view: AbstractComposeView,
parentContext: CompositionContext,
onAddedToWindow: ((View) -> Unit)? = null,
)

public fun startRenderingComposeView(
view: AbstractComposeView,
parentContext: CompositionContext,
): ComposeUiViewRenderer.RenderHandle

public val mapView: T
}

private val <T : View> AbstractMapViewDelegate<T>.tagData: MapTagData
get() = mapView.tag as MapTagData

public class MapViewDelegate(override val mapView: MapView) : AbstractMapViewDelegate<MapView> {
override fun onCreate(savedInstanceState: Bundle?): Unit = mapView.onCreate(savedInstanceState)
override fun onStart(): Unit = mapView.onStart()
Expand All @@ -353,8 +371,26 @@ public class MapViewDelegate(override val mapView: MapView) : AbstractMapViewDel
override fun onLowMemory(): Unit = mapView.onLowMemory()
override fun onDestroy(): Unit = mapView.onDestroy()
override suspend fun awaitMap(): GoogleMap = mapView.awaitMap()
override fun renderComposeViewOnce(view: ComposeView, parentContext: CompositionContext) {
mapView.renderComposeViewOnce(view, parentContext = parentContext)
override fun renderComposeViewOnce(
view: AbstractComposeView,
parentContext: CompositionContext,
onAddedToWindow: ((View) -> Unit)?
) {
mapView.renderComposeViewOnce(
view = view,
parentContext = parentContext,
onAddedToWindow = onAddedToWindow
)
}

override fun startRenderingComposeView(
view: AbstractComposeView,
parentContext: CompositionContext
): ComposeUiViewRenderer.RenderHandle {
return mapView.startRenderingComposeView(
view = view,
parentContext = parentContext,
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import java.io.Closeable
* to a window. [onAddedToWindow] is called in place, and then [view] is removed from the window
* before returning.
*/
internal fun MapView.renderComposeViewOnce(
public fun MapView.renderComposeViewOnce(
view: AbstractComposeView,
onAddedToWindow: ((View) -> Unit)? = null,
parentContext: CompositionContext,
Expand All @@ -38,7 +38,7 @@ internal fun MapView.renderComposeViewOnce(
* to a window. A [ComposeUiViewRenderer.RenderHandle] is returned, which must be disposed after
* this view no longer needs to render. Disposing removes [view] from the [MapView].
*/
internal fun MapView.startRenderingComposeView(
public fun MapView.startRenderingComposeView(
view: AbstractComposeView,
parentContext: CompositionContext,
): ComposeUiViewRenderer.RenderHandle {
Expand Down Expand Up @@ -69,7 +69,7 @@ private fun MapView.ensureContainerView(): NoDrawContainerView {
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Composable
public fun rememberComposeUiViewRenderer(): ComposeUiViewRenderer {
val mapView = (currentComposer.applier as MapApplier).mapView
val mapView = (currentComposer.applier as MapApplier).mapViewDelegate
val compositionContext = rememberCompositionContext()

return remember(compositionContext) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public val DefaultMapContentPadding: PaddingValues = PaddingValues()
@Composable
internal inline fun MapUpdater(mapUpdaterState: MapUpdaterState) = with(mapUpdaterState) {
val map = (currentComposer.applier as MapApplier).map
val mapView = (currentComposer.applier as MapApplier).mapView
val mapView = (currentComposer.applier as MapApplier).mapViewDelegate.mapView
if (mergeDescendants) {
mapView.importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
}
Expand Down
1 change: 1 addition & 0 deletions navigation-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
110 changes: 110 additions & 0 deletions navigation-app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.compose.compiler)
id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
}

android {
namespace = "com.google.maps.android.compose.navigation"
compileSdk = 35

defaultConfig {
applicationId = "com.google.maps.android.compose.navigation"
minSdk = 24
targetSdk = 35
versionCode = 1
versionName = "1.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

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

buildFeatures {
buildConfig = true
compose = true
}
}

configurations.all {
resolutionStrategy {
exclude(group = "com.google.android.gms", module = "play-services-maps")
}
}

dependencies {

implementation(libs.androidx.core)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.compose.activity)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.compose.ui.preview.tooling)
implementation(libs.androidx.material3)
testImplementation(libs.test.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.test.espresso)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.test.compose.ui)
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)

// Instead of the lines below, regular apps would load these libraries from Maven according to
// the README installation instructions
implementation(project(":maps-compose"))
implementation(project(":maps-compose-widgets"))
implementation(project(":maps-compose-utils"))

implementation(libs.maps.ktx.std)
implementation(libs.maps.ktx.utils)

// Use the navigation SDK which includes the maps SDK
implementation(libs.navigation)

implementation(libs.play.services.location)

// testImplementation(libs.robolectric)
testImplementation(libs.androidx.core)
testImplementation(libs.truth)

implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.lifecycle.runtime.compose)

implementation(libs.places)

// Accompanist permission helper
implementation(libs.accompanist.permissions)

}

secrets {
// To add your Maps API key to this project:
// 1. If the secrets.properties file does not exist, create it in the same folder as the local.properties file.
// 2. Add this line, where YOUR_API_KEY is your API key:
// MAPS_API_KEY=YOUR_API_KEY
propertiesFileName = "secrets.properties"

// A properties file containing default secret values. This file can be
// checked in version control.
defaultPropertiesFileName = "local.defaults.properties"
}
21 changes: 21 additions & 0 deletions navigation-app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
Loading

0 comments on commit bcdf6b0

Please sign in to comment.