Imagine the scenario in a Flutter app with go_router for navigation, a bottom navigation bar (say, Home, Search, Profile, etc.) and you want to handle Back button behavior smartly:
-
- If you are on another tab (e.g., Search, Profile), pressing back should switch back to the initial tab (e.g., Home).
- If you are already on the initial tab (Home) but a search/filter is active, pressing back should clear the search first.
- Only when on Home and search is cleared, pressing back should exit the app.
Here’s the general strategy you can use:
- Wrap your whole
Scaffoldin aWillPopScopewidget. - Inside
onWillPop, implement the logic:- If not on the initial tab → set tab to initial, return
false(don’t pop). - If on the initial tab but search is active → clear search, return
false. - If on the initial tab and search is clear → return
trueto allow app exit.
- If not on the initial tab → set tab to initial, return
Example Code Structure:
class MainScreen extends StatefulWidget {
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
int _currentIndex = 0; // Track bottom navigation
bool _isSearchActive = false; // Track search state
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
if (_currentIndex != 0) {
setState(() {
_currentIndex = 0; // Go back to initial tab
});
return false; // Don't exit app
} else if (_isSearchActive) {
setState(() {
_isSearchActive = false; // Clear search
});
return false; // Don't exit app
}
return true; // Allow app to exit
},
child: Scaffold(
body: _buildPage(), // build page based on _currentIndex
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
// Also, navigate with go_router if needed
},
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Search'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
],
),
),
);
}
Widget _buildPage() {
switch (_currentIndex) {
case 0:
return HomePage(
onSearchStateChanged: (bool isSearching) {
_isSearchActive = isSearching;
},
);
case 1:
return SearchPage();
case 2:
return ProfilePage();
default:
return Container();
}
}
}
Important notes:
onSearchStateChangedcan be called from yourHomePagewhen the search bar is active/inactive.- If you’re using
go_router, you might have to sync_currentIndexwith the current location path. - Also for tabbed navigation, you might want to look into
ShellRouteingo_routerfor more advanced setups.
Quick behavior summary:
| Situation | What happens when pressing back |
|---|---|
| On Search tab | Moves back to Home tab |
| On Home tab with active search | Clears the search |
| On Home tab, search cleared | Exits the app |
📚 How WillPopScope executes when multiple are in the widget tree:
- Closest to the focused route (deepest in the tree) gets called first.
- It bubbles up from the innermost WillPopScope to the outer ones, one by one.
- If any
onWillPopreturnsfalse, the pop is cancelled immediately — no further WillPopScope will be called. - If
onWillPopreturnstrue, the next outer WillPopScope (if any) is called.
✅ First one to return false wins → pop canceled.
✅ All must return true for pop to succeed → then the Navigator pops the route.
🔥 Quick mental model:
Imagine WillPopScopes stacked like this:
App
└── Navigator
└── WillPopScope A // In a Root View
└── WillPopScope B - In a Screen View
└── WillPopScope C - In a Sub-Screen View
└── Some Very Inner View
➡️ Press Back →
First WillPopScope C‘s onWillPop is called.
- If C returns
true, thenWillPopScope Bis called. - If B returns
true, thenWillPopScope Ais called. - If A returns
true, finally the pop happens.
If any of them returns false, the pop stops immediately.
🛠 Practical example:
WillPopScope(
onWillPop: () async {
print('Outer Scope');
return true;
},
child: WillPopScope(
onWillPop: () async {
print('Inner Scope');
return true;
},
child: Scaffold(
appBar: AppBar(title: Text('Nested WillPopScope')),
),
),
);
👉 Press Back:
- Prints
Inner Scope - Then prints
Outer Scope - Then pop happens.
If Inner Scope returns false, you will never see Outer Scope being printed.
