mapbox-maps-android icon indicating copy to clipboard operation
mapbox-maps-android copied to clipboard

Map Box is not freeing up the memory after removing the Map Box Navigation View from back stack using Android View Jetpack Compose

Open testus74966 opened this issue 10 months ago • 1 comments

Environment

  • Android OS version: Android Versions 11, 12 and 13
  • Devices affected: Tested on two real devices and one simulator
  • Maps SDK Version: 2.17.6

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 Map Box 1 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

/**

  • @date: 07/12/2023

  • @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<String> = 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") }

         Lifecycle.Event.ON_START -> {
             printLog("Compose_Lifecycles", "on start")
         }
    
         Lifecycle.Event.ON_RESUME -> {
             printLog("Compose_Lifecycles", "on resume")
         }
    
         Lifecycle.Event.ON_PAUSE -> {
             printLog("Compose_Lifecycles", "on pause")
         }
    
         Lifecycle.Event.ON_STOP -> {
             printLog("Compose_Lifecycles", "on stop")
             currentLongitude.value = null
             currentLongitude.value = null
             locationObserver.value?.let {
                 mapBoxNavigation.value?.unregisterLocationObserver(
                     it
                 )
             }
             mapBoxNavigation.value?.stopTripSession()
         }
    
         Lifecycle.Event.ON_DESTROY -> {
             printLog("Compose_Lifecycles", "on destroy")
             currentLongitude.value = null
             currentLongitude.value = null
             locationObserver.value?.let {
                 mapBoxNavigation.value?.unregisterLocationObserver(
                     it
                 )
             }
             mapBoxNavigation.value?.stopTripSession()
             Runtime.getRuntime().gc()
         }
    
         else -> {
             printLog("Compose_Lifecycles", "no lifecycle")
         }
     }
    

    }

    //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 }

         //Registering location Observer to check the location accurately
         locationObserver.value?.let { mapBoxNavigation.value?.registerLocationObserver(it) }
     }*/
    

}

/**

  • @date: 07/12/2023

  • @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)

     onDispose {
         lifecycleOwner.lifecycle.removeObserver(observer)
     }
    

    } }

/**

  • @date: 05/01/2024

  • @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)) }

     override fun onNewRawLocation(rawLocation: Location) {
         currentLatitude.value = rawLocation.latitude
         currentLongitude.value = rawLocation.longitude
         printLog("rawLocation", Gson().toJson(rawLocation))
         printLog(
             "latitude_longitude",
             "${Gson().toJson(currentLongitude.value)}, ${Gson().toJson(currentLatitude.value)}"
         )
     }
    

    } return locationObserver }

/**

  • @date: 05/01/2024

  • @Desc: Composable function to get the routes and start active navigation directions from the

  • current location to the destination location */ @Composable fun StartMapBoxNavigation( currentLatitude: MutableState<Double?>, currentLongitude: MutableState<Double?>, nvView: MutableState<NavigationView?>, showMsg: MutableState<String>, 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<Boolean> = 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 } } } } } }

    /**

    • Method to request the routes to start an active map box navigation directions for ER Requests (07/12/2023)

    • @param context

    • @param nvView Map box Navigation view required to render the route path in Map

    • @param origin Origin Point for the path to be drawn on Map

    • @param destination Destination Point for the path to be drawn on Map

    • @param openDialog Boolean value to open dialog if the path exceeds the maximum path distance or no path is available

    • @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<String>, 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)}" ) }

           override fun onFailure(reasons: List<RouterFailure>, routeOptions: RouteOptions) {
               printLog(
                   "onFailed",
                   "${Gson().toJson(reasons)}, ${Gson().toJson(routeOptions)}"
               )
               openDialog.value = reasons[0].message
      

// showToast(reasons[0].message, context) }

            override fun onRoutesReady(
                routes: List<NavigationRoute>,
                routerOrigin: RouterOrigin
            ) {
                printLog(
                    "onRoutesReady",
                    "${Gson().toJson(routes)}, ${Gson().toJson(routerOrigin)}"
                )

// 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.

testus74966 avatar Mar 28 '24 14:03 testus74966

@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.

pengdev avatar Apr 02 '24 08:04 pengdev