Desenvolvendo soluções de Frontend por gerenciamento de estados

Guilherme Guimarães
17 min readNov 16, 2023

Cada vez mais percebe-se o interesse/necessidade em desenvolver interações e transições complexas em vários escopos de solução de Frontend. Já há alguns anos, se vê um crescente aumento no uso de implementações que usam de metodologias combinadas à gerenciamento de estados para resolver essas necessidade.

O objetivo desse material é apresentar fundamentos e conceitos sobre como desenvolver soluções usando gerenciamento de estados, independente da sua tecnologia de atuação, tendo referência dois exemplos: um mais simples e outro mais complicado.

O que são estados?

Estados são formas que um sistema que pode tomar, resultados de comportamentos implementados. Esses comportamentos são compostos por conjunto de ações (gatilhos / eventos / condições) que quando cumpridas resultam num estado.

Exemplo: Ao acordar, espero um tempo na cama até me sentir disposto, para então levantar.
Representação de um sistema de uma pessoa acordando depois de uma noite de sono: Ela passa pelo gatilho corporal de acordar, deixando-a “acordada”. Enquanto “acordada”, há um processo no corpo acontecendo de recuperação de disposição. Quando terminar esse processo de recuperação de disposição, emite um sinal para o cérebro e deixa a pessoa “disposta”. Somente quando estiver “disposta”, será habilitado a ação de levantar. Quando acionado a ação de levantar, a pessoa entra em um processo transitório de “levantando”, e quando termina de ficar de pé, esse indivíduo acaba esse processo “de pé”.

Qual é a proposta de soluções gerenciadas por estados?

A ideia central é implementar comportamentos através de relacionamentos bem estabelecidos e descritivos; desconstruindo-os entre combinações de gatilhos, eventos e condições que resultam em algo representável (os estados).

A maneira que vou utilizar para explicar como definir esses comportamentos vai ser através de BDD, esclarecendo as regras de negócio definidas nos exemplos. Existem métodos mais específicos para definir esses comportamentos, como diagrama de estados e tabelas de estado mas poderia ser muito complicado para quem quer simplesmente começar.

Quais artifícios computacionais são usados para implementar soluções baseadas em manipulação de estados em Frontend?

As soluções mais modernas para Frontend normalmente implementam um ecossistema responsivo que permite reatividade nas mudanças de valores de variáveis. As estruturas utilizadas para isso acontecer são:

  • Uma lista de estados e uma maneira de comportar os dados necessários para a mudança de cada
  • Um valor mutável observável, representando o estado atual
  • Um observador desse valor

Essa relação entre valor observável e observador são conceitos comuns para programação reativa, amplamente aplicados hoje em dia.

Fundamentalmente, o observador do valor fica de olho na variável observável para perceber mudanças; e quando acontecem, é delegado/executado funções (eventos) com objetivo de progredir no objetivo do sistema.

Mas como esses padrões de observação são implementados nas várias plataformas de Frontend disponíveis?

Em Android (Compose):

  • A propriedade observada é MutableState aplicado à função remember
  • O observador é uma função @Composable que repassa para seus componentes o valor atual e o onValueChange dessa propriedade

Referências: Documentação compose state

Em React:

  • A propriedade observável é um hook State (função useState)
  • O observador é uma função JSX que se modifica de acordo com mudanças do hook State

Referências: Documentação legado dos hooks
Documentação atual dos hooks

Em iOS (pre-SwiftUI):

  • A propriedade observada é uma variável RxSwift.BehaviorSubject ou Combine.Published,
  • O observador é uma UIViewController que vai executar/delegar mudanças

Referências: GitHub RxSwift
Documentação sobre como receber eventos usando Combine

Em SwiftUI:

Há duas opções nativamente disponíveis e ambas bastante utilizadas:

  • Variáveis locais com declaração @State permitem ser observadas pelo próprio dono, normalmente Views, que observam e executam mudanças de acordo.
  • Variáveis locais do tipo @ObservableObject, são observadas por qualquer SwiftUI.View com uso declarado @ObservedObject, que observam e executam mudanças de acordo.

Documentação: https://developer.apple.com/documentation/swiftui/model-data

Vale ressaltar que podem existir outras maneiras aplicar o conceito referido a partir de outros recursos em cada tecnologia, consulte a documentação e a comunidade sobre quais os approaches devem ser utilizados.

Em Flutter:

Há duas opções sugeridas pela comunidade e mantenedores da tecnologia:

  • Uma estrutura Provider que extende de ChangeNotifier e serve de estado. Os Widgets que fizerem uso desse Provider vão ser notificados das mudanças.
  • Padrão e framework BLoC, o mais popular da tecnologia para gerenciamento de estados.

Referências: Lista de estratégias para gerenciamento de estados
Documentação BLoC framework
BLoC explicado em detalhes pelo Sagar Suri

Agora chega de considerações e bora exercitar um pouco! Vamos implementar um exemplo em um pseudocódigo que mistura de muitas linguagens modernas. Afinal, o objetivo não é copiar o algoritmo, mas aprender os conceitos presentes no algoritmo que são amplamente aplicáveis.

Foi proposto para implementar uma lista genérica na home do seu aplicativo. E com isso, também foram colocadas algumas definições de negócio:

  • quando o aplicativo abrir, deve ser iniciado um processo de carregamento dos dados
  • enquanto esses dados forem carregados, a janela inteira deve ser tomada para mostrar o um processo de carregamento
  • quando terminar de carregar os dados, a tela de carregamento deverá sumir e mostrar conteúdo de acordo com resultado
  • se o carregamento dos dados der sucesso, deve ser preenchido uma lista de itens alimentados com os dados da requisição
  • se o carregamento dos dados der errado, o conteúdo da tela deve ser preenchido com uma tela de erro, que informa que houve erro no carregamento e um botão que permite retentar
  • quando o usuário acionar o botão de retentar, deve ser emitida a mesma ação de carregamento que quando o app abre

1- Definindo o estado inicial.

O estado inicial é a ponta de todo sistema com ciclo de vida longo. Em soluções pra Frontend, é nesse momento que deve ser feito todas as configurações iniciais. Em contextos mais simples, muito pouco normalmente tem de ser feito.

Para o exemplo: configurar a máquina de estados e implementando o estado inicial.

// definido o estado por um enumerador
enum ListaDaHomeState {
INICIAL
}

// código central da tela
class TelaListaDaHome {

// valor Observável do tipo ListaDaHomeState, com valor inicial INICIAL
// @observando é uma instrução para dizer que o dono 'TelaListaDaHome'
// está observando mudanças do valor de estado, e quando for alterado,
// ele computará o retorno novamente
//
@observando var estado: Observavel<ListaDaHomeState> = INICIAL

// usarei aspas "" para descrever instruções com implementação visual
// aqui deixei bem parecido com React
return "<TelaListaDaHomeRoot>"
switch estado {
case INICIAL: break // literalmente faz nada
}
"</TelaListaDaHomeRoot>"
}

2- Entendendo o próximo evento e estados resultantes.

O exercício de fazer coisas iterativas e incrementais é algo super compatível quando desenvolvendo Frontend por implementação de eventos e estados. O proximo comportamento a ser implementado é o de carregamento de dados. Esse evento pode ser percebido pelos seguintes momentos:

(1) mostrar o carregamento na tela (estado)
(2) disparar a ação de carregamento dos dados (ação)
(3) mostrar a lista carregada quando receber o retorno com sucesso (estado)
(4) mostrar a tela de erro quando receber o retorno com erro (estado)

Dentro desses momentos, somente o (1), (3) e o (4) representam algo para o usuário, ou seja, novos estados.

Pergunta: o (2) não seria um estado?
O (2) é um processo invisível ao usuário que é ilustrado de maneira genérica pelo carregamento da tela (1). Um jeito fácil de entender é supor que pudessem existir vários eventos que botassem a tela para carregamento.

Para o exemplo: (1) mostrar o carregamento na tela

enum ListaDaHomeState {
INICIAL,
CARREGANDO // novo estado
}

class TelaListaDaHome {

@observando var estado: Observavel<ListaDaHomeState> = INICIAL

// declaração da função que carrega lista de dados
// função `async` determina que ela seja assíncrona, ou seja,
// se precisasse aguardar o retorno deveria usar um operador `await`
// senão só é dispachado a função para background
async function carregarLista() {
// começa por declarar que o próximo estado é de carregando
estado.próximo(CARREGANDO)

// marcação de TODO para deixar marcado de onde os próximos passos partirão
// 'TODO: realizar chamada para carregar lista'
}

return "<TelaListaDaHomeRoot>"
switch estado {
// como não é necessário fazer outras configurações no passo inicial
// basta invocar o método que inicializará o carregamento da lista
case INICIAL: carregarLista()
// já pro carregamento, retorna o componente de carregamento genérico
// que deve ser componentizado no projeto
case CARREGANDO: "<CarregamentoGenerico />"
}
"</TelaListaDaHomeRoot>"
}

O resultado desse novo pseudocódigo deveria:

  1. Quando a tela TelaListaDaHome inicializar, ela passa pelo estado de INICIAL e durante a construção de sua UI, é invocado um método assíncrono carregarLista. Esse método não prende o fila de operações pois é assíncrono, então depois de computar todo o resto do retorno, esse método toma ação.
  2. Quando o método carregarLista for ativado, ele emitirá um novo estado CARREGANDO a partir da função proximo do tipo de dado Observavel<ListaDaHomeState>. E por TelaListaDaHome estar @observandoele como um estado, quando o valor dele for mudado, ele irá recompilar seu retorno.
  3. Quando a tela TelaListaDaHome for renderizar com o novo estado CARREGANDO, ele termina por mostrar o conteúdo do componente de CarregamentoGenerico.

Lembrando que isso é um pseudo-código, qualquer otimização que deveria existir na sua plataforma (por exemplo, memorização pelo memo e/ou repassar os dados sobre um context no React) deve ser considerado e aplicado na solução real.

Agora implementando o (2): disparar a ação de carregamento dos dados

enum ListaDaHomeState {
INICIAL,
CARREGANDO
}

class TelaListaDaHome {

@observando var estado: Observavel<ListaDaHomeState> = INICIAL

// declarando propriedade apiClient que contem as instruções para
// carregamento dos dados da lista
const apiClient = APIClient()

// como a função carregar lista já é assíncrona e quem invoca ela não aguarda
// seu retorno, ela já executa em background
async function carregarLista() {
estado.próximo(CARREGANDO)

// emitido a ação de carregamento da dados
// nota-se que apiClient.carregarDadosDaLista() tem um retorno do tipo
// Promessa<ListaDaHome>, onde promessa é um resultado que pode dar
// sucesso portando um dado de ListaDaHome, ou
// erro, portando uma estrutura de Erro na notação da tecnologia
//
// resultado não é implementado, ou seja, ficará para uma próxima

// 'TODO: implementar resolução da promessa
apiClient.carregarDadosDaLista()
}

return "<TelaListaDaHomeRoot>"
switch estado {
case INICIAL: carregarLista()
case CARREGANDO: "<CarregamentoGenerico />"
}
"</TelaListaDaHomeRoot>"
}

Pergunta: o que é promessa?
Promessa é tradução literal de Promise, um conceito amplamente aplicado em quase todas as tecnologias disponíveis hoje; que se resume à um tipo de dado abstrato que retornam um tipo de dado definido quando der sucesso, e quando falhar retornar uma estrutura de erro. É implementado também como “Result” em outras tecnologias, como iOS. Vai ser mais esclarecido nos próximos passos.

Agora implementando o (3): mostrar a lista carregada quando receber o retorno com sucesso

enum ListaDaHomeState {
INICIAL,
CARREGANDO,
LISTA_CARREGADA // novo estado
}

class TelaListaDaHome {

@observando var estado: Observavel<ListaDaHomeState> = INICIAL

// nova propriedade do tipo ListaDaHome, reservada para armazenas os itens
// do retorno do apiClient.carregarDadosDaLista
// a função dela é somente auxiliar a implementação do estado LISTA_CARREGADA
// então ela não deveria ser @observando e engatilhar algo sozinha
// `.vazio` é só uma notação para determinar que é um conjunto de dados vazio,
// poderia também ser nulo
var listaCarregada: ListaDaHome = .vazio

const apiClient = APIClient()

async function carregarLista() {
estado.proximo(CARREGANDO)

// usado de um método teórico `resolve`, que implementa instruções de código
// para o resultado dessa função de apiClient.carregarDadosDaLista
apiClient.carregarDadosDaLista().resolve {
// listaCarregada é um dado do tipo Lista da Home
// referente à Promessa<ListaDaHome>.sucesso declarada
sucesso: { listaDoRetorno
// preenchido os dados de listaCarregada baseado na listaDoRetorno
listaCarregada = listaDoRetorno
estado.proximo(LISTA_CARREGADA)
}
}
}

return "<TelaListaDaHomeRoot>"
switch estado {
case INICIAL: carregarLista()
case CARREGANDO: "<CarregamentoGenerico />"
// implementado o novo estado, fazendo uso de um componente
// `ComponenteListaDaHome` que consome diretamente uma ListaDaHome
// a partir de uma propriedade dados
case LISTA_CARREGADA: "<ComponenteListaDaHome dados={listaCarregada} />"
}
"</TelaListaDaHomeRoot>"
}

Pergunta: fazer esse negócio de listaCarregada para suplementar os dados de um estado parece estranho/mal feito.
Sim, e é. Cada tecnologia tem suas maneiras especializadas para fornecer dados de contexto quando implementando uma lógica baseada em estados. Sugiro consultar a documentação da sua tecnologia de atuação sobre qual a método mais refinado de repassagem de dados de contexto de um estado.

Pergunta: designar uma propriedade “vazia” ali para listaCarregada não é meio anti-pattern?
Sim. Eu só usei esse exemplo porque nem toda cultura de desenvolvimento tem o costume de implementar nulabilidade/opcionalidade. (por mais que deveria)

E finalmente (4): mostrar a tela de erro quando receber o retorno com erro

enum ListaDaHomeState {
INICIAL,
CARREGANDO,
LISTA_CARREGADA
ERRO_CARREGAMENTO // novo estado
}

class TelaListaDaHome {

@observando var estado: Observavel<ListaDaHomeState> = INICIAL

var listaCarregada: ListaDaHome = .vazio

const apiClient = APIClient()

async function carregarLista() {
estado.proximo(CARREGANDO)

apiClient.carregarDadosDaLista().resolve {
sucesso: { listaDoRetorno
listaCarregada = listaDoRetorno
estado.proximo(LISTA_CARREGADA)
},
falha: { erro // isso gera warning e será respondido em pergunta
estado.proximo(ERRO_CARREGAMENTO)
}
}
}

return "<TelaListaDaHomeRoot>"
switch estado {
case INICIAL: carregarLista()
case CARREGANDO: "<CarregamentoGenerico />"
case LISTA_CARREGADA: "<ComponenteListaDaHome dados={listaCarregada} />"
// implementado o novo estado, fazendo uso de um componente
// `ComponenteErroDaHome` que é padrão e não consome dados externos
case ERRO_CARREGAMENTO: "<ComponenteErroDaHome />"
}
"</TelaListaDaHomeRoot>"
}

Pergunta: e o erro ali que gera warning?
É só pra esclarecimento, aquilo é gerado um warning de variavel inutilizada. Toda promise quando falha ele retorna um dado de tipo Erro. Naquele caso, como não é usado o erro, o correto deveria ser não importar aquele dado pra função de resposta.

E então, acabou? Não. Terminamos de implementar quase tudo, mas faltou implementar a ação do botão de “Tentar novamente” do componente ComponenteErroDaHome. O nosso código final para a resolução do problema termina assim:

enum ListaDaHomeState {
INICIAL,
CARREGANDO,
LISTA_CARREGADA
ERRO_CARREGAMENTO
}

class TelaListaDaHome {

@observando var estado: Observavel<ListaDaHomeState> = INICIAL

var listaCarregada: ListaDaHome = .vazio

const apiClient = APIClient()

async function carregarLista() {
estado.proximo(CARREGANDO)

apiClient.carregarDadosDaLista().resolve {
sucesso: { listaDoRetorno
listaCarregada = listaDoRetorno
estado.proximo(LISTA_CARREGADA)
},
falha: {
estado.proximo(ERRO_CARREGAMENTO)
}
}
}

return "<TelaListaDaHomeRoot>"
switch estado {
case INICIAL: carregarLista()
case CARREGANDO: "<CarregamentoGenerico />"
case LISTA_CARREGADA: "<ComponenteListaDaHome dados={listaCarregada} />"
// vamos simplesmente repassar a função de carregarLista
// para o callback de click `aoClicar` para executar o mesmo processo
// de carregamento de dados
case ERRO_CARREGAMENTO: "<ComponenteErroDaHome aoClicar={carregarLista} />"
}
"</TelaListaDaHomeRoot>"
}

Pergunta: é correto repassar a mesma função (evento) que é engatilhada ao carregar o estado inicial no botão retentar?
Nesse caso, você tem todos os motivos para decidir que sim. É compatível com as definições de negócio e você não tem nenhum impedimento técnico sobre o uso. E por compartilhar, você mantem clareza processual sobre o que está acontecendo no sistema, sem gerar ruídos.

Um outro detalhe importante: perceba que a ação de clicar no botão de retentar não gerou um novo estado para a aplicação. Isso é porque ele já tinha um comportamento implementado com ações e estados que cumpriam essa função.

Está aqui também uma representação final das telas de acordo com cada estado:

Pronto, agora vamos resumir o que foi feito para a construção dessa funcionalidade a partir de manutenção de estados:

  1. Criado um estado inicial para efetuar preparações (no caso do exemplo, mas não limitado a, injetar um gatilho de evento para começar o ciclo de vida da aplicação).
  2. Separamos as regras de negócio por eventos e estados resultantes; especialmente designados para o escopo de definido em negócio.
  3. Estruturamos a pattern de Observer da seguinte forma:
    - ListaDaHomeState foi o modelo de dados que representa os estados do escopo.
    - estado foi a propriedade mutável e observável do tipo do ListaDaHomeState.
    - TelaListaDaHome observa esse estado e executa mudanças de acordo com o valor atual.

Pergunta: a lógica implementada no retorno da função do componente de TelaListaDaHome; é a melhor maneira?
Particularmente não é minha favorita, até porque prefiro programar de maneira mais imperativa. Mas hoje em dia, há uma quantidade massiva de desenvolvedores que preferem desenvolver UI declarativamente. Então fica a gosto do time de desenvolvimento e da orientação da plataforma.

Pergunta: a solução parece funcionar bem mas a arquitetura pouco refinada.
Verdade. Inclusive o pseudocódigo não faz uso de uma arquitetura. A intenção aqui é puramente descrever funcionalmente como uma solução por gerenciamento de estados funciona e como trabalhar de maneira iterativa com esse modelo.

Aqui vou deixar a sugestão de procurar, dentro da sua tecnologia, quais são as melhores maneiras de estruturar uma solução que faça uso disso.

Pergunta: É obrigatório que cada contexto exista somente uma estrutura de estados?
Eu acredito que não. Foi naturalizado que estruturas trabalharem com manipulação concorrente de vários escopos de estado, ao mesmo tempo; pois pragmaticamente, a cultura de componentização, as necessidades de negócio e preferencias da plataforma podem exigem uma maior divisão desses contextos.
Só para construir meu argumento, vou desenvolver um outro exemplo mais complexo, já com a implementação final, já pra ilustrar e descrevendo meu pensamento sobre a solução.

Foi proposto para implementar duas funcionalidades na home do seu aplicativo: um carrossel de banners e uma lista de itens. E com isso, também foram colocadas algumas definições de negócio:

  • quando o aplicativo abrir, deve ser iniciado o carregamento síncrono das duas fontes que alimentam a funcionalidade, mostrando o carregamento enquanto acontece.
  • todo o processo de carregamento deve acontecer sobrepondo o conteúdo da tela, e não escondendo ou substituindo
  • o processo de carregamento só acaba quando retornar as chamadas de ambas funcionalidades
  • se o carregamento dos dados do carrossel de banners der sucesso, deve surgir uma janela de banners no topo da tela
  • se o carregamento dos dados do carrossel de banners der errado, não deve surgir uma janela de banners no topo da tela
  • se o carregamento dos dados da lista de itens der sucesso, deve ser preenchido a tela com itens alimentados com os dados da requisição
  • se o carregamento dos dados da lista de itens der errado, o conteúdo da tela deve ser preenchido com uma tela de erro, que informa que houve erro no carregamento e um botão que permite retentar
  • uma vez que tenha carregado todos os dados, a cada 5 minutos, deve ser buscado uma atualização sobre os dados do banner,
  • se o carregamento da atualização dos dados do carrossel der sucesso, deve ser substituído o conteúdo dos banners com o atual
  • se o carregamento da atualização dos dados do carrossel der errado, deve ser mantido como estava antes
// já que o carregamento será um artifício que vai sobrepor as outras regras
// está sendo separado como um estado apartado
// nesse caso, assume-se que NAO_CARREGANDO é o estado inicial
// esse estado foi definido e incorporado no `ComponenteCarregamento`
// reduzindo a lógica de tratamento sobre esses estados
enum CarregamentoState {
NAO_CARREGANDO,
CARREGANDO
}

// esses estados serão objetivamente para a parte do banner
// nesse caso, assume-se que ESCONDIDO é o estado inicial
enum BannerDaHomeState {
ESCONDIDO
CARREGADO
}

// esse estado que na versão anterior da feature era o estado central
// agora é um dos estados que compõem a funcionalidade
// mas por conveniëncia, vou usar seu estado inicial para engatilhar
// o ecossistema da funcionalidade
enum ListaDaHomeState {
INICIAL
CARREGADO
ERRO_CARREGAMENTO
}

class TelaListaDaHome {

// em vez de ser um único estado observável, agora são três.
// ou seja, quando qualquer um desses mudar, o valor do retorno será
// recomputado
@observando var estadoCarregamento: Observavel<CarregamentoState> = NAO_CARREGANDO
@observando var estadoBanner: Observavel<BannerDaHomeState> = ESCONDIDO
@observando var estadoLista: Observavel<ListaDaHomeState> = INICIAL

// valores auxiliares para as listas
var bannersCarregado: BannersDaHome = .vazio
var listaCarregada: ListaDaHome = .vazio

const apiClient = APIClient()

// criado uma propriedade para ter uma referência ao disparador
// de atualizações do banner a cada 5 minutos
const disparador: Disparador

// agora foi definido uma função "carregar tudo"
// essa função será envocada por como resultado do estado inicial
async function carregarTudo() {
estadoCarregamento.proximo(CARREGANDO)

// aqui é feito uso de uma função hipotética `combinarCom`
// que permite combinar uma promessa com outra promessa de outro tipo
// e a função `resolve` não é desembrulhada, tratamos cada resultado
// da prometa iterativamente
apiClient.carregarDadosDoBanner()
.combinarCom(apiClient.carregarDadosDaLista())
.resolve { promessaBanner, promessaDadosDaLista
switch promessaBanner {
case .sucesso:
bannersCarregado = promessaBanner.dadosSucesso
estadoBanner.proximo(CARREGADO)
}

switch promessaDadosDaLista {
case .sucesso:
listaCarregada = promessaBanner.dadosSucesso
estadoLista.proximo(CARREGADO)
case .falha:
estadoLista.proximo(ERRO_CARREGAMENTO)
}

// engatilha o processo de atualização a cada
// 5 minutos
disparaAtualizarBannerACadaCincoMin()
}
}

// uma função definida para "carregarLista"
// onde será utilizada somente na ação de retentar carregar a lista
// no botão de ERRO_CARREGAMENTO
async function carregarLista() {
estadoCarregamento.proximo(CARREGANDO)

apiClient.carregarDadosDaLista().resolve {
sucesso: { listaDoRetorno
listaCarregada = listaDoRetorno
estado.proximo(LISTA_CARREGADA)
}
}
}

// uma função definida para disparar um processo repetitivo a cada 5 minutos
// para atualizar os dados do banner
async function disparaAtualizarBannerACadaCincoMin() {
// vamos definir Disparador como uma estrutura hipotética que
// que vai criar um disparador por critérios complementares
// o `somenteUmParaDono` diz que esse disparador só pode ser criado uma vez
// para o dono, é mandado uma referência da própria instância por `self`,
// ignoradas, e `aCada(minutos(5))` é uma configuração adicional que diz
// realizar a função encapsulada a cada 5 minutos
// o `ignorar(1)` é uma maneira de ignorar o disparo feito imediatamente
// quando invocado
disparador = Disparador.somenteUmParaDono(self)
.aCada(minutos(5))
.ignorar(1) {
apiClient.carregarDadosDoBanner().resolve {
sucesso: { bannersDoRetorno
bannersCarregado = bannersDoRetorno
// na suposição que essa tecnologia só recomputa estados que
// alteram, definindo função hipotética `forcarProximo`
// que vai recomputar forçadamente o estado, ainda que fosse o mesmo
// anteriormente.
estadoBanner.forcarProximo(CARREGADO)
}
}
}
}

// um callback de objetos de UI que é bem comum existir em quase toda
// tecnologia, que permite preparar a instância antes de ser desconstruida.
// na grande maioria das vezes, pra ser usada como "coletor de lixo"
// estamos usando para esse mesmo motivo, forçando a parada desse gatilho
// que poderia estar definido se os banners tivessem sido carregados
naDeinicialização {
disparador?.parar()
}

return "<TelaListaDaHomeRoot>"
"<CarregamentoComponente estado={estadoCarregamento}>"
// aqui o layout é carregado baseado em duas estruturas
// em cima há o banner, e abaixo a mesma lista da funcionalidade anterior
if estadoBanner == CARREGADO {
"<ComponenteBannerDaHome dados={bannersCarregado}"
}

switch estadoLista {
case INICIAL: carregarTudo()
case CARREGADO: "<ComponenteListaDaHome dados={listaCarregada} />"
case ERRO_CARREGAMENTO: "<ComponenteErroDaHome aoClicar={carregarLista} />
}
"</CarregamentoComponente>"
"</TelaListaDaHomeRoot>"
}

No final, temos uma representação mais orgânica de todos os estados que são possível de acontecer, e cumprindo **todas** as regras de negócio propostas:

Pergunta: essa solução não poderia ter sido feita por uma única estrutura de estados?
Poderia sim; mas não sugiro. Perceba que tanto o carregamento, os banners e lista trabalham de maneira independente, “escopos” diferentes. Se for para encapsular tudo proposto, você teria de literalmente fazer os oito estados resultantes, como demonstra a representação, além de misturar bastante a lógica individual de cada contexto ao ponto que não justifica desenvolver por manipulação de estados; além de perder a reusabilidade de contextos como o ‘carregamento’. Mesmo que se for pra encapsular somente os banners e a lista, por mais que fosse mais plausível ainda não preferiria.

Pergunta: a função carregarTudo ainda é engatilhada no meio da lógica de construção da tela. Isso é a maneira correta de fazer?
Eu não diria que é correto ou incorreto, mas é uma maneira de fazer. Provavelmente há tecnologias que se prejudicam muito desse tipo de implementação, e nesse caso realmente não deveriam ser feitas assim.
Só garanta que os gatilhos estejam bem reservados para acontecer somente quando for preciso.

Pergunta: existem padrões de desenvolvimento que utilizam de manipulação de estados como parte central?
Sim, e existem várias! As duas mais populares são:
-> máquina de estados (fsm) , um dos padrões mais tradicionais em Engenharia da Computação; defende um modelo de transição e repassagem de contexto centralizada. Muito eficiente nos dias de hoje. O motivo de não ser tão popular, mesmo hoje em dia, é pela complexidade de adequação ao padrão/estrutura.
-> state pattern: um padrão moderno que permite os estados comunicar seus controladores da intencionada ação, e o controlador entende as circunstâncias e executa ações de acordo. É mais efetivo quando os estados operam poucas ações e preferencialmente compartilhadas.

Concluindo, quais os benefícios de soluções gerenciadas por eventos e estados?

  • Clareza: por conta de materializar eventos e estados no código fonte, é bem mais fácil de ler e entender o código fonte. É natural que fique mais verboso também, comparado à outras estratégias mais puramente declarativas.
  • Redução de defeitos: por separar de maneira bem definida os comportamentos e estados resultantes, você garante melhor a responsividade sobre cada comportamento e o que deveria resultar; assim ocorrendo menos erros.
  • Escalabilidade: outro benefício por separação bem definida entre os comportamentos, é que a adição de novos e remoção de outros tendem a ser mais prático.
  • Maior tamanho e melhor performance: normalmente, soluções que integram com programação de estados resultam em apps maiores só que mais rápidos, porque seu software é maior só que mais objetivo na resolução do algoritmo.

Obrigado pela leitura, espero ter colaborado e tenha um bom dia.

--

--