7.
Interactive Widgets
Written by Vincent Ngo
In the previous chapter, you learned how to capture lots of data with scrollable widgets. But how do you make your app more engaging? How do you collect input and feedback from your users?
In this chapter, you’ll explore interactive widgets. In particular, you’ll learn to create:
- Bottom Sheets widgets
- Gesture-based widgets
- Time and date picker widgets
- Input and selection widgets
- Dismissable widgets
You’ll continue to work on Yummy, building a more immersive experience. Users will be able to view menu items in detail, adjust quantities, manage and track order status.
You’ll start by enhancing the way users can view and select menu items for their cart.
Next, you’ll implement features for managing order details, including options for delivery or pickup and setting preferences for the date and time. Users will also be able to review their order summary and edit it before submission. Once an order is placed, they can track it in the Orders tab.
Additionally, you’ll ensure your app remains responsive in web mode, providing a seamless experience across different devices.
It’s time to get started.
Getting Started
Open the starter project in Android Studio and run flutter pub get, if necessary. Then, run the app. You’ll see the following:
New Project Files
There are new files in this starter project to help you out. Before you learn how to utilize interactive widgets, take a look at them.
New Packages
In pubspec.yaml under dependencies, there are two new packages:
- uuid: Generates unique keys for each menu item. This helps you know which item to add, update or remove.
- intl: Provides internationalization and localization utilities. You’ll use this to format dates.
Don’t forget to always run flutter pub get after updating pubspec.yaml entries.
New Files in the Models Folder
For your convenience two manager classes have been provided to manage state in your app:
- CartManager: Manages the user’s shopping cart. For example number of items in the cart, functions to update the cart, the total cost, delivery or self-pickup and pickup.
- OrdersManager: Manages all the orders the user has submitted.
Starting from main.dart you will notice the manager objects are initialized and passed all the way down to restaurant_page.dart. Feel free to dive into the code to see how these objects are passed down the widget tree.
With all these new additions you are now ready to start.
Presenting Item Details
Before you display a specific menu item, you’ll need a way to present its widget.
Building a Bottom Sheet
Within lib/screens/restaurant_page.dart locate the comment // TODO: Show Bottom Sheet and replace it with the following:
// 1
void _showBottomSheet(Item item) {
// 2
showModalBottomSheet<void>(
// 3
isScrollControlled: true,
// 4
context: context,
// 5
constraints: const BoxConstraints(maxWidth: 480),
// 6
// TODO: Replace with Item Details Widget
builder: (context) => Container(
color: Colors.red,
height: 400,
),
);
}
Here’s how it works:
- Define a function
_showBottomSheet()that accepts the selected menu item to display in the bottom sheet. - When invoked, create a modal bottom sheet that slides up from the bottom.
-
isScrollControlledistrue, to allow the bottom sheet to have dynamic height. - Pass in the current context to display the bottom sheet.
- Constraint the bottom sheet to have a max width of 480. This is to support responsive UI on mobile or desktop.
-
builder()returns the details to display, but for now it’s just a placeholder container.
Next, you’ll present the bottom sheet.
Presenting the Bottom Sheet
Within the same file, find and replace // TODO: Replace _buildGridItem() and the whole _buildGridItem() function beneath it with the following:
Widget _buildGridItem(int index) {
final item = widget.restaurant.items[index];
return InkWell(
onTap: () => _showBottomSheet(item),
child: RestaurantItem(item: item),
);
}
When the user taps on a menu item you’ll present the item details in a bottom sheet as shown below:
Leave restaurant_page.dart open, you’ll be coming back to it.
Building Item Details
When you tap on a specific menu item it shows a bottom sheet to focus on that specific item. Showing the title, popularity, description and enlarged image of the item.
Within the lib/components directory, create a new file called item_details.dart and add the following code:
import 'package:flutter/material.dart';
import '../models/cart_manager.dart';
import '../models/restaurant.dart';
class ItemDetails extends StatefulWidget {
final Item item;
final CartManager cartManager;
final void Function() quantityUpdated;
// 1
const ItemDetails({
super.key,
required this.item,
required this.cartManager,
required this.quantityUpdated,
});
@override
State<ItemDetails> createState() => _ItemDetailsState();
}
class _ItemDetailsState extends State<ItemDetails> {
@override
Widget build(BuildContext context) {
// 2
final textTheme = Theme.of(context)
.textTheme
.apply(displayColor: Theme.of(context).colorScheme.onSurface);
// 3
final colorTheme = Theme.of(context).colorScheme;
// 4
return Padding(
padding: const EdgeInsets.all(16.0),
// 5
child: Wrap(
children: [
// 6
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.item.name,
style: textTheme.headlineMedium,
),
// TODO: Add Liked Badge
Text(widget.item.description),
// TODO: Add Item Image
// TODO: Add Cart Control
],
),
],
),
);
}
// TODO: Create Most Liked Badge
// TODO: Create Item Image
// TODO: Create Cart Control
}
Here’s how the code works:
- The
ItemDetailswidget takes in the selected item and a cart manager to manage cart operations.quantityUpdatedis a callback that notifies the parent widget that the user updated the quantity. - Retrieve the
textThemeand ensure the text color matches the surface color of the color scheme. - Retrieve the
colorTheme, this ensures the app has a consistent color theme across all widgets in your app. - Add uniform padding of 16.0 all around.
- The
Wrapwidget organizes children in horizontal or vertical runs, adjusting the layout based on space. -
Columnwidget aligns child widgets vertically.
Leave item_details.dart open.
You’ll next replace all the TODOs and add the components to the item details widget.
Showing Item Details
Return to restaurant_page.dart and locate the comment // TODO: Replace with Item Details Widget and replace it and the builder function with the following:
builder: (context) =>
ItemDetails(
item: item,
cartManager: widget.cartManager,
quantityUpdated: () {
setState(() {});
},
),
When the bottom sheet is presented, it initializes the ItemDetails widget. When the quantityUpdated() callback is invoked, you call setState() to trigger a new render of the widget.
Next, add the following import at the top:
import '../components/item_details.dart';
Close and open the bottom sheet, it should now look like this:
Now you are ready to add more widgets!
Creating a Most Liked Badge
Back in item_details.dart, locate the comment // TODO: Create Most Liked Badge and replace it with the following:
// 1
Widget _mostLikedBadge(ColorScheme colorTheme) {
// 2
return Align(
// 3
alignment: Alignment.centerLeft,
// 4
child: Container(
padding: const EdgeInsets.all(4.0),
color: colorTheme.onPrimary,
// 5
child: const Text('#1 Most Liked'),
),
);
}
Here’s how the code works:
- Define a method
_mostLikedBadge(), which takes in aColorScheme. This method will create a badge to indicate whether an item is most liked. - The
Alignwidget is used to align the badge within the parent widget. - Align the widget center-left.
- A
Containeris used to apply padding and color. - A
Textwidget is used to display the content of the badge. In this case, it reads #1 Most Liked.
Next, locate the comment // TODO: Add Liked Badge and replace it with the following:
const SizedBox(height: 16.0),
_mostLikedBadge(colorTheme),
const SizedBox(height: 16.0),
Here you add 16.0 padding between the liked badge.
The details view now looks like this:
Showing an Item Image
Locate the comment // TODO: Create Item Image and replace it with the following:
// 1
Widget _itemImage(String imageUrl) {
// 2
return Container(
height: 200,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.0),
// 3
image: DecorationImage(
image: NetworkImage(imageUrl),
fit: BoxFit.cover,
),
),
);
}
Here’s how it works:
-
_itemImage()takes in animageUrl. - Apply a container to style the image, adding a fixed height and rounded corners.
- Set the background image.
Now, replace // TODO: Add Item Image with:
const SizedBox(height: 16.0),
_itemImage(widget.item.imageUrl),
const SizedBox(height: 16.0),
Here is what the details view looks like now:
Creating a Widget to Control the Cart
For the final piece of the item details view you’ll create a cart control component to update the quantity.
Within the lib/components directory, create a new file called cart_control.dart and add the following code:
import 'package:flutter/material.dart';
// 1
class CartControl extends StatefulWidget {
// 2
final void Function(int) addToCart;
const CartControl({
required this.addToCart,
super.key,
});
// 3
@override
State<CartControl> createState() => _CartControlState();
}
// 4
class _CartControlState extends State<CartControl> {
// 5
int _cartNumber = 1;
@override
Widget build(BuildContext context) {
// 6
final colorScheme = Theme.of(context).colorScheme;
// 7
return Row(
// 8
mainAxisAlignment: MainAxisAlignment.spaceBetween,
// 9
children: [
// TODO: Add Cart Control Components
Container(
color: Colors.red,
height: 44.0,
),
],
);
}
// TODO: Build Minus Button
// TODO: Build Cart Number
// TODO: Build Plus Button
// TODO: Build Add Cart Button
}
Here’s how it works:
- Define a stateful widget called
CartControl. - Define an
addToCart()callback function, which returns an integer to specify the number of items in the cart. - Link this widget to its state
_CartControlState(). - Define the
CartControlstate class. -
_cartNumberis a private state variable used to keep track of the quantity of items to be added to the cart. The default value is 1. - Within the
build()method, retrieve the color scheme for consistency. - Return a
Rowwidget to layout children horizontally. - Use
MainAxisAlignment.spaceBetweento space the children evenly. - Add a placeholder container which will eventually be replaced by cart control components.
Time to add the components!
Creating the Minus Button
First you’ll create the minus button.
Still in cart_control.dart, locate the comment // TODO: Build Minus Button and replace it with the following:
// 1
Widget _buildMinusButton() {
// 2
return IconButton(
icon: const Icon(Icons.remove),
// 3
onPressed: () {
setState(() {
// 4
if (_cartNumber > 1) {
_cartNumber--;
}
});
},
// 5
tooltip: 'Decrease Cart Count',
);
}
Here’s how the code works:
- Create a button to decrease the number of items in the cart.
- Initialize an
IconButtonwith theremovesymbol that renders like a minus sign. - Configure the
onPressed()callback to trigger asetState()to update the UI - Decrements
_cartNumberby 1 if it’s greater than 1 preventing it from going below 1. - Provides a tooltip for accessibility and user guidance.
Creating Cart Number Container
Next, you’ll add the container to display the quantity.
Locate the comment // TODO: Build Cart Number and replace it with the following:
// 1
Widget _buildCartNumberContainer(ColorScheme colorScheme) {
// 2
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
color: colorScheme.onPrimary,
// 3
child: Text(_cartNumber.toString()),
);
}
Here’s how the code works:
- The method takes a
ColorSchemeto style the widget. - It returns a container with spacing and alignment.
- Displays the cart number as a text.
Creating the Plus Button
The next step is to add the plus button.
Locate the comment // TODO: Build Plus Button and replace it with the following:
Widget _buildPlusButton() {
return IconButton(
icon: const Icon(Icons.add),
onPressed: () {
setState(() {
_cartNumber++;
});
},
tooltip: 'Increase Cart Count',
);
}
Similar to the minus button you increment the _cartNumber variable.
Creating the Add to Cart Button
The final component you’ll add is the Add to Cart button.
Replace // TODO: Build Add Cart Button with:
Widget _buildAddCartButton() {
// 1
return FilledButton(
// 2
onPressed: () {
widget.addToCart(_cartNumber);
},
// 3
child: const Text('Add to Cart'),
);
}
Here’s how the code works:
- Initialize a
FilledButtonwhich is a button that fills the button’s background. - When the user presses the button, trigger the
addToCart()callback and pass the number of items the user selected. - The button displays the text Add to Cart.
Showing the Cart Control Components
Now that you’ve built all the components, it’s time to put them to use.
Replace // TODO: Add Cart Control Components and the Container beneath it with the following:
_buildMinusButton(),
_buildCartNumberContainer(colorScheme),
_buildPlusButton(),
const Spacer(),
_buildAddCartButton(),
The Spacer widget is used to create space between the surrounding widgets.
Using the Cart Control
You’ll now add the cart control to the item details view.
Go back to item_details.dart, find // TODO: Create Cart Control and replace it with:
// 1
Widget _addToCartControl(Item item) {
// 2
return CartControl(
// 3
addToCart: (number) {
const uuid = Uuid();
final uniqueId = uuid.v4();
final cartItem = CartItem(
id: uniqueId,
name: item.name,
price: item.price,
quantity: number,
);
// 4
setState(() {
widget.cartManager.addItem(cartItem);
// 5
widget.quantityUpdated();
});
// 6
Navigator.pop(context);
},
);
}
Here’s how it works:
-
_addToCartControl()takes in the selectedItemobject. - It returns a
CartControlwidget. - The
addToCart()callback function will return the item quantity and create a newCartItem. ACartItemrequires a uniquely generated id, item name, price and the quantity selected. - Update the state by adding the new cart item managed by
CartManager. - Invoke the callback to notify the parent widget that the quantity has been updated.
- Close the bottom sheet.
Note: Ensure that
setState()is the most appropriate way to manage state in this context. If your app scales, you might need a more robust state management solution. For more advanced state management techniques check out Chapter 13, “Managing State”.
Add the following imports:
import 'package:uuid/uuid.dart';
import 'cart_control.dart';
If you’re wondering why the cart control isn’t displaying, stop worrying you’re adding it next.
Applying the Cart Control
Locate the comment // TODO: Add Cart Control and replace it with the following:
_addToCartControl(widget.item),
After hot reload runs, your bottom sheet should look like this:
Now the user can add items to their cart!
You still need to create a way to manage the cart and allow users to submit the order. Don’t worry, that’s next!
Building the Checkout Page
In this next section, you’ll learn about how to create drawers and leverage input widgets to capture data.
The user will be able to do the following:
- Select whether the order is delivered or picked up
- Name of the recipient
- Select date and time
- Manage the cart items
- Submit their order
Adding a Drawer
Drawers are commonly used for secondary navigation options.
In restaurant_page.dart, locate // TODO: Define Drawer Max Width and replace it with:
static const double drawerWidth = 375.0;
Here you define a constant variable to determine the max width of the drawer.
Replace // TODO: Create Drawer with the following:
Widget _buildEndDrawer() {
return SizedBox(
width: drawerWidth,
// TODO: Replace with Checkout Page
child: Container(color: Colors.red),
);
}
The _buildEndDrawer() function creates a simple drawer with a specific width and a placeholder red container.
Next, to apply the drawer locate the comment: // TODO: Apply Drawer and replace it with the following:
endDrawer: _buildEndDrawer(),
The scaffold widget is a top-level widget used in Flutter to implement the basic visual layout structure of an app. It includes the endDrawer property to define a drawer that slides in from the right.
Now you need a way to open such a drawer.
Adding a Floating Action Button
You’ll use a floating action button when clicked on to present the drawer.
Opening the Drawer
Locate the comment // TODO: Define Scaffold Key and replace it with the following:
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
Having a GlobalKey for your Scaffold allows you to control the scaffold from anywhere in your code. This is particularly useful for opening drawers, snack bars, or any other action that requires a reference to the ScaffoldState.
Next, locate // TODO: Add Scaffold Key and replace it with:
key: scaffoldKey,
Find and replace // TODO: Open Drawer with the following function:
void openDrawer() {
scaffoldKey.currentState!.openEndDrawer();
}
When openDrawer() is invoked, it will try to access the scaffold’s current state and open the drawer. The ! operator asserts that the current state is not null.
Add a Floating Action Button
Now you need to create the floating action button to open the drawer.
Locate the comment // TODO: Create Floating Action Button and replace it with the following:
// 1
Widget _buildFloatingActionButton() {
// 2
return FloatingActionButton.extended(
// 3
onPressed: openDrawer,
// 4
tooltip: 'Cart',
// 5
icon: const Icon(Icons.shopping_cart),
// 6
label: Text('${widget.cartManager.items.length} Items in cart'),
);
}
Here’s how the code works:
- The function returns a
FloatingActionButtonwidget. - Instantiate a
FloatingActionButton.extended, which allows the button to have both an icon and a label. - When the button is pressed,
openDrawer()is invoked. - Show a tooltip for accessibility.
- Set the button’s icon.
- The label displays the number of items in the cart.
Find and replace // TODO: Apply Floating Action Button with this code:
floatingActionButton: _buildFloatingActionButton(),
Here you set the floating action button within the scaffold widget.
Perform a hot reload. Click a restaurant and press the floating cart button, you’ll see the red drawer shown below:
Now you are ready to start to build your checkout page!
Creating the Checkout Page
Within the lib/screens directory, create a new file called checkout_page.dart and add the following code:
// 1
import 'package:flutter/material.dart';
import '../models/cart_manager.dart';
import '../models/order_manager.dart';
class CheckoutPage extends StatefulWidget {
// 2
final CartManager cartManager;
// 3
final Function() didUpdate;
// 4
final Function(Order) onSubmit;
const CheckoutPage(
{super.key,
required this.cartManager,
required this.didUpdate,
required this.onSubmit,
});
@override
State<CheckoutPage> createState() => _CheckoutPageState();
}
class _CheckoutPageState extends State<CheckoutPage> {
// 5
// TODO: Add State Properties
// TODO: Configure Date Format
// TODO: Configure Time of Day
// TODO: Set Selected Segment
// TODO: Build Segmented Control
// TODO: Build Name Textfield
// TODO: Select Date Picker
// TODO: Select Time Picker
// TODO: Build Order Summary
// TODO: Build Submit Order Button
@override
Widget build(BuildContext context) {
// 6
final textTheme = Theme.of(context)
.textTheme
.apply(displayColor: Theme.of(context).colorScheme.onSurface);
// 7
return Scaffold(
// 8
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.of(context).pop(),
),
),
// 9
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Order Details',
style: textTheme.headlineSmall,
),
// TODO: Add Segmented Control
// TODO: Add Name Textfield
// TODO: Add Date and Time Picker
// TODO: Add Order Summary
// TODO: Add Submit Order Button
],
),
),
);
}
}
Here’s how the checkout page is initialized:
- Import necessary
materiallibrary and manager models. -
CheckoutPageis a stateful widget that requiresCartManagerto manage and update cart items. - Declare a
didUpdate()callback to notify when something changes in the cart. - Create an
onSubmit()callback to notify that the user tapped on the submit button. - Spread some TODO comments to add all the interactive widgets to your checkout page.
- Retrieve the current text theme and apply the color theme to be consistent throughout the app.
- Create a
Scaffoldwidget that sets the app bar and body. -
AppBardisplays a back button that dismisses the drawer when clicked. - The
bodysets up padding and uses aColumnwidget to layout child widgets vertically.
Using the Checkout Page
Back in restaurant_page.dart locate the comment // TODO: Replace with Checkout Page and replace it and the child beneath it with the following:
// 1
child: Drawer(
// 2
child: CheckoutPage(
// 3
cartManager: widget.cartManager,
// 4
didUpdate: () {
setState(() {});
},
// 5
onSubmit: (order) {
widget.ordersManager.addOrder(order);
Navigator.popUntil(context, (route) => route.isFirst);
},
),
),
Here’s how the code works:
- Initialize the
Drawerwidget that slides from the side of the screen. - Use
CheckoutPageas the primary content of the drawer. - Pass in
cartManagerto manage and display cart items. - Configure the
didUpdate()callback to refresh the state of the parent widget. - Set
onSubmit()so that, when the user taps on the submit button, it adds a new order and closes the drawer.
Add the following import:
import 'checkout_page.dart';
Open and close the drawer, you should now see the following:
Now you’re ready to add all the input widgets!
Adding Checkout State Properties
Back in checkout_page.dart, locate // TODO: Add State Properties and replace it with the following:
// 1
final Map<int, Widget> myTabs = const <int, Widget>{
0: Text('Delivery'),
1: Text('Self Pick-Up'),
};
// 2
Set<int> selectedSegment = {0};
// 3
TimeOfDay? selectedTime;
// 4
DateTime? selectedDate;
// 5
final DateTime _firstDate = DateTime(DateTime.now().year - 2);
final DateTime _lastDate = DateTime(DateTime.now().year + 1);
// 6
final TextEditingController _nameController = TextEditingController();
Here is what each property is used for:
- Declare a mapping from integer to delivery type.
- Determines whether the user selected Delivery or Self Pick-Up.
-
selectedTimestores the selected time. -
selectedDatestores the selected date. -
_firstDateand_lastDatedetermines the date range the user can select from. -
_nameControllerrefers to the text field used to enter the customer’s name.
Adding a Segmented Control
The first widget you will build is a segmented control. This is a way for users to toggle between food delivery or pick-up.
Replace the comment // TODO: Set Selected Segment with:
void onSegmentSelected(Set<int> segmentIndex) {
setState(() {
selectedSegment = segmentIndex;
});
}
This function updates the user’s order type.
Next locate // TODO: Build Segmented Control and replace it with the following:
Widget _buildOrderSegmentedType() {
// 1
return SegmentedButton(
// 2
showSelectedIcon: false,
// 3
segments: const [
ButtonSegment(
value: 0,
label: Text('Delivery'),
icon: Icon(Icons.pedal_bike),
),
ButtonSegment(
value: 1,
label: Text('Pickup'),
icon: Icon(Icons.local_mall),
),
],
// 4
selected: selectedSegment,
// 5
onSelectionChanged: onSegmentSelected,
);
}
Here’s how the code works:
- Returns a
SegmentedButtonwidget. - Hide the icons in the segmented button.
- Define two button segments for the user to choose. Delivery or Pickup
- Set the selected segment.
- When a user makes a choice update the selected segment.
Find // TODO: Add Segmented Control and replace it with:
const SizedBox(height: 16.0),
_buildOrderSegmentedType(),
You should now see the segmented control in the drawer:
Adding a Textfield to Enter the Customer Name
You’ll now need a way to gather the customer’s name. This will help the restaurant or the delivery team to know how to address the recipient.
Replace // TODO: Build Name Textfield with:
Widget _buildTextField() {
// 1
return TextField(
// 2
controller: _nameController,
// 3
decoration: const InputDecoration(
labelText: 'Contact Name',
),
);
}
Here’s how the code works:
- The function returns a
TextFieldwidget. -
Textfielduses the controller to manage the text being edited. It allows you to read the current value of the text field, update it, or listen for changes. - Add a placeholder text, to give the user some context about what to type.
Next, to apply the text field, locate the comment // TODO: Add Name Textfield and replace it with the following:
const SizedBox(height: 16.0),
_buildTextField(),
You’ll now see the text field in the drawer:
Onwards with date and time!
Creating a Date Picker
Now you’ll need a way for the user to select the date to pick up or have the food delivered.
Locate the comment // TODO: Configure Date Format and replace it with the following:
// 1
String formatDate(DateTime? dateTime) {
// 2
if (dateTime == null) {
return 'Select Date';
}
// 3
final formatter = DateFormat('yyyy-MM-dd');
return formatter.format(dateTime);
}
This function determines what text the date button should read. Here’s how the code works:
- The function takes an optional
DateTimeas a parameter. - If the
dateTimeis null, return the text Select Date, to ask the user to select a date. - If a
dateTimeexists, return the formatted date.
Add the following import:
import 'package:intl/intl.dart';
Here you’ve added the intl package, which provides internationalization helpers needed by DateFormat.
Next locate the comment // TODO: Select Date Picker and replace it with the following:
// 1
void _selectDate(BuildContext context) async {
// 2
final picked = await showDatePicker(
// 3
context: context,
// 4
initialDate: selectedDate ?? DateTime.now(),
// 5
firstDate: _firstDate,
lastDate: _lastDate,
);
// 6
if (picked != null && picked != selectedDate) {
setState(() {
selectedDate = picked;
});
}
}
Here’s how the date picker works:
-
_selectDate()is an asynchronous function that takesBuildContextas a parameter. -
showDatePicker()opens the date picker dialog. The function waits for the user to pick or cancel the date picker and stores it in thepickedproperty. - You pass in the context to display the dialog.
-
initialDatesets the selected date or defaults to the current date. - Define the date range the user can pick from.
- If the picked date is not null and is different from the currently selected date, update the
selectedDateand trigger a rebuild of the widget to reflect the new selection.
Next you’ll also need a way to select the time.
Creating a Time Picker
Here’s how your time picker will look like.
Find // TODO: Configure Time of Day and replace it with the following:
// 1
String formatTimeOfDay(TimeOfDay? timeOfDay) {
// 2
if (timeOfDay == null) {
return 'Select Time';
}
// 3
final hour = timeOfDay.hour.toString().padLeft(2, '0');
final minute = timeOfDay.minute.toString().padLeft(2, '0');
return '$hour:$minute';
}
- This function takes in
TimeOfDayas a parameter. - If the
timeOfDayis null, return Select Time to indicate to the user to select a time. - Otherwise, return the formatted time.
Next, locate // TODO: Select Time Picker and replace it with this code:
// 1
void _selectTime(BuildContext context) async {
// 2
final picked = await showTimePicker(
// 3
context: context,
// 4
initialEntryMode: TimePickerEntryMode.input,
// 5
initialTime: selectedTime ?? TimeOfDay.now(),
// 6
builder: (context, child) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(
alwaysUse24HourFormat: true,
),
child: child!,
);
},
);
// 7
if (picked != null && picked != selectedTime) {
setState(() {
selectedTime = picked;
});
}
}
Here’s how the time picker works:
-
_selectTime()is an asynchronous function that takesBuildContextas a parameter. -
showTimePicker()opens the time picker dialog. The function waits for the user to pick or cancel the time picker and stores it in thepickedproperty. - You still pass in the context to display the dialog.
-
initialEntryModesets the mode to enter the time.inputmode allows the user to enter values via the keyboard. - Set the
initialTimeto theselectedTime, if null, default to the current time. - The
builder()function builds the time picker.MediaQueryforces it to always show the 24-hour time format, regardless of the device’s default setting. - If the picked time is not null and is different from the currently selected time, update the
selectedTimeand trigger a rebuild of the widget to reflect the new selection.
Now that you have all your widgets ready, it’s time to show them in the drawer.
Showing the Date and Time Pickers
Replace // TODO: Add Date and Time Picker with:
// 1
const SizedBox(height: 16.0),
// 2
Row(
children: [
TextButton(
// 3
child: Text(formatDate(selectedDate)),
// 4
onPressed: () => _selectDate(context),
),
TextButton(
// 5
child: Text(formatTimeOfDay(selectedTime)),
// 6
onPressed: () => _selectTime(context),
),
],
),
// 7
const SizedBox(height: 16.0),
Here’s how the code works:
- Add a 16.0 vertical space from the widget on top.
- Use a
Rowto display the two buttons horizontally. - The first text button displays Select Date or the currently selected date.
- Tapping the button presents the date picker.
- The second text button displays Select Time or the currently selected time.
- Tapping the button presents the time picker.
- Add 16.0 vertical spacing between the widget below.
Now, perform a hot reload.
Select a restaurant and tap the Items in cart button.
You should see Select Date and Select Time buttons. Tap each of them to open the pickers and select a date and a time.
But wait, where’s the order? Don’t worry, you’ll do that next.
Creating Order Summary
Now you’ll create a way to display the list of items the user selected.
Locate the comment // TODO: Build Order Summary and replace it with the following:
// 1
Widget _buildOrderSummary(BuildContext context) {
// 2
final colorTheme = Theme.of(context).colorScheme;
// 3
return Expanded(
// 4
child: ListView.builder(
// 5
itemCount: widget.cartManager.items.length,
itemBuilder: (context, index) {
// 6
final item = widget.cartManager.itemAt(index);
// 7
// TODO: Wrap in a Dismissible Widget
return ListTile(
leading: Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(
Radius.circular(8.0)),
border: Border.all(
color: colorTheme.primary,
width: 2.0,
),
),
child: ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(8.0)),
child: Text('x${item.quantity}'),
),
),
title: Text(item.name),
subtitle: Text('Price: \$${item.price}'),
);
},
),
);
}
Here’s how the code works:
-
_buildOrderSummary()takesBuildContextas a parameter. - Retrieve the color theme for consistency throughout your app.
- Return an
Expandedwidget that allowsListViewto use all available space in its parent widget. - Use
ListView.builder()to create a scrollable list of items. - Set item count.
- Build each item by retrieving the menu item for a given index.
- Construct a
ListTileto display the menu item selected, display the quantity and the total price for each item.
To add order summary and a title, replace // TODO: Add Order Summary with:
const Text('Order Summary'),
_buildOrderSummary(context),
Hot reload and you’ll see the order summary.
But what if the user wants to remove an item from the order? You’ll add that next.
Deleting an Item From an Order
The user will swipe left to remove a menu item.
Find // TODO: Wrap in a Dismissible Widget. Right-click ListTile widget on the line below and select Show Context Actions as shown below:
Next, select Wrap with widget as shown below:
Rename widget to Dismissible and add the following properties to the widget just above the child:
// 1
key: Key(item.id),
// 2
direction: DismissDirection.endToStart,
// 3
background: Container(),
// 4
secondaryBackground: const SizedBox(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Icon(Icons.delete),
],
),
),
// 5
onDismissed: (direction) {
setState(() {
widget.cartManager.removeItem(item.id);
});
// 6
widget.didUpdate();
},
Here’s how the code works:
-
Keyis used to uniquely identify each dismissible item in the list. - Configure the dismiss
directionswiping from right to left. - Set an empty background container.
- Set a secondary background and show a delete (trash) icon aligned to the right end.
- When
onDismissed()is triggered, callsetState()to remove the item from the cart. - Invoke
didUpdate()to notify the parent to refresh the UI or perform other actions.
You have added all the interactive widgets to collect an order.
Now you need a submit button to process the order!
Locate the comment // TODO: Build Submit Order Button and replace it with the following:
Widget _buildSubmitButton() {
// 1
return ElevatedButton(
// 2
onPressed: widget.cartManager.isEmpty
? null
// 3
: () {
final selectedSegment = this.selectedSegment;
final selectedTime = this.selectedTime;
final selectedDate = this.selectedDate;
final name = _nameController.text;
final items = widget.cartManager.items;
// 4
final order = Order(
selectedSegment: selectedSegment,
selectedTime: selectedTime,
selectedDate: selectedDate,
name: name,
items: items,
);
// 5
widget.cartManager.resetCart();
// 6
widget.onSubmit(order);
},
child: Padding(
padding: const EdgeInsets.all(16.0),
// 7
child: Text(
'''Submit Order - \$${widget.cartManager.totalCost.toStringAsFixed(2)}'''),
),
);
}
Here’s how the code works:
- The function returns an
ElevatedButtonwidget. - When the cart is empty,
onPressed()disables the button by setting it tonull. - If the cart is not empty,
onPressed()retrieves all the user data such as selected order type, time, date, name and list of items. - Create an order object.
- Reset the cart.
- Submit the order.
- Show the total cost of the order.
To apply the button, locate // TODO: Add Submit Order Button and replace it with the following:
_buildSubmitButton(),
Perform a hot reload if needed and try to add items to your cart. You’ll see the Submit Order button enabled or disabled based on the number of items in the cart.
Now that you created a way to capture the order data, why not add a page to display the list of orders submitted? That’s up next.
Building the Orders Page
When someone places an order they likely want to see the list of orders they’ve placed. When you’re done with this section the Orders tab will look like this:
Within the lib/screens directory, create a new file called myorders_page.dart and add the following code:
import 'package:flutter/material.dart';
import '../models/order_manager.dart';
class MyOrdersPage extends StatelessWidget {
final OrderManager orderManager;
// 1
const MyOrdersPage({
super.key,
required this.orderManager,
});
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context)
.textTheme
.apply(displayColor: Theme.of(context).colorScheme.onSurface);
// 2
return Scaffold(
appBar: AppBar(
centerTitle: false,
title: Text('My Orders', style: textTheme.headlineMedium),
),
// 3
body: ListView.builder(
// 4
itemCount: orderManager.totalOrders,
itemBuilder: (context, index) {
// 5
return OrderTile(order: orderManager.orders[index]);
},
),
);
}
}
// 6
class OrderTile extends StatelessWidget {
final Order order;
const OrderTile({super.key, required this.order});
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context)
.textTheme
.apply(displayColor: Theme.of(context).colorScheme.onSurface);
// 7
return ListTile(
leading: ClipRRect(
borderRadius: BorderRadius.circular(8.0),
// 8
child: Image.asset(
'assets/food/burger.webp',
width: 50.0,
height: 50.0,
fit: BoxFit.cover,
),
),
// 9
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 10
Text(
'Scheduled',
style: textTheme.bodyLarge,
),
// 11
Text(order.getFormattedOrderInfo()),
// 12
Text('Items: ${order.items.length}'),
],
),
);
}
}
The MyOrdersPage widget is pretty standard:
- It takes an
orderManageras a parameter. This is used to retrieve the list of orders. - It defines a
Scaffoldthat has anAppBar - Displays a
ListViewin the body. - Sets the list view count.
- For each order, it creates an
OrderTilewidget and passes the current order’s index. - It defines an
OrderTilewidget to display the order. - A tile wraps a
ListTile. - The
ListTileleading widget is an image with rounded corners. - The title displays a
Columnto align the order details vertically.
Next you’ll add the MyOrdersPage to your Orders tab.
Showing the Orders Page
Open home.dart and locate // TODO: Replace with Order Page and replace it and the Center code beneath it with the following:
MyOrdersPage(orderManager: widget.ordersManager),
Add the following import:
import 'screens/myorders_page.dart';
Now add items to the cart and submit the order.
Tap on the Orders tab, you should see the list of orders submitted!
Your app now lets your users look at menus and order items for either delivery or pick up. Congratulations!
Key Points
- You can pass data around with callbacks
- You can use callbacks also to pass data one level up.
- Manager objects help you manage functions and state changes in one place.
-
TextEditingControlleris used to listen for changes in aTextFieldwidget. - Split your widgets by screen to keep your code modular and organized.
- Gesture widgets recognize and determine the type of touch event. They provide callbacks to react to events like
onTap()oronDrag(). - You can use dismissible widgets to swipe away items in a list.
Where to Go From Here?
There are many ways to engage and collect data from your users. You’ve learned to pass data around using callbacks. You learned to create different input widgets. You also learned to apply touch events to navigate to parts of your app.
That’s a lot, but you’ve only scratched the surface! There’s a plethora of widgets out there. You can explore other packages at https://pub.dev, a place where you can find the most popular widgets created by the Flutter community!
In the next section, you’ll dive into navigation.