Search code examples
androidkotlinandroid-jetpack-composeandroid-lifecyclemapbox-android

How to handle activity lifecycle events for Mapbox Map in Jetpack Compose?


in the official documentation for Mapbox SDK for android there is a simple example of map using standard UI library for Android. Full documentation for completeness of the question can be found here.

The code mentioned there is like this:

private var mapView: MapView? = null
 
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
 
Mapbox.getInstance(this, getString(R.string.mapbox_access_token))
 
setContentView(R.layout.activity_main)
 
mapView = findViewById(R.id.mapView)
mapView?.onCreate(savedInstanceState)
mapView?.getMapAsync { mapboxMap ->
 
    mapboxMap.setStyle(Style.MAPBOX_STREETS) {
 
        // Map is set up and the style has loaded. Now you can add data or make other map adjustments
    }
 
   }
}

Code for handling activity and mapbox map lifecycle events is like this:

override fun onStart() {
    super.onStart()
    mapView?.onStart()
}
 
override fun onResume() {
    super.onResume()
    mapView?.onResume()
}
 
override fun onPause() {
    super.onPause()
    mapView?.onPause()
}
 
override fun onStop() {
    super.onStop()
    mapView?.onStop()
}
 
override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)
    mapView?.onSaveInstanceState(outState)
}
 
override fun onLowMemory() {
    super.onLowMemory()
    mapView?.onLowMemory()
}
 
override fun onDestroy() {
    super.onDestroy()
    mapView?.onDestroy()
}

My question is, if one is using mapbox map in jetpack compose, are those events of the map dealt with automatically, or does developer need to handle them himself? I am asking, because I do not want to have any doubts how to deal with the best practices regarding the AndroidView in jetpack compose especially when the map is placed in composable outside of any particular activity or fragment.

Jetpack compose code snippet for completeness:

@Composable
fun MapWithFab() {

    ConstraintLayout(modifier = Modifier.fillMaxSize()) {
        val mapboxMap = createRef()
        val fab = createRef()

        AndroidView(
            modifier = Modifier
                .fillMaxSize()
                .padding(bottom = 35.dp)
                .constrainAs(mapboxMap) {
                    top.linkTo(parent.top)
                    start.linkTo(parent.start)
                    end.linkTo(parent.end)
                    bottom.linkTo(parent.bottom)
                },
            factory = { context ->
                Mapbox.getInstance(
                    context,
                    context.getString(R.string.mapbox_access_token)
                )
                MapView(context).apply {
                    getMapAsync { mapboxMap ->
                        mapboxMap.setStyle(Style.MAPBOX_STREETS)

                        val position = CameraPosition.Builder()
                            .target(LatLng(51.04004014308637, 13.744085852141072))
                            .zoom(15.0)
                            .build()

                        mapboxMap.animateCamera(CameraUpdateFactory.newCameraPosition(position), 1)
                    }
                }
            }
        )

        FloatingActionButton(
            onClick = { },
            modifier = Modifier
                .padding(25.dp)
                .width(50.dp)
                .height(50.dp)
                .constrainAs(fab) {
                    end.linkTo(mapboxMap.end)
                    bottom.linkTo(mapboxMap.bottom)
                }
        ) {

        }
    }
}

Thank you for your answers and ideas.


Solution

  • It turns out to be mentioned in one of the example projects on compose right from the google, the example below was taken from google's example project Crane and direct link is not available currently. Probably google's official new versions of map is already lifecycle aware just like for mapbox.

    It could be done something like this:

    @Composable
    fun MapWrapper() {
        ConstraintLayout(modifier = Modifier.fillMaxSize()) {
            val mapboxMap = createRef()
            val fab = createRef()
            val mapView = rememberMapViewWithLifecycle()
    
    
            AndroidView(
                factory = {mapView},
                modifier = Modifier.constrainAs(mapboxMap) {
                    top.linkTo(parent.top)
                    bottom.linkTo(parent.bottom)
                    start.linkTo(parent.start)
                    end.linkTo(parent.end)
            })
    
            FloatingActionButton(
                onClick = {
    
                },
                modifier = Modifier
                    .padding(25.dp)
                    .width(50.dp)
                    .height(50.dp)
                    .constrainAs(fab) {
                        end.linkTo(mapboxMap.end)
                        bottom.linkTo(mapboxMap.bottom)
                    }
            ) {
    
            }
        }
    }
    
    @Composable
    fun rememberMapViewWithLifecycle(): MapView {
        val context = LocalContext.current
        val mapView = remember {
            Mapbox.getInstance(
                context,
                context.getString(R.string.mapbox_access_token)
            )
            MapView(context).apply {
                val mapView = this
                getMapAsync { mapboxMap ->
                    mapboxMap.setStyle(Style.MAPBOX_STREETS)
    
                    val position = CameraPosition.Builder()
                        .target(LatLng(70.04004014308637, -20.744085852141072))
                        .zoom(15.0)
                        .build()
    
                    mapboxMap.animateCamera(CameraUpdateFactory.newCameraPosition(position), 1)
    
                    mapboxMap.getStyle {
                    }
                }
            }
        }
    
        val lifecycle = LocalLifecycleOwner.current.lifecycle
        DisposableEffect(lifecycle, mapView) {
            // Make MapView follow the current lifecycle
            val lifecycleObserver = getMapLifecycleObserver(mapView)
            lifecycle.addObserver(lifecycleObserver)
            onDispose {
                lifecycle.removeObserver(lifecycleObserver)
            }
        }
    
        return mapView
    }
    
    /**
     * Handles lifecycle of provided mapView
     */
    private fun getMapLifecycleObserver(mapView: MapView): LifecycleEventObserver =
        LifecycleEventObserver { _, event ->
            when (event) {
                Lifecycle.Event.ON_CREATE -> mapView.onCreate(Bundle())
                Lifecycle.Event.ON_START -> mapView.onStart()
                Lifecycle.Event.ON_RESUME -> mapView.onResume()
                Lifecycle.Event.ON_PAUSE -> mapView.onPause()
                Lifecycle.Event.ON_STOP -> mapView.onStop()
                Lifecycle.Event.ON_DESTROY -> mapView.onDestroy()
                else -> throw IllegalStateException()
            }
        }
    

    It can be implemented this way for pretty much any AndroidView, which needs to be responsive to the lifecycle events. The only thing which this implementation lacks is that there is no handling of onLowMemory() case.

    Update: The solution above is solving the lifecycle invocations for MapboxMaps v9, MapboxMaps v10 already have it integrated.