Abrova
☰ Docs menu · Feature flags SDK
Coming soon. Abrova A/B Testing is coming soon. This page describes how it will work; it can’t be used yet.

A/B Testing — Feature Flags add-on

AbrovaFeatureFlags is the standalone feature flags client that ships in the Flutter A/B Testing package (abrova_ab_testing). If you only need feature flags without experiments, use it or the dedicated Abrova Flags service.

Flutter 0.2.3Dart 3.0+Last updated: 27 Oct 2026
Looking for standalone Feature Flags?

This page documents AbrovaFeatureFlags, the standalone Flutter feature flags client that ships in the A/B Testing package abrova_ab_testing. If you want full feature flag platform support with iOS, Android, React Native, Node.js, and Next.js SDKs, see the dedicated Abrova Flags service instead. This page is for apps that use (or plan to use) the A/B Testing SDK and just need lightweight feature flags in Flutter. The other A/B Testing SDKs expose flags as methods on the main client (isFeatureEnabled, getFeatureValue, getFeatureFlags); see their SDK pages.

Lightweight & Independent.

AbrovaFeatureFlags is a standalone class for feature flag management. No experiment setup, no event tracking: just feature flags. Use it if you only need feature flags without experiments. It is initialized on its own and does not need AbrovaAbTesting.init().

User tokens.

Evaluating a flag for a user (isEnabled, getValue) needs a signed user token, minted by your backend. Add a tokenProvider to your config (step 3). Listing flags (getAll, refresh) works with the API key alone, so the app can load them before sign-in.

New in 0.2.2: you can initialize before sign-in. The SDK waits for the user token instead of failing (see step 3).

Requirements.
  • Dart 3.0.0 or higher (Flutter 3.10+).
  • Pure Dart: works on Android, iOS, Web, macOS, Windows and Linux.
  • A backend endpoint that mints Abrova user tokens (see step 3).

Installation#

The add-on ships in abrova_ab_testing. Install it from Abrova Center: add the package to your pubspec.yaml with the Abrova Center address:

yaml
dependencies:
  abrova_ab_testing:
    hosted: https://center.abrova.ir/api/packages/abrova/pub
    version: ^0.2.3

Install:

bash
flutter pub get

Platform-specific setup

Android: Add Internet permission in android/app/src/main/AndroidManifest.xml:

xml
<uses-permission android:name="android.permission.INTERNET" />

Quick Start#

Get up and running in a few lines of code:

dart
import 'package:abrova_ab_testing/abrova_ab_testing.dart';

// Initialize
final flags = await AbrovaFeatureFlags.init(
  AbrovaFeatureFlagsConfig(
    apiKey: 'YOUR_API_KEY',
    tokenProvider: fetchAbrovaToken, // returns a user token from your backend (step 3)
  ),
);

// Set the signed-in user
await flags.setUserId('user-123');

// Check a flag
if (await flags.isEnabled('dark-mode')) {
  // Feature is enabled
}

User tokens#

Anyone can read the API key out of your app, so on its own it can't prove which user a request is for. Your backend can: it holds your project's server secret and mints a short-lived token for the signed-in user. The SDK sends that token, and the service takes the user from it.

Mint the token on your server

bash
curl -X POST https://auth.abrova.ir/users/token \
  -H "x-api-key: $ABROVA_API_KEY" \
  -H "x-server-secret: $ABROVA_SERVER_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"userId": "user-123", "expiresIn": 3600}'

# → { "token": "eyJhbGciOi…", "userId": "user-123", "expiresIn": 3600, "expiresAt": "…" }

expiresIn is in seconds: 3600 (1 hour) by default, 86400 (24 hours) maximum. On a Node.js backend, createUserToken(userId) from the Node.js SDK makes this call for you. Never put the server secret in the app.

One token for A/B Testing, Chat and Sync

The token is the same Abrova user token used by Abrova Chat and Abrova Sync. If your app uses them too, mint one token and give it to every SDK's tokenProvider instead of fetching one per product.

Give it to the SDK

Pass a tokenProvider that calls your backend. The SDK caches the token, asks again shortly before it expires, and asks once more if the service reports it expired. Return null while no one is signed in.

dart
import 'dart:convert';
import 'package:http/http.dart' as http;

Future<String?> fetchAbrovaToken() async {
  if (sessionToken == null) return null; // no one signed in: the SDK waits
  // Call YOUR backend, authenticated as the signed-in user
  final res = await http.post(
    Uri.parse('https://your-api.example.com/abrova-token'),
    headers: {'Authorization': 'Bearer $sessionToken'}, // your app's own session
  );
  if (res.statusCode != 200) throw Exception('Could not get an Abrova token');
  return jsonDecode(res.body)['token'] as String;
}

If you already hold a token, you can pass it as userToken instead, and hand over a newer one with setUserToken(token). tokenProvider is preferred: a static token expires.

Before your user signs in

You can call init before anyone is signed in. While there is no token (tokenProvider returned null or failed, or none was set) the SDK is awaiting a user token: flags are answered from the cached flag list, or your default is returned, and nothing is sent. No error is reported.

dart
final flags = AbrovaFeatureFlags.instance;

flags.isAwaitingUserToken;            // true while there is no user token
flags.onAwaitingUserToken(() { });    // called each time the SDK starts waiting

// After sign-in
await flags.refreshUserToken();       // with a tokenProvider: asks it again now
await flags.setUserToken(token);      // without one: hand the token over

// After sign-out
await flags.clearUserId();            // or setUserToken(null)

The init callback reports the same moment as an awaitingUserToken event. To add or remove the provider after init, call setTokenProvider(provider).

Keep it on the device.

A token is valid until it expires, so store it with the user's session (Keychain, Keystore or secure storage) and return it from your tokenProvider across app launches. Your app then only calls your backend when the token is close to expiry, not on every start.

Initialization#

Initialize the SDK in your app's main function. The SDK uses a singleton pattern — initialize once, access anywhere.

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

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

  // Initialize with full config
  await AbrovaFeatureFlags.init(
    AbrovaFeatureFlagsConfig(
      apiKey: 'YOUR_API_KEY',
      tokenProvider: fetchAbrovaToken, // User token from your backend
      debug: true,              // Enable debug logging
      enableOfflineCache: true, // Cache flags for offline use (default: true)
    ),
    callback: (event, data) {
      print('FeatureFlags event: $event, data: $data');
    },
  );

  runApp(const MyApp());
}

// Access the instance anywhere after init
final flags = AbrovaFeatureFlags.instance;

Configuration Options

OptionTypeRequiredDescription
apiKeyStringrequiredYour project API key
baseUrlStringhttps://abtest.abrova.irAPI base URL. Must be https; plain http is accepted only for localhost, 127.0.0.1 and 10.0.2.2
tokenProviderFutureOr<String?> Function()nullReturns a user token minted by your backend, or null while no one is signed in. Needed to evaluate flags for a user
userTokenStringnullA token you already hold (tokenProvider is preferred: a static token expires)
debugboolfalseEnable debug logging
enableOfflineCachebooltrueCache flags locally for offline use

User Identification#

Set a user ID for rollout percentage targeting and user-based flag evaluation. Set user attributes for advanced targeting rules. The service takes the user from the token, so the user ID must match the user your tokenProvider returns a token for.

dart
final flags = AbrovaFeatureFlags.instance;

// Set user ID (persisted across sessions)
await flags.setUserId('user-123');

// Forget the user at sign-out
await flags.clearUserId();

// Get current user ID
final userId = flags.getUserId();

// Set user attributes for targeting rules
flags.setUserAttributes({
  'plan': 'premium',
  'country': 'US',
  'age': 25,
  'is_beta_user': true,
});

Calling setUserId with a different user, handing over a token for a different user, or calling clearUserId() clears the previous user's attributes and token. The next evaluation asks your tokenProvider for the new user's token, so set the new user's attributes after the switch.

Checking Feature Flags#

Boolean Flags (isEnabled)

Check if a feature flag is enabled. Returns the server-evaluated result for the user (sent with the user token). If the service can't be asked (offline, or no user token yet), the SDK uses the service's last answer for that user, then the cached flag list, and returns your default when the flag isn't cached.

dart
final flags = AbrovaFeatureFlags.instance;

// Simple check
if (await flags.isEnabled('dark-mode')) {
  enableDarkMode();
}

// With default value (returned if flag not found)
final showBanner = await flags.isEnabled('promo-banner', defaultValue: false);

Value Flags (getValue)

Get the value of a feature flag. Useful for multivariate flags that return strings, numbers, or JSON.

dart
final flags = AbrovaFeatureFlags.instance;

// Get a string value
final buttonColor = await flags.getValue('button-color', defaultValue: 'blue');

// Get a numeric value
final maxRetries = await flags.getValue('max-retries', defaultValue: 3);

// Get a JSON config value
final config = await flags.getValue('onboarding-config', defaultValue: {});

Get All Flags

dart
// Fetch all flags (server call with cache fallback; API key only)
final allFlags = await AbrovaFeatureFlags.instance.getAll();

for (final flag in allFlags) {
  print('${flag.key}: enabled=${flag.enabled}, value=${flag.defaultValue}');
  if (flag.variants != null) {
    for (final variant in flag.variants!) {
      print('  variant ${variant.key}: ${variant.value} (weight: ${variant.weight})');
    }
  }
}

Targeting Rules#

Feature flags support advanced targeting rules based on user attributes. Rules are configured in the dashboard and evaluated by the service. When the service can't be asked, the SDK falls back to the cached flag list.

Supported Operators

OptionTypeRequiredDescription
equals--Exact match: {"country": {"equals": "US"}}
notEquals--Not equal: {"plan": {"notEquals": "free"}}
in--Value in list: {"country": {"in": ["US", "CA"]}}
notIn--Value not in list: {"plan": {"notIn": ["free"]}}
greaterThan--Numeric comparison: {"age": {"greaterThan": 18}}
lessThan--Numeric comparison: {"age": {"lessThan": 65}}
greaterThanOrEqual--Numeric comparison: {"age": {"greaterThanOrEqual": 18}}
lessThanOrEqual--Numeric comparison: {"age": {"lessThanOrEqual": 65}}
contains--String contains: {"email": {"contains": "@company.com"}}
regex--Regex match: {"email": {"regex": ".*@company\.com$"}}
exists--Attribute exists: {"premium": {"exists": true}}

Setting Attributes for Targeting

dart
// Set attributes that match your targeting rules
AbrovaFeatureFlags.instance.setUserAttributes({
  'country': 'US',
  'plan': 'premium',
  'age': 25,
  'email': 'user@company.com',
});

// Flag with targeting rule {"country": {"in": ["US", "CA"]}}
// will be enabled for this user
final enabled = await AbrovaFeatureFlags.instance.isEnabled('us-feature');

Rollout Percentage#

Feature flags support gradual rollouts using percentage-based targeting. Rollout is decided per user, so the same user always gets the same result.

dart
// User ID is required for rollout percentage to work
await AbrovaFeatureFlags.instance.setUserId('user-123');

// A flag with 25% rollout will be enabled for ~25% of users
// The same user always gets the same result
final enabled = await AbrovaFeatureFlags.instance.isEnabled('new-feature');

Multivariate Flags#

Feature flags can have multiple variants with weighted distribution. Use getValue() to get the assigned variant value.

dart
// A flag with variants: blue (50%), green (30%), red (20%)
final buttonColor = await AbrovaFeatureFlags.instance.getValue(
  'button-color',
  defaultValue: 'blue',
);

// Use the value
switch (buttonColor) {
  case 'green':
    return Colors.green;
  case 'red':
    return Colors.red;
  default:
    return Colors.blue;
}

Offline Support#

Built-in Offline Support.

The service decides every flag. When it can't be asked (offline, or no user token yet), the SDK uses the service's last answer for that user, then the flag list it cached after the last fetch. The cached answer is an approximation; the service's own answer is used whenever there is one.

How It Works

Server-First Evaluation

When online, flags are evaluated server-side for the signed-in user, using their user token.

Cached Fallback

When the service can't be asked, the SDK answers from the cached flag list, and returns your default for a flag that isn't cached.

Persistent Cache

Flags are cached using SharedPreferences, surviving app restarts.

Consistent Results

The same user always gets the same flag value from the service.

dart
final flags = AbrovaFeatureFlags.instance;

// Check connectivity status
print('Online: ${flags.isOnline}');

// Access cached flags directly (no server call)
final cached = flags.cachedFlags;
print('Cached flags: ${cached.length}');

// Force refresh from server
await flags.refresh();

Event Callbacks#

Listen for SDK events using the callback parameter during initialization.

dart
await AbrovaFeatureFlags.init(
  AbrovaFeatureFlagsConfig(
    apiKey: 'YOUR_API_KEY',
    tokenProvider: fetchAbrovaToken,
  ),
  callback: (event, data) {
    switch (event) {
      case 'initialized':
        print('SDK ready, offline: ${data?['offline']}');
        break;
      case 'featureFlagsRefreshed':
        print('Fetched ${data?['count']} flags');
        break;
      case 'awaitingUserToken':
        print('Waiting for a user token');
        break;
      case 'error':
        print('Error: ${data?['message']}');
        break;
    }
  },
);

Advanced Usage#

Refresh Flags

dart
// Manually refresh flags from server
await AbrovaFeatureFlags.instance.refresh();

Reset State

dart
// Clear cached flags, user and token, and reset the SDK.
// Call AbrovaFeatureFlags.init() again before using it afterwards.
// To sign a user out, call clearUserId() instead.
await AbrovaFeatureFlags.instance.reset();

Dispose

dart
// Clean up resources when done (currently a no-op: nothing to release)
AbrovaFeatureFlags.instance.dispose();

Complete Example#

A complete example showing the Feature Flags SDK in a Flutter app:

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

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

  await AbrovaFeatureFlags.init(
    AbrovaFeatureFlagsConfig(
      apiKey: 'YOUR_API_KEY',
      tokenProvider: fetchAbrovaToken, // see step 3
      debug: true,
    ),
  );

  runApp(const MyApp());
}

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  bool _darkMode = false;
  String _bannerText = 'Welcome!';
  bool _loading = true;

  @override
  void initState() {
    super.initState();
    _loadFlags();
  }

  Future<void> _loadFlags() async {
    final flags = AbrovaFeatureFlags.instance;

    // Set user for targeting
    await flags.setUserId('user-123');
    flags.setUserAttributes({
      'plan': 'premium',
      'country': 'US',
    });

    // Check flags
    final darkMode = await flags.isEnabled('dark-mode');
    final bannerText = await flags.getValue(
      'banner-text',
      defaultValue: 'Welcome!',
    ) as String;

    setState(() {
      _darkMode = darkMode;
      _bannerText = bannerText;
      _loading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    if (_loading) {
      return const Scaffold(
        body: Center(child: CircularProgressIndicator()),
      );
    }

    return Theme(
      data: _darkMode ? ThemeData.dark() : ThemeData.light(),
      child: Scaffold(
        appBar: AppBar(title: const Text('My App')),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(
                _bannerText,
                style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
              ),
              const SizedBox(height: 16),
              Text('Dark mode: ${_darkMode ? "ON" : "OFF"}'),
            ],
          ),
        ),
      ),
    );
  }
}

AbrovaFeatureFlags vs AbrovaAbTesting#

Both classes ship in the abrova_ab_testing package. Choose the one that fits your needs:

OptionTypeRequiredDescription
Feature flags--Both have it
A/B testing--Only AbrovaAbTesting
Event tracking--Only AbrovaAbTesting
Offline caching--Both have it
Targeting rules--Both have it
Rollout %--Both have it
User token--Both need one to act for a user (tokenProvider)
Lightweight--AbrovaFeatureFlags is minimal; AbrovaAbTesting is the full SDK

Use AbrovaFeatureFlags if you only need feature flags. Use AbrovaAbTesting if you also need experiments and event tracking. Each is initialized on its own, and both take the same user token.

For a multi-platform feature flag service (iOS, Android, React Native, Node.js, Next.js), use the dedicated Abrova Flags service instead — it has its own SDKs across all platforms.

Was this page helpful?
Report a problem