ctrlapp 0.1.4
ctrlapp: ^0.1.4 copied to clipboard
Remote kill-switch, feature flags, forced update, forms and inbox for Flutter, plus diagnostics, device-integrity checks, push permission and hosted login.
example/lib/main.dart
// A small, complete example of the `ctrlapp` package.
//
// It connects to CtrlApp, then shows the things you will use most:
// * the kill-switch (block the whole app from the dashboard)
// * a feature flag, remote config and remote text
// * an experiment (A/B test)
// * event tracking
// and it updates live when you change something in the dashboard.
//
// Create an app at https://ctrlapp.dev, copy its API key, then run:
//
// flutter run --dart-define=CTRLAPP_API_KEY=ck_sdk_YOUR_KEY
import 'package:ctrlapp/ctrlapp.dart';
import 'package:flutter/material.dart';
const _apiKey = String.fromEnvironment('CTRLAPP_API_KEY');
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// One call at startup. After this, CtrlApp.instance works anywhere.
if (_apiKey.isNotEmpty) {
await CtrlApp.init(apiKey: _apiKey);
}
runApp(const ExampleApp());
}
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'CtrlApp example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
useMaterial3: true,
),
home: _apiKey.isEmpty ? const MissingKeyScreen() : const KillSwitchGate(),
);
}
}
/// Shown until you run the example with your own API key.
class MissingKeyScreen extends StatelessWidget {
const MissingKeyScreen({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Padding(
padding: EdgeInsets.all(24),
child: Text(
'Add your API key to try this example:\n\n'
'flutter run --dart-define=CTRLAPP_API_KEY=ck_sdk_...\n\n'
'Create an app and copy its key at https://ctrlapp.dev',
textAlign: TextAlign.center,
),
),
),
);
}
}
/// The kill-switch. When you block the app in the dashboard, every running
/// copy of your app switches to this screen within about a minute, and it
/// keeps working offline because the decision is cached on the device.
class KillSwitchGate extends StatelessWidget {
const KillSwitchGate({super.key});
@override
Widget build(BuildContext context) {
final ctrl = CtrlApp.instance;
return StreamBuilder<CtrlAppBlock>(
stream: ctrl.blockChanges,
builder: (context, _) {
if (ctrl.isBlocked) {
return BlockedScreen(reason: ctrl.blockReason());
}
return const HomeScreen();
},
);
}
}
class BlockedScreen extends StatelessWidget {
const BlockedScreen({super.key, this.reason});
final String? reason;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.construction, size: 64),
const SizedBox(height: 16),
Text(
'Temporarily unavailable',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
Text(reason ?? 'Please try again later.', textAlign: TextAlign.center),
],
),
),
),
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
final ctrl = CtrlApp.instance;
// Rebuild whenever a control changes in the dashboard.
return StreamBuilder<Map<String, dynamic>>(
stream: ctrl.controlChanges,
builder: (context, _) {
// Remote text, with a default used until the dashboard sets one.
final title = ctrl.text('home.title', fallback: 'Hello from CtrlApp');
// Remote config: a number you can change without a release.
final itemCount = ctrl.configValue<int>('home.items', defaultValue: 3);
return Scaffold(
appBar: AppBar(title: Text(title)),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
// Feature flag: show or hide this card from the dashboard.
if (ctrl.boolFlag('home.banner'))
const Card(
child: ListTile(
leading: Icon(Icons.campaign),
title: Text('New feature!'),
subtitle: Text('Switched on remotely with a feature flag.'),
),
),
for (var i = 1; i <= itemCount; i++)
ListTile(
leading: const Icon(Icons.check_circle_outline),
title: Text('Item $i'),
subtitle: const Text('The number of items is remote config.'),
),
const SizedBox(height: 24),
const ExperimentButton(),
],
),
);
},
);
}
}
/// An experiment: CtrlApp decides which variant this user sees, and you
/// compare results in the dashboard.
class ExperimentButton extends StatelessWidget {
const ExperimentButton({super.key});
@override
Widget build(BuildContext context) {
final ctrl = CtrlApp.instance;
return FutureBuilder<String>(
future: ctrl.treatment('home.cta', defaultValue: 'control'),
builder: (context, snapshot) {
final variant = snapshot.data ?? 'control';
final label = variant == 'b' ? 'Get started now' : 'Get started';
return FilledButton(
onPressed: () {
// Record what people do so you can see it in the dashboard.
ctrl.track('cta_tapped', props: {'variant': variant});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Tracked "cta_tapped" (variant: $variant)')),
);
},
child: Text(label),
);
},
);
}
}