Closed testus74966 closed 1 day ago
@testus74966 for Maps SDK, we have a optional compose extension currently in preview that helps to integrate Maps to the app using jetpack compose, and it handles the Map lifecycle properly. Unfortunately there's no NavigationView
compose extension integration yet at the moment, would be good to open a ticket in Navigation SDK.
Alternatively, you can reference to our lifecycle implementation to properly handle the lifecycle of your NavigationView
, I think the main thing needed is to call mapView.destroy
in the DisposableEffect.onDispose
.
Closing as stale.
Environment
Observed behavior and steps to reproduce
I have used Map Box Navigation to show the navigation directions from one location to another location using Android View in Jetpack Compose. After going back to previous screen I also removed the Map Box Screen from back stack of Jetpack Compose. But after removing the screen, memory space captured by Map Box using Android View Jetpack Compose is not freeing up. This is causing my android application to work slow. Please try to fix this issue. Below are the details with Evidences.
https://gi thub.com/mapbox/mapbox-maps-android/assets/162621406/363d3b88-6d0e-4487-9a78-285232bbd183
Map Box Navigation Code: - import android.Manifest import android.content.Context import android.content.pm.PackageManager import android.location.Location import android.os.Build import androidx.activity.compose.BackHandler import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.viewinterop.AndroidView import androidx.core.app.ActivityCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner import androidx.navigation.NavController import com.google.firebase.crashlytics.ktx.crashlytics import com.google.firebase.ktx.Firebase import com.google.gson.Gson import com.mapbox.geojson.Point import com.mapbox.navigation.core.MapboxNavigation import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp import com.mapbox.navigation.core.trip.session.LocationMatcherResult import com.mapbox.navigation.core.trip.session.LocationObserver import com.mapbox.navigation.dropin.NavigationView import com.metropavia.R import com.metropavia.utils.AppConstants.EMPTY import com.metropavia.utils.AppConstants.HOSPITAL_ROUTE_SCREEN import com.metropavia.utils.CommonMethodsUtils.printLog import com.metropavia.utils.Utils.requestRoutes import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch
/**
@Desc: MapBoxNavigationUI to start the MapBox navigation directions by using Drop In UI of MapBox */ @RequiresApi(Build.VERSION_CODES.S) @Composable fun MapBoxNavigationScreen( destLat: Double, destLong: Double, navController: NavController, context: Context = LocalContext.current ) { val openDialog: MutableState = remember { mutableStateOf(EMPTY) }
val isInComposition = remember { mutableStateOf(true) }
val mapBoxNavigation: MutableState<MapboxNavigation?> = remember { mutableStateOf(null) }
val locationObserver: MutableState<LocationObserver?> = remember { mutableStateOf(null) }
val nvView: MutableState<NavigationView?> = remember { mutableStateOf(null) }
val lastLocation: MutableState<Location?> = remember { mutableStateOf(null) }
val currentLatitude: MutableState<Double?> = remember { mutableStateOf(null) }
val currentLongitude: MutableState<Double?> = remember { mutableStateOf(null) }
DisposableEffect(Unit) { mapBoxNavigation.value = MapboxNavigationApp.current() onDispose { mapBoxNavigation.value = null locationObserver.value = null lastLocation.value = null nvView.value = null currentLatitude.value = null currentLongitude.value = null isInComposition.value = false Runtime.getRuntime().gc() } } printLog("destLaLong", "$destLat, $destLong") //Compose Lifecycles ComposableLifecycle { _, event -> when (event) { Lifecycle.Event.ON_CREATE -> { if (ActivityCompat.checkSelfPermission( context, Manifest.permission.ACCESS_FINE_LOCATION ) == PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission( context, Manifest.permission.ACCESS_COARSE_LOCATION ) == PackageManager.PERMISSION_GRANTED ) { mapBoxNavigation.value?.startTripSession() // nvView.value?.api?.routeReplayEnabled(true) //Starts map navigation by default } printLog("Compose_Lifecycles", "on create") }
}
//On Back press unregistering the location observer and clearing the current location values BackHandler { locationObserver.value?.let { location -> mapBoxNavigation.value?.unregisterLocationObserver(location) } currentLongitude.value = null currentLongitude.value = null navController.navigate(HOSPITAL_ROUTE_SCREEN) { popUpTo(navController.graph.id) { inclusive = false } } } //* Exposes raw updates coming directly from the location services getLocationObserver(locationObserver, lastLocation, currentLatitude, currentLongitude)
//Calling request routes ones to show map box navigation directions in Map Box within the app StartMapBoxNavigation( currentLatitude, currentLongitude, nvView, openDialog, navController, locationObserver )
if (isInComposition.value) { //MabBox with navigation view to show and handle the Map Box UI AndroidView( modifier = Modifier .fillMaxSize(1f), factory = { myContext -> NavigationView( context = myContext, accessToken = myContext.getString(R.string.mapbox_access_token) ).apply { nvView.value = this nvView.value?.customizeViewOptions { enableMapLongClickIntercept = false } } }, update = { //Registering location Observer to check the location accurately locationObserver.value?.let { mapBoxNavigation.value?.registerLocationObserver(it) } }, onRelease = { navigationView -> //https://blog.stackademic.com/how-to-use-android-view-inside-jetpack-compose-and-vise-versa-843596485c5d navigationView.apply { this.removeAllViews() Runtime.getRuntime().gc() } } ) } /* AndroidViewBinding( modifier = Modifier .fillMaxSize(1f), factory = ActivityMapBoxNavigationBinding::inflate ) { nvView.value = navigationView nvView.value?.customizeViewOptions { enableMapLongClickIntercept = false }
}
/**
@Desc: Composable Method to manage the Compose Lifecycles */ @Composable fun ComposableLifecycle( lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, onEvent: (LifecycleOwner, Lifecycle.Event) -> Unit ) { DisposableEffect(lifecycleOwner) { val observer = LifecycleEventObserver { source, event -> onEvent(source, event) } lifecycleOwner.lifecycle.addObserver(observer)
} }
/**
@Desc: Method to get the live location using location observer */ fun getLocationObserver( locationObserver: MutableState<LocationObserver?>, lastLocation: MutableState<Location?>, currentLatitude: MutableState<Double?>, currentLongitude: MutableState<Double?> ): MutableState<LocationObserver?> { locationObserver.value = object : LocationObserver { override fun onNewLocationMatcherResult(locationMatcherResult: LocationMatcherResult) { lastLocation.value = locationMatcherResult.enhancedLocation printLog("last_location", Gson().toJson(lastLocation)) }
} return locationObserver }
/**
current location to the destination location */ @Composable fun StartMapBoxNavigation( currentLatitude: MutableState<Double?>, currentLongitude: MutableState<Double?>, nvView: MutableState<NavigationView?>, showMsg: MutableState,
navController: NavController,
locationObserver: MutableState<LocationObserver?>,
dispatcher: CoroutineDispatcher = Dispatchers.IO
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val mapBoxNavigation: MutableState<MapboxNavigation?> = remember { mutableStateOf(null) }
val openDialog: MutableState = remember { mutableStateOf(false) }
DisposableEffect(Unit) { mapBoxNavigation.value = MapboxNavigationApp.current() onDispose { mapBoxNavigation.value = null } } //Calling request routes ones to show map box navigation directions in Map Box within the app LaunchedEffect(key1 = (currentLatitude.value != null) && (currentLongitude.value != null)) { //Calling find routes to draw route path and get navigation directions printLog( "latitude_longitude", "Start Location:\n Lat: ${currentLatitude.value},\n Long: ${currentLongitude.value}" + " \n Destination Location:\n Lat: ${newLatitude.value},\n Long: ${newLongitude.value}" ) currentLongitude.value?.let { long -> currentLatitude.value?.let { lat -> printLog( "latitude_longitude", "Start Location:\n Lat: ${currentLatitude.value},\n Long: ${currentLongitude.value}" + " \n Destination Location:\n Lat: ${newLatitude.value},\n Long: ${newLongitude.value}" ) nvView.value?.let { scope.launch(dispatcher) {//28.62047897993122, 77.37284099069734 try { requestRoutes( mapBoxNavigation.value, context, it, //28.55549253851863, 77.55376514865961-Fortis Hospital Point.fromLngLat(long, lat), //28.944144718191748, 77.33268965606032 Point.fromLngLat( //28.619763217171204, 77.37181101826435 /API lat and long/ client lat and long/ Noida lat and long/ newLongitude.value.toDouble() /77.37181101826435/, newLatitude.value.toDouble()/28.619763217171204/ ), showMsg, true ) } catch (ex: Exception) { Firebase.crashlytics.recordException(ex) } } } } } }
//Showing Alert Message if the route is not available for longer distances if (showMsg.value.isNotEmpty()) { openDialog.value = true ShowAlertDialog( openDialog = openDialog, title = showMsg.value ) { scope.launch(dispatcher) { locationObserver.value?.let { location -> mapBoxNavigation.value?.unregisterLocationObserver(location) } mapBoxNavigation.value?.stopTripSession() currentLatitude.value = null currentLongitude.value = null } scope.launch { navController.navigate(HOSPITAL_ROUTE_SCREEN) { popUpTo(navController.graph.id) { inclusive = true } } } } } }
/**
@param startRoutePlay Boolean value to start the Navigation Guidance for the patient */ fun requestRoutes( mapBoxNavigation : MapboxNavigation?, context: Context, nvView: NavigationView, origin: Point, destination: Point, openDialog: MutableState,
startRoutePlay: Boolean
) {
mapBoxNavigation?.requestRoutes(
routeOptions = RouteOptions
.builder()
.applyDefaultNavigationOptions()
.applyLanguageAndVoiceUnitOptions(context)
.coordinatesList(listOf(origin, destination))
.alternatives(true)
.build(),
callback = object : NavigationRouterCallback {
override fun onCanceled(routeOptions: RouteOptions, routerOrigin: RouterOrigin) {
printLog(
"onCancelled",
"${Gson().toJson(routeOptions)}, ${Gson().toJson(routerOrigin)}"
)
}
// showToast(reasons[0].message, context) }
// nvView.api.routeReplayEnabled(true) //Starts map navigation by default nvView.api.startRoutePreview(routes) if (startRoutePlay) { nvView.api.startActiveGuidance(routes) } } } ) }
Expected behavior
We want that after removing the screen from back stack i.e. after leaving the Map Box Screen. Map Box must not take the memory space as captured. It should release the memory after completion of navigation directions and leaving the screen. There should be any method to clear the Navigation View object.