Быстрый старт#
Ниже — минимальный, но полностью рабочий пример приложения на 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, инфраструктуру трогать не нужно.
Получился полноценный экран, который:
Умеет читать и писать в БД транзакционно.
Никогда не блокирует главный поток.
Восстанавливается после сбоев приложения (снимок стека сохраняет State Manager).
Готов к расширению: добавление сети, плагинов камеры, офлайн-синхронизации и т.д.