Tornando nossos aplicativos do Android Studio reativos aos componentes de interface do usuário e ao Redux

Blog da Tecnologia Netflix em Netflix TechBlog Seguir 30 de maio · 6 min ler

Por Juliano Moraes , David Henry , Corey Grunewald e Jim Isaacs

Recentemente, a Netflix começou a criar aplicativos móveis para levar tecnologia e inovação para a Studio Physical Productions , a parte responsável pela produção de nossos programas de TV e filmes.

Nosso primeiro aplicativo para dispositivos móveis é chamado de Prodicle e foi desenvolvido para Android e iOS usando a mesma arquitetura reativa em ambas as plataformas, o que nos permitiu criar dois aplicativos do zero em três meses com quatro engenheiros de software.

O aplicativo ajuda as equipes de produção a organizar seus dias de filmagem através de marcos de filmagem e mantém todos em uma produção informados sobre o que está acontecendo atualmente.

Aqui está um dia de filmagem para o Glow Season 3.

Estamos experimentando uma ideia para usar componentes reativos no Android nos últimos dois anos. Embora existam alguns frameworks que implementam isso, queríamos ficar muito próximos do framework nativo do Android. Foi extremamente importante para a equipe que não mudamos completamente a forma como nossos engenheiros escrevem código Android.

Acreditamos que os componentes reativos são a base fundamental para obter interfaces UIs compostos, escaláveis, reutilizáveis, testáveis em unidades e compatíveis com testes AB. UIs compostos contribuem para a rápida velocidade de engenharia e produzem menos bugs de efeito colateral.

Nossa interface de usuário atual no aplicativo Netflix para Android está usando nossa primeira iteração dessa arquitetura de componentização . Aproveitamos a oportunidade para criar o Prodicle para aprimorar o que aprendemos com a interface do Player e criar o aplicativo do zero usando o Redux , o Components e o 100% Kotlin.

Arquitetura Geral

Fragmentos e Atividades

– Fragmento não é sua opinião.

Ter grandes Fragmentos ou Atividades causa todos os tipos de problemas, o que dificulta a leitura, a manutenção e a extensão do código. Mantê-los pequenos ajuda com encapsulamento de código e melhor separação de interesses – a lógica de apresentação deve estar dentro de um componente ou uma classe que representa uma exibição e não no Fragment.

É assim que um Fragmento limpo aparece em nosso aplicativo, não há lógica de negócios. Durante o onViewCreated passamos por contêineres de visualização pré-inflados e pela função de despacho do armazenamento global do redux.

Componentes de UI

Os componentes são responsáveis por possuir seu próprio layout XML e se inflar em um contêiner. Eles implementam uma única interface de renderização (state: ComponentState) e têm seu estado definido por uma classe de dados Kotlin .

O método de renderização de um componente é uma função pura que pode ser facilmente testada criando uma permutação de possíveis variações de estados.

As funções de despacho são a maneira como os componentes disparam ações para alterar o estado do aplicativo, fazer solicitações de rede, se comunicar com outros componentes etc.

Um componente define seu próprio estado como uma classe de dados na parte superior do arquivo. É assim que sua função render () será invocada pelo loop de renderização.

Ele recebe um contêiner do ViewGroup que será usado para inflar o próprio arquivo de layout do componente, R.layout.list_header neste exemplo.

Todas as visualizações do Android são instanciadas usando uma abordagem preguiçosa e a função de renderização é aquela que definirá todos os valores nas visualizações.

Layout

Todos esses componentes são independentes por design, o que significa que eles não sabem nada um do outro, mas de alguma forma precisamos organizar nossos componentes em nossas telas. A arquitetura é muito flexível e oferece diferentes maneiras de alcançá-lo:

  1. Auto Inflação em um Contêiner : Um Componente recebe um ViewGroup como um contêiner no construtor, ele se infla usando o Layout Inflater. Útil quando a tela tem um esqueleto de contêineres ou é um layout linear.
  2. Visualizações pré-infladas. Component aceita uma View em seu construtor, não há necessidade de inflar. Isso é usado quando o layout é de propriedade da tela em um único XML.
  3. Auto Inflação em um Layout de Restrição : Os componentes se inflar em um Layout de Restrição disponível em seu construtor, ele expõe um getMainViewId a ser usado pelo pai para definir restrições programaticamente.

Redux

O Redux fornece uma arquitetura de fluxo de dados unidirecional orientada a eventos através de um estado de aplicativo global e centralizado que só pode ser mutado por Ações seguidas por Redutores . Quando o estado do aplicativo muda, ele cai para todos os componentes inscritos.

Ter um estado de aplicativo centralizado torna a persistência de disco muito simples usando a serialização. Ele também fornece a capacidade de retroceder ações que afetaram o estado gratuitamente. Depois de persistir o estado atual no disco, o próximo lançamento do aplicativo colocará o usuário exatamente no mesmo estado em que estavam antes. Isso elimina a necessidade de todo o clichê associado ao onSaveInstanceState () e onRestoreInstanceState () do Android.

O Android FragmentManager foi removido em favor da navegação gerenciada pelo Redux. As ações são disparadas para Push, Pop e Definir a rota atual. Outro componente, NavigationComponent , ouve alterações no backStack e manipula a criação de novas telas.

O loop de renderização

Render Loop é o mecanismo que percorre todos os componentes e invoca o component.render () se for necessário.

Os componentes precisam se inscrever para alterações no estado do aplicativo para ter seu render () chamado. Para fins de otimização, eles podem especificar uma função de transformação contendo a parte do App State com a qual se importam – o uso de selectWithSkipRepeats impede chamadas de renderização desnecessárias se uma parte do estado mudar com a qual o componente não se importa.

O ComponentManager é responsável por inscrever e cancelar a assinatura de Componentes. Ele estende o Android ViewModel para persistir o estado na alteração da configuração e tem uma associação 1: 1 com as telas ( fragmentos ). Ele está ciente do ciclo de vida e desinscreve todos os componentes quando onDestroy é chamado.

Abaixo está nosso fragmento com suas assinaturas e funções de transformação:

O código do ComponentManager está abaixo:

Visualizações do reciclador

Os componentes devem ser flexíveis o suficiente para trabalhar dentro e fora de uma lista. Para trabalhar em conjunto com a implementação recyclerView do Android, criamos um UIComponent e UIComponentForList , a única diferença é que o segundo estende um ViewHolder e não se inscreve diretamente no Redux Store.

Aqui está como todas as peças se encaixam.

Fragmento:

O Fragment inicializa um MilestoneListComponent assinando-o no Store e implementa sua função de transformação que definirá como o estado global é convertido para o estado do componente.

Listar Componente:

Um Componente de Lista usa um adaptador personalizado que suporta vários tipos de componentes, fornece diff assíncrono na thread de segundo plano através da interface adapter.update () e invoca a função render () do componente de item durante onBind () do item da lista.

Componente da lista de itens:

Os componentes de lista de itens podem ser usados fora de uma lista, eles se parecem com qualquer outro componente, exceto pelo fato de que UIComponentForList estende a classe ViewHolder do Android. Como qualquer outro componente, ele implementa a função de renderização com base em uma classe de dados de estado definida por ele.

Testes Unitários

Os testes de unidade no Android são geralmente difíceis de implementar e demoram para serem executados. De alguma forma, precisamos zombar de todas as dependências – Atividades, Contexto, Ciclo de Vida, etc, para começar a testar o código.

Considerando que nossos componentes são métodos puros, podemos facilmente testá-lo criando estados sem nenhuma dependência adicional.

Neste exemplo de teste de unidade, inicializamos um Componente de UI dentro do before () e para cada teste, invocamos diretamente a função render () com um estado que definimos. Não há necessidade de inicialização de atividades ou qualquer outra dependência.

Conclusão e Próximos Passos

A primeira versão do nosso aplicativo usando essa arquitetura foi lançada há alguns meses e estamos muito felizes com os resultados alcançados até agora. Provou ser compostável, reutilizável e testável – atualmente, temos 60% de cobertura de teste de unidade.

O uso de uma abordagem de arquitetura comum permite que nos movamos muito rapidamente, fazendo com que uma plataforma implemente um recurso primeiro e o outro siga. Uma vez que a camada de dados, lógica de negócios e estrutura de componentes é descoberta, torna-se muito fácil para a plataforma a seguir implementar o mesmo recurso traduzindo o código de Kotlin para Swift ou vice-versa.

Para abraçar completamente essa arquitetura, tivemos que pensar um pouco fora dos paradigmas fornecidos pela plataforma. O objetivo não é lutar contra a plataforma, mas sim suavizar algumas arestas.