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()
}
}
}
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:
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.