Code Monkey home page Code Monkey logo

remo's Introduction

ReactiveModel

Класс ReactiveModel предоставляет контекст для асинхронного запуска задач (jobs) с возможностью наблюдения за процессом их выполнения: состоянием, успешными результатами, ошибками.

Нацелен на использование в декларативно-реактивной манере в процессе реализации логики доменного уровня.

Общее описание архитектурного подхода

Доменная логика реализуется с помощью выполнения набора задач (job) в асинхронном окружении (coroutines). Каждая job рассматривается как повторяемая (например fetchUsers) а также "наблюдаемая" — к примеру UI-слой хочет получать ошибки, результаты выполнения, изменения состояния активности job. В условиях декларативного подхода это реализуется с помощью "реактивности", то есть, при использовании kotlinx.coroutines, с помощью Flow.

Ниже для обзора приведён пример частичной реализации фичи. Далее будут разобраны используемые в примере функции и указаны более интересные/гибкие вариации их использования.

class UserListModel : ReactiveModel {
  val fetch = task { sortConfiguration: SortConfiguration ->
    userNetworkApi.fetch(sortConfiguration)
  }

  val uploadPhoto = task { photo ->
    userNetworkApi.uploadPhoto(photo)
  }

  fun deleteLocalPhoto(userId: UserId) {
    scope.launch {
      repository.deletePhotoFile(userId)
    }
  }
}

fun main(val model: UserListModel) {
  model.start()
  coroutineScope {
    launch {
      model.fetch.jobFlow.state.collect { println("state: $it") }
    }
    launch {
      model.fetch.jobFlow.errors().collect { println("error: $it") }
    }
    launch {
      model.fetch.jobFlow.results().collect { println("result: $it") }
    }
    launch {
      model.fetch.start(SortConfiguration.Ascending)
    }
  }
  model.dispose()
}

Работа с Task, WatchContext и JobFlow

В ReactiveModel используется "jobs", которые представлены suspend-функциями, а также WatchContext, которые предоставляют функционал запуска job-ов в специальном скоупе, который следит за процессом их выполнения, и рапортует подписчикам об изменении хода выполнения через WatchContext.state, WatchContext.successResults, WatchContext.errors. Каждое из этих свойств представляет собой kotlinx.coroutines.flow.Flow.

Класс WatchContext предназначен для использования внутри ReactiveModel, для наблюдателей из внешнего мира рекомендуется возвращать JobFlow, который "прячет" внутренности WatchContext и оставляет клиенту только необходимые свойства: state, results, errors.

Наконец, для удобства клиентов, есть структура Task, которая упаковывает вместе функцию старта job-а, а также WatchContext для наблюдения за ходом выполнения.

Рассмотрим самый простой пример, когда у нас есть одна самодостаточная операция.

class UserListModel : ReactiveModel {

  val fetch = task { sortConfiguration: SortConfiguration ->
    userNetworkApi.fetch(sortConfiguration)
  }

}

Здесь хелпер-функция task создаёт и конфигурирует объект Task, при вызове функции start которого, переданная suspend-лямбда будет запущена в рамках нового WatchContext и будет в нём наблюдаться.

К сведению: код выше равносилен такому

  val fetch = taskIn(WatchContext()) { sortConfiguration: SortConfiguration ->
    userNetworkApi.fetch(sortConfiguration)
  }

Использование одного WatchContext для разных job

В примере с UserListModel выше, каждому таску соответствовал один WatchContext. Но бывает логика посложнее.

Например, представим, что функционал отображения списка пользователей содержит операции

  • изначального получения пользователей
  • изменения сортировки
  • удаления пользователя

При этом все эти операции отсылают разные запросы на сервер, но требования таковы, что для клиента данной фичи это всё относится к одному "контексту": обновлению списка пользователей. Например, это выражается в показе одного и того же лоадера в UI.

В этом случае можно завести один "контекст" и запускать в нём все три job-а. Клиент будет получать уведомления о процессе работы любого из них через один объект JobFlow:

class UserListModel : ReactiveModel {
  // клиент использует данный объект для наблюдения
  // за состоянием выполнения job-ов в этом контексте
  val fetchJobFlow: JobFlow<Unit> = WatchContext()

  fun fetch() {
    fetchJobFlow.executeInModelScope {
      userNetworkApi.fetch()
    }
  }

  fun setSortConfiguration(configuration: SortConfiguration) {
    fetchJobFlow.executeInModelScope {
      userNetworkApi.fetchWithSortConfiguration(configuration)
    }
  }

  fun deleteUser() {
    fetchJobFlow.executeInModelScope {
      userNetworkApi.deleteUser()
    }
  }
}

Работа со state

Базовый вариант ReactiveModel не диктует никаких обязательств по работе с внутренним стейтом. Однако, как правило, его удобно реализовывать через MutableStateFlow, например:

class LoginModel : ReactiveModel {

  private data class State(val otpId: String? = null)
  private val stateFlow = MutableStateFlow(State())

  val login = task { ->
    val otpId = loginApi.login()
    stateFlow.update { state -> state.copy(otpId = otpId) }
  }

  val otpIdChanges: Flow<OtpId> = stateFlow.map { it.otpId }.filterNotNull()
}

Работа с CoroutineScope

Каждый наследник ReactiveModel может запускать корутины в scope модели:

class LoginModel : ReactiveModel {
  fun deleteAuthToken() {
    scope.launch { authRepository.deleteTokens() }
  }
}

Отмена задач

При работе с Task, его функция start возвращает объект Job, который можно использовать для отмены:

class LoginModel : ReactiveModel {
  val fetch = task {
    while (true) {
      delay(100)
      yield()
    }
  }
}

fun main() {
  val model = LoginModel()
  model.start()
  runBlocking {
    coroutineScope {
      val job = model.fetch.start()
      delay(1000)
      job.cancel()
      println("Cancelled")
    }
  }
}

Если модель внутри работает с WatchContext и не отдаёт наружу Task, то она может реализовать аналогичный механизм, через работу с Job, возвращаемую функцией executeInModelScope().

Подписка на состояния: немедленная и ленивая

Бывают ситуации, когда Task/WatchContext.execute() выполняются практически мгновенно, и нет возможности получить результат выполнения без указания results(replayLast = true). В этом случае можно передать функции Task.start()/WatchContext.execute() параметр scheduled, указав StartScheduled.Lazily и задав минимальное количество подписчиков, которые должны появится у WatchContext, прежде чем его задачи начнут выполняться, например:

model.fetchUsers.start(
  scheduled = StartScheduled.Lazily(minResultsSubscribers = 1, minStateSubscribers = 0)
)
delay(2000)
// this will correctly print results while with `StartScheduled.Eagerly` 
// they will be emitted early and won't be ever replayed
model.fetchUsers.results(replayLast = false).collect { println(it) }

Запуск задач друг за другом

Если нужно организовать последовательный запуск нескольких задач, можно сделать это просто вызывая start/execute, например:

  val fetchProfile = task { repository.fetch() }

  val login = task {
    val result = repository.login()
    fetchProfile.start(result)
  }

или, если используются WatchContext:

  val fetchProfileContext = WatchContext()

  val login = task {
    val result = repository.login()
    fetchProfileContext.executeInModelScope { fetchProfileInternal(result) }
  }

Постановка в очередь

Бывают ситуации, когда одну и ту же задачу в рамках одного WatchContext нужно ставить в очередь, если какая-то сейчас уже выполняется:

val updatePushPreferenceContext = WatchContext()

fun updatePushPreference(config: Config) {
  updatePushPreferenceContext.executeInModelScope { repository.update(config) }
}

// in UI

// due to fast user clicks
model.updatePushPreference(Config.Enabled)
model.updatePushPreference(Config.Disabled)
model.updatePushPreference(Config.Enabled)

TODO эта фича пока не реализована. Скорее всего, будет некий флаг enableQueueing при отсылке job-ы в контекст

Lifecycle

У каждой ReactiveModel есть жизненный цикл. Он начинается с вызова функции start, которая возвращает Job, вызвав cancel() у которого можно завершить работу модели. Либо, если был передан parentScope, можно вызывать cancel у него.

remo's People

Contributors

dimsuz avatar

Stargazers

 avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

Forkers

kode-android

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.