chameleon_pilot_sdk 0.1.6
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,
],
),
),
);
}
}