【问题标题】:Convert callback hell to deferred object将回调地狱转换为延迟对象
【发布时间】:2019-09-01 18:10:50
【问题描述】:

背景:所以,我有一个相当大的项目,其中包含很多 API 函数。我正在考虑完全转向协程,但由于它们被实现为Callback 而不是Deferred,我无法有效地使用它们。例如:我想做apiCallOne()apiCallTwo()apiCallThree()异步并调用.await()等到最后一个请求完成后再更改UI。

现在项目的结构如下:

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

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

然后我有一个ClientBase.java: 函数createRequest() 是解析改造响应的主要函数。

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 对象? (我愿意为此创建另一个包装类)

【问题讨论】:

    标签: android kotlin coroutine kotlin-coroutines


    【解决方案1】:

    一般来说,一个简单的方法是从一个挂起函数返回一个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 依赖项,它会为本机添加此支持。

    【讨论】:

      【解决方案2】:
      1. 您可以先在 Kotlin 中将 ApiService.java 转换为 ApiService.kt

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

        要将服务方法的返回类型从Call 更改为Deferred,您可以将上面的行修改为:

        fun getData ( … : Deferred<Object>)
        

      1. 要在 Kotlin 中设置解析改造响应的请求,您可以在 Kotlin 中将其减少到几行。

        在你的onCreate()override fun onCreate(savedInstanceState: Bundle?){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. 再次,在您的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 = 暂停等待结果,不阻塞主 UI 线程


      1. 对于您的initializeApiTwo()initializeApiThree(),您可以为它们使用private suspend fun,使用类似的GlobalScope.launch(Dispatchers.Main){...和val createRequestTwo = initializeApiTwo(),其中:private suspend fun initializeApiTwo() = withContext(Dispatchers.Default) { // 协程范围,& 遵循相同讨论点 2 中概述的方法。

      1. 当我使用上述方法时,我的实现耗时1863ms

        要进一步简化此方法(从顺序到并发),您可以添加以下黄色修改,以使用 Async 移动到 Concurrent(与讨论点相同的代码4.),在我的例子中,它提供了 50% 的时间改进并将持续时间缩短到 901ms

        根据 Kotlin 文档,Async 返回一个 Deferred - 一个轻量级的非阻塞未来表示稍后提供结果的承诺。您可以在延迟值上使用 .await() 以获得其最终结果。

        在你的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 = 异步 { initialiseApiTwo() }

        val apiThreeAsync = 异步 { initialiseApiThree() }

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

        val dataResponse = createRequest.await()

        apiOneTextView.text=”您在此处使用 ${dataResponse.await().your_params_here} 实现的 api 详细信息”

         } 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 
        
        }
        

        要在本节中了解有关编写挂起函数的更多信息,您可以访问由 Kotlin 的文档 here 提供的关于Concurrent using Async 的本节。


      1. 处理PrecomputedTextCompat.getTextFuture的建议方法:

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

      希望这有帮助。

      【讨论】:

        猜你喜欢
        • 2016-08-30
        • 2017-02-01
        • 1970-01-01
        • 1970-01-01
        • 2014-08-27
        • 2015-12-21
        • 2014-06-21
        • 2015-10-12
        • 2016-09-19
        相关资源
        最近更新 更多