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

# Быстрый старт

Ниже — минимальный, но полностью рабочий пример приложения на GMF.
Мы пройдём по всем слоям: от Entity до Compose-экрана, подключим DI-генератор и объясним, куда поместить каждый файл.

## Шаблон проекта

### Зависимости в Gradle

    // build.gradle (модуль :app)
    plugins {
        id("com.android.application")
        kotlin("android")
        alias(libs.plugins.google.devtools.ksp)   // KSP для DI-процессора
    }

    android { /* стандартная конфигурация */ }

    dependencies {
        implementation(libs.kotlinx.coroutines.android)
        implementation(libs.androidx.compose.material3)
        ksp(libs.androidx.room.compiler)

        implementation(project(":common"))        // GMF
        ksp(project(":di-processor"))             // кодогенерация @GsApiBean / @GsPkgBean
    }

### Скелет каталогов

    src/main/java
    └─ ru.my.app
       ├─ api/                  – классы доступа к БД (UserApi и др.)
       ├─ db/                   – Entity, Dao, AppDatabase
       ├─ pkg/                  – сетевые / сервисные пакеты (опционально)
       ├─ ui/
       |   └─ users/
       |       ├─ UsersState.kt
       |       ├─ UsersVci.kt
       |       └─ view/
       |           ├─ UsersView.kt
       |           └─ ViewProvider.kt
       ├─ MainActivity.kt       – точка входа
       └─ Delegates.kt          – DataStore, расширения навигатора и т.д.

### ORM-классы

    // db/User.kt
    @Entity
    data class User(
        var name  : String? = null,
        var email : String? = null,
        var age   : Int?    = null,
    ) : BaseEntity() {
        override fun copyEntity() = copy()
    }

    // db/UserDao.kt
    @Dao
    abstract class UserDao : BaseDao<User>()

### Room-база

    // db/AppDatabase.kt

    @Database(
        entities = [SystemEntity::class, User::class],
        version = 1,
        exportSchema = false
    )
    abstract class AppDatabase : RoomDatabase() {
        abstract val userDao: UserDao

        companion object {
            @Volatile private var INSTANCE: AppDatabase? = null

            fun getInstance(ctx: Context): AppDatabase =
                INSTANCE ?: synchronized(this) {
                    INSTANCE ?: Room.databaseBuilder(
                        ctx,
                        AppDatabase::class.java,
                        "AppDB"
                    ).build().also { INSTANCE = it }
                }
        }
    }

Если нужна базовая реализация БД, можно использовать в `MainActivity`:

    override fun provideDatabase(): PrototypeDatabase =
        RoomDbSingleton.getInstance(this, "PrototypeDatabase")

и не создавать `companion object` в классе БД для хранения инстанса.

### DI-процессор

Аннотация `@GsApiBean` говорит KSP-процессору сгенерировать расширение
`GsSession.getUserApi()` — обращение к нашему API без строковых имён.

    // api/UserApi.kt
    @GsApiBean
    class UserApi(ctx: ApiBeanContext)
        : DbBaseApiGen<User, UserDao, AppDatabase>(ctx) {

        override val dao get() = dbRoom.userDao
        override fun newEntity() = User()

        /** Демо-данные при первом старте */
        fun seed() {
            if (fetchAll().isNotEmpty()) return
            listOf(
                "Alice" to "alice@site.com",
                "Bob"   to "bob@site.com",
                "Chloe" to "chloe@site.com",
            ).forEach { (n, e) ->
                insert().update {
                    it.name  = n
                    it.email = e
                    it.age   = (20..45).random()
                }
            }
            flush()
        }
    }

### Бизнес-логика экрана

    // ui/users/UsersState.kt
    class UsersState : SmStateAbst<UsersState>() {

        val usersRS = newRecordSet()          // RecordSet

        override fun onInit() {
            usersRS.onPopulate { q ->
                q.query("SELECT * FROM User ORDER BY name")
            }
        }

        override fun afterEnter() {
            dbs.getUserApi().seed()           // лениво создаётся через DI
            usersRS.refresh()
        }

        override fun newVci(): SmStateVcp = UsersVci()
    }

### Контроллер главного потока

    // ui/users/UsersVci.kt
    class UsersVci : SmStateVciAbst<UsersState>() {

        var usersORL = nullObservableRecordList

        override fun afterEnter(st: UsersState) {
            usersORL = st.usersRS.observableRecordList
        }

        override fun newScreen() = provideUsersView(this)

        override fun getTitle() = "Пользователи"
    }

VCI — View Control Interface

### Пользовательский интерфейс

    // ui/users/view/UsersView.kt
    fun provideUsersView(vci: UsersVci) = object : VcpScreen {
        @Composable
        override fun Content(pv: PaddingValues) {
            val users by vci.usersORL.observeAsState()
            LazyColumn(
                Modifier
                    .fillMaxSize()
                    .padding(pv)
            ) {
                items(users) { or ->
                    Card(
                        modifier = Modifier
                            .fillMaxWidth()
                            .padding(8.dp),
                        onClick = { /* переход на карточку */ }
                    ) {
                        Column(Modifier.padding(16.dp)) {
                            Text(
                                or.getValueAsString("name"),
                                style = MaterialTheme.typography.titleMedium
                            )
                            Text(
                                or.getValueAsString("email"),
                                style = MaterialTheme.typography.bodyMedium
                            )
                        }
                    }
                }
            }
        }
    }

### Точка входа

    // MainActivity.kt
    class MainActivity : GsBaseActivity<AppNavigator>() {

        @Composable
        override fun ContentView(navigatorCtrl: NavigatorCtrl) {
            GlobalSystemAppTheme {
                navigator.setBaseMenu(
                    listOf(
                        DrawerItem("Пользователи") {
                            getSts().postCallState<UsersState>()
                        }
                    )
                )
                GsDrawerNavigatorViewV2(navigatorCtrl)
            }
        }

        override fun provideNavigator()  = AppNavigator()
        override fun provideDatabase()   = AppDatabase.getInstance(this)
        override fun provideFirstState() = UsersState()
    }

### Итог

- Все SQL-операции, сеть и файлы уже работают внутри транзакций `GsSession`.

- UI-поток чист: ни одного `launch(Dispatchers.IO)` в пользовательском коде.

- Навигация описана в три строки (DrawerItem → `postCallState`).

- Расширение команды: новый экран — это ещё `State + VCI + View`, инфраструктуру трогать не нужно.

Получился полноценный экран, который:

1. Умеет читать и писать в БД транзакционно.

2. Никогда не блокирует главный поток.

3. Восстанавливается после сбоев приложения (снимок стека сохраняет State Manager).

4. Готов к расширению: добавление сети, плагинов камеры, офлайн-синхронизации и т.д.