📚 Pré-requisitos Teóricos: este projeto aplica conceitos ensinados em Especialização em Android Nativo. Recomendado revisar antes de começar.
Bem-vindo à oitava etapa da Fase 7: Arquitetura Avançada & Persistência Reativa! 🏛️
Neste projeto prático, você aprenderá a implementar a arquitetura de software recomendada pela Google (MVVM - Model-View-ViewModel) combinada com persistência local avançada via Room Database, injeção de dependências e o padrão Offline-First (Single Source of Truth).
Com a temática do Tesouro Real e Banco Nacional de Wakanda (Vibranium Kryptos - VK), construiremos um sistema financeiro robusto com cartão digital Black Panther, extrato de transações alimentado por Kotlin Flows reativos, proteção rigorosa contra SQL Injection via consultas DAO parametrizadas e isolamento de banco de dados na sandbox do Android (em conformidade com o OWASP Mobile Security)!
| 1. Banco Local Limpo | 2. Sincronizado (Single Source) | 3. Diálogo de Transferência |
|---|---|---|
| Estado inicial sem dados locais em cache. | Saldo consolidado e extrato persistido no Room. | Modal para registro seguro de nova movimentação. |
Antes de iniciar este laboratório, certifique-se de ter compreendido:
Ao concluir este projeto autoguiado, você será capaz de:
@Entity, definir operações com @Dao e instanciar o @Database.Flow, garantindo que dados da nuvem sejam primeiro persistidos antes de serem exibidos.collectAsState() e emitir atualizações assíncronas em Coroutines.graph TD
A[Servidor / API WakandaBankApi] -->|Sincronização em Nuvem| B[WakandaRepository]
B -->|dao.inserirTodas| C[(SQLite / Room Database)]
C -->|Flow<List<Transacao>>| B
B -->|todasTransacoes Flow| D[WakandaViewModel]
D -->|StateFlow / collectAsState| E[Interface Jetpack Compose]
E -->|Nova Transferência| D
D -->|viewModelScope.launch| B
B -->|dao.inserir| C
[!IMPORTANT] Blindagem de Dados e Prevenção de Vulnerabilidades Bancárias: Armazenamento inseguro de dados e injeção de comandos são vetores críticos em aplicações financeiras.
Nossa Estratégia de Defesa:
- Isolamento em Sandbox do App (
Data Isolation): O banco de dados do Room reside no diretório privado da aplicação (/data/data/br.com.curso.wakanda/databases/), protegido por permissões de UID do Linux que impedem o acesso por outros aplicativos.- Prevenção de SQL Injection: O compilador do Room (
kapt 'androidx.room:room-compiler') analisa as queries em tempo de compilação e utiliza Prepared Statements parametrizados com:parametro, impedindo manipulação maliciosa de strings SQL.- Integridade Criptográfica: Cada transação possui um identificador com assinatura hash simulada (
SHA256:...) para auditoria de integridade.- Higiene de Versionamento: Arquivo
.gitignoreconfigurado para evitar o vazamento de artefatos de compilação ou arquivos locais.
| Termo / Componente | O que é e para que serve? |
|---|---|
| MVVM | Padrão arquitetural que divide o código em Model (Dados), View (Interface) e ViewModel (Estado e Regras de Negócio). |
| Room Database | Biblioteca de abstração do Android sobre o SQLite que oferece persistência tipada e verificação de queries em tempo de compilação. |
| DAO (Data Access Object) | Interface que declara os métodos de leitura e escrita (@Query, @Insert, @Delete) no banco de dados. |
| Single Source of Truth | Prática em que o banco local é a única fonte primária de verdade para a interface do usuário. |
| Kotlin Flow | Fluxo assíncrono e reativo de dados que emite atualizações em tempo real para os observadores da UI. |
| StateFlow | Variável observável e com retenção de estado que notifica o Compose a cada mutação de dados. |
File > New > New Project).Banco de Wakanda (ou android_p19_banco_mvvm_hilt)br.com.curso.wakandaKotlinAPI 24 ("Nougat"; Android 7.0) ou superiorGroovy DSL (build.gradle) ou Kotlin DSL (build.gradle.kts)build.gradle)Abra app > build.gradle:
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.kapt'
}
dependencies {
implementation platform('androidx.compose:compose-bom:2024.02.00')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'
implementation 'androidx.activity:activity-compose:1.8.2'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0'
// Room Database
implementation 'androidx.room:room-runtime:2.6.1'
implementation 'androidx.room:room-ktx:2.6.1'
kapt 'androidx.room:room-compiler:2.6.1'
}
AndroidManifest.xml)Abra app > src > main > AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:label="Banco de Wakanda"
android:supportsRtl="true"
android:theme="@style/Theme.CartaoTreinador">
<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>
</manifest>
Abra app > src > main > java > br > com > curso > wakanda > MainActivity.kt:
@Entity(tableName = "transacoes_wakanda")
data class TransacaoVibranium(
@PrimaryKey val id: String,
val titulo: String,
val valorVK: Double,
val dataHora: String,
val tipo: String, // "ENTRADA" ou "SAIDA"
val hashSeguranca: String
)
@Dao
interface TransacaoDao {
// Retorna Flow reativo monitorado pelo Room
@Query("SELECT * FROM transacoes_wakanda ORDER BY id DESC")
fun listarTodas(): Flow<List<TransacaoVibranium>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun inserir(transacao: TransacaoVibranium)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun inserirTodas(transacoes: List<TransacaoVibranium>)
}
class WakandaRepository(private val dao: TransacaoDao) {
val todasTransacoes: Flow<List<TransacaoVibranium>> = dao.listarTodas()
suspend fun sincronizarComApi() {
delay(1000) // Simulação de latência de rede segura
val dadosNuvem = WakandaBankApi.buscarTransacoesNuvem()
dao.inserirTodas(dadosNuvem)
}
suspend fun registrarTransacao(transacao: TransacaoVibranium) {
dao.inserir(transacao)
}
}
class WakandaViewModel(private val repository: WakandaRepository) : ViewModel() {
val transacoes = repository.todasTransacoes
private val _estaSincronizando = MutableStateFlow(false)
val estaSincronizando: StateFlow<Boolean> = _estaSincronizando.asStateFlow()
fun sincronizar() {
viewModelScope.launch {
_estaSincronizando.value = true
repository.sincronizarComApi()
_estaSincronizando.value = false
}
}
}
@Composable
fun WakandaScreen() {
val context = LocalContext.current
val database = remember { Room.inMemoryDatabaseBuilder(context, WakandaDatabase::class.java).build() }
val repository = remember { WakandaRepository(database.transacaoDao()) }
val viewModel = remember { WakandaViewModel(repository) }
val transacoesList by viewModel.transacoes.collectAsState(initial = emptyList())
val estaSincronizando by viewModel.estaSincronizando.collectAsState()
val saldoTotal = transacoesList.sumOf { if (it.tipo == "ENTRADA") it.valorVK else -it.valorVK }
// Renderização do Cartão Black Panther de Vibranium com saldo formatado em VK
}
| Sintoma Observado | Causa Provável | Como Resolver |
|---|---|---|
Cannot access database on the main thread |
Uma operação de escrita ou leitura síncrona do Room foi chamada na thread principal. | Use suspend fun no DAO e invoque as chamadas dentro de viewModelScope.launch { ... } ou retorne um Flow. |
A lista da UI não atualiza após chamar inserir() |
O DAO está retornando List<T> simples em vez de Flow<List<T>>. |
Altere o tipo de retorno do método de consulta no DAO para Flow<List<TransacaoVibranium>>. |
IllegalStateException: Room cannot verify the data integrity |
Alteração na estrutura da @Entity sem incremento de versão ou fallbackToDestructiveMigration(). |
Incremente a version no @Database ou configure as migrações automáticas do Room. |
| Erro de compilação com KAPT | O plugin org.jetbrains.kotlin.kapt não foi declarado no build.gradle. |
Adicione id 'org.jetbrains.kotlin.kapt' no bloco de plugins do módulo app. |
net.zetetic:android-database-sqlcipher para criptografar o arquivo físico do SQLite com chave AES de 256 bits.Disponível em: app/src/main/java/br/com/curso/wakanda/MainActivity.kt.
No seu editor/IDE, abra a pasta deste projeto (File > Open Folder) ou navegue via terminal:
cd proj_aplicacoes_full_stack/projetos/android_p19_banco_mvvm_hilt
./gradlew build
./gradlew test
# ou abrir no Android Studio e clicar em Run (Shift+F10)
[!TIP] Dica para execução a partir da raiz do repositório: Se você abriu o repositório completo no VS Code ou Android Studio, abra o projeto diretamente pela pasta
proj_aplicacoes_full_stack/projetos/android_p19_banco_mvvm_hilt.