Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
testus74966 opened this issue Mar 28, 2024 · 1 comment
Labels
bug 🪲 Something isn't working compose

Comments

@testus74966
Copy link

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 = 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,
    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
    }
    }
    }
    }
    }
    }

    /**

    • 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,
      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 testus74966 added the bug 🪲 Something isn't working label Mar 28, 2024
@pengdev
Copy link
Member

pengdev commented Apr 2, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug 🪲 Something isn't working compose
Projects
None yet
Development

No branches or pull requests

2 participants