Room Database no Android 2026: Migrations, Relacionamentos e Coroutines
Guia completo sobre Room Database em 2026: migrations automáticas e manuais, relacionamentos entre entidades, integração com Kotlin Coroutines e Flow para aplicativos Android modernos.

Room Database representa a solução de persistência recomendada pelo Google para aplicativos Android. Em 2026, esta biblioteca oferece integração nativa com Kotlin Coroutines e Flow, migrations automáticas robustas e gerenciamento avançado de relacionamentos entre entidades. Este tutorial apresenta as funcionalidades essenciais do Room para desenvolver aplicativos Android de alto desempenho e fácil manutenção.
Room faz parte do conjunto Android Jetpack e fornece uma camada de abstração sobre o SQLite. A versão 2.7+ traz melhorias significativas nas migrations automáticas e suporte nativo para coroutines.
Configuração do Room Database
A integração do Room em um projeto Android moderno requer adicionar as dependências apropriadas e configurar o plugin KSP (Kotlin Symbol Processing) para geração de código.
plugins {
id("com.google.devtools.ksp") version "2.1.0-1.0.29"
}
dependencies {
val roomVersion = "2.7.0"
implementation("androidx.room:room-runtime:$roomVersion")
implementation("androidx.room:room-ktx:$roomVersion")
ksp("androidx.room:room-compiler:$roomVersion")
}A configuração do compilador Room permite ativar funcionalidades avançadas como a exportação do schema para migrations.
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
arg("room.incremental", "true")
arg("room.generateKotlin", "true")
}Definição de Entidades e Relacionamentos
Room utiliza anotações para mapear classes Kotlin para tabelas SQLite. As entidades representam as tabelas do banco de dados.
@Entity(tableName = "users")
data class User(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
@ColumnInfo(name = "full_name")
val fullName: String,
@ColumnInfo(name = "email")
val email: String,
@ColumnInfo(name = "created_at")
val createdAt: Long = System.currentTimeMillis()
)
@Entity(
tableName = "posts",
foreignKeys = [
ForeignKey(
entity = User::class,
parentColumns = ["id"],
childColumns = ["user_id"],
onDelete = ForeignKey.CASCADE
)
],
indices = [Index(value = ["user_id"])]
)
data class Post(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
@ColumnInfo(name = "user_id")
val userId: Long,
@ColumnInfo(name = "title")
val title: String,
@ColumnInfo(name = "content")
val content: String
)Para representar relacionamentos entre entidades, Room oferece classes de dados com a anotação @Relation.
data class UserWithPosts(
@Embedded
val user: User,
@Relation(
parentColumn = "id",
entityColumn = "user_id"
)
val posts: List<Post>
)
data class PostWithUser(
@Embedded
val post: Post,
@Relation(
parentColumn = "user_id",
entityColumn = "id"
)
val user: User
)Os relacionamentos many-to-many requerem uma tabela de junção.
@Entity(tableName = "tags")
data class Tag(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val name: String
)
@Entity(
tableName = "post_tag_cross_ref",
primaryKeys = ["postId", "tagId"]
)
data class PostTagCrossRef(
val postId: Long,
val tagId: Long
)
data class PostWithTags(
@Embedded
val post: Post,
@Relation(
parentColumn = "id",
entityColumn = "id",
associateBy = Junction(
value = PostTagCrossRef::class,
parentColumn = "postId",
entityColumn = "tagId"
)
)
val tags: List<Tag>
)Criação de DAOs com Coroutines e Flow
Os Data Access Objects (DAO) definem as operações de banco de dados. Room 2.7 oferece suporte nativo para coroutines Kotlin e Flow.
@Dao
interface UserDao {
@Query("SELECT * FROM users ORDER BY created_at DESC")
fun getAllUsers(): Flow<List<User>>
@Query("SELECT * FROM users WHERE id = :userId")
fun getUserById(userId: Long): Flow<User?>
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserByIdOnce(userId: Long): User?
@Transaction
@Query("SELECT * FROM users WHERE id = :userId")
fun getUserWithPosts(userId: Long): Flow<UserWithPosts?>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: User): Long
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUsers(users: List<User>): List<Long>
@Update
suspend fun updateUser(user: User): Int
@Delete
suspend fun deleteUser(user: User): Int
@Query("DELETE FROM users WHERE id = :userId")
suspend fun deleteUserById(userId: Long): Int
}
@Dao
interface PostDao {
@Query("SELECT * FROM posts WHERE user_id = :userId ORDER BY id DESC")
fun getPostsByUser(userId: Long): Flow<List<Post>>
@Transaction
@Query("SELECT * FROM posts WHERE id = :postId")
fun getPostWithTags(postId: Long): Flow<PostWithTags?>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertPost(post: Post): Long
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertPostTagCrossRef(crossRef: PostTagCrossRef)
@Transaction
suspend fun insertPostWithTags(post: Post, tagIds: List<Long>) {
val postId = insertPost(post)
tagIds.forEach { tagId ->
insertPostTagCrossRef(PostTagCrossRef(postId, tagId))
}
}
}Configuração do Banco de Dados
A classe abstrata anotada com @Database representa o ponto de entrada principal do Room.
@Database(
entities = [
User::class,
Post::class,
Tag::class,
PostTagCrossRef::class
],
version = 1,
exportSchema = true
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun postDao(): PostDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
instance
}
}
}
}
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time
}
}Migrations de Banco de Dados
Room oferece duas abordagens para migrations: automáticas e manuais. As migrations automáticas gerenciam modificações simples do schema.
@Database(
entities = [User::class, Post::class, Tag::class, PostTagCrossRef::class],
version = 2,
autoMigrations = [
AutoMigration(from = 1, to = 2)
],
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
// ...
}Para modificações complexas que requerem especificação explícita, Room fornece AutoMigrationSpec.
@Database(
entities = [User::class, Post::class, Tag::class, PostTagCrossRef::class],
version = 3,
autoMigrations = [
AutoMigration(from = 1, to = 2),
AutoMigration(from = 2, to = 3, spec = AppDatabase.Migration2To3::class)
],
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
@RenameColumn(tableName = "users", fromColumnName = "full_name", toColumnName = "display_name")
@DeleteColumn(tableName = "users", columnName = "legacy_field")
class Migration2To3 : AutoMigrationSpec
// ...
}As migrations manuais oferecem controle total para transformações de dados complexas.
val MIGRATION_3_4 = object : Migration(3, 4) {
override fun migrate(db: SupportSQLiteDatabase) {
// Criação de uma nova tabela com o schema atualizado
db.execSQL("""
CREATE TABLE users_new (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
display_name TEXT NOT NULL,
email TEXT NOT NULL,
avatar_url TEXT,
created_at INTEGER NOT NULL DEFAULT 0
)
""".trimIndent())
// Migração dos dados existentes
db.execSQL("""
INSERT INTO users_new (id, display_name, email, created_at)
SELECT id, display_name, email, created_at FROM users
""".trimIndent())
// Remoção da tabela antiga e renomeação
db.execSQL("DROP TABLE users")
db.execSQL("ALTER TABLE users_new RENAME TO users")
// Criação de índices
db.execSQL("CREATE INDEX index_users_email ON users(email)")
}
}
// Aplicação da migration
Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
)
.addMigrations(MIGRATION_3_4)
.build()Integração com Repository Pattern
A arquitetura recomendada utiliza um Repository como intermediário entre os ViewModels e os DAOs.
class UserRepository(
private val userDao: UserDao,
private val postDao: PostDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
val allUsers: Flow<List<User>> = userDao.getAllUsers()
fun getUserWithPosts(userId: Long): Flow<UserWithPosts?> {
return userDao.getUserWithPosts(userId)
}
suspend fun createUser(fullName: String, email: String): Result<Long> {
return withContext(ioDispatcher) {
try {
val user = User(fullName = fullName, email = email)
val id = userDao.insertUser(user)
Result.success(id)
} catch (e: Exception) {
Result.failure(e)
}
}
}
suspend fun createPostWithTags(
userId: Long,
title: String,
content: String,
tagIds: List<Long>
): Result<Long> {
return withContext(ioDispatcher) {
try {
val post = Post(userId = userId, title = title, content = content)
postDao.insertPostWithTags(post, tagIds)
Result.success(post.id)
} catch (e: Exception) {
Result.failure(e)
}
}
}
}Uso com ViewModel e StateFlow
A integração do Room com ViewModels permite um gerenciamento reativo da interface do usuário.
class UserViewModel(
private val repository: UserRepository
) : ViewModel() {
private val _selectedUserId = MutableStateFlow<Long?>(null)
val users: StateFlow<List<User>> = repository.allUsers
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
val selectedUserWithPosts: StateFlow<UserWithPosts?> = _selectedUserId
.filterNotNull()
.flatMapLatest { userId ->
repository.getUserWithPosts(userId)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = null
)
fun selectUser(userId: Long) {
_selectedUserId.value = userId
}
fun createUser(fullName: String, email: String) {
viewModelScope.launch {
repository.createUser(fullName, email)
.onSuccess { id ->
_selectedUserId.value = id
}
.onFailure { error ->
// Tratamento do erro
}
}
}
}Testes Unitários do Room Database
Room fornece utilitários para testar migrations e operações de banco de dados.
@RunWith(AndroidJUnit4::class)
class UserDaoTest {
private lateinit var database: AppDatabase
private lateinit var userDao: UserDao
@Before
fun setup() {
val context = ApplicationProvider.getApplicationContext<Context>()
database = Room.inMemoryDatabaseBuilder(
context,
AppDatabase::class.java
)
.allowMainThreadQueries()
.build()
userDao = database.userDao()
}
@After
fun teardown() {
database.close()
}
@Test
fun insertAndRetrieveUser() = runTest {
val user = User(fullName = "John Doe", email = "john@example.com")
val id = userDao.insertUser(user)
val retrieved = userDao.getUserByIdOnce(id)
assertThat(retrieved).isNotNull()
assertThat(retrieved?.fullName).isEqualTo("John Doe")
}
@Test
fun flowEmitsUpdates() = runTest {
val user = User(fullName = "Jane Doe", email = "jane@example.com")
userDao.getAllUsers().test {
assertThat(awaitItem()).isEmpty()
userDao.insertUser(user)
val users = awaitItem()
assertThat(users).hasSize(1)
assertThat(users[0].fullName).isEqualTo("Jane Doe")
cancelAndIgnoreRemainingEvents()
}
}
}
@RunWith(AndroidJUnit4::class)
class MigrationTest {
@get:Rule
val helper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java
)
@Test
fun migrate3To4() {
// Criação da base v3
helper.createDatabase("test_db", 3).apply {
execSQL("INSERT INTO users (id, display_name, email, created_at) VALUES (1, 'Test', 'test@test.com', 0)")
close()
}
// Migration para v4
val db = helper.runMigrationsAndValidate("test_db", 4, true, MIGRATION_3_4)
// Verificação
val cursor = db.query("SELECT * FROM users WHERE id = 1")
assertThat(cursor.moveToFirst()).isTrue()
assertThat(cursor.getString(cursor.getColumnIndex("display_name"))).isEqualTo("Test")
}
}Pronto para mandar bem nas entrevistas de Android?
Pratique com nossos simuladores interativos, flashcards e testes tecnicos.
Perguntas Frequentes em Entrevistas
Durante entrevistas técnicas de Android, as perguntas sobre Room Database aparecem com frequência. Os candidatos devem dominar as diferenças entre Flow e suspend para queries, o gerenciamento de migrations sem perda de dados, e a otimização de queries com relacionamentos.
Os pontos-chave incluem a compreensão do funcionamento interno do Room com SQLite, o uso apropriado de índices para performance, e o gerenciamento de transações para manter a integridade dos dados.
Room Database continua sendo um componente fundamental do ecossistema Android Jetpack. O domínio das migrations, relacionamentos entre entidades e integração com Kotlin Coroutines permite construir aplicativos robustos e de alto desempenho. A abordagem reativa com Flow simplifica a sincronização entre o banco de dados e a interface do usuário, enquanto o padrão Repository garante uma arquitetura limpa e testável.
Compartilhar
Artigos relacionados

Kotlin Flow vs StateFlow vs SharedFlow: perguntas de entrevista Android em 2026
As perguntas sobre Kotlin Flow vs StateFlow vs SharedFlow que os entrevistadores de Android fazem em 2026, com respostas claras, tabela comparativa e código pronto para produção.

Kotlin Multiplatform 2026: compartilhar código Android/iOS
Como o Kotlin Multiplatform compartilha a lógica de negócio entre Android e iOS em 2026: configuração do Gradle, expect/actual, rede com Ktor, interface Compose Multiplatform e perguntas comuns de entrevista de KMP.

Android 16 em 2026: Novas APIs, Desktop Mode e Perguntas de Entrevista
Análise aprofundada do Android 16 API 36: edge-to-edge obrigatório, Desktop Mode, ProgressStyle, navegação preditiva e perguntas de entrevista técnica para desenvolvedores Android em 2026.