flutter_navkit 1.0.4
flutter_navkit: ^1.0.4 copied to clipboard
A powerful navigation toolkit for Flutter with type-safe routing, automatic route generation, and comprehensive navigation observability.
🧭 Flutter NavKit #
A powerful and elegant navigation toolkit for Flutter that simplifies routing with type-safe navigation, automatic route generation, and comprehensive navigation observability.
✨ Features #
- 🎯 Type-Safe Navigation - Auto-generated route constants with IDE autocomplete
- 🔍 Navigation Observer - Built-in tracking with detailed logging and route stack visualization
- 🚀 Simple Extensions - Clean, intuitive extension methods on
BuildContext - 📦 Auto Route Generation - Annotate widgets and generate routes automatically
- 🎨 Custom Transitions - Built-in fade, slide, and scale animations
- 🎭 UI Helpers - Show sheets, dialogs, and snackbars with ease
- 🔄 Stack Management - Check routes, pop multiple screens, navigate safely
- 🛡️ Error Handling - Built-in error logging with safe navigation fallbacks
- 📱 Flutter-Native - Works seamlessly with Flutter's navigation system
📦 Installation #
Add this to your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
flutter_navkit: ^1.0.4
dev_dependencies:
build_runner: ^2.4.13
Then run:
flutter pub get
🚀 Quick Start #
Step 1: Setup Your App #
Replace MaterialApp with NavkitMaterialApp:
import 'package:flutter/material.dart';
import 'package:flutter_navkit/flutter_navkit.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return NavkitMaterialApp(
title: 'My App',
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
observeWithStack: true, // Enable debug logging
navkitRoutes: const [
HomeScreen(), // Must be first if marked as initial!
ProfileScreen(),
SettingsScreen(),
],
);
}
}
Step 2: Annotate Your Screens #
import 'package:flutter/material.dart';
import 'package:flutter_navkit/flutter_navkit.dart';
// Mark as initial/home route
@NavkitRoute(isInitial: true)
class HomeScreen extends StatelessWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Named navigation (recommended)
ElevatedButton(
onPressed: () => context.toNamed(NavkitRoutes.profileScreen),
child: const Text('Go to Profile (Named)'),
),
// Direct widget navigation
ElevatedButton(
onPressed: () => context.to(const ProfileScreen()),
child: const Text('Go to Profile (Direct)'),
),
// With fade animation
ElevatedButton(
onPressed: () => context.toWithFade(const SettingsScreen()),
child: const Text('Go to Settings (Fade)'),
),
],
),
),
);
}
}
@NavkitRoute()
class ProfileScreen extends StatelessWidget {
const ProfileScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Profile')),
body: Center(
child: ElevatedButton(
onPressed: () => context.back(),
child: const Text('Go Back'),
),
),
);
}
}
Step 3: Create build.yaml #
Create a build.yaml file in your project root:
targets:
$default:
builders:
flutter_navkit|navkit_routes_builder:
enabled: true
Step 4: Generate Routes #
# One-time generation
dart run build_runner build --delete-conflicting-outputs
# Or watch for changes (recommended during development)
dart run build_runner watch --delete-conflicting-outputs
This generates lib/main.navkit.dart:
class NavkitRoutes {
NavkitRoutes._();
/// Route name for HomeScreen
static const String homeScreen = '/';
/// Route name for ProfileScreen
static const String profileScreen = '/profileScreenRoute';
/// All registered routes
static const Map<String, String> all = {
'HomeScreen': '/',
'ProfileScreen': '/profileScreenRoute',
};
}
Step 5: Import Generated File #
import 'main.navkit.dart'; // Import generated routes
That's it! 🎉 You're ready to navigate!
🎯 Core Concepts #
Route Generation #
Routes are auto-generated from class names:
@NavkitRoute()
class ProfileScreen extends StatelessWidget {}
// Generated: NavkitRoutes.profileScreen → '/profileScreenRoute'
@NavkitRoute(isInitial: true)
class HomeScreen extends StatelessWidget {}
// Generated: NavkitRoutes.homeScreen → '/' (root route)
1️⃣ Named Navigation Extension #
Type-safe navigation using generated route constants.
Methods: #
/// Push a named route
context.toNamed(NavkitRoutes.profileScreen);
context.toNamed(NavkitRoutes.profileScreen, args: {'userId': 123});
/// Replace current route
context.replaceWithNamed(NavkitRoutes.loginScreen);
/// Pop and push
context.backAndToNamed(NavkitRoutes.homeScreen);
/// Push and clear stack
context.toNamedAndRemoveAll(NavkitRoutes.loginScreen);
/// Pop to specific route
context.backToNamed(NavkitRoutes.homeScreen);
/// Ensure on route (navigate if not in stack, pop to it if exists)
context.ensureOnRoute(NavkitRoutes.settingsScreen);
/// Remove specific route from stack
context.removeRouteByName(NavkitRoutes.oldScreen);
Getters: #
// Check if route exists in stack
bool exists = context.canPopToNamed(NavkitRoutes.profile);
// Check if current route matches
bool isCurrent = context.isCurrentRoute(NavkitRoutes.home);
// Check if route is anywhere in stack
bool inStack = context.isRouteInStack(NavkitRoutes.settings);
// Get stack length
int length = context.stackLength;
// Check if first route
bool isFirst = context.isFirstRoute;
// Check if current route can pop
bool canPop = context.canPopRoute;
// Get current route name
String? name = context.currentRouteName;
2️⃣ Direct Widget Navigation Extension #
Navigate using widget instances directly (no route names needed).
Methods: #
/// Push a screen
context.to(ProfileScreen());
/// Replace current screen
context.replaceWith(LoginScreen());
/// Push and remove all previous
context.toAndRemoveAll(HomeScreen());
Note: Direct widget navigation automatically sets the route name to /${WidgetType} for logging purposes.
3️⃣ Shared Navigation Extension #
Common navigation utilities available everywhere.
Pop Methods: #
/// Simple pop
context.back();
context.back(result: 'some data');
/// Pop to first route
context.backToFirst();
/// Pop multiple screens
context.backMultiple(3); // Pop 3 screens
/// Pop until condition
context.backUntil(predicate: (route) => route.settings.name == '/home');
/// Safe pop (returns bool)
bool didPop = await context.maybeBack();
bool didPop = await context.maybeBack(result: 'data');
UI Helpers: #
/// Show bottom sheet
context.showSheet<String>(
builder: (context) => Container(
height: 300,
child: Center(child: Text('Bottom Sheet')),
),
enableDrag: true,
isScrollControlled: true,
);
/// Show dialog
context.showAppDialog<bool>(
builder: (context) => AlertDialog(
title: const Text('Confirm'),
content: const Text('Are you sure?'),
actions: [
TextButton(
onPressed: () => context.back(result: false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => context.back(result: true),
child: const Text('OK'),
),
],
),
);
/// Show snackbar
context.showSnackBar(
content: const Text('Action completed!'),
action: SnackBarAction(
label: 'UNDO',
onPressed: () {},
),
duration: const Duration(seconds: 3),
);
Argument Helpers: #
/// Check if has arguments of type
if (context.hasArguments<Map<String, dynamic>>()) {
final args = context.arguments<Map<String, dynamic>>();
print('User ID: ${args?['userId']}');
}
Getters: #
// Can navigator pop?
bool canPop = context.canPop;
4️⃣ Animated Transitions Extension #
Custom page transition animations.
Methods: #
/// Custom transition
context.toWithTransition(
ProfileScreen(),
transitionDuration: const Duration(milliseconds: 500),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return RotationTransition(
turns: animation,
child: child,
);
},
);
/// Fade transition
context.toWithFade(SettingsScreen());
/// Slide transition (from right)
context.toWithSlide(ProfileScreen());
/// Scale transition
context.toWithScale(DetailsScreen());
🔍 Navigation Observer #
NavKit includes a powerful observer that tracks all navigation events with beautiful console logs.
Features: #
- ✅ Tracks push, pop, replace, and remove events
- ✅ Displays route stack in debug mode
- ✅ Check if routes exist in the stack
- ✅ Access to full navigation history
Console Output: #
➡️ Push → ProfileScreen (from: HomeScreen)
────────────────────────────────────
📚 Route Stack:
• Initial
• HomeScreen
• ProfileScreen
────────────────────────────────────
⬅️ Pop → ProfileScreen (back to: HomeScreen)
────────────────────────────────────
📚 Route Stack:
• Initial
• HomeScreen
────────────────────────────────────
🔀 Replace → LoginScreen → HomeScreen
🔄 Remove → SettingsScreen
Enable Stack Logging: #
NavkitMaterialApp(
observeWithStack: true, // Show route stack after each navigation
navkitRoutes: [...],
)
Check Route Existence: #
if (NavkitObserver.hasRoute('/profile')) {
print('Profile route exists in stack');
}
// Access routes list
List<Route> routes = NavkitObserver.routes;
print('Total routes: ${routes.length}');
🎨 @NavkitRoute Annotation #
Mark widgets for automatic route generation.
Parameters: #
| Parameter | Type | Default | Description |
|---|---|---|---|
isInitial |
bool |
false |
Mark as the initial/home route (gets "/" path) |
Usage: #
// Initial route (root)
@NavkitRoute(isInitial: true)
class HomeScreen extends StatelessWidget {}
// Generated: NavkitRoutes.homeScreen → '/'
// Regular routes
@NavkitRoute()
class ProfileScreen extends StatelessWidget {}
// Generated: NavkitRoutes.profileScreen → '/profileScreenRoute'
Rules: #
- ⚠️ Only ONE route can have
isInitial: true - ⚠️ The initial route must be FIRST in the
navkitRouteslist - ✅ Route names are auto-generated from class names
- ✅ Class name
HomeScreen→ generateshomeScreenconstant
Naming Convention: #
HomeScreen → homeScreen → '/homeScreenRoute'
ProfileScreen → profileScreen → '/profileScreenRoute'
🛠️ NavkitMaterialApp #
A drop-in replacement for MaterialApp with automatic NavKit integration.
Key Parameters: #
NavkitMaterialApp(
// NavKit-specific
navkitRoutes: const [ // Auto-generate routes from widgets
HomeScreen(),
ProfileScreen(),
],
observeWithStack: true, // Enable stack logging (default: false)
// Standard MaterialApp parameters
home: const HomeScreen(),
title: 'My App',
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
themeMode: ThemeMode.system,
initialRoute: '/',
navigatorKey: navigatorKey,
navigatorObservers: [...],
// All other MaterialApp parameters supported
locale: const Locale('en', 'US'),
supportedLocales: const [Locale('en'), Locale('ar')],
localizationsDelegates: [...],
debugShowCheckedModeBanner: false,
// ... and more
)
How navkitRoutes Works: #
When you provide widgets to navkitRoutes:
- NavKit generates route names from class names
- The route marked with
isInitial: truegets/(root) - Others get auto-generated routes like
/screenNameRoute - Routes are registered in
MaterialApp.routes
Example:
navkitRoutes: const [
HomeScreen(), // Must be first if marked with isInitial: true
ProfileScreen(), // → '/profileScreenRoute'
]
Important: The widget marked with @NavkitRoute(isInitial: true) MUST be the first in the navkitRoutes list!
🎯 Best Practices #
✅ DO: #
- Use
@NavkitRoute(isInitial: true)for your home screen - Place the initial route FIRST in the
navkitRouteslist - Use named navigation (
context.toNamed) for better type safety - Check route existence with
Observer.hasRoute()orcontext.canPopToNamed()before navigating - Enable
observeWithStack: trueduring development for better debugging - Use
context.canPopbefore callingcontext.back()to avoid errors - Pass complex data through constructor arguments instead of route args
- Run code generator after adding/removing
@NavkitRouteannotations - Use typed argument helpers like
context.arguments<T>()for type safety - Handle navigation results with null checks
❌ DON'T: #
- Don't mark multiple routes as
isInitial: true - Don't forget to run
dart run build_runner buildafter adding new routes - Don't use string literals for route names - use generated constants
- Don't navigate without checking if routes exist in production
- Don't pass large objects through route arguments - use state management
- Don't ignore the order - initial route must be first in
navkitRoutes - Don't forget to import the generated
.navkit.dartfile
🔄 Regenerating Routes #
Whenever you add, remove, or modify @NavkitRoute annotations:
# Clean previous builds
dart run build_runner clean
# Generate new routes
dart run build_runner build --delete-conflicting-outputs
# Or watch for changes (recommended during development)
dart run build_runner watch --delete-conflicting-outputs
# With verbose output for debugging
dart run build_runner build --delete-conflicting-outputs --verbose
What Gets Generated: #
For each file with @NavkitRoute annotations, a corresponding .navkit.dart file is created:
lib/
├── main.dart
├── main.navkit.dart ← Generated
├── screens/
│ ├── home_screen.dart
│ ├── home_screen.navkit.dart ← Generated
│ ├── profile_screen.dart
│ └── profile_screen.navkit.dart ← Generated
Add to .gitignore: #
# Generated NavKit files
*.navkit.dart
🐛 Troubleshooting #
Routes Not Generating? #
Problem: Code generator doesn't create .navkit.dart files.
Solutions:
- Ensure
build.yamlexists in project root - Check that
build_runneris indev_dependencies - Verify annotations are correct:
@NavkitRoute() - Run with verbose flag:
dart run build_runner build --delete-conflicting-outputs --verbose - Clean and rebuild:
dart run build_runner clean dart run build_runner build --delete-conflicting-outputs
Navigation Not Working? #
Problem: Navigation methods fail silently or crash.
Solutions:
- Check that the route is registered in
navkitRoutes - Verify the generated file is imported:
import 'main.navkit.dart'; - Enable
observeWithStack: trueto see navigation logs - Check console for error messages
- Ensure you're using the correct route constant:
NavkitRoutes.screenName
Multiple Initial Routes Error? #
Problem: Build fails with "Multiple routes marked as initial" error.
Solution: Only one route can have isInitial: true. Check all your @NavkitRoute annotations and ensure only one has this flag set.
// ❌ Wrong - Multiple initial routes
@NavkitRoute(isInitial: true)
class HomeScreen extends StatelessWidget {}
@NavkitRoute(isInitial: true) // ERROR!
class WelcomeScreen extends StatelessWidget {}
// ✅ Correct - Only one initial route
@NavkitRoute(isInitial: true)
class HomeScreen extends StatelessWidget {}
@NavkitRoute()
class WelcomeScreen extends StatelessWidget {}
Import Errors? #
Problem: Cannot find NavkitRoutes or generated file.
Solution: Make sure to import the generated file:
import 'main.navkit.dart'; // or 'your_file.navkit.dart'
Stack Not Showing in Logs? #
Problem: Navigation logs appear but stack doesn't print.
Solution: Enable stack logging:
NavkitMaterialApp(
observeWithStack: true, // Set to true
navkitRoutes: [...],
)
Route Order Issues? #
Problem: Initial route not working or wrong route is displayed first.
Solution: Ensure the route marked with isInitial: true is first in the navkitRoutes list:
// ❌ Wrong - Initial route not first
navkitRoutes: [
ProfileScreen(),
HomeScreen(), // marked with isInitial: true but not first
]
// ✅ Correct - Initial route is first
navkitRoutes: [
HomeScreen(), // marked with isInitial: true and is first
ProfileScreen(),
]
📄 License #
This project is licensed under the MIT License - see the LICENSE file for details.
📬 Support & Contact #
- 📦 Package: pub.dev
- 🐛 Issues: Report a bug
📊 Package Stats #
- 📦 Version: 1.0.4
- ⭐ GitHub: flutter_navkit
- 📱 Platforms: iOS, Android, Web, Desktop
⭐ Show Your Support #
If you find this package helpful:
- 👍 Like it on pub.dev
- ⭐ Star the repo on GitHub
- 📢 Share with the Flutter community
- 🐛 Report issues to help improve the package
Made by Rado Dayef
Happy Navigating! 🚀