Firebase Remote Config lets you change your app’s behavior without releasing updates. But there’s a problem: the standard way to use it relies on “magic strings” that can break your app with simple typos.

The Problem: Using strings like getString("welcome_message") is risky because:

  • Typos cause silent failures
  • No compile-time safety
  • Hard to refactor
  • Poor developer experience

The Solution: Use remote_config_gen to generate type-safe code that catches errors at compile time instead of runtime.

Table of Contents

  1. Understanding Firebase Remote Config
  2. Problems with Magic Strings
  3. The Solution: remote_config_gen
  4. How to Set it Up
  5. Building Feature Flags

Understanding Firebase Remote Config

Before we look at the problems with the standard way of using Firebase Remote Config, let’s first understand what it is, what it can do, and how it works.

What is Firebase Remote Config?

At its core, Firebase Remote Config is a cloud service that allows developers to store a set of key-value parameters on Firebase’s servers. The client application can then fetch these parameters and use their values to alter its behavior and appearance dynamically. This decouples the application’s configuration from its compiled code, providing a powerful mechanism for post-launch updates without requiring a new release to the Apple App Store or Google Play Store.

Its capabilities extend far beyond simple configuration changes, enabling a suite of modern development practices:

Dynamic UI and Content Updates: Developers can change display text, promotional banners, color themes, or entire layouts to support marketing campaigns, seasonal events, or simply to refresh the user experience.

Feature Management and Gradual Rollouts: Parameters can serve as “feature flags,” which are used to enable or disable functionality within the app. This is fundamental for modern DevOps practices like gradual, percentage-based rollouts, where a new feature can be exposed to a small subset of users (e.g., 1%, then 10%, then 50%) to monitor for crashes or negative feedback before a full launch. It also enables the use of “kill switches” to instantly disable a faulty feature in production.

User Personalization: The service can deliver different parameter values to different user segments. These segments can be defined by a wide range of criteria, including Google Analytics audiences, app version, operating system, language, country, or custom user properties you define. This allows for tailoring the app experience to specific cohorts of users.

A/B Testing: Remote Config integrates seamlessly with Google Analytics to facilitate A/B testing. Developers can create experiments that present different parameter values to different groups of users and measure the impact on key metrics like engagement, retention, or conversion.

AI-Powered Personalization: A more advanced capability allows Firebase’s machine learning algorithms to automatically optimize a parameter’s value for each individual user to maximize a predefined goal, such as ad clicks or in-app purchases.

The FRC Lifecycle: A Technical Walkthrough

Understanding the sequence of operations in the Remote Config client SDK is crucial for implementing it correctly and avoiding common pitfalls like UI jank or stale data.

Setting In-App Defaults: The first and most critical step in any Remote Config implementation is to provide a set of in-app default values using the setDefaults() method. These are the values the app will use if it cannot connect to the Firebase backend, if the connection times out, or before the first successful fetch has completed. This ensures the application always starts in a known, predictable state and never crashes due to a missing configuration value. These defaults act as the application’s ultimate safety net.

Fetching Values: To retrieve updated parameters from the Firebase backend, the app calls a fetch method, most commonly fetchAndActivate(). This single call performs two distinct operations: it fetches the latest values from the server and caches them locally on the device.

Understanding Caching and Throttling: To prevent apps from overloading the backend with excessive requests, the Remote Config client SDK employs aggressive caching. By default, fetched values are cached for 12 hours. This means that even if fetchAndActivate() is called multiple times, a new network request will only be made if the cache is older than this minimum fetch interval. While essential for production stability, this behavior can be a major source of frustration during development when rapid iteration is needed. To address this, developers can use setConfigSettings() to temporarily set a very low minimumFetchInterval (e.g., one minute). It is imperative that this low interval is only used in debug builds and is reverted to a longer duration for production releases to avoid client-side throttling.

Activating Values: The fetch operation merely downloads and caches values; it does not make them “live” in the app. A separate step, activation, is required. The activate() method makes the most recently fetched set of parameters available to the app’s getter methods (e.g., getString(), getBool()). This separation gives developers precise control over when a UI change occurs. For example, new values can be fetched in the background and only activated on the next app start to prevent a jarring visual change while the user is in the middle of a task. The fetchAndActivate() method is a convenient wrapper that performs both steps sequentially.

Real-Time Updates: The most modern and powerful approach to receiving updates is to use the onConfigUpdated stream. By adding a listener to this stream, the app opens a persistent connection to the Remote Config backend. Whenever parameters are published in the Firebase console, the backend sends a signal to the client, which then automatically fetches the new values in real-time, completely bypassing the minimumFetchInterval setting. This is the key to enabling true real-time functionality like instant feature toggling.

Standard Flutter Implementation (The “Before” Picture)

To establish a baseline, here is a minimal but complete example of how Firebase Remote Config is typically set up in a Flutter application using the standard firebase_remote_config package. This code will serve as our point of comparison when we refactor it to a type-safe implementation in a later chapter.

// main.dart
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_remote_config/firebase_remote_config.dart';
import 'package:flutter/material.dart';
import 'firebase_options.dart'; // Generated by FlutterFire CLI

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  // Get the Remote Config instance
  final remoteConfig = FirebaseRemoteConfig.instance;

  // Set configuration settings for development
  await remoteConfig.setConfigSettings(RemoteConfigSettings(
    fetchTimeout: const Duration(minutes: 1),
    minimumFetchInterval: const Duration(seconds: 10), // Low for development
  ));

  // Set in-app default values
  await remoteConfig.setDefaults(const {
    "welcome_message": "Hello from a default value!",
    "show_promo_banner": false,
  });

  // Fetch and activate remote values
  await remoteConfig.fetchAndActivate();

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Remote Config Demo'),
        ),
        body: Center(
          // Accessing the value using a "magic string"
          child: Text(FirebaseRemoteConfig.instance.getString('welcome_message')),
        ),
      ),
    );
  }
}

This implementation is functional, but as we will now explore, it harbors significant underlying risks that can compromise application stability and developer velocity.

Problems with Magic Strings in Remote Config

The standard Remote Config API, while powerful, has a big problem: it uses “magic strings.” This section explains the real risks and problems this approach creates.

The “Magic String” Anti-Pattern

A “magic string” is a hardcoded string literal used as an identifier or key, whose meaning is not self-evident from the code and is not validated by the compiler. The line remoteConfig.getString("welcome_message") is a perfect example. From the Dart compiler’s perspective, "welcome_message" is just an arbitrary sequence of characters. There is no static link between this string in the application code and the parameter key defined in the Firebase console. This connection exists only by convention and relies entirely on developer discipline—a notoriously unreliable foundation for building robust, scalable software.

This approach creates a fragile, implicit contract between two disparate systems (the client code and the Firebase backend). When this contract is inevitably violated, the consequences manifest as runtime bugs, not compile-time errors.

Common Pitfalls and Their Real-World Consequences

The reliance on magic strings introduces several distinct categories of risk, each with significant real-world consequences for development teams.

1. Compile-Time Blindness to Typos

The Problem: A developer intends to access the welcome_message parameter but makes a simple typographical error, writing getString("welcom_message") instead. The Dart compiler will not flag this as an error because it is a syntactically valid string.

The Consequence: The application will compile and run without issue. However, at runtime, the call to getString will fail to find a matching key in the fetched configuration. It will silently fall back to the in-app default value (or an empty string if no default is provided). This leads to insidious bugs that are difficult to diagnose. A feature may appear to be “broken” or a new remote value “not applying,” when the root cause is a single mistyped character. Teams can waste hours debugging an issue that a type-safe system would have caught instantly.

2. The Risk of Runtime TypeErrors

The Problem: The application code expects an integer value for a parameter, calling getInt("max_items"). Meanwhile, a team member updating the configuration in the Firebase console accidentally saves the value as a string (e.g., “100” instead of 100).

The Consequence: This creates a type mismatch that is invisible at compile time. When the app fetches the configuration and attempts to parse the string “100” as an integer, it will throw a TypeError at runtime. Depending on where this call is made, this could crash a screen or even the entire application. This is a critical stability risk, as it allows a non-technical team member to inadvertently introduce a production-crashing bug through a simple data entry error in a web console.

3. The High Cost of Refactoring

The Problem: As an application evolves, parameter names often need to be changed for clarity or consistency. The product team decides that welcome_message should be renamed to home_screen_greeting.

The Consequence: To implement this change, a developer must perform a global, text-based “find and replace” for the string "welcome_message". This process is fraught with peril. It might miss some occurrences if they are constructed dynamically. Worse, it could incorrectly change a similarly named but unrelated string in a different context. There is no compiler assistance to validate that all usages have been correctly updated. This high risk and manual effort makes the codebase rigid and actively discourages the kind of routine refactoring that is essential for long-term code health and maintainability.

4. Degraded Developer Experience

The Problem: When a developer needs to use a remote parameter, the IDE offers no assistance. There is no autocomplete to suggest available keys or validate their existence. The developer must break their workflow, switch context to the Firebase console or internal documentation, and manually type the string key.

The Consequence: This friction slows down the development process and dramatically increases the cognitive load on the developer, making errors more likely. It runs counter to the principles of a modern, integrated development environment, which is designed to assist and guide the developer.

The Solution: remote_config_gen

To fix the problems with string-based APIs, Flutter developers use code generation. The remote_config_gen package applies this pattern to Firebase Remote Config, changing a risky runtime setup into a safe, compile-time setup. It’s a dev_dependency, meaning it’s a tool used during development and doesn’t add any size to your final app.

How Code Generation Works: One Source of Truth

The main idea of remote_config_gen is to make the Firebase Remote Config console your single source of truth and then use a build-time tool to keep your Dart code in sync with it.

The workflow is as follows:

  1. Define the Contract: The developer defines all parameters, their data types (String, Number, Boolean, JSON), default values, and parameter groups directly in the Firebase Remote Config console. This is the canonical definition of the app’s remote configuration.

  2. Capture the Contract: Using the Firebase CLI, the developer downloads this entire configuration into a file named remoteconfig.template.json. This file is a structured representation of the configuration and can be committed to version control, making changes to remote parameters as auditable and reviewable as any other code change.

  3. Generate the API: The remote_config_gen library is a simple dart script. It parses the remoteconfig.template.json file and generates the RemoteConfigParams Dart class.

  4. Enforce the Contract: The library generates a Dart class, typically named RemoteConfigParams, which contains static, type-safe accessors for every single parameter and parameter group defined in the JSON file.

This process changes who’s responsible for keeping things correct. Instead of relying on a developer’s memory to keep string keys the same between the console and the code, the system now relies on an automated build process. If a parameter is renamed in the console but not in the code, the build will fail. If a type is changed, the build will fail. This moves error detection from the user’s device (where it’s unpredictable) to the developer’s machine (where it’s controlled). This makes your code much more reliable and easier to maintain.

Main Benefits

Using this code generation approach gives you several important benefits:

Compile-Time Safety: Typos in parameter names are no longer silent runtime failures. Accessing a non-existent parameter like RemoteConfigParams.welcomMessage will result in a clear compile-time error, preventing the bug from ever being created.

Automatic Type-Casting: The generated code automatically calls the correct firebase_remote_config getter (getString(), getBool(), getInt(), getDouble()). If the type defined in the console (e.g., a String) does not match the type expected by the generated accessor (e.g., an int), it can be flagged, preventing runtime TypeError exceptions.

Better IDE Support: The developer experience is vastly improved. By simply typing RemoteConfigParams., the developer is presented with an autocomplete list of all available, correctly-typed parameters. This enhances discoverability and reduces the need to context-switch to the Firebase console.

Safe and Simple Refactoring: Renaming a parameter becomes a safe, compiler-guided process. The developer renames the key in the Firebase console, re-downloads the template, and re-runs the generator. The compiler will then show an error at every single location where the old parameter name was used, effectively providing a to-do list of all the necessary updates.

Table: String-Based vs. Code-Generated Access

This table provides an at-a-glance comparison, starkly illustrating the benefits of the code generation approach.

FeatureStandard firebase_remote_configWith remote_config_gen
Accessing a ValueremoteConfig.getBool("enable_new_feature")RemoteConfigParams.enableNewFeature.getValue()
Type Safety❌ None (Risk of runtime errors)✅ Full (Caught at compile-time)
Key Safety (Typos)❌ None (Risk of silent failures)✅ Full (Caught at compile-time)
IDE Support❌ None (Manual string entry)✅ Full (Autocomplete, go-to-definition)
Refactoring☠️ High-risk, manual search✅ Safe, compiler-assisted
Source of Truth💔 Split (Console + Code)🥇 Unified (Console via generated code)

How to Set it Up

This section shows you exactly how to add remote_config_gen to your Flutter project. Following these steps will make your Remote Config implementation much safer and more reliable.

Step 0: Pre-requisites

Before you begin, ensure your development environment meets the following criteria:

  • A functioning Flutter project that has already been configured with a Firebase project.
  • The Firebase Command Line Interface (CLI) is installed on your machine and you are authenticated (firebase login).

Step 1: Project Setup

First, add the necessary packages to your project’s pubspec.yaml file. remote_config_gen is a code generator, so it belongs in dev_dependencies, while firebase_remote_config and firebase_core are standard runtime dependencies.

# pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  firebase_core: ^2.27.0
  firebase_remote_config: ^4.3.10

dev_dependencies:
  flutter_test:
    sdk: flutter
  remote_config_gen: ^0.0.2 # Use the latest version from pub.dev

After adding these lines, run flutter pub get in your terminal to install the packages.

Step 2: The Source of Truth - Your Remote Config Template

Navigate to your Firebase project’s console, select “Remote Config” from the Engage section, and define the parameters you want to manage. For this tutorial, we will create the following:

  • welcome_message (Type: String, Default value: “Hello from Firebase!“)
  • max_retry_count (Type: Number, Default value: 3)
  • A parameter group named ui_settings containing one parameter:
    • dark_mode (Type: Boolean, Default value: false)

Once your parameters are defined and published, use the Firebase CLI to download the configuration template into your project’s root directory. This file will serve as the input for our code generator.

firebase remoteconfig:get -o remoteconfig.template.json

This command fetches your current Remote Config template from the server and saves it as remoteconfig.template.json in your project.

Step 3: Configuring the Generator

Next, create a new file in your project’s root directory named remote_config_gen.yaml. This file tells the generator where to find the input template and where to place the generated Dart code.

# remote_config_gen.yaml

input: remoteconfig.template.json
output: lib/generated/remote_config.dart

This configuration specifies that the generator should read from remoteconfig.template.json and write the output to a new file at lib/generated/remote_config.dart.

Step 4: Generating the Code

With the configuration in place, you can now run the code generator. Execute the following command in your project’s terminal:

dart run remote_config_gen

Step 5: Putting It to Use

If you inspect the newly created lib/generated/remote_config.dart file, you will see the generated code. It will contain a RemoteConfigParams class with static getters for each of your parameters, including nested classes for parameter groups.

Now, let’s change the simple MyApp widget from the first section to use this new, type-safe API.

Before (Using Magic Strings):

// old_widget.dart
import 'package:firebase_remote_config/firebase_remote_config.dart';
//...
class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final remoteConfig = FirebaseRemoteConfig.instance;
    final welcomeText = remoteConfig.getString('welcome_message');
    final isDarkMode = remoteConfig.getBool('dark_mode'); // Prone to error if key is in a group
    
    return Text(welcomeText);
  }
}

After (Type-Safe and Robust):

// new_widget.dart
// 1. Import the generated file
import 'package:your_app_name/generated/remote_config.dart';
import 'package:flutter/material.dart';

//...
class MyWidget extends StatelessWidget {
  const MyWidget({super.key});

  @override
  Widget build(BuildContext context) {
    // 2. Access values via the generated class. No magic strings!
    final welcomeText = RemoteConfigParams.welcomeMessage.getValue();
    final maxRetries = RemoteConfigParams.maxRetryCount.getValue();
    
    // 3. Access grouped parameters through the nested class
    final isDarkMode = RemoteConfigParams.uiSettings.darkMode.getValue();

    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text(welcomeText),
        Text('Max retries: $maxRetries'),
        Text('Dark mode: $isDarkMode'),
      ],
    );
  }
}

This direct comparison highlights the immediate benefits. The code is cleaner, self-documenting, and, most importantly, safe. Any typo in a parameter name or mismatch in type would now be caught by the compiler before the app is even run, completely eliminating the class of runtime errors discussed previously.

Building Feature Flags with remote_config_gen

While Remote Config is useful for many types of dynamic configuration, its most powerful use is for feature flagging. This section shows how to combine the type-safe code from remote_config_gen with Remote Config’s real-time features to build a solid feature flagging system.

What are Feature Flags?

Feature flags (or feature toggles) are an important part of modern software development. They allow teams to add new, incomplete features to the main codebase but keep them turned off. This gives you several benefits:

Lower Risk: New features can be rolled out slowly to a small percentage of users. If crashes or other issues happen, the feature can be turned off instantly without needing an emergency app release.

Deploy Anytime: Code can be deployed to production at any time, but the feature is only “released” to users when the flag is enabled. This separates the technical work from the business decision.

Test Safely: Features can be enabled only for internal testers or specific user groups, allowing for real-world testing before a public launch.

A/B Testing: Flags are the foundation for A/B testing different versions of a feature to see which performs better.

Firebase Remote Config is an excellent backend for managing these flags due to its powerful targeting and real-time update capabilities.

Implementing a Type-Safe Feature Flag

Let’s compare the implementation of a feature flag with and without our code generation workflow.

Scenario: We are developing a new, enhanced search results page and want to control its visibility with a flag named enable_enhanced_search.

Before (The Risky Way):

// Accessing the flag with a magic string
final useEnhancedSearch = FirebaseRemoteConfig.instance.getBool('enable_enhanced_search');

// In the widget tree
if (useEnhancedSearch) {
  return EnhancedSearchPage();
} else {
  return ClassicSearchPage();
}

As established, this approach is fragile. A typo in the key or a type mismatch in the console (e.g., saving the value as the string “true”) will cause the flag to silently fail, likely defaulting to false and preventing the new feature from ever being seen by users.

After (The Robust Way):

// Import the generated code
import 'package:your_app_name/generated/remote_config.dart';

// Accessing the flag with the type-safe getter
final useEnhancedSearch = RemoteConfigParams.enableEnhancedSearch.getValue();

// In the widget tree
if (useEnhancedSearch) {
  return EnhancedSearchPage();
} else {
  return ClassicSearchPage();
}

The improvement is profound. The code is not only cleaner but fundamentally safer. The existence of RemoteConfigParams.enableEnhancedSearch is a compile-time guarantee that this flag is defined in our single source of truth, the remoteconfig.template.json file. The call to .getValue() is guaranteed to access a boolean value, protecting against type errors.

Real-Time Toggling for Instant Control

The true power of this system is realized when we combine our type-safe accessors with Remote Config’s real-time update listener. This allows for the creation of features that can be enabled or disabled for all active users instantly and safely.

The following example demonstrates how this could be implemented using a simple ValueNotifier for state management. In a real application, a more robust state management solution like Riverpod or BLoC would be used.

// A simple service to manage the feature flag state
class FeatureFlagService {
  // Initialize the notifier with the current, type-safe value
  final enhancedSearchEnabled = ValueNotifier<bool>(
    RemoteConfigParams.enableEnhancedSearch.getValue(),
  );

  FeatureFlagService() {
    // Listen for real-time updates from Firebase
    FirebaseRemoteConfig.instance.onConfigUpdated.listen((event) async {
      // When an update is detected, activate the new config
      await FirebaseRemoteConfig.instance.activate();
      
      // Update the notifier with the new, type-safe value
      enhancedSearchEnabled.value = RemoteConfigParams.enableEnhancedSearch.getValue();
    });
  }
}

// In the UI:
ValueListenableBuilder<bool>(
  valueListenable: myFeatureFlagService.enhancedSearchEnabled,
  builder: (context, isEnabled, child) {
    // The UI automatically rebuilds when the flag changes
    return isEnabled ? EnhancedSearchPage() : ClassicSearchPage();
  },
)

This combination creates a system that is both immensely powerful and remarkably safe. The real-time listener provides the ability to flip a switch in the Firebase console and have it affect all active users within seconds. This is invaluable for deploying a “kill switch” to disable a buggy feature. The danger of such power is that a mistake in the switching mechanism itself could be catastrophic. However, because our implementation uses the type-safe, generated RemoteConfigParams.enableEnhancedSearch.getValue() accessor, we have a compile-time guarantee that our switching mechanism is robust. We are protected from typos and type errors, allowing us to use this powerful real-time capability with a high degree of confidence. This synergy between real-time updates and type safety is a massive victory for operational stability and developer peace of mind.

Conclusion: Write Safer, Ship Faster

Firebase Remote Config is an indispensable tool for the modern Flutter developer, offering the agility to dynamically update applications, test new ideas, and manage features without the friction of constant app store releases. However, we have seen that the standard, string-based API, while functional, introduces a significant class of preventable runtime errors that can lead to silent failures, production crashes, and a brittle codebase that is difficult to maintain.

By adopting a code-generation workflow with a library like remote_config_gen, we elevate our development practice. We shift the responsibility of ensuring correctness from fallible human memory to an automated, reliable build tool. This approach provides:

  • Compile-time safety, eliminating bugs caused by typos and type mismatches.
  • A superior developer experience with IDE autocompletion and effortless navigation.
  • Safe and simple refactoring, allowing the codebase to evolve without fear.

This is not just about convenience; it’s a smart decision to build more professional, reliable, and maintainable applications. By creating a strong, type-safe connection between your app and its remote configuration, you can use the full power of Firebase Remote Config with confidence. This allows development teams to move faster, reduce release risk, and ship better products to their users.

Image

Bootstrap your project Ship everywhere with Flutter. Fast!

Our boilerplate project with Flutter superpowers can help you ship your idea in days instead of months. Release your project for Android, iOS, Desktop and Web with our Flutter starter-kit builder.