Skip to main content
RapidDev - Software Development Agency
flutterflow-tutorials

How to Manage Multiple App Versions with Different Features in FlutterFlow

Manage feature availability in FlutterFlow by storing feature flags in a Firestore app_config document. Load the config once at app launch, cache it in App State, and wrap feature UI in Conditional visibility widgets that check the flag. Use the package_info_plus package to enforce minimum version gates. For gradual rollouts, assign users a 0-100 random rollout_bucket integer on registration and compare it to the feature's rollout_percentage in the config.

What you'll learn

  • How to create a Firestore app_config document with feature flags for remote control
  • How to load and cache feature flags at app launch using App State
  • How to implement gradual rollouts by assigning users a rollout bucket percentage
  • How to enforce minimum version requirements using package_info_plus
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner11 min read30-45 minFlutterFlow Free+ (package_info_plus requires code export for version gating)March 2026RapidDev Engineering Team
TL;DR

Manage feature availability in FlutterFlow by storing feature flags in a Firestore app_config document. Load the config once at app launch, cache it in App State, and wrap feature UI in Conditional visibility widgets that check the flag. Use the package_info_plus package to enforce minimum version gates. For gradual rollouts, assign users a 0-100 random rollout_bucket integer on registration and compare it to the feature's rollout_percentage in the config.

Remote Feature Flags for FlutterFlow Apps

Shipping a new feature to 100% of users immediately is risky — if there is a bug or unexpected user behavior, everyone is affected. Feature flags solve this by putting a runtime switch between your code and your users. Store the switch in Firestore, and you can turn a feature on or off for all users, a specific user segment, or a gradually expanding percentage — all without publishing a new app version. In FlutterFlow, feature flags are implemented as a Firestore document loaded at startup and cached in App State, with Conditional visibility widgets that check the flag values.

Prerequisites

  • A FlutterFlow project with Firebase and Authentication configured
  • Basic understanding of FlutterFlow App State variables and Conditional visibility
  • A Firebase project with Firestore enabled

Step-by-step guide

1

Create the app_config Firestore document with feature flags

In the Firebase console, open Firestore and create a collection called 'app_config'. Add a single document with the ID 'features'. This document stores all your feature flags as Boolean or Map fields. Create these fields: new_checkout_flow (Boolean, default false), dark_mode_v2 (Boolean, default true), ai_assistant (Boolean, default false), min_required_version (String, e.g., '2.1.0'), maintenance_mode (Boolean, default false), and rollouts (Map — containing sub-fields like new_checkout_flow_percentage: Integer 0-100). In FlutterFlow, import this as a Firestore document in your Firestore panel. You will bind these values to App State variables after loading them at startup.

Expected result: An app_config/features document exists in Firestore with all flag fields set to their default values. You can manually toggle them in the Firebase console to test feature visibility.

2

Load feature flags at app launch and cache in App State

Add App State Boolean variables for each feature flag you want to control: isNewCheckoutEnabled, isAiAssistantEnabled, isMaintenanceMode, etc. Also add a String App State variable called 'minRequiredVersion'. On your Entry Page's initState, add a Backend Query action (one-time fetch, not real-time) targeting app_config/features. In the Backend Query's on-success callback, add Set App State actions for each variable, binding to the corresponding Firestore field. Critically, do this before any other navigation: check isMaintenanceMode first — if true, navigate to a maintenance screen instead of the normal app flow. Checking feature flags on every page is wasteful; loading once at launch and caching in App State is the correct pattern.

Expected result: App State reflects the Firestore flag values within 1 second of app launch. Toggling a flag in Firestore takes effect on the next app launch (or implement a refresh on foreground for near-real-time updates).

3

Gate feature UI with Conditional visibility widgets

For each feature controlled by a flag, wrap its UI in a Conditional visibility widget. Select the widget or container that represents the feature, go to its Properties panel, and toggle the 'Visible' property to a Condition. Set the condition to: App State > isNewCheckoutEnabled equals true. This makes the widget completely invisible (and not rendered at all) when the flag is false. For feature flags that replace existing functionality (like a new checkout flow replacing the old one), show the new UI when the flag is true and the old UI when it is false — use two parallel containers with opposing conditions: one visible when true, one visible when false.

Expected result: Toggling a flag in the Firebase console and restarting the app shows or hides the corresponding UI. The flag controls exactly the intended feature without affecting other parts of the app.

4

Implement gradual rollout using user rollout buckets

Gradual rollout releases a feature to an increasing percentage of users. When a user registers, assign them a random integer between 0 and 99 and store it in their Firestore user document as 'rollout_bucket'. This value never changes — it stably assigns the user to a consistent cohort. In your feature flag document, store each feature's rollout percentage: new_checkout_flow_percentage: 25 means users with rollout_bucket 0-24 see the feature. In your App State initialization, add a comparison: isNewCheckoutEnabled = (userRolloutBucket < featureRolloutPercentage). To roll out to more users, increase the percentage in Firestore — no app update required.

custom_actions/assign_rollout_bucket.dart
1// Custom Action: assign rollout bucket on registration
2// Run once during user registration flow
3import 'dart:math';
4import 'package:cloud_firestore/cloud_firestore.dart';
5import 'package:firebase_auth/firebase_auth.dart';
6
7Future<void> assignRolloutBucket() async {
8 final uid = FirebaseAuth.instance.currentUser?.uid;
9 if (uid == null) return;
10 final userRef = FirebaseFirestore.instance.collection('users').doc(uid);
11 final snap = await userRef.get();
12 // Only assign if not already assigned
13 if (snap.data()?.containsKey('rollout_bucket') == true) return;
14 final bucket = Random().nextInt(100); // 0-99
15 await userRef.update({'rollout_bucket': bucket});
16}

Expected result: New users receive a rollout_bucket integer between 0-99 on registration. Users with bucket 0-24 see the feature when rollout_percentage is 25. Increasing rollout_percentage to 50 automatically includes users 25-49.

5

Enforce minimum version requirements and maintenance mode

After code export, add the package_info_plus package (version ^5.0.1) to pubspec.yaml. Create a Custom Action called 'checkAppVersion' that calls PackageInfo.fromPlatform() to get the current app version string, compares it to the minRequiredVersion App State variable using semantic version comparison, and if the app is below the minimum, shows a non-dismissible dialog with an 'Update App' button that calls launchUrl() to open the app store listing. For maintenance mode, in your Entry Page initState check the isMaintenanceMode App State variable immediately after the feature flag load — if true, navigate to a MaintenancePage with an estimated return time loaded from Firestore.

custom_actions/check_app_version.dart
1// custom_actions/check_app_version.dart
2import 'package:package_info_plus/package_info_plus.dart';
3
4// Returns true if current version meets minimum requirement
5// Version format: major.minor.patch (e.g., '2.1.0')
6Future<bool> checkAppVersion(String minVersion) async {
7 final info = await PackageInfo.fromPlatform();
8 final current = info.version;
9 return _compareVersions(current, minVersion) >= 0;
10}
11
12int _compareVersions(String v1, String v2) {
13 final parts1 = v1.split('.').map(int.tryParse).toList();
14 final parts2 = v2.split('.').map(int.tryParse).toList();
15 for (int i = 0; i < 3; i++) {
16 final p1 = (i < parts1.length ? parts1[i] : 0) ?? 0;
17 final p2 = (i < parts2.length ? parts2[i] : 0) ?? 0;
18 if (p1 != p2) return p1.compareTo(p2);
19 }
20 return 0;
21}

Expected result: Setting minRequiredVersion to '999.0.0' in Firestore forces the update dialog to appear. Setting it back to '1.0.0' dismisses it. Toggling maintenance_mode to true shows the maintenance screen immediately on next launch.

Complete working example

custom_actions/feature_flags.dart
1// feature_flags.dart — Load and evaluate feature flags from Firestore
2// Call loadFeatureFlags() from Entry Page initState
3// Use evaluateFlag() to check if a feature is enabled for the current user
4
5import 'package:cloud_firestore/cloud_firestore.dart';
6import 'package:firebase_auth/firebase_auth.dart';
7
8class FeatureFlags {
9 static FeatureFlags? _instance;
10 static FeatureFlags get instance => _instance ??= FeatureFlags._();
11 FeatureFlags._();
12
13 Map<String, dynamic> _flags = {};
14 int _userRolloutBucket = 100; // Default: not in any rollout
15
16 bool get maintenanceMode => _flags['maintenance_mode'] == true;
17 String get minRequiredVersion => _flags['min_required_version'] as String? ?? '1.0.0';
18
19 Future<void> load() async {
20 final uid = FirebaseAuth.instance.currentUser?.uid;
21 try {
22 // Load feature flags
23 final flagSnap = await FirebaseFirestore.instance
24 .collection('app_config').doc('features')
25 .get().timeout(const Duration(seconds: 3));
26 _flags = flagSnap.data() ?? {};
27
28 // Load user's rollout bucket
29 if (uid != null) {
30 final userSnap = await FirebaseFirestore.instance
31 .collection('users').doc(uid)
32 .get().timeout(const Duration(seconds: 3));
33 _userRolloutBucket =
34 (userSnap.data()?['rollout_bucket'] as int?) ?? 100;
35 }
36 } catch (e) {
37 // Use cached/default values on load failure
38 print('Feature flags load failed: $e');
39 }
40 }
41
42 /// Check if a feature is enabled for the current user.
43 /// Handles: simple Boolean flags, rollout percentage flags.
44 bool isEnabled(String flagName) {
45 final flag = _flags[flagName];
46 if (flag == null) return false;
47 if (flag is bool) return flag;
48 if (flag is Map) {
49 final enabled = flag['enabled'] as bool? ?? false;
50 if (!enabled) return false;
51 final rolloutPct = flag['rollout_percentage'] as int? ?? 0;
52 return _userRolloutBucket < rolloutPct;
53 }
54 return false;
55 }
56
57 /// Check if user is in a specific A/B test variant
58 String? getVariant(String experimentName) {
59 final experiment = _flags['experiments'];
60 if (experiment is! Map) return null;
61 final exp = experiment[experimentName];
62 if (exp is! Map) return null;
63 final variants = exp['variants'] as List?;
64 if (variants == null || variants.isEmpty) return null;
65 // Assign deterministically based on rollout bucket
66 final variantIndex = _userRolloutBucket % variants.length;
67 return variants[variantIndex] as String?;
68 }
69}
70
71// Convenience Custom Action for FlutterFlow
72Future<void> loadFeatureFlags() async {
73 await FeatureFlags.instance.load();
74}
75
76// Check a Boolean feature flag
77bool isFeatureEnabled(String flagName) {
78 return FeatureFlags.instance.isEnabled(flagName);
79}
80
81// Get A/B test variant for an experiment
82String getExperimentVariant(String experimentName) {
83 return FeatureFlags.instance.getVariant(experimentName) ?? 'control';
84}

Common mistakes

Why it's a problem: Checking feature flags with a Backend Query on every widget that uses them

How to avoid: Load the entire app_config/features document once at app launch, store all flag values in App State variables, and check App State in each widget's Conditional visibility. Single read at launch, zero reads per page after that.

Why it's a problem: Using App State to store feature flags without handling load failure

How to avoid: Define sensible safe defaults for each flag (most features on, maintenance mode off). On Firestore load failure, keep the current App State values unchanged rather than resetting to empty. Log the failure for monitoring.

Why it's a problem: Changing a user's rollout_bucket after initial assignment

How to avoid: Assign the rollout bucket once during registration and never change it. The assignRolloutBucket Custom Action should check if the field already exists before writing, as shown in the example code.

Why it's a problem: Storing the minimum required version as an integer instead of a semantic version string

How to avoid: Always store and compare version numbers as semantic version strings ('major.minor.patch'). Compare each component as an integer in sequence, as shown in the checkAppVersion Custom Action.

Best practices

  • Load feature flags once at app launch and cache in App State — never fetch per-page or per-widget.
  • Use rollout buckets (0-99) assigned at registration for gradual rollouts rather than user segments based on recency.
  • Start every new feature rollout at 5% for 24 hours before increasing to 25%, 50%, and 100%.
  • Always define safe default values for all flags so the app functions correctly when Firestore is unreachable.
  • Document every feature flag in a comment inside the app_config document or a separate team wiki — abandoned flags accumulate and create confusion without documentation.
  • Set a TTL: remove old flags from Firestore and the codebase within 2 weeks of a feature reaching 100% rollout. Dead flags become technical debt.
  • Use separate app_config documents for different environments (app_config/features_dev, app_config/features_prod) so you can test flag changes in development without affecting production users.

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I'm implementing feature flags for a FlutterFlow app using Firestore. Write a Dart FeatureFlags singleton class that: fetches an app_config/features Firestore document on load, handles both simple Boolean flags and Map flags with 'enabled' Boolean and 'rollout_percentage' Integer sub-fields, compares a user's rollout_bucket integer against rollout_percentage for gradual rollout evaluation, and caches results so subsequent calls do not re-fetch Firestore. Include a 3-second timeout fallback.

FlutterFlow Prompt

In my FlutterFlow project I have an App State Boolean variable called 'isNewCheckoutEnabled'. I want to show a 'New Checkout' Container when this is true and a 'Classic Checkout' Container when it is false. I also have a Backend Query on app_config/features that loads the feature flag at app launch. Walk me through: setting up the Backend Query on my splash page, mapping the Firestore 'new_checkout_flow' Boolean field to the App State variable, and configuring Conditional visibility on both checkout Containers.

Frequently asked questions

Can I update a feature flag and have it take effect immediately without users restarting the app?

Yes. Change the Backend Query on your Entry Page (or a separate app config Component) from 'One Time' to 'Real Time'. Now whenever the Firestore document changes, the App State variables update immediately and any Conditional widgets respond. The trade-off is one persistent Firestore listener per active user.

How is this different from Firebase Remote Config?

Firebase Remote Config is Google's purpose-built feature flag service with A/B testing, condition-based targeting, and a dedicated dashboard. It is more powerful than the Firestore approach but requires adding the firebase_remote_config package after code export. The Firestore approach described here is simpler to set up within FlutterFlow's visual editor and gives you full control of the data structure.

What is the difference between feature flags and user roles?

Feature flags control whether a feature exists for a set of users. User roles control what actions a user is permitted to take. Both can gate UI, but feature flags are temporary (removed when the feature is fully rolled out) while roles are permanent business logic.

Can I show different app store screenshots for users in the new checkout experiment?

App store screenshots are static — you cannot target them by A/B variant. However, you can use different App Store Connect product pages (iOS 15+) for different A/B groups if you set up product page optimization in App Store Connect.

How do I handle a feature flag that was set to false for a user who is mid-flow through the new feature?

Add an App State listener on the feature flag. If the flag turns off while the user is on a new-feature page, navigate them back to the classic equivalent page with a gentle message: 'This feature is temporarily unavailable.' Don't abandon users mid-checkout or mid-form.

How many feature flags can I have in one Firestore document?

A Firestore document has a 1MB size limit. Storing 1,000 simple Boolean flags would use roughly 50KB — well within limits. In practice, apps rarely need more than 20-50 active flags. Regularly remove flags for features that are 100% rolled out to keep the document clean.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.