How can I return a result from inner anonymous class before the function returns the result?

415 Views Asked by At

I'm using Microsoft's msal library to have users sign in to their account. I'm using MVVM so I need to the function to return a Resource<String> but the function is returning a value before the msal callbacks are being invoked and updating the value I need to return. I've tried:

  1. Using CoroutineScope(IO).launch {} and running the sign-in function in there but that does not return asynchronously.
  2. Using CoroutineScope(IO).async {} and then return@async Resource inside the callback. I'm compiler complains since the callback is void type.
  3. Using Kotlin FLow<Resource> and emit value from inside the callback.

Can anyone help me out with any ideas of how to get this to run asynchronously or return a value from within the callbacks? Thanks!

suspend fun microsoftSignIn(activity: Activity): Resource<String> {
    var resource: Resource<String> = Resource.Error(Throwable(""))
    CoroutineScope(Dispatchers.IO).async {
            PublicClientApplication.createSingleAccountPublicClientApplication(context, R.raw.auth_config_single_account, object : IPublicClientApplication.ISingleAccountApplicationCreatedListener {
                override fun onCreated(application: ISingleAccountPublicClientApplication?) {
                    application?.signIn((activity as AllActivityBase), null, Array(1) {"api://ScopeBlahBlah"}, object : AuthenticationCallback {
                        override fun onSuccess(authenticationResult: IAuthenticationResult?) {
                            Log.i("TOKEN_MSAL ${authenticationResult?.accessToken}" )
                            authenticationResult?.accessToken?.let { storeToken(it) }
                            ///// I need this to run first before "return resource" runs!!! /////
                            resource = Resource.Success("Success")
                        }

                        override fun onCancel() {
                            resource = Resource.Error(Throwable(""))
                        }

                        override fun onError(exception: MsalException?) {=
                            resource = Resource.Error(Throwable(""))
                        }

                    })
                }

                override fun onError(exception: MsalException?) {
                    Log.i("TOKEN_MSAL_A EX ${exception?.message}")
                    resource = Resource.Error(Throwable(""))
                }

            })
    }
    return resource
}
2

There are 2 best solutions below

3
broot On

If you need to consume a traditional asynchronous API and make it suspendable, the easiest is to use either suspendCoroutine() or CompletableDeferred. I can't easily reuse your code, so I will provide you just a generic solution, but it should be easy for you to adjust it to your needs.

suspendCoroutine():

suspend fun test1(): String {
    return withContext(Dispatchers.IO) {
        suspendCoroutine { cont ->
            doSomethingAsync(object : Callback {
                override fun onSuccess() {
                    cont.resume("success")
                }

                override fun onError() {
                    cont.resume("error")
                }
            })
        }
    }
}

CompletableDeferred:

suspend fun test2(): String {
    val result = CompletableDeferred<String>()
    withContext(Dispatchers.IO) {
        doSomethingAsync(object : Callback {
            override fun onSuccess() {
                result.complete("success")
            }

            override fun onError() {
                result.complete("error")
            }
        })
    }
    return result.await()
}

As you can see, both solutions are pretty similar. I'm not sure if there are any big differences between them. I guess not, so use whichever you like more.

Also, in both cases you can throw an exception instead of returning an error. You can use resumeWithException() for the first example and completeExceptionally() for the second.

One final note: you used Dispatchers.IO in your example, so I did the same, but I doubt it is needed here. Asynchronous operations don't block the thread by definition, so it should be ok to run them from any dispatcher/thread.

1
Tenfour04 On

To convert a callback-based function into something you can use properly in coroutines, you need to use suspendCoroutine or suspendCancellableCoroutine. Assuming your code above is correct (I'm not familiar with msal), it should look something like this. You have two callbacks to convert. These are top-level utility functions that you can put anywhere in your project:

suspend fun createSingleAccountPublicClientApplication(
    context, 
    @RawRes authConfig: Int
): ISingleAccountPublicClientApplication = suspendCoroutine { continuation ->
    PublicClientApplication.createSingleAccountPublicClientApplication(context, authConfig, object : IPublicClientApplication.ISingleAccountApplicationCreatedListener {
        override fun onCreated(application: ISingleAccountPublicClientApplication) {
            continuation.resume(application)
        }

        override fun onError(exception: MsalException) {
            continuation.resumeWithException(exception)
        }
    })
}

suspend fun ISingleAccountPublicClientApplication.signInOrThrow(
    activity: AllActivityBase, // or whatever type this is in the API
    someOptionalProperty: Any?, // whatever this type is in the API
    vararg args: String
): IAuthenticationResult = suspendCancellableCoroutine { continuation ->
    signIn(activity, someOptionalProperty, args, object: : AuthenticationCallback {
        override fun onSuccess(authenticationResult: IAuthenticationResult) {
            continuation.resume(authenticationResult)
        }

        override fun onCancel() = continuation.cancel()

        override fun onError(exception: MsalException) {
            continuation.resumeWithException(exception)
        }
    })
}

Then you can use this function in any coroutine, and since it's a proper suspend function, you don't have to worry about trying to run it asynchronously or specifying Dispatchers.

Usage might look like this, based on what I think you were doing in your code:

suspend fun microsoftSignIn(activity: Activity): Resource<String>
    try {
        val application = createSingleAccountPublicClientApplication(context, R.raw.auth_config_single_account)
        val authenticationResult = application.signInOrThrow((activity as AllActivityBase), null, "api://ScopeBlahBlah")
        Log.i(TAG, "TOKEN_MSAL ${authenticationResult.accessToken}" )
        authenticationResult.accessToken?.let { storeToken(it) }
        return Resource.Success("Success")
    } catch (e: MsalException) {
        return Resource.Error(e)
    }
}