https://developer.android.com/topic/libraries/architecture/coroutines
Android coroutines plus liveData documentation states that we can use the liveData builder function in case we want to want to perform async operations inside the live data function
val user: LiveData<User> = liveData {
val data = database.loadUser() // loadUser is a suspend function.
emit(data)
}
val user: LiveData<Result> = liveData {
emit(Result.loading())
try {
emit(Result.success(fetchUser())
} catch(ioException: Exception) {
emit(Result.error(ioException))
}
}
I tried installing the lifecycle-viewmodel-ktx library but couldn't find this block.
Where is it located?
Try:
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha01'
The function lives here:
https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-master-dev/lifecycle/livedata/ktx/src/main/java/androidx/lifecycle/CoroutineLiveData.kt
And is (currently) defined as:
#UseExperimental(ExperimentalTypeInference::class)
fun <T> liveData(
context: CoroutineContext = EmptyCoroutineContext,
timeoutInMs: Long = DEFAULT_TIMEOUT,
#BuilderInference block: suspend LiveDataScope<T>.() -> Unit
): LiveData<T> = CoroutineLiveData(context, timeoutInMs, block)
I also had this problem, I would recommend just adding the dependencies they suggested here.
The issue is Google's Android docs on coroutines were not explicit in mentioning that these ktx extensions specifically (as you would see in the link) are very important to be able to get the liveData builder that supplies the LiveDataScope.
Don't make the mistake of thinking that you can just use lower versions i.e 2.1.0, just use it as explicitly specified in the doc i.e. the alpha/RC versions as in 2.2.0-alpha01.
Related
I am trying to migrate from LiveData to Kotlin Flow. Right now I am working on a project that has offline supports in Room.
I was looking through the documentation and I managed to write an observable query in coroutines with Flow. (see: here)
The problem that I am facing right now is that whenever I add the suspend keyword inside the DAO class and try to run the project, it fails with the following error:
error: Not sure how to convert a Cursor to this method's return type (kotlinx.coroutines.flow.Flow<MyModel>).
The code with the problem:
#Transaction
#Query("SELECT * FROM table WHERE status = :status LIMIT 1")
suspend fun getWithSpecificStatus(status: String): Flow<MyModel?>
I am calling the code like this:
val modelLiveData: LiveData<MyModel?> = liveData(Dispatchers.IO) {
val result = databaseService.getWithSpecificStatus(Enum.IN_PROGRESS.status).first()
result?.let {
emit(it)
}
}
I tried to keep things simple. why is my code failing?
You could directly initialise the value of modelLiveData as:
val modelLiveData=databaseService.
getWithSpecificStatus(Enum.IN_PROGRESS.status).first()
.asLiveData()
You have used Flow so asLiveData() is used to convert it to LiveData
Also suggestion , you do should not use the suspend keyword because when you are returning Flow, Room automatically does this asynchronously. You just need to consume the Flow.
My shared module contains Repository class which has two functions that return a list of items wrapped in a custom class extending Flow called CFlow.
I took the code for CFlow from kotlinconf-app and here:
fun <T> Flow<T>.asCFlow(): CFlow<T> = CFlow(this)
class CFlow<T>(private val origin: Flow<T>) : Flow<T> by origin {
fun watch(block: (T) -> Unit): Closeable {
val job = Job()
onEach {
block(it)
}.launchIn(CoroutineScope(Dispatchers.Main + job))
return object : Closeable {
override fun close() {
job.cancel()
}
}
}
}
Repository example functions:
fun getData1(): CFlow<List<Profile>>
fun getData2(): CFlow<List<String>>
When I try to call this functions in iOS swift code the return type of the functions get converted to CFlow<NSArray> and inside of watch function the type of array is Any.
This is weird because in both kotlinconf-app and here the return types of functions are preserved and there is no casting involved in their codebase.
Question: How can I make the type of CFlow to be known in Xcode iOS project?
Android Studio version: 4.1.1
Kotlin lang and plugin version: 1.4.21
Kotlin Multiplatform Mobile plugin version: 0.2.0
Xcode version: 12.2
right now the compiler can't understand nested generics. as a workaround for now, wrap your list inside a data class like this
data class ProfileResult(val data: List<Profile>)
fun getData1(): CFlow<ProfileResult>
it will give you a concrete type in ios
This is because there are no generics in Objective-C. Arrays are ordered collections of objects.
So using any generic collection type in Kotlin, will lose it's type when translated to NSArray
I believe you have three options here:
Wait for direct Kotlin - Swift interop (which is postponed currently)
Cast values in Swift
Don't use generics with collections. I'm personally not using the Flow wrapper currently and doing something like this:
fun observeItems(onChange: (List<Item>) -> Unit) {
items.onEach {
onChange(it)
}.launchIn(coroutineScope)
}
Exposing a dispose function to iOS
fun dispose() {
coroutineScope.cancel()
}
And consuming like this:
repo.observeItems { items in
...
}
But definitely this is more work and hopefully these interop issues will be solved along the way
I'm developing in MVP. In my Presenter, I call my Repository thanks to the suspend function. In this suspend function, I launch a Coroutine - not on the main thread.
When this coroutine is finished, I want to execute some code: I am using withContext() in order to do so.
In my Repository, I am launching a Coroutine (and maybe I am wrong) to insert my data, using DAO, in my Room database.
When I debug my application, it goes into my Presenter but crashes before going into my Repository.
Presenter
override suspend fun insertUserResponse(activity: Activity, data: UserResponse) {
scope.launch(Dispatchers.IO) {
try {
userResponseRepository.insertUserResponse(data)
withContext(Dispatchers.Main) {
redirectToClientMainPage(activity)
}
} catch (error: Exception) {
parentJob.cancel()
Log.e("ERROR PRESENTER", "${error.message}")
}
}
}
Repository
override suspend fun insertUserResponse(userResponse: UserResponse) {
GlobalScope.launch(Dispatchers.IO) {
try {
val existingUser: UserResponse? =
userResponseDAO.searchUserByID(userResponse.profilePOJO.uniqueID)
existingUser?.let {
userResponseDAO.updateUser(userResponse)
} ?: userResponseDAO.insertUser(userResponse)
} catch (error: Exception) {
Log.e("ERROR REPOSITORY", "${error.message}")
}
}
}
I have no error shown in my logcat.
EDIT:
Scope initialization
private var parentJob: Job = Job()
override val coroutineContext: CoroutineContext
get() = uiContext + parentJob
private val scope = CoroutineScope(coroutineContext)
val uiContext: CoroutineContext = Dispatchers.Main (initialized in my class constructor)
Stack trace
I finally found the answer!
Thanks to #sergiy I read part II of this article https://medium.com/androiddevelopers/coroutines-on-android-part-ii-getting-started-3bff117176dd and it mentions that you can't catch error except Throwable and CancellationException.
So instead of catching Exception, I traded it for Throwable. And finally I had an error shown in my logcat.
I am using Koin to inject my repository and all. I was missing my androidContext() in my Koin application.
That's it.
Without stacktrace it's hard to help you.
Here is an article, that might be helpful. Replacing ViewModel mentioned in the article with Presenter in your case you can get several general recommendations in using coroutines:
As a general pattern, start coroutines in the ViewModel (Read: Presenter)
I don't see the need to launch one more coroutine in your case in Repository.
As for switching coroutine's context for Room:
Room uses its own dispatcher to run queries on a background thread. Your code should not use withContext(Dispatchers.IO) to call suspending room queries. It will complicate the code and make your queries run slower.
I couldn't find the same recommendation in official docs, but it's mentioned in one of the Google code labs.
Both Room and Retrofit make suspending functions main-safe.
It's safe to call these suspend funs from Dispatchers.Main, even though they fetch from the network and write to the database.
So you can omit launch (as well as withContext) in Repository, since Room guarantees that all methods with suspend are main-safe. That means that they can be called even from the main thread. Also you can not to define Dispatchers.IO explicitly in Presenter.
One more thing. If Presenter's method insertUserResponse is suspend, then you call it from another launched coroutine, don't you? In that case, why you launch one more coroutine inside this method? Or maybe this method shouldn't be suspend?
There are apparently Kotlin coroutines extension functions for SqlDelight, but I don't know how to implement them since I can't find documentation.
I have a normal query that looks like this:
val allItems
get() = itemQueries.selectAll().mapToList()
Can I turn this into a suspend function?
There is currently (v1.2.1) no suspend function support for SqlDelight queries, however you can consume a Coroutines Flow object, which is even better. To do this you need to add the coroutines extension library in your app gradle:
dependencies {
implementation "com.squareup.sqldelight:coroutines-extensions:1.2.1"
}
Then turn your query into this:
val allItems: Flow<List<Item>> =
itemQueries.selectAll()
.asFlow()
.mapToList()
This flow emits the query result, and emits a new result every time the database changes for that query.
You can then .collect{} the results inside a coroutine scope.
For single-shot queries, you don't need the coroutine extension library. Instead, just do:
suspend fun getAllItems() = withContext(Dispatchers.IO) {
itemQueries.selectAll().mapToList()
}
The other answer is specific to when you want to react to changes in the database.
I'm trying to rewrite interactors with rxjava chains to kotlin flow. In LocationHandlerImpl I'm using LocationService for getting my current location. In addOnSuccessListener and addOnFailureListener I'm emitting my model but having error:
"Suspension function can be called only within coroutine body". Am i doing it wrong? But i can call emit outside of listeners (look below flow builder)
It seems that you are trying to get the last location from the Android location service. This is one of many Task-returning calls in the Google Play Services. Kotlin already has a module, kotlinx-coroutines-play-services, that contributes a function
suspend fun <T> Task<T>.await(): T?
With that in your project, you can simply write this:
suspend fun getMyLocation(): Location? =
LocationServices.getFusedLocationProvider(context)
.lastLocation
.await()
If you want to integrate it with other Flow-based code, add this wrapper function:
fun <T> Task<T>.asFlow() = flow { emit(await()) }
and now you can write
fun getLocationAsFlow(): Flow<Location?> =
LocationServices.getFusedLocationProvider(context)
.lastLocation
.asFlow()
If, for educational purposes, you would like to see how it can be implemented directly, without the additional module, then the most straightforward approach would be as follows:
fun getLocationAsFlow() = flow {
val location = suspendCancellableCoroutine<Location?> { cont ->
LocationServices.getFusedLocationProvider(context)
.lastLocation
.addOnCompleteListener {
val e = exception
when {
e != null -> cont.resumeWithException(e)
isCanceled -> cont.cancel()
else -> cont.resume(result)
}
}
}
emit(location)
}
This is the result of inlining a simplified implementation of Task.await() into its use site.
As mentioned from Prokash (here), Flows are designed to be self contained. Your location services listener is not within the scope of the Flow.
You can however check out the callbackFlow which provides you the mechanics you are looking for to build a flow using a Callback-based API.
Callback Flow Documentation, be aware that the callback flow is still in Experimental phase.