geolocationandroid-jetpack-composeandroid-snackbar

Get user location in Compose with M3


I am trying to get the User Location in Compose.

I found this link and tried the Mitch solution. But it has an infinite loop and the snackbar is not shown. From other links I read that it needs to be attached to a scaffold but it seems than on M3 is no longer valid.

I change the infinite loop from:

LaunchedEffect(true) {
    while (true) {
        isGPSEnabled = isGPSEnabled(context)
        delay(2.seconds)
    }
}

To:

LaunchedEffect(true) {
    while (!isGPSEnabled) {   //while (true) {
        isGPSEnabled = isGPSEnabled(context)
        delay(2.seconds)
    }
}

But I don't know how to make the snackbar to show:

DisposableEffect(
    isGPSEnabled,
    permissions.shouldShowRationale,
    permissions.allPermissionsGranted,
) {
    if (!permissions.allPermissionsGranted || permissions.shouldShowRationale) {
        scope.launch {
            val result = snackbarHostState.showSnackbar(
                "Missing required permissions",
                "Grant",
                withDismissAction = true,
                duration = SnackbarDuration.Indefinite,
            )

            when (result) {
                SnackbarResult.Dismissed -> {

                }

                SnackbarResult.ActionPerformed -> {
                    permissions.launchMultiplePermissionRequest()
                }
            }
        }
        return@DisposableEffect SimulatedDisposableEffectResult
    }

I know the code [val result = snackbarHostState.showSnackbar(...) ]is executed because I have a break point in debug that reach it. It stays there an never reach the [when(result)] since it has[duration = SnackbarDuration.Indefinite]

On my Screen I have the following to call the function:

Image(painter = painterResource(R.drawable.baseline_my_location_24),
                contentDescription = "Get Coordinates",
                modifier = Modifier
                    .clickable {
                        doGet = true
                    }
                    .size(40.dp)
            )

            if (doGet) {
                ExampleForegroundLocationTrackerScreen()
            }

That is for testing purpose, but I really want to receive the value(latitud, longitud) to show them on the screen and later save then in a Database.


Solution

  • After many days or weeks I end up using a BottomSheetScaffold.

    @OptIn(ExperimentalMaterial3Api::class)
    @Composable
    fun LocationScreen(
    startLocationUpdates: ()->Unit,
    context: Context,
    currentLocation: LatLng
    ) {
    
    ////// This permissions are use down bellow at a Button Click event that
    ////// actually calls for the GPS location. The Button  is part of the BottomSheet content.
    val permissions = arrayOf(
        Manifest.permission.ACCESS_COARSE_LOCATION,
        Manifest.permission.ACCESS_FINE_LOCATION,
    )
    
    ////// startActivityForResult function is deprecated and in Jetpack Compose the solution is 
    ////// to use "rememberLauncherForActivityResult" with "ActivityResultContracts" 
    val launchMultiplePermissions = rememberLauncherForActivityResult(
        ActivityResultContracts.RequestMultiplePermissions()
    ) { permissionMaps ->
        val areGranted = permissionMaps.values.reduce { acc, next -> acc && next }
        if (areGranted) {
            locationRequired = true
            startLocationUpdates()
            Toast.makeText(context, "Permission Granted", Toast.LENGTH_SHORT).show()
        } else {
            Toast.makeText(context, "Permission Denied", Toast.LENGTH_SHORT).show()
        }
    }
    
    var readable by remember { mutableStateOf("") }
    //val scope = rememberCoroutineScope()
    val scaffoldState = rememberBottomSheetScaffoldState()
    
    BottomSheetScaffold(
        //modifier = Modifier.heightIn(min=0.dp, max=400.dp),
        scaffoldState = scaffoldState,
        sheetPeekHeight = 590.dp,
        sheetSwipeEnabled = false,
        sheetContent = {
    
            Column(
                Modifier
                    .fillMaxWidth()
                    .padding(6.dp),
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                if (currentLocation.latitude != 0.toDouble() && currentLocation.longitude != 0.toDouble() && readable != "") {
                    WebViewScreen(location = currentLocation) //, address = readable)
                    showdialog.value = false
                } else {Text(text = "Please click [Get your location]")}
                
            }
        }) { innerPadding ->
        Box(Modifier.padding(innerPadding)) {
    
            Column(
                modifier = Modifier.fillMaxWidth(),
                verticalArrangement = Arrangement.SpaceEvenly,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Text(text = "Your location: ${currentLocation.latitude}/${currentLocation.longitude}")
                if (currentLocation.latitude != 0.toDouble() && currentLocation.longitude != 0.toDouble()) {
                    Text(
                        text = "Readable location: $currentLocation"
                    )
                    location = ""
                    readable = getReadableLocation(currentLocation.latitude, currentLocation.longitude, context)
                    glocation = currentLocation
                }
        //////Here is where the permissions are used to call for the system permissions pop up.
                Button(onClick = {
                    if (permissions.all {
                            ContextCompat.checkSelfPermission(
                                context,
                                it
                            ) == PackageManager.PERMISSION_GRANTED
                        }) {
                        //Get Locations
      ////// Here once the permissions are granted I call my function that actually gets the GPS Location
                        startLocationUpdates()
                    } else {
                        launchMultiplePermissions.launch(permissions)
                    }
                    =
                }) {
                    Text(text = "Get your location")
                }
            }
        }
    }
    

    }

    Maybe you can read this post for better explanation. In there the example is directed toward Pictures instead of GPS location.

    The GPS Location function is the following:

    private lateinit var fusedLocationClient: FusedLocationProviderClient
    private lateinit var locationCallback: LocationCallback
    
    
    override fun onResume() {
        super.onResume()
        if (locationRequired)
            startLocationUpdates()
    }
    
    override fun onPause() {
        super.onPause()
        if (this::locationCallback.isInitialized) {
            locationCallback?.let {
                fusedLocationClient?.removeLocationUpdates(it)
            }
        }
    }
    
    @SuppressLint("MissingPermission")
    private fun startLocationUpdates() {
        locationCallback?.let {
            val locationRequest = LocationRequest.Builder(
                Priority.PRIORITY_HIGH_ACCURACY, 100
            )
                .setWaitForAccurateLocation(false)
                .setMinUpdateIntervalMillis(3000)
                .setMaxUpdateDelayMillis(100)
                .build()
    
            fusedLocationClient?.requestLocationUpdates(
                locationRequest,
                it,
                Looper.getMainLooper()
            )
        }
    }
    

    This code is written in the MainActivity before the onCreate and is pass to the Screen as a Lambda function along the currentLocation:LatLng which is calculated/retrieved by the "startLocationUpdates" function already detail above.

    Then inside the onCreate the following code:

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
    
        setContent {
    
            var currentLocation by remember {
                mutableStateOf(LatLng(0.toDouble(), 0.toDouble()))
            }
    
            locationCallback = object : LocationCallback() {
                override fun onLocationResult(p0: LocationResult) {
                    super.onLocationResult(p0)
                    for (location in p0.locations) {
                        currentLocation = LatLng(location.latitude, location.longitude)
    
                    }
                }
            }
    
            MainTheme { ...
            
            NavHost(navController = navController, startDestination = "MainScreen") {
                  composable("LocationScreen") {
                       LocationScreen(
                          { startLocationUpdates() },
                            this@MainActivity,
                            currentLocation
                          )
                       }
    
            ...