Чистая архитектура UseCase с прогрессом [Нет RxJava] - PullRequest
1 голос
/ 05 ноября 2019

У меня проблема с попыткой выяснить лучший способ реализации UseCase с прогрессом. Я видел такие примеры:

Приложение Google I / O для Android https://github.com/google/iosched/blob/master/shared/src/main/java/com/google/samples/apps/iosched/shared/domain/UseCase.kt

Что мне не нравится, так это:

  • Выполучить, чтобы получить результат в пользовательском интерфейсе и принимать там много решений.
  • Нет прогресса

И что-то, что я вижу в качестве обновления до вышеупомянутогоэто:

Чертежи архитектуры Android

https://github.com/android/architecture-samples/blob/usecases/app/src/main/java/com/example/android/architecture/blueprints/todoapp/domain/ActivateTasksUseCase.kt

Они используют сопрограммы, и теперь результат находится во ViewModel, и он намного лучше. Но опять же: Нет прогресса

Моя проблема здесь в том, что все используют RxJava, потому что все остальные используют его. Я вижу, что многие люди думают, что это то, что "выполняет задачи в фоновом режиме". Но это слишком много для меня. Мне это не нужно

Я видел несколько примеров с каналами Coroutines, но они действительно ужасны.

Так недавно я наткнулся на эту статью Романа Елизарова:

https://medium.com/@elizarov/callbacks-and-kotlin-flows-2b53aa2525cf

Я сделал что-то вроде этого:

class ProduceValueWithProgressUseCase @Inject constructor(
    private val executor: Executor
) {

    operator fun invoke(): Flow<Result<Int, Int>> {
        return callbackFlow {
            val callback = object : CallbackProgress {
                override fun onProgress(result: Int) {
                    offer(Result.Loading(result))
                }

                override fun onSuccess(result: Int) {
                    offer(Result.Success(result))
                    close()
                }

                override fun onError(e: Exception) {
                    offer(Result.Error(e))
                }

            }

            val producer = ValueWithProgressProducer(callback)
            executor.execute(producer)
            awaitClose {}
        }
    }

}

Идея состоит в том, что этот «производитель» использует обратный вызов для распространения данных, как большинство «старых» API. Поэтому я хочу распространить код через поток в ViewModel и не помещать туда обратные вызовы. Вот так:

viewModelScope.launch {
            produceValueWithProgressUseCase().collect {
                when (it) {
                    is Success -> view.showWithProgressResult(it.data)
                    is Loading -> view.updateProgress(it.progress)
                    else -> view.showError()
                }
            }
        }

Так что да, в основном API-интерфейс Flows сделает всю работу за меня. Я создал даже небольшое приложение для тестирования, где я просто генерирую числа, и оно работало нормально. Что мне не понравилось в этом, так это:

  • Слишком много аннотаций ExperimentalCoroutinesApi. Например, здесь (извините за форматирование):

    @ Suppress ("NOTHING_TO_INLINE") @ExperimentalCoroutinesApi public inline fun callbackFlow (@BuilderInference noinline block: suspend ProducerScope. () -> Unit): Flow = channelFlow (block))

Приведенный выше код является частью файла: kotlinx.coroutines.flow.Builders.kt из версии: kotlinx-coroutines-core-1.3.2

  • В какой-то момент я кое-что ударил аннотацией @Preview. (Если честно, не помню, где. Это было то, что я удалил.)
  • Я тоже немного попробовал посмотреть, кактестирование будет идти, но это не так просто. То же самое можно увидеть в коде от Blueprinst.
  • Я также смешиваю код, который выполняет задачу, и саму задачу. Я имею в виду использование callbackFlow ().

Итак, в конце я вижу нечто, похожее на призыв к переменам в следующем году. Поэтому, пожалуйста, выскажите свои мысли по этому поводу.

1 Ответ

0 голосов
/ 05 ноября 2019

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

Текущая реализация основана на стабильной версии для Kotlin Coroutines 1.3.2 .

Интерфейс для обратного вызова

interface CallbackProgress {
    suspend fun onProgress(result: Int)
    suspend fun onSuccess(result: Int)
    suspend fun onError(exception: Exception)
}

Производитель Итерация и выполнение некоторых методов обратного вызова,Пытаясь подражать вашему.

class Producer(private val callback: CallbackProgress) {
    suspend fun execute(fail: Boolean) {
        (0 until 10).forEach {
            when {
                it < 9 -> callback.onProgress(it)
                fail -> callback.onError(InterruptedException("blabla"))
                else -> callback.onSuccess(it)
            }
            delay(500)
        }
    }
}

Состояние

sealed class State {
    class Success(val value: Int) : State()
    class Loading(val progress: Int) : State()
    class Error(val exception: Exception) : State()
}

Interactor Вы должны быть осторожны, так как выбросы должныВыполните в той же подпрограмме, в противном случае вам нужно будет использовать нестабильный API, такой как channelFlow .

class UseCase {
    operator fun invoke(fail: Boolean) = flow {
        val callback = object : CallbackProgress {
            override suspend fun onSuccess(result: Int) {
                withContext(coroutineContext) { emit(State.Success(result)) }
            }

            override suspend fun onError(exception: Exception) {
                withContext(coroutineContext) { emit(State.Error(exception)) }
            }

            override suspend fun onProgress(result: Int) {
                withContext(coroutineContext) { emit(State.Loading(result)) }
            }
        }

        Producer(callback).execute(fail)
    }
}

Чтобы проверить вышесказанное, я написал следующее, которое демонстрирует как отказы, так и выбросы без сбоев.

fun main() = runBlocking {
    val useCase = UseCase()
    useCase(true).collect {
        when (it) {
            is State.Loading -> println("State for failure [Loading -> ${it.progress}]")
            is State.Success -> println("State for failure [Success -> ${it.value}]")
            is State.Error -> println("State for failure [Error -> ${it.exception.message}]")
        }
    }
    useCase(false).collect {
        when (it) {
            is State.Loading -> println("State  without failure [Loading -> ${it.progress}]")
            is State.Success -> println("State without failure [Success -> ${it.value}]")
            is State.Error -> println("State without failure [Error -> ${it.exception.message}]")
        }
    }
}

Выход

State for failure [Loading -> 1]
State for failure [Loading -> 2]
State for failure [Loading -> 3]
State for failure [Loading -> 4]
State for failure [Loading -> 5]
State for failure [Loading -> 6]
State for failure [Loading -> 7]
State for failure [Loading -> 8]
State for failure [Error -> blabla]

-------------------------------------

State  without failure [Loading -> 0]
State  without failure [Loading -> 1]
State  without failure [Loading -> 2]
State  without failure [Loading -> 3]
State  without failure [Loading -> 4]
State  without failure [Loading -> 5]
State  without failure [Loading -> 6]
State  without failure [Loading -> 7]
State  without failure [Loading -> 8]
State without failure [Success -> 9]
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...