Состояние#
State создаётся для представления и выполняет следующие функции:
Хранит данные для контроллера представления
Примечание
UI-слой только подписывается на Observable-источники, не знает о SQL и транзакциях.
Позволяет обращаться к контроллерам данных, использовать БД и сеть
Запускает тяжёлые задачи
Принимает решения о навигации
Внимание
Любые запросы к данным должны проходить через State, чтобы гарантировать сериализацию и единый откат.
Минимальный скелет#
class UsersState : SmStateAbst<UsersState>() {
/* ----------- Данные ----------- */
val usersRS = newRecordSet()
/* ----------- Контроллер ----------- */
override fun newVci(): SmStateVcp = UsersVci()
/* 1. Конструируем запросы и подписки */
override fun onInit() {
usersRS.onPopulate { q ->
q.query("SELECT * FROM Users ORDER BY sName")
}
}
/* 2. Первый вход в стек (UI ещё не создан) */
override fun afterEnter() {
dbs.getApi(UserApi::class).seed() // демо-данные
// выполняет onPopulate() каждый раз; если вызвать populate(), то считывание будет однократным
usersRS.refresh()
}
}
Жизненный цикл#
Этап |
Поток |
Назначение |
|---|---|---|
|
Server |
Вызывается один раз после создания; регистрируем SQL-запросы и подписки |
|
Server |
Сразу после |
|
Server |
Даёт возможность выполнить сквозной переход до создания UI; вернуть |
|
Server |
Каждый раз, когда State становится верхним; обновляем данные, запускаем |
|
Main |
UI построен; точка синхронизации в VCI (scroll, диалоги, реакция на данные) |
|
Main |
Пользователь покидает экран; сохраняем scroll, закрываем диалоги, забираем изменённые поля |
|
Server |
UI уже снят; транзакция ещё открыта — пишем изменения в БД, вызываем |
|
Server |
Ловим необработанные ошибки; можно вернуть альтернативный |
|
Server |
State окончательно удалён из стека; освобождаем ресурсы, отписываемся |
Контейнеры данных#
Список записей#
Предоставляет удобные способы работы как из главного, так и из серверного потоков.
Для серверного потока:
запрос данных из базы данных
Для главного потока:
получение данных
возможность безопасного редактирования с последующей передачей изменений в серверный поток
val ordersRS = newRecordSet()
ordersRS.onPopulate {
it.query("SELECT * FROM Orders WHERE gidCustomer = ?", arrayOf(custGid))
}
ordersRS.onUpdateRecord { changes ->
val newQty = changes.getNewValueAsInt("nQuantity")
dbs.execSql(
"UPDATE Orders SET nQuantity = ? WHERE id = ?",
newQty,
changes.id
)
}
Единственная строка#
Обёртка над списком записей для удобной работы с одной строкой.
val orderSR = newSingleRecord()
orderSR.onPopulate {
it.query("SELECT * FROM Orders WHERE gid = ?", arrayOf(orderGid))
}
Строка по значению#
Декоратор над набором строк, позволяющий преобразовывать data-класс в строку и обратно.
val editMode = newRecordValue(EditFlags(isReadonly = false))
Исполнители#
Используются для выделения конкретного потока в разрезе Activity для выполнения специализированных задач.
val camExec = newCameraExecutorSubscription()
sm.doLaunch("resize") {
camExec.executor.submit {
val path = imageUtil.resize(raw)
postSharedTask("link photo") { st ->
dbs.getApi(FileApi::class).attach(orderGid, path)
}
}
}
Исполнитель запускается, когда StateManager активен, и гасится при onStop() Activity.
Внимание
Не забывайте вызывать close() у ExecutorSubscription, если держите его дольше жизни State.
Сквозной переход#
Позволяет автоматически переходить на следующий экран без ожидания действий пользователя.
override fun onVisit(): SmTrans? =
if (dbs.getPkg<AuthPkg>().hasToken())
callStateTrans<HomeState>() // уже залогинен
else
callStateTrans<LoginState>() // требуется авторизация
Отмена длительных операций#
InterruptingLock.validate() вызывается в потенциально длинных циклах.
Если пользователь нажал «Отмена»:
выбрасывается
CancelTaskExceptionпроисходит
rollback()UI возвращается к последнему стабильному экрану.
Хорошие практики#
Один экран — один State
Не склеивайте разные сущности.SQL/REST — только из State
VCI должен оставаться чистым.Методы State делайте идемпотентными
Пользователь может нажать кнопку дважды.