
Ao desenvolver uma aplicação Android, frequentemente precisamos armazenar informações localmente no dispositivo. Embora seja possível utilizar diretamente a API SQLite, o Android oferece o Room como uma camada de abstração que simplifica o acesso ao banco de dados e reduz a quantidade de código necessário.
Neste artigo veremos como utilizar o Room com Kotlin e Jetpack Compose, organizando a aplicação em camadas.
O objetivo não é apenas mostrar como criar uma tabela, mas compreender como as diferentes partes da aplicação se relacionam:
Compose → ViewModel → Repository → DAO → Room → SQLite
Também veremos o papel da classe Application, da MainActivity e da ViewModelFactory.

A interface Jetpack Compose não acessa diretamente o banco de dados. Ela se comunica com o ViewModel, que utiliza o Repository. O Repository acessa o DAO, e o DAO utiliza o Room para realizar as operações sobre o SQLite.
Este tutorial utiliza Room 2.7.2.
O que vamos construir
Para tornar os conceitos mais claros, construiremos uma pequena aplicação capaz de cadastrar, consultar, alterar e excluir usuários.
A aplicação utilizará:
- Kotlin
- Jetpack Compose
- Room
- ViewModel
- Repository
- DAO
- Flow
- MVVM
2. Estrutura de arquivos
A UserViewModelFactory faz parte da camada ViewModel e é responsável por fornecer o UserRepository ao UserViewModel quando ele é criado pela MainActivity.
com.example.app
app/
│
├── data/
│ ├── database/
│ │ ├── AppDatabase.kt
│ │ └── UserDao.kt
│ │
│ ├── entity/
│ │ └── User.kt
│ │
│ └── repository/
│ └── UserRepository.kt
│
├── viewmodel/
│ ├── UserViewModel.kt
│ └── UserViewModelFactory.kt
│
├── ui/
│ └── UserScreen.kt
│
├── App.kt
└── MainActivity.kt
3. Dependências do Room
No app/build.gradle.kts:
plugins {
id(“com.android.application”)
id(“org.jetbrains.kotlin.android”)
id(“com.google.devtools.ksp”)
}
dependencies {
val roomVersion = “2.7.2”
implementation(“androidx.room:room-runtime:$roomVersion”)
implementation(“androidx.room:room-ktx:$roomVersion”)
ksp(“androidx.room:room-compiler:$roomVersion”)
}
room-runtime
implementation("androidx.room:room-runtime:$roomVersion")Ela fornece as classes e APIs necessárias para que a aplicação utilize o Room em tempo de execução.
Em outras palavras:
room-runtimeé o núcleo do Room que será utilizado pela aplicação.
room-ktx
implementation("androidx.room:room-ktx:$roomVersion")Fornece a integração do Room com recursos do Kotlin, especialmente Coroutines e Flow.
Isso é particularmente importante neste tutorial porque nosso DAO utiliza:
suspend fun insert(...)e:
fun getAll(): Flow<List<UserEntity>>Neste projeto,
room-ktxé importante porque utilizaremos as funcionalidades de Kotlin para trabalhar de forma assíncrona e observar alterações no banco através deFlow.
room-compiler
ksp("androidx.room:room-compiler:$roomVersion")O Room utiliza anotações como:
@Entity
@Dao
@Database
@Query
@Insert
@Update
@DeleteMas essas anotações, sozinhas, não executam as operações.
O Room Compiler analisa essas classes e anotações durante a compilação e gera o código necessário para implementar o funcionamento do Room.
Por isso usamos:
ksp(...)em vez de:
implementation(...)
room-compilernão é uma biblioteca utilizada diretamente pelo aplicativo em tempo de execução. Ele é utilizado durante a compilação para gerar o código do Room.
KSP
id("com.google.devtools.ksp")O KSP (Kotlin Symbol Processing) é utilizado pelo Room para processar as anotações das classes durante a compilação e gerar automaticamente o código necessário.
@Entity
@Dao
@Query
@Insert
│
▼
KSP
│
▼
Room Compiler
│
▼
Código gerado
4. Entity
Arquivo: data/entity/UserEntity.kt
package com.example.app.data.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = “users”)
data class UserEntity(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val name: String,
val email: String
)
Importante destacar:
@Entityinforma ao Room que a classe representa uma tabela.- Cada propriedade da classe representa uma coluna.
@PrimaryKeydefine a chave primária.
Por exemplo, o código é:
@Entity(tableName = "users")
data class UserEntity(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val name: String,
val email: String
)Isso é equivalente a :
UserEntity
│
│ @Entity
▼
Tabela: users
│
├── id → chave primária
├── name → coluna
└── email → colunaIdentificador único id:
@PrimaryKey(autoGenerate = true)
val id: Long = 0“Se o
idcomeça em zero, como o Room sabe qual número usar?”
Explicação curta:
Com
autoGenerate = true, o identificador é gerado automaticamente pelo banco quando um novo registro é inserido. O valor0é utilizado no objeto Kotlin para indicar que ainda não existe um ID atribuído àquele registro.
Nome da tabela:
@Entity(tableName = "users")
tableName = "users"define explicitamente o nome da tabela no banco. Se o nome da tabela não fosse informado, o Room utilizaria o nome da classe como referência para a tabela.
UserEntity é o nome da classe Kotlin, enquanto users é o nome da tabela no banco de dados.
5. DAO
O DAO (Data Access Object) é a interface responsável por definir as operações que a aplicação pode realizar sobre os dados armazenados pelo Room.
Enquanto a Entity define como os dados são representados no banco, o DAO define como esses dados serão consultados, inseridos, alterados ou excluídos.
Para a nossa UserEntity, podemos criar o seguinte DAO:
@Dao
interface UserDao {
@Query("SELECT * FROM users ORDER BY name")
fun getAll(): Flow<List<UserEntity>>
@Insert
suspend fun insert(user: UserEntity)
@Update
suspend fun update(user: UserEntity)
@Delete
suspend fun delete(user: UserEntity)
}@Dao
A anotação:
@Daoinforma ao Room que essa interface contém as operações de acesso ao banco de dados.
O Room analisa essas definições durante a compilação e gera automaticamente a implementação necessária.
Consultando os dados
A consulta:
@Query("SELECT * FROM users ORDER BY name")
fun getAll(): Flow<List<UserEntity>>utiliza SQL para selecionar todos os registros da tabela users, ordenando-os pelo nome.
O retorno é um:
Flow<List<UserEntity>>O Flow é importante porque permite observar as alterações realizadas nos dados.
Quando os registros observados pela consulta são alterados, o Room pode emitir uma nova lista através do Flow.
Podemos representar esse fluxo assim:
Banco de dados
│
▼
Room
│
▼
DAO
│
▼
Flow<List<UserEntity>>
│
▼
ViewModel
│
▼
Jetpack ComposeDessa forma, a interface pode receber automaticamente as alterações dos dados sem precisar consultar o banco novamente a cada mudança.
Inserindo dados
Para inserir um usuário utilizamos:
@Insert
suspend fun insert(user: UserEntity)A anotação @Insert informa ao Room que o método deve inserir um registro na tabela correspondente à UserEntity.
O modificador suspend permite que a operação seja executada utilizando Kotlin Coroutines, evitando que uma operação de banco de dados seja realizada diretamente na thread principal da interface.
Atualizando dados
Para alterar um registro existente:
@Update
suspend fun update(user: UserEntity)O Room utiliza a chave primária da entidade para identificar o registro que deve ser atualizado.
Excluindo dados
Para excluir um registro:
@Delete
suspend fun delete(user: UserEntity)O Room utiliza a entidade recebida para identificar o registro que deverá ser removido.
O papel do DAO na arquitetura
O DAO é a camada que conhece as operações do banco de dados.
A interface da aplicação, por outro lado, não precisa conhecer SQL:
Jetpack Compose
│
▼
ViewModel
│
▼
Repository
│
▼
DAO
│
▼
Room
│
▼
SQLiteAssim, cada componente possui uma responsabilidade bem definida:
- Entity: representa os dados armazenados.
- DAO: define as operações sobre esses dados.
- Room: executa essas operações e gerencia o acesso ao SQLite.
Em resumo: a
Entitydescreve a estrutura dos dados e oDAOdescreve as operações que podem ser realizadas sobre eles. O Room utiliza essas informações para gerar o código necessário para acessar o banco de dados.
Arquivo: data/dao/UserDao.kt
package com.example.app.data.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import com.example.app.data.entity.UserEntity
import kotlinx.coroutines.flow.Flow
@Dao
interface UserDao {
@Insert
suspend fun insert(user: UserEntity)
@Update
suspend fun update(user: UserEntity)
@Delete
suspend fun delete(user: UserEntity)
@Query(“SELECT * FROM users ORDER BY name”)
fun getAll(): Flow<List<UserEntity>>
}
O DAO possui as operações básicas de inserir, atualizar, deletar e listar. O Flow permite que a interface receba automaticamente as alterações do banco.
6. Database
A classe AppDatabase representa o banco de dados da aplicação no Room.
Ela deve herdar de RoomDatabase e utilizar a anotação @Database, informando quais Entitys fazem parte do banco e qual é a versão atual do banco de dados.
Para o nosso exemplo:
@Database(
entities = [UserEntity::class],
version = 1,
exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}@Database
A anotação:
@Database(
entities = [UserEntity::class],
version = 1,
exportSchema = false
)informa ao Room as principais características do banco.
O parâmetro entities indica quais entidades fazem parte do banco de dados:
entities = [UserEntity::class]Neste caso, a aplicação possui apenas a tabela users, representada pela UserEntity.
Em uma aplicação com várias tabelas, podemos informar várias entidades:
entities = [
UserEntity::class,
ProductEntity::class,
OrderEntity::class
]Versão do banco
O parâmetro:
version = 1representa a versão do esquema do banco de dados.
Quando a estrutura do banco for modificada em uma versão futura da aplicação, essa versão deverá ser incrementada e será necessário definir como os dados existentes serão migrados.
Neste exemplo estamos começando com a versão 1.
exportSchema
Utilizamos:
exportSchema = falsepara não exportar o esquema do banco durante a compilação deste exemplo.
Em projetos maiores, especialmente quando são utilizadas migrações e controle da evolução do banco, pode ser interessante manter a exportação do schema habilitada.
O DAO fornecido pelo Database
A classe também declara:
abstract fun userDao(): UserDaoIsso permite que outras partes da aplicação obtenham o DAO através do banco:
AppDatabase
│
└── userDao()
│
▼
UserDaoO AppDatabase funciona, portanto, como o ponto central de acesso aos DAOs da aplicação.
Como Entity, DAO e Database se relacionam?
Até aqui temos três componentes fundamentais:
Room Database
│
┌───────┴────────┐
│ │
Entity DAO
│ │
▼ ▼
"O que é?" "O que fazer?"
│ │
▼ ▼
Tabela users SELECT / INSERT
UPDATE / DELETEA Entity descreve a estrutura dos dados, o DAO define as operações que podem ser realizadas e o AppDatabase reúne essas informações para que o Room possa gerenciar o banco de dados.
O AppDatabase não é criado diretamente pela classe
A classe AppDatabase é abstrata:
abstract class AppDatabase : RoomDatabase()Não criamos uma instância utilizando AppDatabase().
A instância será criada pelo próprio Room utilizando um databaseBuilder, como veremos no próximo passo, quando configurarmos o banco na classe App.
Arquivo: data/database/AppDatabase.kt
package com.example.app.data.database
import androidx.room.Database
import androidx.room.RoomDatabase
import com.example.app.data.dao.UserDao
import com.example.app.data.entity.UserEntity
@Database(
entities = [UserEntity::class],
version = 1,
exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
7. App — a Application da aplicação
Agora que criamos o AppDatabase, precisamos criar uma instância desse banco para que as demais partes da aplicação possam utilizá-lo.
Para isso, vamos criar uma classe chamada App, que herda de Application.
class App : Application() {
val database: AppDatabase by lazy {
Room.databaseBuilder(
applicationContext,
AppDatabase::class.java,
"app_database"
).build()
}
}O que é a classe Application?
Application é uma classe fornecida pelo Android que representa o estado global da aplicação enquanto o processo da aplicação estiver em execução.
Ao criar:
class App : Application()estamos criando uma classe personalizada de Application para o nosso aplicativo.
Neste exemplo, vamos utilizá-la para disponibilizar a instância do AppDatabase.
Criando o banco de dados
A instância do banco é criada através de:
Room.databaseBuilder(
applicationContext,
AppDatabase::class.java,
"app_database"
).build()O databaseBuilder() recebe:
applicationContext— o contexto da aplicação;AppDatabase::class.java— a classe que representa nosso banco;"app_database"— o nome do arquivo do banco de dados.
O resultado é uma instância de AppDatabase.
Podemos visualizar essa relação:
App
│
▼
AppDatabase
│
▼
UserDaoPor que utilizar by lazy?
Utilizamos:
val database: AppDatabase by lazy {
...
}para que o banco seja criado somente quando a propriedade database for utilizada pela primeira vez.
Depois de criada, a mesma instância é reutilizada enquanto o objeto App permanecer em memória.
Isso evita a criação desnecessária de várias instâncias do RoomDatabase.
Por que o banco fica na App?
A MainActivity não deve ser responsável por criar o banco de dados.
Em vez disso, a App disponibiliza a infraestrutura necessária para que a Activity e as demais partes da aplicação possam utilizá-la.
Assim, na MainActivity, podemos obter o banco através de:
val app = application as Appe então acessar o DAO:
val userDao = app.database.userDao()A partir daí podemos criar o Repository e o ViewModel.
O fluxo fica:
App
│
└── AppDatabase
│
└── UserDao
│
└── UserRepository
│
└── UserViewModel
│
└── Jetpack ComposeApp não é a MainActivity
É importante não confundir essas duas classes.
A App herda de:
Applicatione representa a aplicação no nível global.
A MainActivity, por outro lado, é uma Activity e representa uma tela/ponto de entrada da interface da aplicação.
Portanto:
Application
│
└── App
│
└── AppDatabase
Activity
│
└── MainActivity
│
└── Jetpack ComposeEm resumo: neste projeto, a classe
Appé utilizada como ponto central para criar e disponibilizar a instância doAppDatabase. AMainActivityutiliza essa infraestrutura para montar oRepository, oViewModele a interface Jetpack Compose.
Arquivo: App.kt
package com.example.app
import android.app.Application
import androidx.room.Room
import com.example.app.data.database.AppDatabase
class App : Application() {
val database: AppDatabase by lazy {
Room.databaseBuilder(
applicationContext,
AppDatabase::class.java,
“app_database”
).build()
}
}
O by lazy faz com que o AppDatabase seja criado somente no primeiro acesso a database. Depois disso, a mesma instância é reutilizada.
8. AndroidManifest.xml
O AndroidManifest.xml informa ao Android os principais componentes da aplicação. Neste projeto, precisamos declarar a nossa classe App e a MainActivity.
<application
android:name=".App"
... >
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>A declaração:
android:name=".App"informa ao Android que a classe App será a implementação de Application utilizada pela aplicação.
Já:
android:name=".MainActivity"declara a MainActivity como uma Activity da aplicação.
O intent-filter com MAIN e LAUNCHER indica que essa Activity é o ponto de entrada da interface quando o usuário inicia o aplicativo.
Assim, podemos visualizar:
Android
│
├── cria App
│ └── disponibiliza AppDatabase
│
└── inicia MainActivity
└── inicia Jetpack ComposeImportante:
AppeMainActivitynão têm a mesma função.Appé aApplicationda aplicação, enquantoMainActivityé a Activity que inicia a interface.
9. App x MainActivity — quem é a principal?
É comum surgir a dúvida: entre App e MainActivity, qual é a classe principal da aplicação?
A resposta é: as duas possuem papéis diferentes.
App é a classe que representa a Application da aplicação e possui um ciclo de vida associado ao processo do aplicativo. Neste projeto, ela é utilizada para disponibilizar a instância do AppDatabase.
MainActivity é uma Activity e funciona como o ponto de entrada da interface da aplicação. É nela que iniciamos a composição da nossa UI com Jetpack Compose.
Podemos resumir assim:
Application
│
└── App
└── AppDatabase
└── UserDao
Activity
│
└── MainActivity
└── Jetpack ComposePortanto, não devemos pensar que uma delas “substitui” a outra:
Appcuida da infraestrutura global da aplicação, enquantoMainActivityinicia e apresenta a interface ao usuário.
Neste projeto, a MainActivity obtém a instância do banco disponibilizada pela App e, a partir dela, monta as dependências necessárias para o ViewModel.
App
│
└── AppDatabase
│
└── UserDao
│
▼
UserRepository
│
▼
UserViewModel
│
▼
MainActivity
│
▼
Jetpack ComposeEm outras palavras: App controla a infraestrutura da aplicação; MainActivity inicia e apresenta a interface.
10. MainActivity completa
Arquivo: MainActivity.kt
package com.example.app
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import com.example.app.data.repository.UserRepository
import com.example.app.ui.UserScreen
import com.example.app.viewmodel.UserViewModel
import com.example.app.viewmodel.UserViewModelFactory
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val app = application as App
val repository = UserRepository(
app.database.userDao()
)
val factory = UserViewModelFactory(repository)
val viewModel: UserViewModel by viewModels {
factory
}
setContent {
UserScreen(viewModel)
}
}
}
Observe que a MainActivity não cria o banco. Ela obtém a instância existente através de application as App e usa app.database. A Activity apenas conecta a infraestrutura à interface.
11. A instância única do Room
O RoomDatabase deve ser tratado como uma instância compartilhada pela aplicação.
No nosso projeto, essa instância é criada na classe App:
class App : Application() {
val database: AppDatabase by lazy {
Room.databaseBuilder(
applicationContext,
AppDatabase::class.java,
"app_database"
).build()
}
}O uso de by lazy garante que o banco seja criado somente quando for necessário. Depois de criado, a mesma instância de AppDatabase é reutilizada enquanto o processo da aplicação estiver em execução.
Por que não criar o banco várias vezes?
Imagine que cada parte da aplicação criasse seu próprio banco:
MainActivity
│
└── AppDatabase #1
OutraActivity
│
└── AppDatabase #2
Outro componente
│
└── AppDatabase #3Além de desnecessário, isso pode consumir recursos e dificultar o gerenciamento do acesso aos dados.
Em nosso projeto, temos uma única instância compartilhada:
App
│
▼
AppDatabase
│
┌──────────┼──────────┐
▼ ▼ ▼
UserDao TripDao CountryDaoTodas as partes da aplicação que precisam acessar o banco utilizam essa mesma instância.
Por que colocar o banco na App?
A classe App possui um ciclo de vida associado ao processo da aplicação, sendo um local adequado para manter objetos que precisam ser compartilhados por diferentes componentes.
Assim, a MainActivity não precisa criar o banco. Ela simplesmente obtém a instância já disponibilizada pela App:
val app = application as App
val database = app.databaseA partir dela, podemos obter o DAO:
val userDao = database.userDao()E continuar a construção das camadas da aplicação:
App
│
└── AppDatabase
│
└── UserDao
│
└── UserRepository
│
└── UserViewModelEm resumo: criamos uma única instância do
AppDatabasee a compartilhamos entre os componentes da aplicação. Isso evita a criação desnecessária de múltiplas instâncias do banco e mantém o acesso aos dados centralizado.
12. Repository
O Repository é a camada que fica entre o ViewModel e as fontes de dados da aplicação.
No nosso exemplo, o UserRepository será responsável por intermediar o acesso ao UserDao.
A ideia é evitar que o ViewModel precise conhecer os detalhes de implementação do Room.
ViewModel
│
▼
Repository
│
▼
UserDao
│
▼
Room
│
▼
SQLiteCriando o UserRepository
class UserRepository(
private val userDao: UserDao
) {
val users: Flow<List<UserEntity>> =
userDao.getAll()
suspend fun insert(user: UserEntity) {
userDao.insert(user)
}
suspend fun update(user: UserEntity) {
userDao.update(user)
}
suspend fun delete(user: UserEntity) {
userDao.delete(user)
}
}O Repository recebe o UserDao pelo construtor e utiliza esse DAO para realizar as operações necessárias.
Por que não usar o DAO diretamente no ViewModel?
Poderíamos fazer:
ViewModel
│
▼
UserDao
│
▼
Roome uma aplicação pequena poderia até funcionar dessa maneira.
Entretanto, dessa forma o ViewModel passa a conhecer diretamente a camada de persistência.
Utilizando o Repository:
ViewModel
│
▼
UserRepository
│
▼
UserDaoo ViewModel passa a depender do Repository, e não dos detalhes de como os dados são armazenados.
Isso cria uma separação mais clara entre as responsabilidades.
O Repository não substitui o DAO
É importante entender que Repository e DAO possuem funções diferentes.
DAO
Define as operações diretamente relacionadas ao banco:
@Query("SELECT * FROM users")
fun getAll(): Flow<List<UserEntity>>Repository
Utiliza o DAO e fornece uma interface de acesso aos dados para as camadas superiores:
val users: Flow<List<UserEntity>> =
userDao.getAll()Podemos resumir:
DAO
│
└── Conhece o banco e as consultas
Repository
│
└── Organiza o acesso aos dados
ViewModel
│
└── Utiliza o RepositoryUma vantagem para o futuro
Hoje nossa fonte de dados é apenas o Room:
UserRepository
│
▼
UserDao
│
▼
RoomMas uma aplicação pode futuramente utilizar outras fontes de dados.
Por exemplo:
UserRepository
/ \
▼ ▼
Room API RESTO ViewModel não precisa conhecer esses detalhes. Ele continua trabalhando com o UserRepository.
Em resumo: o DAO define como acessar o banco, enquanto o Repository organiza e intermedeia esse acesso para o restante da aplicação. O ViewModel utiliza o Repository sem precisar conhecer os detalhes do Room.
13. ViewModel
O ViewModel é a camada responsável por disponibilizar os dados e as ações que a interface Jetpack Compose precisa.
Ele recebe o UserRepository e utiliza o Repository para consultar ou modificar os dados.
Dessa forma, a interface não precisa conhecer o Room, o DAO ou o banco de dados.
A arquitetura fica:
Jetpack Compose
│
▼
UserViewModel
│
▼
UserRepository
│
▼
UserDao
│
▼
Room
│
▼
SQLiteCriando o UserViewModel
class UserViewModel(
private val repository: UserRepository
) : ViewModel() {
val users: StateFlow<List<UserEntity>> =
repository.users
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5_000),
emptyList()
)
fun insert(user: UserEntity) {
viewModelScope.launch {
repository.insert(user)
}
}
fun update(user: UserEntity) {
viewModelScope.launch {
repository.update(user)
}
}
fun delete(user: UserEntity) {
viewModelScope.launch {
repository.delete(user)
}
}
}Por que o ViewModel recebe o Repository?
O ViewModel não deve precisar conhecer os detalhes do banco de dados.
Ele não precisa saber que os dados estão armazenados em uma tabela SQLite, nem precisa executar SQL.
Ele simplesmente solicita ao Repository:
repository.insert(user)ou observa os usuários:
repository.usersAssim, cada camada possui uma responsabilidade:
Repository
│
└── Acesso aos dados
ViewModel
│
└── Estado e ações da tela
Compose
│
└── Interface do usuárioviewModelScope
As operações de gravação utilizam:
viewModelScope.launch {
repository.insert(user)
}viewModelScope é um escopo de coroutines associado ao ciclo de vida do ViewModel.
Isso permite executar operações assíncronas sem bloquear a interface.
Quando o ViewModel deixa de existir, as coroutines associadas ao seu viewModelScope também são canceladas.
Por que transformar Flow em StateFlow?
O Repository fornece:
Flow<List<UserEntity>>No ViewModel, transformamos esse Flow em um StateFlow:
val users: StateFlow<List<UserEntity>> =
repository.users
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5_000),
emptyList()
)O StateFlow representa o estado atual da tela.
Inicialmente temos:
emptyList()Quando o Room detectar alterações nos dados observados pelo DAO, um novo valor será emitido pelo Flow, chegará ao ViewModel e atualizará o StateFlow.
O Compose observa o estado
Na interface Jetpack Compose, podemos observar esse estado:
val users by viewModel.users.collectAsStateWithLifecycle()Quando users mudar, o Compose será recomposto e a interface poderá apresentar os novos dados.
O fluxo completo fica:
Room
│
▼
DAO
│
▼
Flow
│
▼
Repository
│
▼
ViewModel
│
StateFlow
│
▼
Compose
│
▼
TelaO ViewModel também recebe ações da interface
O caminho inverso acontece quando o usuário realiza alguma ação.
Por exemplo, ao pressionar o botão Salvar:
Usuário
│
▼
Compose
│
▼
ViewModel
│
▼
Repository
│
▼
DAO
│
▼
RoomA interface apenas informa ao ViewModel o que o usuário deseja fazer:
viewModel.insert(user)O ViewModel então utiliza o Repository para realizar a operação.
Em resumo: o ViewModel funciona como intermediário entre a interface e os dados. Ele mantém o estado utilizado pelo Compose, recebe as ações do usuário e utiliza o Repository para acessar os dados, mantendo a interface desacoplada do Room.
14. Factory do ViewModel
No item anterior criamos o UserViewModel com o seguinte construtor:
class UserViewModel(
private val repository: UserRepository
) : ViewModel() {
// ...
}Observe que o UserViewModel precisa receber um UserRepository para funcionar.
Esse detalhe é importante porque, normalmente, o Android consegue criar um ViewModel automaticamente quando ele possui um construtor sem parâmetros. Porém, neste caso, nosso UserViewModel possui uma dependência:
UserViewModel
│
└── precisa de → UserRepositoryPrecisamos então informar ao Android como criar esse ViewModel e qual UserRepository deve ser fornecido.
É justamente essa a função da UserViewModelFactory.
Criando a UserViewModelFactory
class UserViewModelFactory(
private val repository: UserRepository
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(
modelClass: Class<T>
): T {
if (modelClass.isAssignableFrom(UserViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return UserViewModel(repository) as T
}
throw IllegalArgumentException(
"Unknown ViewModel class"
)
}
}A Factory recebe o UserRepository no seu próprio construtor.
Depois, quando o Android precisar criar um UserViewModel, a Factory faz a ligação:
UserRepository
│
▼
UserViewModelFactory
│
│ cria
▼
UserViewModelQual é a relação entre Factory e ViewModel?
É importante não confundir as responsabilidades das duas classes.
O UserViewModel contém a lógica relacionada ao estado e às ações da tela:
UserViewModel
│
├── observa usuários
├── insere usuário
├── atualiza usuário
└── exclui usuárioA UserViewModelFactory não executa essas operações.
Sua função é simplesmente criar o UserViewModel fornecendo as dependências que ele precisa.
Podemos pensar na Factory como uma “fábrica” do ViewModel:
UserRepository
│
▼
UserViewModelFactory
│
│ create()
▼
UserViewModelPor que não criar o ViewModel diretamente?
Poderíamos escrever:
val viewModel = UserViewModel(repository)Mas um ViewModel deve ser obtido através do mecanismo de gerenciamento de ViewModels do Android, para que ele seja associado corretamente ao ciclo de vida do componente que o utiliza.
Por isso utilizamos a Factory juntamente com:
viewModel(...)ou com o mecanismo equivalente de criação de ViewModel.
A Factory informa ao sistema:
“Quando você precisar de um
UserViewModel, crie-o utilizando esteUserRepository.”
De onde vem o Repository?
A cadeia completa que construímos até agora é:
App
│
└── AppDatabase
│
└── UserDao
│
▼
UserRepository
│
▼
UserViewModelFactory
│
▼
UserViewModel
│
▼
Jetpack ComposeAssim, cada classe possui uma responsabilidade específica:
| Classe | Responsabilidade |
|---|---|
AppDatabase | Representa o banco Room |
UserDao | Define as operações no banco |
UserRepository | Intermedeia o acesso aos dados |
UserViewModel | Mantém o estado e as ações da tela |
UserViewModelFactory | Cria o UserViewModel fornecendo o UserRepository |
Factory não é o ViewModel
Essa distinção é importante:
UserViewModelFactory
│
│ cria
▼
UserViewModel
│
│ utiliza
▼
UserRepositoryA UserViewModelFactory não substitui o UserViewModel e também não contém a lógica da tela.
Ela existe para resolver o problema de criação de um ViewModel que possui dependências no construtor.
Em resumo:
UserViewModelé responsável pelo estado e pelas ações da tela.UserViewModelFactoryé responsável por criar oUserViewModele fornecer a ele oUserRepositoryde que precisa.
15. Tela Compose: listar, editar e deletar
Agora chegamos à interface da aplicação.
A tela Jetpack Compose não acessa diretamente o Room, o DAO ou o banco de dados. Ela trabalha com o UserViewModel.
O fluxo da aplicação fica:
Room
│
▼
DAO
│
▼
Repository
│
▼
ViewModel
│
▼
ComposeE, quando o usuário realiza uma ação, o fluxo ocorre no sentido contrário:
Compose
│
▼
ViewModel
│
▼
Repository
│
▼
DAO
│
▼
RoomObtendo o estado no Compose
Na tela, observamos o StateFlow disponibilizado pelo UserViewModel:
@Composable
fun UserScreen(
viewModel: UserViewModel
) {
val users by viewModel.users.collectAsStateWithLifecycle()
// interface
}O collectAsStateWithLifecycle() transforma o StateFlow em um estado que pode ser observado pelo Compose.
Quando os dados mudarem, o Compose será recomposto e a lista apresentada na tela será atualizada.
Listando os usuários
Podemos utilizar LazyColumn para apresentar os usuários:
LazyColumn {
items(users) { user ->
UserItem(
user = user,
onEdit = {
// editar
},
onDelete = {
viewModel.delete(user)
}
)
}
}A tela não precisa saber como o usuário será excluído do banco.
Ela simplesmente informa ao ViewModel:
viewModel.delete(user)O ViewModel utiliza o Repository, que utiliza o DAO:
UserScreen
│
│ delete(user)
▼
UserViewModel
│
▼
UserRepository
│
▼
UserDao
│
▼
RoomEditando um usuário
O mesmo princípio vale para a edição.
A interface pode informar ao ViewModel que o usuário foi alterado:
viewModel.update(user)O caminho será:
Compose
│
▼
ViewModel
│
▼
Repository
│
▼
DAO
│
▼
RoomDepois que o banco for atualizado, o Flow observado pelo DAO emitirá os novos dados.
Esses dados percorrem novamente as camadas:
Room
↓
DAO
↓
Flow
↓
Repository
↓
ViewModel
↓
StateFlow
↓
ComposeO resultado é que a tela pode ser atualizada automaticamente.
Edição e exclusão não pertencem ao Compose
É importante observar que o Compose não executa operações de banco de dados.
Ele apenas apresenta os dados e envia as ações do usuário para o ViewModel.
Assim:
Compose
│
├── apresenta dados
├── recebe interação
│
└── chama ViewModel
│
├── insert()
├── update()
└── delete()Essa separação mantém a interface independente da tecnologia utilizada para persistir os dados.
Hoje utilizamos Room, mas a interface não precisa conhecer essa implementação.
O ciclo completo
Neste ponto, temos praticamente toda a arquitetura da aplicação:
┌──────────────┐
│ Compose │
│ Tela │
└──────┬───────┘
│
ações / estado
│
▼
┌──────────────┐
│ ViewModel │
└──────┬───────┘
│
▼
┌──────────────┐
│ Repository │
└──────┬───────┘
│
▼
┌──────────────┐
│ DAO │
└──────┬───────┘
│
▼
┌──────────────┐
│ Room │
└──────┬───────┘
│
▼
SQLitePara leitura dos dados, o caminho retorna através do Flow, permitindo que o estado da interface seja atualizado conforme os dados do banco mudam.
Em resumo: a tela Compose apresenta os dados e recebe as ações do usuário. O
ViewModelcoordena essas ações, oRepositoryintermedeia o acesso aos dados, o DAO executa as operações e o Room gerencia a persistência no SQLite.
Arquivo: ui/UserScreen.kt
package com.example.app.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.example.app.data.entity.UserEntity
import com.example.app.viewmodel.UserViewModel
@Composable
fun UserScreen(
viewModel: UserViewModel
) {
val users by viewModel.users.collectAsState()
var editingUser by remember {
mutableStateOf<UserEntity?>(null)
}
var name by remember {
mutableStateOf(“”)
}
var email by remember {
mutableStateOf(“”)
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text(“Nome”) },
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = email,
onValueChange = { email = it },
label = { Text(“E-mail”) },
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp)
)
Button(
onClick = {
if (editingUser == null) {
viewModel.insert(name, email)
} else {
viewModel.update(
editingUser!!.copy(
name = name,
email = email
)
)
}
name = “”
email = “”
editingUser = null
},
modifier = Modifier.padding(top = 8.dp)
) {
Text(
if (editingUser == null)
“Adicionar”
else
“Salvar”
)
}
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(
items = users,
key = { it.id }
) { user ->
UserCard(
user = user,
onEdit = {
editingUser = user
name = user.name
email = user.email
},
onDelete = {
viewModel.delete(user)
}
)
}
}
}
}
@Composable
private fun UserCard(
user: UserEntity,
onEdit: () -> Unit,
onDelete: () -> Unit
) {
Card(
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(user.name)
Text(user.email)
}
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Button(onClick = onEdit) {
Text(“Editar”)
}
Button(onClick = onDelete) {
Text(“Deletar”)
}
}
}
}
}
A tela utiliza LazyColumn para a lista. Cada usuário aparece em um Card com os botões Editar e Deletar. Ao editar, os dados são carregados no formulário; ao salvar, o registro existente é atualizado pelo id.
16. Entity x Model
Em projetos pequenos, a Entity pode ser usada diretamente pela UI. Em projetos maiores, é recomendável separar a Entity do Model da aplicação.
data class User(
val id: Long,
val name: String,
val email: String
)
17. Fluxo completo
Android
│
├── App
│ └── AppDatabase
│ └── DAO
│ └── Repository
│
└── MainActivity
└── ViewModel
└── Compose / UserScreen
No acesso aos dados, o fluxo é:
UserScreen → ViewModel → Repository → DAO → Room → SQLite
18. Construindo a tela com Jetpack Compose
A interface do nosso exemplo foi construída utilizando Jetpack Compose. Nesta seção vamos identificar os principais componentes utilizados no arquivo UserScreen.kt e entender a função de cada um.
O objetivo não é apresentar todos os recursos do Compose, mas explicar os componentes necessários para compreender a tela criada neste tutorial.
@Composable
Uma função que constrói uma parte da interface é marcada com:
@Composable
fun UserScreen(
viewModel: UserViewModel
) {
...
}@Composable informa ao Compose que a função pode ser utilizada para construir a interface de usuário.
Também criamos um componente específico para apresentar cada usuário:
@Composable
private fun UserCard(
user: UserEntity,
onEdit: () -> Unit,
onDelete: () -> Unit
) {
...
}Assim, podemos dividir uma tela complexa em componentes menores.
Column
O Column organiza seus componentes verticalmente, um abaixo do outro.
Na nossa tela:
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
...
}Ele é utilizado como estrutura principal da tela:
Column
│
├── Campo Nome
├── Campo E-mail
├── Botão
└── LazyColumnRow
O Row organiza os componentes horizontalmente.
No UserCard, utilizamos um Row para colocar as informações do usuário de um lado e os botões do outro:
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
...
}Visualmente:
Nome / E-mail Editar DeletarTambém utilizamos outro Row para organizar os dois botões.
LazyColumn
O LazyColumn é utilizado para criar uma lista vertical.
No nosso exemplo:
LazyColumn {
items(
items = users,
key = { it.id }
) { user ->
UserCard(
user = user,
onEdit = { ... },
onDelete = { ... }
)
}
}A lista recebe os usuários fornecidos pelo ViewModel e cria um UserCard para cada usuário.
A palavra Lazy indica que o Compose pode criar e manter em composição apenas os itens necessários para a exibição, tornando esse componente adequado para listas maiores.
Card
O Card cria uma área visual para agrupar informações relacionadas.
No nosso exemplo, cada usuário é apresentado dentro de um Card:
Card(
modifier = Modifier.fillMaxWidth()
) {
...
}O resultado é aproximadamente:
┌────────────────────────────────────┐
│ João Silva [Editar] [Deletar]│
│ joao@email.com │
└────────────────────────────────────┘Text
Text apresenta texto na interface.
Por exemplo:
Text(user.name)
Text(user.email)Também utilizamos Text nos campos e botões:
Text("Nome")e:
Text("Deletar")OutlinedTextField
OutlinedTextField cria um campo de entrada de texto.
No nosso formulário:
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Nome") },
modifier = Modifier.fillMaxWidth()
)Existem duas informações importantes:
value indica o valor atual do campo:
value = namee onValueChange recebe o novo valor digitado:
onValueChange = { name = it }Assim, o campo e a variável name permanecem sincronizados.
O mesmo mecanismo é utilizado para o campo email.
Button
Button cria um botão interativo.
No nosso exemplo:
Button(
onClick = {
...
}
) {
Text("Adicionar")
}O código dentro de onClick é executado quando o usuário pressiona o botão.
Na nossa aplicação, essa ação pode resultar em uma inserção ou atualização no banco, mas o Compose não realiza essa operação diretamente.
Ele chama o ViewModel:
viewModel.insert(name, email)ou:
viewModel.update(...)Modifier
Modifier é utilizado para configurar características e comportamento dos componentes.
No nosso código encontramos, por exemplo:
Modifier
.fillMaxWidth()
.padding(16.dp)Nesse caso:
fillMaxWidth()faz o componente ocupar a largura disponível;padding(16.dp)adiciona espaço ao redor do componente.
O Modifier aparece em praticamente toda a construção da nossa interface.
remember e mutableStateOf
O Compose precisa saber quais valores representam o estado da interface.
No formulário utilizamos:
var name by remember {
mutableStateOf("")
}mutableStateOf cria um estado observável pelo Compose.
remember mantém esse estado durante as recomposições da função.
Quando fazemos:
name = ito estado muda e o Compose pode recompor os componentes que dependem desse valor.
Na nossa tela também utilizamos estado para controlar a edição:
var editingUser by remember {
mutableStateOf<UserEntity?>(null)
}Esse estado determina se o formulário está no modo Adicionar ou Salvar.
collectAsState
Os usuários vêm do ViewModel através de um Flow.
Na tela fazemos:
val users by viewModel.users.collectAsState()O collectAsState() permite que o Compose observe os valores emitidos pelo Flow como estado da interface.
Assim, quando a lista de usuários for alterada, a interface poderá ser atualizada.
Room
↓
DAO
↓
Flow
↓
ViewModel
↓
collectAsState()
↓
Compose
↓
LazyColumnComo os componentes se combinam
A tela completa pode ser entendida como uma árvore de componentes:
UserScreen
│
└── Column
│
├── OutlinedTextField
│ └── Text
│
├── OutlinedTextField
│ └── Text
│
├── Button
│ └── Text
│
└── LazyColumn
│
└── UserCard
│
└── Card
│
└── Row
│
├── Column
│ ├── Text
│ └── Text
│
└── Row
├── Button
│ └── Text
│
└── Button
└── TextEssa é uma das características fundamentais do Jetpack Compose: a interface é construída pela composição de funções e componentes.
Em resumo:
ColumneRoworganizam os elementos,LazyColumnapresenta a lista,Cardagrupa informações,Textapresenta textos,OutlinedTextFieldrecebe dados,Buttonrecebe ações eModifiercontrola layout e aparência.remember,mutableStateOfecollectAsStatepermitem que a interface reaja às mudanças de estado.
19. Conclusão
O exemplo agora possui todas as peças necessárias para demonstrar uma aplicação Android simples com Room e Jetpack Compose: App, MainActivity, Database, Entity, DAO, Repository, ViewModel, Factory e uma tela Compose com operações de inserir, listar, editar e deletar.
O ponto principal é separar responsabilidades. A App mantém a infraestrutura global, especialmente a instância do RoomDatabase. A MainActivity inicia a interface. O ViewModel coordena o estado, o Repository organiza o acesso aos dados, o DAO executa as operações do Room e o Compose apresenta os dados.

Deixe um comentário