Use GoogleMaps and get Marker Address Details in Jetpack Compose

Use GoogleMaps and get Marker Address Details in Jetpack Compose

Output:

Pre-requisites:

  1. Prior knowledge of MVVM & Jetpack compose is mandatory, I won’t explain every single thing.

  2. Better to test it on a Physical device.

NOTE: We will only focus on the functionality and not the UI. You can create your UI and freely use this implementation.

Implementation:

Dependency:

implementation "com.google.maps.android:maps-compose:2.11.1"
implementation "com.google.android.gms:play-services-maps:18.1.0"
implementation "com.google.android.gms:play-services-location:21.0.1"
//+other coroutine/arch related(depends on your implementation)

Steps:

  1. Create UI :

After having the dependencies above, we can have a composable named ‘GoogleMap’, that we can use to show maps in our UI. I will only show the implementation for that, the rest you can design by yourself.

Box(
    modifier = Modifier
        .fillMaxSize()
        .padding(top = padding.calculateTopPadding())
) {
    GoogleMap(
        cameraPositionState = cameraPositionState
    ) {
        Marker(
            state = MarkerState(
                position = cameraPositionState.position.target
            )
        )
    }
}

There you can see we are using a ‘cameraPositionState’, that will help us to get the marker position and other values.

val cameraPositionState = rememberCameraPositionState()//Nothing different(using rememberSaveable internally)
//Will have default value as LatLng(0.0,0.0)

Note: Getting the current location is a whole different thing. Here our goal is to just get the address detail of the marker on the map.

2. Now we know we can get the LatLng values on the map using the cameraPositionState, so now we will create a method to get the address details based on the current LatLng values, and every time our LatLng values change, we will just call that method and get the address details.

We will create the method in ViewModel.

private val _markerAddressDetail = MutableStateFlow<ResponseState<Address>>(ResponseState.Idle)//ResponseState is a wrapper class
val markerAddressDetail = _markerAddressDetail.asStateFlow()

fun getMarkerAddressDetails(lat: Double, long: Double, context: Context) {
    _markerAddressDetail.value = ResponseState.Loading//We will show loading first
        try {
            //Not a good practice to pass context in vm, instead inject this Geocoder
            val geocoder = Geocoder(context, Locale.getDefault())
            if (SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                geocoder.getFromLocation(//Pass LatLng and get address
                    lat,
                    long,
                    1,//no. of addresses you want
                ) { p0 ->
                    _markerAddressDetail.value = ResponseState.Success(p0[0])
                }
            } else {
                val addresses = geocoder.getFromLocation(//This method is deprecated for >32
                    lat,
                    long,
                    1,
                )
                _markerAddressDetail.value =
                        if(!addresses.isNullOrEmpty()){//The address can be null or empty
                            ResponseState.Success(addresses[0])
                        }else{
                            ResponseState.Error(Exception("Address is null"))
                        }
            }
        } catch (e: Exception) {
            _markerAddressDetail.value = ResponseState.Error(e)
        }
}
sealed class ResponseState<out T> {
    object Idle : ResponseState<Nothing>()
    object Loading : ResponseState<Nothing>()
    data class Error(val error: Throwable) : ResponseState<Nothing>()
    data class Success<R>(val data: R) : ResponseState<R>()
}

3. Cool, now we have the method to pass the LatLng and magically get the address.

But, when should we call it?

We have to call it every time our LatLng values change in the UI.

//Add it anywhere after declaring cameraPositionState
LaunchedEffect(cameraPositionState.position.target) {
    mapsViewModel.getMarkerAddressDetails(
        cameraPositionState.position.target.latitude,//Use the object we created
        cameraPositionState.position.target.longitude,
        context
    )
}

See details for LaunchEffect working here.

4. Now, we will call our method every time our LatLng value changes from the map marker. And we are storing the result in the ‘markerAddressDetail’ in our vm. So, collect this StateFlow and check if you got the Address.

val address by mapsViewModel.markerAddressDetail.collectAsStateWithLifecycle()
when (address) {
    is ResponseState.Loading -> {
        LoadingAnimation()
    }

    is ResponseState.Success -> {
        Log.d("MAP",address.data.getAddressLine(0))
    }

    is ResponseState.Error -> {
        scope.launch {
            mapsViewModel.getMarkerAddressDetails(
                cameraPositionState.position.target.latitude,
                cameraPositionState.position.target.longitude,
                context
            )
        }
    }

    else -> {}
}

Here, I am just showing the Log but in my app, I have a complete UI with proper handling of the cases.

You can observe your Logcat and keep changing the cameraPosition in the Map.

5. Wohoo! It is working as expected now!!

Flow Summary:

GoogleMap UI is shown to the user with a marker →

When the user scrolls, cameraPosition changes →

LaunchEffect will be invoked every time cameraPosition changes →

getMarkerAddressDetails is called inside LaunchEffect →

address is stored in markerAddressDetail StateFlow →

We are observing this StateFlow from the UI, so UI is also updated automatically.

Reference.

I hope you found this helpful. If yes, then do FOLLOW ‘Sagar Malhotra’ for more Android-related content.

#androidWithSagar #android #androiddevelopment #development #compose #kotlin #proandroiddev #google #androiddev