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 no Android 2026: Migrations, Relacionamentos e Coroutines

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.

build.gradle.kts (Module: app)kotlin
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.

build.gradle.ktskotlin
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.

kotlin
@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.

kotlin
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.

kotlin
@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.

kotlin
@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.

kotlin
@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.

kotlin
@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.

kotlin
@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.

kotlin
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.

kotlin
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.

kotlin
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.

kotlin
@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