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

Ниже — минимальный, но полностью рабочий пример приложения на 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. Готов к расширению: добавление сети, плагинов камеры, офлайн-синхронизации и т.д.