Callback and Co-routine on AWS SDK - android

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

Related

How to convert custom callback to coroutine

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*/ }
}

Coroutine and Callback handler in kotlin

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.

How can I wait the withContext in suspend function completes?

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)
}
})
}
}

Kotlin: how to pass a function with varying arguments as a parameter to other function

So, I'm rewriting my app's code te be "clean" (separation of layers, following MVVM pattern recommended by Android Team)
Here I've got my simple Retrofit interface to communicate with my API
interface Api {
#GET("comments")
suspend fun getPlaceComments(#Query("placeId") placeId: String): Response<List<CommentResponse>>
#POST("comments")
suspend fun addPlaceComment(#Header("placeId") placeId: String, #Header("text") text: String): Response<Unit>
#DELETE("comments")
suspend fun deletePlaceComment(#Header("placeId") placeId: String): Response<Unit>
}
Just a simple CRUD.
Now, one layer up, I've got my SocialRepository. To avoid code repetition, I created generic method callSafely that takes a suspending API function and a placeId as its parameters.
class SocialRepository {
private val client: Api = ApiClient.webservice
private suspend fun <T> callSafely(
apiMethod: suspend (placeId: String) -> Response<T>,
placeId: String,
): T? {
Log.d(TAG, "$apiMethod called safely")
var response: Response<T>? = null
try {
response = apiMethod(placeId)
} catch (e: Exception) {
e.printStackTrace()
}
if (response?.isSuccessful != true) {
Log.w(TAG, "response.isSuccessful isn't true.")
}
return response?.body()
}
suspend fun getPlaceComments(placeId: String): List<CommentResponse>? {
return callSafely(client::getPlaceComments, placeId)
}
suspend fun deletePlaceComment(placeId: String): Unit? {
return callSafely(client::deletePlaceComment, placeId)
}
suspend fun addPlaceComment(placeId: String, text: String): Unit? {
return callSafely(client::addPlaceComment, placeId, text) // HERE LIES THE PROBLEM
// I can't pass additional data because the method signature won't match with what's defined in callSafely()
}
}
Now, it works pretty well, of course I've got also my Activity and its ViewModel and the ViewModel calls a method in the repository etc. It doesn't matter.
What matters is that adding a place comment requires additional data, like, the actual text of the comment. Getting and deleting comments require only placeId, whereas when adding a comment, its content, its text is also required.
I've read that passing vararg functions is impossible in Kotlin. I also wouldn't like to clutter all the API methods with something like a List of params that will most of the time be empty and will just create confusion.
I can go the easy way and just copy the code of callSafely to addPlaceComment and alter it, but that's not what I'm looking for. I know how to solve the problem, but I don't know how to do it the clean way. In the future I might add some more endpoints requiring additional data (except placeId) and the problem will show up again.
What would you do in this situation? How to write it "the correct way"?
I'm not even sure how to properly express what I'm looking for, that's why this post's so rambling. Sorry for that in advance. I really hope you can help me.
The "clean way" is very broad concept. Everything depends on your needs and there is no "The one good way of doing things".
In your particular case you have several options:
1) Typealiases
typealias ApiCall1<P, R> = suspend (P) -> Response<R>
typealias ApiCall2<P1, P2, R> = suspend (P1, P2) -> Response<R>
fun <P> callSafely(param: P, call: ApiCall1<P, YourResult>): YourResult
fun <P1, P2> callSafely(param1: P1, param2: P2, call: ApiCall2<P1, P2, YourResult>): YourResult
2) Varargs
fun callSafely(vararg params: String, call: suspend (arr: Array<String>) -> YourResult {
...
call(*params)
...
}
3) Lambdas (preferable for your situation)
No-one is forcing you to use method references. Use lambdas when you need it. But place the lambda as the last parameter for "cleaner" code.
private suspend fun <T> callSafely(
placeId: String,
apiMethod: suspend (placeId: String) -> Response<T>
): T?
suspend fun getPlaceComments(placeId: String): List<CommentResponse>? {
return callSafely(placeId, client::getPlaceComments)
}
suspend fun deletePlaceComment(placeId: String): Unit? {
return callSafely(placeId, client::deletePlaceComment)
}
suspend fun addPlaceComment(placeId: String, text: String): Unit? {
return callSafely(placeId) { id -> client.addPlaceComment(id, text) }
}
Try this:
class SocialRepository {
private val client: Api = ApiClient.webservice
private suspend fun <T> callSafely(
apiMethod: suspend (placeId: String) -> Response<T>,
vararg stringParams: String,
): T? {
Log.d(TAG, "$apiMethod called safely")
var response: Response<T>? = null
try {
response = apiMethod(stringParams[0])
} catch (e: Exception) {
e.printStackTrace()
}
if (response?.isSuccessful != true) {
Log.w(TAG, "response.isSuccessful isn't true.")
}
return response?.body()
}
suspend fun getPlaceComments(placeId: String): List<CommentResponse>? {
return callSafely(apiMethod= client::getPlaceComments, stringParams=*arrayOf(placeId))
}
suspend fun deletePlaceComment(placeId: String): Unit? {
return callSafely(apiMethod=client::deletePlaceComment, stringParams=*arrayOf(placeId))
}
suspend fun addPlaceComment(placeId: String, text: String): Unit? {
return callSafely(apiMethod = client::addPlaceComment,stringParams= *arrayOf(placeId,text))
}
}

How to return error responses from Coroutines

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.

Categories

Resources