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.
provider
package to manage state between screens.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
CupertinoApp
, which provides theming, navigation, text direction, and other defaults required to create an app that an iOS user expects.CupertinoStoreHomePage
as the homepage. 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
CupertinoPageScaffold
supports single pages and accepts a Cupertino-style navigation bar, background color, and holds the widget tree for the page. You'll learn about the second type of scaffold in the next step. 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
CupertinoTabBar
requires at least two items, or you will see errors at run-time.tabBuilder:
is responsible for making sure the specified tab is built. In this case, it calls a class constructor to set up each respective tab, wrapping all three in CupertinoTabView
and CupertinoPageScaffold
. 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
Consumer
, from the provider
package, assists with state management. More about the model later.CustomScrollView
with a CupertinoSliverNavigationBar
widget. 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
CustomScrollView
. 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
AppStateModel
shows a way of centralizing the state of the application, and making the state available throughout the whole application. In later steps we will use this state to drive our Search and Shopping Cart functionality. 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
AppStateModel
at the top of the widget tree to make it available throughout the entire app.ChangeNotifierProvider
from the provider package, which monitors AppStateModel for change notifications.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
CupertinoSliverNavigationBar
is how we get iOS 11 style expanding titles in the navigation bar. This is important to make an iOS user feel at home in the app. 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
CupertinoSliverNavigationBar
).SliverSafeArea
's top
property to false
so that it ignores the notch.SliverSafeArea
's left
and right
properties still default to true
in case the phone is rotated, and it still accounts for the bottom
so that the sliver can scroll past the bottom home bar to avoid obstruction when it's scrolled to the end.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
_SearchTabState
is where we keep state specific to searching. In this implementation we store what the search terms are, and we hook into the AppStateModel
to fulfill the search capability. In the case where we implement an API back end, here is a good place to do network access for Search. 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
CupertinoDatePicker
is quick to do, and gives iOS users an intuitive way to enter dates and times. 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.
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.
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:
You can find more info at the following links: