Используйте сопрограммы для обновления пользовательского интерфейса при выполнении сетевого вызова

Я пытаюсь показать счетчик при совершении сетевого вызова с использованием сопрограмм. Кажется, что пользовательский интерфейс не показывает состояние LOADING_ITEMS (счетчик) до тех пор, пока не будет возвращен вызов itemsFromRepo, затем в течение доли секунды отображается счетчик, а затем отображаются элементы. У меня сложилось впечатление, что, как и в сопрограмме, состояние будет установлено на LOADING_ITEMS, элементы будут очищены, сетевой вызов будет выполнен в фоновом режиме, пока счетчик отображается в пользовательском интерфейсе. Затем, когда сетевой вызов завершится, сопрограмма продолжит работу и установит элементы, затем установит состояние.

Это правильный способ использования сопрограмм? И масштаб, я думаю, это новое из экспериментальных сопрограмм, с которыми я играл несколько месяцев назад.

// ViewModel.kt
enum class State { LOADING_ITEMS, SELECTING_ITEM } 

var state = ObservableField<State>()   
var items = ObservableField<List<String>>()    

private fun loadItems() {
    state.set(State.LOADING_ITEMS)
    items.set(emptyList())
    GlobalScope.launch(Dispatchers.Main) {
        val itemsFromRepo = apiRepo.getItems() // a network call
        items.set(itemsFromRepo)
        state.set(State.SELECTING_ITEM)
    }
}


// Repo.kt
suspend fun getItems() = suspendCoroutine<List<String>> { cont ->
    FirebaseDatabase.getInstance().getReference("Items")
            .addListenerForSingleValueEvent(
            object : ValueEventListener {
                override fun onCancelled(error: DatabaseError?) {
                    cont.resume(listOf(error?.message ?: "Unknown error"))
                }

                override fun onDataChange(snap: DataSnapshot?) {
                    cont.resume(snap?.children?.map { it.key } ?: emptyList())
                }
            })
}

2 ответа

Решение

Рекомендуется использовать локальную область видимости для обработки сопрограмм:

class ViewModel : CoroutineScope {
    private var job: Job = Job()

    // To use Dispatchers.Main (CoroutineDispatcher - runs and schedules coroutines) in Android add
    // implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.1'
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job

    enum class State { LOADING_ITEMS, SELECTING_ITEM } 

    var state = ObservableField<State>()   
    var items = ObservableField<List<String>>()


    fun detachView() {
        job.cancel()
    }

    private fun loadItems() {
        state.set(State.LOADING_ITEMS)
        items.set(emptyList())
        launch {
            val itemsFromRepo = apiRepo.getItems()
            items.set(itemsFromRepo)
            state.set(State.SELECTING_ITEM)
        }
    }
}

А по поводу вашего вопроса:

Это правильный способ использования сопрограмм?

Да, это правильный путь. Если у вас есть сетевой вызов внутри suspend функция (это в вашем случае), то эта функция будет приостанавливать выполнение сопрограммы, пока вы не вызовете continuation.resume() или другие связанные методы для возобновления сопрограммы. И приостановка сопрограммы не будет блокировать main нить.

Замените метод loadItems() следующим:

private fun loadItems() {
    state.set(State.LOADING_ITEMS)
    items.set(emptyList())

    GlobalScope.launch(Dispatchers.Main) {
        val itemsFromRepo = async(Dispatchers.Default) {   apiRepo.getItems()  }
        items.set(itemsFromRepo.await())
        state.set(State.SELECTING_ITEM)
    }
}

Вы выполняете вызов API в главном потоке Android, а для фонового вызова вы должны использовать Dispatchers.Default.

Информацию о Диспетчерах смотрите по этой ссылке

Другие вопросы по тегам