0Pricing
Flutter Mobile Development · 课时

远程配置、功能开关与分阶段发布

远程切换功能并逐步发布变更,无需重新提交应用。

远程配置、功能开关与分阶段发布 是 CoddyKit 上的免费 Flutter Mobile Development 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Flutter Mobile Development 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Flutter Mobile Development 课程共包含 4 节课。

本课时的部分内容尚未翻译,以英文显示。

Why Remote Config

App store review cycles are slow. If a feature ships broken, you may wait days for a hotfix to reach users. Remote configuration decouples shipping the code from turning it on.

  • Feature flags gate code paths behind a boolean you control from a server.
  • Remote config delivers tunable values (strings, numbers, JSON) without a new build.
  • Staged rollouts expose a change to a growing percentage of users.

In Flutter the common tool is firebase_remote_config, but the same patterns apply to LaunchDarkly, ConfigCat, or your own backend.

Modeling a Flag Locally

Before wiring any SDK, model your flags as plain Dart so the rest of the app never touches raw strings. A typed config object keeps defaults explicit and makes the code testable.

This is pure Dart with sensible defaults baked in — exactly what you ship if the network is unavailable.

class AppConfig {
  final bool newCheckoutEnabled;
  final int maxUploadMb;
  final String welcomeMessage;

  const AppConfig({
    this.newCheckoutEnabled = false,
    this.maxUploadMb = 10,
    this.welcomeMessage = 'Welcome!',
  });
}

void main() {
  const defaults = AppConfig();
  print('checkout: ${defaults.newCheckoutEnabled}');
  print('maxUploadMb: ${defaults.maxUploadMb}');
  print('welcome: ${defaults.welcomeMessage}');
}

Firebase Remote Config Setup

With Firebase initialized, you grab the FirebaseRemoteConfig singleton, set in-app defaults, and configure fetch behavior via RemoteConfigSettings.

  • fetchTimeout caps how long a fetch waits.
  • minimumFetchInterval throttles fetches to respect quotas. Use a small value in debug, hours in production.

Defaults guarantee the app behaves sanely before the first successful fetch.

import 'package:firebase_remote_config/firebase_remote_config.dart';

Future<FirebaseRemoteConfig> initRemoteConfig() async {
  final rc = FirebaseRemoteConfig.instance;

  await rc.setConfigSettings(RemoteConfigSettings(
    fetchTimeout: const Duration(seconds: 10),
    minimumFetchInterval: const Duration(hours: 6),
  ));

  await rc.setDefaults(const {
    'new_checkout_enabled': false,
    'max_upload_mb': 10,
    'welcome_message': 'Welcome!',
  });

  return rc;
}

Fetch and Activate

Remote Config separates fetch (download values into a local cache) from activate (make the fetched values live). The convenience method fetchAndActivate() does both and returns whether new values were activated.

Always wrap the call: a network failure should fall back to the last activated values or your in-app defaults — never crash the launch path.

Future<void> refreshConfig(FirebaseRemoteConfig rc) async {
  try {
    final updated = await rc.fetchAndActivate();
    if (updated) {
      // New values are now live; rebuild dependent UI.
      print('Remote config activated with new values');
    }
  } catch (e) {
    // Offline or quota hit: keep last-known-good values.
    print('Remote config fetch failed, using cached/defaults: $e');
  }
}

Reading Typed Values

Remote Config stores everything as strings under the hood but exposes typed getters: getBool, getInt, getDouble, getString. Map them back into your AppConfig so the rest of the app stays typed.

Centralize this mapping in one place. If a key name changes, you fix it once.

import 'package:firebase_remote_config/firebase_remote_config.dart';

AppConfig readConfig(FirebaseRemoteConfig rc) {
  return AppConfig(
    newCheckoutEnabled: rc.getBool('new_checkout_enabled'),
    maxUploadMb: rc.getInt('max_upload_mb'),
    welcomeMessage: rc.getString('welcome_message'),
  );
}

Gating UI Behind a Flag

A feature flag should guard a single decision point. Read it once near the top of the widget tree (or from a provider) and branch.

Keep the gate shallow: branch on the flag, not deep inside business logic, so removing the flag later is a clean deletion.

Widget buildCheckout(BuildContext context, AppConfig config) {
  if (config.newCheckoutEnabled) {
    return const NewCheckoutScreen();
  }
  return const LegacyCheckoutScreen();
}

Parsing JSON-Valued Config

For richer payloads, store a JSON string in a single key and decode it. This lets one flag carry a whole structured experiment — themes, thresholds, ordered lists — without adding dozens of keys.

Below is standalone Dart showing the decode-and-fallback pattern you would apply to rc.getString('promo_banner').

import 'dart:convert';

class PromoBanner {
  final String text;
  final int priority;
  PromoBanner(this.text, this.priority);
}

PromoBanner parsePromo(String raw) {
  try {
    final map = jsonDecode(raw) as Map<String, dynamic>;
    return PromoBanner(
      map['text'] as String? ?? '',
      map['priority'] as int? ?? 0,
    );
  } catch (_) {
    return PromoBanner('', 0); // malformed JSON -> safe default
  }
}

void main() {
  final ok = parsePromo('{"text":"Summer Sale","priority":5}');
  print('${ok.text} (${ok.priority})');
  final bad = parsePromo('not-json');
  print('fallback text empty: ${bad.text.isEmpty}');
}

Staged Rollout by Percentage

A staged rollout exposes a feature to, say, 1% of users, then 10%, then 50%, then 100% — watching crash and metric dashboards at each step. Firebase Remote Config does this server-side with percentage conditions based on a stable, randomized user bucket.

The key requirement: bucketing must be stable. A user assigned to the rollout must stay in it across sessions, otherwise the UI flickers and metrics are meaningless.

Stable Client-Side Bucketing

If you roll your own backend instead of Firebase, you implement bucketing on the client. Hash a stable user id into a value in [0, 100) and compare against the rollout threshold. The same id always lands in the same bucket.

This deterministic hashing is the heart of staged rollouts and A/B splits.

int bucketOf(String userId) {
  // Simple stable hash -> 0..99
  var hash = 0;
  for (final code in userId.codeUnits) {
    hash = (hash * 31 + code) & 0x7fffffff;
  }
  return hash % 100;
}

bool isInRollout(String userId, int percent) {
  return bucketOf(userId) < percent;
}

void main() {
  const id = 'user-42';
  print('bucket: ${bucketOf(id)}');
  print('in 10% rollout: ${isInRollout(id, 10)}');
  print('stable on retry: ${bucketOf(id) == bucketOf(id)}');
}

Kill Switches and Safe Defaults

The most valuable flag is a kill switch: a remote boolean that instantly disables a risky feature without a release. Two rules make kill switches reliable:

  • The safe state must be the default. A kill switch should default to false (feature off) so a failed fetch never accidentally enables the risky path.
  • Read the flag at the decision point every time, not once at startup, so flipping it takes effect on the next config refresh.

For Remote Config, schedule a refresh on app resume so a kill switch propagates within one foreground cycle.

// Default OFF => a fetch failure leaves the risky feature disabled.
bool experimentalSyncEnabled(AppConfig config) {
  return config.newCheckoutEnabled; // example risky path
}

void onAppResumed(FirebaseRemoteConfig rc) {
  // Re-fetch so a flipped kill switch reaches the user quickly.
  refreshConfig(rc);
}

Listening for Real-Time Updates

Newer Firebase Remote Config supports real-time updates: a stream that fires when the server publishes new values, so you do not have to wait for the next scheduled fetch. You still call activate() to make them live.

Use this for kill switches where minutes matter. Combine the listener with setState/provider invalidation to rebuild affected widgets.

import 'package:firebase_remote_config/firebase_remote_config.dart';

void subscribeToUpdates(FirebaseRemoteConfig rc, void Function() onChanged) {
  rc.onConfigUpdated.listen((event) async {
    await rc.activate();
    onChanged(); // e.g. trigger a UI rebuild
  });
}

Quick Check

Test your understanding of safe rollout design.

Recap

You learned how to ship features dark and control them remotely:

  • Model flags as a typed AppConfig with explicit defaults.
  • Initialize Remote Config with setDefaults and tuned RemoteConfigSettings, then fetchAndActivate() defensively.
  • Read typed values and gate UI at a single shallow decision point; carry rich payloads as JSON strings.
  • Run staged rollouts with stable percentage bucketing so users stay consistently in or out.
  • Make every kill switch default to its safe state, and use real-time updates plus on-resume refresh so flips propagate fast.

Together these let you decouple deploy from release and respond to incidents without an app-store round trip.

常见问题解答

「远程配置、功能开关与分阶段发布」课时是免费的吗?

是的 — 「远程配置、功能开关与分阶段发布」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Flutter Mobile Development 课程的其余内容,请升级到 CoddyKit PRO。 Flutter Mobile Development 课程共包含 4 节课。

「远程配置、功能开关与分阶段发布」这节课中我会学到什么?

远程切换功能并逐步发布变更,无需重新提交应用。 你通过在浏览器中直接运行的动手代码来练习 Flutter Mobile Development,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Flutter Mobile Development 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Flutter Mobile Development 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。

「远程配置、功能开关与分阶段发布」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Flutter Mobile Development 课中编写并运行代码吗?

能。每节 Flutter Mobile Development 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 构建变体与环境配置
  2. 使用 Fastlane 和 GitHub Actions 实现自动化流水线
  3. 崩溃报告与符号化堆栈跟踪
  4. 远程配置、功能开关与分阶段发布
← 返回 Flutter Mobile Development