Welcome to the Flutter Cupertino codelab!

In this codelab, you'll create a Cupertino (iOS-style) app using Flutter. The Flutter SDK ships with two styled widget libraries (in addition to the basic widget library):

Why write a Cupertino app? The Material design language was created for any platform, not just Android. When you write a Material app in Flutter, it has the Material look and feel on all devices, even iOS. If you want your app to look like a standard iOS-styled app, then you would use the Cupertino library.

You can technically run a Cupertino app on either Android or iOS, but (due to licensing issues) Cupertino won't have the correct fonts on Android. For this reason, use an iOS-specific device when writing a Cupertino app.

You'll implement a Cupertino style shopping app containing three tabs: one for the product list, one for a product search, and one for the shopping cart.

What you'll learn in this codelab

What would you like to learn from this codelab?

I'm new to the topic, and I want a good overview. I know something about this topic, but I want a refresher. I'm looking for example code to use in my project. I'm looking for an explanation of something specific.

Create the initial app using a CupertinoPageScaffold.

Create a simple templated Flutter app, using the instructions in Getting Started with your first Flutter app. Name the project cupertino_store (instead of myapp). You'll be modifying this starter app to create the finished app.

Replace the contents of lib/main.dart.
Delete all of the code from lib/main.dart, which creates a Material-themed button counting app. Replace with the following code, which initializes a Cupertino app.

lib/main.dart

import 'package:flutter/cupertino.dart';

import 'app.dart';

void main() {
  return runApp(CupertinoStoreApp());
}

Observations

Create lib/styles.dart.
Add a file to the lib directory called styles.dart. The Styles class defines the text and color styling to customize the app. Here is a sample of the file, but you can get the full content on GitHub: lib/styles.dart.

lib/styles.dart

// THIS IS A SAMPLE FILE. Get the full content at the link above.
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';

abstract class Styles {
  static const TextStyle productRowItemName = TextStyle(
    color: Color.fromRGBO(0, 0, 0, 0.8),
    fontSize: 18,
    fontStyle: FontStyle.normal,
    fontWeight: FontWeight.normal,
  );

  static const TextStyle productRowTotal = TextStyle(
    color: Color.fromRGBO(0, 0, 0, 0.8),
    fontSize: 18,
    fontStyle: FontStyle.normal,
    fontWeight: FontWeight.bold,
  );



 // ...
// THIS IS A SAMPLE FILE. Get the full content at the link above.

Observations

Create lib/app.dart and add the CupertinoStoreApp class.
Add the following CupertinoStoreApp class to lib/app.dart.

lib/app.dart

import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';

class CupertinoStoreApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // This app is designed only to work vertically, so we limit
    // orientations to portrait up and down.
    SystemChrome.setPreferredOrientations(
        [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);

    return CupertinoApp(
      home: CupertinoStoreHomePage(),
    );
  }
}

Observations

Add the CupertinoStoreHomePage class.
Add the following CupertinoStoreHomePage class to lib/app.dart to create the layout for the homepage.

lib/app.dart

class CupertinoStoreHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return const CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        middle: Text('Cupertino Store'),
      ),
      child: SizedBox(),
    );
  }
}

Observations

Update the pubspec.yaml file.
At the top of the project, edit the pubspec.yaml file. Add the libraries that you will need, and a list of the image assets. Here is a sample of the file, find the full content on GitHub: pubspec.yaml.

pubspec.yaml

# THIS IS A SAMPLE OF THE FILE. Get the full file at the link above.
name: cupertino_store
description: Creating a Store in Cupertino widgets
version: 1.0.0+1

environment:
  sdk: ^2.4.0
  flutter: ^1.7.0

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^0.1.3
  intl: ^0.16.1
  provider: ^4.1.1
  shrine_images: ^1.0.0

dev_dependencies:
  pedantic: ^1.9.0

flutter:
  assets:
   - packages/shrine_images/0-0.jpg
# THIS IS A SAMPLE OF THE FILE. Get the full file at the link above.

Observations

Run the app. You should see the following white screen containing the Cupertino navbar and a title:

Problems?

If your app is not running correctly, look for typos. If needed, use the code at the following links to get back on track.

The final app features 3 tabs:

In this step, you'll update the home page with three tabs using a CupertinoTabScaffold. You'll also add a data source that provides the list of items for sale, with photos and prices.

In the previous step, you created a CupertinoStoreHomePage class using a CupertinoPageScaffold. Use this scaffold for pages that have no tabs. The final app has three tabs, so swap out the CupertinoPageScaffold for a CupertinoTabScaffold.

Cupertino tab has a separate scaffold because on iOS, the bottom tab is commonly persistent above nested routes rather than inside pages.

Update lib/app.dart.
Replace the CupertinoStoreHomePage class with the following, which sets up a 3-tab scaffold:

lib/app.dart

class CupertinoStoreHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CupertinoTabScaffold(
      tabBar: CupertinoTabBar(
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(CupertinoIcons.home),
            title: Text('Products'),
          ),
          BottomNavigationBarItem(
            icon: Icon(CupertinoIcons.search),
            title: Text('Search'),
          ),
          BottomNavigationBarItem(
            icon: Icon(CupertinoIcons.shopping_cart),
            title: Text('Cart'),
          ),
        ],
      ),
      tabBuilder: (context, index) {
        CupertinoTabView returnValue;
        switch (index) {
          case 0:
            returnValue = CupertinoTabView(builder: (context) {
              return CupertinoPageScaffold(
                child: ProductListTab(),
              );
            });
            break;
          case 1:
            returnValue = CupertinoTabView(builder: (context) {
              return CupertinoPageScaffold(
                child: SearchTab(),
              );
            });
            break;
          case 2:
            returnValue = CupertinoTabView(builder: (context) {
              return CupertinoPageScaffold(
                child: ShoppingCartTab(),
              );
            });
            break;
        }
        return returnValue;
      },
    );
  }
}

Observations

Add stub classes for the content of the new tabs.
Create a lib/product_list_tab.dart file for the first tab that compiles cleanly, but only displays a white screen. Use the following content:

lib/product_list_tab.dart

import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';

import 'model/app_state_model.dart';

class ProductListTab extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<AppStateModel>(
      builder: (context, model, child) {
        return const CustomScrollView(
          slivers: <Widget>[
            CupertinoSliverNavigationBar(
              largeTitle: Text('Cupertino Store'),
            ),
          ],
        );
      },
    );
  }
}

Observations

Add a search page stub.
Create a lib/search_tab.dart file that compiles cleanly, but only displays a white screen. Use the following content:

lib/search_tab.dart

import 'package:flutter/cupertino.dart';

class SearchTab extends StatefulWidget {
  @override
  _SearchTabState createState() {
    return _SearchTabState();
  }
}

class _SearchTabState extends State<SearchTab> {
  @override
  Widget build(BuildContext context) {
    return const CustomScrollView(
      slivers: <Widget>[
        CupertinoSliverNavigationBar(
          largeTitle: Text('Search'),
        ),
      ],
    );
  }
}

Observations

Add a shopping cart page stub.
Create a lib/shopping_cart_tab.dart file that compiles cleanly, but only displays a white screen. Use the following content:

lib/shopping_cart_tab.dart

import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';

import 'model/app_state_model.dart';

class ShoppingCartTab extends StatefulWidget {
  @override
  _ShoppingCartTabState createState() {
    return _ShoppingCartTabState();
  }
}

class _ShoppingCartTabState extends State<ShoppingCartTab> {
  @override
  Widget build(BuildContext context) {
    return Consumer<AppStateModel>(
      builder: (context, model, child) {
        return const CustomScrollView(
          slivers: <Widget>[
             CupertinoSliverNavigationBar(
              largeTitle: Text('Shopping Cart'),
            ),
          ],
        );
      },
    );
  }
}

Observations

Update lib/app.dart.
Update the import statements in lib/app.dart to pull in the new tab widgets:

lib/app.dart

import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'product_list_tab.dart';   // NEW
import 'search_tab.dart';         // NEW
import 'shopping_cart_tab.dart';  // NEW

In the second part of this step, continued on the next page, you'll add code for managing and sharing state across the tabs.

The app has some common data that needs to be shared across multiple screens, so you need a simple way to flow the data to each of the objects that need it. The provider package provides an easy way to do that. In provider, you define the data model and then use ChangeNotifierProvider to provide your data model down the tree.

Create the data model classes.
Create a model directory under lib. Add a lib/model/product.dart file that defines the product data coming from the data source:

lib/model/product.dart

import 'package:flutter/foundation.dart';

enum Category {
  all,
  accessories,
  clothing,
  home,
}

class Product {
  const Product({
    @required this.category,
    @required this.id,
    @required this.isFeatured,
    @required this.name,
    @required this.price,
  })  : assert(category != null),
        assert(id != null),
        assert(isFeatured != null),
        assert(name != null),
        assert(price != null);

  final Category category;
  final int id;
  final bool isFeatured;
  final String name;
  final int price;

  String get assetName => '$id-0.jpg';
  String get assetPackage => 'shrine_images';

  @override
  String toString() => '$name (id=$id)';
}

Observations

The ProductsRepository class contains the full list of products for sale, along with their price, title text, and a category. Our app won't do anything with the isFeatured property. The class also includes a loadProducts() method that returns either all products, or all products in a given category.

Create the products repository.
Create a lib/model/products_repository.dart file. This file contains all products for sale. Each product belongs to a category. Here is a sample of the file, but you can get the entire contents on GitHub: products_repository.dart.

lib/model/products_repository.dart

// THIS IS A SAMPLE FILE. Get the full content at the link above.

import 'product.dart';

class ProductsRepository {
 static const _allProducts = <Product>[
   Product(
     category: Category.accessories,
     id: 0,
     isFeatured: true,
     name: 'Vagabond sack',
     price: 120,
   ),
   Product(
     category: Category.home,
     id: 9,
     isFeatured: true,
     name: 'Gilt desk trio',
     price: 58,
   ),
   Product(
     category: Category.clothing,
     id: 33,
     isFeatured: true,
     name: 'Cerise scallop tee',
     price: 42,
   ),
   // THIS IS A SAMPLE FILE. Get the full content at the link above.
 ];

 static List<Product> loadProducts(Category category) {
   if (category == Category.all) {
     return _allProducts;
   } else {
     return _allProducts.where((p) => p.category == category).toList();
   }
 }
}

Observations

You are now ready to define the model. Create a lib/model/app_state_model.dart file. In the AppStateModel class, provide methods for accessing data from the model. For example, add a method for accessing the shopping cart total, another for a list of selected products to purchase, another for the shipping cost, and so on.

Create the model class.
Here is the list of method signatures provided by this class. Get the full content on GitHub: lib/model/app_state_model.dart.

lib/model/app_state_model.dart

// THIS IS A SAMPLE FILE ONLY. Get the full content at the link above.

import 'package:flutter/foundation.dart' as foundation;

import 'product.dart';
import 'products_repository.dart';

double _salesTaxRate = 0.06;
double _shippingCostPerItem = 7;

class AppStateModel extends foundation.ChangeNotifier {
 List<Product> _availableProducts;
 Category _selectedCategory = Category.all;
 final _productsInCart = <int, int>{};

 Map<int, int> get productsInCart
 int get totalCartQuantity 
 Category get selectedCategory
 double get subtotalCost
 double get shippingCost

 double get tax
 double get totalCost
 List<Product> getProducts()
 List<Product> search(String searchTerms)
 void addProductToCart(int productId)
 void removeItemFromCart(int productId)
 Product getProductById(int id)
 void clearCart()
 void loadProducts()
 void setCategory(Category newCategory)
// THIS IS A SAMPLE FILE ONLY. Get the full content at the link above.

Observations

Update lib/main.dart.
In the main()method, initialize the model. Add the lines marked with NEW.

lib/main.dart

import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';             // NEW

import 'app.dart';
import 'model/app_state_model.dart';                 // NEW

void main() {
 return runApp(
   ChangeNotifierProvider<AppStateModel>(            // NEW
     create: (_) => AppStateModel()..loadProducts(), // NEW
     child: CupertinoStoreApp(),                     // NEW
   ),
 );
}

Observations

Run the app. You should see the following white screen containing the Cupertino navbar, a title, and a drawer with 3 labeled icons representing the three tabs. You can switch between the tabs, but all three pages are currently blank.

Problems?

If your app is not running correctly, look for typos. If needed, use the code at the following links to get back on track.

In this step, display the products for sale in the product list tab.

Add lib/product_row_item.dart to display the products.
Create the lib/product_row_item.dart file, with the following content:

lib/product_row_item.dart

import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';

import 'model/app_state_model.dart';
import 'model/product.dart';
import 'styles.dart';

class ProductRowItem extends StatelessWidget {
  const ProductRowItem({
    this.index,
    this.product,
    this.lastItem,
  });

  final Product product;
  final int index;
  final bool lastItem;

  @override
  Widget build(BuildContext context) {
    final row = SafeArea(
      top: false,
      bottom: false,
      minimum: const EdgeInsets.only(
        left: 16,
        top: 8,
        bottom: 8,
        right: 8,
      ),
      child: Row(
        children: <Widget>[
          ClipRRect(
            borderRadius: BorderRadius.circular(4),
            child: Image.asset(
              product.assetName,
              package: product.assetPackage,
              fit: BoxFit.cover,
              width: 76,
              height: 76,
            ),
          ),
          Expanded(
            child: Padding(
              padding: const EdgeInsets.symmetric(horizontal: 12),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.start,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  Text(
                    product.name,
                    style: Styles.productRowItemName,
                  ),
                  const Padding(padding: EdgeInsets.only(top: 8)),
                  Text(
                    '\$${product.price}',
                    style: Styles.productRowItemPrice,
                  )
                ],
              ),
            ),
          ),
          CupertinoButton(
            padding: EdgeInsets.zero,
            onPressed: () {
              final model = Provider.of<AppStateModel>(context, listen: false);
              model.addProductToCart(product.id);
            },
            child: const Icon(
              CupertinoIcons.plus_circled,
              semanticLabel: 'Add',
            ),
          ),
        ],
      ),
    );

    if (lastItem) {
      return row;
    }

    return Column(
      children: <Widget>[
        row,
        Padding(
          padding: const EdgeInsets.only(
            left: 100,
            right: 16,
          ),
          child: Container(
            height: 1,
            color: Styles.productRowDivider,
          ),
        ),
      ],
    );
  }
}

Observations

In lib/product_list_tab.dart, import the product_row_item.dart file.

lib/product_list_tab.dart

import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';

import 'model/app_state_model.dart';
import 'product_row_item.dart';      // NEW

In the build() method for ProductListTab, get the product list and the number of products. Add the new lines indicated below:

class ProductListTab extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return CupertinoPageScaffold(
     child: Consumer<AppStateModel>(
       builder: (context, child, model) {
         final products = model.getProducts();  // NEW
         return CustomScrollView(
           semanticChildCount: products.length, // NEW
           slivers: <Widget>[
             CupertinoSliverNavigationBar(
               largeTitle: const Text('Cupertino Store'),
             ),
           ],
         );
       },
     ),
   );
 }
}

Also in the build() method, add a new sliver to the sliver widgets list to hold the product list. Add the new lines indicated below:

class ProductListTab extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<AppStateModel>(
      builder: (context, model, child) {
        final products = model.getProducts();
        return CustomScrollView(
          semanticChildCount: products.length,
          slivers: <Widget>[
            const CupertinoSliverNavigationBar(
              largeTitle: Text('Cupertino Store'),
            ),
            SliverSafeArea(  // BEGINNING OF NEW CONTENT
              top: false,
              minimum: const EdgeInsets.only(top: 8),
              sliver: SliverList(
                delegate: SliverChildBuilderDelegate(
                  (context, index) {
                    if (index < products.length) {
                      return ProductRowItem(
                        index: index,
                        product: products[index],
                        lastItem: index == products.length - 1,
                      );
                    }

                    return null;
                  },
                ),
              ),
            )                // END OF NEW CONTENT
          ],
        );
      },
    );
  }
}

Observations

Run the app. In the product tab, you should see a list of products with images, prices, and a button with a plus sign that adds the product to the shopping cart. The button will be implemented later, in the step where you'll build out the shopping cart.

Problems?

If your app is not running correctly, look for typos. If needed, use the code at the following links to get back on track.

In this step, you'll build out the search tab and add the ability to search through the products.

Update the imports in lib/search_tab.dart.

Add imports for the classes that the search tab will use:

lib/search_tab.dart

import 'package:flutter/cupertino.dart'
import 'package:provider/provider.dart'
import 'model/app_state_model.dart'
import 'product_row_item.dart'
import 'search_bar.dart'
import 'styles.dart'

Update the build() method in _SearchTabState.

Initialize the model and replace the CustomScrollView with individual components for searching and listing.

class _SearchTabState extends State<SearchTab> {
// ...

  @override
  Widget build(BuildContext context) {
    final model = Provider.of<AppStateModel>(context);   
    final results = model.search(_terms);                

    return DecoratedBox(
      decoration: const BoxDecoration(
        color: Styles.scaffoldBackground,
      ),
      child: SafeArea(
        child: Column(
          children: [
            _buildSearchBox(),
            Expanded(
              child: ListView.builder(
                itemBuilder: (context, index) => ProductRowItem(
                      index: index,
                      product: results[index],
                      lastItem: index == results.length - 1,
                    ),
                itemCount: results.length,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Observations

Add supporting variables, functions, and methods to the _SearchTabState class.

These include initState(), dispose(), _onTextChanged(), and _buildSearchBox(), as shown below:

class _SearchTabState extends State<SearchTab> {
  TextEditingController _controller;
  FocusNode _focusNode;
  String _terms = '';

  @override
  void initState() {
    super.initState();
    _controller = TextEditingController()..addListener(_onTextChanged);
    _focusNode = FocusNode();
  }

  @override
  void dispose() {
    _focusNode.dispose();
    _controller.dispose();
    super.dispose();
  }

  void _onTextChanged() {
    setState(() {
      _terms = _controller.text;
    });
  }

  Widget _buildSearchBox() {
    return Padding(
      padding: const EdgeInsets.all(8),
      child: SearchBar(
        controller: _controller,
        focusNode: _focusNode,
      ),
    );
  }                                     // TO HERE

 @override
 Widget build(BuildContext context) {

Observations

Add a SearchBar class.

Create a new file, lib/search_bar.dart. The SearchBar class handles the actual search in the product list. Seed the file with the following content:

lib/search_bar.dart

import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'styles.dart';

class SearchBar extends StatelessWidget {
  const SearchBar({
    @required this.controller,
    @required this.focusNode,
  });

  final TextEditingController controller;
  final FocusNode focusNode;

  @override
  Widget build(BuildContext context) {
    return DecoratedBox(
      decoration: BoxDecoration(
        color: Styles.searchBackground,
        borderRadius: BorderRadius.circular(10),
      ),
      child: Padding(
        padding: const EdgeInsets.symmetric(
          horizontal: 4,
          vertical: 8,
        ),
        child: Row(
          children: [
            const Icon(
              CupertinoIcons.search,
              color: Styles.searchIconColor,
            ),
            Expanded(
              child: CupertinoTextField(
                controller: controller,
                focusNode: focusNode,
                style: Styles.searchText,
                cursorColor: Styles.searchCursorColor,
              ),
            ),
            GestureDetector(
              onTap: controller.clear,
              child: const Icon(
                CupertinoIcons.clear_thick_circled,
                color: Styles.searchIconColor,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Observations

Run the app. Select the search tab and enter "shirt" into the text field. You should see a list of 5 products that contain "shirt" in the name.

Problems?

If your app is not running correctly, look for typos. If needed, use the code at the following links to get back on track.

In the next three steps, you'll build out the shopping cart tab. In this first step, you'll add fields for capturing customer info.

Update the lib/shopping_cart_tab.dart file.

Add private methods for building the name, email, and location fields. Then add a _buildSliverChildBuildDelegate() method that build out parts of the user interface.

lib/shopping_cart_tab.dart

class _ShoppingCartTabState extends State<ShoppingCartTab> {
  String name;      // ADD FROM HERE
  String email;
  String location;
  String pin;
  DateTime dateTime = DateTime.now();

  Widget _buildNameField() {
    return CupertinoTextField(
      prefix: const Icon(
        CupertinoIcons.person_solid,
        color: CupertinoColors.lightBackgroundGray,
        size: 28,
      ),
      padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 12),
      clearButtonMode: OverlayVisibilityMode.editing,
      textCapitalization: TextCapitalization.words,
      autocorrect: false,
      decoration: const BoxDecoration(
        border: Border(
          bottom: BorderSide(
            width: 0,
            color: CupertinoColors.inactiveGray,
          ),
        ),
      ),
      placeholder: 'Name',
      onChanged: (newName) {
        setState(() {
          name = newName;
        });
      },
    );
  }

  Widget _buildEmailField() {
    return const CupertinoTextField(
      prefix: Icon(
        CupertinoIcons.mail_solid,
        color: CupertinoColors.lightBackgroundGray,
        size: 28,
      ),
      padding: EdgeInsets.symmetric(horizontal: 6, vertical: 12),
      clearButtonMode: OverlayVisibilityMode.editing,
      keyboardType: TextInputType.emailAddress,
      autocorrect: false,
      decoration: BoxDecoration(
        border: Border(
          bottom: BorderSide(
            width: 0,
            color: CupertinoColors.inactiveGray,
          ),
        ),
      ),
      placeholder: 'Email',
    );
  }

  Widget _buildLocationField() {
    return const CupertinoTextField(
      prefix: Icon(
        CupertinoIcons.location_solid,
        color: CupertinoColors.lightBackgroundGray,
        size: 28,
      ),
      padding: EdgeInsets.symmetric(horizontal: 6, vertical: 12),
      clearButtonMode: OverlayVisibilityMode.editing,
      textCapitalization: TextCapitalization.words,
      decoration: BoxDecoration(
        border: Border(
          bottom: BorderSide(
            width: 0,
            color: CupertinoColors.inactiveGray,
          ),
        ),
      ),
      placeholder: 'Location',
    );
  }

  SliverChildBuilderDelegate _buildSliverChildBuilderDelegate(
      AppStateModel model) {
    return SliverChildBuilderDelegate(
      (context, index) {
        switch (index) {
          case 0:
            return Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              child: _buildNameField(),
            );
          case 1:
            return Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              child: _buildEmailField(),
            );
          case 2:
            return Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              child: _buildLocationField(),
            );
          default:
          // Do nothing. For now.
        }
        return null;
      },
    );
  }                  // TO HERE

Observations

Update the build() method in the _ShoppingCartTabState class.

Add a SliverSafeArea that calls the _buildSliverChildBuilderDelegate method:

  @override
  Widget build(BuildContext context) {
    return Consumer<AppStateModel>(
      builder: (context, model, child) {
        return CustomScrollView(
          slivers: <Widget>[
            const CupertinoSliverNavigationBar(
              largeTitle: Text('Shopping Cart'),
            ),
            SliverSafeArea(
              top: false,
              minimum: const EdgeInsets.only(top: 4),
              sliver: SliverList(
                delegate: _buildSliverChildBuilderDelegate(model),
              ),
            )
          ],
        );
      },
    );
  }
}

Observations

Run the app. Select the shopping cart tab. You should see three text fields for gathering customer information:

Problems?

If your app is not running correctly, look for typos. If needed, use the code at the following link to get back on track.

In this step, add a CupertinoDatePicker to the shopping cart so the user can select a preferred shipping date.

Add imports and a const to lib/shopping_cart_tab.dart.

Add the new lines, as shown:

lib/shopping_cart_tab.dart

import 'package:flutter/cupertino.dart';
import 'package:intl/intl.dart';            // NEW
import 'package:provider/provider.dart';
import 'model/app_state_model.dart';
import 'styles.dart';                       // NEW

const double _kDateTimePickerHeight = 216;  // NEW

Add a _buildDateAndTimePicker() function to the _ShoppingCartTab widget.

Add the function, as follows:

class _ShoppingCartTabState extends State<ShoppingCartTab> {
  // ...

Widget _buildDateAndTimePicker(BuildContext context) { // NEW FROM HERE
 return Column(
   children: <Widget>[
     Row(
       mainAxisAlignment: MainAxisAlignment.spaceBetween,
       children: <Widget>[
         Row(
           mainAxisAlignment: MainAxisAlignment.start,
           children: const <Widget>[
             Icon(
               CupertinoIcons.clock,
               color: CupertinoColors.lightBackgroundGray,
               size: 28,
             ),
             SizedBox(width: 6),
             Text(
               'Delivery time',
               style: Styles.deliveryTimeLabel,
             ),
           ],
         ),
         Text(
           DateFormat.yMMMd().add_jm().format(dateTime),
           style: Styles.deliveryTime,
         ),
       ],
     ),
     Container(
       height: _kDateTimePickerHeight,
       child: CupertinoDatePicker(
         mode: CupertinoDatePickerMode.dateAndTime,
         initialDateTime: dateTime,
         onDateTimeChanged: (newDateTime) {
           setState(() {
             dateTime = newDateTime;
           });
         },
       ),
     ),
   ],
 );
}                                                // TO HERE

SliverChildBuilderDelegate _buildSliverChildBuilderDelegate(
   AppStateModel model) {
  // ...

Observations

Add a call to build the date and time UI, to the _buildSliverChildBuilderDelegate function. Add the new code, as shown:

  SliverChildBuilderDelegate _buildSliverChildBuilderDelegate(
      AppStateModel model) {
    return SliverChildBuilderDelegate(
      (context, index) {
        switch (index) {
          case 0:
            return Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              child: _buildNameField(),
            );
          case 1:
            return Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              child: _buildEmailField(),
            );
          case 2:
            return Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              child: _buildLocationField(),
            );
          case 3:                // ADD FROM HERE
            return Padding(
              padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
              child: _buildDateAndTimePicker(context),
            );                   // TO HERE
          default:
          // Do nothing. For now.
        }
        return null;
      },
    );
  }

Run the app. Select the shopping cart tab. You should see an iOS style date picker below the text fields for gathering the customer info:

Problems?

If your app is not running correctly, look for typos. If needed, use the code at the following link to get back on track.

In this step, add the selected items to the shopping cart to complete the app.

Import the product package in shopping_cart_tab.dart.

lib/shopping_cart_tab.dart

import 'package:flutter/cupertino.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'model/app_state_model.dart';
import 'model/product.dart';              // NEW
import 'styles.dart';

Add a currency format to the _ShoppingCartTabState class.

Add the line marked NEW:

class _ShoppingCartTabState extends State<ShoppingCartTab> {
 String name;
 String email;
 String location;
 String pin;
 DateTime dateTime = DateTime.now();
 final _currencyFormat = NumberFormat.currency(symbol: '\$'); // NEW

Add a product index to the _buildSliverChildBuilderDelegate function.

Add the line marked NEW:

SliverChildBuilderDelegate _buildSliverChildBuilderDelegate(
   AppStateModel model) {
 return SliverChildBuilderDelegate(
   (context, index) {
     final productIndex = index - 4;    // NEW
     switch (index) {]
  // ...

In the same function, display the items to purchase.

Add the code to the default: section of the switch statement, as follows:

switch (index) {
 case 0:
   return Padding(
     padding: const EdgeInsets.symmetric(horizontal: 16),
     child: _buildNameField(),
   );
 case 1:
   return Padding(
     padding: const EdgeInsets.symmetric(horizontal: 16),
     child: _buildEmailField(),
   );
 case 2:
   return Padding(
     padding: const EdgeInsets.symmetric(horizontal: 16),
     child: _buildLocationField(),
   );
 case 3:
   return Padding(
     padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
     child: _buildDateAndTimePicker(context),
   );
 default:                      // NEW FROM HERE
   if (model.productsInCart.length > productIndex) {
     return ShoppingCartItem(
       index: index,
       product: model.getProductById(
           model.productsInCart.keys.toList()[productIndex]),
       quantity: model.productsInCart.values.toList()[productIndex],
       lastItem: productIndex == model.productsInCart.length - 1,
       formatter: _currencyFormat,
     );
   } else if (model.productsInCart.keys.length == productIndex &&
       model.productsInCart.isNotEmpty) {
     return Padding(
       padding: const EdgeInsets.symmetric(horizontal: 20),
       child: Row(
         mainAxisAlignment: MainAxisAlignment.end,
         children: <Widget>[
           Column(
             crossAxisAlignment: CrossAxisAlignment.end,
             children: <Widget>[
               Text(
                 'Shipping '
                     '${_currencyFormat.format(model.shippingCost)}',
                 style: Styles.productRowItemPrice,
               ),
               const SizedBox(height: 6),
               Text(
                 'Tax ${_currencyFormat.format(model.tax)}',
                 style: Styles.productRowItemPrice,
               ),
               const SizedBox(height: 6),
               Text(
                 'Total  ${_currencyFormat.format(model.totalCost)}',
                 style: Styles.productRowTotal,
               ),
             ],
           )
         ],
       ),
     );
   }
}                       // TO HERE

At the bottom of the file, add a new ShoppingCartItem class:

class ShoppingCartItem extends StatelessWidget {
  const ShoppingCartItem({
    @required this.index,
    @required this.product,
    @required this.lastItem,
    @required this.quantity,
    @required this.formatter,
  });

  final Product product;
  final int index;
  final bool lastItem;
  final int quantity;
  final NumberFormat formatter;

  @override
  Widget build(BuildContext context) {
    final row = SafeArea(
      top: false,
      bottom: false,
      child: Padding(
        padding: const EdgeInsets.only(
          left: 16,
          top: 8,
          bottom: 8,
          right: 8,
        ),
        child: Row(
          children: <Widget>[
            ClipRRect(
              borderRadius: BorderRadius.circular(4),
              child: Image.asset(
                product.assetName,
                package: product.assetPackage,
                fit: BoxFit.cover,
                width: 40,
                height: 40,
              ),
            ),
            Expanded(
              child: Padding(
                padding: const EdgeInsets.symmetric(horizontal: 12),
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.start,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: <Widget>[
                        Text(
                          product.name,
                          style: Styles.productRowItemName,
                        ),
                        Text(
                          '${formatter.format(quantity * product.price)}',
                          style: Styles.productRowItemName,
                        ),
                      ],
                    ),
                    const SizedBox(
                      height: 4,
                    ),
                    Text(
                      '${quantity > 1 ? '$quantity x ' : ''}'
                      '${formatter.format(product.price)}',
                      style: Styles.productRowItemPrice,
                    )
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );

    return row;
  }
}

Run the app. From the products tab, select a few items to purchase using the plus-sign button to the right of each item. Select the shopping cart tab. You should see the items listed in the shopping cart below the date picker:

Problems?

If your app is not running correctly, look for typos. If needed, use the code at the following link to get back on track.

Congratulations!

You have completed the codelab and have built a Flutter app with the Cupertino look and feel! You've also used the provider package to manage app state across screens. When you have time, you might want to learn more about managing state in our state management documentation.

Other next steps

This codelab has built a front end for a shopping experience, but the next step in making it real is to create a back-end that handles user accounts, products, shopping carts and the like. There are multiple ways of accomplishing this goal:

Learn more

You can find more info at the following links: