Add to your pubspec.yaml:
dependencies:
adaptive_navigation: ^1.0.0 # Use latest versionThen run:
flutter pub getAdd to your pubspec.yaml:
dependencies:
adaptive_navigation:
git:
url: https://github.com/your-username/adaptive_navigation.git
ref: main # or specific commit/tagdependencies:
adaptive_navigation:
path: ../adaptive_navigationimport 'package:adaptive_navigation/adaptive_navigation.dart';class MyHomePage extends StatefulWidget {
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
return AdaptiveConvexNavigation(
selectedIndex: _selectedIndex,
onTap: (index) => setState(() => _selectedIndex = index),
items: const [
TabItem(icon: Icons.home, title: 'Home'),
TabItem(icon: Icons.search, title: 'Search'),
TabItem(icon: Icons.notifications, title: 'Alerts'),
TabItem(icon: Icons.person, title: 'Profile'),
],
child: _buildPageContent(),
);
}
Widget _buildPageContent() {
switch (_selectedIndex) {
case 0:
return HomePage();
case 1:
return SearchPage();
case 2:
return AlertsPage();
case 3:
return ProfilePage();
default:
return HomePage();
}
}
}What you get:
- Mobile: ConvexAppBar at bottom
- Desktop: NavigationRail on side
- Automatic switching at 600px
AdaptiveConvexNavigation(
selectedIndex: _selectedIndex,
onTap: (index) => setState(() => _selectedIndex = index),
items: const [
TabItem(icon: Icons.home, title: 'Home'),
TabItem(icon: Icons.search, title: 'Search'),
TabItem(icon: Icons.person, title: 'Profile'),
],
child: MyContentWidget(),
)Perfect for: Messaging apps, social media, any app with notifications
class _MyAppState extends State<MyApp> {
int _selectedIndex = 0;
// Badge state - hides when viewing, clears when read
final Map<int, dynamic> _badges = {
1: '5', // 5 new messages
2: '99+', // 99+ notifications
};
// Visible badges (excludes active tab)
Map<int, dynamic> get _visibleBadges {
final visible = Map.from(_badges);
visible.remove(_selectedIndex);
return visible;
}
void _clearBadge(int index) {
setState(() => _badges.remove(index));
}
@override
Widget build(BuildContext context) {
return AdaptiveConvexNavigation.badge(
_visibleBadges,
selectedIndex: _selectedIndex,
onTap: (index) => setState(() => _selectedIndex = index),
// Badge styling
badgeColor: Colors.red,
badgeTextColor: Colors.white,
items: const [
TabItem(icon: Icons.home, title: 'Home'),
TabItem(icon: Icons.message, title: 'Messages'),
TabItem(icon: Icons.notifications, title: 'Alerts'),
TabItem(icon: Icons.person, title: 'Profile'),
],
child: _buildContent(),
);
}
}For branding: Match your app's design system
AdaptiveConvexNavigation(
selectedIndex: _selectedIndex,
onTap: _handleTap,
// Custom breakpoint
breakpoint: 800,
// Mobile styling
convexStyle: TabStyle.react,
backgroundColor: Colors.deepPurple,
activeColor: Colors.white,
color: Colors.white70,
gradient: LinearGradient(
colors: [Colors.purple, Colors.blue],
),
// Desktop styling
railBackgroundColor: Colors.deepPurple,
railLabelType: NavigationRailLabelType.all,
railSelectedIconTheme: IconThemeData(color: Colors.white, size: 28),
railUnselectedIconTheme: IconThemeData(color: Colors.white70, size: 24),
items: const [...],
child: MyContent(),
)class ShoppingAppNavigation extends StatefulWidget {
@override
State<ShoppingAppNavigation> createState() => _ShoppingAppNavigationState();
}
class _ShoppingAppNavigationState extends State<ShoppingAppNavigation> {
int _selectedIndex = 0;
int _cartItemCount = 3;
@override
Widget build(BuildContext context) {
return AdaptiveConvexNavigation.badge(
{2: _cartItemCount > 0 ? '$_cartItemCount' : null},
selectedIndex: _selectedIndex,
onTap: (index) => setState(() => _selectedIndex = index),
backgroundColor: Theme.of(context).primaryColor,
items: const [
TabItem(icon: Icons.home, title: 'Home'),
TabItem(icon: Icons.search, title: 'Search'),
TabItem(icon: Icons.shopping_cart, title: 'Cart'),
TabItem(icon: Icons.favorite, title: 'Wishlist'),
TabItem(icon: Icons.person, title: 'Account'),
],
child: _buildShoppingContent(),
);
}
}class SocialAppNavigation extends StatefulWidget {
@override
State<SocialAppNavigation> createState() => _SocialAppNavigationState();
}
class _SocialAppNavigationState extends State<SocialAppNavigation> {
int _selectedIndex = 0;
final Map<int, dynamic> _notifications = {
2: '12', // Messages
3: Icons.circle, // Activity dot
};
@override
Widget build(BuildContext context) {
return AdaptiveConvexNavigation.badge(
_getVisibleBadges(),
selectedIndex: _selectedIndex,
onTap: _handleNavigation,
convexStyle: TabStyle.reactCircle,
items: const [
TabItem(icon: Icons.home, title: 'Feed'),
TabItem(icon: Icons.search, title: 'Explore'),
TabItem(icon: Icons.message, title: 'Messages'),
TabItem(icon: Icons.notifications, title: 'Activity'),
TabItem(icon: Icons.person, title: 'Profile'),
],
child: _buildFeed(),
);
}
Map<int, dynamic> _getVisibleBadges() {
final badges = Map<int, dynamic>.from(_notifications);
badges.remove(_selectedIndex);
return badges;
}
}The widget automatically adapts to different screen sizes:
| Platform | Small Screen | Large Screen |
|---|---|---|
| iOS | ConvexAppBar (bottom) | NavigationRail (side) |
| Android | ConvexAppBar (bottom) | NavigationRail (side) |
| Web | ConvexAppBar (bottom) | NavigationRail (side) |
| Desktop | N/A | NavigationRail (side) |
Breakpoint: 600px by default (customizable)
// β
GOOD: Badges hide on active tab, reappear when navigating away
Map<int, dynamic> get _visibleBadges {
final visible = Map.from(_badgeData);
visible.remove(_selectedIndex); // Hide active tab's badge
return visible;
}
// β BAD: Badges permanently disappear on tap
void _onTap(int index) {
setState(() {
_badges.remove(index); // Don't do this!
});
}// β
GOOD: Clear badges from page content after action
void _markMessagesAsRead() {
// Mark messages as read in your backend
setState(() {
_badges.remove(1); // Then clear the badge
});
}
// Use post-frame callback for button clicks
void _clearBadge(int index) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() => _badges.remove(index));
}
});
}// β
GOOD: Your content should also be responsive
child: LayoutBuilder(
builder: (context, constraints) {
final isWide = constraints.maxWidth >= 600;
return isWide
? DesktopLayout()
: MobileLayout();
},
)// β BAD: Don't nest Scaffolds
child: Scaffold( // AdaptiveConvexNavigation already creates Scaffold on mobile!
appBar: AppBar(...),
body: Content(),
)
// β
GOOD: Use Column with AppBar
child: Column(
children: [
AppBar(...),
Expanded(child: Content()),
],
)TabStyle.fixed // Fixed center convex
TabStyle.fixedCircle // Fixed center with circle
TabStyle.react // Convex follows selection
TabStyle.reactCircle // Convex follows with circle
TabStyle.textIn // Text animates in
TabStyle.titled // Text always visible
TabStyle.flip // Flip animation// Define theme colors
const primary = Colors.blue;
const activeColor = Colors.white;
const inactiveColor = Colors.white70;
AdaptiveConvexNavigation(
// Mobile colors
backgroundColor: primary,
activeColor: activeColor,
color: inactiveColor,
// Desktop colors (match mobile)
railBackgroundColor: primary,
railSelectedIconTheme: IconThemeData(color: activeColor),
railUnselectedIconTheme: IconThemeData(color: inactiveColor),
// ...
)See ADAPTIVE_NAVIGATION.md for full API documentation.
- Check that badge data exists for that index
- Ensure badge data is not null or empty string
- Verify active tab's badge is being hidden intentionally
- Use
WidgetsBinding.instance.addPostFrameCallbackfor state updates - Add
mountedcheck before calling setState - Use
ValueKeyfor widgets that change state
- Avoid nested Scaffolds
- Use Column instead of Scaffold for child content
- Test at different breakpoints
- Check that you're removing from the correct index
- Verify
_visibleBadgesgetter is excluding active tab - Ensure setState is being called
See example/lib/adaptive_badge_example.dart for a complete, production-ready implementation with:
- β Badge state management
- β Clear badge functionality
- β Responsive design
- β Multiple tab types
- β Custom styling
- API Documentation: See inline documentation in source code
- Examples: Check
example/folder for working demos - Styling Guide: See
doc/adaptive-navigation.md - Badge Improvements: See
doc/badge-improvements.md
- Start Simple: Begin with basic implementation, add features as needed
- Test Responsive: Always test at multiple screen sizes
- Badge Behavior: Follow platform conventions for notification badges
- Styling: Match your app's theme for consistent user experience
- Performance: Use const constructors where possible
Your navigation is now:
- β Responsive (mobile + desktop)
- β Beautiful (ConvexAppBar + NavigationRail)
- β Functional (badges, theming, animations)
- β Production-ready (error-free, tested)
Happy coding! π