go_router_back_handler

pub package License: MIT

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

1. Update 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

  1. Native Integration: Intercepts back button at Android native level
  2. MethodChannel: Communicates with Flutter via platform channel
  3. Smart Detection: Analyzes current route and finds appropriate parent
  4. 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?

  1. Hot restart required: Native code changes need full restart
  2. Check MainActivity.kt: Ensure it's properly configured
  3. Enable debug mode: Set debugMode: true to see logs
  4. Verify navigator key: Ensure you're passing the correct key

Exit dialog not showing?

  • Ensure the route is in rootRoutes list
  • Check if context has Scaffold ancestor
  • Try custom exit dialog for more control

🤝 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

"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!

Libraries

go_router_back_handler
A robust back button handler for GoRouter that works on all routes.