guided_flow 1.0.0
guided_flow: ^1.0.0 copied to clipboard
Interactive guided tours for Flutter. Users tap the real widgets: spotlights are tap-through, work across bottom sheets and pushed routes, and advance automatically.
import 'package:flutter/material.dart';
import 'package:guided_flow/guided_flow.dart';
/// A four-step tour across three surfaces — a page, a bottom sheet, and a
/// pushed page — to show the two things that make `guided_flow` different:
/// the user taps the real buttons, and the spotlight survives the route change.
void main() => runApp(const ExampleApp());
/// Ids shared by the steps and the anchors. Constants, so a typo is a compile
/// error rather than a tour that quietly does nothing.
class Anchors {
const Anchors._();
static const String addButton = 'add-button';
static const String category = 'category';
static const String amount = 'amount';
static const String save = 'save';
}
final List<GuidedFlowStep> tourSteps = [
const GuidedFlowStep(
anchorId: Anchors.addButton,
title: 'Start here',
body: 'Every expense begins with this button. Give it a tap.',
actionLabel: 'Tap the + button',
shape: BoxShape.circle,
padding: EdgeInsets.all(10),
),
const GuidedFlowStep(
anchorId: Anchors.category,
title: 'Pick a category',
body: 'The sheet slides up and the spotlight follows it — no extra work.',
actionLabel: 'Tap Groceries',
),
const GuidedFlowStep(
anchorId: Anchors.amount,
title: 'Check the amount',
body:
'Nothing to press here, so this step waits for the button on the card. '
'Use waitForButton whenever two steps share one screen.',
actionLabel: 'Looks right',
waitForButton: true,
),
const GuidedFlowStep(
anchorId: Anchors.save,
title: 'Save it',
body: 'Tapping this really saves — the tour never stood in the way.',
actionLabel: 'Tap Save',
endsTour: true,
),
];
class ExampleApp extends StatefulWidget {
const ExampleApp({super.key});
@override
State<ExampleApp> createState() => _ExampleAppState();
}
class _ExampleAppState extends State<ExampleApp> {
late final GuidedFlowController _tour = GuidedFlowController(
steps: tourSteps,
onFinish: (ending) => debugPrint('Tour ended: ${ending.name}'),
);
@override
void dispose() {
_tour.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'guided_flow example',
theme: ThemeData(
colorSchemeSeed: const Color(0xFF4F46E5),
useMaterial3: true,
),
// One line, above the Navigator: that is the whole installation.
builder: (context, child) => GuidedFlow(
controller: _tour,
child: child!,
),
home: HomePage(tour: _tour),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key, required this.tour});
final GuidedFlowController tour;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Expenses')),
body: Center(
child: FilledButton.tonal(
onPressed: tour.start,
child: const Text('Start the tour'),
),
),
floatingActionButton: GuidedFlowAnchor(
id: Anchors.addButton,
child: FloatingActionButton(
onPressed: () => _openCategorySheet(context),
child: const Icon(Icons.add),
),
),
);
}
Future<void> _openCategorySheet(BuildContext context) async {
final String? category = await showModalBottomSheet<String>(
context: context,
builder: (_) => const CategorySheet(),
);
if (category == null || !context.mounted) return;
await Navigator.of(context).push(
MaterialPageRoute<void>(builder: (_) => ExpensePage(category: category)),
);
}
}
class CategorySheet extends StatelessWidget {
const CategorySheet({super.key});
@override
Widget build(BuildContext context) {
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 12),
const Text('What did you spend on?'),
const SizedBox(height: 12),
GuidedFlowAnchor(
id: Anchors.category,
child: ListTile(
leading: const Icon(Icons.shopping_basket_outlined),
title: const Text('Groceries'),
onTap: () => Navigator.of(context).pop('Groceries'),
),
),
ListTile(
leading: const Icon(Icons.directions_bus_outlined),
title: const Text('Transport'),
onTap: () => Navigator.of(context).pop('Transport'),
),
const SizedBox(height: 12),
],
),
);
}
}
class ExpensePage extends StatelessWidget {
const ExpensePage({super.key, required this.category});
final String category;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(category)),
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
GuidedFlowAnchor(
id: Anchors.amount,
child: Card(
child: ListTile(
title: const Text('Amount'),
subtitle: const Text('Rp 250.000'),
trailing: const Icon(Icons.edit_outlined),
onTap: () {},
),
),
),
const Spacer(),
GuidedFlowAnchor(
id: Anchors.save,
child: FilledButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Saved.')),
);
Navigator.of(context).pop();
},
child: const Text('Save'),
),
),
],
),
),
);
}
}