Um aplicativo de conferência robusto precisa preservar o contexto do usuário mesmo após a interrupção abrupta do processo. O projeto DroidKaigi 2024 implementa uma estratégia abrangente para restaurar o estado da interface do usuário em plataformas Android e iOS. Este guia técnico explora as arquiteturas e padrões utilizados para garantir uma experiência contínua.
Visão Geral da Arquitetura de Restauração
A aplicação segue uma abordagem de gerenciamento de estado em camadas, separando claramente o estado recuperável do estado temporário da interface. Os módulos centrais são:
- Android: Módulos como
core/dataecore/mainsão responsáveis pela persistência e gerenciamento de ciclo de vida. - iOS: Camadas de modelo em
Sources/Model/Entitye reducers definem estados serailizáveis. - Código Compartilhado (KMP): Contratos de estado e repositórios em
core/modelecore/commonasseguram consistência.
Implementação na Plataforma Android (Jetpack Compose)
No lado Android, a persistência é gerenciada principalmente através do SavedStateHandle dentro dos ViewModels e da API rememberSaveable nos Composables.
Um ViewModel de exemplo demonstra como salvar e restaurar o ID de uma sessão selecionada:
class ProgramDetailViewModel(
private val stateHolder: SavedStateHandle,
private val repository: ProgramRepository
) : ViewModel() {
val currentSession = stateHolder.getStateFlow<String?>("active_session_id", null)
fun onSessionSelected(id: String) {
stateHolder["active_session_id"] = id
// Dispara carregamento de detalhes...
}
}
Na camada de interface, componentes anotados com @Composable utilizam rememberSaveable para preservar estados de UI, como a expansão de uma seção:
@Composable
fun SessionAgendaView(
agendaId: String,
detailViewModel: ProgramDetailViewModel = hiltViewModel()
) {
val expandedItems = rememberSaveable { mutableListOf<String>() }
// Lógica do componente...
}
Implementação na Plataforma iOS (SwiftUI & Combine)
No iOS, o estado é modelado como structs Equatable e codificado para persistência. O framework Combine gerencia as atualizações.
Uma struct de estado para a agenda pode ser definida assim:
struct AgendaViewState: Codable, Equatable {
var chosenDay: EventDay
var focusedSessionKey: String?
var activeFilters: [FilterCategory]
var serialized: Data? {
try? JSONEncoder().encode(self)
}
static func deserialize(from data: Data) -> AgendaViewState? {
try? JSONDecoder().decode(AgendaViewState.self, from: data)
}
}
O reducer da aplicação centraliza as ações de salvar e restaurar o estado usando UserDefaults:
struct ApplicationReducer {
static func process(action: ApplicationAction, state: inout ApplicationState) -> Effect<ApplicationAction> {
switch action {
case .didLaunch:
if let persisted = UserDefaults.standard.data(forKey: "agenda_view") {
state.agendaView = AgendaViewState.deserialize(from: persisted) ?? .initial
}
return .none
case .agendaViewChanged(let viewState):
UserDefaults.standard.set(viewState.serialized, forKey: "agenda_view")
state.agendaView = viewState
return .none
}
}
}
Sincronização de Estado Entre Plataformas
O uso do Kotlin Multiplatform permite compartilhar lógica de validação e modelos de domínio. A sincronização é garantida por:
- Contratos Compartilhados: Interfaces e expectativas em código comum definem o comportamneto.
- Implementações Específicas: Cada plataforma provê sua implementação de persistência (Android: SavedStateHandle, iOS: UserDefaults).
- Camada de Repositório: Atua como ponto único de acesso, garantindo atomicidade nas operações de leitura/escrita do estado.
[Diagrama Conceitual: Fluxo de Sincronização de Estado entre Plataformas]
Estratégias de Teste e Validação
Suites de testes automatizados verificam a robustez da restauração em cenários desafiadores:
- Encerramento limpo do aplicativo.
- Terminação do prcoesso pelo sistema (memória baixa).
- Modo offline e perda de conectividade.
- Conflitos em cenários de sincronização em múltiplos dispositivos.
Um teste típico verifica se o estado é corretamente mantido após a "morte" e "recriação" do ViewModel:
@Test
fun session_focus_persists_across_recreation() = runTest {
val repository = FakeProgramRepository()
val stateStorage = SavedStateHandle()
val initialModel = ProgramDetailViewModel(stateStorage, repository)
initialModel.onSessionSelected("session_abc")
// Simular ciclo de vida, recriando o ViewModel com o mesmo stateStorage
val recreatedModel = ProgramDetailViewModel(stateStorage, repository)
assertThat(recreatedModel.currentSession.value).isEqualTo("session_abc")
}
Resumo de Padrões Recomendados
| Tipo de Estado | Padrão Recomendado | Exemplo no Projeto |
|---|---|---|
| Estado efêmero da UI (ex: aberta/fechada) | Android: rememberSaveable | iOS: @State |
expandedItems em SessionAgendaView |
| Estado de rota/tela | ViewModel + SavedStateHandle (Android) | Reducer + UserDefaults (iOS) | ProgramDetailViewModel (Android) |
| Estado global do aplicativo | Singleton Repository + Armazenamento persistente | ApplicationReducer (iOS) com UserDefaults |
| Estado compartilhado entre plataformas | Kotlin Multiplatform (definição comum) + implementações nativas | Modelos em core/model e lógica em core/common |