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

Convert callback hell to deferred object

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

现在项目结构如下:

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

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

然后我有一个ClientBase.java: function 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 函数 return Deferred 对象? (我愿意为此创建另一个包装器 class)

所以一般来说,一个简单的方法是从挂起函数 return 一个 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)
            }
        });
    }
}

基本上 return 等同于 SettableFuture,然后在成功或失败时将其标记为完成。如果你想通过异常处理来处理错误,还有continueWithException(Throwable)

也就是说:

由于您使用的是 Retrofit,我建议您只添加 retrofit2-kotlin-coroutines-adapter 依赖项,这会为您提供本机支持。

  1. 你可以先在Kotlin中将ApiService.java转换成ApiService.kt

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

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

    fun getData ( … : Deferred<Object>)
    

  1. 在Kotlin中设置解析retrofit响应的请求,在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要return一个长文本块,你也可以考虑使用此 post 底部的建议方法。

    我包含了一种将文本计算传递给 PrecomputedTextCompat.getTextFuture 的方法,根据 Android 文档,它是 PrecomputedText 的助手,return 是未来与 AppCompatTextView.setTextFuture(Future).

  2. 一起使用

  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 = 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. 当我使用上述方法时,我的实现花费了 1863ms

    为了进一步简化此方法(从顺序到并发),您可以添加以下黄色修改,以使用 Async[=103 移动到 Concurrent =](来自点讨论 4. 的相同代码),就我而言,它提供了 50% 的时间改进 并将持续时间缩短到 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.

    在你的 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 
    
    }
    

    要在本节中了解有关组合挂起函数的更多信息,您可以访问 Kotlin 文档 here.[=40= 提供的 使用 Async 并发的本节]


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

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

希望这对您有所帮助。