Tutorial 7: Flutter Navigation, Layouts, Forms, and Input Elements
Learning Objectives
After completing this tutorial, students are expected to be able to:
- Understand basic navigation and routing in Flutter.
- Understand input and form elements in Flutter.
- Understand the process of creating forms and handling data in Flutter.
- Understand and implementing simple clean architecture.
Page Navigation in Flutter
When learning web development, you've probably already learned that on a website, you can navigate between pages based on the accessed URL. The same concept applies to app development, where you can move from one 'page' to another. However, in an application, navigations is not done by accessing different URLs.
To implement navigation in Flutter, a fairly complete system has been provided to navigate between pages. One way to navigate between pages is by using the Navigator widget. The Navigator widget displays pages as if they were in a stack. To navigate to a new page, you can access the Navigator through the BuildContext and call functions like push(), pop(), and pushReplacement().
Note: In Flutter, screens and pages are often referred to as route.
We will explain some of the most frequently encountered uses of Navigator in application development.
Push (push())
...
if (item.name == "Add Product") {
Navigator.push(context,
MaterialPageRoute(builder: (context) => const ShopFormPage()));
}
...
The push() method adds a route to the route stack managed by Navigator. This method causes the added route to be at the top of the stack, so that the newly added route will appear and be displayed to the user.
Pop (pop())
...
onPressed: () {
Navigator.pop(context);
},
...
The pop() method removes the route currently displayed to the user (or in other words, the route at the top of the stack) from the stack routes managed by the Navigator. This method causes the application to move from the route currently displayed to the user to the route that is below it in the stack managed by Navigator.
Push Replacement (pushReplacement())
...
onTap: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => MyHomePage(),
));
},
...
The pushReplacement() method removes the currently displayed route and replaces it with a new route. This method causes the application to transition from the currently displayed route to the provided route. In the managed route stack by the Navigator, the old route at the top of the stack is directly replaced by the new route without altering the state of the stack elements beneath it.
Although push() and pushReplacement() may seem similar , the key difference lies in what they do to the route at the top of the stack. push() adds a new route on top of the existing routes, while pushReplacement() replaces the existing route at the top of the stack with the new route. It's important to consider the order and contents of the stack because if the stack is empty, and you press the Back button on the device, the system will exit the application.
In addition to these three Navigator methods, there are other methods that can facilitate routing in app development, such as popUntil(), canPop(), and maybePop(). Feel free to explore these methods on your own. For a deeper understanding of Navigator, you can refer to the documentation at the following link: https://api.flutter.dev/flutter/widgets/Navigator-class.html
Input and Form in Flutter
Just like a website, an application can also interact with users through input and forms. Flutter provides a Form widget that serves as a container for multiple input field widgets you create. Similar to web input fields, Flutter offers various types of input fields, including the TextField widget.
To try a Form widget, run the following command:
flutter create --sample=widgets.Form.1 form_sample
For further information about the Form widget, you can refer to the following link: Flutter Form Class.
Tutorial: Adding a Menu Drawer for Navigation
To simplify navigation in a Flutter application, you can add a drawer menu. A drawer menu is a menu that appears from the left or right side of the screen and typically contains navigation links to other pages in the application.
Open the project that you previously created in tutorial 6 using your favorite IDE.
Create a new file in a new directory called
widgetswith the nameleft_drawer.dart. Add the following code to theleft_drawer.dartfile.import 'package:flutter/material.dart';
class LeftDrawer extends StatelessWidget {
const LeftDrawer({super.key});
@override
Widget build(BuildContext context) {
return Drawer(
child: ListView(
children: [
const DrawerHeader(
// TODO: drawer header section
),
// TODO: routing section
],
),
);
}
}Next, add imports for the pages you want to include in the navigation drawer. In this example, we will add navigation to the
MyHomePageandShopFormPagepages.import 'package:flutter/material.dart';
import 'package:shopping_list/menu.dart';
// TODO: Import the ShopFormPage page hereAfter importing, add routing for the imported pages to the
TODO: Routing section....
ListTile(
leading: const Icon(Icons.home_outlined),
title: const Text('Homa Page'),
// redirect to MyHomePage
onTap: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => MyHomePage(),
));
},
),
ListTile(
leading: const Icon(Icons.add_shopping_cart),
title: const Text('Add Product'),
// redirect to ShopFormPage
onTap: () {
/*
TODO: Create routing to ShopFormPage here
*/
},
),
...Then, decorate the drawer by adding a drawer header in the
TODO: Drawer header section....
const DrawerHeader(
decoration: BoxDecoration(
color: Colors.indigo,
),
child: Column(
children: [
Text(
'Shopping List',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
Padding(padding: EdgeInsets.all(10)),
Text("Write all your shopping needs here!",
// TODO: Add a text style with center alignment, font size 15, white color, and regular weight
),
],
),
),
...Congratulations, you have successfully created a drawer menu. Now, add the drawer to the page where you want to include the drawer. For this step, we will provide an example of adding it to the
menu.dartpage....
// import drawer widget
import 'package:shopping_list/widgets/left_drawer.dart';
...
return Scaffold(
appBar: AppBar(
title: const Text(
'Shopping List',
),
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
),
// Add drawer as parameter of the Scaffold widget
drawer: const LeftDrawer(),
...Congratulations, the drawer and navigation have been set up perfectly. Run the program to see the results. Don't forget to complete the remaining
TODOtasks before submitting the tutorial (submitted tutorials should have noTODOleft). Also, don't forget to add the drawer to theShopFormPageif that page has been created.
Tutorial: Adding Forms and Input Elements
Now, we will create a simple form to enter product data into the application, allowing you to add new data to be displayed later.
Create a new file in the
libdirectory namedshoplist_form.dart. Add the following code to theshoplist_form.dartfile.import 'package:flutter/material.dart';
// TODO: Import the previously created drawer
class ShopFormPage extends StatefulWidget {
const ShopFormPage({super.key});
@override
State<ShopFormPage> createState() => _ShopFormPageState();
}
class _ShopFormPageState extends State<ShopFormPage> {
@override
Widget build(BuildContext context) {
return Placeholder();
}
}Replace the
Placeholderwidget with the following code.Scaffold(
appBar: AppBar(
title: const Center(
child: Text(
'Add Product Form',
),
),
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
),
// TODO: Add the previously created drawer here
body: Form(
child: SingleChildScrollView(),
),
);Code Explanation:
The
Formwidget serves as a container for several input field widgets that we will create later.The
SingleChildScrollViewwidget makes the child widget inside it scrollable.
Create a new variable named
_formKeyand add it to thekeyattribute of theFormwidget. Thekeyattribute serves as the handler for form state, form validation, and form storage....
class _ShopFormPageState extends State<ShopFormPage> {
final _formKey = GlobalKey<FormState>();
......
body: Form(
key: _formKey,
child: SingleChildScrollView(),
),
...Next, we will start adding input fields to the
Formwidget. Create some variables to store input from each field you're going to create....
class _ShopFormPageState extends State<ShopFormPage> {
final _formKey = GlobalKey<FormState>();
String _name = "";
int _price = 0;
String _description = "";
...Create a
Columnwidget as a child of theSingleChildScrollView....
body: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column()
),
...Create a
TextFormFieldwidget wrapped inPaddingas one of the children of theColumn. Then, add thecrossAxisAlignmentattribute to control the alignment of theColumn's children....
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
decoration: InputDecoration(
hintText: "Product Name",
labelText: "Product Name",
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5.0),
),
),
onChanged: (String? value) {
setState(() {
_name = value!;
});
},
validator: (String? value) {
if (value == null || value.isEmpty) {
return "Name cannot be empty!";
}
return null;
},
),
),
...Code Explanation:
onChangedis called whenever there is a change in theTextFormField.validatoris used to validate the content of theTextFormFieldand return aStringin case of an error.null-safetyis implemented in the code with the use ofString?andvalue!. The?operator indicates that the variable can contain either aStringornull. The!operator indicates that the variable is guaranteed not to benull.
To learn more about null safety, you can refer to the Dart
null-safetydocumentation here.Create two more
TextFormFieldwidgets wrapped inPaddingas children of theColumnfor thepriceanddescriptionfields....
Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
decoration: InputDecoration(
hintText: "Price",
labelText: "Price",
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5.0),
),
),
onChanged: (String? value) {
setState(() {
_price = int.parse(value!);
});
},
validator: (String? value) {
if (value == null || value.isEmpty) {
return "Price cannot be empty!";
}
if (int.tryParse(value) == null) {
return "Price must be a number!";
}
return null;
},
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
decoration: InputDecoration(
hintText: "Description",
labelText: "Description",
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5.0),
),
),
onChanged: (String? value) {
setState(() {
_description = value!;
});
},
validator: (String? value) {
if (value == null || value.isEmpty) {
return "Description cannot be empty!";
}
return null;
},
),
),
...Create a button as the next child of the
Column. Wrap the button withPaddingandAlign. This time, we won't save data to the database, but we will display it in a popup that appears after clicking the button....
Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ElevatedButton(
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all(Colors.indigo),
),
onPressed: () {
if (_formKey.currentState!.validate()) {}
},
child: const Text(
"Save",
style: TextStyle(color: Colors.white),
),
),
),
),
...
Tutorial: Displaying Data
Add the
showDialog()function inside theonPressed()section of the button and display anAlertDialogwidget in this function. Also, add a function to reset the form....
child: ElevatedButton(
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all(Colors.indigo),
),
onPressed: () {
if (_formKey.currentState!.validate()) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Product successfully saved'),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text('Name: $_name'),
// TODO: Display other values
],
),
),
actions: [
TextButton(
child: const Text('OK'),
onPressed: () {
Navigator.pop(context);
},
),
],
);
},
);
_formKey.currentState!.reset();
}
},
child: const Text(
"Save",
style: TextStyle(color: Colors.white),
),
),
...Run your program, use the form you created, and see the results.
Tutorial: Adding Navigation to Buttons
Up to this point, we've successfully created a drawer that can navigate to other pages in the application, as well as a form page. In the previous tutorial, we also created three button widgets that can perform certain actions when clicked. Now, we'll add navigation functionality to these buttons so that when pressed, the user will be shown other pages.
In the
ShopItemwidget in themenu.dartfile created in the previous tutorial, we will modify the code within theonTapattribute ofInkWellto navigate to another route (add additional code below theScaffoldMessengercode that displays a snackbar)....
// Area responsive to touch
onTap: () {
// Show SnackBar when clicked
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(SnackBar(
content: Text("You pressed the ${item.name} button!")));
// Navigate to the appropriate route (depending on the button type)
if (item.name == "Add Product") {
// TODO: Use Navigator.push to navigate to a MaterialPageRoute that encompasses ShopFormPage.
}
},
...Note that for this button, we use
Navigator.push(), so users can press the Back button to return to the menu page. In addition, by usingNavigator.pop(), you can code the program to return to the menu page.Run your program, use the buttons with the new functionality, and see what happens. Compare it to what happens when you navigate through the drawer (of course, after completing all the TODOs in the drawer).
Tutorial: Refactoring Files
After creating the shoplist_form.dart page, our application has become more extensive. Let's move the pages we've created so far into a screens folder to make things easier in the future.
Before starting, make sure you have the Flutter extension installed in your IDE or text editor.
Create a new file named
shop_card.dartin thewidgetsdirectory.Move the
ShopItemwidget's contents frommenu.dartto thewidgets/shop_card.dartfile.Make sure to import the
shoplist_form.dartpage into thewidgets/shop_card.dartfile and import theshop_card.dartpage into themenu.dartfile.Create a new folder named
screensin thelibdirectory.
Move the
menu.dartandshoplist_form.dartfiles to thescreensfolder. Make sure to move these files through your IDE or text editor that has the Flutter extension or plugin, not through a regular file manager (such as File Explorer or Finder). This is done so that your IDE or text editor can perform automatic refactoring.View in Visual Studio Code

View in Android Studio

After refactoring the files, your lib directory structure should look like this:

Closing
- Congratulations! You have successfully completed Tutorial 7. 😄
Run the following commands to
add,commit, andpush:git add .
git commit -m "<commit_message>"
git push -u origin <main_branch>- Replace
<commit_message>with your desired message. For example:git commit -m "Completed tutorial 7". - Replace
<your_main_branch>with your main branch name. For example:git push -u origin mainorgit push -u origin master.
- Replace
Additional References
Contributors
- Muhammad Raditya Hanif
- Hugo Sulaiman Setiawan
- Andi Muhamad Dzaky Raihan
- Alek Yoanda Partogi Tampubolon
- Aidah Novallia Putri (EN Translator)
- Bonaventura Galang (EN Translator)
- Ferry (EN Translator)
Credits
This tutorial was developed based on PBP Odd 2023 and PBP Even 2023 written by the 2023 Platform-Based Programming Teaching Team. All tutorials and instructions included in this repository are designed so that students who are taking Platform-Based Programming courses can complete the tutorials during lab sessions.