Portabilidade Flutter: Adaptabilidade e Responsividade

Um framework, mais de 5 plataformadas

Igor L Sambo💙🇲🇿
GDG Maputo
9 min readJan 20, 2023

--

Photo by Anastase Maragos on Unsplash

A comunidade Flutter cresce a cada dia e igualmente o suporte que o framework tem com as várias plataformas que usamos no nosso dia-á-dia, desde dispositivos móveis — smartphones —, desktops/laptops á sistemas embarcados. Com isso surge uma enorme necessidade de antes de mais, definir que plataforma(s) ou utilizadores pretende-se atingir com a solução por desenvolver e a partir desse ponto, surgem alguns questionamentos extra, como por exemplo, como garantir uma experiência similar (boa experiência de utilizador) nas plataformas escolhidas sem ferir a“design language” (mais sobre isto adiante) da plataforma e ainda os diferentes tamanhos que estas plataformas podem apresentar.

É com intuito de o ajudar a responder alguns destes questionamentos que lhe trago este artigo, para discutir dois pontos importantes, a adaptabilidade e responsividade das nossas aplicações Flutter a medida que nos deparamos com cenários em que devemos segmentar diferentes plataformas ou dispositivos. Vamos?

Portabilidade Flutter, como assim?

A medida que o Flutter foi se desenvolvendo várias plataformas foram tendo suporte, o que iniciou com Android e iOS, foi para Web, Desktop (MacOS, Linux, Windows) e até sistemas embarcados, o que faz do mesmo um framework portável, pela vasta gama de plataformas que consegue atingir, a partir de um mesmo conjunto de ferramentas e código.
Portanto, é também importante escrever o código a contar com a portabilidade do mesmo, das funcionalidades do projecto, target entre as várias plataformas.

Para isso dois conceitos são relevantes saber:

Adaptabilidade

Adaptabilidade em desenvolvimento de sistemas representa o quão um sistema consegue adaptar-se ao ambiente ao qual está a operar sem precisar de intervenção externa. No caso de uma aplicação desenvolvida em Flutter parte de algumas actividades simples como garantir que o código é escrito de modo que a cada plataforma que for a correr ele está de acordo com a design language adoptada pela plataforma, ou pelo menos “aceite”, considerando outros factores como a forma como os inputs são geridos em diferentes plataformas, a titúlo de exemplo o drag-and-drop em plataformas desktop, comportamento baseado no mouse, scrollbar, entre outros. Mas o que seria uma design language?

Design Language

Design language ou linguagem de design é um conjunto de regras e príncipios adoptados para garantir que haja coerência no desenvolvimento de produtos (digitais ou não) entre os vários intervenientes em uma organização ou comunidade. Um exemplo disto que eu aprecio bastante fora do mundo de desenvolvimento são as várias design languages presentes no mundo automotivo, desde os traços rectos da Volvo, a grelha icónica das BMW (borboleta), às curvas da Hyundai, entre outras.

Para o desenvolvimento de aplicativos móveis, temos obviamente o Material Design, que nos é trazida pela Google e mais virada a integração com dispositivos Android, Cupertino para iOS — Human Interface Guidelines genericamente para os OS X — , Ubuntu Brand Guidelines para Linux e Fluent para Windows.
É importante ter estes conceitos todos em mente e antes de inciar o desenvolvimento começar a colocá-los na mesa, de modo que, ao segmentar uma plataforma específica, vá de acordo com as regras e princípios definidos, isso não muda o sentido das funcionalidades, mas torna o uso da aplicação mais natural para o utilizador na plataforma a qual tem acesso.

@override
Widget build(BuildContext context) {
return Container(
height: 50,
width: MediaQuery.of(context).size.width,
child: Platform.isAndroid
? AlertDialog(...
)
: CupertinoAlertDialog(...
),
);
}

Conforme podemos ver, a partir do nosso código conseguimos ter uma lógica (assumindo que trabalhamos Android e iOS) que nos permitem segmentar alguns widgets de modo que se adaptem à plataforma que o utilizador está a usar, permitindo uma integração perfeita à plataforma e ainda uma experiência perfeita ao utilizador.

Em suma…
Para aplicar o conceito da adaptabilidade basta-nos simplesmente manter consciência que estamos a lidar com diferentes público-alvos e desenvolver a solução a pensar na melhor experiência para o utilizador na plataforma que se encontra a usar. Com base na definição incial e no exemplo demonstrado acima, podemos também verificar que não é algo que o utilizador vai poder necessariamente interagir, quanto menos ele prestar atenção ao detalhe, melhor foi a nossa integração, pois, é uma espécie de camuflagem, que permite que ele foque mais em usar as funcionalidades que necessariamente procurar entender como a aplicação funciona.

Responsividade

Para quem já trabalhou em sistemas web, de certeza está bem familiarizado com este conceito. Ou então no sistema de linhas usado pelo bootstrap, que facilita o processo de responsividade entre os diferentes tamanhos de telas.

Fonte: https://www.bitdegree.org/learn/bootstrap-grid-system

De modo geral, responsividade refere-se a capacidade de reagir rápido e positivamente à alguma mudança, e no caso, estamos lidando com mudança de dimensões do ecrã que acontece muitas das vezes sistemas web, que podem ser usados em versões desktop ou móveis.
Com Flutter tendo suporte a Web, a regra é igualmente válida, embora, o framework não possui o sistema de linhas nativamente, oferece recursos que podemos usar de modo a garantir que a regra é usada desde Widgets que nos permitem segmentar os widgets em diferentes tamanhos e/ou layouts.

De acordo com a documentação oficial do flutter temos duas formas principais de atingir a responsividade: usando o LayoutBuilder ou MediaQuery.of() — irei aprofundar mais adiante — , que são ambas recursos directos do framework.

Eu gostaria de apresentar aqui mais propostas e dividir em duas categorias:

1. LayoutBuilder, MediaQuery.of() e outros

Esta categoria engloba, como indiquei antes, recursos do framework.
Vamos começar discutindo um pouco sobre as estratégias LayoutBuilder e MediaQuery.of(), ambos permitem ter os componentes renderizados de acordo com os requisitos. Entretanto, enquanto o LayoutBuilder é um widget e depende das dimensões do widget pai, o MediaQuery.of() é um metódo que depende inteiramente do contexto, e nos dá o tamanho e orientação do contexto completo da aplicação.

Salientar, que o Flutter não tem uma abordagem 100% nativa para responsividade e devemos considerar sempre definir quantos ecrãs queremos atingir independentemente da estratégia a usar, ou seja, quais intervalos, devendo, na maioria dos casos considerar pelo menos 3:

  • Smartphones: Dispositivos com ecrãs inferiores a 600px;
  • Tablets: Dispositivos com ecrãs iguais ou superiores a 600px e inferiores a 1024px; Podem também ser smartphones com ecrãs relativamente grandes ou laptops com ecrãs relativamente pequenos;
  • Laptops: Dispositivos com ecrãs iguais ou superiores a 1024px.

Nota: Podem ser consideradas mais variações.

LayoutBuilder

O LayoutBuilder, ou seja, a classe LayoutBuilder possui uma função builder que funciona de maneira inteligente, pois, ela não só pega as dimensões do widget pai, como também é capaz de validar se as mesmas forão alteradas desde o último build e só com condição verdadeira ela volta a ser acionada e possui o BoxConstraints para permitir-nos tomar decisões com base nos diferentes constraints (limites) do widget pai e de facto atingir o resultado esperado (responsividade), através deste conseguimos saber informação como altura máxima (do widget pai), minima, assim como a largura máxima e mínima do mesmo.

Por ele depender do widget pai, existem algumas nuances a considerar, para melhor perceber, consideremos os casos abaixo:

Caso 1: LayoutBuilder na raíz

Sendo widget pai do LayoutBuilder, o Scaffold, ele pega as medidas do dispositivo como constraints.

Caso 2: Com widget pai e dimensões fixas

Quando colocado como dependente de um widget com dimensões fixas, ele toma estas como constraints a usar dentro do builder.

Caso 3: Com widget pai e sem dimensões fixas

Widgets como Columns e Rows não possuem algumas dimensões fixas (acho que já sofremos com os overflows por isso), sendo que não possuem altura e largura fixa, respectivamente.

Como podemos ver, resulta em resultados indesejados, para o caso de uma Row, teriamos largura infinita.

Em suma, o uso de LayoutBuilder é dependente do widget antecedente a ele, pelo que, é necessário garantir que usamos devidamente, ou seja, o widget pai ou antecedente tenha dimensões fixas.

Agora que temos uma noção de como o LayoutBuilder funciona, o que fazemos com ele? Bom, peguemos nas medidas que registamos e iniciemos o processo.

import 'package:flutter/material.dart';

const int largeScreenSize = 1024;
const int mediumScreenSize = 600;




class ResponsiveWidget extends StatelessWidget {
final Widget largeScreen;
final Widget mediumScreen;
final Widget smallScreen;


const ResponsiveWidget({
Key key,
//neste exemplo consideramos versão desktop como default, por isso @required
@required this.largeScreen,
this.mediumScreen,
this.smallScreen,
}) : super(key: key);


@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth >= largeScreenSize) {
return largeScreen;
} else if (constraints.maxWidth < largeScreenSize &&
constraints.maxWidth >= mediumScreenSize) {
return mediumScreen ?? largeScreen;
} else {
return smallScreen ?? largeScreen;
}
},
);
}
}

Esta é a nossa classe responsável por controlar a responsividade do nosso aplicativo, com recurso ao LayoutBuilder, devendo se colocar os widgets de acordo com a disposição de cada classe de ecrã, ou seja, é importante criar os widgets de modo que adaptem-se aos ecrãs ou então widgets específicos para cada intervalo, ou pelo menos para os que consideramos acrescentar complexidade desnecessária ao widget principal.

E a implementação fica da seguinte forma:

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ResponsiveWidget(
largeScreen: LargeScreen(),
smallScreen: smallScreen()
),
);
}
}

MediaQuery.of()

Como deixamos claro acima, a abordagem do MediaQuery, irá retornar as dimensões do dispositivos em qualquer local do código em que for chamado, bastando apenas que se passe o contexto.

print(MediaQuery.of(context).size.width);              
print(MediaQuery.of(context).size.height);


//podemos definir funções para retornar as dimensões
double screenWidth(BuildContext context){
return MediaQuery.of(context).size.width;
}

double screenHeight(BuildContext context){
return MediaQuery.of(context).size.height;
}

E podemos ter uma implementação similar a do LayoutBuilder em termos de lógica, entretanto, o build da nossa tela ficaria:


import 'package:flutter/material.dart';

const int largeScreenSize = 1024;
const int mediumScreenSize = 600;

class ResponsiveWidget extends StatelessWidget {
final Widget largeScreen;
final Widget mediumScreen;
final Widget smallScreen;

const ResponsiveWidget({
Key key,
//neste exemplo consideramos versão desktop como default, por isso @required
@required this.largeScreen,
this.mediumScreen,
this.smallScreen,
}) : super(key: key);

@override
Widget build(BuildContext context) {
if (screenWidth >= largeScreenSize) {
return largeScreen;
} else if (screenWidth < largeScreenSize &&
screenWidth >= mediumScreenSize) {
return mediumScreen ?? largeScreen;
} else {
return smallScreen ?? largeScreen;
}
},
);
}
}

Ainda nesta categoria podemos ter outras várias abordagens, com destaque para o OrientationBuilder que ajuda-nos a saber a orientação do widget e passar o widget que irá adequar-se a orientação do dispositivo.
Ver mais abordagens [en].

2. Packages

É também possivel usar packages para obter os mesmos resultados, um dos mais usados é o sizer , que nos dá a facilidade de passar toda a nossa aplicação (MaterialApp) como widget filho do ResponsiveSizer, permitindo que de forma mais rápida, tornemos a mesma responsiva, embora, seja importante salientar que é mais para facilitar a configuração de dimenões para diferentes dispositivos; a criação de widgets a serem populados mantêm-se responsabilidade do desenvolvedor. Pode ver mais clicando aqui ou pelo link directo para o pub.dev.

Por favor, partilhe com seu grupo do Flutter💙, e deixe ficar qual abordagem prefere? Ou se já combinou algumas? Quais?

Pode ver este repositório do Santos Enoque para um exemplo mais completo usando LayoutBuilder.

Espero que tenha aprendido com este artigo e que se tenha divertido enquanto lia.
Se tiver gostado, deixe ficar a sua reação 👏🏾. E fique a vontade para sugerir um tema que gostaria de ver.

Agradecia também que deixasse ficar o seu comentário, assim como sugestões para próximos tópicos a abordar, assim como sua curiosidade, podendo fazê-lo pela caixa de comentários 💬, ✉️ igorlsambo1999@gmail.com ou 🐦 @lsambo02.

Obrigado por acompanhar até ao fim e espero por você no próximo artigo!

--

--