Google Map

Manage Car Location

We need to keep track of our car across application runs. To do this, we'll use a Preferences Datastore to store the latitude and longitude of the car.

The Datastore API exposes the data from the preference as a Kotlin Flow; whenever the preference is updated, the new value is emitted. We'll expose the car location to the UI from the view model, allowing new values to be collected. The preferences data store is a simple file that stores values. This is useful when a database would be overkill for storing small amounts of data. The datastore is typically used to hold simple application state across application runs.

We start by Adding the Datastore Dependency.

show in full file gradle/libs.versions.toml
[versions]
// ...
dokka = "1.9.20"
icons-extended = "1.7.5"
datastore-preferences = "1.1.1"

[libraries]
datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore-preferences" }
icons-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "icons-extended" }
location-services = { group = "com.google.android.gms", name = "play-services-location", version.ref = "location-services" }
// ...
[plugins]
// ...
show in full file app/build.gradle.kts
// ...

dependencies {
    implementation(libs.datastore.preferences)
//    implementation(libs.icons.extended)
    implementation(libs.location.services)
    // ...
}
// ...

Then we can store and load the car's location.

show in full file app/src/main/java/com/androidbyexample/compose/google/google/maps/CarViewModel.kt
// ...
class CarViewModel(application: Application) : AndroidViewModel(application) {
    // ...
    }

    private val Context.preferencesDataStore:
            DataStore<Preferences> by preferencesDataStore(name = "carfinder")
    val carLatLng = application.preferencesDataStore.data.map { preferences ->
        preferences[LAT_PREF]?.let { latString ->
            preferences[LON_PREF]?.let { lonString ->
                LatLng(latString.toDouble(), lonString.toDouble())
            }
        }
    }
    fun clearCarLocation() {
        viewModelScope.launch {
            getApplication<Application>().preferencesDataStore.edit { preferences ->
                preferences.remove(LAT_PREF)
                preferences.remove(LON_PREF)
            }
        }
    }
    fun setCarLocation() {
        viewModelScope.launch {
            _currentLocation.value?.let { location ->
                getApplication<Application>().preferencesDataStore.edit { preferences ->
                    preferences[LAT_PREF] = location.latitude.toString()
                    preferences[LON_PREF] = location.longitude.toString()
                }
            } ?: run {
                clearCarLocation()
            }
        }
    }
}
  • carLatLng is a Flow created by mapping the data store's Flow into a LatLng instance. If either latitude or longitude are missing, a null is emitted.
  • clearCarLocation removes the latitude and longitude from the data store.
  • setCarLocation stores the current location's latitude and longitude in datastore (or removes them if there is no current location)

When clearCarLocation or setCarLocation change the latitude/longitude in the data store, new data is emitted to the data store Flow, which will trigger the carLatLng to emit a new LatLng or null.

We'll need a new icon for the Car. I found a nice free car Marker, made by Vectors Market https://www.flaticon.com/authors/vectors-market from https://www.flaticon.com.

show in full file app/src/main/res/drawable/ic_car.xml
<!--
Car icon made by Vectors Market (https://www.flaticon.com/authors/vectors-market) from https://www.flaticon.com
-->
<vector android:height="48dp" android:viewportHeight="496.474"
    android:viewportWidth="496.474" android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
    <path android:fillColor="#E95353" android:pathData="M489.544,269.628c-0.729,-14.739 -6.206,-28.858 -15.205,-40.572c-20.294,-26.422 -24.25,-47.399 -24.25,-47.399c-15.934,-41.751 -30.099,-69.57 -40.51,-87.226c-10.954,-18.541 -29.898,-31.03 -51.262,-33.776c-42.961,-5.554 -177.23,-5.554 -220.191,0c-21.349,2.762 -40.293,15.251 -51.247,33.776c-10.411,17.656 -24.56,45.475 -40.51,87.226c0,0 -3.956,20.977 -24.25,47.399c-8.983,11.714 -14.476,25.833 -15.189,40.572c-1.536,31.977 7.727,39.005 16.012,111.368c0.652,5.694 5.461,9.976 11.202,9.976h428.203c5.741,0 10.55,-4.298 11.202,-9.976c2.327,-20.294 9.232,-61.456 9.232,-61.456C488.83,299.076 490.149,282.397 489.544,269.628z"/>
    <path android:fillColor="#168DE2" android:pathData="M441.074,179.982c-12.567,-31.449 -25.383,-58.911 -38.136,-80.803c-9.666,-16.555 -26.717,-28.113 -45.583,-30.642c-43.055,-5.71 -175.476,-5.663 -218.221,0c-18.866,2.529 -35.918,14.088 -45.584,30.642c-12.924,22.171 -25.91,50.005 -38.291,80.803C183.662,190.082 312.655,190.082 441.074,179.982z"/>
    <path android:fillColor="#FFFFFF" android:pathData="M422.844,216.349c-4.903,1.536 -9.464,3.693 -13.281,5.834c-6.951,3.879 -14.088,7.494 -21.535,10.318c-12.35,4.686 -14.445,11.683 -14.445,11.683c-0.372,0.481 -0.729,0.962 -1.071,1.458c-9.371,13.917 2.932,32.442 19.549,30.084c24.033,-3.398 43.83,-10.364 53.884,-14.398c5.756,-2.312 10.255,-7.121 11.729,-13.157c0.14,-0.574 0.264,-1.148 0.372,-1.707C461.74,226.543 442.191,210.314 422.844,216.349z"/>
    <path android:fillColor="#FFE21F" android:pathData="M448.522,327.857h-26.733c-4.018,0 -6.035,4.856 -3.196,7.711l26.733,26.733c2.839,2.839 7.711,0.822 7.711,-3.196v-26.733C453.036,329.874 451.019,327.857 448.522,327.857z"/>
    <path android:fillColor="#454545" android:pathData="M473.547,380.965c-0.652,5.71 -5.477,10.007 -11.217,10.007h-51.433v29.851c0,10.566 8.564,19.146 19.146,19.146h32.303c10.566,0 19.146,-8.564 19.146,-19.146v-93.448C479.366,340.315 475.239,366.179 473.547,380.965z"/>
    <path android:fillColor="#ED6262" android:pathData="M496.107,159.083c-1.552,-6.951 -8.083,-11.667 -15.205,-11.667h-12.909c-8.27,0 -14.972,6.703 -14.972,14.972v9.2h-6.842l3.925,9.325h15.05c7.789,0 15.5,-1.676 22.575,-4.918C494.105,173.078 497.674,166.096 496.107,159.083z"/>
    <path android:fillColor="#FFBD49" android:pathData="M431.315,240.46m-18.82,0a18.82,18.82 0,1 1,37.64 0a18.82,18.82 0,1 1,-37.64 0"/>
    <path android:fillColor="#FFDB6F" android:pathData="M393.458,255.308m-13.824,0a13.824,13.824 0,1 1,27.648 0a13.824,13.824 0,1 1,-27.648 0"/>
    <path android:fillColor="#6F6F6F" android:pathData="M357.634,255.324l-21.613,37.639l-175.569,0l-21.613,-37.639z"/>
    <path android:fillColor="#FFFFFF" android:pathData="M123.945,245.642c-0.326,-0.496 -0.683,-0.977 -1.071,-1.458c0,0 -2.095,-6.997 -14.445,-11.683c-7.447,-2.824 -14.569,-6.439 -21.535,-10.318c-3.832,-2.141 -8.378,-4.298 -13.281,-5.834c-19.332,-6.051 -38.896,10.193 -35.219,30.115c0.109,0.574 0.233,1.133 0.357,1.707c1.474,6.035 5.973,10.845 11.729,13.157c10.054,4.034 29.851,11 53.9,14.398C121.012,278.084 133.316,259.559 123.945,245.642z"/>
    <path android:fillColor="#FFE21F" android:pathData="M74.684,327.857H47.951c-2.498,0 -4.515,2.017 -4.515,4.515v26.733c0,4.018 4.872,6.035 7.711,3.196l26.733,-26.733C80.719,332.729 78.702,327.857 74.684,327.857z"/>
    <path android:fillColor="#777777" android:pathData="M426.226,374.511c-27.834,-48.159 -59.842,-46.654 -59.842,-46.654H130.073c0,0 -31.992,-1.505 -59.842,46.654H22.119l0.59,6.206c0.543,5.834 5.415,10.256 11.248,10.256h428.56c5.834,0 10.705,-4.422 11.249,-10.255l0.59,-6.206L426.226,374.511L426.226,374.511z"/>
    <path android:fillColor="#454545" android:pathData="M34.143,390.988c-5.741,0 -10.566,-4.313 -11.217,-10.007c-1.691,-14.786 -5.834,-40.665 -7.944,-53.589v93.448c0,10.566 8.564,19.146 19.146,19.146H66.43c10.566,0 19.146,-8.564 19.146,-19.146v-29.851H34.143z"/>
    <path android:fillColor="#ED6262" android:pathData="M43.436,171.588v-9.2c0,-8.27 -6.703,-14.972 -14.957,-14.972H15.571c-7.121,0 -13.653,4.717 -15.205,11.667c-1.567,6.997 2.017,13.995 8.378,16.896c7.09,3.243 14.786,4.918 22.575,4.918h15.05l3.925,-9.325h-6.858V171.588z"/>
    <path android:fillColor="#FFBD49" android:pathData="M65.142,240.46m-18.82,0a18.82,18.82 0,1 1,37.64 0a18.82,18.82 0,1 1,-37.64 0"/>
    <path android:fillColor="#FFDB6F" android:pathData="M103.03,255.308m-13.824,0a13.824,13.824 0,1 1,27.648 0a13.824,13.824 0,1 1,-27.648 0"/>
    <path android:fillColor="#6F6F6F" android:pathData="M348.294,341.029H148.179c-3.134,0 -5.663,2.544 -5.663,5.663c0,3.134 2.529,5.663 5.663,5.663h200.099c3.134,0 5.663,-2.544 5.663,-5.663C353.957,343.558 351.412,341.029 348.294,341.029z"/>
    <path android:fillColor="#6F6F6F" android:pathData="M348.294,363.635H148.179c-3.134,0 -5.663,2.544 -5.663,5.663c0,3.134 2.529,5.663 5.663,5.663h200.099c3.134,0 5.663,-2.544 5.663,-5.663C353.957,366.164 351.412,363.635 348.294,363.635z"/>
    <path android:fillColor="#3AA2EB" android:pathData="M420.098,132.009v1.939c0,7.804 -6.408,14.057 -14.367,13.731c-7.339,-0.465 -12.955,-7.028 -12.955,-14.522v-27.834c0,-6.563 -5.461,-11.559 -12.024,-11.388c-0.155,0 -0.155,0 -0.31,0c0,0 0,0 -0.155,0c-6.563,-0.155 -12.024,4.841 -12.024,11.388v25.181c0,7.804 -6.718,14.057 -14.522,13.576c-7.339,-0.31 -12.955,-7.028 -12.955,-14.367v-21.892c0,-6.563 -5.461,-12.024 -12.179,-12.024h-0.931c-6.253,0 -11.404,5.151 -11.404,11.404v28.47c0,7.804 -6.563,14.041 -14.522,13.731c-7.339,-0.465 -12.8,-7.028 -12.8,-14.522V96.712c0,-6.082 -5.306,-11.233 -11.559,-10.768h-0.931c-6.408,0 -11.543,5.151 -11.543,11.404v39.021c0,7.494 -6.082,13.731 -13.731,13.731c-7.339,0 -13.421,-5.927 -13.576,-13.265v-32.752c0,-7.494 -5.616,-14.041 -12.955,-14.522c-7.959,-0.31 -14.367,5.927 -14.367,13.731l-0.155,30.255c0,7.804 -6.392,14.041 -14.367,13.731c-7.199,-0.45 -12.66,-6.78 -12.878,-13.948V97.829c0,-7.37 -5.539,-13.964 -12.909,-14.398c-7.897,-0.341 -14.414,5.88 -14.414,13.7v37.19c-0.217,7.618 -6.609,13.684 -14.445,13.374c-7.339,-0.465 -12.8,-7.028 -12.8,-14.522v-27.85c0,-6.563 -5.616,-11.559 -12.179,-11.388c-0.155,0 -0.155,0 -0.155,0c-0.155,0 -0.155,0 -0.155,0c-6.718,-0.155 -12.179,4.841 -12.179,11.388v25.181c0,7.804 -6.563,14.057 -14.367,13.576c-6.796,-0.264 -11.854,-5.942 -12.722,-12.521c-7.215,14.941 -14.367,31.123 -21.318,48.407c128.403,10.116 257.412,10.116 385.815,0C434.139,162.636 427.126,146.655 420.098,132.009z"/>
</vector>

To use these in the map, we add some new parameters, set up some state, pass the state and management functions to the top bar, load the icon as we did the location icon, and display the car marker on the map

show in full file app/src/main/java/com/androidbyexample/compose/google/google/maps/GoogleMapDisplay.kt
// ...

@Composable
fun GoogleMapDisplay(
    currentLocation: Location?,
    carLatLng: LatLng?,
    cameraPositionState: CameraPositionState,
    onSetCarLocation: () -> Unit,
    onClearCarLocation: () -> Unit,
    modifier: Modifier,
) {
    // ...
    var currentLocationIcon by
        remember { mutableStateOf<BitmapDescriptor?>(null) }
    val carState = rememberMarkerState("car")
    var carIcon by remember { mutableStateOf<BitmapDescriptor?>(null) }
    val scope = rememberCoroutineScope()

    // ...
    Scaffold(
        topBar = {
            CarTopBar(
                currentLocation = currentLocation,
//              carLatLng = null,
//              onSetCarLocation = { TODO() },
//              onClearCarLocation = { TODO() },
                carLatLng = carLatLng,
                onSetCarLocation = onSetCarLocation,
                onClearCarLocation = onClearCarLocation,
                onWalkToCar = { TODO() },
                onGoToCurrentLocation = {
                    // ...
            )
        },
        content = { paddingValues ->
            Box(
                // ...
            ) {
                Column(
                    // ...
                ) {
                    // ...
                    GoogleMap(
                        // ...
                        onMapLoaded = {
                            // ...
                            scope.launch(Dispatchers.IO) {
                                // ...
                                        R.drawable.ic_current_location
                                    )
                                carIcon =
                                    context.loadBitmapDescriptor(
                                        R.drawable.ic_car
                                    )
                            }
                        },
                        // ...
                    ) {
                        // ...
                            )
                        }
                        carLatLng?.let {
                            carState.position = it
                            MarkerInfoWindowContent(
                                state = carState,
                                icon = carIcon,
                                anchor = Offset(0.5f, 0.5f),
                                title = stringResource(
                                    id = R.string.car_location
                                ),
                            )
                        }
                    }
                }
                // ...
            }
        }
    )
}

Finally, we collect and pass the location and management functions to the GoogleMapDisplay

show in full file app/src/main/java/com/androidbyexample/compose/google/google/maps/MainActivity.kt
// ...
class MainActivity : ComponentActivity() {
    // ...
    @SuppressLint("MissingPermission")
    fun startLocationAndMap() {
        // ...
        setContent {
            GoogleMapsTheme {
                // ...
                GoogleMapDisplay(
                    currentLocation = currentLocation,
                    carLatLng = carLatLng,
                    cameraPositionState = cameraPositionState,
                    onSetCarLocation = viewModel::setCarLocation,
                    onClearCarLocation = viewModel::clearCarLocation,
                    modifier = Modifier.fillMaxSize(),
                )
            }
        }
    }
    // ...
}

When we run the application, we can press the star icon to save the current location as the car's location. This will cause the car icon to be added to the map.

The location is saved in the preferences datastore, so if you close and reopen the app, the location will appear again.

Pressing the trash can will remove the location from the datastore, causing the icon to be removed.


All code changes

CHANGED: app/build.gradle.kts
plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.kotlin.compose)
alias(libs.plugins.secrets)
alias(libs.plugins.dokka)
} android { namespace = "com.androidbyexample.compose.google.google.maps" compileSdk = 35 defaultConfig { applicationId = "com.androidbyexample.compose.google.google.maps" 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_11 targetCompatibility = JavaVersion.VERSION_11 } kotlinOptions { jvmTarget = "11" } buildFeatures { compose = true } } dependencies {
implementation(libs.datastore.preferences)
// implementation(libs.icons.extended)
implementation(libs.location.services) implementation(libs.lifecycle.runtime.compose)
implementation(libs.maps.compose) implementation(libs.maps.compose.utils)
implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.activity.compose) implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.ui) implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) }
secrets { propertiesFileName = "secrets.properties" }
CHANGED: app/src/main/java/com/androidbyexample/compose/google/google/maps/CarViewModel.kt
package com.androidbyexample.compose.google.google.maps

import android.app.Application
import android.content.Context
import android.location.Location
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.google.android.gms.maps.model.LatLng
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch

class CarViewModel(application: Application) : AndroidViewModel(application) { private val LAT_PREF = stringPreferencesKey("lat") private val LON_PREF = stringPreferencesKey("lon") private val _currentLocation = MutableStateFlow<Location?>(null) val currentLocation: Flow<Location?> get() = _currentLocation fun updateLocation(location: Location?) { _currentLocation.value = location }
private val Context.preferencesDataStore: DataStore<Preferences> by preferencesDataStore(name = "carfinder") val carLatLng = application.preferencesDataStore.data.map { preferences -> preferences[LAT_PREF]?.let { latString -> preferences[LON_PREF]?.let { lonString -> LatLng(latString.toDouble(), lonString.toDouble()) } } } fun clearCarLocation() { viewModelScope.launch { getApplication<Application>().preferencesDataStore.edit { preferences -> preferences.remove(LAT_PREF) preferences.remove(LON_PREF) } } } fun setCarLocation() { viewModelScope.launch { _currentLocation.value?.let { location -> getApplication<Application>().preferencesDataStore.edit { preferences -> preferences[LAT_PREF] = location.latitude.toString() preferences[LON_PREF] = location.longitude.toString() } } ?: run { clearCarLocation() } } }
}
CHANGED: app/src/main/java/com/androidbyexample/compose/google/google/maps/GoogleMapDisplay.kt
package com.androidbyexample.compose.google.google.maps

import android.location.Location
import android.widget.Toast
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.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
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 com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.model.BitmapDescriptor
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.rememberMarkerState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

@Composable fun GoogleMapDisplay( currentLocation: Location?, carLatLng: LatLng?, cameraPositionState: CameraPositionState, onSetCarLocation: () -> Unit, onClearCarLocation: () -> Unit, 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 carState = rememberMarkerState("car") var carIcon by remember { mutableStateOf<BitmapDescriptor?>(null) }
val scope = rememberCoroutineScope() var initialBoundsSet by remember { mutableStateOf(false) } LaunchedEffect(key1 = mapLoaded, key2 = currentLocation) { if (mapLoaded) { 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() }, carLatLng = carLatLng, onSetCarLocation = onSetCarLocation, onClearCarLocation = onClearCarLocation,
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.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 )
carIcon = context.loadBitmapDescriptor( R.drawable.ic_car )
} }, 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 ), ) }
carLatLng?.let { carState.position = it MarkerInfoWindowContent( state = carState, icon = carIcon, anchor = Offset(0.5f, 0.5f), title = stringResource( id = R.string.car_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/java/com/androidbyexample/compose/google/google/maps/MainActivity.kt
package com.androidbyexample.compose.google.google.maps

import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Intent
import android.content.pm.PackageManager
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.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.app.ActivityCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.androidbyexample.compose.google.google.maps.ui.theme.GoogleMapsTheme
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.model.CameraPosition
import com.google.android.gms.maps.model.LatLng
import com.google.maps.android.compose.GoogleMap
import com.google.maps.android.compose.rememberCameraPositionState

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()
            }
        }

    @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()
        )

        enableEdgeToEdge()
setContent { GoogleMapsTheme { val googleHQ = LatLng(37.42423291057923, -122.08811454627153) val defaultCameraPosition = CameraPosition.fromLatLngZoom(googleHQ, 11f) val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition } val currentLocation by viewModel.currentLocation.collectAsStateWithLifecycle( initialValue = null ) val carLatLng by viewModel.carLatLng.collectAsStateWithLifecycle(initialValue = null)
GoogleMapDisplay( currentLocation = currentLocation, carLatLng = carLatLng, cameraPositionState = cameraPositionState, onSetCarLocation = viewModel::setCarLocation, onClearCarLocation = viewModel::clearCarLocation, modifier = Modifier.fillMaxSize(), )
} } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) GoogleApiAvailability.getInstance() .makeGooglePlayServicesAvailable(this) .addOnSuccessListener { if (ActivityCompat.checkSelfPermission( this, android.Manifest.permission.ACCESS_FINE_LOCATION ) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission( this, android.Manifest.permission.ACCESS_COARSE_LOCATION ) != PackageManager.PERMISSION_GRANTED ) { getLocationPermission.launch( arrayOf( android.Manifest.permission.ACCESS_FINE_LOCATION, android.Manifest.permission.ACCESS_COARSE_LOCATION, ) ) } else { startLocationAndMap() } }.addOnFailureListener(this) { Toast.makeText( this, "Google Play services required (or upgrade required)", Toast.LENGTH_SHORT ).show() finish() } } }
ADDED: app/src/main/res/drawable/ic_car.xml
<!-- Car icon made by Vectors Market (https://www.flaticon.com/authors/vectors-market) from https://www.flaticon.com --> <vector android:height="48dp" android:viewportHeight="496.474" android:viewportWidth="496.474" android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android"> <path android:fillColor="#E95353" android:pathData="M489.544,269.628c-0.729,-14.739 -6.206,-28.858 -15.205,-40.572c-20.294,-26.422 -24.25,-47.399 -24.25,-47.399c-15.934,-41.751 -30.099,-69.57 -40.51,-87.226c-10.954,-18.541 -29.898,-31.03 -51.262,-33.776c-42.961,-5.554 -177.23,-5.554 -220.191,0c-21.349,2.762 -40.293,15.251 -51.247,33.776c-10.411,17.656 -24.56,45.475 -40.51,87.226c0,0 -3.956,20.977 -24.25,47.399c-8.983,11.714 -14.476,25.833 -15.189,40.572c-1.536,31.977 7.727,39.005 16.012,111.368c0.652,5.694 5.461,9.976 11.202,9.976h428.203c5.741,0 10.55,-4.298 11.202,-9.976c2.327,-20.294 9.232,-61.456 9.232,-61.456C488.83,299.076 490.149,282.397 489.544,269.628z"/> <path android:fillColor="#168DE2" android:pathData="M441.074,179.982c-12.567,-31.449 -25.383,-58.911 -38.136,-80.803c-9.666,-16.555 -26.717,-28.113 -45.583,-30.642c-43.055,-5.71 -175.476,-5.663 -218.221,0c-18.866,2.529 -35.918,14.088 -45.584,30.642c-12.924,22.171 -25.91,50.005 -38.291,80.803C183.662,190.082 312.655,190.082 441.074,179.982z"/> <path android:fillColor="#FFFFFF" android:pathData="M422.844,216.349c-4.903,1.536 -9.464,3.693 -13.281,5.834c-6.951,3.879 -14.088,7.494 -21.535,10.318c-12.35,4.686 -14.445,11.683 -14.445,11.683c-0.372,0.481 -0.729,0.962 -1.071,1.458c-9.371,13.917 2.932,32.442 19.549,30.084c24.033,-3.398 43.83,-10.364 53.884,-14.398c5.756,-2.312 10.255,-7.121 11.729,-13.157c0.14,-0.574 0.264,-1.148 0.372,-1.707C461.74,226.543 442.191,210.314 422.844,216.349z"/> <path android:fillColor="#FFE21F" android:pathData="M448.522,327.857h-26.733c-4.018,0 -6.035,4.856 -3.196,7.711l26.733,26.733c2.839,2.839 7.711,0.822 7.711,-3.196v-26.733C453.036,329.874 451.019,327.857 448.522,327.857z"/> <path android:fillColor="#454545" android:pathData="M473.547,380.965c-0.652,5.71 -5.477,10.007 -11.217,10.007h-51.433v29.851c0,10.566 8.564,19.146 19.146,19.146h32.303c10.566,0 19.146,-8.564 19.146,-19.146v-93.448C479.366,340.315 475.239,366.179 473.547,380.965z"/> <path android:fillColor="#ED6262" android:pathData="M496.107,159.083c-1.552,-6.951 -8.083,-11.667 -15.205,-11.667h-12.909c-8.27,0 -14.972,6.703 -14.972,14.972v9.2h-6.842l3.925,9.325h15.05c7.789,0 15.5,-1.676 22.575,-4.918C494.105,173.078 497.674,166.096 496.107,159.083z"/> <path android:fillColor="#FFBD49" android:pathData="M431.315,240.46m-18.82,0a18.82,18.82 0,1 1,37.64 0a18.82,18.82 0,1 1,-37.64 0"/> <path android:fillColor="#FFDB6F" android:pathData="M393.458,255.308m-13.824,0a13.824,13.824 0,1 1,27.648 0a13.824,13.824 0,1 1,-27.648 0"/> <path android:fillColor="#6F6F6F" android:pathData="M357.634,255.324l-21.613,37.639l-175.569,0l-21.613,-37.639z"/> <path android:fillColor="#FFFFFF" android:pathData="M123.945,245.642c-0.326,-0.496 -0.683,-0.977 -1.071,-1.458c0,0 -2.095,-6.997 -14.445,-11.683c-7.447,-2.824 -14.569,-6.439 -21.535,-10.318c-3.832,-2.141 -8.378,-4.298 -13.281,-5.834c-19.332,-6.051 -38.896,10.193 -35.219,30.115c0.109,0.574 0.233,1.133 0.357,1.707c1.474,6.035 5.973,10.845 11.729,13.157c10.054,4.034 29.851,11 53.9,14.398C121.012,278.084 133.316,259.559 123.945,245.642z"/> <path android:fillColor="#FFE21F" android:pathData="M74.684,327.857H47.951c-2.498,0 -4.515,2.017 -4.515,4.515v26.733c0,4.018 4.872,6.035 7.711,3.196l26.733,-26.733C80.719,332.729 78.702,327.857 74.684,327.857z"/> <path android:fillColor="#777777" android:pathData="M426.226,374.511c-27.834,-48.159 -59.842,-46.654 -59.842,-46.654H130.073c0,0 -31.992,-1.505 -59.842,46.654H22.119l0.59,6.206c0.543,5.834 5.415,10.256 11.248,10.256h428.56c5.834,0 10.705,-4.422 11.249,-10.255l0.59,-6.206L426.226,374.511L426.226,374.511z"/> <path android:fillColor="#454545" android:pathData="M34.143,390.988c-5.741,0 -10.566,-4.313 -11.217,-10.007c-1.691,-14.786 -5.834,-40.665 -7.944,-53.589v93.448c0,10.566 8.564,19.146 19.146,19.146H66.43c10.566,0 19.146,-8.564 19.146,-19.146v-29.851H34.143z"/> <path android:fillColor="#ED6262" android:pathData="M43.436,171.588v-9.2c0,-8.27 -6.703,-14.972 -14.957,-14.972H15.571c-7.121,0 -13.653,4.717 -15.205,11.667c-1.567,6.997 2.017,13.995 8.378,16.896c7.09,3.243 14.786,4.918 22.575,4.918h15.05l3.925,-9.325h-6.858V171.588z"/> <path android:fillColor="#FFBD49" android:pathData="M65.142,240.46m-18.82,0a18.82,18.82 0,1 1,37.64 0a18.82,18.82 0,1 1,-37.64 0"/> <path android:fillColor="#FFDB6F" android:pathData="M103.03,255.308m-13.824,0a13.824,13.824 0,1 1,27.648 0a13.824,13.824 0,1 1,-27.648 0"/> <path android:fillColor="#6F6F6F" android:pathData="M348.294,341.029H148.179c-3.134,0 -5.663,2.544 -5.663,5.663c0,3.134 2.529,5.663 5.663,5.663h200.099c3.134,0 5.663,-2.544 5.663,-5.663C353.957,343.558 351.412,341.029 348.294,341.029z"/> <path android:fillColor="#6F6F6F" android:pathData="M348.294,363.635H148.179c-3.134,0 -5.663,2.544 -5.663,5.663c0,3.134 2.529,5.663 5.663,5.663h200.099c3.134,0 5.663,-2.544 5.663,-5.663C353.957,366.164 351.412,363.635 348.294,363.635z"/> <path android:fillColor="#3AA2EB" android:pathData="M420.098,132.009v1.939c0,7.804 -6.408,14.057 -14.367,13.731c-7.339,-0.465 -12.955,-7.028 -12.955,-14.522v-27.834c0,-6.563 -5.461,-11.559 -12.024,-11.388c-0.155,0 -0.155,0 -0.31,0c0,0 0,0 -0.155,0c-6.563,-0.155 -12.024,4.841 -12.024,11.388v25.181c0,7.804 -6.718,14.057 -14.522,13.576c-7.339,-0.31 -12.955,-7.028 -12.955,-14.367v-21.892c0,-6.563 -5.461,-12.024 -12.179,-12.024h-0.931c-6.253,0 -11.404,5.151 -11.404,11.404v28.47c0,7.804 -6.563,14.041 -14.522,13.731c-7.339,-0.465 -12.8,-7.028 -12.8,-14.522V96.712c0,-6.082 -5.306,-11.233 -11.559,-10.768h-0.931c-6.408,0 -11.543,5.151 -11.543,11.404v39.021c0,7.494 -6.082,13.731 -13.731,13.731c-7.339,0 -13.421,-5.927 -13.576,-13.265v-32.752c0,-7.494 -5.616,-14.041 -12.955,-14.522c-7.959,-0.31 -14.367,5.927 -14.367,13.731l-0.155,30.255c0,7.804 -6.392,14.041 -14.367,13.731c-7.199,-0.45 -12.66,-6.78 -12.878,-13.948V97.829c0,-7.37 -5.539,-13.964 -12.909,-14.398c-7.897,-0.341 -14.414,5.88 -14.414,13.7v37.19c-0.217,7.618 -6.609,13.684 -14.445,13.374c-7.339,-0.465 -12.8,-7.028 -12.8,-14.522v-27.85c0,-6.563 -5.616,-11.559 -12.179,-11.388c-0.155,0 -0.155,0 -0.155,0c-0.155,0 -0.155,0 -0.155,0c-6.718,-0.155 -12.179,4.841 -12.179,11.388v25.181c0,7.804 -6.563,14.057 -14.367,13.576c-6.796,-0.264 -11.854,-5.942 -12.722,-12.521c-7.215,14.941 -14.367,31.123 -21.318,48.407c128.403,10.116 257.412,10.116 385.815,0C434.139,162.636 427.126,146.655 420.098,132.009z"/> </vector>
CHANGED: app/src/main/res/values/strings.xml
<resources>
    <string name="app_name">Google Maps</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 Location</string>
    <string name="car_location">Car Location</string>
</resources>
CHANGED: gradle/libs.versions.toml
[versions]
agp = "8.7.3"
kotlin = "2.0.21"
coreKtx = "1.15.0"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.9.3"
composeBom = "2024.12.01"
secrets = "2.0.1"
maps-compose = "6.2.1"
location-services = "21.3.0" lifecycle-runtime-compose = "2.8.7"
dokka = "1.9.20"
icons-extended = "1.7.5"
datastore-preferences = "1.1.1" [libraries] datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore-preferences" }
icons-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "icons-extended" }
location-services = { group = "com.google.android.gms", name = "play-services-location", version.ref = "location-services" } lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle-runtime-compose" }
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" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-ui = { group = "androidx.compose.ui", name = "ui" } androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secrets" }
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }