animated_route_breadcrumbs 0.1.0
animated_route_breadcrumbs: ^0.1.0 copied to clipboard
Animated responsive breadcrumbs synchronized with Flutter Navigator routes and URL-style locations.
import 'package:animated_route_breadcrumbs/animated_route_breadcrumbs.dart';
import 'package:flutter/material.dart';
void main() => runApp(const BreadcrumbStudioApp());
class BreadcrumbStudioApp extends StatefulWidget {
const BreadcrumbStudioApp({super.key});
@override
State<BreadcrumbStudioApp> createState() => _BreadcrumbStudioAppState();
}
class _BreadcrumbStudioAppState extends State<BreadcrumbStudioApp> {
late final RouteBreadcrumbController _controller;
late final RouteBreadcrumbObserver _observer;
ThemeMode _themeMode = ThemeMode.light;
@override
void initState() {
super.initState();
_controller = RouteBreadcrumbController();
_observer = RouteBreadcrumbObserver(
controller: _controller,
resolver: _resolveBreadcrumb,
);
}
RouteBreadcrumb? _resolveBreadcrumb(Route<dynamic> route) {
final name = route.settings.name;
if (name == null) return null;
final destination = _destinations[name] ?? _destinations['/']!;
return RouteBreadcrumb(
id: route,
label: destination.label,
location: name,
icon: destination.icon,
semanticLabel: 'Go to ${destination.label}',
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Orbit workspace',
themeMode: _themeMode,
theme: _theme(Brightness.light),
darkTheme: _theme(Brightness.dark),
navigatorObservers: [_observer],
onGenerateRoute: (settings) => MaterialPageRoute<void>(
settings: settings,
builder: (context) => _DashboardPage(
routeName: settings.name ?? '/',
controller: _controller,
themeMode: _themeMode,
onToggleTheme: () => setState(() {
_themeMode = _themeMode == ThemeMode.light
? ThemeMode.dark
: ThemeMode.light;
}),
),
),
);
}
ThemeData _theme(Brightness brightness) {
const seed = Color(0xFF6D5CE7);
final scheme = ColorScheme.fromSeed(
seedColor: seed,
brightness: brightness,
);
return ThemeData(
useMaterial3: true,
brightness: brightness,
colorScheme: scheme,
scaffoldBackgroundColor: brightness == Brightness.light
? const Color(0xFFF7F7FC)
: const Color(0xFF101016),
cardTheme: CardThemeData(
elevation: 0,
margin: EdgeInsets.zero,
color: brightness == Brightness.light
? Colors.white
: const Color(0xFF191920),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
),
dividerColor: scheme.outlineVariant.withValues(alpha: .55),
);
}
}
class _DashboardPage extends StatelessWidget {
const _DashboardPage({
required this.routeName,
required this.controller,
required this.themeMode,
required this.onToggleTheme,
});
final String routeName;
final RouteBreadcrumbController controller;
final ThemeMode themeMode;
final VoidCallback onToggleTheme;
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: SingleChildScrollView(
child: Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 1180),
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 18, 20, 48),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_TopBar(
routeName: routeName,
themeMode: themeMode,
onToggleTheme: onToggleTheme,
),
const SizedBox(height: 38),
_BreadcrumbHeader(
controller: controller,
routeName: routeName,
),
const SizedBox(height: 30),
AnimatedSwitcher(
duration: const Duration(milliseconds: 320),
switchInCurve: Curves.easeOutCubic,
transitionBuilder: (child, animation) => FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, .025),
end: Offset.zero,
).animate(animation),
child: child,
),
),
child: KeyedSubtree(
key: ValueKey(routeName),
child: _RouteContent(routeName: routeName),
),
),
],
),
),
),
),
),
),
);
}
}
class _TopBar extends StatelessWidget {
const _TopBar({
required this.routeName,
required this.themeMode,
required this.onToggleTheme,
});
final String routeName;
final ThemeMode themeMode;
final VoidCallback onToggleTheme;
@override
Widget build(BuildContext context) {
return Row(
children: [
const _Logo(size: 40),
const SizedBox(width: 12),
Expanded(
child: Text(
'Breadcrumb Studio',
overflow: TextOverflow.ellipsis,
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w900),
),
),
PopupMenuButton<String>(
tooltip: 'Explore demo routes',
initialValue: routeName,
onSelected: (route) => _navigateTo(context, route),
itemBuilder: (context) => [
for (final entry in _destinations.entries)
PopupMenuItem(
value: entry.key,
child: Row(
children: [
Icon(entry.value.icon, size: 19),
const SizedBox(width: 10),
Text(entry.value.label),
],
),
),
],
icon: const Icon(Icons.explore_outlined),
),
IconButton(
tooltip: themeMode == ThemeMode.dark ? 'Light mode' : 'Dark mode',
onPressed: onToggleTheme,
icon: Icon(
themeMode == ThemeMode.dark
? Icons.light_mode_outlined
: Icons.dark_mode_outlined,
),
),
const SizedBox(width: 6),
const CircleAvatar(
radius: 18,
backgroundColor: Color(0xFFE7E2FF),
child: Text(
'GS',
style: TextStyle(
color: Color(0xFF4E3FB5),
fontSize: 12,
fontWeight: FontWeight.w800,
),
),
),
],
);
}
}
class _BreadcrumbHeader extends StatelessWidget {
const _BreadcrumbHeader({required this.controller, required this.routeName});
final RouteBreadcrumbController controller;
final String routeName;
@override
Widget build(BuildContext context) {
final destination = _destinations[routeName] ?? _destinations['/']!;
final colors = Theme.of(context).colorScheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).cardTheme.color,
borderRadius: BorderRadius.circular(18),
border: Border.all(
color: colors.outlineVariant.withValues(alpha: .6),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
child: AnimatedRouteBreadcrumbs(
breadcrumbs: controller,
compactBreakpoint: 600,
maximumVisibleBreadcrumbs: 4,
theme: AnimatedRouteBreadcrumbThemeData(
backgroundColor: Colors.transparent,
foregroundColor: colors.onSurfaceVariant,
currentBackgroundColor: colors.primaryContainer,
currentForegroundColor: colors.onPrimaryContainer,
separatorColor: colors.outline,
borderRadius: BorderRadius.circular(11),
padding: const EdgeInsets.symmetric(
horizontal: 11,
vertical: 8,
),
),
),
),
),
const SizedBox(height: 26),
Text(
destination.label,
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.w900,
letterSpacing: -.7,
),
),
const SizedBox(height: 7),
Text(
destination.description,
style: Theme.of(
context,
).textTheme.bodyLarge?.copyWith(color: colors.onSurfaceVariant),
),
],
);
}
}
class _Logo extends StatelessWidget {
const _Logo({this.size = 42});
final double size;
@override
Widget build(BuildContext context) => Container(
width: size,
height: size,
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF8B7CF6), Color(0xFF5845D6)],
),
borderRadius: BorderRadius.circular(size * .31),
),
child: Icon(Icons.route_rounded, size: size * .55, color: Colors.white),
);
}
class _RouteContent extends StatelessWidget {
const _RouteContent({required this.routeName});
final String routeName;
@override
Widget build(BuildContext context) {
return switch (routeName) {
'/projects' => const _ProjectsView(),
'/projects/animated-route-breadcrumbs' => const _PackageView(),
'/projects/animated-route-breadcrumbs/analytics' =>
const _AnalyticsView(),
_ => const _OverviewView(),
};
}
}
class _OverviewView extends StatelessWidget {
const _OverviewView();
@override
Widget build(BuildContext context) {
return Column(
children: [
const _ResponsiveGrid(
minItemWidth: 190,
children: [
_MetricCard(
label: 'Active projects',
value: '12',
change: '+3 this month',
icon: Icons.layers_outlined,
accent: Color(0xFF6D5CE7),
),
_MetricCard(
label: 'Team members',
value: '24',
change: '4 online now',
icon: Icons.people_alt_outlined,
accent: Color(0xFFEB7B5C),
),
_MetricCard(
label: 'Completion',
value: '86%',
change: '+8.4% growth',
icon: Icons.auto_graph_rounded,
accent: Color(0xFF20A882),
),
],
),
const SizedBox(height: 18),
_SectionCard(
title: 'Recent projects',
subtitle: 'Continue where your team left off.',
action: FilledButton.icon(
onPressed: () => _openRoute(context, '/projects'),
icon: const Icon(Icons.arrow_forward_rounded, size: 18),
label: const Text('View all'),
),
child: Column(
children: [
_ProjectRow(
icon: Icons.route_rounded,
color: const Color(0xFF6D5CE7),
title: 'Animated route breadcrumbs',
subtitle: 'Flutter package · Updated today',
progress: .86,
onTap: () => _openProject(context),
),
const Divider(height: 24),
const _ProjectRow(
icon: Icons.web_rounded,
color: Color(0xFFEB7B5C),
title: 'Portfolio redesign',
subtitle: 'Flutter web · Updated yesterday',
progress: .64,
),
const Divider(height: 24),
const _ProjectRow(
icon: Icons.phone_android_rounded,
color: Color(0xFF20A882),
title: 'Mobile design system',
subtitle: 'Components · Updated 3 days ago',
progress: .42,
),
],
),
),
],
);
}
}
class _ProjectsView extends StatelessWidget {
const _ProjectsView();
@override
Widget build(BuildContext context) {
return _ResponsiveGrid(
minItemWidth: 270,
children: [
_ProjectTile(
icon: Icons.route_rounded,
color: const Color(0xFF6D5CE7),
title: 'Animated route breadcrumbs',
category: 'Flutter package',
status: 'In review',
onTap: () => _openProject(context),
),
const _ProjectTile(
icon: Icons.space_dashboard_outlined,
color: Color(0xFFEB7B5C),
title: 'Portfolio dashboard',
category: 'Flutter web',
status: 'Published',
),
const _ProjectTile(
icon: Icons.widgets_outlined,
color: Color(0xFF20A882),
title: 'UI component lab',
category: 'Design system',
status: 'Planning',
),
],
);
}
}
class _PackageView extends StatelessWidget {
const _PackageView();
@override
Widget build(BuildContext context) {
return Column(
children: [
_SectionCard(
title: 'animated_route_breadcrumbs',
subtitle: 'A polished navigation trail for Flutter applications.',
action: FilledButton.icon(
onPressed: () => _openAnalytics(context),
icon: const Icon(Icons.query_stats_rounded, size: 18),
label: const Text('View insights'),
),
child: const _PackageHero(),
),
const SizedBox(height: 18),
const _ResponsiveGrid(
minItemWidth: 180,
children: [
_MetricCard(
label: 'Pub points',
value: '160',
change: 'Perfect score',
icon: Icons.verified_outlined,
accent: Color(0xFF20A882),
),
_MetricCard(
label: 'Platforms',
value: '6',
change: 'Flutter supported',
icon: Icons.devices_rounded,
accent: Color(0xFF6D5CE7),
),
_MetricCard(
label: 'Dependencies',
value: '0',
change: 'Lightweight API',
icon: Icons.bolt_rounded,
accent: Color(0xFFEB7B5C),
),
],
),
],
);
}
}
class _AnalyticsView extends StatelessWidget {
const _AnalyticsView();
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Column(
children: [
const _ResponsiveGrid(
minItemWidth: 190,
children: [
_MetricCard(
label: 'Weekly views',
value: '2.4K',
change: '+18.2%',
icon: Icons.visibility_outlined,
accent: Color(0xFF6D5CE7),
),
_MetricCard(
label: 'Installs',
value: '842',
change: '+12.8%',
icon: Icons.download_done_rounded,
accent: Color(0xFF20A882),
),
_MetricCard(
label: 'Likes',
value: '126',
change: '+9 this week',
icon: Icons.favorite_border_rounded,
accent: Color(0xFFEB7B5C),
),
],
),
const SizedBox(height: 18),
_SectionCard(
title: 'Package activity',
subtitle: 'Illustrative data for the interactive package example.',
child: SizedBox(
height: 230,
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
for (final item in const [35, 52, 44, 72, 61, 88, 76])
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6),
child: TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: item.toDouble()),
duration: const Duration(milliseconds: 700),
curve: Curves.easeOutCubic,
builder: (context, value, _) => Container(
height: value * 2,
decoration: BoxDecoration(
color: colors.primary.withValues(alpha: .18),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(10),
),
border: Border(
top: BorderSide(color: colors.primary, width: 3),
),
),
),
),
),
),
],
),
),
),
],
);
}
}
class _ResponsiveGrid extends StatelessWidget {
const _ResponsiveGrid({required this.children, required this.minItemWidth});
final List<Widget> children;
final double minItemWidth;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final columns = (constraints.maxWidth / minItemWidth).floor().clamp(
1,
children.length,
);
final width = (constraints.maxWidth - (columns - 1) * 16) / columns;
return Wrap(
spacing: 16,
runSpacing: 16,
children: [
for (final child in children) SizedBox(width: width, child: child),
],
);
},
);
}
}
class _MetricCard extends StatelessWidget {
const _MetricCard({
required this.label,
required this.value,
required this.change,
required this.icon,
required this.accent,
});
final String label;
final String value;
final String change;
final IconData icon;
final Color accent;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(22),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 42,
height: 42,
decoration: BoxDecoration(
color: accent.withValues(alpha: .12),
borderRadius: BorderRadius.circular(13),
),
child: Icon(icon, color: accent, size: 21),
),
const SizedBox(height: 20),
Text(
value,
style: Theme.of(
context,
).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.w900),
),
const SizedBox(height: 3),
Text(label, style: Theme.of(context).textTheme.bodyMedium),
const SizedBox(height: 12),
Text(
change,
style: TextStyle(
color: accent,
fontSize: 12,
fontWeight: FontWeight.w700,
),
),
],
),
),
);
}
}
class _SectionCard extends StatelessWidget {
const _SectionCard({
required this.title,
required this.subtitle,
required this.child,
this.action,
});
final String title;
final String subtitle;
final Widget child;
final Widget? action;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LayoutBuilder(
builder: (context, constraints) {
final information = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w900,
),
),
const SizedBox(height: 5),
Text(
subtitle,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
);
if (action == null) return information;
if (constraints.maxWidth < 540) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
information,
const SizedBox(height: 16),
action!,
],
);
}
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: information),
const SizedBox(width: 12),
action!,
],
);
},
),
const SizedBox(height: 26),
child,
],
),
),
);
}
}
class _ProjectRow extends StatelessWidget {
const _ProjectRow({
required this.icon,
required this.color,
required this.title,
required this.subtitle,
required this.progress,
this.onTap,
});
final IconData icon;
final Color color;
final String title;
final String subtitle;
final double progress;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
return InkWell(
borderRadius: BorderRadius.circular(16),
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Container(
width: 46,
height: 46,
decoration: BoxDecoration(
color: color.withValues(alpha: .12),
borderRadius: BorderRadius.circular(14),
),
child: Icon(icon, color: color),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(fontWeight: FontWeight.w800),
),
const SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontSize: 12,
),
),
],
),
),
SizedBox(
width: 76,
child: LinearProgressIndicator(
value: progress,
color: color,
backgroundColor: color.withValues(alpha: .12),
borderRadius: BorderRadius.circular(10),
),
),
const SizedBox(width: 10),
const Icon(Icons.chevron_right_rounded),
],
),
),
);
}
}
class _ProjectTile extends StatelessWidget {
const _ProjectTile({
required this.icon,
required this.color,
required this.title,
required this.category,
required this.status,
this.onTap,
});
final IconData icon;
final Color color;
final String title;
final String category;
final String status;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
return Card(
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: color.withValues(alpha: .12),
borderRadius: BorderRadius.circular(14),
),
child: Icon(icon, color: color),
),
const Spacer(),
const Icon(Icons.arrow_outward_rounded, size: 20),
],
),
const SizedBox(height: 28),
Text(
title,
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w900),
),
const SizedBox(height: 7),
Text(
category,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 22),
DecoratedBox(
decoration: BoxDecoration(
color: color.withValues(alpha: .1),
borderRadius: BorderRadius.circular(20),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
child: Text(
status,
style: TextStyle(
color: color,
fontSize: 12,
fontWeight: FontWeight.w700,
),
),
),
),
],
),
),
),
);
}
}
class _PackageHero extends StatelessWidget {
const _PackageHero();
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 28,
runSpacing: 24,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Container(
width: 150,
height: 150,
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF8B7CF6), Color(0xFF5845D6)],
),
borderRadius: BorderRadius.circular(34),
),
child: const Icon(Icons.route_rounded, color: Colors.white, size: 68),
),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 460),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Navigator-aware. Responsive. Animated.',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w900,
),
),
const SizedBox(height: 12),
Text(
'The trail above is the actual package. Open Insights and tap '
'any ancestor to jump back through the Navigator stack.',
style: TextStyle(
height: 1.55,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 18),
const Wrap(
spacing: 8,
runSpacing: 8,
children: [
Chip(label: Text('Navigator 1.0')),
Chip(label: Text('go_router ready')),
Chip(label: Text('Responsive')),
Chip(label: Text('Accessible')),
],
),
],
),
),
],
);
}
}
class _Destination {
const _Destination(this.label, this.description, this.icon);
final String label;
final String description;
final IconData icon;
}
const _destinations = <String, _Destination>{
'/': _Destination(
'Overview',
'A clear view of your workspace and latest activity.',
Icons.home_outlined,
),
'/projects': _Destination(
'Projects',
'Everything your team is designing, building, and shipping.',
Icons.folder_copy_outlined,
),
'/projects/animated-route-breadcrumbs': _Destination(
'Route breadcrumbs',
'Package details, quality signals, and release readiness.',
Icons.route_rounded,
),
'/projects/animated-route-breadcrumbs/analytics': _Destination(
'Analytics',
'Track adoption and engagement across every supported platform.',
Icons.query_stats_rounded,
),
};
void _navigateTo(BuildContext context, String route) {
switch (route) {
case '/':
_goRoot(context);
case '/projects':
_openRoute(context, route);
case '/projects/animated-route-breadcrumbs':
_openProject(context);
case '/projects/animated-route-breadcrumbs/analytics':
_openDeepRoute(context);
}
}
void _goRoot(BuildContext context, {bool closeDrawer = false}) {
final navigator = Navigator.of(context);
if (closeDrawer) navigator.pop();
navigator.popUntil((route) => route.isFirst);
}
void _openRoute(
BuildContext context,
String route, {
bool closeDrawer = false,
}) {
final navigator = Navigator.of(context);
final currentRoute = ModalRoute.of(context)?.settings.name;
if (closeDrawer) navigator.pop();
if (currentRoute == route) return;
navigator.popUntil((candidate) => candidate.isFirst);
if (route != '/') navigator.pushNamed(route);
}
void _openProject(BuildContext context) {
final navigator = Navigator.of(context);
navigator.popUntil((route) => route.isFirst);
navigator.pushNamed('/projects');
navigator.pushNamed('/projects/animated-route-breadcrumbs');
}
void _openAnalytics(BuildContext context) {
Navigator.pushNamed(
context,
'/projects/animated-route-breadcrumbs/analytics',
);
}
void _openDeepRoute(BuildContext context, {bool closeDrawer = false}) {
final navigator = Navigator.of(context);
if (closeDrawer) navigator.pop();
navigator.popUntil((route) => route.isFirst);
navigator.pushNamed('/projects');
navigator.pushNamed('/projects/animated-route-breadcrumbs');
navigator.pushNamed('/projects/animated-route-breadcrumbs/analytics');
}