I'm trying to get my head around suspendCoroutine and suspendCancellableCoroutine. I think they could be useful in the following case:
When the coroutine is launched, check if the user is logged in.
If not, ask for credentials and pause the currently executing coroutine.
When the credentials are submitted, resume the coroutine from the same line where it was suspended.
This compiles but never makes it past "delay over", i.e. the continuation never resumes:
import kotlinx.coroutines.*
fun main(args: Array<String>) {
println("Hello, world!")
runBlocking {
launch {
postComment()
}
}
}
var isLoggedIn = false
var loginContinuation: CancellableContinuation<Unit>? = null
suspend fun postComment() {
if (!isLoggedIn) {
showLoginForm()
suspendCancellableCoroutine<Unit> {
loginContinuation = it
}
}
// call the api or whatever
delay(1000)
println("comment posted!")
}
suspend fun showLoginForm() {
println("show login form")
// simulate delay while user enters credentials
delay(1000)
println("delay over")
isLoggedIn = true
// resume coroutine on submit
loginContinuation?.resume(Unit) { println("login cancelled") }
}
I've tried everything I can think of, including moving the call to suspendCancellableCoroutine outside of the login check, wrapping the contents of showLoginForm in withContext(Dispatchers.IO), using coroutineScope.launch(newSingleThreadContext("MyOwnThread"), etc. The impression I get from reading the internet is that this is a valid use case. What am I doing wrong?
First of all, you misunderstand the concept of suspend functions. Calling function showLoginForm() does not start a new coroutine. Code in a single coroutine is always executed sequentially - at first you call showLoginForm(), it delays, it does not resume any continuations because loginContinuation is null, and then suspendCancellableCoroutine suspends your coroutine forever and causes a deadlock.
Starting a new coroutine that executes showLoginForm() can make your code work:
suspend fun CoroutineScope.postComment() {
if (!isLoggedIn) {
launch {
showLoginForm()
}
suspendCancellableCoroutine<Unit> {
loginContinuation = it
}
}
// call the api or whatever
delay(1000)
println("comment posted!")
}
This code still can fail (*), but in this particular case it does not. Working version of this code can look like this:
import kotlin.coroutines.*
import kotlinx.coroutines.*
fun main(args: Array<String>) {
println("Hello, world!")
runBlocking {
postComment()
}
}
var isLoggedIn = false
suspend fun CoroutineScope.postComment() {
if (!isLoggedIn) {
suspendCancellableCoroutine<Unit> { continuation ->
launch {
showLoginForm(continuation)
}
}
}
delay(1000)
println("comment posted!")
}
suspend fun showLoginForm(continuation: CancellableContinuation<Unit>) {
println("show login form")
delay(1000)
println("delay over")
isLoggedIn = true
continuation.resume(Unit) { println("login cancelled") }
}
Also, in your example suspending coroutines is not needed. Why do we need another coroutine if we can just execute its code in the same coroutine? We need to wait until it finishes anyway. Since coroutines execute code sequentially, we will go to the code after if branch only after showLoginForm() finishes:
var isLoggedIn = false
suspend fun postComment() {
if (!isLoggedIn) {
showLoginForm()
}
delay(1000)
println("comment posted!")
}
suspend fun showLoginForm() {
println("show login form")
delay(1000)
println("delay over")
isLoggedIn = true
}
This approach is the best for your example, where all code is sequential.
(*) - This code still can cause deadlock if suspendCancellableCoroutine is invoked after showLoginForm finishes - for example, if you remove delay call in showLoginForm or if you use a multithreaded dispatcher - in JVM there is no guarantee that suspendCancellableCoroutine will be invoked earlier than showLoginForm. Moreover, loginContinuation is not #Volatile, so with multithreaded dispatcher the code can fail also from visibility issues - thread that executes showLoginForm may observe that loginContinuation is null.
Passing around Continuations is messy and can easily lead to the error you have...one function finishes before the continuation has even been assigned to the continuation property.
Since the login form is what you want to turn into a suspend function, that's where you should use suspendCoroutine. suspendCoroutine is low level code that you should put as low as possible so your main program logic can use easy-to-read sequential coroutines without the nested launch/suspendCoroutine calls.
var isLoggedIn = false
suspend fun postComment() {
if (!isLoggedIn) {
showLoginForm()
}
println("is logged in: $isLoggedIn")
if (isLoggedIn) {
// call the api or whatever
delay(1000)
println("comment posted!")
}
}
suspend fun showLoginForm(): Unit = suspendCancellableCoroutine { cont ->
println("Login or leave blank to cancel:")
//Simulate user login or cancel with console input
val userInput = readLine()
isLoggedIn = !userInput.isNullOrBlank()
cont.resume(Unit)
}
I didn't use delay() in showLoginForm() because you can't call suspend functions within a suspendCancellableCoroutine block. Those last three lines could also be wrapped in a scope.launch and use delay instead of readLine, but in reality, your UI interaction wouldn't be a coroutine with a delay anyway.
EDIT:
Trying to pass a continuation to another Activity would be especially messy. Google does not even recommend using multiple Activities in an app because it is difficult to pass objects between them. To do it with Fragments, you could maybe write your LoginFragment class to have a private continuation property like this:
class LoginFragment(): Fragment {
private val continuation: Continuation<Boolean>? = null
private var loginComplete = false
suspend fun show(manager: FragmentManager, #IdRes containerViewId: Int, tag: String? = null): Boolean = suspendCancelableCoroutine { cont ->
continuation = cont
retainInstance = true
manager.beginTransaction().apply {
replace(containerViewId, this#LoginFragment, tag)
addToBackStack(null)
commit()
}
}
// Call this when login is complete:
private fun onLoginSuccessful() {
loginComplete = true
activity?.fragmentManager?.popBackStack()
}
override fun onDestroy() {
super.onDestroy()
continuation?.resume(loginComplete)
}
}
Then you would show this fragment from another fragment like this:
lifecycleScope.launch {
val loggedIn = LoginFragment().show(requireActivity().fragmentManager, R.id.fragContainer)
// respond to login state here
}
So long as you are using a Fragment's lifecycleScope rather than an Activity's lifecycleScope and the first Fragment also uses retainInstance = true, I think you should be safe from screen rotations. But I haven't done this myself.
Related
fun viewDidLoad() {
observeViewModel()
initializeUI()
getVideos()
getQuestions()
}
fun initializeUI() {
/*
1 - Create a list as;
VideosTitleRow("Videos"),
VideosBodyRow(null),
QuestionTitleRow("Questions"),
QuestionsBodyRow(null),
...."
Note: The bodies are null right now. There will fill with correct values after.
---
2 - Call updateUI functions for update ui with initialize list.
*/
}
fun getVideos() {
viewModel.getVideos()
}
fun getQuestions() {
viewModel.getQuestions()
}
fun observeViewModel() {
// Videos
CoroutineScope(Dispatchers.IO).launch {
when (flow) {
// ...
is VideosFlow.DataReceived -> {
val row = SectionVideosBodyData(flow.videos)
updateUI(1, row)
}
}
}
// Questions
CoroutineScope(Dispatchers.IO).launch {
when (flow) {
// ...
is QuestionsFlow.DataReceived -> {
val row = SectionQuestionsBodyData(flow.questions)
updateUI(3, row)
}
}
}
// And others LiveDatas under observing ....
}
fun updateUI(indexAt: Int? = null, row: BaseAdapterData = null) {
/*
If indexAt and row parameters are not null;
edit the global row list then created new row list from this (global list) and send the new list to adapter for update ui.
If parameters are null; send the global list to adapter for update ui.
*/
}
This is the current structure.
Let me try to explain what is problem;
When i start the initializeUI() function i know when it will finish the job. Because this is a synchronous function.
getVideos() or getQuestions() functions are a synchronous functions too but they start to api call, send request, take response, convert to ui model, bla bla operations. And we have to observe the results. So, we don't know when this job will finish!
getVideos() function just like says to viewModel.getVideos; "Let me know when you see the red car" but we can't know when is the red car pass on street.
In this case; although getQuestions() function is called before getVideos(), it may finish before it. In fact, both can end at the same time.
Here is i need;
I want to use kotlin coroutines scopes in getVideos(), getQuestions(), ... functions run as asynchronous but every job must wait for finished updateUI() function.
1- initializeUI() function runs, create a list, send to adapter, update the ui,
2- getVideos() or getQuestions() runs as asynchronous,
let say; livedata(getVideos) finish the their work but livedata (getQuestions) still working.
getQuestion() when call the updateUI() function, getVideos() can not call until updateUI() function finished called from getQuestion()
I know this is a complicated case :)
But the LiveData is messing up my mind.
I tried Mutex but although getVideos() or getQuestions() functions are synchronous, they starts an asynchronous job.
(Kotlin Coroutines sequential execution)
I know AsyncRestTemplate is deprecated but anyway. Am I doing right writing code like this to marry AsyncRestTemplate with coroutines?
suspend fun dodo(url: URL): String {
val result = AsyncRestTemplate().getForEntity(url, String::class.java)
return result.awaitBody()
}
suspend fun <T> ListenableFuture<ResponseEntity<T>>.awaitBody(): T {
while (this.isDone.not)
yield()
return this.get().body!!
}
Your code is correct but inefficient, since it is effectively a busy-wait loop repeatedly calling ListenableFuture.isDone until it finishes.
You should make use of the callback API provided whenever you need to convert between an async API and suspend functions:
suspend fun <T> ListenableFuture<T>.await(): T = suspendCancellableCoroutine { cont ->
// Special handling for Future<*> objects
cont.cancelFutureOnCancellation(this)
addCallback(object : ListenableFutureCallback<T> {
override fun onFailure(ex: Throwable) {
cont.resumeWithException(ex)
}
override fun onSuccess(result: T) {
cont.resume(result)
}
})
}
This will not waste time checking if the future finishes, but just tell the future to resume the suspended function when a result is actually available.
Im running this code since addListenerForSingleEvent is a long Running operation:
CoroutineScope(IO).launch {
userRef.addListenerForSingleValueEvent(object : ValueEventListener {
override fun onCancelled(p0: DatabaseError) {
}
override fun onDataChange(p0: DataSnapshot) {
if (p0.exists()) {
withContext(Main) {
toggleLoading()
val intent = Intent(this#LogInActivity, MainActivity::class.java)
startActivity(intent)
finish()
}
} else{
withContext(Main) {
var addUsernameIntent = Intent(this#LogInActivity,
AddUsernameActivity::class.java)
startActivityForResult(addUsernameIntent, CHOOSE_USERNAME_REQUEST)
}
}
}
})
}
I get an error where i write withContext(Main) that says :
Suspension functions can be called only within coroutine body
But I have a coroutine body right? Before i just had a Thread(runnable {..}) instead of a couroutine, but i read that i shouldn't do intents inside any other Thread than main-thread so i changed to coroutine.
The Firebase client already runs any network and disk I/O operations on a separate thread. There almost never is a need to run addListenerForSingleEvent on a separate thread yourself.
Also see:
Do we need to use background thread for retrieving data using firebase?
Firebase Android: onDataChange() event always executed in Main UI Thread?
Firebase database - run on different thread
Call method in another thread on completion
Function of an anonymous object may capture the variables of the scope, but it is not in enclosing coroutine body. Replace withContext(Main) with creating a new coroutine: <CoroutineScope>.launch(Main).
Firebase anonymous sign in returns a task (which is basically Google promise implementation):
val task:Task<AuthResult> = FirebaseAuth.getInstance().signInAnonymously()
How it would be possible create a signInAnonymous wrapper where:
It is a suspend function, waiting for the task completion
suspend fun signInAnonymous(): Unit
It returns a Deferred object, delivering the result asynchronously
fun signInAnonymous() : Deferred
The package kotlinx.coroutines.tasks now includes the follwing utility functions:
public suspend fun <T> Task<T>.await(): T { ... }
From the docs:
Awaits for completion of the task without blocking a thread.
This suspending function is cancellable.
If the Job of the current coroutine is cancelled or completed while this suspending function is waiting, this function stops waiting for the completion stage and immediately resumes with CancellationException.
public fun <T> Task<T>.asDeferred(): Deferred<T> { ... }
From the docs:
Converts this task to an instance of Deferred.
If task is cancelled then resulting deferred will be cancelled as well.
So you can just do:
suspend fun signInAnonymouslyAwait(): AuthResult {
return FirebaseAuth.getInstance().signInAnonymously().await()
}
or:
fun signInAnonymouslyDeferred(): Deferred<AuthResult> {
return FirebaseAuth.getInstance().signInAnonymously().asDeferred()
}
Based on this GitHub library, here's a way to transform a Task into a suspending function in the "usual" way to adapt callback based async calls to coroutines:
suspend fun <T> Task<T>.await(): T = suspendCoroutine { continuation ->
addOnCompleteListener { task ->
if (task.isSuccessful) {
continuation.resume(task.result)
} else {
continuation.resumeWithException(task.exception ?: RuntimeException("Unknown task exception"))
}
}
}
You can also wrap it in a Deferred of course, CompletableDeferred comes in handy here:
fun <T> Task<T>.asDeferred(): Deferred<T> {
val deferred = CompletableDeferred<T>()
deferred.invokeOnCompletion {
if (deferred.isCancelled) {
// optional, handle coroutine cancellation however you'd like here
}
}
this.addOnSuccessListener { result -> deferred.complete(result) }
this.addOnFailureListener { exception -> deferred.completeExceptionally(exception) }
return deferred
}
Add this to gradle
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.4.3'
And then you can use it like this:
suspend fun login(email: String, pass: String) {
FirebaseAuth.getInstance().signInWithEmailAndPassword(email, pass).await()
}
To transform it into a coroutine-ready function, I would use the Tasks.await() function from the Tasks API:
suspend fun FirebaseAuth.signInAnonymouslyAwait(): AuthResult {
return Tasks.await(this.signInAnonymously())
}
As for Deferred, i'd stick with zsmb13's answer
I'm trying to access chatting room Using firestore and coroutines.
fun getOwner() {
runBlocking {
var de = async(Dispatchers.IO) {
firestore.collection("Chat").document("cF7DrENgQ4noWjr3SxKX").get()
}
var result = de.await().result
}
But i get error like this :
E/AndroidRuntime: FATAL EXCEPTION: Timer-0
Process: com.example.map_fetchuser_trest, PID: 19329
java.lang.IllegalStateException: Task is not yet complete
at com.google.android.gms.common.internal.Preconditions.checkState(Unknown Source:29)
at com.google.android.gms.tasks.zzu.zzb(Unknown Source:121)
at com.google.android.gms.tasks.zzu.getResult(Unknown Source:12)
at com.example.map_fetchuser_trest.model.Repository$getOwner$1.invokeSuspend(Repository.kt:53)
How can i get chat document? when i use origin api like below, I can access chat room document.
firestore.collection("Chat").document(
"cF7DrENgQ4noWjr3SxKX"
).get().addOnCompleteListener { task ->
if (task.isSuccessful) {
val chatDTO = task.result?.toObject(Appointment::class.java)
}
}
Task is the thing one awaits on, but you wrapped it in another layer of async. Remove the async:
fun getOwner() {
runBlocking {
var de = firestore.collection("Chat").document("cF7DrENgQ4noWjr3SxKX").get()
var result = de.await().result
}
}
However, by using runBlocking() you have shot yourself in the foot and wrote blocking code that just formally uses an async API to no good effect.
To truly benefit from it, you must have a
suspend fun getOwner() = firestore
.collection("Chat")
.document("cF7DrENgQ4noWjr3SxKX")
.get()
.await()
.result
and launch a coroutine at the place you call it from:
launch {
val owner = getOwner()
// update the GUI
}
This assumes you're calling launch from an object that is a CoroutineScope.
val db = FirebaseFirestore.getInstance()
override suspend fun saveBinToDB(bin: Bin): Result<Unit> {
lateinit var result:Result<Unit>
db.collection("bins")
.add(bin)
.addOnSuccessListener { documentReference ->
Log.d(TAG, "DocumentSnapshot written with ID: ${documentReference.id}")
result = Result.Success(Unit)
}
.addOnFailureListener { e ->
Log.w(TAG, "Error adding document", e)
result = Result.Error(Exception())
}
.await()
return result
}
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.3.7"
The usage of runBlocking{..} in the first code snippet works as follows: the runBlocking function blocks to execute the parameter lambda code (and the lambda code will suspend inside). It gives less sense.
You may want to start a coroutine with launch{..} function instead and use withContext(Dispatchers.Main){..} to have the block executed in the UI thread, e.g. to show the fetched results. You may also implement CoroutineScope in your activity class.
The first step - you need to turn the Firebase API call into a suspending function. It could be done with suspendCoroutine{..} function (there are several more functions like suspendCancellableCoroutine{..} in the kotlinx.coroutines library.
There is an integration library with Google Play Services that provides support for Firebase
https://github.com/Kotlin/kotlinx.coroutines/tree/master/integration/kotlinx-coroutines-play-services
I struggled to get await() function. Check your build.gradle should have below dependencies -
def coroutines = '1.6.4'
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines"
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
api "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$coroutines"
Now instead of Complete/Failure Listener, use await() just like used in #Marko answer -
suspend fun getDataFromFireStore() = firestore
.collection("some key")
.document("some key")
.get()
.await()
.documents