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.
Related
I have two suspend functions which are callbackFlow. I call one of them from suspend function which return String. I want to wait for location from getLocation() in serializeEvent() function and after getting value return string.
suspend fun getLocation(applicationContext: Context) = callbackFlow {
val locationProvider = LocationServices.getFusedLocationProviderClient(applicationContext)
if (isLocationPermissionGranted(applicationContext)) {
locationProvider.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, null)
.addOnSuccessListener {
if (it != null) {
trySend(it)
} else {
launch { trySend(getLastKnownLocation(locationProvider)) }
}
}
}
}
private suspend fun getLastKnownLocation(locationProvider: FusedLocationProviderClient) = callbackFlow {
runCatching {
locationProvider.lastLocation
.addOnSuccessListener { trySend(it) }
.addOnCanceledListener { trySend(null) }
.addOnFailureListener { trySend(null) }
}.onFailure {
trySend(null)
}
awaitClose { this.cancel() }
}
How I can return string when I get value from getLastKnownLocation()
suspend fun serializeEvent(eventJson: JSONObject): String {
CoroutineScope(Dispatchers.Default).launch {
LocationProvider.getLocation(getApplicationContext!!).collect {
}
}
//some code here
return eventJson.toString()
}
It doesn't make sense to turn a Google Task into a Flow using callbackFlow, because a Task only produces one thing, not a series of things. Typically, the way to convert for coroutines a callback that returns only once is to convert it into a suspend function using suspendCoroutine or suspendCancellableCoroutine.
However, a Task.await() extension suspend function is already provided so you can use it synchronously with coroutines. (Make sure you're using the -ktx version of the location library.) Instead of using listeners, you surround it with try/catch.
It seems like you just want to return null if no location is available, so here's how I would rewrite that function:
suspend fun getLocationOrNull(applicationContext: Context): Location? {
if (!isLocationPermissionGranted(applicationContext)) {
return null
}
val locationProvider = LocationServices.getFusedLocationProviderClient(applicationContext)
return try {
locationProvider.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, null)
.await() ?: locationProvider.lastLocation.await()
} catch (e: Exception) {
Log.w(TAG, "Failed to retrieve current location")
null
}
}
In your usage site code, it doesn't make sense that you're launching a coroutine from within the suspend function. If you want to do something and wait for it in a suspend function, you can just do it directly without firing off a new coroutine in some other CoroutineScope.
suspend fun serializeEvent(eventJson: JSONObject): String {
val knownLocation: Location? =
LocationProvider.getLocation(getApplicationContext!!)
//some code here. Do something with knownLocation, which might be null
return eventJson.toString()
}
Side note, it looks like a code smell to me that you have to use !! when getting your application context. An Application Context must always exist, so it shouldn't need to be a nullable property.
.addOnSuccessListener { trySend(it) }
I am trying to run a Google ML Kit function and the result will be in callback and need to pass that value as a return type for the method in which it was executing in Kotlin. I tried some of the samples of Kotlin coroutines but still I am missing something and it was failing. I am still learning Kotlin.
internal fun processImageSync(image: InputImage) : String{
var doctype = ""
val recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)
recognizer.process(image)
.addOnSuccessListener { visionText ->
var texttoscan = visionText.text.trim()
doctype = findstr(texttoscan)
}
.addOnFailureListener {
}
return doctype;
}
How can I solve the issue?
Use kotlinx-coroutines-play-services module
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.0")
}
Use extension Task.await
internal suspend fun processImageSync(image: InputImage): String {
val recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)
return recognizer.process(image)
.await()
.let { visionText -> findstr(visionText.text.trim()) }
}
Fragment
private fun makeApiRequest() {
vm.getRandomPicture()
var pictureElement = vm.setRandomPicture()
GlobalScope.launch(Dispatchers.Main) {
// what about internet
if (pictureElement != null && pictureElement!!.fileSizeBytes!! < 400000) {
Glide.with(requireContext()).load(pictureElement!!.url)
.into(layout.ivRandomPicture)
layout.ivRandomPicture.visibility = View.VISIBLE
} else {
getRandomPicture()
}
}
}
viewmodel
fun getRandomPicture() {
viewModelScope.launch {
getRandomPictureItemUseCase.build(Unit).collect {
pictureElement.value = it
Log.d("inspirationquotes", "VIEWMODEL $pictureElement")
Log.d("inspirationquotes", "VIEWMODEL VALUE ${pictureElement.value}")
}
}
}
fun setRandomPicture(): InspirationQuotesDetailsResponse? {
return pictureElement.value
}
Flow UseCase
class GetRandomPictureItemUseCase #Inject constructor(private val api: InspirationQuotesApi): BaseFlowUseCase<Unit, InspirationQuotesDetailsResponse>() {
override fun create(params: Unit): Flow<InspirationQuotesDetailsResponse> {
return flow{
emit(api.getRandomPicture())
}
}
}
My flow task from viewmodel doesn't goes on time. I do not know how to achieve smooth downloading data from Api and provide it further.
I was reading I could use runBlocking, but it is not recommended in production as well.
What do you use in your professional applications to achieve nice app?
Now the effect is that that image doesn't load or I have null error beacause of my Log.d before GlobalScope in Fragment (it is not in code right now).
One more thing is definding null object I do not like it, what do you think?
var pictureElement = MutableStateFlow<InspirationQuotesDetailsResponse?>(null)
EDIT:
Viewmodel
val randomPicture: Flow<InspirationQuotesDetailsResponse> = getRandomPictureItemUseCase.build(Unit)
fragment
private fun makeApiRequest() = lifecycleScope.launch {
vm.randomPicture
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect { response ->
if (response.fileSizeBytes < 600000) {
Log.d("fragment", "itGetsValue")
Glide.with(requireContext()).load(response.url)
.into(layout.ivRandomPicture)
layout.ivRandomPicture.visibility = View.VISIBLE
} else {
onFloatingActionClick()
}
}
}
Edit2 problem on production, another topic:
Link -> What is the substitute for runBlocking Coroutines in fragments and activities?
First of all, don't use GlobalScope to launch a coroutine, it is highly discouraged and prone to bugs. Use provided lifecycleScope in Fragment:
lifecycleScope.launch {...}
Use MutableSharedFlow instead of MutableStateFlow, MutableSharedFlow doesn't require initial value, and you can get rid of nullable generic type:
val pictureElement = MutableSharedFlow<InspirationQuotesDetailsResponse>()
But I guess we can get rid of it.
Method create() in GetRandomPictureItemUseCase returns a Flow that emits only one value, does it really need to be Flow, or it can be just a simple suspend function?
Assuming we stick to Flow in GetRandomPictureItemUseCase class, ViewModel can look something like the following:
val randomPicture: Flow<InspirationQuotesDetailsResponse> = getRandomPictureItemUseCase.build(Unit)
And in the Fragment:
private fun makeApiRequest() = lifecycleScope.launch {
vm.randomPicture
.flowWithLifecycle(lifecycle, State.STARTED)
.collect { response ->
// .. use response
}
}
Dependency to use lifecycleScope:
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0'
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.)
As an example, I'm using FusedLocationProviderClient to access the current location, which returns a task which callback will eventually return the location. The method looks something like follows:
fun getLocation(callback: MyCallback){
val flpc = LocationServices.getFusedLocationProviderClient(it)
flpc.lastLocation.addOnSuccessListener {
callback.onLocation(it)
}
}
Is it possible to transform this so that I can use corroutines to suspend this function and wait for the task returned by flpc.lastLocation so I can return it in this method and this way get rid of that callback? For example something like this:
suspend fun getLocation(): Location? =
withContext(Dispachers.IO){
val flpc = LocationServices.getFusedLocationProviderClient(it)
return#withContext flpc.lastLocation.result()
}
My question is if there is something around coroutines where I can return the result of a Task (in this example, a Task<Location>)
Thanks in advance!
The kotlinx-coroutines-play-services library has a Task<T>.await(): T helper.
import kotlinx.coroutines.tasks.await
suspend fun getLocation(): Location? =
LocationServices.getFusedLocationProviderClient(context).lastLocation.await()
Alternatively take a look at Blocking Tasks
It would be used the next way:
suspend fun getLocation(): Location? =
withContext(Dispachers.IO){
val flpc = LocationServices.getFusedLocationProviderClient(context)
try{
return#withContext Tasks.await(flpc.lastLocation)
catch(ex: Exception){
ex.printStackTrace()
}
return#withContext null
}
Just to add to this example, for completion purposes, the call to getLocation() would be done the next way:
coroutineScope.launch(Dispatchers.Main) {
val location = LocationReceiver.getLocation(context)
...
}
However this negates the benefits of coroutines by not leveraging the available callback and blocking a thread on the IO dispatcher and should not be used if the alternative is available.
Another way that I have done this that can also be used with any callback type interface is to use suspendCoroutine<T> {}.
So for this example it would be:
suspend fun getLocation(): Location? {
return suspendCoroutine<Location?> { continuation ->
val flpc = LocationServices.getFusedLocationProviderClient(it)
flpc.lastLocation.addOnSuccessListener { location ->
continuation.resume(location)
}
// you should add error listener and call 'continuation.resume(null)'
// or 'continuation.resumeWith(Result.failure(exception))'
}
}