Gerenciando ambientes e chaves de configurações privadas com dart-define

Vinicius Oliveira
Flutter Brasil
Published in
7 min readApr 7, 2024

Com o Dart define, vocês poderão gerenciar de forma mais eficiente as configurações de seus aplicativos, permitindo uma abordagem mais organizada e controlada durante o desenvolvimento.

O que é o dart-define?

É uma ferramenta robusta que possibilita a definição de variáveis de ambiente durante a compilação do seu aplicativo Flutter. Essa funcionalidade confere a capacidade de configurar distintos ambientes, como produção, desenvolvimento e homologação, sem a necessidade de intervenção manual para alterações de código.

Por que usar?

Imagine que você está criando um aplicativo de entrega de comida. Antes de lançá-lo, você precisa testá-lo em diferentes situações, como ambiente de desenvolvimento, homologação e produção, para garantir que tudo funcione corretamente. Com o dart define, você pode fazer esses testes sem precisar reescrever seu código toda vez que mudar de ambiente. Ele permite alternar facilmente entre os diferentes ambientes de forma rápida e eficiente.

Um Exemplo Prático: DeliveryFácil

Digamos que você esteja construindo o aplicativo “DeliveryFácil”. Vamos dar uma olhada em como usar o Dart Define pode ajudar nesse cenário.

  1. Produção: Quando você está pronto para lançar seu aplicativo, você configura o Dart define para apontar para o servidor de produção, onde seus usuários finais poderão acessar o aplicativo sem problemas.
  2. Desenvolvimento: Durante o desenvolvimento, você precisa acessar um servidor de teste para fazer alterações e depurar o código. Com o Dart define, você pode configurar facilmente seu aplicativo para apontar para um ambiente de desenvolvimento específico.
  3. Homologação: Antes de lançar seu aplicativo, é importante testá-lo em um ambiente de homologação para detectar e corrigir problemas. Com o Dart define, você pode mudar rapidamente para esse ambiente e garantir que tudo esteja funcionando como esperado.

No terminal, ao compilar seu aplicativo, adicione as flags --dart-define seguidas das variáveis que deseja definir. Por exemplo:

flutter run --dart-define=API_URL=https://api.deliveryfacil.com

Em seu código Dart, você pode acessar essas variáveis usando.

const String apiUrl = String.fromEnvironment('API_URL');

Simples assim!

Vamos simplificar: após aprender sobre o Dart Define, agora vamos criar uma enum para gerenciar ambientes em nossos aplicativos. Com essa abordagem, você poderá implementar facilmente diferentes configurações, como produção, desenvolvimento e homologação, em seus projetos Flutter, tornando o processo de desenvolvimento mais organizado e eficiente.

Vamos começar definindo três ambientes.

Aqui está o conteúdo do launch.json para sua referência:

{
"version": "0.2.0",
"configurations": [
{
"name": "DEBUG DEV",
"request": "launch",
"type": "dart",
"args": [
"--dart-define=ENV=development"
]
},
{
"name": "DEBUG PROD",
"request": "launch",
"type": "dart",
"args": [
"--dart-define=ENV=production"
]
},
{
"name": "DEBUG TEST",
"request": "launch",
"type": "dart",
"args": [
"--dart-define=ENV=homolog"
]
}
]
}

Para executar o aplicativo em cada ambiente esse assim.

flutter run --dart-define=ENV=development
flutter run --dart-define=ENV=production
flutter run --dart-define=ENV=homolog

Vamos criar um enum.

enum ENV {
development(baseUrl: 'http://localhost:3000', title: 'Development'),
production(baseUrl: 'https://production.com', title: 'Production'),
homolog(baseUrl: 'https://homolog.com', title: 'Homolog');

const ENV({required this.baseUrl, required this.title});
final String title;
final String baseUrl;

static ENV fromString(String env) {
return ENV.values.firstWhere(
(e) => e.name.toLowerCase() == env.toLowerCase(),
orElse: () => ENV.production,
);
}
}

Em seguida, vamos definir uma variável de ambiente no arquivo main.dart para acessá-la em todo o aplicativo.

final env = ENV.fromString(
const String.fromEnvironment('ENV',defaultValue: 'production'),
);

Future<void> main() async {
debugPrint('ENV: ${env.baseUrl}');
await AppConfiguration.initialize();
runApp(
EasyLocalization(
supportedLocales: const [
Locale('en', 'US'),
Locale('pt', 'BR'),
],
path: 'assets/translations',
child: ModularApp(
module: AppModule(),
child: const AppWidget(),
),
),
);
}
Na barra inferior do VSCode, você verá a opção habilitada para trocar de ambiente.

Com esse exemplo, podemos trocar de ambiente e pegar a baseUrl do env, cada vez que executarmos o aplicativo definimos qual ambiente que queremos, assim a baseUrl ira ficar dinamica e correspondara ao ambiente escolhido.

Configurando Variáveis de Ambiente Dart-Define no Android e iOS

Android

Vá para android/app/build.gradlee usaremos o código a seguir para analisar todas as variáveis ​​​​disponíveis --dart-definee disponibilizá-las como um mapa chave: valor para usar de todas as maneiras.

def dartEnvironmentVariables = []
if (project.hasProperty('dart-defines')) {
dartEnvironmentVariables = project.property('dart-defines')
.split(',')
.collectEntries { entry ->
def pair = new String(entry.decodeBase64(), 'UTF-8').split('=')
[(pair.first()): pair.last()]
}
}

Nesse trecho de código, estamos inicializando uma lista vazia chamada dartEnvironmentVariables. Em seguida, verificamos se a propriedade 'dart-defines' está presente no projeto. Se estiver, dividimos essa propriedade em uma lista, usando a vírgula como delimitador, e iteramos sobre cada elemento dessa lista.

Durante essa iteração, cada elemento é decodificado de Base64 e convertido para uma string utilizando UTF-8. Em seguida, dividimos essa string em um par chave-valor, usando o sinal de igual como separador. Por fim, adicionamos esse par à lista dartEnvironmentVariables, que armazenará todas as variáveis de ambiente definidas em 'dart-defines'.

Adicione o seguinte ao bloco defaultConfig:

    defaultConfig {
applicationId "br.com.seupacoteaqui"

minSdkVersion 20
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
// Adicione aqui as variáveis para ser usadas no AndroidManifest
manifestPlaceholders += [
// Substituirá qualquer conteúdo por ${} no seu AndroidManifest
// Exemplo: android:value="${apiKeyGoogleMaps}"
apiKeyGoogleMaps: dartEnvironmentVariables.apiKeyGoogleMaps
]
}

Se estiver utilizando algo que requer uma entrada no AndroidManifest.xml, podemos usar assim:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.imarker_app">
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<application
android:label="imarker_app"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${apiKeyGoogleMaps}"/> <!-- Adicione sua chave assim -->
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>

Vídeo demonstrativo de no Android com a configuração de chaves e segredos usando dart-define.

iOS

Estamos agora focados em lidar com chaves e segredos de API, o que difere consideravelmente dos artigos anteriores que abordam a alteração do nome e do pacote do aplicativo. Embora seja menos complexo, seu escopo é mais específico e direcionado.

Primeiramente, adicione o seguinte ao arquivo Info.plist localizado em ios/Runner:

<key>DART_DEFINES</key> 
<string>$(DART_DEFINES)</string>

Como ficou o Info.plis, apos adicionarmos o código assim.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Imarker App</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>imarker_app</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>DART_DEFINES</key>
<string>$(DART_DEFINES)</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs your location to test the location feature of the Google Maps plugin.</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

Isso é necessário para acessar o conteúdo final bruto do — dart-define dentro do nosso código Swift. O método .xcconfig torna os valores disponíveis apenas dentro do Xcode. Em seguida, adicione o seguinte à sua substituição didFinishLaunchingWithOptions:

let dartDefinesString = Bundle.main.infoDictionary!["DART_DEFINES"] as! String
var dartDefinesDictionary = [String:String]()
for definedValue in dartDefinesString.components(separatedBy: ",") {
let decoded = String(data: Data(base64Encoded: definedValue)!, encoding: .utf8)!
let values = decoded.components(separatedBy: "=")
dartDefinesDictionary[values[0]] = values[1]
}

Vamos ver como ficou o conteudo do arquivo AppDelegate.


import UIKit
import Flutter
import GoogleMaps

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let dartDefinesString = Bundle.main.infoDictionary!["DART_DEFINES"] as! String
var dartDefinesDictionary = [String:String]()
for definedValue in dartDefinesString.components(separatedBy: ",") {
let decoded = String(data: Data(base64Encoded: definedValue)!, encoding: .utf8)!
let values = decoded.components(separatedBy: "=")
dartDefinesDictionary[values[0]] = values[1]
}
// Aqui vamos pegar a chave da api do google maps.
// Poderia ser qualquer outra variável
if let apiKeyGoogleMaps = dartDefinesDictionary["apiKeyGoogleMaps"] as? String {
GMSServices.provideAPIKey(apiKeyGoogleMaps)
}
GeneratedPluginRegistrant.register(with: self)

return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

Isso é equivalente ao que adicionamos no arquivo build.gradle anteriormente. Ele pega os valores brutos e os converte de base64 em um dicionário chave:valor que podemos usar para configurar qualquer SDKs da seguinte maneira:

if let apiKeyGoogleMaps = dartDefinesDictionary["apiKeyGoogleMaps"] as? String {
GMSServices.provideAPIKey(apiKeyGoogleMaps)
}

Vídeo demonstrativo de no iOS com a configuração de chaves e segredos usando dart-define.

Aprendemos que o uso do dart-define é essencial para configurar o ambiente em aplicativos Flutter. Exploramos como recuperar essas chaves no iOS e no Android, permitindo um acesso seguro e eficiente aos recursos do aplicativo. Com essa abordagem, simplificamos o gerenciamento de variáveis de ambiente em nossos projetos.

Link para a comunidade flutter do discord:

Participe da comunidade Flutter no Discord! Junte-se a nós em discussões e colaborações sobre Flutter. Clique aqui para acessar. Além disso, recomendo seguir criadores de conteúdo como Toshi Ossada, Brunno Marques e Edson Souza para mais conteúdo de qualidade.

Referência:

--

--