Chapters

Hide chapters

Flutter Apprentice

First Edition – Early Access 3 · Flutter 1.22.6 · Dart 2.10.5 · Android Studio 4.1.2

Section III: Navigating Between Screens

Section 3: 3 chapters
Show chapters Hide chapters

Section IV: Networking, Persistence and State

Section 4: 7 chapters
Show chapters Hide chapters

Appendices

Section 6: 2 chapters
Show chapters Hide chapters

6. Interactive Widgets
Written by Vincent Ngo

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

In the last chapter, you learned how to capture lots of data with scrollable widgets. But how do you make an 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:

  • Gesture-based widgets
  • Time and date picker widgets
  • Input and selection widgets
  • Dismissable widgets

You’ll continue to work on Fooderlich, building the final tab: To Buy. This tab allows the user to create a grocery list of items to buy, make modifications to them, and check them off their TODO list when they’re done. They’ll be able to add, remove and update the items in the list.

You’ll also get a quick introduction to Provider, a package that helps you manage state and notify components that there’s updated data to display.

You’ll start by building an empty screen. If there are no grocery items available, the user has two options:

  1. Click Browse Recipes to view other recipes.
  2. Click the Plus (+) button to add a new grocery item.

When the user taps the Plus (+) button, the app will present a screen for the user to create an item:

The screen consists of the following data attributes:

  • The name of the item
  • A tag that shows the item’s importance level
  • The date and time when you want to buy this item
  • The color you want to label this item
  • The quantity of the item

Also, when you create the item, the app will show a preview of the item itself! How cool is that?

When you create your first item, the grocery list replaces the empty screen:

The user will be able to take four actions on this new screen:

  1. Tap a grocery item to update some information.
  2. Tap the checkbox to mark an item as complete.
  3. Swipe away the item to delete it.
  4. Create and add another item to the list.

By the end of this chapter, you’ll have built a functional TODO list for users to manage their grocery items. You’ll even add light and dark mode support!

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 Fooderlich app from the previous chapter. When you tap the To Buy tab, you’ll see a blue screen. Don’t worry, soon you’ll add an image so your users won’t think there’s a problem.

Inside assets/fooderlich_assets, you’ll find a new image.

You’ll display empty_list.png when there aren’t any items in the list.

Now, it’s time to add some code!

Creating the grocery item model

First, you’ll set up the model for the information you want to save about the items. In the models directory, create a new file called grocery_item.dart, then add the following code:

import 'package:flutter/painting.dart';

// 1
enum Importance { low, medium, high }

class GroceryItem {
  // 2
  final String id;
  // 3
  final String name;
  final Importance importance;
  final Color color;
  final int quantity;
  final DateTime date;
  final bool isComplete;

  GroceryItem(
      {this.id,
      this.name,
      this.importance,
      this.color,
      this.quantity,
      this.date,
      this.isComplete = false});

  // 4
  GroceryItem copyWith(
      {String id,
      String name,
      Importance importance,
      Color color,
      int quantity,
      DateTime date,
      bool isComplete}) {
    return GroceryItem(
        id: id ?? this.id,
        name: name ?? this.name,
        importance: importance ?? this.importance,
        color: color ?? this.color,
        quantity: quantity ?? this.quantity,
        date: date ?? this.date,
        isComplete: isComplete ?? this.isComplete);
  }
}
export 'grocery_item.dart';

Creating the grocery screen

Now that you’ve set up your model, it’s time to create the grocery screen. This screen will display one of two screens:

Creating the shell

The first thing you need to do is create GroceryScreen, which determines whether to display the empty or list screen.

import 'package:flutter/material.dart';

class GroceryScreen extends StatelessWidget {
  const GroceryScreen({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // TODO 2: Replace with EmptyGroceryScreen
    return Container(color: Colors.green);
  }
}

Displaying the grocery screen

Your next task is to give your users a way to see the new page — when it’s ready, that is. When you click on the To Buy tab, it needs to show GroceryScreen, not the solid color.

import 'screens/grocery_screen.dart';
const GroceryScreen()

Creating the empty grocery screen

Within the screens directory, create a new Dart file called empty_grocery_screen.dart and add the following:

import 'package:flutter/material.dart';

class EmptyGroceryScreen extends StatelessWidget {
  const EmptyGroceryScreen({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // TODO 3: Replace and add layout widgets
    return Container(color: Colors.purple);
  }
}

Adding the empty screen

Before you continue building EmptyGroceryScreen, you need to set up the widget for hot reload so you can see your updates live.

import 'empty_grocery_screen.dart';
// TODO 4: Add a scaffold widget
return const EmptyGroceryScreen();

Adding layout widgets

Next, you’ll lay the foundation for the final look of the page by adding widgets that handle the layout of the empty grocery screen.

// 1
return Padding(
  padding: const EdgeInsets.all(30.0),
  // 2
  child: Center(
      // 3
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          // TODO 4: Add empty image
          // TODO 5: Add empty screen title
          // TODO 6: Add empty screen subtitle
          // TODO 7: Add browse recipes button
        ],),),
);

Adding the visual pieces

Finally, it’s time to go beyond a colorful screen and add some text and images.

AspectRatio(
  aspectRatio: 1 / 1,
  child: Image.asset('assets/fooderlich_assets/empty_list.png'),),
const SizedBox(height: 8.0),
const Text('No Groceries', style: TextStyle(fontSize: 21.0),),
const SizedBox(height: 16.0),
const Text(
  'Shopping for ingredients?\n'
  'Tap the + button to write them down!',
  textAlign: TextAlign.center,
),
MaterialButton(
  textColor: Colors.white,
  child: const Text('Browse Recipes'),
  shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(30.0),),
  color: Colors.green,
  onPressed: () {
    // TODO 8: Go to Recipes Tab
},),

Switching tabs

You have two options to implement switching to the Recipes tab:

Provider overview

Provider is a convenient way to pass state down the widget tree and rebuild your UI when changes occur. You’ll add it to your project next.

Adding Provider

Open pubspec.yaml and add the following package under dependencies:

provider: ^4.3.2+3

Creating the TabManager ChangeNotifier

In the models directory, create a new file called tab_manager.dart and add the following code:

import 'package:flutter/material.dart';

// 1
class TabManager extends ChangeNotifier {
  // 2
  int selectedTab = 0;

  // 3
  void goToTab(index) {
    // 4
    selectedTab = index;
    // 5
    notifyListeners();
  }

  // 6
  void goToRecipes() {
    selectedTab = 1;
    // 7
    notifyListeners();
  }
}
export 'tab_manager.dart';

Managing tab state

So how will you use Provider? Here’s a blueprint:

Providing TabManager

First, you need to provide TabManager at Fooderlich’s top level to let descending widgets access the state object.

import 'package:provider/provider.dart';
import 'models/models.dart';
// 1
home: MultiProvider(
  providers: [
    // 2
    ChangeNotifierProvider(create: (context) => TabManager(),),
    // TODO 10: Add GroceryManager Provider
  ],
  child: const Home(),),

Adding a TabManager consumer

Now, it’s time to set up the consumer so the app can listen to changes broadcast by TabManager.

import 'package:provider/provider.dart';
import 'models/models.dart';
// 1
return Consumer<TabManager>(builder: (context, tabManager, child) {
  return Scaffold(
      appBar: AppBar(
          title: Text('Fooderlich',
              style: Theme.of(context).textTheme.headline6),),
      // 2
      body: pages[tabManager.selectedTab],
      bottomNavigationBar: BottomNavigationBar(
          selectedItemColor: Theme.of(context).textSelectionColor,
          // 3
          currentIndex: tabManager.selectedTab,
          onTap: (index) {
            // 4
            tabManager.goToTab(index);
          },
          items: <BottomNavigationBarItem>[
            const BottomNavigationBarItem(
                icon: Icon(Icons.explore), label: 'Explore',),
            const BottomNavigationBarItem(
                icon: Icon(Icons.book), label: 'Recipes',),
            const BottomNavigationBarItem(
                icon: Icon(Icons.list), label: 'To Buy',),
          ],),);
},);

Switching to the recipes tab

There’s one last step to implement the switching of tabs.

import 'package:provider/provider.dart';
import '../models/models.dart';
Provider.of<TabManager>(context, listen: false).goToRecipes();

Managing the grocery items

Before you display or create grocery items, you need a way to manage them.

import 'package:flutter/material.dart';
import 'grocery_item.dart';

class GroceryManager extends ChangeNotifier {
  // 1
  final _groceryItems = <GroceryItem>[];

  // 2
  List<GroceryItem> get groceryItems => List.unmodifiable(_groceryItems);

  // 3
  void deleteItem(int index) {
    _groceryItems.removeAt(index);
    notifyListeners();
  }

  // 4
  void addItem(GroceryItem item) {
    _groceryItems.add(item);
    notifyListeners();
  }

  // 5
  void updateItem(GroceryItem item, int index) {
    _groceryItems[index] = item;
    notifyListeners();
  }

  // 6
  void completeItem(int index, bool change) {
    final item = _groceryItems[index];
    _groceryItems[index] = item.copyWith(isComplete: change);
    notifyListeners();
  }
}
export 'grocery_manager.dart';

Adding GroceryManager as a provider

Much like you did with TabManager you‘ll now add GroceryManager as a provider.

ChangeNotifierProvider(create: (context) => GroceryManager(),)

Consuming the changes

How does the To Buy screen react to changes in the grocery list? So far, it doesn’t, but you’re now ready to hook up the new manager with the view that displays grocery items. :]

import 'package:provider/provider.dart';
import '../models/models.dart';
Widget buildGroceryScreen() {
  // 1
  return Consumer<GroceryManager>(
    // 2
    builder: (context, manager, child) {
    // 3
    if (manager.groceryItems.isNotEmpty) {
      // TODO 25: Add GroceryListScreen
      return Container();
    } else {
      // 4
      return const EmptyGroceryScreen();
    }
  },);
}
// 5
return Scaffold(
  // 6
  floatingActionButton: FloatingActionButton(
      child: const Icon(Icons.add),
      onPressed: () {
        // TODO 11: Present GroceryItemScreen
      },),
  // 7
  body: buildGroceryScreen(),);

Adding new packages

Before going any further, you need to add three new packages. Open pubspec.yaml and add the following under dependencies:

flutter_colorpicker: ^0.3.4
intl: ^0.16.1
uuid: ^2.2.2

Creating GroceryItemScreen

In the screens directory, create a new file called grocery_item_screen.dart and add the following:

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
import 'package:intl/intl.dart';
import 'package:uuid/uuid.dart';
import '../models/models.dart';

class GroceryItemScreen extends StatefulWidget {
  // 1
  final Function(GroceryItem) onCreate;
  // 2
  final Function(GroceryItem) onUpdate;
  // 3
  final GroceryItem originalItem;
  // 4
  final bool isUpdating;

  const GroceryItemScreen(
      {Key key, this.onCreate, this.onUpdate, this.originalItem,})
      : isUpdating = (originalItem != null),
        super(key: key);

  @override
  _GroceryItemScreenState createState() => _GroceryItemScreenState();
}

class _GroceryItemScreenState extends State<GroceryItemScreen> {

  // TODO: Add grocery item screen state properties

  @override
  Widget build(BuildContext context) {
    // TODO 12: Add GroceryItemScreen Scaffold
    return Container(color: Colors.orange);
  }
}

Presenting GroceryItemScreen

Open grocery_screen.dart and add the following import:

import 'grocery_item_screen.dart';
// 1
final manager = Provider.of<GroceryManager>(
  context,
  listen: false);
// 2
Navigator.push(
  context,
  // 3
  MaterialPageRoute(
    // 4
    builder: (context) => GroceryItemScreen(
      // 5
      onCreate: (item) {
        // 6
        manager.addItem(item);
        // 7
        Navigator.pop(context);
      },
    ),),);

Adding GroceryItemScreen state properties

Now, it’s time to give the grocery items some properties to make them more useful.

final _nameController = TextEditingController();
String _name = '';
Importance _importance = Importance.low;
DateTime _dueDate = DateTime.now();
TimeOfDay _timeOfDay = TimeOfDay.now();
Color _currentColor = Colors.green;
int _currentSliderValue = 0;

Initializing GroceryItemScreen’s properties

Next, within _groceryitemScreenState, add the following code before build():

@override
void initState() {
  // 1
  if (widget.originalItem != null) {
    _nameController.text = widget.originalItem.name;
    _currentSliderValue = widget.originalItem.quantity;
    _importance = widget.originalItem.importance;
    _currentColor = widget.originalItem.color;
    final date = widget.originalItem.date;
    _timeOfDay = TimeOfDay(hour: date.hour, minute: date.minute);
    _dueDate = date;
  }

  // 2
  _nameController.addListener(() {
    setState(() {
      _name = _nameController.text;
    });
  });

  super.initState();
}
@override
void dispose() {
  _nameController.dispose();
  super.dispose();
}

Adding GroceryItemScreen layout widgets

Still in grocery_item_screen.dart, locate // TODO 12: Add GroceryItemScreen Scaffold and replace return Container(color: Colors.orange) with the following:

// 1
return Scaffold(
  // 2
  appBar: AppBar(
    actions: [
      IconButton(
          icon: const Icon(Icons.check),
          onPressed: () {
            // TODO 24: Add callback handler
          },)
    ],
    // 3
    elevation: 0.0,
    // 4
    title: Text('Grocery Item',
        style: GoogleFonts.lato(fontWeight: FontWeight.w600),),),
  // 5
  body: Container(
    padding: const EdgeInsets.all(16.0),
    child: ListView(
      children: [
      // TODO 13: Add name TextField
      // TODO 14: Add Importance selection
      // TODO 15: Add date picker
      // TODO 16: Add time picker
      // TODO 17: Add color picker
      // TODO 18: Add slider
      // TODO: 19: Add Grocery Tile
    ],),),
  );

Adding the text field to enter a grocery name

The first input widget you’ll create is TextField, which is a helpful widget when you need the user to enter some text. In this case, it will capture the name of the grocery item.

Widget buildNameField() {
  // 1
  return Column(
    // 2
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      // 3
      Text('Item Name', style: GoogleFonts.lato(fontSize: 28.0),),
      // 4
      TextField(
        // 5
        controller: _nameController,
        // 6
        cursorColor: _currentColor,
        // 7
        decoration: InputDecoration(
          // 8
          hintText: 'E.g. Apples, Banana, 1 Bag of salt',
          // 9
          enabledBorder: const UnderlineInputBorder(
              borderSide: BorderSide(color: Colors.white),),
          focusedBorder: UnderlineInputBorder(
              borderSide: BorderSide(color: _currentColor),),
          border: UnderlineInputBorder(
              borderSide: BorderSide(color: _currentColor),),
        ),),
  ],);
}
buildNameField(),

Building the importance widget

Your next step is to let the users choose how important a grocery item is. You’ll do this using a Chip. Chips represent information about an entity. You can present a collection of chips for the user to select.

Understanding chips

There are four different types of chip widgets:

Ebuwo jfek: ycwxb://voyexoow.iu/facgoxopbx/hfovj/nxulrun#zlzog

Widget buildImportanceField() {
  // 1
  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      // 2
      Text('Importance', style: GoogleFonts.lato(fontSize: 28.0),),
      // 3
      Wrap(
        spacing: 10.0,
        children: [
          // 4
          ChoiceChip(
            // 5
            selectedColor: Colors.black,
            // 6
            selected: _importance == Importance.low,
            label: const Text(
              'low',
              style: TextStyle(color: Colors.white),),
            // 7
            onSelected: (selected) {
              setState(() => _importance = Importance.low);
            },),
          ChoiceChip(
            selectedColor: Colors.black,
            selected: _importance == Importance.medium,
            label: const Text(
              'medium',
              style: TextStyle(color: Colors.white),),
            onSelected: (selected) {
              setState(() => _importance = Importance.medium);
            },),
          ChoiceChip(
            selectedColor: Colors.black,
            selected: _importance == Importance.high,
            label: const Text(
              'high',
              style: TextStyle(color: Colors.white),),
            onSelected: (selected) {
              setState(() => _importance = Importance.high);
            },),
        ],)
    ],);
}
buildImportanceField(),

Building the date widget

DatePicker is a useful widget when you need the user to enter a date. You’ll use it here to let the user set a deadline to buy their grocery item.

Widget buildDateField(BuildContext context) {
  // 1
  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      // 2
      Row(
        // 3
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          // 4
          Text('Date', style: GoogleFonts.lato(fontSize: 28.0),),
          // 5
          FlatButton(
            child: const Text('Select'),
            // 6
            onPressed: () async {
              final currentDate = DateTime.now();
              // 7
              final selectedDate = await showDatePicker(
                context: context,
                initialDate: currentDate,
                firstDate: currentDate,
                lastDate: DateTime(currentDate.year + 5),
              );
              // 8
              setState(() {
                if (selectedDate != null) {
                  _dueDate = selectedDate;
                }
              });
          },),
        ],),
      // 9
      if (_dueDate != null)
        Text('${DateFormat('yyyy-MM-dd').format(_dueDate)}'),
  ],);
}
buildDateField(context),

Building the time widget

Now that the user can set the date when they want to buy an item, you’ll also let them set the time. To do this, you’ll use TimePicker — a widget that’s useful when you need the user to enter the time.

Widget buildTimeField(BuildContext context) {
  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
        Text('Time of Day', style: GoogleFonts.lato(fontSize: 28.0),),
        FlatButton(
          child: const Text('Select'),
          onPressed: () async {
            // 1
            final timeOfDay = await showTimePicker(
              // 2
              initialTime: TimeOfDay.now(),
              context: context,
            );

            // 3
            setState(() {
              if (timeOfDay != null) {
                _timeOfDay = timeOfDay;
              }
            });
          },),
      ]),
    if (_timeOfDay != null)
      Text('${_timeOfDay.format(context)}'),
  ],);
}
buildTimeField(context),

Building the color picker widget

Now, you’re ready to let the user pick a color to tag the grocery items. For this, you’ll use a third-party widget, ColorPicker, which presents the user with a color palette.

Widget buildColorPicker(BuildContext context) {
  // 1
  return Row(
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    children: [
      // 2
      Row(
        children: [
          Container(height: 50.0, width: 10.0, color: _currentColor,),
          const SizedBox(width: 8.0),
          Text('Color', style: GoogleFonts.lato(fontSize: 28.0),),
        ],
      ),
      // 3
      FlatButton(
          child: const Text('Select'),
          onPressed: () {
            // 4
            showDialog(
              context: context,
              builder: (context) {
                // 5
                return AlertDialog(
                  content: BlockPicker(
                      pickerColor: Colors.white,
                      // 6
                      onColorChanged: (color) {
                        setState(() => _currentColor = color);
                      },),
                  actions: [
                    // 7
                    FlatButton(
                      child: const Text('Save'),
                      onPressed: () {
                        Navigator.of(context).pop();
                      },
                    ),
                  ],
                );
            },);
          })
  ],);
}
const SizedBox(height: 10.0),
buildColorPicker(context),

Building a quantity widget

For your next step, you’ll let the user indicate how much of any given item they need. For this, you’ll use a widget that’s useful for capturing a quantity or amount: Slider.

Widget buildQuantityField() {
  // 1
  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      // 2
      Row(
          crossAxisAlignment: CrossAxisAlignment.baseline,
          children: [
            Text(
              'Quantity',
              style: GoogleFonts.lato(fontSize: 28.0),),
            const SizedBox(width: 16.0),
            Text(
              _currentSliderValue.toInt().toString(),
              style: GoogleFonts.lato(fontSize: 18.0),),
          ],
        ),
      // 3
      Slider(
        // 4
        inactiveColor: _currentColor.withOpacity(0.5),
        activeColor: _currentColor,
        // 5
        value: _currentSliderValue.toDouble(),
        // 6
        min: 0.0,
        max: 100.0,
        // 7
        divisions: 100,
        // 8
        label: _currentSliderValue.toInt().toString(),
        // 9
        onChanged: (double value) {
          setState(() {
            _currentSliderValue = value.toInt();
          },);
        },
      ),
    ],);
}
const SizedBox(height: 10.0),
buildQuantityField(),

Creating a grocery tile

Start by creating GroceryTile. Here’s what it looks like:

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:intl/intl.dart';
import '../models/grocery_item.dart';

class GroceryTile extends StatelessWidget {
  // 1
  final GroceryItem item;
  // 2
  final Function(bool) onComplete;
  // 3
  final TextDecoration textDecoration;

  // 4
  GroceryTile({
    Key key,
    this.item,
    this.onComplete}) :
    textDecoration =
    item.isComplete ? TextDecoration.lineThrough : TextDecoration.none,
    super(key: key);

  @override
  Widget build(BuildContext context) {
    // TODO 21: Change this Widget
    return Container(
      height: 100.0,
      // TODO 20: Replace this color
      color: Colors.red,
    );
  }
}

Setting up hot reload

Switch back to grocery_item_screen.dart and add the following import:

import '../components/grocery_tile.dart';
const SizedBox(height: 16),
GroceryTile(
  item: GroceryItem(
    name: _name,
    importance: _importance,
    color: _currentColor,
    quantity: _currentSliderValue,
    date: DateTime(_dueDate.year, _dueDate.month, _dueDate.day,
        _timeOfDay.hour, _timeOfDay.minute,),),),

Building GroceryTile

Now that you’ve set up the live update, it’s time to add more details to your grocery tile. Switch back to grocery_tile.dart.

Displaying the importance label

So far, you’ve let the user pick an importance for each grocery item, but you’re not displaying that information. To fix this, add the following code below build():

Widget buildImportance() {
  if (item.importance == Importance.low) {
    return Text(
      'Low',
      style: GoogleFonts.lato(decoration: textDecoration));
  } else if (item.importance == Importance.medium) {
    return Text(
      'Medium',
      style: GoogleFonts.lato(
        fontWeight: FontWeight.w800,
        decoration: textDecoration));
  } else if (item.importance == Importance.high) {
    return Text(
      'High',
      style: GoogleFonts.lato(
        color: Colors.red,
        fontWeight: FontWeight.w900,
        decoration: textDecoration,),);
  } else {
    throw Exception('This importance type does not exist');
  }
}

Displaying the selected date

Now, you need to fix the same problem with the date to buy the groceries. To do this, add the following code below buildImportance():

Widget buildDate() {
  final dateFormatter = DateFormat('MMMM dd h:mm a');
  final dateString = dateFormatter.format(item.date);
  return Text(
    dateString,
    style: TextStyle(
      decoration: textDecoration),);
}

Displaying the checkbox

Similarly, you’ve added the functionality to let the user mark an item as complete, but haven’t shown the checkbox anywhere. Fix this by adding the following code below buildDate():

Widget buildCheckbox() {
  return Checkbox(
    // 1
    value: item.isComplete,
    // 2
    onChanged: onComplete,);
}

Finishing the GroceryItem screen

At this point, you’re ready to put all the elements in place to finish the Grocery Item screen.

child: Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  children: [
    // TODO 22: Add Row to group (name, date, importance)
    // TODO 23: Add Row to group (quantity, checkbox)
  ],),

Adding the first Row

Locate // TODO 22: Add Row to group (name, date, importance) and replace it with the following:

// 1
Row(
  children: [
    // 2
    Container(width: 5.0, color: item.color),
    // 3
    const SizedBox(width: 16.0),
    // 4
    Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 5
          Text(item.name,
              style: GoogleFonts.lato(
                  decoration: textDecoration,
                  fontSize: 21.0,
                  fontWeight: FontWeight.bold),),
          const SizedBox(height: 4.0),
          buildDate(),
          const SizedBox(height: 4.0),
          buildImportance(),
        ],),
  ],
),

Adding the second Row

Next, locate // TODO 23: Add Row to group (quantity, checkbox) and replace it with the following:

// 6
Row(
  children: [
    // 7
    Text(item.quantity.toString(),
        style:
            GoogleFonts.lato(
              decoration: textDecoration,
              fontSize: 21.0),),
    // 8
    buildCheckbox(),
],),

Saving the user’s work

For the finishing touch, the user needs to be able to save the item.

// 1
final groceryItem = GroceryItem(
    id: widget.originalItem?.id ?? Uuid().v1(),
    name: _nameController.text,
    importance: _importance,
    color: _currentColor,
    quantity: _currentSliderValue,
    date: DateTime(_dueDate.year, _dueDate.month,
        _dueDate.day, _timeOfDay.hour, _timeOfDay.minute,),);

if (widget.isUpdating) {
  // 2
  widget.onUpdate(groceryItem);
} else {
  // 3
  widget.onCreate(groceryItem);
}

Creating the GroceryListScreen

In screens, create a new file called grocery_list_screen.dart.

import 'package:flutter/material.dart';
import '../components/grocery_tile.dart';
import '../models/models.dart';
import 'grocery_item_screen.dart';

class GroceryListScreen extends StatelessWidget {
  final GroceryManager manager;

  const GroceryListScreen({Key key, this.manager}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // TODO 26: Replace with ListView
    return Container();
  }
}

Adding items to the grocery screen

Open grocery_screen.dart and add the following imports:

import 'grocery_list_screen.dart';
return GroceryListScreen(manager: manager);

Creating a GroceryList view

Open grocery_list_screen.dart, then locate // TODO 26: Replace with ListView and replace Container with the following:

// 1
final groceryItems = manager.groceryItems;
// 2
return Padding(
  padding: const EdgeInsets.all(16.0),
  // 3
  child: ListView.separated(
      // 4
      itemCount: groceryItems.length,
      itemBuilder: (context, index) {
        final item = groceryItems[index];
        // TODO 28: Wrap in a Dismissable
        // TODO 27: Wrap in an InkWell
        // 5
        return GroceryTile(
          key: Key(item.id),
          item: item,
          // 6
          onComplete: (change) {
            // 7
            manager.completeItem(index, change);
        },);
      },
      // 8
      separatorBuilder: (context, index) {
        return const SizedBox(height: 16.0);
      },),
);

Adding gestures

Before you add gestures, here is a quick overview!

Applying different behavior

Another thing to be aware of with gesture widgets is HitTestBehavior, which controls how the gesture behaves during a hit test.

Adding an InkWell

Open grocery_list_screen.dart, locate // TODO 27: Wrap in an InkWell and replace the existing return GroceryTile() code with the following:

// 1
return InkWell(
  child: GroceryTile(
    key: Key(item.id),
    item: item,
    onComplete: (change) {
      manager.completeItem(index, change);
  }),
  // 2
  onTap: () {
    Navigator.push(
        context,
        MaterialPageRoute(
            builder: (context) => GroceryItemScreen(
              originalItem: item,
              // 3
              onUpdate: (item) {
                // 4
                manager.updateItem(item, index);
                // 5
                Navigator.pop(context);
              },
            ),),);
  },
);

Dismissing items with a swipe

Next, you’ll learn how to dismiss or delete items from the list. You’ll use Dismissible, a widget that clears items from the list when the user swipes left or right. It even supports swiping in the vertical direction.

Dismissible(
  // 6
  key: Key(item.id),
  // 7
  direction: DismissDirection.endToStart,
  // 8
  background: Container(
    color: Colors.red,
    alignment: Alignment.centerRight,
    child: const Icon(Icons.delete_forever,
      color: Colors.white, size: 50.0)),
  // 9
  onDismissed: (direction) {
    // 10
    manager.deleteItem(index);
    // 11
    Scaffold.of(context).showSnackBar(
      SnackBar(content: Text('${item.name} dismissed')));
  },
  child:

Caching your page selection

You’re almost done, but there’s one final thing to tweak! Did you notice any problems when you switched tabs?

body: IndexedStack(index: tabManager.selectedTab, children: pages),

Key points

  • You can pass data around with callbacks or provider packages.
  • If you need to pass data one level up, use callbacks.
  • If you need to pass data deep in the widget tree, use providers.
  • Provider is a state management helper that acts as a wrapper around inherited widgets.
  • Provider helps expose state model objects to widgets below it.
  • Consumer listens for changes in value and rebuilds the widgets below itself.
  • Split your widgets by screen to keep code modular and organized.
  • Create manager objects to manage functions and state changes in one place.
  • 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 and providers. You learn to create different input widgets. You also learned to apply touch events to navigate to parts of your app.

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.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now