Utilizando Python para sincronizar as variáveis de ambiente nos arquivos launch.json e config.json no desenvolvimento de App's Flutter
Alguns dias atrás estava estudando sobre variáveis de ambiente em tempo de compilação utilizando --dart-define em um app Flutter. A documentação do Dart, diz o seguinte:
You can specify compilation environment declarations when building or running a Dart application. Compilation environment declarations specify configuration options as key-value pairs that are accessed and evaluated at compile time.
Ou seja, é possível em tempo de compilação fornecer à aplicação Dart pares de chave-valor. Na empresa que trabalho atualmente, possuímos os seguintes ambientes: o de desenvolvimento, homologação e produção. Cada um desse ambientes possuem seus próprios domínios e um conjuntos de outras configurações.
Imagine o seguinte cenário. Você está desenvolvendo um aplicativo Flutter e este aplicativo precisa listar os produtos através de um determinado end-point, como por exemplo, product-list. No momento que está codificando o app, você acaba conectando o domínio da API do ambiente de desenvolvimento ao end-point product-list para assim realizar a chamada e receber como resposta a tão esperada lista de produtos.
De repente aparece um cara que é responsável pela homologação do aplicativo e pede para você gerar uma versão do seu lindo aplicativo de lista de produtos para o ambiente de homologação. Então você deve pensar, ótimo, basta abrir o arquivo onde faço a chamada da API para a lista de produtos e substituo o domínio de desenvolvimento para o de homologação e voilà! Está aqui o seu app, cara da homologação.
Você super feliz na sua mesa e tomando seu café e do nada aparece uma moça do setor de negócios da empresa e fala que precisa testar o aplicativo em produção. Então as coisas começam a ficar repetitivas e você começa a se perguntar, vou ter que ficar fazendo sempre essas alterações de domínio no aplicativo? E se ao invés de uma variável de ambiente você tivesse 20, 40 ou mais, e ter que mudá-las toda vez que alguém pedir um app para cada ambiente da sua empresa?
Pensando nisso é que o Dart disponibiliza uma opção chamada --define que você pode utilizar assim:
$ dart run --define=DEBUG=true
Onde DEBUG=true é o par chave-valor que é passado para a aplicação em tempo de compilação. Mas como estamos falando de aplicações Flutter, você irá utilizar algo como:
$ flutter run --dart-define=DEBUG=true
Para saber mais, consulte a documentação do Dart, clicando aqui.
Mas voltando para o nosso exemplo da listagem de produtos. Se quiséssemos passar o domínio do ambiente de desenvolvimento para o nosso aplicativo para rodar em um emulador, seria assim:
$ flutter run --dart-define=URL=https://dev.meusite.com.br
Se fosse para gerar um APK, seria assim:
$ flutter build apk --dart-define=URL=https://dev.meusite.com.br
Para executar/buildar o app para produção ou qualquer outro ambiente, basta apenas trocar o valor da variável URL. OK Fernando, e como eu recupero o valor desta variável no meu código? Para isso basta fazer da seguinte forma:
final String urlApi = const String.fromEnvironment('API_KEY');
Além de String, é possível utilizar também para valores booleanos e para inteiros:
final bool urlApi = const bool.fromEnvironment('DEBUG');
final int urlApi = const int.fromEnvironment('COUNT');
Certo, agora já sabemos como utilizar o --dart-define para utilizar as nossas variáveis de ambiente em nosso código. Maravilha! :D Você não precisará mais ficar trocando manualmente o domínio da sua API diretamente no código toda vez que alguém pedir uma versão do seu aplicativo para determinado ambiente. Ufa!
Mas então depois de um tempo o seu aplicativo cresce, novas telas, novas funcionalidades legais e consequentemente novas variáveis de ambiente. Agora seu aplicativo utiliza serviços de terceiros e esses serviços podem oferecer domínios para diferentes ambientes de desenvolvimento. Então você lembra da dica que dei, aaaah é só usar o --dart-define com todas as variáveis de ambinte.
$ flutter run --dart-define=URL=https://dev.meusite.com.br --dart-define=SERVICE_ONE=https://dev.serviceone.com.br --dart-define=SERVICE_TWO=https://dev.servicetwo.com.br --dart-define=SERVICETHREE=https://dev.servicethree.com.br
O que achou? Eficiente? Muito pelo contrário, não é? Com quatro variáveis já achei estranho, imagina ter 20 ou mais sendo passadas em um único comando? É de deixar qualquer um estressado :). Mas essa era a forma que muitos desenvolvedores utilizavam para inserir as variáveis de ambiente em seus aplicativos em tempo de compilação. Você leu certo, era a maneira que utilizavam para fazer isso. Atualmente já temos uma forma mais simples e fácil de manter.
Os desenvolvedores percebendo a dificuldade de manter essa linha de comando começaram a sugerir alterações para facilitar a manutenção e inclusão de novas variáveis de ambiente. Sendo assim, na versão 3.7 do Flutter, foi lançada a propriedade --dart-define-from-file, que recebe como valor um arquivo .JSON. O nosso exemplo anterior ficaria assim:
$ flutter run --dart-define-from-file=config.json
e no arquivo config.json:
{ "DEFINE_APP_NAME": "App produtos", "DEFINE_APP_SUFIX_NAME": "DEV" }
Agora sim o/, muito melhor dessa forma, não?! Você pode conferir alguns PR no repositório do Flutter sobre --dart-define-from-file nos links abaixo:
Então até agora aprendemos como definir as variáveis de ambiente em um arquivo .JSON e utilizá-las em nossos apps. Mas onde entra o Python nessa história? Então, como utilizo o VS Code para programar e para executar/depurar um aplicativo utilizo o arquivo launch.json para definir as variáveis de ambiente, acabo que com essa nova propriedade --dart-define-from-file, tenho dois arquivos para manter: o launch.json e o config.json.
Sendo assim, tenho que manter as variáveis de ambiente atualizadas nos dois arquivos e isso é um problema. Para resolver essa questão utilizei os recursos do Python para criar um CLI (Command-Line Interface), ou uma interface de linha de comando, que ao executar o arquivo no terminal irá exibir isso:
Como você pode ver na image, através do CLI poderei criar/atualizar os arquivos launch.json e config.json, executar aplicativos e fazer os builds para Android e iOS. Mas antes que você possa ver e utilizar o arquivo quero deixar alguns pontos:
- O código foi desenvolvido para sanar um problema que estava passando no momento e também para estudo. Então pode ser que isso não faça sentido para você e tudo bem :). Mas se puder ajudar outra pessoa, já valeu a pena.
- É a melhor forma de resolver isso? A resposta para isso é que depende do contexto. Se você usa CI/CD, é possível configurar essas variáveis de ambiente na esteira de desenvolvimento para assim gerar o APK/BUNDLE ou o IPA para as respectivas lojas. Você poderia também fazer um makefile sem problemas ;).
- Posso melhorar o código? Deve! Mas se possível deixa uma referência de onde pegou o mesmo e se tiver uma melhoria pode abrir uma PR.
Para conferir o código responsável por executar estas ações, basta acessar meu github clicando aqui.
Ao abrir o arquivo, você irá ver isso:
O que temos na imagem acima é o que no Python é chamado de dicionário e é nesta faixa de código que você define uma única vez as variáveis para cada ambiente e depois que estas variáveis estiverem definidas, basta chamar no terminal:
$ python builder.py
Depois digite a opção [1] para Criar/Atualizar launch.json (VS Code)/config.json (Environments) e pronto. Os arquivos launch.json e config.json estão com as variáveis de ambiente definidas e sincronizadas. Com isso, você já pode utilizar as outras opções e o restante do código do builder.py faz todo o trabalho.
Só mais uma coisa, o arquivo builder.py deve ficar da raiz do seu projeto Flutter.
É isso, espero que este código possa ajudar alguém e conto com vocês para aperfeiçoa-lo e se ficou alguma dúvida pode deixar nos comentários que irei responder assim que possível. Valeu e até a próxima ;)