Chapters

Hide chapters

Flutter Apprentice

Fourth Edition · Flutter 3.16.9 · Dart 3.2.6 · Android Studio 2023.1.1

Section II: Everything’s a Widget

Section 2: 5 chapters
Show chapters Hide chapters

Section IV: Networking, Persistence & State

Section 4: 6 chapters
Show chapters Hide chapters

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:

  1. Define a function _showBottomSheet() that accepts the selected menu item to display in the bottom sheet.
  2. When invoked, create a modal bottom sheet that slides up from the bottom.
  3. isScrollControlled is true, to allow the bottom sheet to have dynamic height.
  4. Pass in the current context to display the bottom sheet.
  5. Constraint the bottom sheet to have a max width of 480. This is to support responsive UI on mobile or desktop.
  6. 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:

  1. The ItemDetails widget takes in the selected item and a cart manager to manage cart operations. quantityUpdated is a callback that notifies the parent widget that the user updated the quantity.
  2. Retrieve the textTheme and ensure the text color matches the surface color of the color scheme.
  3. Retrieve the colorTheme, this ensures the app has a consistent color theme across all widgets in your app.
  4. Add uniform padding of 16.0 all around.
  5. The Wrap widget organizes children in horizontal or vertical runs, adjusting the layout based on space.
  6. Column widget 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:

  1. Define a method _mostLikedBadge(), which takes in a ColorScheme. This method will create a badge to indicate whether an item is most liked.
  2. The Align widget is used to align the badge within the parent widget.
  3. Align the widget center-left.
  4. A Container is used to apply padding and color.
  5. A Text widget 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:

  1. _itemImage() takes in an imageUrl.
  2. Apply a container to style the image, adding a fixed height and rounded corners.
  3. 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:

  1. Define a stateful widget called CartControl.
  2. Define an addToCart() callback function, which returns an integer to specify the number of items in the cart.
  3. Link this widget to its state _CartControlState().
  4. Define the CartControl state class.
  5. _cartNumber is a private state variable used to keep track of the quantity of items to be added to the cart. The default value is 1.
  6. Within the build() method, retrieve the color scheme for consistency.
  7. Return a Row widget to layout children horizontally.
  8. Use MainAxisAlignment.spaceBetween to space the children evenly.
  9. 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:

  1. Create a button to decrease the number of items in the cart.
  2. Initialize an IconButton with the remove symbol that renders like a minus sign.
  3. Configure the onPressed() callback to trigger a setState() to update the UI
  4. Decrements _cartNumber by 1 if it’s greater than 1 preventing it from going below 1.
  5. 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:

  1. The method takes a ColorScheme to style the widget.
  2. It returns a container with spacing and alignment.
  3. 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:

  1. Initialize a FilledButton which is a button that fills the button’s background.
  2. When the user presses the button, trigger the addToCart() callback and pass the number of items the user selected.
  3. 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:

  1. _addToCartControl() takes in the selected Item object.
  2. It returns a CartControl widget.
  3. The addToCart() callback function will return the item quantity and create a new CartItem. A CartItem requires a uniquely generated id, item name, price and the quantity selected.
  4. Update the state by adding the new cart item managed by CartManager.
  5. Invoke the callback to notify the parent widget that the quantity has been updated.
  6. 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:

  1. The function returns a FloatingActionButton widget.
  2. Instantiate a FloatingActionButton.extended, which allows the button to have both an icon and a label.
  3. When the button is pressed, openDrawer() is invoked.
  4. Show a tooltip for accessibility.
  5. Set the button’s icon.
  6. 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:

  1. Import necessary material library and manager models.
  2. CheckoutPage is a stateful widget that requires CartManager to manage and update cart items.
  3. Declare a didUpdate() callback to notify when something changes in the cart.
  4. Create an onSubmit() callback to notify that the user tapped on the submit button.
  5. Spread some TODO comments to add all the interactive widgets to your checkout page.
  6. Retrieve the current text theme and apply the color theme to be consistent throughout the app.
  7. Create a Scaffold widget that sets the app bar and body.
  8. AppBar displays a back button that dismisses the drawer when clicked.
  9. The body sets up padding and uses a Column widget 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:

  1. Initialize the Drawer widget that slides from the side of the screen.
  2. Use CheckoutPage as the primary content of the drawer.
  3. Pass in cartManager to manage and display cart items.
  4. Configure the didUpdate() callback to refresh the state of the parent widget.
  5. 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:

  1. Declare a mapping from integer to delivery type.
  2. Determines whether the user selected Delivery or Self Pick-Up.
  3. selectedTime stores the selected time.
  4. selectedDate stores the selected date.
  5. _firstDate and _lastDate determines the date range the user can select from.
  6. _nameController refers 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:

  1. Returns a SegmentedButton widget.
  2. Hide the icons in the segmented button.
  3. Define two button segments for the user to choose. Delivery or Pickup
  4. Set the selected segment.
  5. 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:

  1. The function returns a TextField widget.
  2. Textfield uses 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.
  3. 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:

  1. The function takes an optional DateTime as a parameter.
  2. If the dateTime is null, return the text Select Date, to ask the user to select a date.
  3. If a dateTime exists, 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:

  1. _selectDate() is an asynchronous function that takes BuildContext as a parameter.
  2. showDatePicker() opens the date picker dialog. The function waits for the user to pick or cancel the date picker and stores it in the picked property.
  3. You pass in the context to display the dialog.
  4. initialDate sets the selected date or defaults to the current date.
  5. Define the date range the user can pick from.
  6. If the picked date is not null and is different from the currently selected date, update the selectedDate and 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';
}
  1. This function takes in TimeOfDay as a parameter.
  2. If the timeOfDay is null, return Select Time to indicate to the user to select a time.
  3. 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:

  1. _selectTime() is an asynchronous function that takes BuildContext as a parameter.
  2. showTimePicker() opens the time picker dialog. The function waits for the user to pick or cancel the time picker and stores it in the picked property.
  3. You still pass in the context to display the dialog.
  4. initialEntryMode sets the mode to enter the time. input mode allows the user to enter values via the keyboard.
  5. Set the initialTime to the selectedTime, if null, default to the current time.
  6. The builder() function builds the time picker. MediaQuery forces it to always show the 24-hour time format, regardless of the device’s default setting.
  7. If the picked time is not null and is different from the currently selected time, update the selectedTime and 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:

  1. Add a 16.0 vertical space from the widget on top.
  2. Use a Row to display the two buttons horizontally.
  3. The first text button displays Select Date or the currently selected date.
  4. Tapping the button presents the date picker.
  5. The second text button displays Select Time or the currently selected time.
  6. Tapping the button presents the time picker.
  7. 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:

  1. _buildOrderSummary() takes BuildContext as a parameter.
  2. Retrieve the color theme for consistency throughout your app.
  3. Return an Expanded widget that allows ListView to use all available space in its parent widget.
  4. Use ListView.builder() to create a scrollable list of items.
  5. Set item count.
  6. Build each item by retrieving the menu item for a given index.
  7. Construct a ListTile to 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:

  1. Key is used to uniquely identify each dismissible item in the list.
  2. Configure the dismiss direction swiping from right to left.
  3. Set an empty background container.
  4. Set a secondary background and show a delete (trash) icon aligned to the right end.
  5. When onDismissed() is triggered, call setState() to remove the item from the cart.
  6. 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:

  1. The function returns an ElevatedButton widget.
  2. When the cart is empty, onPressed() disables the button by setting it to null.
  3. If the cart is not empty, onPressed() retrieves all the user data such as selected order type, time, date, name and list of items.
  4. Create an order object.
  5. Reset the cart.
  6. Submit the order.
  7. 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:

  1. It takes an orderManager as a parameter. This is used to retrieve the list of orders.
  2. It defines a Scaffold that has an AppBar
  3. Displays a ListView in the body.
  4. Sets the list view count.
  5. For each order, it creates an OrderTile widget and passes the current order’s index.
  6. It defines an OrderTile widget to display the order.
  7. A tile wraps a ListTile.
  8. The ListTile leading widget is an image with rounded corners.
  9. The title displays a Column to 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.
  • TextEditingController is used to listen for changes in a TextField widget.
  • 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() or onDrag().
  • 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.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.