Flutter Ready to Go (flavors, conectividade e mais) — ptBr

Julio Henrique Bitencourt
Flutter — Comunidade BR
12 min readMar 15, 2019

--

A ideia desse artigo é apresentar a vocês um projeto (GitHub) que pode servir como uma arquitetura básica de organização e com os recursos que geralmente são utilizados no desenvolvimento de aplicações em Flutter, levando em consideração uma abordagem mais profissional durante o desenvolvimento, principalmente para o trabalho em equipe. Portanto, bastaria um fork e algumas adaptações para sair utilizando em qualquer projeto. Então vamos lá, sem muita enrolação.

Índice

  1. Flavors
  2. Flavors e Dart
  3. Identificando visualmente cada flavor
  4. Identificanto dados do device com nosso Banner
  5. Flavors no android
  6. Ícones do app baseado no flavor
  7. Nome do app baseado no flavor
  8. Rodando os flavors pela IDE
  9. Usando valores específicos do flavor
  10. Controlando erros de conexão

Esse post foi originalmente escrito em blog.juliobitencourt.com e para uma melhor leitura aconselho ir lá, uma vez que o medium é muito limitado.

Flavors:

Nas empresas geralmente quando trabalhamos em equipe, temos um processo de desenvolvimento bem definido do ciclo de vida de construção de um software, onde as funcionalidades passam pelas mais variadas etapas como a equipe de desenvolvimento, a equipe de testes, a equipe de QA (dependendo da empresa as nomenclaturas podem mudar) e finalmente vai para produção. E pode acontecer de cada uma dessas etapas haver algo diferente, como a URL de uma API utilizada para desenvolvimento e outra para etapa de QA, que consequentemente é diferente da API de produção.

Esse é o conceito de Flavors para o Android (ou schemes no iOS)! Onde disponibilizaríamos um flavor (uma versão) do nosso App que para algumas situações se comporta de forma diferente. Imagine a situação onde nosso App possui uma versão free e outra versão paga, cada uma poderia ser um flavor. Mas no nosso caso levaremos em consideração as etapas da produção de um app.

O SDK Flutter por si só tem seus build modes, no qual poderíamos fazer algo diferente dependendo o modo em que nosso app está rodando, são eles:

  • Debug: É o único modo que podemos rodar em emuladores, conhecido pelo banner debug apresentado ao rodar o app. As assertions no código são habilitadas, assim como o observatory. Possui um maior tamanho de pacote final gerado, uma vez que é otimizado apenas para o desenvolvimento. Para rodar basta o comando flutter run.
  • Profile: Ainda mantém algumas funcionalidades de debug, só pode ser executado em devices físicos, para manter a performance real. Para rodar basta flutter run --profile.
  • Release: Também roda apenas em devices físicos, é o modo em que usamos para gerar o pacote final para as lojas, pois é otimizado para execução e tamanho final dos binários, gerado através do flutter run --release ou flutter build.

Acontece que quando queremos disponibilizar o app para testes, ou para equipe de QA, é interessante que ele esteja em modo Release, para que a performance seja otimizada e igual ao do usuário final. Uma combinação dos flavors com build modes é perfeita nesse caso.

Flavors em Dart:

Para a definição dos flavors em dart e utilização dos mesmos em todo o código, começamos com o arquivo flavor_config.dart:

No emun Flavor declaramos os flavors que iremos utilizar, assim como FlavorValues será responsável por manter todos os valores específicos para cada flavor, como urls ou nomes de database, não é recomendado utilizá-la para guardar dados sensíveis como tokens de acesso, para isso deve ser usado algo como o flutter_secure_storage.

FlavorConfig é a classe responsável por manter o estado de configuração do nosso flavor. A mesma poderia ter sido definida como um InheritedWidget, mas a ideia é que possamos acessá-la de qualquer lugar de nosso app para validar qual flavor estamos rodando no momento, nesse caso estaríamos ferindo a regra principal de abstração de responsabilidades ao acessar um widget no bloc, ou até mesmo a dificuldade de ter que ficar passando como parâmetro essa configuração por todas as camadas do nosso app. Por conta disso defini a classe como sendo um singleton, facilmente acessada de qualquer lugar.

Agora para cada um dos flavors definimos um arquivo main, o qual utilizaremos para dar o start no app.

  • main_dev.dart : Nosso flavor para o desenvolvimento.
  • main_qa.dart : Nosso flavor para a equipe de qa.
  • main_production.dart: Nosso flavor para o produto final.

No main_qa.dart acima utilizamos o FlavorConfig criado anteriormente para definir o flavor, uma cor personalizada, e os valores. O mesmo é feito para o main_dev.dart e o main_production.dart.

Em app.dart criamos o MyApp que simplismente irá chamar nossa HomePage. Essa por sua vez utiliza o FlavorConfig para imprimir o flavor sendo utilizado. Ao rodar o app então definimos qual o flavor desejado:

  • flutter run -t lib/main_qa.dart
  • flutter run -t lib/main_dev.dart
  • flutter run -t lib/main_production.dart

No comando acima estamos rodando o app em modo debug, em -t ou --targetdefinimos o ponto de partida, arquivo que iniciará o app.

Identificando visualmente cada flavor:

Seria incrível identificar visualmente qual flavor nosso app está rodando, sem a necessidade de imprimir um texto com o nome do mesmo, não seria? Bom, temos um ótimo exemplo disso com o build mode debug do flutter, onde é exibido um Banner no canto superior direito do nosso app. Porque não usar este mesmo conceito com nossos flavors? Mãos a obra então.

Nosso FlavorBanner utiliza uma Stack para posicionar o banner acima de seu widget child e caso estivermos rodando em produção, obviamente não mostraremos nenhum banner. BannerConfig possui o label e cor, e com a ajuda de um CustomPaintpodemos utilizar o BannerPainter (o mesmo utilizado para o banner debug) para desenhar nosso banner no topo esquerdo do nosso app.

Envelopando o Scaffold da HomePage chegamos ao resultado:

Identificando dados do device com nosso Banner:

Não seria ainda mais incrível, se ao clicarmos em nosso banner, abrisse um dialog com informações do device rodando o app? Claro que seria! Com isso nossa equipe de testes ou QA poderia colher informações mais precisas em casos de algum comportamento diferente em algum smartphone. Vamos utilizar para isso o plugin device_info.

Nossa classe DeviceUtils contém 3 métodos utilitários, androidDeviceInfo() e iosDeviceInfo() irão basicamente pegar através do plugin, as informações de cada plataforma. Já o currentBuildMode() irá nos ajudar a identificar em qual build mode do flutter estamos rodando o app, então, se o ambiente conter a flag dart.vm.product significa que estamos em RELEASE, se conseguirmos rodar e validar um assert() significa que estamos em DEBUG, caso contrário estaremos rodando em PROFILE.

Com DeviceInfoDialog iremos basicamente validar em qual plataforma estamos rodando através de Platform.isAndroid ou Platform.isIOS, e baseado nesse resultado utilizamos o método correspondente do DeviceUtils para carregar as informações do device. É importante salientar que as informações adquiridas da plataforma android são diferentes do iOS, acima eu omiti o conteúdo do iOS por simplicidade, mas o exemplo pode ser visualizado completo no github.

Com todo esse setup feito, agora só adicionamos um GestureDetector no nosso FlavorBanner para identificar um longPress e abrir nosso dialog.

E agora a mágica acontece:

FlavorBanner Dialog.

Pronto, agora com um simples toque longo no nosso banner, identificamos se estamos rodando em um device físico, qual a plataforma, modelo, fabricante, versão do OS, entre outros. Isso pode não parecer tão útil, mas lembre-se, nosso app pode estar sendo testado por uma outra equipe de QA, da maneira que eles quiserem, e qualquer coisa, nós como devs podemos apenas pedir um print do dialog de informações do device.

Flavors no android:

Ainda que os flavors apenas no dart já ajudem bastante, pode ser interessante definirmos também na plataforma, assim conseguimos por exemplo definir um ícone personalizado para cada flavor ou até mesmo mudar o nome do app. Vamos alterar então nosso android/app/build.gradle para definição dos productFlavors:

O nome da dimension pode ser qualquer um, contanto que seja o mesmo para todos os flavors, em dev e qa também podemos definir um suffixo do id e versão da aplicação. Para rodar nosso app agora temos as seguintes opções de comandos:

Rodando cada flavor em modo DEBUG:

  • flutter run –flavor qa -t lib/main_qa.dart
  • flutter run –flavor dev -t lib/main_dev.dart
  • flutter run –flavor prod -t lib/main_production.dart

Rodando cada flavor em modo PROFILE:

  • flutter run –profile –flavor qa -t lib/main_qa.dart
  • flutter run –profile –flavor dev -t lib/main_dev.dart
  • flutter run –profile –flavor prod -t lib/main_production.dart

Rodando cada flavor em modo RELEASE:

  • flutter run –release –flavor qa -t lib/main_qa.dart
  • flutter run –release –flavor dev -t lib/main_dev.dart
  • flutter run –release –flavor prod -t lib/main_production.dart

As vezes ao mudar entre os flavors é necessário um flutter clean para limpar o build do nosso app.

Ícone do app baseado no flavor:

Para alterar os ícones dinamicamente é fácil, para isso vamos até android/app/src e criaremos um diretório para cada flavor, sendo {flavor}/res, menos para o flavor de produção, esse irá utilizar o diretório padrão main. Dentro de cada res criamos os diretórios mipmap exatamente como criados no main/res, assim em cada mipmap colocamos o ícone de acordo com o flavor. O ícone deve ter o mesmo nome em todos, recomendo manter a nomenclatura que já está configurada ic_launcher. A estrutura final deve ser essa:

Assim ao rodar nossos diferentes flavors temos:

Diferentes ícones para cada flavor.

Nome do app baseado no flavor:

Podemos usar a mesma lógica do ícone para o nome, dentro de cada diretório rescriamos um diretório values com um arquivo strings.xml dentro. E para cada um simplesmente definimos uma string app_name com o nome do app:

Dentro do android/app/src/main/AndroidManifest.xml agora referenciamos essa nova string como o parâmetro android:label:

Com isso para cada flavor agora temos um nome diferente:

Diferentes nomes para cada flavor.

Para gerar os ícones em diferentes tamanhos, pode ser utilizado o pacote flutter_launcher_icons

Rodando os flavors pela IDE:

Embora eu prefira utilizar o terminal da IDE para rodar o app, caso você utilize o Intellij ou Android Studio pode ser interessante definir perfis de inicialização para cada build mode + flavor. Por exemplo:

Editamos as configurações de inicialização e criamos uma nova baseada em Flutter.

Agora basta dar um nome, definir o arquivo main correspondente, o argumento de inicialização (para os build modes profile e release apenas), e o flavor definido em build.gradle. Poderíamos ter um resultado semelhante a este:

Usando valores específicos do flavor:

Agora vamos simular um cenário onde:

  • Caso esteja rodando em dev: Eu quero que o meu repositório utilize um json local, para desenvolver e testar meu app rapidamente sem depender do retorno da uma API
  • Caso esteja rodando em qa: Eu quero que o meu repositório faça uma chamada REST real, em uma API que usada para testes.
  • Caso esteja rodando em production: Eu quero que o meu repositório faça uma chamada REST real, na API utilizada em produção.

Vamos começar criando alguns arquivos.

PersonJson é um singleton que irá conter o json que utilizaremos local, caso estivermos rodando em dev. Para qa e production vamos usar as URLs raw do Github para os arquivos person_qa.json e person_production.json, simulando um endereço para uma API.

No nosso bean de Person com a ajuda do json_serializable geramos a person.g.dartpara transformar o json em um objeto.

A BaseApi vai nos ajudar a fazer as chamadas rest, nela pegamos a baseUrl definida para o flavor, e limitamos a chamada com um timeout de 5 segundos, ao passar isso uma exceção é lançada. Poderíamos adicionar nessa classe outros métodos para chamadas post, put, entre outras, inclusive para chamadas privadas com headers de autenticação.

Nosso provider vai extender de BaseApi, fazer a chamada para nossa API e deserializar o json em uma Person. Note que caso estivermos em dev, ao invés de fazer um request, pegamos o json local e simulamos um delay de 2 segundos.

O Repository é também um singleton, responsável por saber de onde pegar os dados, sendo que no nosso exemplo ele apenas faz a chamada para a api, porém, é nessa mesma classe que definiríamos estratégias de cache por exemplo, pegando dados de uma database local ao invés da API.

Agora vamos para a definição do nosso Bloc, para entendimento do mesmo (e inclusive a implementação da qual eu utilizei), recomendo realmente a leitura do artigo do Didier.

O BlocProvider é uma combinação de StatefulWidget com InheritedWidget para nos fornecer a instância do bloc necessário através da árvore de widgets.

HomeBloc é o nosso bloc com as regras da HomePage, nele disponibilizamos uma stream para ouvir e pegar a pessoa, sendo assim, no fetchData() chamamos o Repository e adicionamos o resultado no streamController. Em caso de erro por enquanto estamos apenas printando no console.

Agora alteramos o MyApp para adicionar o Bloc, assim como a HomePage para imprimir as informações da pessoa retornada pelo request.

Com apenas isso já teremos o resultado desejado, com as chamadas do json de acordo com cada flavor.

Controlando erros de conexão:

Para aplicativos que precisamos fazer chamadas a uma API, é essencial que cuidemos dos possíveis erros gerados nessas chamadas, e principalmente, termos o conhecimento do tipo de conexão utilizado por nossos usuários, ou até mesmo se eles estão sequer conectados, para tomarmos ações a partir destas informações, garantindo assim uma melhor experiência na utilização do nosso app, privando nossos usuários de erros indesejados.

Para nos ajudar com essa tarefa vamos utilizar o plugin connectivity, que irá permitir identificarmos o status de conexão do nosso usuário. Vale salientar que o usuário pode estar em uma conexão com VPN ou wifi de hotel por exemplo, que não necessariamente irão garantir que o mesmo esteja conectado a internet, por isso nos prevenimos de erros e usamos um timeout nas chamadas.

ConnectivityHandler é nossa classe abstrata que usaremos para disparar callbacks da conexão.

Alteramos o fetchData() do HomeBloc para receber um delegate (classe que implementa o ConnectivityHandler) e caso ocorra algum erro na requisição dos dados, disparamos através do delegate.onError(). Acontece que além de identificar os possíveis erros, seria interessante identificarmos quando o status ou tipo de conexão do usuário muda, para nossa sorte, o plugin connectivity disponibiliza uma stream para escutarmos essas alterações.

Criamos dessa vez o ApplicationBloc, esse vai manter uma lista de _delegates e disparar um delegate.onConnectivityChanged() para os mesmos a cada mudança de status. Utilizamos um bloc diferente, pois esse deve tratar de regras mais gerais da aplicação, assim como regras de login e fluxo de autenticação por exemplo.

Mudamos o MyApp para incluir o ApplicationBloc acima de toda a árvore de widgets do nosso app. Na nossa rota cadastramos a homePage para escutar a stream de connectivity do ApplicationBloc.
Agora a HomePage precisa implementar o ConnectivityHandler para gerenciar os erros e status de conexão.

Agora em caso de erro na chamada ou mudança no status da conexão, mostraremos um snackBar na tela com as informações.

Notificação funcionando ao mudar de conexão

E em caso de um erro de timeout, teríamos o seguinte comportamento:

Notificação de erro no timeout da requisição

Note pelo gif que caso ocorra um erro, ao alterar o status da conexão realizamos a chamada REST novamente.

É isso aí, nesse artigo e projeto vimos algumas questões de arquitetura e organização do código que com certeza tornam nossa codificação mais clean e profissional. A organização final dos nossos pacotes ficou assim:

Observações finais:

Não inclui os schemes do iOS nesse artigo pois no momento não consigo compilar para iOS, assim que tiver um Mac em mãos atualizarei o mesmo. Provavelmente também atualizarei o projeto em breve com um fluxo completo de autenticação.

Achou útil? Deixa aí umas 👏 e uma ⭐️ no repositório ;)

--

--