Information

Controlling Rollouts in Production: Feature Flags Done Right in Flutter

Master feature flagging in Flutter to dynamically control app features, streamline subscription management, and improve user experience in your fitness app.


Controlling Rollouts in Production: Feature Flags Done Right in Flutter

Introduction

In today’s fast-moving mobile app ecosystem, delivering personalized and controlled user experiences is more critical than ever—especially in fitness applications, where user engagement, monetization, and retention hinge on offering just the right set of features at the right time.

While building our cross-platform fitness app with Flutter, we faced a common but challenging requirement: supporting six different subscription plans—Standard and Premium, each with Weekly, Monthly, and Annual durations. Each plan unlocks a unique combination of features, timelines, and access levels. Managing these variations with hardcoded logic or repetitive conditionals would not only bloat the codebase but also introduce risks during feature rollouts and updates.

To solve this, we implemented a robust Feature Flagging system—a dynamic, scalable, and modular solution for Advanced Feature Management. With this architecture, features can be toggled, tested, or tiered without needing to ship a new version of the app. This enabled us to:

  • Seamlessly gate premium or experimental features,

  • Dynamically control plan-specific access,

  • Implement A/B testing and beta rollouts,

  • Maintain a clean, extensible codebase.

In this article, we'll explore how we designed and implemented this system, the Flutter classes that power it, and how it transformed our ability to ship and manage features confidently in production.

 

The Problem: When simple categorization fails

As our fitness app evolved, we quickly realized that managing user access through just two broad categories—Standard and Premium—wasn’t going to scale. While it seemed straightforward on the surface, the underlying complexity was significant. Here’s why:

  1. Feature & Timeline Fragmentation
    Each of the six plans (Standard: Weekly, Monthly, Annually and Premium: Weekly, Monthly, Annually) had distinct feature sets and durations. Attempting to manage these variations using just a boolean flag like isPremium led to convoluted conditional logic, which became hard to maintain and error-prone. What we needed was a way to decouple features from rigid plan categories.

  2. Initial Plan Detection
    Determining a user's current plan when they entered the app wasn't always reliable—especially after reinstalls or device changes. This made it difficult to ensure the right set of features were active from the moment the user launched the app.

  3. Real-time Plan Retrieval on App Launch
    With dynamic plans and the potential for server-side updates, retrieving the most up-to-date plan details at each app launch became crucial. But fetching and interpreting this data correctly without delaying the UX was a challenge.

  4. Plan Upgrade/Downgrade Management
    Supporting seamless transitions between plans—whether users upgraded from Standard Monthly to Premium Weekly or downgraded from Premium Annual to Standard Monthly—required a flexible, centralized logic to enable and disable the correct features in real-time, without forcing app restarts or deep user interaction.

These limitations signaled the need for a smarter feature management system, one that treated features as independent capabilities rather than static properties of a plan. That’s when we decided to implement a custom Feature Flagging system tailored to our app's unique requirements.

 

 

The Solution: A Scalable Feature Flagging System

To address the growing complexity of subscription-based feature management, we designed a Feature Flagging System tailored for scale, clarity, and flexibility. Here’s how it works:

1. Centralized Plan Identification via Enum

We began by formalizing all possible subscription types using an enum called SubscriptionPlans. This allowed us to treat each plan (Standard Weekly, Premium Monthly, etc.) as a distinct entity, making the logic around access control explicit and type-safe:

enum SubscriptionPlans {
  freeTrial,
  standardWeekly,
  premiumWeekly,
  standardMonthly,
  premiumMonthly,
  standardAnnually,
  premiumAnnually
}



We also implemented a utility function getSubscriptionPlan() to derive the appropriate enum based on backend values like plan name and duration. This minimized the chances of misclassification and centralized plan parsing logic.

 

2. Feature Constants as Identifiers

All current and upcoming features were declared as string constants in a dedicated Features class. This approach provided:

  • A single source of truth for all features,

  • Support for partial or experimental rollout,

  • Easy mapping between feature flags and UI/logic.

class Features {
  static const String realTimeFormCorrection = 'real_time_form_correction';
  static const String gamifiedHabitCoach = 'gamified_habit_formation_coach';
  ...
}

 

3. Access Control via Feature Mapping

We created a featuresAccessMap to define which features were available to each plan. This configuration is declarative, making it easy to audit, test, and expand as new plans or features roll out.

final Map<SubscriptionPlans, Set<String>> featuresAccessMap = {
  SubscriptionPlans.premiumAnnually: {
    Features.realTimeFormCorrection,
    Features.advancedCoachingAI,
    ...
  },
  ...
};

 

This effectively decoupled plan logic from app logic—making the feature access system data-driven instead of hardcoded.

4. A Singleton FeatureManager

At the heart of this system is the FeatureManager singleton. It is initialized once with the current user’s plan and provides a simple hasFeature() method for any feature check throughout the app.

FeatureManager.initialize(userPlan);

if (FeatureManager.instance.hasFeature(Features.realTimeFormCorrection)) {
  // Show this section in the UI
}

 

This pattern ensured:

  • Global availability of feature logic,

  • Consistent feature access checks across the entire app,

  • Seamless upgrades or downgrades without needing app restarts or deep context rewiring.

Together, these components formed a powerful, testable, and scalable solution to the problem of managing subscription-based features in a growing app ecosystem.

 

Real-World Usage: How Feature Flagging Transformed User Experience

Implementing a robust feature flagging system wasn’t just a backend or architectural decision — it directly enhanced the user experience, performance, and flexibility of our fitness application. Here’s how it translated into practical, real-world impact:


1. Seamless Free Trial Experience

New users get immediate access to the Free Trial plan, which unlocks nearly every premium feature for 7 days. This onboarding flow is powered entirely by the feature flagging system:

  • No special-case UI code or hardcoding.

  • Features are gated via FeatureManager.hasFeature(), making the experience consistent across the app.

  • When the trial expires, switching to a limited plan is seamless — feature access just updates automatically based on plan.

2. Dynamic UI Based on User Plan

With a single source of truth (FeatureManager), the app can instantly determine what to show or hide:

if (FeatureManager.instance.hasFeature(Features.advancedNutritionAI)) {
  // Render the advanced nutrition section
}

 

This dynamic control:

  • Prevents UI clutter for non-eligible users,

  • Encourages plan upgrades by graying out premium sections,

  • Keeps the UX personalized and relevant.


3. Support for Upgrades and Downgrades

When users upgrade or downgrade their subscription (e.g., from Standard Monthly to Premium Annual), the app doesn’t need a restart or reinitialization of all layers:

  • The new plan is passed to FeatureManager.initialize(),

  • The new feature set becomes available instantly,

  • All views react accordingly since they're built around hasFeature() logic.

This allows for:

  • In-app plan switching,

  • Instant gratification after upgrades (crucial for user retention),

  • Zero risk of old feature access lingering post-downgrade.


4. Feature Experiments and Gradual Rollouts

Some features are still under development or being A/B tested. Our system makes it easy to expose these features to only certain plans (or even specific user cohorts later on):

  • Features like advancedCoachingAI, gamifiedHabitCoach, or wearablesIntegration are already defined in the Features class.

  • As they’re ready to roll out, we simply update the featuresAccessMap—no need to touch the UI or business logic layers.

This keeps development decoupled and makes gradual releases safer.


5. Powerful for Marketing and Monetization

By explicitly tying features to plans in a single map, we also unlocked several strategic advantages:

  • The marketing team can now clearly communicate plan benefits.

  • The sales team can design targeted upsell prompts for locked features.

  • Referral programs or limited-time unlocks become trivial to implement via temporary plan overrides.


This system not only simplified engineering but also created a foundation for flexible growth, faster iteration, and a personalized fitness journey for every user.

Key Takeaways: Building Scalable Feature Management in a Subscription-Based App

Designing a scalable and dynamic feature flagging system was a game-changer for our fitness app. Here are the key takeaways from the journey:


Treat Subscription Plans as First-Class Citizens

Instead of just "Standard" and "Premium," we acknowledged the nuance across timeframes (weekly, monthly, annually) and built a dedicated enum to represent each unique plan. This clarity helped eliminate ambiguity across both logic and UI.


One Centralized Source of Truth

By maintaining a central map (featuresAccessMap) linking plans to features, and routing all access checks through FeatureManager.hasFeature(), we ensured:

  • No duplicate conditionals or scattered feature checks

  • UI, backend sync, and business logic all align automatically


Support Upgrades, Downgrades, and Expirations Effortlessly

Thanks to runtime initialization (FeatureManager.initialize()), any change in plan — whether due to payment, trial expiry, or manual switching — reflects across the app instantly and without app restarts.


Future-Proof Feature Development

Our modular Features class and mapping approach means we can:

  • Add features mid-cycle without touching UI

  • Roll out gradually (A/B tests, phased releases)

  • Toggle feature access remotely if needed (future cloud integration)


Business Agility & Monetization

Product and marketing teams now have a concrete map of what’s tied to which plan. That directly feeds into:

  • Feature-based upselling

  • Dynamic feature previews

  • Promotional campaigns (like unlocking premium features for a week)


What's Next?

We’re already thinking ahead:

  • Remote Config Integration using Firebase or a custom backend to toggle features without releasing updates.

  • User-level feature experimentation for A/B testing new ideas before full rollout.

  • Analytics-backed decision making, tracking which features drive the most upgrades.


Final Thoughts

Building this system wasn't just about writing cleaner code — it was about enabling product scalability, personalized UX, and monetization from the ground up. Whether you're building a fitness app, a productivity tool, or any subscription-based product, investing in smart feature management early pays dividends in the long run.

 

Similar posts

Get notified on new marketing insights

Be the first to know about new B2B SaaS Marketing insights to build or refine your marketing function with the tools and knowledge of today’s industry.