I have a scenario, I want to show user current weather data for that I am getting his/her current lat/lng and reverse geocoding it to get the city name. Once I have the city name I will make a network call and show the weather data. Apart from this, there are many location operations I need to perform.
So I have created a class named as LocationUtils.kt. I am following MVVM architecture and want to know which is the ideal layer to call the LocationUtils methods, is it the view layer or the viewmodel layer or the data layer. Since FusedLocationProvider needs context and if I use it in ViewModel it will leak. So how to solve this problem?
LocationUtils.kt:
class LocationUtils {
private lateinit var fusedLocationClient: FusedLocationProviderClient
private fun isLocationEnabled(weakContext: Context?): Boolean {
return when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P -> {
// This is new method provided in API 28
val locationManager = weakContext?.getSystemService(Context.LOCATION_SERVICE) as LocationManager
locationManager.isLocationEnabled
}
Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT -> {
// This is Deprecated in API 28
val mode = Settings.Secure.getInt(
weakContext?.contentResolver, Settings.Secure.LOCATION_MODE,
Settings.Secure.LOCATION_MODE_OFF
)
mode != Settings.Secure.LOCATION_MODE_OFF
}
else -> {
val locationProviders = Settings.Secure.getString(weakContext?.contentResolver, Settings.Secure.LOCATION_PROVIDERS_ALLOWED)
return !TextUtils.isEmpty(locationProviders)
}
}
}
#SuppressLint("MissingPermission")
fun getCurrentLocation(
weakContext: WeakReference<Context>,
success: (String?) -> Unit,
error: () -> Unit
) {
if (isLocationEnabled(weakContext.get())) {
weakContext.get()
?.let { context ->
fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
fusedLocationClient.lastLocation.addOnSuccessListener { location ->
getCurrentCity(context, location, success)
}
}
} else {
error()
}
}
private fun getCurrentCity(
context: Context,
location: Location?,
success: (String?) -> Unit
) {
val city = try {
location?.let {
val geocoder = Geocoder(context, Locale.getDefault())
val address = geocoder.getFromLocation(it.latitude, it.longitude, 1)
address[0].locality
}
} catch (e: Exception) {
"Bangalore"
}
success(city)
}
}
I am also working on the same problem. I also have to deal with showing weather data to user using MVVM architecture. At the moment, I am stuck at the same point where you are right now. The solution seems to be something called 'Dependency Injection (DI)'. Basically, we can inject dependencies like Context to our ViewModel using tools/frameworks like 'Dagger 2'. DI has lower coupling than directly passing Context to ViewModel and results in better compliance with MVVM. So, the actual place of FusedLocationProvider, IMO, will be in ViewModel but after implementing DI. Maybe someone else can better elaborate on my explanation. I will update my answer once I implement Dependency Injection myself.
I put it in my ViewModel.
In order to pass context as an argument to your ViewModel you can extend AndroidViewModel instead of ViewModel. Example:
class CurrentViewModel(application: Application) : AndroidViewModel(application) {
val context = application
val locationResolver = LocationResolver(context)//this one converts latitude and longitude into City name of type String
fun detectCity() {
Log.d(LocationResolver.TAG, "entered detectLocation()")
val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
fusedLocationClient.lastLocation
.addOnSuccessListener { location ->
if (location != null) {
Repository._currentName.value = locationResolver.getLocationFromCoordinates(
location.latitude,
location.longitude
)
Log.d(
LocationResolver.TAG,
"New city name is:" + Repository.currentLocationCity
)
}
}
}
Then you can observe the ouput via DataBinding.
Related
When I have my Location Provider as a Singleton, and assume I don't have the location permission and my condition closes the flow.
Then calling highAccuracyLocationFlow() function won't create callbackFlow again.
If I remove the #singleton, it works but creates multiple instances of the class for each subscriber.
How can I go about it?
#Singleton
class DefaultLocationProvider #Inject constructor(
#ApplicationContext private val context: Context,
private val fusedLocationClient: FusedLocationProviderClient,
) : LocationProvider {
init {
Timber.d("init: ")
}
private val _receivingLocationUpdates: MutableStateFlow<Boolean> =
MutableStateFlow(false)
override val receivingLocationUpdates: StateFlow<Boolean>
get() = _receivingLocationUpdates
private var _lastKnownLocation : Location? = null
override fun getLastKnownLocation(): Location? {
return _lastKnownLocation
}
private lateinit var locationRequest: LocationRequest
private val highAccuracyLocationRequest = LocationRequest.create().apply {
interval = TimeUnit.SECONDS.toMillis(2)
fastestInterval = TimeUnit.SECONDS.toMillis(1)
priority = Priority.PRIORITY_HIGH_ACCURACY
smallestDisplacement = 0f
}
private val balancedPowerLocationRequest = LocationRequest.create().apply {
interval = TimeUnit.SECONDS.toMillis(60)
fastestInterval = TimeUnit.SECONDS.toMillis(30)
priority = Priority.PRIORITY_BALANCED_POWER_ACCURACY
smallestDisplacement = 50f
}
#SuppressLint("MissingPermission")
private val _locationUpdates = callbackFlow {
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
Timber.d("New location: ${result.lastLocation.toString()}")
// Send the new location to the Flow observers
_lastKnownLocation = result.lastLocation
result.lastLocation?.let {
trySend(it).isSuccess
}
}
}
if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION)
!= PackageManager.PERMISSION_GRANTED
) close()
Timber.d("Starting location updates")
_receivingLocationUpdates.value = true
fusedLocationClient.requestLocationUpdates(
locationRequest,
callback,
Looper.getMainLooper()
).addOnFailureListener { e ->
e.printStackTrace()
close(e) // in case of exception, close the Flow
}
awaitClose {
Timber.d("Stopping location updates")
_receivingLocationUpdates.value = false
fusedLocationClient.removeLocationUpdates(callback) // clean up when Flow collection ends
}
}.shareIn(
MainScope(),
replay = 1,
started = SharingStarted.WhileSubscribed()
)
override fun highAccuracyLocationFlow(): Flow<Location> {
Timber.d("highAccuracyLocationFlow req")
locationRequest = highAccuracyLocationRequest
return _locationUpdates
}
override fun balancedPowerLocationFlow(): Flow<Location> {
locationRequest = balancedPowerLocationRequest
return _locationUpdates
}
}
Side note, I see a bug in your code. You used _locationUpdates with shareIn so there can only be one session of requestLocationUpdates going on at a time. So if you call balancedPowerLocationFlow() and start collecting that flow, followed by calling highAccuracyLocationFlow() while there is still a subscriber of the balanced power flow, it will remain as a balanced power flow even for the new subscriber.
Here are a couple of different strategies for the issue you're asking about:
Mark your two functions with #RequiresPermission(ACCESS_FINE_LOCATION) so you are leaving it up to the caller to only get the Flow reference if it knows the permission is already granted. The annotation helps catch some situations where you accidentally forget to check for the permission first. If you do this, you can remove the safety check that closes the Flow.
Create an internal Channel for tracking when the permission has been granted. Any class that uses this class can be responsible for informing it when permission has been granted (or that permission has already been granted).
private val permissionGrantedChannel = Channel<Unit>()
fun notifyLocationPermissionGranted() {
permissionGrantedChannel.trySend(Unit)
}
Then you can replace your if(/*...*/) close() with permissionGrantedChannel.receive() so the Flow simply suspends until it is known that the permissions have been granted.
Edit: Actually, this should probably be a MutableStateFlow instead of channel so when your flow gets restarted due to falling to 0 subscribers momentarily, the true value is already there. I put this version in the code below.
Here is a potential strategy for the issue I mentioned at the top. I didn't test this. The idea here is that we keep track in a StateFlow of how many high-accuracy subscriptions are currently being collected and use flatMapLatest on that to automatically restart our flow with the right type of location updates whenever we move between 0 and 1 collectors that require high accuracy.
A high-accuracy flow is wrapped at the start and end with making updates to that StateFlow, but otherwise is just passing through the same flow as you would get if requesting balanced updates. So there is only ever one location request going on at once. Collectors that only want balanced updates will simply get temporarily swapped to high accuracy whenever there is at least one high accuracy collector simultaneously getting updates.
Note, this is just to illustrate the concept. I removed a lot of your boilerplate just for brevity.
private val scope = MainScope() + CoroutineName("DefaultLocationProvider CoroutineScope")
private var highAccuracyCollectorCount = 0
private val isHighAccuracy = MutableStateFlow(false)
private val permissionGranted = MutableStateFlow(false)
fun notifyLocationPermissionGranted() {
permissionGranted.value = true
}
#OptIn(ExperimentalCoroutinesApi::class)
val balancedPowerLocations = isHighAccuracy.flatMapLatest { shouldUseHighAccuracy ->
callbackFlow {
val callback = object : LocationCallback() {
//...
}
permissionGranted.first { it }
fusedLocationClient.requestLocationUpdates(
locationRequest,
callback,
Looper.getMainLooper()
).addOnFailureListener { e ->
e.printStackTrace()
close(e) // in case of exception, close the Flow
}
awaitClose {
fusedLocationClient.removeLocationUpdates(callback)
}
}
}
.distinctUntilChanged() // in case switching callbacks tries to replay the last known location
.shareIn(scope, SharingStarted.WhileSubscribed(), replay = 1)
val highAccuracyLocations = balancedPowerLocations
.onStart {
isHighAccuracy.value = true
highAccuracyCollectorCount++
}
.onCompletion {
isHighAccuracy.value = --highAccuracyCollectorCount > 0
}
While I am trying to implement a simple example getting the location of the device, I found that a document which is "seemingly official": https://developer.android.com/training/location/retrieve-current#BestEstimate
The document claims that FusedLocationProviderClient provides the following two methods: getLastLocation() and getCurrentLocation(). But as one can see in the example - https://developer.android.com/training/location/retrieve-current#last-known - both getLast/CurrentLocation() lives in Java. The corresponding Kotlin example says that fusedLocationClient.getLastLocation() "is the same as" fusedLocationClient.lastLocation and, indeed, it works well.
I naively assume that there should be corresponding "currentLocation" for example, fusedLocationClient.currentLocation.
I am wondering there is no such, or I am the only one who fails to find the corresponding Kotlin method.
in kotlin any method of the form getX can be written as just x, this is called "property access syntax". There is no separate kotlin version. fusedLocationClient.lastLocation is really exactly the same as fusedLocationClient.getLastLocation(). You can even write this last form in kotlin if you want.
However, this is only true for "get" methods without parameters. The thing is, getCurrentLocation does have parameters so property access syntax is not possible in this case. as you can see here this is the signature of this method:
public Task<Location> getCurrentLocation (int priority, CancellationToken token)
So you should use it like that. for example
fusedLocationClient.getCurrentLocation(LocationRequest.PRIORITY_HIGH_ACCURACY, null)
EDIT:
apparently null as parameter is not allowed. According to https://stackoverflow.com/a/72159436/1514861 this is a possibility:
fusedLocationClient.getCurrentLocation(LocationRequest.PRIORITY_HIGH_ACCURACY, object : CancellationToken() {
override fun onCanceledRequested(p0: OnTokenCanceledListener) = CancellationTokenSource().token
override fun isCancellationRequested() = false
})
.addOnSuccessListener { location: Location? ->
if (location == null)
Toast.makeText(this, "Cannot get location.", Toast.LENGTH_SHORT).show()
else {
val lat = location.latitude
val lon = location.longitude
}
}
fusedLocationClient.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, object : CancellationToken() {
override fun onCanceledRequested(listener: OnTokenCanceledListener) = CancellationTokenSource().token
override fun isCancellationRequested() = false
})
.addOnSuccessListener {
if (it == null)
Toast.makeText(this, "Cannot get location.", Toast.LENGTH_SHORT).show()
else {
val lat = it.latitude
val lon = it.longitude
}
}
What I am trying to do
I made a weather application based on MVVM architecture in which I am fetching the device location using GPS and passing the location to a weather API service to get the Weather Reports, but because of the fact that the location may change and the user needs to fetch the weather again for new location, I wrapped the location in LiveData and observing it and passing it to the API service class.
In short I am launching a coroutine with GlobalScope inside the observeForever of LiveData
WeatherNetworkAbstractionsImpl.kt
class WeatherNetworkAbstractionsImpl(
private val weatherApiService: WeatherApiService,
private val context: Context
) : WeatherNetworkAbstractions {
private val _downloadedWeather = MutableLiveData<WeatherResponse>()
override val downloadedWeather: LiveData<WeatherResponse>
get() = _downloadedWeather
override suspend fun fetchWeather(location: LiveData<String>) {
try {
location.observeForever {
GlobalScope.launch(Dispatchers.IO) {
val fetchedWeather = weatherApiService.getWeather(it).await()
_downloadedWeather.postValue(fetchedWeather)
}
}
}
catch (e: NoConnectivityException){
Log.e("Connectivity","No internet connection.",e)
}
}
}
Repository.kt
class RepositoryImpl(
private val weatherDao: WeatherDataDao,
private val weatherNetworkAbstractions: WeatherNetworkAbstractions,
private val weatherLocationDao: WeatherLocationDao,
private val recordMapperImpl: RecordMapperImpl,
private val locationMapperImpl: LocationMapperImpl,
private val locationProvider: LocationProvider
) : Repository {
init {
weatherNetworkAbstractions.downloadedWeather.observeForever {latestWeatherReports->
persistLatestWeatherReports(latestWeatherReports, recordMapperImpl, locationMapperImpl)
}
}
}
What my problem is
I am getting this error:
Cannot invoke observeForever on a background thread
even if I changed the Dispatchers.IO to Dispatchers.Main but still getting the same error.
How can I resolve the issue? I am new to coroutines.
I'm trying to use the last known location of an Android user as a variable for a separate coroutine API call
private fun getLocation() {
lateinit var latLong: String
val client: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(getApplicationContext())
client.lastLocation
val location = client.lastLocation
location.addOnSuccessListener {
latLong = "${it.latitude},${it.longitude}"
}
makeApiCall(latLong)}
}
Is it possible to force a wait for the addOnSuccessListener to ensure the variable is updated accordingly?
You can get the last known location synchronously by working directly with Android's LocationManager.
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val location: Location? = with(locationManager) {
val provider = getProvider(LocationManager.GPS_PROVIDER)
getLastKnownLocation(provider)
}
If you want a fallback in case GPS is off, you can use the getProviders() function with a Criteria argument. In either case, all location services might be turned off by the user, so the result might be a null Location.
Note: You can also convert Java callbacks into suspend functions using suspendCoroutine. Something like this:
/** Await the result of a task and return its result, or null if the task failed or was canceled. */
suspend fun <T> Task<T>.awaitResult() = suspendCoroutine<T?> { continuation ->
if (isComplete) {
if (isSuccessful) continuation.resume(it.result)
else continuation.resume(null)
return#suspendCoroutine
}
addOnSuccessListener { continuation.resume(it.result) }
addOnFailureListener { continuation.resume(null) }
addOnCanceledListener { continuation.resume(null) }
}
Then if your function were a suspend function, you could use it like this:
private suspend fun getLocation() {
val client: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(getApplicationContext())
val location = client.lastLocation.awaitResult()
val latLong = location?.run { "$latitude,$longitude" } ?: "null"
makeApiCall(latLong)
}
(Just an example. Don't know what you want to pass to makeApiCall if you don't have a location, or if you want to call it at all.)
Kotlin Coroutines question... struggling w/ using a property instead of a function being the accessor for an asynchronous call.
Background is that I am trying to use the FusedLocationProviderClient with the kotlinx-coroutines-play-services library in order to use the .await() method on the Task instead of adding callbacks...
Currently having a property getter kick out to a suspend function, but not sure on how to launch the coroutine properly in order to avoid the
required Unit found XYZ
error...
val lastUserLatLng: LatLng?
get() {
val location = lastUserLocation
return if (location != null) {
LatLng(location.latitude, location.longitude)
} else {
null
}
}
val lastUserLocation: Location?
get() {
GlobalScope.launch {
return#launch getLastUserLocationAsync() <--- ERROR HERE
}
}
private suspend fun getLastUserLocationAsync() : Location? = withContext(Dispatchers.Main) {
return#withContext if (enabled) fusedLocationClient.lastLocation.await() else null
}
Any thoughts on how to handle this?
Properties can't be asynchronous. In general you should not synchronize asynchronous calls. You'd have to return a Deferred and call await() on it when you need a value.
val lastUserLatLng: Deferredd<LatLng?>
get() = GlobalScope.async {
lastUserLocation.await()?.run {
LatLng(latitude, longitude)
}
}
val lastUserLocation: Deferred<Location?>
get() = GlobalScope.async {
getLastUserLocationAsync()
}
private suspend fun getLastUserLocationAsync() : Location? = withContext(Dispatchers.Main) {
return#withContext if (enabled) fusedLocationClient.lastLocation.await() else null
}
But technically it's possible, though you should not do it. runBlocking() blocks until a value is available and returns it.