Construindo um projeto Android com UnidirectionalDataFlow e JetpackCompose

Lucas C. Wottrich
Android Dev BR
Published in
7 min readJan 27, 2022

Olá! Lucas Wottrich aqui.

Depois de um artigo sobre porque é recomendado utilizar Jetpack Compose com UnidirectionalDataFlow(UDF) vamos para um papo mais prático sobre como aplicar o UDF em um projeto feito em Compose.

Achei importante e interessante fazer esse artigo pois acompanho alguns projetos que utilizam esse design pattern e todos tem uma forma diferente de resolver as coisas, aqui quero trazer um pouco do que estudei e ando utilizando nos meus projetos.

Para esse artigo construí dois projetos de exemplo, um escrito em Compose e outro utilizando XML, para vermos a diferença que temos entre as frameworks, mas nesse artigo em questão estaremos olhando para o projeto em Compose.

O que é importante saber para entender o artigo:

  • Noção básica sobre UnidirectionalDataFlow
  • Noção básica sobre Jetpack Compose
  • Noção básica sobre Flow, SharedFlow e StateFlow

⚠️ Disclaimer ⚠️
Antes de começarmos é interessante entender que não existe verdade absoluta sobre como construir, utilizar e ou arquitetar um Fluxo de dados unidirecional, isso sempre vai depender do contexto onde você vai estar inserido, UDF pode ser dito como um conceito que pode ser interpretado e utilizado de múltiplas maneiras. Igual o desenho que temos no artigo que falamos sobre UnidirectionalDataFlow + Compose (imagem abaixo), o que vamos ver aqui é uma ideia construída a partir de alguns estudos sobre o assunto, se você tem opiniões diferentes sinta-se a vontade para fazer um comentário!

Examplos de UnidirectionalDataFlow

State, Actions & Effects ⛱

Para começarmos precisamos entender que a ideia desse artigo é fazer o UnidirectionalDataFlow dividir o app em 3 áreas principais:

  • State: Responsável por segurar a ultima informação persistida na tela. Ele vai representar, em forma de data class, o que a view deve mostrar.
  • Actions: Consistem em interações do usuário ou algum evento do ciclo de vida do Android.
  • Effects: Ocorrências únicas de UI que não persistem, como navegação ou dialogs (os efeitos vêm de eventos do usuário)

A partir desses pontos mencionados acima que vamos definir como será construída nossa arquitetura de eventos unidirecionais.

Olhado de um ponto de vista mais macro nosso projeto vai ficar dessa forma:

Detalhando a implementação 🍍

Vamos conversar um pouco sobre essas escolhas.

Se você já trabalhou um pouco com Compose + UDF já deve ter passado na sua cabeça:

Como eu faço para emitir uma ação de navegação utilizando apenas uma model que faz a conversa entre a minha ViewModel e o meu código Compose?

Como eu faço para emitir um snackbar?

Como eu faço para responder um ação do usuário só depois de uma resposta de requisição?

Se você pensou em utilizar um LiveData, você não está errado, mas com isso você teria que elevar o nível de implementação da sua ViewModel para a activity e assim observar o evento e dessa forma você está saindo da função Composable, o que acaba complicando um pouco se você estiver utilizando Navigation Components.

Ou se você quer emitir um snackbar que precisa do scaffoldState e também evitar Side-effects de navegação/visualização.

Bom, já sabemos onde vamos parar: muito código para resolver algo simples.

🕵 O fim do LiveData?

Então agora começamos a repensar no que devemos utilizar no nosso código, já que LiveData vai dar tanto trabalho assim e temos um aliado muito forte que faz a mesma coisa pela metade do esforço.

A partir desse ponto podemos começar a falar sobre as Actions.

Actions 📬

Como podemos ver na visão macro do nosso projeto as actions só vão em direção a nossa ViewModel e isso tem um porem.

🚦Pending actions

As nossas ações vão ser apenas representadas por interações do usuário ou eventos do ciclo de vida do Android. E por não sabermos quando isso vai acontecer iremos colocar isso em uma fila de ações, para serem executadas assim que possível. Para isso iremos utilizar nosso grande aliado da vez: o MutableSharedFlow.

MutableSharedFlow consegue manter no seu Buffer ações suficientes para não perdemos nenhuma dela e ainda é thread-safe.

Ok! Mas se não são as ações que vão comunicar navegações, snackbars, dialogs, etc… o que vai?

Effects ⚡️

Effects serão nossas actions do futuro. Como dito antes os effects são ocorrências únicas de UI que não persistem, como navegação ou dialogs (os effects vêm de eventos do usuário, como podemos ver na imagem acima).

Se você pensar bem isso pode lembrar muito o que o SingleLiveEvent faz para o LiveData limitando ele a emitir um evento por vez.

E nesse caso não vai ser diferente, vamos ter o SingleShotEventBus que, diferente do MutableSharedFlow, tem uma capacidade definida para não ter Buffer, logo, não manter valores.

Na função composable que vamos encontrar a maior mudança, pois iremos utilizar o LaunchedEffect do Compose para evitar Side-effects e poder usar suspend functions.

No parametro de entrada chamado key1 do LaunchedEffect iremos passar nosso uiEffects, isso faz com que o LaunchedEffect reinicie toda vez que o uiEffects mudar.

Agora já temos nossas actions e effects, mas nada disso faz sentido sem a cereja do bolo.

States 👥

Ao pensar em desenvolver a classe do seu state você primeiro precisa entender todas as necessidades da tela e a partir disso saber transformar essas necessidades em states imutáveis que você irá utilizar para mudar o que você acha necessário.

Vamos exercitar isso, olhando para imagem abaixo como vamos estruturar nossa class de state?

A resposta para essa pergunta vai depender do que você quer controlar.

Um exemplo é o botão, se você quer que ele esteja sempre habilitado você não precisa controlá-lo, logo não precisa dele na sua classe de state. (Apenas um exemplo, ainda vamos considerar o estado do botão)

Para o TextField é interessante criarmos uma variável para controlar o estado do texto, a mensagem de erro e se o campo está habilitado ou não.

Podemos imaginar que essa tela vai ter um loading para quando fizermos alguma requisição, e queremos controlar isso também.

Com essas informações podemos começar a construir nossa classe:

Para informar nossa função composable da troca de estados vamos utilizar o StateFlow/MutableStateFlow, com ele conseguimos coletar as informação como States do compose utilizando a função collectAsState().

Com a nossa classe de state definida temos que fazer ela trabalhar juntamente com a nossa UI.

Antes de olharmos para a UI é interessante entender que é recomendado sempre isolar os códigos de state em funções stateless em vez de funções stateful. Isso torna o código mais testável, legível e diminui a chance de bugs e Side-effects. Observe onde utilizamos nosso InitialUiState no código abaixo:

Fácil, não? Então vamos entender como isso está funcionando na ViewModel.

♻️ Atualizando estados e mantendo os dados

Já vimos que estamos utilizando MutableStateFlow para emitir valores para nossa UI e por isso a pergunta da vez é:

Como atualizar uma informação da tela sem perder outra?

Como estamos utilizando apenas uma classe essa pergunta é super válida e fácil de responder.

Já vi alguns lugares utilizarem sealead class para representar state da tela, é uma solução possível mas eu particularmente acho difícil manter states dessa forma.

Como vimos nossa classe de state é um data class e ela nos ajuda a responder essa pergunta.

Para manter os dados e alterar apenas o que for necessário vamos utilizar a função copy().

👷 Mãos a obra

Na nossa classe de state temos um parâmetro de isLoading e podemos usar ele como exemplo.

Vamos pensar que o botão de confirmar foi pressionado, por causa dessa ação do usuário precisamos alterar o estado de isLoading para true e manter os estados do TextField e do botão sem alterações, com isso temos o seguinte código acontecendo:

O que esperamos que aconteça na tela é apenas um loading aparecendo, sem mais alterações.

🍥 Conclusão

Nessa conclusão espero que você tenha entendido as ideias trabalhadas nesse artigo sobre como estruturar um fluxo UDF e com ele consegue alterar states da tela, emitir effects e receber actions em compose.

Lembrando que qualquer duvida, pensamentos ou ideia, comente para conversarmos sobre!

🎉 Obrigado!!

Chegamos no fim desse artigo, muito obrigado por ter separado esse tempo do seu dia para ler :)

Se você gostou e acha que mereço o seu 👏(clap), por favor, sinta-se a vontade. ❤️

Tenha um bom resto de dia!
Até a próxima.

--

--

Lucas C. Wottrich
Android Dev BR

Android Developer at iFood. Descomplicando códigos e ajudando a comunidade a crescer com conteúdos pt-BR.