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 is your goal?

I'm an Android developer who wants to learn how to build for iOS. I'm an iOS developer who wants to build with Flutter. I'm a web developer who wants to see what Flutter can do.

You need two pieces of software to complete this lab: the Flutter SDK and an editor. You can use your preferred editor, such as Android Studio or IntelliJ with the Flutter and Dart plugins installed, or Visual Studio Code with the Dart Code and Flutter extensions.

You can run this codelab using one of the following devices:

You'll also need:

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.

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

import 'app.dart';

void main() {
  // This app is designed only to work vertically, so we limit
  // orientations to portrait up and down.
  SystemChrome.setPreferredOrientations(
      [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);

  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.

// 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.

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

class CupertinoStoreApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    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.

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

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.

# 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

environment:
 sdk: ">=2.2.0 <3.0.0"

dependencies:
 flutter:
   sdk: flutter

 cupertino_icons: ^0.1.2
 intl: ^0.15.7
 provider: ^2.0.0+1
 shrine_images: ^1.0.0

dev_dependencies:
 pedantic: ^1.4.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:

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) {
        switch (index) {
          case 0:
            return CupertinoTabView(builder: (context) {
              return CupertinoPageScaffold(
                child: ProductListTab(),
              );
            });
          case 1:
            return CupertinoTabView(builder: (context) {
              return CupertinoPageScaffold(
                child: SearchTab(),
              );
            });
          case 2:
            return CupertinoTabView(builder: (context) {
              return CupertinoPageScaffold(
                child: ShoppingCartTab(),
              );
            });
        }
      },
    );
  }
}

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:

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 CustomScrollView(
          slivers: const <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:

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 CustomScrollView(
      slivers: const <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:

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 CustomScrollView(
          slivers: const <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:

import 'package:flutter/cupertino.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 scoped_model package provides an easy way to do that. In scoped_model, you define the data model used to pass data from the parent widget to its descendants. Wrapping the model in a ScopedModel widget makes the model available to all descendant widgets. The ScopedModelDescendant widget finds the correct ScopedModel in the widget 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:

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.

// 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.

// 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.

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

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

void main() {
 SystemChrome.setPreferredOrientations(
     [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);

 return runApp(
   ChangeNotifierProvider<AppStateModel>(        // NEW
     model: model,                               // 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:

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,
            child: const Icon(
              CupertinoIcons.plus_circled,
              semanticLabel: 'Add',
            ),
            onPressed: () {
              final model = Provider.of<AppStateModel>(context);
              model.addProductToCart(product.id);
            },
          ),
        ],
      ),
    );

    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.

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 ProductRowTab, 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 CupertinoPageScaffold(
     child: Consumer<AppStateModel>(
       builder: (context, child, model) {
         final products = model.getProducts();
         return CustomScrollView(
           semanticChildCount: products.length,
           slivers: <Widget>[
             CupertinoSliverNavigationBar(
               largeTitle: const 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:

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:

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.

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 _SearchTabState class.

Add a SliverSafeArea that calls the _buildSliverChildBuildingDelegate 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:

import 'package:flutter/cupertino.dart';
import 'package:intl/intl.dart';            // NEW
import 'package:scoped_model/scoped_model.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.

import 'package:flutter/cupertino.dart';
import 'package:intl/intl.dart';
import 'package:scoped_model/scoped_model.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

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: