Как обновить UI в сопрограммах в Kotlin 1.3

Я пытаюсь вызвать API, и когда мои переменные будут готовы, обновите компоненты пользовательского интерфейса соответственно.

Это мой сетевой синглтон, который запускает сопрограмму:

object MapNetwork {
    fun getRoute(request: RoutesRequest,
                 success: ((response: RoutesResponse) -> Unit)?,
                 fail: ((throwable: Throwable) -> Unit)? = null) {
        val call = ApiClient.getInterface().getRoute(request.getURL())

        GlobalScope.launch(Dispatchers.Default, CoroutineStart.DEFAULT, null, {

            try {
                success?.invoke(call.await())
            } catch (t: Throwable) {
                fail?.invoke(t)
            }

        })
    }
}

И так я это называю:

network.getRoute(request,
            success = {
                // Make Some UI updates
            },
            fail = {
                // handle the exception
            }) 

И я получаю исключение, в котором говорится, что нельзя обновить пользовательский интерфейс из любого потока, кроме потока пользовательского интерфейса:

com.google.maps.api.android.lib6.common.apiexception.c: Not on the main thread

Я уже пробовал это решение, но resume в Continuation<T> классе" устарело ", начиная с Kotlin 1.3


person Mohsen    schedule 31.10.2018    source источник
comment
resume не устарел, это просто развлечение.   -  person Marko Topolnik    schedule 05.11.2018


Ответы (3)


Чтобы ответить на ваш непосредственный вопрос, вы должны просто запустить сопрограмму в правильном контексте:

val call = ApiClient.getInterface().getRoute(request.getURL())
GlobalScope.launch(Dispatchers.Main) {
    try {
        success?.invoke(call.await())
    } catch (t: Throwable) {
        fail?.invoke(t)
    }
}

Однако это будет лишь верхушкой айсберга, потому что ваш подход - неправильный способ использования сопрограмм. Их ключевое преимущество - избегать обратных вызовов, но вы их повторно вводите. Вы также нарушаете структурированный параллелизм передовой практикой с использованием GlobalScope, не предназначенного для производственного использования.

По-видимому, у вас уже есть асинхронный API, который дает вам Deferred<RoutesResponse>, который вы можете await использовать. Способ его использования следующий:

scope.launch {
    val resp = ApiClient.getInterface().getRoute(request.getURL()).await()
    updateGui(resp)
}

Вы можете быть огорчены тем фактом, что я предлагаю иметь блок launch в каждом обратном вызове графического интерфейса, где вы должны выполнять приостанавливаемый код, но на самом деле это рекомендуемый способ использования этой функции. Это строго параллельно с написанием Thread { ... my code ... }.start(), потому что содержимое вашего launch блока будет выполняться одновременно с кодом за его пределами.

Приведенный выше синтаксис предполагает, что у вас есть готовая переменная scope, которая реализует CoroutineScope. Например, это может быть ваш Activity:

class MyActivity : AppCompatActivity(), CoroutineScope by MainScope {

    override fun onDestroy() {
        super.onDestroy()
        cancel()
    }
}

Делегат MainScope устанавливает диспетчер сопрограмм по умолчанию на Dispatchers.Main. Это позволяет использовать простой синтаксис launch { ... }.

person Marko Topolnik    schedule 05.11.2018
comment
the GlobalScope which is not meant for production use. Я почти уверен, что нет ничего плохого в использовании GlobalScope, если вы не собираетесь отменять свою работу. - person EpicPandaForce; 05.11.2018
comment
Хотя есть случаи использования, когда вы хотите, чтобы сопрограммы работали в фоновом режиме, использование GlobalScope открывает вам утечки сопрограмм, если что-то в них застревает, а также странные проблемы с параллелизмом, когда один задерживается, а затем запускается другой, выполняющий то же самое. . Например, если операция представляет собой запись в БД, записи могут быть переупорядочены, что приведет к потере обновлений. Думаю, это еще не полностью решенная проблема. - person Marko Topolnik; 16.12.2019
comment
Похоже, я мог бы захотеть запустить эти записи в диспетчере, созданном из однопоточного исполнителя. - person EpicPandaForce; 16.12.2019
comment
Но записи можно приостанавливать, они чередуются. - person Marko Topolnik; 16.12.2019
comment
Что ж, это ваш собственный выбор, если вы сделаете его приостанавливаемым для каждого элемента или нет. Я думаю, что тот факт, что Room сделал свои методы DAO вставки suspend fun-совместимыми, был ошибкой, поскольку они должны быть строго синхронными в фоновом потоке. - person EpicPandaForce; 16.12.2019
comment
Для меня это звучит неуклюже. Правильный способ получить упорядоченные операции - это исполнитель или аналогичный объект, обрабатывающий запросы по одному без необходимости даже в одном фоновом потоке. Что снова избавляет от необходимости писать GlobalScope.launch где угодно, вы просто отправляете задачу актеру и двигаетесь дальше. - person Marko Topolnik; 16.12.2019
comment
Ни одной фоновой ветки? Вот что делают диспетчеры: отправляют в другие потоки. Я был бы удивлен, если бы актеры волшебным образом сделали так, чтобы пользовательский интерфейс не блокировался, когда вы выполняете сетевой запрос, а это занимает 5 секунд. - person EpicPandaForce; 16.12.2019
comment
Похоже, мы говорим мимо друг друга ... вы говорите о блокировке ввода-вывода, а я говорю о приостанавливаемом вводе-выводе. Для блокировки ввода-вывода вам вообще не нужны сопрограммы. Просто используйте старый добрый ExecutorService. - person Marko Topolnik; 16.12.2019

Если вы используете coroutines-android, вы можете использовать Dispatchers.Main
(зависимость gradle implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.0")

network.getRoute(request,
        success = {
            withContext(Dispatchers.Main) {
                // update UI here
            }
        },
        fail = {
            // handle the exception
        }) 
person Mikhail Olshanski    schedule 31.10.2018
comment
В Kotlin 1.3 нет такой вещи, как Dispatchers.Main - person Mohsen; 31.10.2018
comment
@Mohsen Действительно есть Dispatchers.Main, см. здесь. Убедитесь, что вы включили зависимость Android выше, чтобы получить ее реализацию. - person jguerinet; 31.10.2018
comment
Спасибо @jguerinet за разъяснения. Есть ли разница между этими двумя методами? с использованием withContext и runOnUiThread - person Mohsen; 31.10.2018
comment
runOnUiThread запускает код в следующем цикле событий и больше не является частью сопрограммы. - person EpicPandaForce; 31.10.2018
comment
Чтобы расширить ответ @EpicPandaForce, runOnUiThread - это метод, предоставляемый Android, а не сопрограммами. Поэтому он полностью отделен. Если вы собираетесь использовать сопрограммы, вам, вероятно, следует использовать их везде (и, следовательно, использовать withContext). runOnUiThread() в этом случае менее эффективен. - person jguerinet; 31.10.2018
comment
Вот Medium статья, объясняющая, что на самом деле делает runOnUiThread(). - person jguerinet; 31.10.2018
comment
Это просто патч на фоне неправильного подхода. OP запускает сопрограмму в Dispatchers.Default, и вы предлагаете немедленно переключиться обратно на Dispatchers.Main. Почему бы не порекомендовать для начала запускать в правильном контексте? - person Marko Topolnik; 05.11.2018
comment
@MarkoTopolnik, вы правы, я был неосторожен и не уделил достаточно внимания, чтобы дать лучший ответ. Я думаю, OP должен принять ваш ответ вместо моего. - person Mikhail Olshanski; 05.11.2018

person    schedule
comment
Мне было интересно, как загрузить некоторые фоновые данные и обновить представление сразу после загрузки данных с помощью сопрограмм. Это действительно сработало для меня. Спасибо! - person Carlos Pérez Iruela; 15.11.2020