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
Scaffold
in aWillPopScope
widget. - 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
true
to 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:
onSearchStateChanged
can be called from yourHomePage
when the search bar is active/inactive.- If you’re using
go_router
, you might have to sync_currentIndex
with the current location path. - Also for tabbed navigation, you might want to look into
ShellRoute
ingo_router
for 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
onWillPop
returnsfalse
, the pop is cancelled immediately — no further WillPopScope will be called. - If
onWillPop
returnstrue
, 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 B
is called. - If B returns
true
, thenWillPopScope A
is 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.