Преобразование ада обратного вызова в отложенный объект

Предыстория. Итак, у меня довольно большой проект с множеством функций API. Думаю полностью перейти на сопрограммы, но так как они реализованы как Callback а не Deferred, то не могу их эффективно использовать. Например: я хотел бы сделать apiCallOne(), apiCallTwo() и apiCallThree() асинхронным и вызвать .await(), чтобы дождаться завершения последнего запроса перед изменением пользовательского интерфейса.

Сейчас проект устроен так:

В самом низу (или вверху) находится 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(...);
        }
    });
}

ApiCallback и ApiResponse выглядят так:

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 объектов? (Я хочу создать для этого еще один класс-оболочку)


person MaartinAndroid    schedule 11.04.2019    source источник


Ответы (2)


В общем, простой способ сделать это — вернуть 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 зависимость, которая добавляет эту поддержку для вас изначально.

person Kevin Coppock    schedule 11.04.2019

  1. Сначала вы можете преобразовать 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. #P7#
    // 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 
    
    }
    
    <цитата> #P8#

  1. Для ваших initializeApiTwo() и initializeApiThree() вы можете использовать для них private suspend fun, используя аналогичные GlobalScope.launch(Dispatchers.Main){... и val createRequestTwo = initializeApiTwo(), где: private suspend fun initializeApiTwo() = withContext(Dispatchers.Default) { // область действия сопрограммы, и следуя тому же подходу, который описан в пункте обсуждения 2.

  1. #P9# #P10#
    #P11#
    #P12#
    // Set up a Coroutine Scope
    GlobalScope.launch(Dispatchers.Main){
    
    val time = measureTimeMillis{ 
    // important to always check that you are on the right track 
    
    try {
    
    #P13# #P14# #P15# #P16# #P17#
     } 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 
    
    }
    
    #P18#

  1. Предлагаемый подход к работе с PrecomputedTextCompat.getTextFuture:

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

Надеюсь, это полезно.

person B4eight    schedule 13.04.2019