← Back to posts Cover image for Mastering Feature Flags in Flutter: A Practical Guide to OpenFeature Integration

Mastering Feature Flags in Flutter: A Practical Guide to OpenFeature Integration

· 5 min read
Weekly Digest

The Flutter news you actually need

No spam, ever. Unsubscribe in one click.

Chris
By Chris

Mastering Feature Flags in Flutter: A Practical Guide to OpenFeature Integration

You’ve spent weeks building a brilliant new feature for your app. The design is pixel-perfect, the logic is robust, and the animations are silky smooth. But the moment you hit the release button, anxiety sets in. What if there’s a hidden bug affecting all your users? What if the new UI confuses a segment of your audience? Releasing software doesn’t have to be an all-or-nothing gamble. This is where feature flags come to the rescue.

Feature flags (or feature toggles) are a powerful technique that allows you to decouple deployment from release. They let you control the availability of features in your app dynamically, without requiring a new app store submission or server deployment. You can enable features for specific user segments, run A/B tests, perform gradual rollouts, or create an instant kill switch if something goes wrong.

While you can roll your own feature flag system, managing the logic, consistency across client and server, and scaling can become a burden. This is where OpenFeature shines. It’s an open-source, vendor-agnostic specification that standardizes how you evaluate feature flags. It separates the flag evaluation logic from the flag management backend (like LaunchDarkly, Split, or your own service), making your code cleaner and your architecture more flexible.

Let’s dive into how you can integrate OpenFeature into your Flutter and Dart applications.

Setting Up OpenFeature in Flutter

First, add the necessary dependencies to your pubspec.yaml. We’ll need the core OpenFeature SDK and a provider. For this example, we’ll use a simple in-memory provider, but you can easily swap it for an HTTP provider connecting to a commercial service.

dependencies:
  flutter:
    sdk: flutter
  openfeature: ^0.1.0

For a basic setup, you can initialize OpenFeature in your app’s main entry point.

import 'package:openfeature/openfeature.dart';
import 'package:flutter/material.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // 1. Create an in-memory provider for demonstration
  final provider = InMemoryProvider();
  
  // 2. Add some sample feature flag rules
  provider.addFlag(
    Flag(
      key: 'newCheckoutUi',
      state: FlagState.enabled,
      targetingRules: [
        // Enable for 50% of users
        Rule(
          condition: TargetingConditions.randomPercentage(50),
          variant: 'enabled',
        ),
        Rule(
          condition: TargetingConditions.alwaysTrue(),
          variant: 'disabled',
        ),
      ],
    ),
  );

  // 3. Initialize the OpenFeature API with our provider
  await OpenFeature.instance.setProvider(provider);

  runApp(const MyApp());
}

Using Feature Flags in Your Widgets

Once initialized, evaluating a feature flag is straightforward. The OpenFeature client provides a clean, asynchronous API. It’s best practice to centralize your flag evaluations, perhaps in a service class.

class FeatureFlagService {
  static final client = OpenFeature.instance.client;

  static Future<bool> isFeatureEnabled(String key, {Object? context}) async {
    final evaluation = await client.getBooleanValue(
      key: key,
      defaultValue: false, // The fallback value if the flag is undefined
      evaluationContext: context, // Optional user/targeting context
    );
    return evaluation.value;
  }
}

Now, in your UI, you can conditionally render widgets based on the flag’s status. This pattern is perfect for wrapping new features.

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Feature Flags Demo')),
      body: FutureBuilder<bool>(
        future: FeatureFlagService.isFeatureEnabled('newCheckoutUi'),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }
          
          final showNewUi = snapshot.data ?? false;

          return Column(
            children: [
              ElevatedButton(
                onPressed: () {
                  Navigator.push(
                    context,
                    MaterialPageRoute(
                      // Navigate to different screens based on the flag
                      builder: (_) => showNewUi 
                          ? const NewCheckoutScreen()
                          : const OldCheckoutScreen(),
                    ),
                  );
                },
                child: const Text('Go to Checkout'),
              ),
              // Or conditionally render parts of a widget
              if (showNewUi) ...[
                const SizedBox(height: 20),
                const Text('✨ You are seeing the new UI!'),
              ],
            ],
          );
        },
      ),
    );
  }
}

Advanced Usage: Targeting and Context

The real power of feature flags lies in targeted rollouts. You can enable features for specific users, app versions, or custom attributes. To do this, you provide an EvaluationContext when evaluating a flag.

Let’s extend our service to support user-based targeting:

class UserContext {
  final String userId;
  final String tier; // e.g., 'free', 'premium'
  final String countryCode;

  const UserContext({
    required this.userId,
    required this.tier,
    required this.countryCode,
  });

  // Convert to an OpenFeature EvaluationContext
  EvaluationContext toEvaluationContext() {
    return EvaluationContext(
      targetingKey: userId,
      attributes: {
        'tier': AttributeValue.string(tier),
        'country': AttributeValue.string(countryCode),
      },
    );
  }
}

Now, when checking a flag, pass in the user’s context. Your flag management backend can then define rules like “enable for premium users in Canada.”

Future<bool> isCheckoutUiEnabledForUser(UserContext user) async {
  return FeatureFlagService.isFeatureEnabled(
    'newCheckoutUi',
    context: user.toEvaluationContext(),
  );
}

Common Mistakes and Best Practices

  1. Hardcoding Defaults as “Enabled”: Always set a safe, conservative defaultValue (like false). This ensures your app remains stable if your flag provider fails or a flag key is mistyped.
  2. Forgetting Cleanup: Feature flags are not permanent. Establish a process to remove flags once a feature is fully launched and stable. Lingering flags increase code complexity.
  3. Overusing Boolean Flags: OpenFeature supports strings, numbers, and objects as flag values. Use string variants ('control', 'variantA') for A/B tests or complex configuration objects to pass dynamic parameters to your features.
  4. Mixing Flag Logic with Business Logic: Keep your flag evaluation code separate, as shown in the FeatureFlagService. This makes it easy to mock for tests and swap out the underlying provider.
  5. Not Planning for Failure: Network calls to external flag providers can fail. Use timeouts, caching strategies (which many providers offer), and sensible fallback values to ensure your app is resilient.

Wrapping Up

Integrating OpenFeature into your Flutter workflow brings professionalism and control to your release process. It transforms feature releases from a stressful single-point event into a managed, measurable, and reversible operation. By abstracting the flag evaluation logic, you future-proof your application, giving you the freedom to change your flag management backend without rewriting your application code.

Start by wrapping your next new feature in a flag. You’ll gain the confidence to ship more frequently, experiment safely, and ultimately deliver a better experience to your users. Happy (and safe) shipping

This blog is produced with the assistance of AI by a human editor. Learn more

Related Posts

Cover image for Flutter App Size Optimization: Strategies to Shrink Your APK/IPA Below 10MB

Flutter App Size Optimization: Strategies to Shrink Your APK/IPA Below 10MB

Flutter apps are often criticized for their larger-than-native binary sizes. This post will dive into practical, actionable strategies to significantly reduce your Flutter app's footprint, including build configurations, asset optimization, and dependency management, demonstrating how to achieve app sizes under 10MB.

Cover image for Mastering In-App Subscriptions in Flutter: A Guide to Avoiding Common Pitfalls

Mastering In-App Subscriptions in Flutter: A Guide to Avoiding Common Pitfalls

Implementing in-app subscriptions in Flutter can be surprisingly complex, especially when dealing with cross-platform nuances, receipt validation, and managing different store configurations. This post will delve into the common challenges developers face, compare solutions like RevenueCat with custom implementations, and provide best practices for setting up and maintaining a robust subscription system.

Cover image for Taming Android Emulator RAM: A Developer's Guide to Reducing Memory Usage for Flutter

Taming Android Emulator RAM: A Developer's Guide to Reducing Memory Usage for Flutter

Android emulators are notorious for hogging RAM, often pushing developer machines into swap and slowing down Flutter development. This post will explore the reasons behind high emulator memory consumption and provide practical, actionable strategies and tools (including a custom-built solution) to significantly reduce RAM usage, ensuring a smoother development experience without sacrificing functionality.