I am currently building an app using AWS SDK. One of the API is a sign in and is requiring, in addition to email and password, a Callback in order to get back the status of the request. The issue is that I am not able to send back the result.
This is my code:
override suspend fun signIn(email: String, password: String): Result<SignInResult> =
withContext(ioDispatcher) {
try {
api.signIn(email, password, object : Callback<SignInResult> {
override fun onResult(result: SignInResult?) {
Result.Success(result!!)
}
override fun onError(e: Exception?) {
Result.Error(e!!)
}
})
} catch (e: Exception) {
Result.Error(e)
}
}
The issue is that coroutine sign in is requiring a return of Result but I do not know what to return because I should only return when onResult, onError and when catching an exception.
Any idea how to make it works ?
Thanks
You can use suspendCoroutine or suspendCancellableCoroutine to work with callbacks:
override suspend fun signIn(email: String, password: String): Result<SignInResult> =
suspendCoroutine { continuation ->
try {
api.signIn(email, password, object : Callback<SignInResult> {
override fun onResult(result: SignInResult) {
// Resume coroutine with a value provided by the callback
continuation.resumeWith(Result.Success(result))
}
override fun onError(e: Exception) {
continuation.resumeWith(Result.Error(e))
}
})
} catch (e: Exception) {
continuation.resumeWith(Result.Error(e))
}
}
suspendCoroutine suspends coroutine in which it executed until we decide to continue by calling appropriate methods - Continuation.resume.... suspendCoroutine mainly used when we have some legacy code with callbacks.
There is also suspendCancellableCoroutine builder function, it behaves similar to suspendCoroutine with additional feature - provides an implementation of CancellableContinuation to the block.
Related
I am using Stripe library which provides me with custom callback functionality.
I want a custom callback convert to Kotlin coroutine
Here is the code
override fun retrievePaymentIntent(clientSecret: String): Flow<Resource<PaymentIntent>> = flow{
emit(Resource.Loading())
Terminal.getInstance().retrievePaymentIntent(clientSecret,
object : PaymentIntentCallback {
override fun onFailure(e: TerminalException) {}
override fun onSuccess(paymentIntent: PaymentIntent) {
emit(Resource.Success(paymentIntent))
}
})
}
The problem is I can't call emit function inside onSuccess/onFailure. The error shown in the picture.
Is it possible to change something here to make it work or how could I convert custom callback to coroutine?
You can use suspendCancellableCoroutine to model your callback-based one-shot request like so:
suspend fun retrievePaymentIntent(clientSecret: String): PaymentIntent =
suspendCancellableCoroutine { continuation ->
Terminal.getInstance().retrievePaymentIntent(clientSecret,
object : PaymentIntentCallback {
override fun onFailure(e: TerminalException)
{
continuation.resumeWithException(e)
}
override fun onSuccess(paymentIntent: PaymentIntent)
{
continuation.resume(paymentIntent)
}
})
continuation.invokeOnCancellation { /*cancel the payment intent retrieval if possible*/ }
}
I am building an app which is using AWS mobile Client SDK but I would like to use coroutine but I have an issue as the SDK from AWS is using Callback.
override suspend fun login(username: String, password: String): ResultResponse<SignInResult> {
AWSMobileClient.getInstance()
.signIn(username, password, null, object : Callback<SignInResult> {
override fun onResult(signInResult: SignInResult) {
ResultResponse.Success(signInResult)
}
override fun onError(e: Exception?) {
if (e != null) {
ResultResponse.Error(e)
}
}
})
}
Any idea how to workaround this or do it properly ?
Regards
Asynchronous callbacks can be converted into synchronous suspend functions using either suspendCoroutine() or suspendCancellableCoroutine(). Typically you would create a suspend extension function version of the corresponding asynchronous call so you can use it anywhere you would normally use the SDK call (rather than using suspendCoroutine at each location, since it is somewhat clumsy).
suspendCancellableCoroutine is used when the API call provides a way to cancel it early. If that's the case, you can make your suspend function support coroutine cancellation, so the API call is automatically cancelled if the associated coroutine is cancelled. Exactly what you want since on Android, lifecycleScope and viewModelScope automatically cancel their coroutines when their associated lifecycles are over.
I don't know what version of AWS SDK you're using, but here's approximately how you what your suspend function would look like:
suspend fun AWSMobileClient.signIn(userName: String, password: String, someParam: Any?): SignInResult = suspendCoroutine { continuation ->
signIn(userName, password, someParam, object : Callback<SignInResult> {
override fun onResult(signInResult: SignInResult) {
continuation.resume(signInResult)
}
override fun onError(e: Exception) {
continuation.resumeWithException(e)
}
})
}
I don't know the type of that third parameter so I just put Any?.
And now your existing function would become:
override suspend fun login(username: String, password: String): ResultResponse<SignInResult> {
return try {
val signInResult = AWSMobileClient.getInstance().signIn(username, password, null)
ResultResponse.Success(signInResult)
} catch(e: Exception) {
ResultResponse.Error(e)
}
}
I'm not sure if ResultResponse is your own class. In case it is, be aware the Kotlin standard library already contains a Result class, which would make this function simpler with the use of runCatching:
override suspend fun login(username: String, password: String): Result<SignInResult> {
return runCatching {
AWSMobileClient.getInstance().signIn(username, password, null)
}
}
The usual way to convert callback-based APIs to suspending functions is to use suspendCoroutine or suspendCancellableCoroutine (if the callback API is cancellable).
In your case, it will look something like this:
override suspend fun login(username: String, password: String): SignInResult {
return suspendCoroutine { cont ->
AWSMobileClient.getInstance()
.signIn(username, password, null, object : Callback<SignInResult> {
override fun onResult(signInResult: SignInResult) {
cont.resume(signInResult)
}
override fun onError(e: Exception?) {
if (e != null) {
cont.resumeWithException(e)
}
}
})
}
}
This suspends the current coroutine until the callback is called with the result or an error. Note that the login() method will throw an exception if onError is called (which is usually what you want when using suspend functions).
I assume you are using this SDK
https://docs.aws.amazon.com/sdk-for-android/
If you want to use an AWS SDK that supports Kotlin features likes coroutines, use the new AWS SDK for Kotlin.
Setting up the AWS SDK for Kotlin
I am reading about Kotlin coroutine in Google 's documentation. I'm adviced to use withContext(Dispacher.IO) to a different thread to main-safety. But I have a problem , fetchData() done before response from server so fetchData() return null result. Any help that I appreciate.
https://developer.android.com/kotlin/coroutines/coroutines-best-practices#main-safe
class GameRemoteDataSource #Inject constructor(val api : GameApi) {
val IODispatcher: CoroutineDispatcher = Dispatchers.IO
suspend fun fetchData() : Resource<ListGameResponse> {
var resource : Resource<ListGameResponse> = Resource.loading(null)
withContext(IODispatcher){
Log.d("AAA Thread 1", "${Thread.currentThread().name}")
api.getAllGame(page = 1).enqueue(object : Callback<ListGameResponse>{
override fun onResponse(
call: Call<ListGameResponse>,
response: Response<ListGameResponse>
) {
if(response.code()==200){
resource = Resource.success(response.body())
}else{
resource = Resource.success(response.body())
}
Log.d("AAA code",response.code().toString())
}
override fun onFailure(call: Call<ListGameResponse>, t: Throwable) {
resource = Resource.error(t.message.toString(),null)
Log.d("AAA Thread", "${Thread.currentThread()}")
}
})
Log.d("AAA Thread", "${Thread.currentThread()}")
Log.d("AAA resource",resource.data.toString()+ resource.status.toString())
}
return resource
}
}
withContext is not helpful for converting an asynchronous function with callback into suspending code that can be used in a coroutine. It is more applicable to converting synchronous blocking code. Your non-working strategy of creating an empty variable and trying to fill it in the callback to synchronously return is described in the answers to this question.
For an asynchronous function with callback, if it returns a single value like your code above, this is typically converted to a suspend function using suspendCoroutine or suspendCancellableCoroutine. If it returns a series of values over time (calls the callback multiple times), it would be fitting to use callbackFlow to convert it to a Flow that can be collected in a coroutine.
But it looks like you're using Retrofit, which already has a suspend function alternatives to enqueue so you don't need to worry about all this. You can use the await() or awaitResponse() functions instead. In this case, await() would return ListGameResponse and awaitResponse() would return Response<ListGameResponse>. So awaitResponse() is better if you need to check the response code.
Awaiting returns the response and throws an exception if there's an error, so you can use try/catch instead of adding a failure listener.
class GameRemoteDataSource #Inject constructor(val api : GameApi) {
suspend fun fetchData(): Resource<ListGameResponse> {
return try {
val response = api.getAllGame(page = 1).awaitResponse()
Log.d("AAA code", response.code().toString())
Resource.success(response.body())
} catch (exception: Exception) {
Resource.error(exception.message.toString(),null)
}
}
}
You should use suspendCancellableCoroutine to convert asynchronous API into a coroutine flow, like this
suspend fun fetchData(): ListGameResponse = withTimeout(Duration.seconds(60)) {
suspendCancellableCoroutine<ListGameResponse> { cont ->
api.getAllGame(page = 1).enqueue(object : Callback<ListGameResponse> {
override fun onResponse(
call: Call<ListGameResponse>,
response: Response<ListGameResponse>
) {
Log.d("AAA code", response.code().toString())
cont.resume(response.body())
}
override fun onFailure(call: Call<ListGameResponse>, t: Throwable) {
cont.resumeWithException(t)
}
})
}
}
I have function:
#ExperimentalCoroutinesApi
override suspend fun checkIsPostLiked(userId: String, postId: String): Flow<FirebaseEventResponse> = callbackFlow {
try {
FirebaseFirestore.getInstance().collection(postCollection).document(postId).get().addOnSuccessListener {
trySend(SuccessDocument(it))
}.addOnFailureListener {
trySend(ExceptionEvent(it))
}.await()
} catch (e: Exception) {
trySend(ExceptionEvent(e))
}
awaitClose { this.cancel() }
}
When i want to use it second time code is not called. I tested another function with only Log.d inside and had the same problem.
A flow is a type that can emit multiple values sequentially. An addOnSuccessListener or addOnFailureListener would emit their results just once. So what you probably want to use is a suspendCancellableCoroutine builder that can be used for one-shot requests.
Here's what that might look like:
override suspend fun checkIsPostLiked(userId: String, postId: String): FirebaseEventResponse = suspendCancellableCoroutine { continuation ->
try {
FirebaseFirestore.getInstance().collection(postCollection).document(postId).get().addOnSuccessListener {
continuation.resume(SuccessDocument(it), null)
}.addOnFailureListener {
continuation.resumeWithException(ExceptionEvent(it))
}
} catch (e: Exception) {
continuation.resumeWithException(ExceptionEvent(e))
}
}
And you can call it in any coroutine scope (e.g viewModelScope) like so:
viewModelScope.launch {
try {
val isPostLiked = checkIfPostIsLiked(userId, postId)
} catch(e: Exception) {
// handle exception
}
}
Side benefit: You don't have to use the #ExperimentalCoroutinesApi decorator ;).
EDIT
If you're using await then you're already using Firestore with coroutines. So there's no need to use any coroutine builder, just call the suspended function without the addOnSuccessListener and addOnFailureListener
override suspend fun checkIsPostLiked(userId: String, postId: String): FirebaseEventResponse =
try {
FirebaseFirestore.getInstance()
.collection(postCollection)
.document(postId)
.get()
.await()
} catch (e: Exception) {
// handle exception
}
}
I'm trying to change all my callbacks to coroutines, I have readed about them and they are fasinating !
What I want to accomplish is just login a user, but if the login logic fails, notify it to my presenter.
This is what I have made
LoginPresenter.kt
class LoginPresenter #Inject constructor(private val signInInteractor: SignInInteractor) : LoginContract.Presenter, CoroutineScope {
private val job = Job()
override val coroutineContext: CoroutineContext = job + Dispatchers.Main
override fun signInWithCoroutines(email: String, password: String) {
launch {
view?.showProgress()
withContext(Dispatchers.IO){
signInInteractor.signInWithCoroutinesTest(email,password)
}
view?.hideProgress()
}
}
}
And now the problem is in my interactor, since its a suspend function, I would love to return an error response in order to do a view.showError(errorMsg) from my presenter
SignInInteractor.kt
override suspend fun signInWithCoroutinesTest(email: String, password: String) {
FirebaseAuth.getInstance()?.signInWithEmailAndPassword(email, password).addOnCompleteListener {
if(it.isSuccessful){
//Want to notify the presenter that the coroutine has ended succefully
}else{
//want to let the courutine know about it.exception.message.tostring
}
}
}
The way I was doing it was with callbacks that notify my presenter
override fun signInWithCoroutinesTest(email: String, password: String) {
FirebaseAuth.getInstance()?.signInWithEmailAndPassword(email, password,listener:OnSuccessCallback).addOnCompleteListener {
if(it.isSuccessful){
listener.onSuccess()
}else{
listener.onFailure(it.exception.message.toString())
}
}
}
Question
How to return if the operation has succeded from coroutines and notify my presenter?
Thanks
You must explicitly suspend the coroutine:
override suspend fun signInWithCoroutinesTest(
email: String, password: String
) = suspendCancellableCoroutine { continuation ->
FirebaseAuth.getInstance()?.signInWithEmailAndPassword(email, password).addOnCompleteListener {
if (it.isSuccessful) {
continuation.resume(Unit)
} else {
continuation.resumeWithException(it.exception)
}
}
}
Also, since your code is suspendable and not blocking, don't run it withContext(IO). Simply call it directly from the main thread, that's the beauty of coroutines.
Think of coroutines as you would normal, synchronous code. How would you write this if the background work completed immediately? Maybe something like this:
override fun signInWithCoroutinesTest(email: String, password: String) {
view?.showProgress()
if(!signInInteractor.signIn(email,password)) view?.showSignInError()
view?.hideProgress()
}
Or if you want to catch the error, something like this
override fun signInWithCoroutinesTest(email: String, password: String) {
view?.showProgress()
try {
signInInteractor.signIn(email,password))
} catch(e: AuthenticationException) {
view?.showError(e.message)
}
view?.hideProgress()
}
With coroutines, you just write the exact same code but the methods themselves suspend rather than block threads. So in this case signIn would be a suspending function and will need to be called from a coroutine or other suspend function. Based on that, you probably want the outer function to suspend and then you would launch that coroutine rather than trying to launch inside of signInWithCoroutinesTest.
The original example isn't completely realistic but normally you would launch this kind of thing from an existing scope, perhaps associated with your activity or viewModel. Ultimately, it would look something like this:
fun hypotheticalLogic() {
...
viewModelScope.launch {
signInWithCoroutinesTest(email, password)
}
...
}
override suspend fun signInWithCoroutinesTest(email: String, password: String) {
view?.showProgress()
try {
signInInteractor.signIn(email,password))
} catch(e: AuthenticationException) {
view?.showError(e.message)
}
view?.hideProgress()
}
The key point is just to think of coroutines the same as you would "normal" sequential code.