Перейти к основному содержанию
`Ctrl`+`K`

# Состояние

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