configdirector_flutter_client_sdk 1.0.0 copy "configdirector_flutter_client_sdk: ^1.0.0" to clipboard
configdirector_flutter_client_sdk: ^1.0.0 copied to clipboard

Flutter client SDK for ConfigDirector. ConfigDirector is a remote configuration and feature flag service.

example/lib/main.dart

// Sample app for the ConfigDirector Flutter client SDK: it reads a few configs
// and re-renders as their values change.
//
//   flutter run --dart-define-from-file=env.json

import 'dart:async';

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

// Supplied at build time so the key does not have to be committed. See
// env.example.json.
const String sdkKey = String.fromEnvironment('CONFIGDIRECTOR_SDK_KEY');

void main() => runApp(const SampleApp(sdkKey: sdkKey));

// Owns the client: one instance for the life of the app.
class SampleApp extends StatefulWidget {
  const SampleApp({required this.sdkKey, super.key});

  final String sdkKey;

  @override
  State<SampleApp> createState() => _SampleAppState();
}

class _SampleAppState extends State<SampleApp> {
  ConfigDirectorClient? _client;

  @override
  void initState() {
    super.initState();
    if (widget.sdkKey.isEmpty) {
      return;
    }

    final client = ConfigDirectorClient(
      clientSdkKey: widget.sdkKey,
      // The SDK logs at `warn` by default; turned up here so the connection can
      // be followed in the console.
      options: ConfigDirectorClientOptions(
        logger: ConsoleLogger(level: ConfigDirectorLogLevel.debug),
      ),
    );
    _client = client;

    // Configs return their defaults until this completes, so there is nothing
    // to wait for before building the UI.
    unawaited(client.initialize(_contextFromEnvironment()));
  }

  @override
  void dispose() {
    _client?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final client = _client;

    return MaterialApp(
      title: 'ConfigDirector Flutter Sample',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF0EA5E9)),
      ),
      home: client == null
          ? const _MissingSdkKey()
          : ConfigDirectorScope(client: client, child: const HomePage()),
    );
  }
}

// The context targeting rules are evaluated against. Call `updateContext` to
// change it while the app is running.
ConfigDirectorContext _contextFromEnvironment() {
  const id = String.fromEnvironment('CONFIGDIRECTOR_USER_ID');
  const name = String.fromEnvironment('CONFIGDIRECTOR_USER_NAME');
  const role = String.fromEnvironment('CONFIGDIRECTOR_USER_ROLE');

  return ConfigDirectorContext(
    id: id.isEmpty ? null : id,
    name: name.isEmpty ? null : name,
    traits: role.isEmpty ? null : {'role': role},
  );
}

// Hands the client to the widgets below it: `ConfigDirectorScope.of(context)`.
class ConfigDirectorScope extends InheritedWidget {
  const ConfigDirectorScope({
    required this.client,
    required super.child,
    super.key,
  });

  final ConfigDirectorClient client;

  static ConfigDirectorClient of(BuildContext context) {
    final scope = context
        .dependOnInheritedWidgetOfExactType<ConfigDirectorScope>();
    assert(scope != null, 'No ConfigDirectorScope found above this widget.');
    return scope!.client;
  }

  @override
  bool updateShouldNotify(ConfigDirectorScope oldWidget) =>
      client != oldWidget.client;
}

// Builds from the current value of a config and rebuilds whenever it changes,
// whether from an edit in the dashboard or a context update.
class ConfigValue<T extends Object> extends StatefulWidget {
  const ConfigValue({
    required this.configKey,
    required this.defaultValue,
    required this.builder,
    super.key,
  });

  final String configKey;

  // Determines `T`, and is what gets built until the client is ready.
  final T defaultValue;

  final Widget Function(BuildContext context, T value) builder;

  @override
  State<ConfigValue<T>> createState() => _ConfigValueState<T>();
}

class _ConfigValueState<T extends Object> extends State<ConfigValue<T>> {
  // `watch` hands out a new stream per call, so it is called when the client
  // changes rather than on every build.
  Stream<T>? _values;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _values = ConfigDirectorScope.of(
      context,
    ).watch(widget.configKey, widget.defaultValue);
  }

  @override
  Widget build(BuildContext context) => StreamBuilder<T>(
    stream: _values,
    initialData: widget.defaultValue,
    builder: (context, snapshot) =>
        widget.builder(context, snapshot.data ?? widget.defaultValue),
  );
}

// The keys below are the ones in the ConfigDirector sample project. Against a
// project without them, each config falls back to its default value.
class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(
      title: const Text('ConfigDirector'),
      actions: const [_ReadyIndicator(), SizedBox(width: 16)],
    ),
    body: SafeArea(
      child: ListView(
        padding: const EdgeInsets.all(20),
        children: const [
          _Config<bool>('temporary-feature-flag', true),
          _Config<bool>('permanent-kill-switch', false),
          _Config<int>('integer-config', 10),
          _Config<String>('day-of-the-week-config', 'Friday'),
          _Config<Map<String, dynamic>>('json-value-config', {}),
          Divider(height: 40),
          _ActiveContext(),
        ],
      ),
    ),
  );
}

// One config, read as `T` — which is what its default value determines.
class _Config<T extends Object> extends StatelessWidget {
  const _Config(this.configKey, this.defaultValue);

  final String configKey;
  final T defaultValue;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);

    return ConfigValue<T>(
      configKey: configKey,
      defaultValue: defaultValue,
      builder: (context, value) => ListTile(
        contentPadding: EdgeInsets.zero,
        title: Text(configKey, style: _mono(theme.textTheme.bodySmall!)),
        subtitle: Text('$value', style: theme.textTheme.titleMedium),
      ),
    );
  }
}

class _ReadyIndicator extends StatelessWidget {
  const _ReadyIndicator();

  @override
  Widget build(BuildContext context) {
    final client = ConfigDirectorScope.of(context);

    return Center(
      child: StreamBuilder<ClientReadyEvent>(
        stream: client.onClientReady,
        builder: (context, snapshot) =>
            Text(client.isReady ? 'Ready' : 'Connecting…'),
      ),
    );
  }
}

// The context the configs above were evaluated against.
class _ActiveContext extends StatelessWidget {
  const _ActiveContext();

  @override
  Widget build(BuildContext context) {
    final active = ConfigDirectorScope.of(context).context;
    final theme = Theme.of(context);

    return Text(
      active == null
          ? 'No context — configs are evaluated without one.'
          : 'Context\n'
                'id: ${active.id ?? '—'}\n'
                'name: ${active.name ?? '—'}\n'
                'traits: ${active.traits ?? '—'}',
      style: _mono(theme.textTheme.bodySmall!),
    );
  }
}

class _MissingSdkKey extends StatelessWidget {
  const _MissingSdkKey();

  @override
  Widget build(BuildContext context) => const Scaffold(
    body: Center(
      child: Padding(
        padding: EdgeInsets.all(32),
        child: Text(
          'No client SDK key. Copy env.example.json to env.json, put your '
          'client SDK key in it, and run:\n\n'
          'flutter run --dart-define-from-file=env.json',
          textAlign: TextAlign.center,
        ),
      ),
    ),
  );
}

// The family names differ per platform, so the fallbacks cover the ones the
// sample runs on.
TextStyle _mono(TextStyle style) => style.copyWith(
  fontFamily: 'monospace',
  fontFamilyFallback: const ['Menlo', 'Courier New', 'monospace'],
);
1
likes
150
points
211
downloads

Documentation

Documentation
API reference

Publisher

verified publisherconfigdirector.com

Weekly Downloads

Flutter client SDK for ConfigDirector. ConfigDirector is a remote configuration and feature flag service.

Homepage
Repository (GitHub)
View/report issues
Contributing

Topics

#feature-flags #remote-config #configuration

License

MIT (license)

Dependencies

collection, crypto, flutter, http, package_info_plus, uuid, web

More

Packages that depend on configdirector_flutter_client_sdk