Using Google Maps in a Jetpack Compose app

Share on:

I’m in process of migrating Galway Bus over to use Jetpack Compose and one particular requirement is the ability to show bus stops (and also bus positions) on a Google Map. It seems like this is something that ultimately will exist as specific Jetpack Compose @Composable but for now it looks like only way to add a map like this is using AndroidView.

The following is what I currently have (very much open to suggestions for better way to do anything here)….it uses an AndroidViewto show the map in a Column above the list of nearby bus stops. As the user moves around on the map we call in to ViewModel to set location and also to trigger retrieval of updated list of bus stops (and in turn recomposition of UI). It also adds marker on map for each of the stops.

@Composable
fun BusStopListBody(viewModel: GalwayBusViewModel, fragmentManager: FragmentManager) {
    val busStopState = viewModel.uiState.observeAsState(UiState.Loading)
    val currentLocation = viewModel.location.observeAsState()

    Column {
        val uiState = busStopState.value
        if (uiState is UiState.Success) {

            AndroidView(resId = R.layout.map_layout, modifier = Modifier.weight(0.4f)) {
                val mapFragment = fragmentManager.findFragmentById(R.id.map) as SupportMapFragment

                mapFragment.getMapAsync { map ->

                    map.isMyLocationEnabled = true
                    map.uiSettings.isZoomControlsEnabled = true

                    currentLocation.value?.let {
                        map.moveCamera(CameraUpdateFactory.newLatLngZoom(LatLng(it.latitude, it.longitude), 15.0f))
                    }

                    map.setOnCameraIdleListener{
                        val cameraPosition = map.cameraPosition
                        val location = Location(cameraPosition.target.latitude, cameraPosition.target.longitude)
                        viewModel.setLocation(location)
                        viewModel.getNearestStops(location)
                    }

                    for (busStop in uiState.data) {
                        val busStopLocation = LatLng(busStop.latitude.toDouble(), busStop.longitude.toDouble())

                        val icon = bitmapDescriptorFromVector(it.context, R.drawable.ic_stop, R.color.mapMarkerGreen)
                        val markerOptions = MarkerOptions()
                                .title(busStop.shortname)
                                .position(busStopLocation)
                                .icon(icon)

                        val marker = map.addMarker(markerOptions)
                        marker.tag = busStop
                    }
                }
            }
        }

        Box(modifier = Modifier.weight(0.6f)) {
            when (val uiState = busStopState.value) {
                is UiState.Success -> {
                    LazyColumnItems(items = uiState.data) { stop ->
                        StopViewRow(stop) {
                            viewModel.navigateTo(Screen.BusStopView(stop.stopid, stop.shortname))
                        }
                    }
                }
                is UiState.Loading -> {
                    Box(modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.Center)) {
                        CircularProgressIndicator()
                    }
                }
                is UiState.Error -> {
                    Snackbar(text = { Text("Error retrieving bus stop info") })
                }
            }
        }
    }
}

The map_layout.xml layout file referred to in AndroidView contains following

<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/map"
    android:name="com.google.android.gms.maps.SupportMapFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

This is still very much work in progress and there are some known limitations around rendering of map if you “navigate” to another compose screen and back to this one again.

Featured in Kotlin Weekly Issue #208

Purpose of providing this is primarily to help “close the loop” and provide way to post feedback/ask questions etc.

For example, Leland Richardson has replied with suggestion to use emitView (and to structure things somewhat differently)…. emitView was introduced in Compose dev15 release which unfortunately we can’t use right now due to following