From everything-claude-code-mobile
Provides Android Room database patterns including entity definitions, DAO interfaces, relations, embedded objects, migrations, TypeConverters, and Kotlin Flow integration. Useful for local SQLite persistence in Android apps.
npx claudepluginhub ahmed3elshaer/everything-claude-code-mobile --plugin everything-claude-code-mobileThis skill uses the workspace's default tool permissions.
```kotlin
Provides JetBrains Exposed ORM patterns for Kotlin including DSL queries, DAOs, transactions, HikariCP pooling, Flyway migrations, and repositories. Use for database access setup.
Provides Kotlin patterns for JetBrains Exposed ORM: DSL/DAO queries, coroutine transactions, HikariCP pooling, Flyway migrations, repository pattern.
Provides Kotlin Exposed ORM patterns for PostgreSQL: schema design, soft deletes, Flyway migrations, tables, entities, repositories. Use for creating tables or implementing data access.
Share bugs, ideas, or general feedback.
plugins {
id("com.google.devtools.ksp") version "2.0.21-1.0.27"
}
dependencies {
val roomVersion = "2.6.1"
implementation("androidx.room:room-runtime:$roomVersion")
implementation("androidx.room:room-ktx:$roomVersion")
ksp("androidx.room:room-compiler:$roomVersion")
testImplementation("androidx.room:room-testing:$roomVersion")
}
@Entity(
tableName = "users",
indices = [
Index(value = ["email"], unique = true),
Index(value = ["created_at"])
]
)
data class UserEntity(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
@ColumnInfo(name = "display_name")
val displayName: String,
@ColumnInfo(name = "email")
val email: String,
@ColumnInfo(name = "avatar_url")
val avatarUrl: String? = null,
@ColumnInfo(name = "created_at")
val createdAt: Long = System.currentTimeMillis(),
@Ignore
val isOnline: Boolean = false
)
data class Address(
val street: String,
val city: String,
@ColumnInfo(name = "zip_code") val zipCode: String
)
@Entity(tableName = "profiles")
data class ProfileEntity(
@PrimaryKey val userId: Long,
@Embedded(prefix = "address_") val address: Address
)
data class UserWithPosts(
@Embedded val user: UserEntity,
@Relation(
parentColumn = "id",
entityColumn = "author_id"
)
val posts: List<PostEntity>
)
@Dao
interface UserDao {
@Query("SELECT * FROM users ORDER BY created_at DESC")
fun observeAll(): Flow<List<UserEntity>>
@Query("SELECT * FROM users WHERE id = :userId")
fun observeById(userId: Long): Flow<UserEntity?>
@Query("SELECT * FROM users WHERE email = :email LIMIT 1")
suspend fun findByEmail(email: String): UserEntity?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsert(user: UserEntity): Long
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertIfNotExists(users: List<UserEntity>): List<Long>
@Update
suspend fun update(user: UserEntity)
@Delete
suspend fun delete(user: UserEntity)
@Query("DELETE FROM users WHERE id = :userId")
suspend fun deleteById(userId: Long)
@Transaction
@Query("SELECT * FROM users WHERE id = :userId")
fun observeUserWithPosts(userId: Long): Flow<UserWithPosts?>
@Transaction
suspend fun replaceAll(users: List<UserEntity>) {
deleteAll()
insertIfNotExists(users)
}
@Query("DELETE FROM users")
suspend fun deleteAll()
}
class Converters {
@TypeConverter
fun fromDate(date: Date?): Long? = date?.time
@TypeConverter
fun toDate(timestamp: Long?): Date? = timestamp?.let { Date(it) }
@TypeConverter
fun fromStatus(status: UserStatus): String = status.name
@TypeConverter
fun toStatus(value: String): UserStatus = UserStatus.valueOf(value)
@TypeConverter
fun fromStringList(list: List<String>): String =
Json.encodeToString(list)
@TypeConverter
fun toStringList(value: String): List<String> =
Json.decodeFromString(value)
}
@Database(
entities = [
UserEntity::class,
PostEntity::class,
ProfileEntity::class
],
version = 2,
exportSchema = true
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun postDao(): PostDao
}
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE users ADD COLUMN avatar_url TEXT")
db.execSQL(
"CREATE INDEX IF NOT EXISTS index_users_created_at ON users(created_at)"
)
}
}
// Destructive fallback for development
val database = Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
.addMigrations(MIGRATION_1_2)
.fallbackToDestructiveMigration() // only in debug builds
.build()
val databaseModule = module {
single {
Room.databaseBuilder(
androidContext(),
AppDatabase::class.java,
"app.db"
)
.addMigrations(MIGRATION_1_2)
.build()
}
single { get<AppDatabase>().userDao() }
single { get<AppDatabase>().postDao() }
}
class UserRepository(private val userDao: UserDao) {
val allUsers: Flow<List<UserEntity>> = userDao.observeAll()
fun observeUser(id: Long): Flow<UserEntity?> = userDao.observeById(id)
suspend fun addUser(user: UserEntity): Long = userDao.upsert(user)
suspend fun removeUser(userId: Long) = userDao.deleteById(userId)
}
// In ViewModel
class UserListViewModel(
private val repository: UserRepository
) : ViewModel() {
val users: StateFlow<List<UserEntity>> = repository.allUsers
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
}
@RunWith(AndroidJUnit4::class)
class UserDaoTest {
private lateinit var database: AppDatabase
private lateinit var userDao: UserDao
@Before
fun setup() {
database = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
AppDatabase::class.java
)
.allowMainThreadQueries()
.build()
userDao = database.userDao()
}
@After
fun tearDown() {
database.close()
}
@Test
fun insertAndRetrieveUser() = runTest {
val user = UserEntity(displayName = "Alice", email = "alice@test.com")
val id = userDao.upsert(user)
val result = userDao.findByEmail("alice@test.com")
assertNotNull(result)
assertEquals("Alice", result?.displayName)
}
@Test
fun observeUsersEmitsUpdates() = runTest {
val emissions = mutableListOf<List<UserEntity>>()
val job = launch(UnconfinedTestDispatcher()) {
userDao.observeAll().toList(emissions)
}
userDao.upsert(UserEntity(displayName = "Bob", email = "bob@test.com"))
advanceUntilIdle()
assertTrue(emissions.last().any { it.displayName == "Bob" })
job.cancel()
}
}
Flow return types for reactive queries; use suspend for one-shot operations.OnConflictStrategy.REPLACE for upsert patterns, IGNORE for insert-if-absent.exportSchema = true) to track migration history in version control.@Transaction for queries returning relations or when performing multi-step writes.MigrationTestHelper from room-testing.