androidandroid-studiokotlinandroid-jetpack-composeandroid-location

Making network call to receive image and show it when location access has been granted


i'm wanting to make a network call when location access has been granted. so i'm using LaunchedEffect(key1 = location.value){...} to decide when to make that network call to recompose, but facing some issues.

upon initial launch user is greeted with the location request (either precise or coarse). during this, the Toast.makeText(context, "Allow location access in order to see image", Toast.LENGTH_SHORT).show() get's called twice and shows up twice. when the user selects an option from the location request dialog, i would assume location.value would end up changing and viewModel.getImage(location.value!!) get's called. debugging through this, that all happens, but the image doesn't end up showing. i got it to work sometimes by force closing the app, then opening it again, then the image shows up. any insights? here is the location code in that same file:

    val locationLiveData = LocationLiveData(context)
    val location = locationLiveData.observeAsState()

    val requestSinglePermissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
        when {
            it.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false) -> {
                locationLiveData.startLocationUpdates()
            }
            it.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false) -> {
                locationLiveData.startLocationUpdates()
            } else -> {
            Toast.makeText(context, "Allow location access", Toast.LENGTH_SHORT).show()
        }
        }
    }

    if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PermissionChecker.PERMISSION_GRANTED ||
        ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PermissionChecker.PERMISSION_GRANTED) {
        locationLiveData.startLocationUpdates()
    } else {
        // true so we execute once not again when we compose or so
        LaunchedEffect(key1 = true) {
            requestSinglePermissionLauncher.launch(arrayOf(
                Manifest.permission.ACCESS_FINE_LOCATION,
                Manifest.permission.ACCESS_COARSE_LOCATION))
        }
    }

EDIT 2 LocationLiveData

class LocationLiveData(var context: Context): LiveData<LocationDetails>() {
    // used to get last known location
    private val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)

    // We have at least 1 observer or 1 component looking at us
    // here we can get the last known location of the device
    override fun onActive() {
        super.onActive()
        if (ActivityCompat.checkSelfPermission(
                context,
                Manifest.permission.ACCESS_FINE_LOCATION
            ) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(
                context,
                Manifest.permission.ACCESS_COARSE_LOCATION
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            return
        }

        fusedLocationClient.lastLocation.addOnSuccessListener {
            setLocationData(it)
        }
    }

    // no one is looking at this live data anymore
    override fun onInactive() {
        super.onInactive()
        fusedLocationClient.removeLocationUpdates(locationCallback)
    }

    internal fun startLocationUpdates() {
        if (ActivityCompat.checkSelfPermission(
                context,
                Manifest.permission.ACCESS_FINE_LOCATION
            ) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(
                context,
                Manifest.permission.ACCESS_COARSE_LOCATION
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            return
        }
        fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper())
    }

    private fun setLocationData(location: Location) {
        value = LocationDetails(longitude = location.longitude.toString(), latitude = location.latitude.toString())
    }

    private val locationCallback = object : LocationCallback() {
        override fun onLocationResult(p0: LocationResult) {
            super.onLocationResult(p0)
            for (location in p0.locations) {
                setLocationData(location)
            }
        }
    }

    companion object {
        private const val ONE_MINUTE: Long = 60_000
        val locationRequest: LocationRequest = LocationRequest.create().apply {
            interval = ONE_MINUTE
            fastestInterval = ONE_MINUTE / 4
            priority = Priority.PRIORITY_HIGH_ACCURACY
        }
    }
}

COMPOSABLE

@RequiresApi(Build.VERSION_CODES.N)
@Composable
fun HomeScreen(viewModel: HomeScreenViewModel = hiltViewModel(), navigateToAuthScreen: () -> Unit, navigateToAddImage: () -> Unit){
    var text by remember { mutableStateOf(TextFieldValue("")) }
    val context = LocalContext.current

    val locationLiveData = remember { LocationLiveData(context) }
    val location = locationLiveData.observeAsState()

    val requestSinglePermissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
        when {
            it.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false) -> {
                locationLiveData.startLocationUpdates()
            }
            it.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false) -> {
                locationLiveData.startLocationUpdates()
            }
        }
    }

    if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PermissionChecker.PERMISSION_GRANTED ||
        ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PermissionChecker.PERMISSION_GRANTED) {
        locationLiveData.startLocationUpdates()
    } else {
        // true so we execute once not again when we compose or so
        LaunchedEffect(key1 = true) {
            requestSinglePermissionLauncher.launch(arrayOf(
                Manifest.permission.ACCESS_FINE_LOCATION,
                Manifest.permission.ACCESS_COARSE_LOCATION))
        }
    }

    Scaffold( topBar = {
        HomeScreenTopBar()
    },
        floatingActionButton = {
        FloatingActionButton(onClick = {
            if (location.value != null) {
                navigateToAddImageScreen()
            } else {
                Toast.makeText(context, "allow location access to add image", Toast.LENGTH_SHORT).show()
            }
        },
            backgroundColor = MaterialTheme.colors.primary
        ) {
            Icon(
                imageVector = Icons.Default.Add,
                contentDescription = "Save note"
            )
        }
    }) {innerPadding ->
        Column(modifier = Modifier
            .fillMaxSize()
            .padding(innerPadding)) {
            LaunchedEffect(key1 = location.value) {
                if (location.value != null) {
                    viewModel.getListings(location.value!!)
                } else {
                    Toast.makeText(context, "Allow location access in order to see image", Toast.LENGTH_SHORT).show()
                }
            }
}

Solution

  • This line

    val locationLiveData = LocationLiveData(context)
    

    creates a new LocationLiveData instance on every recomposition.

    You have to remember the same instance of LocationLiveData across recompositions, if you want it to hold any state or view state.

    Change it to

    // remember LocationLiveData across recompositions
    // this does not survive configuration changes, nor other short Activity restarts
    val locationLiveData = remember { LocationLiveData(context) }
    

    As also mentioned in the code comment above, now locationLiveData will survive re-compositions, but it will still get reset on:

    1. every configuration change (examples include but are not limited to: orientation change, light/dark mode change, language change...)
    2. every short Activity restart, caused by the system in some cases
    3. also application death (but that is somewhat expected)

    To solve 1. and 2. you can use rememberSaveable that can save primitive and other Parcelable types automatically (in your case you can also implement the Saver interface), to solve 3. you have to save the state to any of the persistent storage options and then restore as needed.

    To learn more about working with state in Compose see the documentation section on Managing State. This is fundamental information to be able to work with state in Compose and trigger recompositions efficiently. It also covers the fundamentals of state hoisting. If you prefer a coding tutorial here is the code lab for State in Jetpack Compose.

    An introduction to handling the state as the complexity increases is in the video from Google about Using Jetpack Compose's automatic state observation.