Google Map

Add Top Bar

Now we'll add a top bar that provides action buttons to

  • Go to the current location
  • Remember the car is parked at the current location
  • Navigate using the Google Map application from the current position to the car (in walk mode)
  • Forget the car location

We Define the top bar with the above actions calling event parameters.

These actions use icons from the Extended Icon Set, so we add its dependency. In the next step, we'll pull out the icons that we want to keep and remove the dependency to avoid creating a huge application file.

Set up a Scaffold that will move the camera to the current location, and placeholders for the functions we're not implementing yet.

Note

Kotlin's TODO() function throws an exception. Much nicer than having a TODO comment that just does nothing silently. In this case, if we tap any of the buttons that call TODO(), the application will crash.

Our application now looks like

Top Bar Actions

If we pan away from the current location and tap the location button on the Top Bar, Google Maps will animate back to the current location over one second.

Code Changes

CHANGED: /app/build.gradle.kts
@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed
plugins {
    alias(libs.plugins.androidApplication)
    alias(libs.plugins.kotlinAndroid)
alias(libs.plugins.secrets)
} kotlin { jvmToolchain(17) } android { namespace = "com.androidbyexample.googlemap" compileSdk = 34 defaultConfig { applicationId = "com.androidbyexample.googlemap" minSdk = 24 targetSdk = 34 versionCode = 1 versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary = true } } buildTypes { release { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = "1.5.3" } packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } } dependencies {
implementation(libs.icons.extended)
implementation(libs.location.services) implementation(libs.runtime.compose)
implementation(libs.maps.compose) implementation(libs.maps.compose.utils)
implementation(libs.core.ktx) implementation(libs.lifecycle.runtime.ktx) implementation(libs.activity.compose) implementation(platform(libs.compose.bom)) implementation(libs.ui) implementation(libs.ui.graphics) implementation(libs.ui.tooling.preview) implementation(libs.material3) testImplementation(libs.junit) androidTestImplementation(libs.androidx.test.ext.junit) androidTestImplementation(libs.espresso.core) androidTestImplementation(platform(libs.compose.bom)) androidTestImplementation(libs.ui.test.junit4) debugImplementation(libs.ui.tooling) debugImplementation(libs.ui.test.manifest) }
ADDED: /app/src/main/java/com/androidbyexample/googlemap/CarTopBar.kt
@file:OptIn(ExperimentalMaterial3Api::class)package com.androidbyexample.googlemapimport android.location.Locationimport androidx.compose.material.icons.Iconsimport androidx.compose.material.icons.automirrored.filled.DirectionsWalkimport androidx.compose.material.icons.filled.Deleteimport androidx.compose.material.icons.filled.DirectionsWalkimport androidx.compose.material.icons.filled.GpsFixedimport androidx.compose.material.icons.filled.Starimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.Iconimport androidx.compose.material3.IconButtonimport androidx.compose.material3.Textimport androidx.compose.material3.TopAppBarimport androidx.compose.runtime.Composableimport androidx.compose.ui.res.stringResourceimport com.google.android.gms.maps.model.LatLng
@Composablefun CarTopBar( currentLocation: Location?, carLatLng: LatLng?, onSetCarLocation: () -> Unit, onGoToCurrentLocation: () -> Unit, onClearCarLocation: () -> Unit, onWalkToCar: () -> Unit,) { TopAppBar( title = { Text(text = stringResource(id = R.string.app_name)) }, actions = { currentLocation?.let { IconButton(onClick = onGoToCurrentLocation) { Icon( imageVector = Icons.Filled.GpsFixed, contentDescription = stringResource(id = R.string.go_to_current_location), ) } IconButton(onClick = onSetCarLocation) { Icon( imageVector = Icons.Filled.Star, contentDescription = stringResource(id = R.string.remember_location), ) } } carLatLng?.let { IconButton(onClick = onWalkToCar) { Icon( imageVector = Icons.AutoMirrored.Filled.DirectionsWalk, contentDescription = stringResource(id = R.string.navigate), ) } IconButton(onClick = onClearCarLocation) { Icon( imageVector = Icons.Filled.Delete, contentDescription = stringResource(id = R.string.forget_location), ) } } }, )}
CHANGED: /app/src/main/java/com/androidbyexample/googlemap/MainActivity.kt
package com.androidbyexample.googlemap

import android.Manifest
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Intent
import android.content.pm.PackageManager
import android.location.Location
import android.net.Uri
import android.os.Bundle
import android.os.Looper
import android.provider.Settings
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.paddingimport androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffoldimport androidx.compose.material3.Surface
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.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.core.app.ActivityCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.androidbyexample.googlemap.ui.theme.GoogleMapTheme
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationResult
import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.Priority
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.model.BitmapDescriptor
import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.LatLng
import com.google.maps.android.compose.CameraPositionState
import com.google.maps.android.compose.GoogleMap
import com.google.maps.android.compose.MapProperties
import com.google.maps.android.compose.MapType
import com.google.maps.android.compose.MarkerInfoWindowContent
import com.google.maps.android.compose.MarkerState
import com.google.maps.android.compose.rememberCameraPositionState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class MainActivity : ComponentActivity() {
    private val viewModel: CarViewModel by viewModels()

private lateinit var fusedLocationProviderClient: FusedLocationProviderClient private val locationCallback = object : LocationCallback() { override fun onLocationResult(locationResult: LocationResult) { viewModel.updateLocation(locationResult.lastLocation) } }
private val getLocationPermission = registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions() ) { isGranted -> if (isGranted.values.any { it }) { startLocationAndMap() } else { // if the user denied permissions, tell them they // cannot use the app without them. In general, // you should try to just reduce function and let the // user continue, but location is a key part of this // application. // (Note that a real version of this application // might allow the user to manually click on the map // to set their current location, and we wouldn't // show this dialog, or perhaps only show it once) // NOTE: This is a normal Android-View-based dialog, not a compose one! AlertDialog.Builder(this) .setTitle("Permissions Needed") .setMessage( "We need coarse-location or fine-location permission " + "to locate a car (fine location is highly " + "recommended for accurate car locating). " + "Please allow these permissions via App Info " + "settings") .setCancelable(false) .setNegativeButton("Quit") { _, _ -> finish() } .setPositiveButton("App Info") { _, _ -> startActivity( Intent( Settings.ACTION_APPLICATION_DETAILS_SETTINGS ).apply { data = Uri.parse("package:$packageName") } ) finish() } .show() } }
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)
GoogleApiAvailability.getInstance() .makeGooglePlayServicesAvailable(this) .addOnSuccessListener {
if (ActivityCompat.checkSelfPermission( this, Manifest.permission.ACCESS_FINE_LOCATION ) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission( this, Manifest.permission.ACCESS_COARSE_LOCATION ) != PackageManager.PERMISSION_GRANTED ) { getLocationPermission.launch( arrayOf( Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION ) ) } else { startLocationAndMap() }
}.addOnFailureListener(this) { Toast.makeText( this, "Google Play services required (or upgrade required)", Toast.LENGTH_SHORT ).show() finish() }
} @SuppressLint("MissingPermission") fun startLocationAndMap() {
val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 5000) .setWaitForAccurateLocation(false) .setMinUpdateIntervalMillis(0) .setMaxUpdateDelayMillis(5000) .build() fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this) fusedLocationProviderClient.requestLocationUpdates( locationRequest, locationCallback, Looper.getMainLooper() )
setContent { GoogleMapTheme { // A surface container using the 'background' color from the theme Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) {
val cameraPositionState = rememberCameraPositionState()
val currentLocation by viewModel.currentLocation.collectAsStateWithLifecycle( initialValue = null )
GoogleMapDisplay( currentLocation = currentLocation, cameraPositionState = cameraPositionState, modifier = Modifier.fillMaxSize(), )
} } } } }
@Composable fun GoogleMapDisplay( currentLocation: Location?, cameraPositionState: CameraPositionState, modifier: Modifier, ) {
var mapLoaded by remember { mutableStateOf(false) }
var currentMapType by remember { mutableStateOf(MapType.NORMAL) } var mapProperties by remember { mutableStateOf(MapProperties(mapType = MapType.NORMAL)) }
val currentLocationState = remember(currentLocation) { currentLocation?.let { MarkerState( LatLng( it.latitude, it.longitude ) ) } }
val context = LocalContext.current var currentLocationIcon by remember { mutableStateOf<BitmapDescriptor?>(null) } val scope = rememberCoroutineScope()
var initialBoundsSet by remember { mutableStateOf(false) } LaunchedEffect(key1 = currentLocation) { if (currentLocation != null) { if (!initialBoundsSet) { initialBoundsSet = true val current = LatLng(currentLocation.latitude, currentLocation.longitude) cameraPositionState.animate( CameraUpdateFactory.newLatLngZoom( current, 16f ), 1000 ) } } }
Scaffold( topBar = { CarTopBar( currentLocation = currentLocation, carLatLng = null, onSetCarLocation = { TODO() }, onClearCarLocation = { TODO() }, onWalkToCar = { TODO() }, onGoToCurrentLocation = { currentLocation?.let { curr -> scope.launch { cameraPositionState.animate( CameraUpdateFactory.newLatLngZoom( LatLng(curr.latitude, curr.longitude), 16f ), 1000 ) } } ?: Toast.makeText( context, "No current location available", Toast.LENGTH_LONG ).show() }, ) }, content = { paddingValues -> Box(
// modifier = modifier, modifier = modifier.padding(paddingValues),
) {
Column( modifier = Modifier.fillMaxSize() ) { MapTypeSelector( currentValue = currentMapType, modifier = Modifier.fillMaxWidth(), ) { mapProperties = mapProperties.copy(mapType = it) currentMapType = it }
GoogleMap(
cameraPositionState = cameraPositionState,
onMapLoaded = { mapLoaded = true
scope.launch(Dispatchers.IO) { currentLocationIcon = context.loadBitmapDescriptor( R.drawable.ic_current_location ) }
},
properties = mapProperties,
modifier = Modifier .fillMaxWidth() .weight(1f),
) {
currentLocationState?.let { MarkerInfoWindowContent( state = it,
icon = currentLocationIcon, anchor = Offset(0.5f, 0.5f),
title = stringResource( id = R.string.current_location ), ) }
} } }
if (!mapLoaded) { AnimatedVisibility( visible = true, modifier = Modifier.fillMaxSize(), enter = EnterTransition.None, exit = fadeOut() ) { CircularProgressIndicator( modifier = Modifier .background(MaterialTheme.colorScheme.background) .wrapContentSize() ) } }
} )}
CHANGED: /app/src/main/res/values/strings.xml
<resources>
//  <string name="app_name">Google map</string>    <string name="app_name">Car finder</string>    <string name="map_type">Map Type</string>
    <string name="current_location">Current Location</string>
    <string name="go_to_current_location">Go to Current Location</string>    <string name="remember_location">Remember Location</string>    <string name="navigate">Walk to Car</string>    <string name="forget_location">Forget Car Location</string></resources>
CHANGED: /gradle/libs.versions.toml
[versions]
agp = "8.2.0-beta06"
kotlin = "1.9.10"
core-ktx = "1.12.0"
junit = "4.13.2"
androidx-test-ext-junit = "1.1.5"
espresso-core = "3.5.1"
lifecycle-runtime-ktx = "2.6.2"
activity-compose = "1.8.0"
compose-bom = "2023.10.00"
secrets = "2.0.1"
maps-compose = "3.0.0" # bug in 3.1.0 - map only takes up 50% of column
location-services = "21.0.1" runtime-compose = "2.6.2" icons-extended = "1.6.0-alpha06" # needed to bump to version that has source [libraries] core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" } espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" } lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-ktx" } activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" } compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" } ui = { group = "androidx.compose.ui", name = "ui" } ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } material3 = { group = "androidx.compose.material3", name = "material3" }
maps-compose = { group = "com.google.maps.android", name = "maps-compose", version.ref = "maps-compose" } maps-compose-utils = { group = "com.google.maps.android", name = "maps-compose-utils", version.ref = "maps-compose" }
location-services = { group = "com.google.android.gms", name = "play-services-location", version.ref = "location-services" } runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "runtime-compose" } icons-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "icons-extended" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secrets" }
CHANGED: /settings.gradle.kts
pluginManagement {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}

//rootProject.name = "Google map"rootProject.name = "Car finder"include(":app")