问题
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)
回答1:
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.
回答2:
You can first convert
ApiService.java
toApiService.kt
in Kotlin:interface ApiService { @GET("…") fun getData ( … : Call<Object>) }
To change the return type of your service methods from
Call
toDeferred
, you can modify the above line to:fun getData ( … : Deferred<Object>)
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()
inoverride fun onCreate(savedInstanceState: Bundle?){
inMainActivity.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
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 forPrecomputedText
that returns a future to be used withAppCompatTextView.setTextFuture(Future)
.
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
- For your
initializeApiTwo()
&initializeApiThree()
, you can useprivate suspend fun
for them, using the similarGlobalScope.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.
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.
Suggested Approach for handling
PrecomputedTextCompat.getTextFuture
:if (true) { (apiOneTextView as AppCompatTextView).setTextFuture( PrecomputedTextCompat.getTextFuture( apiOneTextView.text, TextViewCompat.getTextMetricsParams(apiOneTextView), null) ) }
Hope this is helpful.
来源:https://stackoverflow.com/questions/55626871/convert-callback-hell-to-deferred-object