Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Convert callback hell to deferred object

Background: So, I have a pretty big project with a lot of API functions. I'm thinking of completely moving to coroutines, but as they are implemented as Callback and not Deferred, I can not use them efficiently. For instance: I would like to do apiCallOne(), apiCallTwo() and apiCallThree() async and call .await() to wait till the last request is completed before changing UI.

Now the project is structured like this:

At the very bottom (or top) is ApiService.java:

interface ApiService {
    @GET("...")
    Call<Object> getData();
    ...
}

Then I have a ClientBase.java: function createRequest() is main function for parsing retrofit response.

void getUserName(String name, ApiCallback<ApiResponse<...>> callback) {
    createRequest(ApiService.getData(...), new ApiCallback<ApiResponse<?>>() {
        @Override
        public void onResult(ServiceResponse response) {
            callback.onResult(response);
        }
    });
}

private void createRequest(Call call, final ApiCallback<ApiResponse<?>> callback) {

    call.enqueue(new Callback() {
        @Override
        public void onResponse(Call call, retrofit2.Response response) {
                //heavy parsing
            }

            // return request results wrapped into ApiResponse object
            callback.onResult(new ApiResponse<>(...));
        }

        @Override
        public void onFailure(Call call, Throwable t) {
            // return request results wrapped into ApiResponse object
            callback.onResult(...);
        }
    });
}

ApiCallback and ApiResponse looks like this:

public interface ApiCallback<T> {
    void onResult(T response);
}

public class ApiResponse<T> {
    private T mResult;
    private ServiceError mError;
    ...
}

So, before all of this, I have also ApiClient.java which uses ClientBase.createRequest():

public void getUserName(String name, ApiCallback<ApiResponse<..>> callback) {
    ClientBase.getUserName(secret, username, new ServiceCallback<ServiceResponse<RegistrationInvite>>() {
        @Override
        public void onResult(ServiceResponse<RegistrationInvite> response) {
            ...
            callback.onResult(response);
        }
    });
}

As you can see, this is very, very bad. How can I transfer some of this code at least to make sure, that ApiClient.java function return Deferred objects? (I'm willing to create another wrapper class for this)

like image 266
MaaAn13 Avatar asked Apr 11 '19 07:04

MaaAn13


2 Answers

So in general, a simple way to do this is to return a suspendCancellableCoroutine from a suspending function, which you can then complete asynchronously. So in your case, you might write something like:

suspend fun getUserName(name: String): ApiResponse<...> {
    return suspendCancellableCoroutine { continuation ->
        createRequest(ApiService.getData(...), new ApiCallback<ApiResponse<...>>() {
            @Override
            public void onResult(ApiResponse<...> response) {
                continuation.resume(response)
            }
        });
    }
}

You basically return the equivalent of a SettableFuture and then mark it complete when you get success or failure. There's also continueWithException(Throwable) if you want to handle errors via exception handling.

That said:

Since you're using Retrofit, I would recommend just adding in the retrofit2-kotlin-coroutines-adapter dependency which adds in this support for you natively.

like image 103
Kevin Coppock Avatar answered Sep 30 '22 11:09

Kevin Coppock


  1. You can first convert ApiService.java to ApiService.kt in Kotlin:

    interface ApiService {
        @GET("…")
        fun getData ( … : Call<Object>)
    }
    

    To change the return type of your service methods from Call to Deferred, you can modify the above line to:

    fun getData ( … : Deferred<Object>)
    

  1. To set up the request for parsing the retrofit response in Kotlin, you can reduce it to a few lines in Kotlin.

    In your onCreate() in override fun onCreate(savedInstanceState: Bundle?){ in MainActivity.kt:

    val retrofit = Retrofit.Builder()
    // Below to add Retrofit 2 ‘s Kotlin Coroutine Adapter for Deferred
              .addCallAdapterFactory(CoroutineCallAdapterFactory()) 
              .baseUrl(“YOUR_URL”)
              .build()
    
    val service = retrofit.create(ApiService::class.java) 
    // Above using :: in Kotlin to create a class reference/ member reference
    
    val apiOneTextView = findViewById<TextView>(R.id.api_one_text_view)
    // to convert cast to findViewById with type parameters
    

  1. I don’t know the use case for your API, but if your API is going to return a long text chunk, you can also consider using a suggested approach at the bottom of this post.

    I included on an approach to pass the text computation to PrecomputedTextCompat.getTextFuture, which according to Android documentation, is a helper for PrecomputedText that returns a future to be used with AppCompatTextView.setTextFuture(Future).


  1. Again, inside your MainActivity.kt:

    // Set up a Coroutine Scope
    GlobalScope.launch(Dispatchers.Main){
    
    val time = measureTimeMillis{ 
    // important to always check that you are on the right track 
    
    try {
    
        initialiseApiTwo()
    
        initialiseApiThree()
    
        val createRequest = service.getData(your_params_here)
    
        apiOneTextView.text=”Your implementation here with api details using ${createRequest.await().your_params_here}”
    
     } catch (exception: IOException) {
    
           apiOneTextView.text=”Your network is not available.”
        }
      }
        println(“$time”) 
        // to log onto the console, the total time taken in milliseconds taken to execute 
    
    }
    

    Deferred + Await = to suspend to await result, does not block main UI thread


  1. For your initializeApiTwo() & initializeApiThree(), you can use private suspend fun for them, using the similar GlobalScope.launch(Dispatchers.Main){... & val createRequestTwo = initializeApiTwo(), where: private suspend fun initializeApiTwo() = withContext(Dispatchers.Default) { // coroutine scope, & following the same approach as outlined in discussion point 2.

  1. When I used the method outlined above, my implementation took 1863ms.

    To further streamline this method (from sequentially to concurrently), you can add the following modifications in yellow, to move to Concurrent using Async (same code from point discussion 4.), which in my case, gave a 50% time improvement & cut duration to 901ms.

    According to Kotlin documentation, Async returns a Deferred – a light-weight non-blocking future that represents a promise to provide a result later. You can use .await() on a deferred value to get its eventual result.

    Inside your MainActivity.kt:

    // Set up a Coroutine Scope
    GlobalScope.launch(Dispatchers.Main){
    
    val time = measureTimeMillis{ 
    // important to always check that you are on the right track 
    
    try {
    

    val apiTwoAsync = async { initialiseApiTwo() }

    val apiThreeAsync = async { initialiseApiThree() }

    val createRequest = async { service.getData(your_params_here) }

    val dataResponse = createRequest.await()

    apiOneTextView.text=”Your implementation here with api details using ${dataResponse.await().your_params_here}”

     } catch (exception: IOException) {
    
           apiOneTextView.text=”Your network is not available.”
        }
      }
        println(“$time”) 
        // to log onto the console, the total time taken in milliseconds taken to execute 
    
    }
    

    To find out more on composing suspending functions in this section, you can visit this section on Concurrent using Async provided by Kotlin's documentation here.


  1. Suggested Approach for handling PrecomputedTextCompat.getTextFuture:

    if (true) {
       (apiOneTextView as AppCompatTextView).setTextFuture(
          PrecomputedTextCompat.getTextFuture(
              apiOneTextView.text,
              TextViewCompat.getTextMetricsParams(apiOneTextView), null)
     )
    }
    

Hope this is helpful.

like image 30
B4eight Avatar answered Sep 30 '22 10:09

B4eight