Состояние#

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()
    }
}

Жизненный цикл#

Этап

Поток

Назначение

onInit()

Server

Вызывается один раз после создания; регистрируем SQL-запросы и подписки

afterPush()

Server

Сразу после stateStack.push(); лёгкий хук, не запускать тяжёлые операции

onVisit()

Server

Даёт возможность выполнить сквозной переход до создания UI; вернуть SmTrans или null

afterEnter()

Server

Каждый раз, когда State становится верхним; обновляем данные, запускаем SharedTask

afterEnterGui()

Main

UI построен; точка синхронизации в VCI (scroll, диалоги, реакция на данные)

beforeExitGui()

Main

Пользователь покидает экран; сохраняем scroll, закрываем диалоги, забираем изменённые поля

beforeExit()

Server

UI уже снят; транзакция ещё открыта — пишем изменения в БД, вызываем flush() у RecordSet

onError(e)

Server

Ловим необработанные ошибки; можно вернуть альтернативный SmTrans

onClose()

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 делайте идемпотентными
    Пользователь может нажать кнопку дважды.