A common Android UI pattern for edit screens is to ask the user to confirm that they want to discard their changes when they tap the back button or back navigation arrow. So, how do you catch this user event in Flutter?
Well, after a fair amount of trial and error, the solution turned out to be quite simple, using a WillPopScope.
Set up an app with a list screen and an edit screen
In this code tutorial, we will create an app with 2 screens. The home screen shows a list of items. The second screen, accessed when tapping an item in the list, allows the user to edit the item title. This screen has a confirm icon in the top right of the app bar, and a back arrow navigation in the top left. If the user has edited the item title, a confirmation dialog asking the user to confirm they want to abandon their changes is shown.
To follow the code tutorial, create a new app as follows.
1 |
flutter create backeventexample |
If you’re unsure how to set up a Flutter app, check out Getting started with Flutter official tutorial.
Firstly, we create a Material app in main.dart, which will launch the ListPage widget.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import 'package:flutter/material.dart'; import 'list/list_page.dart'; void main() { runApp(new MyApp()); } class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return new MaterialApp( title: 'Back Button Event Example', theme: new ThemeData( primaryColor: const Color(0xFF43a047), accentColor: const Color(0xFFffcc00), primaryColorBrightness: Brightness.dark, ), home: new ListPage(), ); } } |
Secondly, we create a subpackage list and create list_page.dart in it. It displays a simple list using ListTile. Tapping on a list item opens EditPage.
Note: the items are simply stored in the stateful widget, but for a real app, you should pay attention to your app architecture and separate your concerns (for specific Flutter examples, check out MVP or Flux ).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
import 'package:flutter/material.dart'; import 'dart:async'; import '../edit/edit_page.dart'; const double _ITEM_HEIGHT = 70.0; class ListPage extends StatefulWidget { ListPage({Key key}) : super(key: key); @override _ListPageState createState() => new _ListPageState(); } class _ListPageState extends State<ListPage> { List<Item> _items; @override void initState() { super.initState(); // TODO - this is a shortcut to specify items. // In a real app, you would get them // from your data repository or similar. _items = new List<Item>(); _items.add(new Item(0, "Apples")); _items.add(new Item(1, "Oranges")); _items.add(new Item(2, "Rosemary")); _items.add(new Item(3, "Carrots")); _items.add(new Item(4, "Potatoes")); _items.add(new Item(5, "Mushrooms")); _items.add(new Item(6, "Thyme")); _items.add(new Item(7, "Tomatoes")); _items.add(new Item(8, "Peppers")); _items.add(new Item(9, "Salt")); _items.add(new Item(10, "Ground ginger")); _items.add(new Item(11, "Cucumber")); } @override Widget build(BuildContext context) { Widget itemsWidget = new ListView( scrollDirection: Axis.vertical, shrinkWrap: true, children: _items.map((Item item) { return _singleItemDisplay(item); }).toList()); return new Scaffold( appBar: new AppBar( title: new Text("List of items"), ), body: new Padding( padding: new EdgeInsets.symmetric(vertical: 0.0, horizontal: 8.0), child: new Column(children: <Widget>[ new Expanded( child: itemsWidget, ), ], ), ), ); } Widget _singleItemDisplay(Item item) { return new ListTile( title: new Text(item.displayName), onTap: () { _showItem(item); }, ); } @override Future _showItem(Item item) async { Map results = await Navigator.of(context).push( new MaterialPageRoute<dynamic>( builder: (BuildContext context) { return new EditPage(item: item,); }, )); } } class Item { final int id; String displayName; Item(this.id, this.displayName); } |
Thirdly, we create a subpackage edit and create edit_page.dart in it. It displays EditPage, which contains a TextField with the list item title.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
import 'package:flutter/material.dart'; import '../list/list_page.dart'; class EditPage extends StatefulWidget { EditPage({Key key, this.item}) : super(key: key); Item item; @override _EditPageState createState() => new _EditPageState(); } class _EditPageState extends State<EditPage> { TextEditingController controller; @override void initState() { super.initState(); controller = new TextEditingController(); controller.text = widget.item.displayName; } @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text('Edit Item #' + widget.item.id.toString()), ), body: new Padding( padding: new EdgeInsets.symmetric(vertical: 0.0, horizontal: 8.0), child: _buildTextField(), ), ); } Widget _buildTextField() { InputDecoration inputDecoration = new InputDecoration( hintText: 'New title', hintStyle: _styleHint(), ); return new TextField( decoration: inputDecoration, controller: controller, ); } TextStyle _styleHint() { return new TextStyle( color: new Color(0x99000000), fontSize: 14.0, ); } } |
Now, we have an edit screen that does absolutely nothing when the user changes the data in the field. Let’s add some logic to it!
Show confirm icon in toolbar only if user has changed the title
On the edit screen, let’s show a confirm icon if, and only if, the user has made some changes to the title. For this, we track the edited title. Let’s amend edit_page.dart as below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
import 'package:flutter/material.dart'; import '../list/list_page.dart'; class EditPage extends StatefulWidget { EditPage({Key key, this.item}) : super(key: key); Item item; @override _EditPageState createState() => new _EditPageState(); } class _EditPageState extends State<EditPage> { TextEditingController controller; String _editedTitle; @override void initState() { super.initState(); controller = new TextEditingController(); controller.text = widget.item.displayName; _editedTitle = widget.item.displayName; } @override Widget build(BuildContext context) { List<Widget> menu = _hasUserEditedTitle()? <Widget>[ new IconButton( icon: new Icon(Icons.done), tooltip: 'Save', onPressed: () => _save(), ) ]: null; return new Scaffold( appBar: new AppBar( title: new Text('Edit Item #' + widget.item.id.toString()), actions: menu, ), body: new Padding( padding: new EdgeInsets.symmetric(vertical: 0.0, horizontal: 8.0), child: _buildTextField(), ), ); } void _save() { // TODO } Widget _buildTextField() { InputDecoration inputDecoration = new InputDecoration( hintText: 'New title', hintStyle: _styleHint(), ); return new TextField( decoration: inputDecoration, controller: controller, onChanged: (String value) { setState( () { _editedTitle = value; }); }, ); } bool _hasUserEditedTitle() { return widget.item.displayName != _editedTitle; } TextStyle _styleHint() { return new TextStyle( color: new Color(0x99000000), fontSize: 14.0, ); } } |
Now, when the user taps the confirm icon, _save() is called. Let’s implement this to pop the screen and pass back the title to ListPage. So we edit _save() in edit_page.dart.
1 2 3 |
void _save() { Navigator.of(context).pop({'NewTitle':_editedTitle}); } |
And now, we amend _showItem in list_page.dart.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@override Future _showItem(Item item) async { Map results = await Navigator.of(context).push( new MaterialPageRoute<dynamic>( builder: (BuildContext context) { return new EditPage(item: item,); }, )); if (results != null && results.containsKey('NewTitle')) { setState(() { item.displayName = results['NewTitle']; }); } } |
Add WillPopScope to catch ‘pop’ event
But what about the back button and back navigation arrow? We need to know when the user has tapped them, because if the title has changed, we want to ask the user to confirm they want to discard their changes. To catch this user event, we will add a WillPopScope to EditPage as below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import 'dart:async'; [...] @override Widget build(BuildContext context) { List<Widget> menu = _hasUserEditedTitle()? <Widget>[ new IconButton( icon: new Icon(Icons.done), tooltip: 'Save', onPressed: () => _save(), ) ]: null; return new WillPopScope( onWillPop: _requestPop, child: new Scaffold( appBar: new AppBar( title: new Text('Edit Item #' + widget.item.id.toString()), actions: menu, ), body: new Padding( padding: new EdgeInsets.symmetric(vertical: 0.0, horizontal: 8.0), child: _buildTextField(), ), ), ); } Future<bool> _requestPop() { // TODO return new Future.value(true); } |
Now, when the user tap either the device back button or the left arrow in the app bar, the method _requestPop() is fired. Let’s implement it. It will show a dialog if, and only if, the user has made some changes to the title.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
Future<bool> _requestPop() { if (_hasUserEditedTitle()) { showDialog<Null>( context: context, barrierDismissible: false, child: new AlertDialog( title: new Text('Discard your changes?'), content: new Text(''), actions: <Widget>[ new FlatButton( child: new Text('NO'), onPressed: () { Navigator.of(context).pop(); }, ), new FlatButton( child: new Text('DISCARD'), onPressed: () { Navigator.of(context).pop(); Navigator.of(context).pop(); }, ), ], ), ); return new Future.value(false); } else { return new Future.value(true); } } |
Voila!

What next?
Can you write an integration test to verify the edited item title is shown properly in the list? Read How to write an integration test in Flutter.

Mobile app developer with 12 years experience. Started with the Android SDK in 2010 and switched to Flutter in 2017. Past apps range from start ups to large tech (Google) and non tech (British Airways) companies, in various sectors (transport, commercial operations, e-commerce).
Nice. Thanks.
Thanks for this great article. Articles like this that provide a complete way to solve a more common patterns in building app and situations is best articles for beginners in new frameworks. Provide more examples how to solve a general practical tasks in flutter! We will read it with enjoy!
Thank you very much, just what I was searching for ‘willPopScope’.
Very useful. i m new to coding.
Hello,
Tried to use willpopscope to handle Android back button but when we edit the textfield or take focus to textfield available on second screen then willpop method doesn’t work . It is working only the time I don’t select textfield. Pls help Me want to take user to previous screen from current one having textfield.
Thanks