Arquitetura e design de aplicativos Android (minha experiência)

Habr, olá!

Hoje eu quero falar sobre a arquitetura que sigo em meus aplicativos Android. Tomo a arquitetura limpa como base e uso os componentes da arquitetura Android (ViewModel, LiveData, LiveEvent) + Kotlin Coroutines como ferramentas. Anexado é um código de exemplo fictício que está disponível no GitHub .

aviso Legal


Quero compartilhar minha experiência de desenvolvimento, de maneira nenhuma pretendo que minha solução seja a única verdadeira e desprovida de falhas. A arquitetura do aplicativo é um tipo de modelo que escolhemos resolver um problema específico e, para o modelo selecionado, é importante sua adequação na aplicação a uma tarefa específica.

Problema: por que precisamos de arquitetura?


A maioria dos projetos dos quais participei tem o mesmo problema: a lógica do aplicativo é colocada dentro do ambiente do Android, o que leva a uma grande quantidade de código dentro do Fragment and Activity. Assim, o código é cercado por dependências que não são necessárias, o teste de unidade se torna quase impossível e a reutilização. Fragmentos se tornam objetos de Deus ao longo do tempo, até pequenas mudanças levam a erros, apoiar um projeto se torna caro e emocionalmente caro.

Existem projetos que não possuem arquitetura (tudo está claro aqui, não há perguntas para eles), existem projetos com uma reivindicação de arquitetura, mas exatamente os mesmos problemas aparecem lá de qualquer maneira. Está na moda usar a Arquitetura Limpa no Android. Vi muitas vezes que a Arquitetura Limpa se limita à criação de repositórios e scripts que invocam esses repositórios e não fazem mais nada. Pior ainda: esses scripts retornam modelos dos repositórios chamados. E nessa arquitetura não há sentido algum. E porque Como os scripts simplesmente chamam os repositórios necessários, geralmente a lógica se baseia no ViewModel ou, pior ainda, se instala em fragmentos e atividades. Tudo isso se transforma em uma bagunça, não passível de teste automático.

O objetivo da arquitetura e design


O objetivo da arquitetura é separar nossa lógica de negócios dos detalhes. Por detalhes, quero dizer, por exemplo, APIs externas (quando desenvolvemos um cliente para um serviço REST), Android - um ambiente (UI, serviços) etc. No núcleo, eu uso a arquitetura Limpa, mas com minhas suposições de implementação.

O objetivo do design é unir a interface do usuário, a API, a lógica de negócios e os modelos, para que tudo isso se preste a testes automáticos, seja fracamente acoplado e se estenda facilmente. No design, eu uso os Componentes da Arquitetura do Android.

Para mim, a arquitetura deve atender aos seguintes critérios:

  1. A interface do usuário é o mais simples possível e possui apenas três funções:
  2. Apresentar dados para o usuário. Os dados estão prontos para exibição. Esta é a principal função da interface do usuário. Aqui estão widgets, animações, fragmentos, etc.
  3. . ViewModel LiveData.
  4. . framework, . .
  5. - . .


O diagrama esquemático da arquitetura é mostrado na figura abaixo:

imagem

Estamos passando de baixo para cima em camadas, e a camada abaixo não sabe nada sobre a camada acima. E a camada superior refere-se apenas a uma camada um nível abaixo. Essa. A camada da API não pode se referir a um domínio.

Uma camada de domínio contém uma entidade comercial com sua própria lógica. Geralmente, existem entidades que existem sem um aplicativo. Por exemplo, para um banco, pode haver entidades de empréstimo com lógica complexa para calcular juros, etc.

A camada lógica do aplicativo contém scripts para o próprio aplicativo. É aqui que todas as conexões do aplicativo são determinadas, sua essência é construída.

A camada da API, o Android é apenas uma implementação específica de nosso aplicativo no ambiente Android. Idealmente, essa camada pode ser alterada para qualquer coisa.

, , — . . 2- . , . . TDD , . Android, API ..

Android-.

imagem

Então, a camada lógica é a chave, é a aplicação. Somente uma camada lógica pode se referir e interagir com um domínio. Além disso, a camada lógica contém interfaces que permitem que a lógica interaja com os detalhes do aplicativo (API, Android, etc.). Esse é o chamado princípio da inversão de dependência, que permite que a lógica não dependa de detalhes, mas vice-versa. A camada lógica contém os cenários de uso do aplicativo (Casos de Uso), que operam em diferentes dados, interagem com o domínio, repositórios, etc. No desenvolvimento, eu gosto de pensar em scripts. Para cada ação ou evento do usuário do sistema, é lançado um determinado script que possui parâmetros de entrada e saída, além de apenas um método - para executar o script.

Alguém introduz um conceito adicional de interator, que pode combinar vários cenários de uso e encerrar lógica adicional. Mas eu não faço isso, acredito que cada script pode expandir ou incluir qualquer outro script, isso não requer um interator. Se você observar os esquemas UML, poderá ver as conexões de inclusão e extensão.

O esquema geral do aplicativo é o seguinte:

imagem

  1. Um ambiente android é criado (atividade, fragmentos etc.).
  2. Um ViewModel é criado (um ou mais).
  3. O ViewModel cria os scripts necessários que podem ser executados neste ViewModel. É melhor injetar cenários com DI.
  4. O usuário confirma uma ação.
  5. Cada componente da interface do usuário está associado a um comando que pode ser executado.
  6. Um script é executado com os parâmetros necessários, por exemplo, Login.execute (login, senha).
  7. DI , . ( api, ). . , , , REST JSON . , , . , . , . - . , . , , . , , .
  8. . ViewModel, UI. LiveData (.9 10).

Essa. o papel principal que temos é a lógica e seu modelo de dados. Vimos uma conversão dupla: a primeira é a transformação do repositório no modelo de dados do cenário e a segunda é a conversão quando o script fornece os dados ao ambiente como resultado de seu trabalho. Normalmente, o resultado do script é fornecido ao viewModel para exibição na interface do usuário. O script deve retornar esses dados com os quais o viewModel e a UI não fazem mais nada.

Comandos A

interface do usuário inicia a execução do script usando um comando. Nos meus projetos, uso minha própria implementação de comandos, eles não fazem parte dos componentes da arquitetura ou de qualquer outra coisa. Em geral, sua implementação é simples, como um entendimento mais profundo da ideia, você pode ver a implementação de comandos em reactiveui.netpara c #. Infelizmente, não posso definir meu código de trabalho, apenas uma implementação simplificada por exemplo.

A principal tarefa do comando é executar algum script, passando os parâmetros de entrada para ele e, após a execução, retornar o resultado do comando (dados ou mensagem de erro). Normalmente, todos os comandos são executados de forma assíncrona. Além disso, a equipe encapsula o método de cálculo em segundo plano. Eu uso corotinas, mas elas são fáceis de substituir pelo RX, e você só precisa fazer isso no grupo abstrato de comandos + casos de uso. Como um bônus, uma equipe pode relatar seu status: se está sendo executado agora ou não e se pode ser executado em princípio. As equipes resolvem facilmente alguns problemas, por exemplo, o problema de chamada dupla (quando o usuário clicou no botão várias vezes enquanto a operação está em execução) ou os problemas de visibilidade e cancelamento.

Exemplo


Implementar recurso: faça login no aplicativo usando login e senha.
A janela deve conter os campos de login e senha e o botão "Login". A lógica do trabalho é a seguinte:

  1. O botão "Login" deve ficar inativo se o nome de usuário e a senha contiverem menos de 4 caracteres.
  2. O botão "Login" deve estar inativo durante o processo de login.
  3. Durante o procedimento de login, um indicador (carregador) deve ser exibido.
  4. Se o login for bem-sucedido, uma mensagem de boas-vindas deve ser exibida.
  5. Se o login e / ou senha estiverem incorretos, uma mensagem de erro deve aparecer acima do campo de login.
  6. Se uma mensagem de erro for exibida na tela, qualquer entrada de um caractere nos campos de login ou senha removerá essa mensagem até a próxima tentativa.

Você pode resolver esse problema de várias maneiras, por exemplo, colocando tudo em MainActivity.
Mas eu sempre sigo a implementação das minhas duas regras principais:

  1. A lógica de negócios é independente dos detalhes.
  2. A interface do usuário é o mais simples possível. Ele lida apenas com sua tarefa (ele apresenta os dados que foram transferidos para ele e também transmite comandos do usuário).

É assim que o aplicativo se parece:

imagem

MainActivity se parece com isso:

class MainActivity : AppCompatActivity() {

   private val vm: MainViewModel by viewModel()

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       bindLoginView()
       bindProgressBar()

       observeAuthorization()
       observeRefreshView()
   }

   private fun bindProgressBar() {
       progressBar.bindVisibleWithCommandIsExecuting(this, vm.loginCommand)
   }

   private fun bindLoginView() {
       loginEdit.bindAfterTextChangedWithCommand(vm.loginValidityCommand)
       passwordEdit.bindAfterTextChangedWithCommand(vm.passwordValidityCommand)

       loginButton.bindCommand(this, vm.loginCommand) {
           LoginParameters(loginEdit.text.toString(), passwordEdit.text.toString())
       }
   }

   private fun observeAuthorization() {
       vm.authorizationSuccessLive.observe(this, Observer {
           showAuthorizeSuccessMsg(it?.data)
       })
       vm.authorizationErrorLive.observe(this, Observer {
           showAuthorizeErrorMsg()
       })
   }

   private fun observeRefreshView() {
       vm.refreshLoginViewLive.observe(this, Observer {
           hideAuthorizeErrorMsg()
       })
   }

   private fun showAuthorizeErrorMsg() {
       loginErrorMsg.isInvisible = false
   }

   private fun hideAuthorizeErrorMsg() {
       loginErrorMsg.isInvisible = true
   }

   private fun showAuthorizeSuccessMsg(name : String?) {
       val msg = getString( R.string.success_login, name)
       Toast.makeText(this, msg, Toast.LENGTH_LONG).show()
   }
}


A atividade é bastante simples, a regra da interface do usuário é executada. Escrevi algumas extensões simples, como bindVisibleWithCommandIsExecuting, para associar comandos a elementos da interface do usuário e não a códigos duplicados.

O código deste exemplo com comentários está disponível no GitHub , se estiver interessado, você pode baixar e se familiarizar.

Isso é tudo, obrigado por assistir!

All Articles