chameleon_pilot_sdk 0.1.6 copy "chameleon_pilot_sdk: ^0.1.6" to clipboard
chameleon_pilot_sdk: ^0.1.6 copied to clipboard

Flutter SDK for ChameleonPilot runtime config, updates, maintenance, and feature flags.

example/lib/main.dart

import 'package:chameleon_pilot_sdk/chameleon_pilot_sdk.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart';

const _baseUrl = String.fromEnvironment(
  'CHAMELEON_BASE_URL',
  defaultValue: 'http://10.0.2.2:8090',
);

final navigatorKey = GlobalKey<NavigatorState>();
final localeNotifier = ValueNotifier<Locale>(const Locale('en'));
final themeModeNotifier = ValueNotifier<ThemeMode>(ThemeMode.light);

const _locales = [
  Locale('en', 'US'),
  Locale('fr', 'FR'),
  Locale('es', 'ES'),
  Locale('de', 'DE'),
  Locale('ar', 'SA'),
];

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const ChameleonPilotExampleApp());
}

class ChameleonPilotExampleApp extends StatelessWidget {
  const ChameleonPilotExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<Locale>(
      valueListenable: localeNotifier,
      builder: (context, locale, _) => ValueListenableBuilder<ThemeMode>(
        valueListenable: themeModeNotifier,
        builder: (context, mode, _) => MaterialApp(
          locale: locale,
          themeMode: mode,
          supportedLocales: _locales,
          localizationsDelegates: const [
            GlobalMaterialLocalizations.delegate,
            GlobalWidgetsLocalizations.delegate,
            GlobalCupertinoLocalizations.delegate,
          ],
          navigatorKey: navigatorKey,
          debugShowCheckedModeBanner: false,
          title: 'ChameleonPilot SDK',
          theme: ThemeData(
            colorScheme: ColorScheme.fromSeed(
              seedColor: const Color(0xFF2D6A4F),
            ),
            useMaterial3: true,
          ),
          darkTheme: ThemeData(
            colorScheme: ColorScheme.fromSeed(
              seedColor: const Color(0xFF2D6A4F),
              brightness: Brightness.dark,
            ),
            useMaterial3: true,
          ),
          home: const ApiKeyScreen(),
        ),
      ),
    );
  }
}

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

  @override
  State<ApiKeyScreen> createState() => _ApiKeyScreenState();
}

class _ApiKeyScreenState extends State<ApiKeyScreen> {
  final _apiKeyController = TextEditingController();
  bool _isInitializing = false;
  String? _error;

  @override
  void dispose() {
    _apiKeyController.dispose();
    super.dispose();
  }

  Future<void> _pasteApiKey() async {
    final clipboard = await Clipboard.getData(Clipboard.kTextPlain);
    final text = clipboard?.text?.trim();
    if (text == null || text.isEmpty) return;
    _apiKeyController.text = text;
    setState(() => _error = null);
  }

  Future<void> _initialize() async {
    final apiKey = _apiKeyController.text.trim();
    if (apiKey.isEmpty) {
      setState(() => _error = 'Paste your ChameleonPilot API key first.');
      return;
    }

    FocusScope.of(context).unfocus();
    setState(() {
      _isInitializing = true;
      _error = null;
    });

    try {
      await ChameleonPilot.initialize(baseUrl: _baseUrl, apiKey: apiKey);
      if (!mounted) return;
      Navigator.of(context).pushReplacement(
        MaterialPageRoute(builder: (_) => const SplashGateScreen()),
      );
    } catch (error) {
      if (!mounted) return;
      setState(() {
        _isInitializing = false;
        _error = error.toString();
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: SafeArea(
        child: Center(
          child: SingleChildScrollView(
            padding: const EdgeInsets.all(28),
            child: ConstrainedBox(
              constraints: const BoxConstraints(maxWidth: 440),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  const SizedBox(height: 30),
                  const Text(
                    'Connect ChameleonPilot',
                    textAlign: TextAlign.center,
                    style: TextStyle(
                      color: Colors.white,
                      fontSize: 30,
                      fontWeight: FontWeight.w900,
                    ),
                  ),
                  const SizedBox(height: 10),
                  const Text(
                    'Paste your SDK API key to initialize once and cache the latest runtime config.',
                    textAlign: TextAlign.center,
                    style: TextStyle(color: Colors.white60, height: 1.35),
                  ),
                  const SizedBox(height: 34),
                  TextField(
                    controller: _apiKeyController,
                    minLines: 3,
                    maxLines: 5,
                    style: const TextStyle(color: Colors.white),
                    cursorColor: const Color(0xFF72D3A1),
                    decoration: InputDecoration(
                      labelText: 'API key',
                      labelStyle: const TextStyle(color: Colors.white70),
                      hintText: 'cpk...',
                      hintStyle: const TextStyle(color: Colors.white30),
                      filled: true,
                      fillColor: const Color(0xFF111111),
                      enabledBorder: OutlineInputBorder(
                        borderRadius: BorderRadius.circular(18),
                        borderSide: const BorderSide(color: Colors.white24),
                      ),
                      focusedBorder: OutlineInputBorder(
                        borderRadius: BorderRadius.circular(18),
                        borderSide: const BorderSide(
                          color: Color(0xFF72D3A1),
                          width: 1.5,
                        ),
                      ),
                    ),
                  ),
                  const SizedBox(height: 14),
                  OutlinedButton.icon(
                    onPressed: _isInitializing ? null : _pasteApiKey,
                    icon: const Icon(Icons.content_paste),
                    label: const Text('Paste API key'),
                    style: OutlinedButton.styleFrom(
                      foregroundColor: Colors.white,
                      side: const BorderSide(color: Colors.white24),
                      padding: const EdgeInsets.symmetric(vertical: 15),
                    ),
                  ),
                  const SizedBox(height: 12),
                  FilledButton(
                    onPressed: _isInitializing ? null : _initialize,
                    style: FilledButton.styleFrom(
                      backgroundColor: const Color(0xFF2D6A4F),
                      padding: const EdgeInsets.symmetric(vertical: 17),
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(18),
                      ),
                    ),
                    child: _isInitializing
                        ? const SizedBox(
                            width: 22,
                            height: 22,
                            child: CircularProgressIndicator(
                              strokeWidth: 2.6,
                              color: Colors.white,
                            ),
                          )
                        : const Text(
                            'Initialize SDK',
                            style: TextStyle(fontWeight: FontWeight.w800),
                          ),
                  ),
                  if (_error != null) ...[
                    const SizedBox(height: 16),
                    Text(
                      _error!,
                      textAlign: TextAlign.center,
                      style: const TextStyle(color: Color(0xFFFF8A80)),
                    ),
                  ],
                  const SizedBox(height: 24),
                  Text(
                    'Base URL: $_baseUrl',
                    textAlign: TextAlign.center,
                    style: const TextStyle(color: Colors.white38),
                  ),
                  const SizedBox(height: 16),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      const Text(
                        'Language: ',
                        style: TextStyle(color: Colors.white60),
                      ),
                      DropdownButton<String>(
                        value: localeNotifier.value.languageCode,
                        dropdownColor: const Color(0xFF111111),
                        style: const TextStyle(color: Colors.white),
                        underline: const SizedBox(),
                        items: [
                          for (final l in _locales)
                            DropdownMenuItem(
                              value: l.languageCode,
                              child: Text(l.languageCode.toUpperCase()),
                            ),
                        ],
                        onChanged: (code) {
                          if (code != null) {
                            localeNotifier.value = Locale(code);
                          }
                        },
                      ),
                      const SizedBox(width: 24),
                      const Text(
                        'Theme: ',
                        style: TextStyle(color: Colors.white60),
                      ),
                      DropdownButton<ThemeMode>(
                        value: themeModeNotifier.value,
                        dropdownColor: const Color(0xFF111111),
                        style: const TextStyle(color: Colors.white),
                        underline: const SizedBox(),
                        items: const [
                          DropdownMenuItem(
                            value: ThemeMode.light,
                            child: Text('LIGHT'),
                          ),
                          DropdownMenuItem(
                            value: ThemeMode.dark,
                            child: Text('DARK'),
                          ),
                        ],
                        onChanged: (mode) {
                          if (mode != null) {
                            themeModeNotifier.value = mode;
                            ChameleonPilot.setThemeMode(mode);
                          }
                        },
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

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

  @override
  State<SplashGateScreen> createState() => _SplashGateScreenState();
}

class _SplashGateScreenState extends State<SplashGateScreen> {
  String _status = 'Checking runtime config...';

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) => _runSdkGate());
  }

  Future<void> _runSdkGate() async {
    setState(() => _status = 'Checking maintenance...');
    final maintenanceShown = await ChameleonPilot.showMaintenance(
      barrierDismissible: false,
      context: context,
    );
    if (!mounted || maintenanceShown) return;

    setState(() => _status = 'Checking updates...');
    final updateShown = await ChameleonPilot.showUpdate(
      barrierDismissible: true,
      context: context,
    );
    if (!mounted) return;

    if (updateShown) {
      setState(() => _status = 'Update action completed...');
    }

    await Future<void>.delayed(const Duration(milliseconds: 350));
    if (!mounted) return;
    Navigator.of(
      context,
    ).pushReplacement(MaterialPageRoute(builder: (_) => const HomeScreen()));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: SafeArea(
        child: Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              const _LogoMark(size: 110),
              const SizedBox(height: 26),
              const Text(
                'ChameleonPilot',
                style: TextStyle(
                  color: Colors.white,
                  fontSize: 28,
                  fontWeight: FontWeight.w900,
                ),
              ),
              const SizedBox(height: 10),
              Text(_status, style: const TextStyle(color: Colors.white60)),
              const SizedBox(height: 30),
              const SizedBox(
                width: 28,
                height: 28,
                child: CircularProgressIndicator(
                  strokeWidth: 2.8,
                  color: Color(0xFF72D3A1),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

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

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  final _flagKeyController = TextEditingController(text: 'customKey');
  Object? _flagValue;

  @override
  void dispose() {
    _flagKeyController.dispose();
    super.dispose();
  }

  void _readFlag() {
    final key = _flagKeyController.text.trim();
    setState(() => _flagValue = ChameleonPilot.getFlag<Object?>(key));
  }

  String _resolve(LocalizedString value) {
    return value.resolve(localeNotifier.value.languageCode);
  }

  @override
  Widget build(BuildContext context) {
    final update = ChameleonPilot.update;
    final maintenance = ChameleonPilot.maintenance;
    final flags = ChameleonPilot.featureFlags;
    final currentLocale = localeNotifier.value.languageCode.toUpperCase();

    return Scaffold(
      appBar: AppBar(
        title: const Text('ChameleonPilot SDK'),
        actions: [
          IconButton(
            icon: Icon(
              themeModeNotifier.value == ThemeMode.dark
                  ? Icons.dark_mode
                  : Icons.light_mode,
            ),
            onPressed: () {
              final next = themeModeNotifier.value == ThemeMode.dark
                  ? ThemeMode.light
                  : ThemeMode.dark;
              themeModeNotifier.value = next;
              ChameleonPilot.setThemeMode(next);
            },
          ),
          PopupMenuButton<Locale>(
            initialValue: localeNotifier.value,
            icon: Text(
              currentLocale,
              style: const TextStyle(fontWeight: FontWeight.w700),
            ),
            onSelected: (locale) => localeNotifier.value = locale,
            itemBuilder: (_) => [
              for (final l in _locales)
                PopupMenuItem(
                  value: l,
                  child: Text(l.languageCode.toUpperCase()),
                ),
            ],
          ),
        ],
      ),
      body: ListView(
        padding: const EdgeInsets.all(20),
        children: [
          const _StatusCard(
            title: 'Home',
            child: Text('No blocking maintenance or update is active.'),
          ),
          const SizedBox(height: 12),
          _StatusCard(
            title: 'Cached SDK state',
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  'Update: ${update == null ? 'none' : _resolve(update.interface.title)}',
                ),
                Text(
                  'Maintenance: ${maintenance == null ? 'none' : _resolve(maintenance.interface.title)}',
                ),
                Text('Feature flags: ${flags.length}'),
              ],
            ),
          ),
          const SizedBox(height: 12),
          const _ConfiguredWidgetStack(),
          const SizedBox(height: 12),
          _StatusCard(
            title: 'Feature flag',
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                TextField(
                  controller: _flagKeyController,
                  decoration: const InputDecoration(
                    labelText: 'Flag key',
                    border: OutlineInputBorder(),
                  ),
                ),
                const SizedBox(height: 12),
                FilledButton.tonal(
                  onPressed: _readFlag,
                  child: const Text('Read flag'),
                ),
                if (_flagValue != null) ...[
                  const SizedBox(height: 8),
                  Text('Value: $_flagValue'),
                ],
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class _ConfiguredWidgetStack extends StatefulWidget {
  const _ConfiguredWidgetStack();

  @override
  State<_ConfiguredWidgetStack> createState() => _ConfiguredWidgetStackState();
}

class _ConfiguredWidgetStackState extends State<_ConfiguredWidgetStack> {
  String _screen = 'home';

  @override
  Widget build(BuildContext context) {
    final layout = ChameleonPilot.layout(_screen);
    return _StatusCard(
      title: 'Published layout demo',
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          DropdownButtonFormField<String>(
            initialValue: _screen,
            decoration: const InputDecoration(labelText: 'Screen'),
            items: const [
              DropdownMenuItem(value: 'home', child: Text('Home')),
              DropdownMenuItem(value: 'menu', child: Text('Menu')),
            ],
            onChanged: (value) => setState(() => _screen = value ?? 'home'),
          ),
          const SizedBox(height: 12),
          Text(
            layout == null
                ? 'Publish a $_screen layout in the dashboard to preview it here.'
                : '${layout.type} layout · version ${layout.version}',
            style: Theme.of(context).textTheme.bodySmall,
          ),
          ChameleonPilotLayoutView(
            screen: _screen,
            registry: {
              'widget_1': (_, item) => const _DemoWidgetCard(
                icon: Icons.auto_awesome,
                title: 'Widget 1',
                subtitle: 'First registry widget',
              ),
              'widget_2': (_, item) => const _DemoWidgetCard(
                icon: Icons.insights_outlined,
                title: 'Widget 2',
                subtitle: 'Second registry widget',
              ),
              'widget_3': (_, item) => const _DemoWidgetCard(
                icon: Icons.notifications_none,
                title: 'Widget 3',
                subtitle: 'Third registry widget',
              ),
            },
            empty: const Text('No published widgets for this screen.'),
          ),
        ],
      ),
    );
  }
}

class _DemoWidgetCard extends StatelessWidget {
  const _DemoWidgetCard({
    required this.icon,
    required this.title,
    required this.subtitle,
  });

  final IconData icon;
  final String title;
  final String subtitle;

  @override
  Widget build(BuildContext context) => Container(
    margin: const EdgeInsets.only(bottom: 8),
    padding: const EdgeInsets.all(14),
    decoration: BoxDecoration(
      color: Theme.of(context).colorScheme.surfaceContainerHighest,
      borderRadius: BorderRadius.circular(14),
    ),
    child: ListTile(
      contentPadding: EdgeInsets.zero,
      dense: true,
      leading: Icon(icon, color: Theme.of(context).colorScheme.primary),
      title: Text(title, overflow: TextOverflow.ellipsis),
      subtitle: Text(subtitle, maxLines: 2, overflow: TextOverflow.ellipsis),
    ),
  );
}

class _LogoMark extends StatelessWidget {
  const _LogoMark({required this.size});

  final double size;

  @override
  Widget build(BuildContext context) {
    return Container(
      width: size,
      height: size,
      decoration: BoxDecoration(
        color: const Color(0xFF2D6A4F),
        borderRadius: BorderRadius.circular(size * 0.25),
        boxShadow: [
          BoxShadow(
            color: const Color(0xFF2D6A4F).withValues(alpha: 0.42),
            blurRadius: size * 0.45,
            spreadRadius: size * 0.04,
          ),
        ],
      ),
      clipBehavior: Clip.antiAlias,
      child: Image.asset(
        'assets/logo.jpg',
        fit: BoxFit.cover,
        width: size,
        height: size,
        errorBuilder: (_, _, _) {
          return Icon(
            Icons.change_circle_outlined,
            color: Colors.white,
            size: size * 0.58,
          );
        },
      ),
    );
  }
}

class _StatusCard extends StatelessWidget {
  const _StatusCard({required this.title, required this.child});

  final String title;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              title,
              style: Theme.of(
                context,
              ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700),
            ),
            const SizedBox(height: 8),
            child,
          ],
        ),
      ),
    );
  }
}
0
likes
110
points
127
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Flutter SDK for ChameleonPilot runtime config, updates, maintenance, and feature flags.

Repository (GitHub)

Topics

#remote-config #feature-flags #maintenance #updates

License

MIT (license)

Dependencies

flutter, http, package_info_plus, shared_preferences, url_launcher

More

Packages that depend on chameleon_pilot_sdk