class API {
val nonBlockingStub: HealthcareAPIGrpc.HealthcareAPIStub //Generated Java GRPC Stub
suspend fun exampleRequest(params: Params) : ReturnedResult = suspendCancellableCoroutine { routine ->
val obs = object : StreamObserver<ReturnedResult> {
override fun onNext(value: ReturnedResult?) {
routine.resume(value!!)
}
override fun onError(t: Throwable?) {
routine.resumeWithException(t!!)
}
override fun onCompleted() {
}
}
nonBlockingStub.exampleRequest(params, obs)
}
}
So I'm working on an Kotlin android application, which has a grpc client generated in java. Recently I had to move all the API requests to use kotlin coroutines. For every request I have to copy-paste this exampleRequest function.
I am curious if it's possible to make a generic function which does this, takes some params and calls the underlying stub function
Ideally there should be a stub generator that generates the appropriate calls as suspend/Flow methods, but you can still abstract much of the conversion with a dedicated helper function:
fun <T> grpcFlow(
#BuilderInference block: suspend (StreamObserver<T>) -> Unit
): Flow<T> = callbackFlow {
// Use ClientCallStreamObserver to support cancellation
val observer = object : ClientCallStreamObserver<T>() {
override fun onNext(value: T) {
sendBlocking(value)
}
override fun onError(t: Throwable) {
cancel(CancellationException("GRPC Error", t))
}
override fun onCompleted() = channel.close()
}
block(observer)
awaitClose {
observer.cancel("flow cancellation", null)
}
}
Then your API simply becomes:
class API {
val nonBlockingStub: HealthcareAPIGrpc.HealthcareAPIStub
suspend fun exampleRequest(params: Params) = grpcFlow {
// #BuilderInference should ensure the correct type is used
nonBlockingStub.exampleRequest(params, it)
}.single() // Since we only expect a single response value.
// And for returns (stream ReturnedResult)
suspend fun exampleStreamingRequest(params: Params) = gcpcFlow {
nonBlockingStub.exampleStreamingRequest(params, it)
} // Flow<ReturnedResult>
}
Related
I'm new to kotlin coroutines. I've been trying to run multiple API calls in parallel and then when all the calls are done update my UI and dismiss the loader, but with no success. This is my code
private fun getScoreForType() {
val job = CoroutineScope(Dispatchers.IO).launch {
types.forEach { type ->
getScore(type)
}
}
runBlocking {
job.join()
// do some ui work
dismissLoader()
}
}
private fun getScore(type: String) {
val call = MyApi.getScores(type)
call.enqueue(object : Callback<Score> {
override fun onResponse(call: Call<Score>, response: Response<Score>) {
setScore(response)
}
override fun onFailure(call: Call<Score>, t: Throwable) {
}
})
}
I've also tried using async and awaitAll but couldn't make it work either. The loader is always dismissed before all the calls are done. Any help on how I could make this work would be much appreciated
Use Flow and collectData it will works as LiveData.
For example:
val myIntFlow = MutableStateFlow(-1)
Try something like;
in ViewModelMethods
private fun getScoreForType() {
It goes first:
CoroutineScope(Dispatchers.IO).launch {
types.forEach { type ->
getScore(type)
}
// it means to change value of flow
myIntFlow.value = 1
}
// Now collect data in fragment to change UI
}
// in fragment like:
CoroutineScope(Dispatchers.Main).launch {
// flow will be triggered, on every changed value
viewModel.myIntFlow.collect {
viewModel.methodFromViewModelToChangeUI()
dissmisloader()
myIntFlow.value = -1
}
}
// try the same here as you wish
private fun getScore(type: String) {
val call = MyApi.getScores(type)
call.enqueue(object : Callback<Score> {
override fun onResponse(call: Call<Score>, response: Response<Score>) {
setScore(response)
}
override fun onFailure(call: Call<Score>, t: Throwable) {
}
})
}
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'm tring to implement NetworkBoundResource class in my project and this is what i'm trying. Everything work correctly getting response, caching but when im emiting value inside flowBuilder then it crashes and showing this error.
error i'm getting:
Emission from another coroutine is detected.
Child of ProducerCoroutine{Active}#df26eb9, expected child of FlowCoroutine{Active}#a0bb2fe.
FlowCollector is not thread-safe and concurrent emissions are prohibited.
To mitigate this restriction please use 'channelFlow' builder instead of 'flow')' has been detected.
Emissions from 'catch' blocks are prohibited in order to avoid unspecified behaviour, 'Flow.catch' operator can be used instead.
For a more detailed explanation, please refer to Flow documentation.
NetworkBoundResource class:
abstract class NetworkBoundResource<ResultType, RequestType> {
fun invoke(): Flow<Resource<ResultType>> = flow {
val rawData = loadFromDb()
if (shouldFetch(rawData)) {
fetchDataFromServer()
.onStart { emit(Resource.loading(rawData)) } // emit() causing issue
.catch { emit(Resource.error(it, null)) } // emit() causing issue
.collectLatest { }
}
}
// Save API response result into the database
protected abstract suspend fun cacheInDb(items: RequestType)
// Need to fetch data from server or not.
protected abstract fun shouldFetch(data: ResultType?): Boolean
// Show cached data from the database.
protected abstract suspend fun loadFromDb(): ResultType
// Fetch the data from server.
protected abstract suspend fun fetchDataFromServer(): Flow<ApiResponse<List<Category>>>
// when the fetch fails.
protected open fun onFetchFailed() {}
}
Repository class:
fun getCategories(): Flow<Resource<List<Category>>> {
return object : NetworkBoundResource<List<Category>, List<Category>>() {
override suspend fun cacheInDb(items: List<Category>) {
withContext(Dispatchers.IO) { database.getCategories().insert(items) }
}
override fun shouldFetch(data: List<Category>?): Boolean {
return true
}
override suspend fun loadFromDb(): List<Category> {
return withContext(Dispatchers.IO) { database.getCategories().read() }
}
override suspend fun fetchDataFromServer(): Flow<ApiResponse<List<Category>>> {
return flow { emit(RetrofitModule.getCategories()) }
}
}.invoke()
}
myViewModelClass:
init {
viewModelScope.launch {
repository.getCategories().collectLatest {
if(it.data!=null){
_categories.value = it.data
Log.d("appDebug", " ViewModel : $it")
}
}
}
}
As the exception says, cold flows doesn't allow to emit() concurrently.
You have two options:
Replace flow { } with channelFlow { } and send values with send() (Probably easier in your case)
Make sure no emit() is called concurrently
When working with the MVVM pattern in Android developments, we create a repository class where we execute all the network requests. The problem is since retrofit's .enqueue() method is asynchronous, my method that calls .enqueue doesn't wait until the callback is obtained(which is pretty logical) and returns null.
One way to solve this problem is to pass MutableLiveData object to my repository method and set its value in the callback, but I don't want to observe all my ViewModel properties in my view(fragment).
What is the common way to solve this problem?
fun createRoute(newRoute: RouteToSend): String {
var responseMessage: String? = null
webService.createRoute(authToken!!, newRoute).enqueue(object: Callback<Message> {
override fun onFailure(call: Call<Message>, t: Throwable) {
Log.e(TAG, t.message!!)
}
override fun onResponse(call: Call<Message>, response: Response<Message>) {
response.body()?.let { responseMessage = it.message }
}
})
return responseMessage!!
}
Pass a callback as an argument, e.g.
createRoute(newRoute: RouteToSend, callback: CreateRouteListener)
with
interface CreateRouteListener {
fun onFailure()
fun onResponse(response: String)
}
and call the corresponding method when the async process finishes:
override fun onFailure(call: Call<Message>, t: Throwable) {
Log.e(TAG, t.message!!)
callback.onFailure()
}
override fun onResponse(call: Call<Message>, response: Response<Message>) {
response.body()?.let {
responseMessage = it.message
callback.onResponse(responseMessage)
}
}
Calling createRoute will then look like this:
createRoute(RouteToSend(), object: CreateRouteListener {
override fun onFailure() {
// handle failure
}
override fun onResponse(response: String) {
// handle response
}
}
Yes, using MutableLiveData is one way, on the other hand using callback mechanism is another and more suitable way.
If you want to use callbacks you can change your method like
fun createRoute(newRoute: RouteToSend, callback : (String?) -> Unit): String {
var responseMessage: String? = null
webService.createRoute(authToken!!, newRoute).enqueue(object: Callback<Message> {
override fun onFailure(call: Call<Message>, t: Throwable) {
Log.e(TAG, t.message!!)
callback(responseMessage)
}
override fun onResponse(call: Call<Message>, response: Response<Message>) {
response.body()?.let { responseMessage = it.message
callback(responseMessage)}
}
})
}
then you can call your createRoute method like this
createRoute(route_to_send_variable,
callback = {
it?.let {
// use the response of your createRoute function here
}
})
Android Studio 3.6
My custom callback interface:
interface RecoveryPasswordConfirmCodeCallback {
fun onSuccess()
fun onError(ex: Throwable?)
}
Use:
val result = TransportService.recoverPasswordConfirmCode(
confirmCode,
ex,
object : RecoveryPasswordConfirmCodeCallback {
override fun onSuccess() {
}
override fun onError(ex: Throwable?) {
if (ex is InvalidOtpException) {
toastMessage.value = SingleEvent(
getApplication<Application>().applicationContext.getString(
R.string.incorrect_confirm_code
)
)
} else {
toastMessage.value = SingleEvent(
getApplication<Application>().applicationContext.getString(
R.string.default_error_message
))
}
}
})
fun recoverPasswordConfirmCode(
confirmCode: String,
ex: NeedTfaException,
callBack: RecoveryPasswordConfirmCodeCallback
) {
//some code here
}
Nice. It's work fine. But... is it possible to replace my custom callback interface by Kotlin's coroutine. I don't want to create custom interface only for execute method recoverPasswordConfirmCode
You can convert recoverPasswordConfirmCode() to a suspend function and return the result in the form of a sealed class to indicate if it's an error or the valid response. Something like this:
// Generic response class
sealed class Response<out T>{
data class Error(val ex: Throwable) : Response<Nothing>()
data class Data<T>(val data: T) : Response<T>()
}
// in your TransportService class
suspend fun recoverPasswordConfirmCode(confirmCode, ex): Response<RecoverPasswordResponse>{
// Do your stuff here
// return Response.Data<RecoverPasswordResponse>(/* your data object here */)
}
Then call it like this and check the response type:
val result = TransportService.recoverPasswordConfirmCode(confirmCode, ex)
when(result){
is Response.Error -> // Do something
is Response.Data -> // Do something
}
Note that you will have to call the suspend function inside a coroutine context.
You don't need to create a custom interface. Consume your API like this:
suspend fun recoverPasswordConfirmCode(confirmCode: String): YourReturnType = suspendCancellableCoroutine { cont ->
try {
val result = //Do your blocking API calls here
if(result.code == confirmCode) //Check confirm code is correct
cont.resume(YourResult) //Return your result here
else
cont.resumeWithException(YourException) //Throw an exception otherwise
} catch (e: Exception) {
cont.resumeWithException(e)
}
}
Call recoverPasswordConfirmCode method inside a Coroutine Scope.