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
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 {} @SuppressLint("MissingPermission") fun startLocationAndMap() {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() }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 = truescope.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 columnlocation-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")