How to Integrate Google Maps in Jetpack Compose with Ola Maps API
Introduction
In this blog post, I’ll show you how to build an Android app using Jetpack Compose that integrates Google Maps and Ola Maps API. We’ll create a search bar at the top of the screen, utilize the Ola Maps API for search functionality, and implement nearby search chips for categories like restaurants, gyms, hospitals, and ATMs. Additionally, we’ll display the top 5 nearby places in a bottom sheet and highlight their locations on the map with markers.
By the end of this tutorial, you will learn:
- How to integrate Google Maps in Jetpack Compose.
- How to use Ola Maps API for searching places.
- How to implement chips for quick category search.
- How to display search results using markers on the map and a bottom sheet.
Step 1: Setting Up the Project
Before we dive into code, make sure your project is set up with the necessary dependencies. Add the following dependencies in your build.gradle
file:
dependencies {
implementation 'com.google.android.libraries.maps:maps:3.1.0'
implementation "androidx.compose.ui:ui:1.5.0"
implementation "androidx.compose.material3:material3:1.0.0-alpha15"
implementation "com.google.accompanist:accompanist-permissions:0.26.2-beta"
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
}
Step 2: Integrating Google Maps with Jetpack Compose
To display a map, we’ll use the Google Maps SDK and wrap it inside Jetpack Compose. This part focuses on how to load the map, handle user location, and add markers dynamically.
@Composable
fun MapScreen(latLng: LatLng) {
var query by remember { mutableStateOf("") }
var isSearchActive by remember { mutableStateOf(false) }
var nearbySpots by remember { mutableStateOf(listOf<NearbySpot>()) }
var isBottomSheetVisible by remember { mutableStateOf(false) }
var selectedType by remember { mutableStateOf("") }
var markers by remember { mutableStateOf<List<MarkerOptions>>(emptyList()) }
var isSatelliteView by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
val cameraPositionState =
rememberCameraPositionState { position = CameraPosition.fromLatLngZoom(latLng, 10f) }
val chipItems = listOf(
ChipItem("Restaurants", "restaurant"),
ChipItem("Gym", "gym"),
ChipItem("Hospitals", "hospital"),
ChipItem("ATMs", "atm")
)
fun animateToLocation(location: LatLng) {
scope.launch {
cameraPositionState.animate(
update = CameraUpdateFactory.newLatLngZoom(location, 15f),
durationMs = 500 // Duration of the animation
)
}
}
fun toggleMapView() {
isSatelliteView = !isSatelliteView
}
fun onSearch(searchQuery: String) {
val location = "${latLng.latitude},${latLng.longitude}"
scope.launch {
markers = RetrofitClient.instance.getAutocomplete(
location, searchQuery, Constants.OLA_API_KEY
).predictions.map {
MarkerOptions().position(
LatLng(
it.geometry.location.lat, it.geometry.location.lng
)
).title(it.structured_formatting.main_text)
}
}
}
Box(Modifier.fillMaxSize()) {
GoogleMap(
modifier = Modifier.fillMaxSize(),
cameraPositionState = cameraPositionState,
uiSettings = MapUiSettings(zoomControlsEnabled = true),
properties = MapProperties(mapType = if (isSatelliteView) MapType.SATELLITE else MapType.NORMAL)
) {
MarkerInfoWindow(
state = MarkerState(position = latLng),
title = "Current Location",
snippet = "This is where you are!",
icon = BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_GREEN)
)
markers.forEach { markerOptions ->
Marker(
state = MarkerState(position = markerOptions.position),
title = markerOptions.title
)
}
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
CustomSearchBar(query = query,
onQueryChange = { query = it },
onSearch = ::onSearch,
active = isSearchActive,
onActiveChange = { isSearchActive = it },
modifier = Modifier.fillMaxWidth(),
placeHolder = { Text(stringResource(R.string.search_here), color = Color.Gray) },
leadingIcon = {
Icon(
Icons.Default.Search, contentDescription = "Search Icon", tint = Color.Gray
)
}
) {
if (query.isNotEmpty()) {
IconButton(onClick = { query = "" }) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "Clear Icon",
tint = Color.Gray
)
}
}
}
LazyRow(modifier = Modifier.fillMaxWidth()) {
items(chipItems) { chip ->
ChipItemView(chip, chip.type == selectedType) {
selectedType = chip.type
val location = "${latLng.latitude},${latLng.longitude}"
scope.launch {
val nearbySpotResponse = RetrofitClient.instance.fetchNearbySpots(
"venue",
chip.type,
location,
Constants.OLA_API_KEY
)
// Map response to NearbySpot objects
nearbySpots = nearbySpotResponse.predictions.map { prediction2 ->
NearbySpot(
placeId = prediction2.place_id,
mainText = prediction2.structured_formatting.main_text,
secondaryText = prediction2.structured_formatting.secondary_text,
distanceMeters = prediction2.distance_meters
)
}
isBottomSheetVisible = true
}
}
}
}
Box(
modifier = Modifier
.padding(16.dp)
) {
Column {
IconButton(onClick = { toggleMapView() }) {
Icon(
imageVector = if (isSatelliteView) Icons.Outlined.CheckCircle else Icons.Filled.CheckCircle,
contentDescription = if (isSatelliteView) "Normal View" else "Satellite View"
)
}
}
}
}
//----
FloatingActionButtonView { animateToLocation(latLng) }
if (isBottomSheetVisible) {
NearbySpotsBottomSheet(nearbySpots) { isBottomSheetVisible = false }
}
}
}
Step 3: Adding the Search Bar with Ola Maps API
Now, we’ll implement a search bar at the top of the screen using Ola Maps API for querying locations.
@Composable
fun CustomSearchBar(
query: String,
onQueryChange: (String) -> Unit,
onSearch: (String) -> Unit,
active: Boolean,
onActiveChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
placeHolder: @Composable (() -> Unit)? = null,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
) {
val focusRequester = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
Box(modifier = modifier) {
BasicTextField(value = query,
onValueChange = onQueryChange,
textStyle = TextStyle(fontSize = 16.sp, color = Color.Black),
enabled = enabled,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions(onSearch = { onSearch(query) }),
singleLine = true,
modifier = Modifier
.height(56.dp)
.focusRequester(focusRequester)
.onFocusChanged { onActiveChange(it.isFocused) }
.fillMaxWidth()
.background(Color.White, RoundedCornerShape(30.dp))
.border(1.dp, Color.LightGray, RoundedCornerShape(30.dp))
.padding(horizontal = 16.dp, vertical = 12.dp),
decorationBox = { innerTextField ->
Row(verticalAlignment = Alignment.CenterVertically) {
leadingIcon?.invoke()
Spacer(modifier = Modifier.width(8.dp))
Box(Modifier.weight(1f)) {
if (query.isEmpty()) placeHolder?.invoke()
innerTextField()
}
trailingIcon?.invoke()
}
})
LaunchedEffect(active) {
if (!active) focusManager.clearFocus()
}
}
}
Step 4: Implementing Chips for Nearby Search
We’ll add chips below the search bar to provide quick access to categories like restaurants, gyms, hospitals, etc. When a chip is clicked, we’ll display the top 5 nearby places in a bottom sheet.
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ChipItemView(chip: ChipItem, isSelected: Boolean, onClick: () -> Unit) {
FilterChip(
selected = isSelected,
onClick = onClick,
label = { Text(chip.label) },
modifier = Modifier.padding(8.dp),
colors = FilterChipDefaults.filterChipColors(
containerColor = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface
)
)
}
Step 5: Displaying Results in a Bottom Sheet and Markers on the Map
When the nearby places are fetched, we’ll display them in a bottom sheet and also show markers on the map.
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NearbySpotsBottomSheet(spots: List<NearbySpot>, onDismiss: () -> Unit) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
ModalBottomSheet(sheetState = sheetState, onDismissRequest = onDismiss) {
Column(Modifier.padding(16.dp)) {
Text("Nearby Spots", style = MaterialTheme.typography.headlineSmall)
spots.forEach { spot ->
Column(Modifier.padding(vertical = 8.dp)) {
Text(spot.mainText, fontWeight = FontWeight.Bold)
Text(spot.secondaryText)
Text("${spot.distanceMeters} meters away")
}
}
}
}
}
Conclusion
In this tutorial, we successfully integrated Google Maps with Jetpack Compose, used Ola Maps API for search, and displayed nearby places based on categories using chips. We also demonstrated how to show search results in a bottom sheet and add markers to the map for easy visualization.
Feel free to check out the code on Github and customize it for your own projects!
Connect with me on LinkedIn.
Best Regards.