flowboard_flutter 0.0.2
flowboard_flutter: ^0.0.2 copied to clipboard
A versatile suite of UI components and flow management tools for building dynamic onboarding and dashboards in Flutter.
example/lib/main.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flowboard_flutter/flowboard.dart';
import 'paywall_widget.dart';
import 'signup_widget.dart';
import 'end_demo_widget.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
Flowboard.init(
apiToken:
"eyJhbGciOiJSUzI1NiIsImtpZCI6ImZsb3dib2FyZC1rZXktMSJ9.eyJ1c2VySWQiOiJjbGllbnRfMTIzIiwiYXBwSWQiOiJXM0VSQzVzR1hsZVZnWWlxYmhLUCIsInNjb3BlcyI6WyJwYXl3YWxsOnJlYWQiLCJvbmJvYXJkOnJlYWQiLCJvbmJvYXJkOndyaXRlIl0sImlhdCI6MTc3MjEyNzY4MSwiaXNzIjoiZmxvd2JvYXJkLWF1dGgiLCJhdWQiOiJmbG93Ym9hcmQtYXBpIiwianRpIjoiYjhmNjNkZGItNTUyMS00MWFkLTgzM2UtZmY3NzFjNmZmMDljIiwiZXhwIjoyMDg3NzAzNjgxfQ.g3F3w4pCP_5BKBl6qhz8bAop2ocNNZnJQfxEj8z-H08K6h3p7vAL_LgdaTSbjaJZbOx-I_0ckR2tj46Z0mH_3bqj4zAh7SFK0qDGtyNC5So6KIcEhOCgMRvk-FvNxvsm850SyCtYFA-TG2zp4tj6FMuy4Pb6Hvthf3MBNUEcjTLOzppMLhQgeY2QzBUVDuoxsoZ-oqqX8CiZCJ4RHqLV4zeIuY4AniHLa_FYYwYNyp7x74-JiwOBffBq0_PDT9YeVRTLDTUvalh5Z2NLqyHzSgMcQORKvdpu3CObs9pjerbIxKT-ayx0TWVW7u19On9vhCVage97pC9Fdw0hiNH6xg",
debug: true,
);
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final TextEditingController _jsonController = TextEditingController();
String _jsonStatus = '';
void _loadButtonStrokeDemoJson() {
final demoFlow = {
'flow_id': 'button_stroke_effects_demo',
'screens': [
{
'id': 'button_styles',
'padding': 20,
'children': [
{
'type': 'text',
'properties': {
'text': 'Button stroke/effects preview',
'fontSize': 18,
'fontWeight': 'bold',
},
},
{
'type': 'button',
'properties': {
'label': 'Default (no stroke/effects)',
'color': '0xFF1E88E5',
},
},
{
'type': 'button',
'properties': {
'label': 'Inside stroke',
'color': '0xFF43A047',
'stroke': {
'enabled': true,
'color': '#FFFFFF',
'opacity': 0.8,
'width': 2,
'position': 'inside',
},
},
},
{
'type': 'button',
'properties': {
'label': 'Center stroke',
'color': '0xFFFB8C00',
'stroke': {
'enabled': true,
'color': '#3E2723',
'opacity': 1,
'width': 2,
'position': 'center',
},
},
},
{
'type': 'button',
'properties': {
'label': 'Outside stroke + multi shadow',
'color': '0xFF8E24AA',
'stroke': {
'enabled': true,
'color': '#000000',
'opacity': 0.25,
'width': 3,
'position': 'outside',
},
'effects': [
{
'type': 'dropShadow',
'enabled': true,
'x': 0,
'y': 4,
'blur': 12,
'spread': 0,
'color': '#000000',
'opacity': 0.2,
},
{
'type': 'dropShadow',
'enabled': true,
'x': 0,
'y': 10,
'blur': 24,
'spread': 0,
'color': '#000000',
'opacity': 0.12,
},
],
},
},
],
},
],
};
_jsonController.text = const JsonEncoder.withIndent(' ').convert(demoFlow);
setState(() {
_jsonStatus =
'Button stroke/effects demo JSON loaded. Tap "Launch Pasted JSON".';
});
}
@override
void dispose() {
_jsonController.dispose();
super.dispose();
}
Future<void> _launchApiFlow(BuildContext context) async {
try {
await Flowboard.launchOnboarding(
context,
customScreenBuilder: _buildCustomScreenRouter,
onOnboardEnd: (formData) {
debugPrint('Onboarding ended with data: $formData');
Navigator.of(
context,
).push(MaterialPageRoute(builder: (context) => const EndDemoPage()));
},
);
} catch (e) {
if (!context.mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Error: $e')));
}
}
Future<void> _launchInjectedJsonFlow(BuildContext context) async {
final raw = _jsonController.text.trim();
if (raw.isEmpty) {
setState(() {
_jsonStatus = 'Paste a JSON payload first.';
});
return;
}
try {
final decoded = jsonDecode(raw);
if (decoded is! Map<String, dynamic>) {
setState(() {
_jsonStatus = 'JSON must be an object.';
});
return;
}
final flowData = Map<String, dynamic>.from(decoded);
final screens = flowData['screens'];
if (screens is! List || screens.isEmpty) {
setState(() {
_jsonStatus = 'JSON must include a non-empty "screens" array.';
});
return;
}
final flowId = flowData['flow_id'];
if (flowId is! String || flowId.trim().isEmpty) {
flowData['flow_id'] = 'injected_flow';
}
setState(() {
_jsonStatus = 'Injected flow loaded (${screens.length} screens).';
});
if (!context.mounted) return;
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => FlowboardFlow(
data: flowData,
customScreenBuilder: _buildCustomScreenRouter,
onOnboardEnd: (formData) {
debugPrint('Injected flow ended with data: $formData');
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const EndDemoPage()),
);
},
),
fullscreenDialog: true,
),
);
} catch (e) {
setState(() {
_jsonStatus = 'Invalid JSON: $e';
});
}
}
Future<void> _launchButtonEffectsDemo(BuildContext context) async {
final flowData = _buttonEffectsDemoFlow();
if (!context.mounted) return;
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => FlowboardFlow(
data: flowData,
customScreenBuilder: _buildCustomScreenRouter,
),
fullscreenDialog: true,
),
);
}
Map<String, dynamic> _buttonEffectsDemoFlow() {
return {
'flow_id': 'button_effects_demo',
'screens': [
{
'id': 'button_styles',
'padding': 20,
'children': [
{
'type': 'text',
'properties': {
'text': 'Button stroke/effects demo',
'fontSize': 20,
'fontWeight': 'bold',
},
},
{
'type': 'button',
'properties': {'label': 'Default (no effects)'},
},
{
'type': 'button',
'properties': {
'label': 'Inside stroke',
'color': '0xFFFFFFFF',
'textColor': '0xFF111111',
'stroke': {
'enabled': true,
'color': '#111111',
'opacity': 0.35,
'width': 2,
'position': 'inside',
},
},
},
{
'type': 'button',
'properties': {
'label': 'Center stroke',
'color': '0xFFFFFFFF',
'textColor': '0xFF111111',
'stroke': {
'enabled': true,
'color': '#111111',
'opacity': 0.5,
'width': 2,
'position': 'center',
},
},
},
{
'type': 'button',
'properties': {
'label': 'Outside stroke + shadows',
'color': '0xFFFFFFFF',
'textColor': '0xFF111111',
'stroke': {
'enabled': true,
'color': '#3B82F6',
'opacity': 1,
'width': 2,
'position': 'outside',
},
'effects': [
{
'type': 'dropShadow',
'enabled': true,
'x': 0,
'y': 4,
'blur': 12,
'spread': 0,
'color': '#000000',
'opacity': 0.2,
},
{
'type': 'dropShadow',
'enabled': true,
'x': 0,
'y': 10,
'blur': 20,
'spread': -4,
'color': '#2563EB',
'opacity': 0.25,
},
],
},
},
{
'type': 'button',
'action': 'next',
'properties': {'label': 'Finish'},
},
],
},
],
};
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Flowboard Test')),
body: Builder(
builder: (context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ElevatedButton(
onPressed: () => _launchApiFlow(context),
child: const Text('Launch Onboarding (API)'),
),
const SizedBox(height: 12),
OutlinedButton(
onPressed: () async {
await Flowboard.reinit();
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Re-initialized! (Cache cleared, fresh fetch triggered)',
),
),
);
},
child: const Text('Re-initialize SDK'),
),
const SizedBox(height: 20),
const Divider(),
const SizedBox(height: 12),
const Text(
'Inject JSON (no API)',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
const SizedBox(height: 8),
OutlinedButton(
onPressed: _loadButtonStrokeDemoJson,
child: const Text('Load Button Stroke/Effects Demo JSON'),
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: () => _launchInjectedJsonFlow(context),
child: const Text('Launch Pasted JSON'),
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: () => _launchButtonEffectsDemo(context),
child: const Text('Launch Button Effects Demo'),
),
const SizedBox(height: 8),
TextField(
controller: _jsonController,
minLines: 8,
maxLines: 14,
keyboardType: TextInputType.multiline,
decoration: const InputDecoration(
hintText: 'Paste flow JSON here (with "screens")',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 8),
if (_jsonStatus.isNotEmpty)
Text(_jsonStatus, style: const TextStyle(fontSize: 12)),
],
),
);
},
),
),
);
}
}
/// Router for custom screens - handles different custom screen types
Widget _buildCustomScreenRouter(FlowboardContext ctx) {
final screenId = ctx.screenData['id'] as String?;
switch (screenId) {
case 'paywall':
return PaywallPage(ctx: ctx);
case 'signup':
return SignupPage(ctx: ctx);
default:
// Fallback for unknown screen IDs
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Unknown custom screen: $screenId'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: ctx.onNext,
child: const Text('Continue'),
),
],
),
),
);
}
}