Máquina de Estados

Nota honesta antes de começar: o InkShelf não tem uma máquina de estados formal em lugar nenhum do código — nenhuma classe sealed modela estados como variantes explícitas com transições nomeadas. O que existe é estado informal: flags booleanas e campos mutáveis, atualizados diretamente conforme eventos acontecem. Este documento descreve como esse estado informal se comporta nos dois lugares onde o app mais se parece com uma máquina de estados de fato — a varredura da biblioteca e a sessão de leitura — e o padrão geral usado no resto da interface.

Varredura da biblioteca: um flag global

ScanState é um singleton com um único StateFlow<Boolean> (isScanning), lido por qualquer tela que precise saber se uma varredura está rodando no momento (ver Scanner) e escrito no início/fim de cada varredura. Não há estados intermediários (ex.: "varrendo pasta X de Y") — é binário: escaneando ou não.

Sincronização remota: dois flags de app inteiro, não por tela

Fontes remotas (Komga/Kavita, ver Fontes Remotas) têm dois estados globais próprios, independentes do ScanState local: RemoteSyncState (quais sourceId têm uma sincronização em andamento no momento — mostrado como spinner no card daquela fonte) e RemoteAvailabilityState (mapa sourceId -> Boolean, alimentado por um poll a cada 30s enquanto a tela de Configurações de Biblioteca está visível, puramente informativo pro "led" de disponibilidade — nunca afeta se uma sync ou download prossegue). Nenhum dos dois é hierárquico; ambos são mapas simples atualizados por chave.

Sessão de leitura: o caso mais próximo de uma máquina de estados real

Embora não seja uma classe sealed, o ciclo de vida de uma sessão de leitura segue uma progressão clara, com estado guardado em campos privados do ViewModel do leitor:

  1. Início — ao carregar um arquivo, uma nova sessão começa: horário de início, contadores de páginas/passos lidos zerados, e o ponto mais distante alcançado é registrado.
  2. Progresso — cada mudança de página/capítulo atualiza os contadores, mas só quando o usuário avança além do ponto mais distante já alcançado naquela sessão. Reler páginas anteriores (voltar) não conta como progresso novo — o rastreamento é monotônico em relação ao ponto mais longe já visto, não ao ponto atual.
  3. Fim da sessão — acontece ao carregar outro arquivo (a sessão anterior é finalizada primeiro) ou ao destruir o ViewModel. A sessão finalizada é persistida como um registro de Estatísticas, numa coroutine que roda fora do ciclo de vida normal do ViewModel (não-cancelável), para sobreviver mesmo que o ViewModel seja destruído no meio do processo de gravação.
  4. Conclusão do arquivo — independente do fim da sessão: acontece quando a página/capítulo atual chega ao final e o arquivo ainda não estava marcado como lido. Nesse momento o arquivo é marcado como lido, o cache de página é liberado, o lembrete de retomar leitura é cancelado, e a sugestão de próximo arquivo é agendada (ver Notificações).

Sessão e conclusão são conceitos independentes: é possível encerrar uma sessão sem concluir o arquivo (leu um pouco e fechou), e é possível concluir o arquivo no meio de uma sessão que continua (a sessão só termina quando o usuário sai ou troca de arquivo).

Padrão geral: UiState por tela, sem hierarquia de estados

Cada ViewModel de tela expõe um único data class XxxUiState (não uma hierarquia sealed), com campos como isLoading: Boolean e error: String? misturados aos dados já carregados, atualizado via MutableStateFlow.update { }. Não existe uma distinção formal entre "carregando", "vazio", "erro" e "com dados" como estados mutuamente exclusivos — a tela decide qual visual mostrar combinando esses campos na hora de renderizar (ver, por exemplo, os três estados de carregamento distintos em Tela Biblioteca, montados a partir da combinação de isLoading/isInitializing/lista vazia, não de um enum de estado único).