go_router_back_handler 1.0.2
go_router_back_handler: ^1.0.2 copied to clipboard
A robust back button handler for GoRouter that works on all routes including root routes. Solves the PopScope limitation with native Android integration.
go_router_back_handler #
A robust, production-ready back button handler for GoRouter that works on all routes, including root routes. Solves the PopScope limitation in GoRouter v13+ with native Android integration.
🎯 Problem Solved #
Starting with GoRouter v13+, PopScope doesn't work on root routes (see Flutter issue #140869). This package provides a reliable solution using native Android MethodChannel integration.
✨ Features #
- ✅ Works on ALL routes - Including root routes where PopScope fails
- ✅ Smart route detection - Automatically finds parent routes
- ✅ Customizable exit confirmation - Beautiful default dialog with full customization
- ✅ Sequential flow support - Perfect for registration/wizard flows
- ✅ Dynamic route handling - Works with parameterized routes like
/user/:id - ✅ Zero configuration - Works out of the box with sensible defaults
- ✅ Production tested - Battle-tested in real-world applications
- ✅ Type-safe API - Full Dart type safety
- ✅ Detailed logging - Debug mode for troubleshooting
📦 Installation #
Add to your pubspec.yaml:
dependencies:
go_router_back_handler: ^1.0.0
Run:
flutter pub get
🚀 Quick Start #
⚠️ IMPORTANT: This package requires native Android code changes. You MUST update your
MainActivity.ktfile for the package to work. Without this step, the back button will not be intercepted.
1. Update MainActivity.kt (REQUIRED) #
Location: android/app/src/main/kotlin/com/yourcompany/yourapp/MainActivity.kt
Replace your MainActivity.kt with:
package com.yourcompany.yourapp
import io.flutter.embedding.android.FlutterActivity
import android.os.Bundle
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
flutterEngine?.dartExecutor?.binaryMessenger?.let { messenger ->
val channel = MethodChannel(messenger, "com.gorouter.back_handler/back_button")
channel.invokeMethod("onBackPressed", null, object : MethodChannel.Result {
override fun success(result: Any?) {
// Flutter handled it
}
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
this@MainActivity.finish()
}
override fun notImplemented() {
this@MainActivity.finish()
}
})
}
}
}
2. Initialize in main.dart #
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:go_router_back_handler/go_router_back_handler.dart';
final rootNavigatorKey = GlobalKey<NavigatorState>();
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
// Initialize the back button handler
GoRouterBackHandler.initialize(
navigatorKey: rootNavigatorKey,
routeHierarchy: {
'/profile': '/home',
'/settings': '/home',
'/details': '/list',
},
rootRoutes: ['/', '/login', '/home'],
);
}
@override
void dispose() {
GoRouterBackHandler.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: GoRouter(
navigatorKey: rootNavigatorKey,
routes: [
// Your routes here
],
),
);
}
}
3. That's it! 🎉 #
Your back button now works intelligently on all routes.
📖 Usage Examples #
Basic Setup #
GoRouterBackHandler.initialize(
navigatorKey: rootNavigatorKey,
);
Custom Route Hierarchy #
GoRouterBackHandler.initialize(
navigatorKey: rootNavigatorKey,
routeHierarchy: {
// Authentication flow
'/login/otp': '/login',
'/register/step2': '/register/step1',
'/register/step3': '/register/step2',
// App navigation
'/profile': '/home',
'/settings': '/home',
'/details': '/list',
// Booking flow
'/checkout': '/cart',
'/payment': '/checkout',
'/confirmation': '/payment',
},
rootRoutes: ['/', '/login', '/home'],
moduleHomes: {
'/home': '/dashboard',
'/profile': '/dashboard',
},
);
Custom Exit Message #
GoRouterBackHandler.initialize(
navigatorKey: rootNavigatorKey,
exitMessage: 'Tap back again to exit',
exitDialogTitle: 'Exit App?',
exitDialogMessage: 'Are you sure you want to exit?',
);
Enable Debug Logging #
GoRouterBackHandler.initialize(
navigatorKey: rootNavigatorKey,
debugMode: true, // Shows detailed logs
);
🎨 Customization #
Custom Exit Dialog #
GoRouterBackHandler.initialize(
navigatorKey: rootNavigatorKey,
customExitDialog: (context) async {
return await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text('Custom Exit Dialog'),
content: Text('Do you want to exit?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: Text('Exit'),
),
],
),
);
},
);
Custom Toast Widget #
GoRouterBackHandler.initialize(
navigatorKey: rootNavigatorKey,
customToast: (context, message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
},
);
🔧 How It Works #
- Native Integration: Intercepts back button at Android native level
- MethodChannel: Communicates with Flutter via platform channel
- Smart Detection: Analyzes current route and finds appropriate parent
- Fallback Strategy:
- Check if root route → Show exit confirmation
- Find parent in hierarchy → Navigate to parent
- Try GoRouter's
canPop()→ Pop - Fallback → Navigate to default route
📱 Behavior #
Root Routes #
On root routes (/, /login, /home):
- First back press: Shows toast "Press back again to exit"
- Second back press (within 2s): Shows exit confirmation dialog
- After 2s: Timer resets
Child Routes #
On child routes:
- Navigates to parent route defined in hierarchy
- Falls back to GoRouter's pop if no parent defined
Module Homes #
On module home routes:
- Navigates to their parent module (e.g., /profile → /dashboard)
🐛 Troubleshooting #
Back button not working at all? #
Most common issue: MainActivity.kt not updated
- ✅ Verify MainActivity.kt is updated with the code from Step 1
- ✅ Check the channel name is exactly
com.gorouter.back_handler/back_button - ✅ Hot restart required: Native code changes need full app restart (not hot reload)
- ✅ Check package name: Update
package com.yourcompany.yourappto match your app - ✅ Enable debug mode: Set
debugMode: trueto see logs - ✅ Verify navigator key: Ensure you're passing the correct
rootNavigatorKey
Back button works but behavior is wrong? #
- Check route hierarchy: Ensure parent routes are correctly defined
- Check root routes: Verify root routes list includes your exit routes
- Enable debug logs: Set
debugMode: trueto see navigation decisions
Exit dialog not showing? #
- Ensure the route is in
rootRouteslist - Check if context has Scaffold ancestor
- Try custom exit dialog for more control
App exits immediately without confirmation? #
- MainActivity.kt is not properly configured
- Channel name mismatch between Kotlin and Dart
- Navigator key not initialized before back button press
🤝 Contributing #
Contributions are welcome! Please read our contributing guide.
📄 License #
MIT License - see LICENSE file for details.
👨💻 Author #
Md. Jehad (Jehadur Rahman Emran)
Full Stack Developer & System Architect | Cloud Connect AI
- 💼 GitHub: @JehadurRE
- 📧 Email: emran.jehadur@gmail.com
- 🔗 LinkedIn: linkedin.com/in/jehadurre
"Building digital solutions that empower developers through thoughtful design and robust engineering."
🙏 Acknowledgments #
- Inspired by the need to solve GoRouter's PopScope limitation
- Built with ❤️ for the Flutter community
📊 Stats #
- ⭐ Star this repo if you find it helpful!
- 🐛 Report issues on GitHub
- 💡 Feature requests welcome!