将回调地狱转换为延迟对象

5
背景: 我有一个非常庞大的项目,其中包含很多API函数。我考虑完全转向协程,但由于它们是以"回调"而不是"延迟"的形式实现的,所以我无法有效地使用它们。例如:我想异步执行 apiCallOne()apiCallTwo()apiCallThree(),并调用 .await() 等待最后一个请求完成后再更改UI。

现在项目结构如下:

在最底部(或顶部)是 ApiService.java:

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

接下来,我有一个名为ClientBase.java的文件: 函数createRequest()是解析Retrofit响应的主要函数。

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(...);
        }
    });
}

ApiCallbackApiResponse看起来是这样的:

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

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

在此之前,我还有一个 ApiClient.java 文件,其中使用了 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);
        }
    });
}

正如你看到的那样,这非常非常糟糕。我该如何转移一些代码,至少确保ApiClient.java函数返回Deferred对象?(我愿意为此创建另一个封装类)

2个回答

6

一般来说,实现异步操作的一个简单方法是从挂起函数返回一个 suspendCancellableCoroutine,然后在异步完成时完成该操作。因此,在您的情况下,您可以编写类似以下方式的代码:

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

你基本上返回一个SettableFuture的等效项,然后在成功或失败时标记它为完成。如果你想通过异常处理来处理错误,也可以使用continueWithException(Throwable)

话虽如此:

既然你正在使用Retrofit,我建议只需添加retrofit2-kotlin-coroutines-adapter依赖项,它会为你本地添加此支持。


1
  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. 我不知道你的API用途是什么,但如果你的API要返回一个长文本块,你也可以考虑使用本帖底部建议的方法。

    我提供了一种将文本计算传递给PrecomputedTextCompat.getTextFuture的方法,根据Android文档,这是一个PrecomputedText的助手,返回一个未来与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. 对于你的 initializeApiTwo()initializeApiThree(),你可以为它们使用 private suspend fun,使用类似的 GlobalScope.launch(Dispatchers.Main){... & val createRequestTwo = initializeApiTwo() 的方法,其中: private suspend fun initializeApiTwo() = withContext(Dispatchers.Default) { // 协程范围,并按照讨论点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)
     )
    }
    
希望这有所帮助。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接