Flexbox Layout vs Constraint Layout no Android com Beagle
É muito provável, caso seja um desenvolvedor Android, que você já tenha usado Constraint Layout para posicionar componentes. E caso já tenha desbravado o mundo do desenvolvimento Web, certamente tenha esbarrado no posicionamento com Flexbox por aí. E esses dois possuem grandes diferenças na forma estruturar a tela.
Pasme você, mas também é possível posicionar com Flexbox no Android. Para isso existem algumas ferramentas que abstraem o uso desse modo, como por exemplo o FlexboxLayout da Google.
Neste tutorial usaremos um framework Server Driven UI (User Interface) fantástico chamado Beagle que permite, além de outras funções, criar views declarativas posicionando com Flex no Android.
Traçaremos um paralelo com Constraint Layout mostrando o passo a passo da construção de uma mesma tela de ambas as formas.
Para saber o passo a passo de como configurar o Beagle no seu projeto Android clique aqui!
Para você não ficar perdido, gostaria de reforçar alguns conceitos que usaremos aqui:
O que é Flexbox Layout?
É um conceito do CSS que visa organizar os elementos dentro de seus contêineres, que são como caixas, que podem agrupar views de forma hierárquica. Ou seja, independente das dimensões de tela, ele sempre manterá um layout flexível dentro do contêiner pai, reorganizando-se de acordo com a necessidade.
O que é Constraint Layout?
É uma forma de estruturação de UI que permite que você crie layouts grandes e complexos e sem a necessidade de encapsular vários layouts, uns dentro dos outros. Neste conceito, todas as views são dispostas de acordo com as relações entre views irmãs e o layout pai. Para definir a posição de uma view, você precisará adicionar ao menos uma restrição horizontal e uma vertical para a view. Cada restrição representa uma conexão ou alinhamento em relação a outra view.
O que é uma UI Declarativa?
É uma forma simplificada de escrever um layout com menos código, pois é projetada de forma que você descreve o que sua UI deve ter, passando os atributos necessários para definição, estilização e posicionamento de cada view. Porém as etapas de criação e de codificação, no momento de utilização dessas views, já estarão prontas e você não terá que se preocupar em como elas foram implementadas.
Aperte os cintos e bora codar! O blueprint acima é um spoiler mas agora oficialmente, vamos a tela que iremos trabalhar aqui:
A imagem colorida do lado direito é uma representação da tela do lado esquerdo e foi criada para ficar mais didático, facilitar a descrição e focarmos especificamente no posicionamento.
Portanto todas as views que usaremos neste tutorial são basicamente um componente de texto, que recebe um número, com uma cor de fundo e diferentes tamanhos e posições na tela.
Dito isto, vamos definir as views 1, 2 e 3 que formam o Container vermelho. No layout real são respectivamente o banner do filme, o botão de fechar e o botão de compartilhar.
Tanto no layout Flex quanto no ConstraintLayout vou usar um componente ScrollView como view raiz, ou seja a mais externa na hierarquia, para garantir que todos elementos da tela estejam visíveis, independente do tamanho da tela.
Layout Vermelho
Constraint Layout
Basicamente o que temos acima é uma ScrollView que ocupa todo espaço da tela em altura e largura devido aos seguintes valores:
android:layout_width="match_parent"
android:layout_height="match_parent"
E dentro do ScrollView temos um ConstraintLayout que tem três views filhas. São três TextView.
Como para o posicionamento precisamos definir pelo menos duas restrições, portanto:
No TextView com id android:id=“@+id/one_banner” e o com id android:id=“@+id/two_close” fixou-se baseado no pai. No início (lado direito) de seu pai app:layout_constraintStart_toStartOf=“parent” e no topo de seu pai app:layout_constraintTop_toTopOf=“parent”.
Já TextView com id android:id=“@+id/three_share” fixou-se com o fim (lado esquerdo) de seu pai app:layout_constraintEnd_toEndOf=“parent” e com o topo de seu pai app:layout_constraintTop_toTopOf=“parent”.
Neste caso o pai destes componentes de texto é o ConstraintLayout. O início e o fim coincidem com os lados esquerdo e direito da tela. E o topo do pai é o topo da tela.
Declarativo
No declarativo, usaremos a linguagem Kotlin. Este código pode ser definido em qualquer arquivo .kt no seu projeto Android e executado em um Fragment ou Activity.
Neste exemplo foi declarado no método onCreate, da nossa MainActivity o método declarative() que contém trecho de código abaixo:
startActivity(this.newServerDrivenIntent<AppBeagleActivity>(
PositioningBeagleBuilder.build()))
finish()
Onde o this.newServerDrivenIntent<AppBeagleActivity>(PositioningBeagleBuilder.build()) é um método para criar uma nova Intent do Beagle que iniciará uma nova Activity, que exibirá o layout declarativo que criaremos e depois finalizará a Activity atual.
O código declarativo que está sendo chamado, vem de uma classe object PositioningBeagleBuilder e é executado quando o método build() for chamado.
A classe object que foi chamada na MainActivity é essa acima. Essa é a estrutura base de toda essa nossa tela declarativa em Flex. O método build() recebe como retorno uma Screen que tem como filha uma ScrollView que recebe uma lista de filhos children que tem dentro todos os componentes da tela.
A seguir vamos nos ater apenas a detalhar a implementação do método red()
Este método red() retorna um Container que possui uma lista de filhos children e dentro tem três componentes Text.
Observe que para o Text com o valor “1” não foi definida nenhuma regra de posicionamento. Pois as regras default do Flex já o deixava onde queríamos.
Já para os Text com os valores “2” e “3” que estão na cor vermelho mais claro recebem parâmetros de posicionamento.
O Text com o valor “2” foi definido na sua posição default que é no topo do lado esquerdo.
Com margem entre a view e o topo de top = 16.unitReal() e entre a view e a esquerda de left = 16.unitReal().
margin = EdgeValue(top = 16.unitReal(), left = 16.unitReal())
O tipo de posicionamento foi PositionType.ABSOLUTE, o que permite a sobreposição de views.
O Text com o valor “3” foi definido na posição de right = 0.unitReal() de distância da direita e top = 0.unitReal() de distância do topo. O que o posiciona do lado oposto do Text “2”.
position = EdgeValue(right = 0.unitReal(), top = 0.unitReal())
Com uma margem entre a view e o topo de top = 16.unitReal() e entre a view e a direita de right = 16.unitReal().
margin = EdgeValue(top = 16.unitReal(), right = 16.unitReal())
O tipo de posicionamento também foi PositionType.ABSOLUTE, para sobrepor as views.
Layout Preto
Constraint Layout
Nos dois TextView acima foram usadas duas regras de posicionamento e uma outra regra de translação.
A propriedade android:translationY serve para determinar o posicionamento no eixo Y na tela que é a linha de movimentação vertical.
Colocando negativo android:translationY=”-25dp” estamos definindo que ele vai andar na vertical 25dp para cima em direção ao topo da página. E como essas views foram declaradas abaixo da view com id android:id=”@+id/one_banner” elas ficam por cima dessa view, que é o que precisamos.
Ademais disso, no TextView com o id android:id=”@+id/four_favorite” está aplicada uma regra alinha o fim (lado direito) da view, no começo da view (lado esquerdo) com o id android:id=”@id/five_vote_average”:
app:layout_constraintEnd_toStartOf=”@id/five_vote_average”
E outra regra que posiciona o topo dessa view na parte inferior da view com o id “@id/one_banner”:
app:layout_constraintTop_toBottomOf=”@id/one_banner”
Já para o TextView com o id android:id=”@+id/five_vote_average” está aplicada uma regra que alinha o fim (lado direito) da view com o fim (lado direito) da view pai.
E outra regra que posiciona o topo dessa view na parte inferior da view com o id android:id=”@id/one_banner”:
app:layout_constraintTop_toBottomOf=”@id/one_banner”
Declarativo
Os filhos desse Container não possuem definição de atributos de posicionamento, a não ser a margem direita de 16.unitReal() em relação ao pai.
Já o Container possui a definição de posicionamento à direita com o valor 0.unitReal() e além disso foi definido o PositionType.ABSOLUTE que além de permitir a sobreposição desse Container ao Text com valor “1”, esse também ignora a orientação do Container pai dele que é FlexDirection.ROW. Porém ele próprio aplica a seus filhos essa mesma propriedade que foi aplicada a ele e anulada pelo PositionType.ABSOLUTE. Colocando-os um ao lado do outro.
Layout Azul
Constraint Layout
Neste caso, o TextView com id android:id=”@+id/six_poster” recebeu duas regras de posicionamento, o começo (lado esquerdo) da view, foi alinhado com o pai: app:layout_constraintStart_toStartOf=”parent”
E a outra regra foi fixada para que o topo da view alinhe com a parte inferior do TextView com id android:id=”@+id/one_banner”:
app:layout_constraintTop_toBottomOf=”@id/one_banner”
O TextView com id android:id=”@+id/seven_watched” também possui a mesma regra alinhando o início da view no lado esquerdo do pai:
app:layout_constraintStart_toStartOf=”parent”
E como segundo critério de posicionamento foi fixada uma regra para que o topo dessa view alinhe com a parte inferior do TextView com id android:id=”@+id/one_banner”:
app:layout_constraintTop_toBottomOf=”@id/six_poster”
Declarativo
Antes de tudo vamos relembrar o layout e o trecho de código da estrutura da tela geral onde mostra onde está o posicionamento do azul:
Observe que o método blue() é chamado dentro de um Container que também possui os métodos pink() e black() como filhos. Esse Container possui uma regra de posicionamento no topo:
position = EdgeValue(top = UnitValue(-25.0, UnitType.REAL)),
Esse posicionamento negativo -25 possibilita que esse Container juntamente com os filhos ultrapassem a linha do topo a uma distância de UnitValue(-25.0, UnitType.REAL). Quando usamos o posicionamento negativo a view se movimenta no sentido contrário do habitual.
Note também que o para definição de um posicionamento positivo, o Beagle permite declarar dessa maneira encurtada: 25.unitReal() porém para declarar um valor negativo você precisará usar a versão completa UnitValue(-25.0, UnitType.REAL).
Além disso outra propriedade que influencia no posicionamento dos itens do Container é a flexDirection. Ao optar por FlexDirection.ROW você mudará a orientação dos filhos dentro do container. Ao invés de serem posicionados um embaixo do outro, serão posicionados um ao lado do outro.
Agora sim, podemos analisar o código do método blue()
Esse método devolve um Container contendo duas views filhas, que são o Text com o valor text = “6” e o com o valor text = “7”. O Container em si não possui nenhum critério de posicionamento, somente margem esquerda:
margin = EdgeValue(left = 16.unitReal())
No Text com o valor text = “6” foi aplicado PositionType.ABSOLUTE, para que ele sobreponha o Text com o valor text = “1”
O Text com o valor text = “7” recebeu o parâmetro position com o valor EdgeValue(top = 205.unitReal()). Devido ao text = “6” ter tamanho de altura de 200.unitReal() para que o text = “7” seja posicionado abaixo do text = “6” ele recebeu o valor de posicionamento de 205.unitReal(). Portanto ele ficou a 5.unitReal() abaixo do text = “6”. Isso foi necessário pois quando um elemento recebe o PositionType.ABSOLUTE, o Flex desconsidera a sua altura na hora de posicionar um outro componente abaixo.
Layout Rosa
Constraint Layout
Você pode ver neste caso que usei não apenas duas regras de posicionamento, mas três regras. E em todos os quatro TextView com background rosa, duas das três regras, estão em comum para todos. Elas dizem respeito ao começo e ao fim da view, ou seja o lado esquerdo e direito:
app:layout_constraintEnd_toEndOf=”parent”
app:layout_constraintStart_toEndOf=”@id/six_poster”
Portanto o fim (lado direito) de cada view deverá alinhar com o fim (lado direito) da view pai. Já o início da view (lado esquerdo) começará sempre a partir do fim(lado direito) da view com id android:id=”@id/six_poster”.
A regra de posicionamento que difere em cada TextView é sobre quem vai ficar embaixo de quem:
Declarativo
Continuamos dentro do Container que possuem os métodos black(), blue() e pink().
Depois de falar do blue(), seguimos para o pink():
Para os filhos dos Text com o background rosa acima não foram definidos atributos de posicionamento apenas uma margem inferior com o valor de 5.unitReal():
margin = EdgeValue(bottom = 5.unitReal())
Já no Container são usadas duas regras. Uma delas é o position que já usamos antes, posicionamos à distância de 80.unitReal() do topo para que esse Container esteja abaixo da altura do topo dos azuis (poster do filme e o botão de já assistido) e abaixo dos pretos (botão de favoritos e nota média do filme).
A outra propriedade, é legal explicar pois ainda não usamos anteriormente. Usamos o Flex que é uma propriedade de Style e nele atribuímos o parâmetro grow = 1.0. Basicamente o que esse parâmetro faz é preencher o espaço restante do pai horizontalmente para as views filhas desse Container. Então, todos os Text que estão dentro desse Container vão ajustar a largura de acordo com o tamanho restante disponível no pai. Semelhante ao que fazemos no ConstraintLayout ao atribuir o valor android:layout_width=”0dp” para uma view filha dele.
Com isso terminamos de descrever os três filhos definidos no Container que está dentro do ScrollView na estrutura inicial da página declarativa que foram os métodos black(), blue() e pink().
Layout Roxo
Constraint Layout
Ambos TextView acima possuem duas regras de posicionamento em comum, que são:
app:layout_constraintEnd_toEndOf=”parent”
app:layout_constraintStart_toStartOf=”parent”
Essas regras acima determinam que o começo e o fim ou seja os lados direito e esquerdo de cada view devem alinhar com os respectivos lados do pai.
Além dessas regras em comum, cada um tem uma regra que define quem vai ficar embaixo de quem:
Declarativo
Os Text dentro filhos desse Container não possuem regras de posicionamento. Apenas margem direita e esquerda no valor de 16.unitReal():
margin = EdgeValue(left = 16.unitReal(), right = 16.unitReal())
Já o Container em si tem uma regra de posicionamento no topo com o valor de UnitValue(-25.0, UnitType.REAL). Esse valor é para compensar a distância que o Container acima subiu negativamente no topo também. Caso contrário ficaria com a distância de 25.unitReal() do Container de cima e queremos apenas uma margem de 16.unitReal().
position = EdgeValue(top = UnitValue(-25.0, UnitType.REAL)),
margin = EdgeValue(top = 16.unitReal())
Outra forma de implementar, seria uma compensação somando aos -25 (position) + 16 (margin) e colocado apenas o
position = EdgeValue(top = UnitValue(-9.0, UnitType.REAL)) o que daria o mesmo resultado e não precisaria atribuir a margem.
Layout Marrom
Constraint Layout
Esse TextView simboliza a lista de atores do filme, e ele possui três regras de posicionamento, que são:
app:layout_constraintEnd_toEndOf=”parent”
app:layout_constraintStart_toStartOf=”parent”
Essas duas regras determinam que o começo e o fim ou seja os lados direito e esquerdo de cada view devem alinhar com respectivos lados do pai.
Além dessas duas, tem uma regra que define onde deve se posicionar verticalmente, pois esse TextView com o id android:id=”@+id/fifteen_list_cast” tem uma regra que define que a parte superior dessa view ficará embaixo da view com o id android:id=”@id/fourteen_trailer_button”:
app:layout_constraintTop_toBottomOf=”@id/fourteen_trailer_button”
Declarativo
Neste caso não temos nenhuma regra de posicionamento pois o comportamento default do Flex já posiciona da forma que desejamos. E não vamos fazer a compensação no topo com -25 pois queremos que esse Text esteja realmente a 25 de distância do Container anterior. Então foi atribuída apenas uma margem esquerda de 16.unitReal():
margin = EdgeValue(left = 16.unitReal())
Layout Verde
Constraint Layout
Chegamos as última views, então vamos ver a última parte do ConstraintLayout:
Essa última parte, tem dois TextView verdes. Eles representam no layout real os botões do Netflix e Spotify.
O TextView com o id android:id=”@+id/sixteen_netflix” possui quatro regras:
A primeira diz que a parte inferior desta view deve alinhar com a parte inferior do pai: app:layout_constraintBottom_toBottomOf=”parent”
A segunda diz que a parte fim (lado direito) dessa view deve alinhar com o começo da view com o id android:id=”@id/fifteen_list_cast”:
app:layout_constraintEnd_toStartOf=”@id/seventeen_spotify”
A terceira diz que a parte começo (lado direito) desta view deve alinhar com o começo (lado direito) do pai: app:layout_constraintStart_toStartOf=”parent”
A quarta diz que a parte superior dessa view deve alinhar abaixo da view com o id: android:id=”@id/fifteen_list_cast”
app:layout_constraintTop_toBottomOf=”@id/fifteen_list_cast”
Já o TextView com o id android:id=”@+id/seventeen_spotify” possui 3 regras.
O interessante desse TextView é que ele está posicionado somente com uma regra em relação ao pai e as outras duas são em relação a view com o id android:id=”@id/sixteen_netflix”
Essas são as regras:
app:layout_constraintBaseline_toBaselineOf=”@id/sixteen_netflix”
app:layout_constraintStart_toEndOf=”@id/sixteen_netflix”
A primeira linha diz que linha base dessa view deve ser alinhada verticalmente com a linha base da view com id android:id=”@id/sixteen_netflix”. E a segunda linha diz que o começo dessa view (lado direito) deve iniciar após o fim (lado esquerdo) da view do Netflix.
Resta então essa última regra:
app:layout_constraintEnd_toEndOf=”parent”
E ela diz que o fim (lado esquerdo) dessa view com id android:id=”@+id/seventeen_spotify” deve coincidir com o fim (lado esquerdo) da view pai.
Declarativo
Vamos explicar agora o último trecho do código declarativo:
Nenhum dos Text possui parâmetros de posicionamento. Mas fizemos algumas atribuições no Flex do Container pai, que influencia a forma de posicionar de seus filhos.
O FlexDirection.ROW como já disse antes muda o sentido de um embaixo do outro, para um do lado do outro.
Já o JustifyContent.SPACE_BETWEEN faz da largura deste Container ele posicione cada um dos dois filhos um em cada ponta da linha vertical. Criando um espaço entre eles.
E por fim o AlignSelf.CENTER coloca em relação ao pai esse Container e o que está dentro dele centralizado na tela horizontalmente.
Conclusão
Espero que com esse tutorial você que pretende usar o Beagle tenha entendido um pouco mais de como é o posicionamento declarativo em Kotlin com flex, que pode ser usando tanto no BFF (Backend for Frontend) no caso de uma aplicação Server Driven quanto diretamente no seu Front Android.
E também tenha percebido a diferença entre as duas formas de estruturar o Layout. Quem sabe até tenha aprendido um pouco mais de Constraint Layout que é muito popular numa aplicação Android nativa.
Para o código completo da aplicação acima segue o link do repositório caso queira brincar com ele: