Lab3 - Fooderlich App
Lab3 - Fooderlich App
4. Let’s explore main.dart in this project. Open it and you’ll see the following:
void main() {
// 1
runApp(const Fooderlich());
}
// 4
Page 1 of 26
home: Scaffold(
// 5
appBar: AppBar(title: const Text('Fooderlich')),
body: const Center(child: Text('Let\'s get cooking! 👩👩🍳🍳')),
),
);
}
}
Notes:
// 1 Everything in Flutter starts with a widget. runApp takes in the root widget
Fooderlich.
// 2 Every stateless widget must override the build() method.
// 3 The Fooderlich widget starts by composing a MaterialApp widget to give it a
Material Design system look and feel. See https://material.io for more
details about Material Design.
// 4 The MaterialApp widget contains a Scaffold widget, which defines the layout
and structure of the app. More on this later.
// 5 The scaffold has two properties: an appBar and a body. An Appbar’s title
contains a Text widget. The body has a Center widget, whose child property is a
Text widget.
5. Let’s customize our theme. To share colors and font styles throughout your app, you’ll provide
a ThemeData object to MaterialApp. In the lib directory, open fooderlich_theme.dart, which
contains a predefined theme for your app.
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class FooderlichTheme {
// 1
static TextTheme lightTextTheme = TextTheme(
bodyText1: GoogleFonts.openSans(
fontSize: 14.0,
fontWeight: FontWeight.w700,
color: Colors.black),
headline1: GoogleFonts.openSans(
fontSize: 32.0,
fontWeight: FontWeight.bold,
color: Colors.black),
headline2: GoogleFonts.openSans(
fontSize: 21.0,
fontWeight: FontWeight.w700,
color: Colors.black),
headline3: GoogleFonts.openSans(
fontSize: 16.0,
fontWeight: FontWeight.w600,
color: Colors.black),
headline6: GoogleFonts.openSans(
fontSize: 20.0,
fontWeight: FontWeight.w600,
color: Colors.black),
Page 2 of 26
);
// 2
static TextTheme darkTextTheme = TextTheme(
bodyText1: GoogleFonts.openSans(
fontSize: 14.0,
fontWeight: FontWeight.w600,
color: Colors.white),
headline1: GoogleFonts.openSans(
fontSize: 32.0,
fontWeight: FontWeight.bold,
color: Colors.white),
headline2: GoogleFonts.openSans(
fontSize: 21.0,
fontWeight: FontWeight.w700,
color: Colors.white),
headline3: GoogleFonts.openSans(
fontSize: 16.0,
fontWeight: FontWeight.w600,
color: Colors.white),
headline6: GoogleFonts.openSans(
fontSize: 20.0,
fontWeight: FontWeight.w600,
color: Colors.white),
);
// 3
static ThemeData light() {
return ThemeData(
brightness: Brightness.light,
primaryColor: Colors.white,
accentColor: Colors.black,
textSelectionTheme:
const TextSelectionThemeData(selectionColor:
Colors.green),
textTheme: lightTextTheme,
);
}
// 4
static ThemeData dark() {
return ThemeData(
brightness: Brightness.dark,
primaryColor: Colors.grey[900],
accentColor: Colors.green[600],
textTheme: darkTextTheme,
);
}
}
Notes:
// 1 Declares a TextTheme called lightTextTheme, which uses the Google font
Open Sans and has a predefined font size and weight. Most importantly, the colour
of the text is black.
// 2 Then it defines darkTextTheme. In this case, the text is white.
Page 3 of 26
// 3 Next, it defines a static method, light, which returns the colour tones for a light
theme using the lightTextTheme you created in step 1.
// 4 Finally, it declares a static method, dark, which returns the colour tones for a dark
theme using the darkTextTheme you created in step 2.
6. To use the theme that you’ve seen in 5, open main.dart and replace it with the following
codes:
import 'package:flutter/material.dart';
import 'fooderlich_theme.dart';
void main() {
// 1
runApp(const Fooderlich());
}
Page 4 of 26
Notes:
// 1 Imported the FooderlichTheme.
// 2 Defined a variable that holds the theme. If you want to
change it to dark theme just replace light() with
dark().
// 3 Added the MaterialApp widget’s theme property.
// 4 Added AppBar text styling.
// 5 Finally, added body text styling.
7. Next is setting up the Home widget. Scaffold needs to handle some state changes, via a
StatefulWidget. Your next step is to move code out of main.dart into a new StatefulWidget
named Home. To do so, create a new file in your lib named home.dart. In, home.dart add
the following code:
import 'package:flutter/material.dart';
// 1
class Home extends StatefulWidget {
const Home({Key? key}) : super(key: key);
@override
_HomeState createState() => _HomeState();
}
Page 5 of 26
style:
Theme.of(context).textTheme.headline1)),
);
}
}
Notes:
Most of the Scaffold code looks like what you have in main.dart, but there are a few
changes:
// 1 Your new class extends StatefulWidget.
// 2 The AppBar style now reads: Theme.of(context).textTheme.headline6
instead of: theme.textTheme.headline6.Theme.of(context) returns the
nearest Theme in the widget tree. If the widget has a defined Theme, it returns that.
Otherwise, it returns the app’s theme.
// 3 As with the AppBar, you’ve also updated the Text style to use the
Theme.of(context).
void main() {
// 1
runApp(const Fooderlich());
}
9. Now, open home.dart and add the following code in the Scaffold widget after the body
parameter.
// 4
bottomNavigationBar: BottomNavigationBar(
// 5
selectedItemColor:
Page 6 of 26
Theme.of(context).textSelectionTheme.selectionColor,
// 6
items: <BottomNavigationBarItem>[
const BottomNavigationBarItem(
icon: Icon(Icons.card_giftcard),
label: 'Card'),
const BottomNavigationBarItem(
icon: Icon(Icons.card_giftcard),
label: 'Card2'),
const BottomNavigationBarItem(
icon: Icon(Icons.card_giftcard),
label: 'Card3'),
]
)
Notes:
// 4 Defined a BottomNavigationBar.
// 5 Set the selection colour of an item when tapped.
// 6 Defined three bottom navigation tab bar items.
10. Now that you’ve set up the bottom navigation bar, you need to implement the navigation
between tab bar items. Before you can let the user switch between tab bar items, you need to
know which index they selected.
a) Go to home.dart. Add the following at the top of _HomeState, above the build()
method:
// 7
int _selectedIndex = 0;
// 8
static List<Widget> pages = <Widget>[
Container(color: Colors.red),
Container(color: Colors.green),
Page 7 of 26
Container(color: Colors.blue)
];
// 9
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
Notes:
// 7 _selectedIndex keeps track of which tab is currently selected. The underscore
in _selectedIndex signifies it’s private. The selected index is the state being
tracked by _HomeState.
// 8 Here, you define the widgets that will display on each tab. For now, when you tap
between the different tab bar items, it shows container widgets of different
colours. Soon, you’ll replace each of these with card widgets.
// 9 This function handles tapped tab bar items. Here, you set the index of the item
that the user pressed. setState() notifies the framework that the state of this
object has changed, then rebuilds this widget internally.
Notes
// 10 currentIndex will tell the bottom navigation bar which tab bar item to
highlight.
// 11 When the user taps on a tab bar item, it calls the _onItemTapped
handler, which updates the state with the correct index. In this case, it
changes the colour.
d) After all your updates your home.dart should look like his:
import 'package:flutter/material.dart';
// 1
class Home extends StatefulWidget {
const Home({Key? key}) : super(key: key);
@override
_HomeState createState() => _HomeState();
}
Page 8 of 26
class _HomeState extends State<Home> {
@override
// 7
int _selectedIndex = 0;
// 8
static List<Widget> pages = <Widget>[
Container(color: Colors.red),
Container(color: Colors.green),
Container(color: Colors.blue)
];
// 9
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
Page 9 of 26
11. Composing Card1: The Main Recipe Card.
a) In the lib directory, create a new file called card1.dart and add the following code to
it:
import 'package:flutter/material.dart';
// 2
@override
Widget build(BuildContext context) {
// 3
return Center(
child: Container(),
);
}
}
Notes:
// 1 Define string variables to display on the card. This is just sample data to help
build the card.
// 2 Every stateless widget comes with a build() method that you override.
// 3 Start with a Container laid out in the center.
Page 10 of 26
b) Open home.dart. In the pages List, replace the first Container with Card1() as
shown below:
static List<Widget> pages = <Widget>[
const Card1(),
Container(color: Colors.green),
Container(color: Colors.blue),
];
c) import 'card1.dart'; should appear at the top of the file in the import section. If it
didn’t, add it manually.
d) After the updates, hit Shift + R your apps going to look like the given image below.
e) Next is adding the image. Go to card1.dart. To add the image to Card1, replace the
empty Container() widget with the following:
Container(
// 1
padding: const EdgeInsets.all(16),
// 2
constraints: const BoxConstraints.expand(width: 350, height:
450),
// 3
decoration: const BoxDecoration(
// 4
image: DecorationImage(
// 5
image: AssetImage('assets/mag1.png'),
// 6
fit: BoxFit.cover,
),
// 7
borderRadius: BorderRadius.all(Radius.circular(10.0)),
Page 11 of 26
),
)
Notes:
// 1 Apply a padding of 16 on all sides of the box. Flutter units
are specified in logical pixels, which are like dp on
Android.
// 2 Constrain the container’s size to a width of 350 and a
height of 450.
// 3 Apply BoxDecoration. This describes how to draw a
box.
// 4 In BoxDecoration, set up DecorationImage, which
tells the box to paint an image.
// 5 Set which image to paint in the box using an AssetImage,
an image found in the starter project assets.
// 6 Cover the entire box with that image.
// 7 Apply a corner radius of 10 to all sides of the container.
h) You’re going to add three lines of text describing what the card does. Add the
following code to your card1.dart file:
import 'fooderlich_theme.dart';
i) Stack places these new widgets on top of each other. Add the following code as the
child of Container:
child: Stack(
children: [
Text(category, style:
FooderlichTheme.darkTextTheme.bodyText1),
Text(title, style:
FooderlichTheme.darkTextTheme.headline5),
Text(description, style:
FooderlichTheme.darkTextTheme.bodyText1),
Text(chef, style:
FooderlichTheme.darkTextTheme.bodyText1),
],
),
Page 12 of 26
j) Hot reload and see the changes on your apps.
k) The interface was not quite right because it’s not readable. Next, we need to position
the text properly. Replace the Stack with the following code:
Stack(
children: [
// 8
Text(category, style: FooderlichTheme.darkTextTheme.bodyText1,),
// 9
Positioned(
child: Text(
title,
style:
FooderlichTheme.darkTextTheme.headline2,),top: 20,),
// 10
Positioned(
child: Text(
description,
style:
FooderlichTheme.darkTextTheme.bodyText1,),
bottom: 30,
right: 0,),
// 11
Positioned(
child: Text(
chef, style:
FooderlichTheme.darkTextTheme.bodyText1,),
bottom: 10,
right: 0,)
],
),
Page 13 of 26
Notes:
For the relevant Text, you apply a Positioned widget. That
widget controls where you position the Text in the Stack. Here
are the positions you’re using:
// 8 The category, Editor’s Choice, stays where it is.
Remember, Container already applies a padding of 16 on
all sides.
// 9 You place the title 20 pixels from the top.
// 10 Here, you position the description 30 pixels from the
bottom and 0 to the right.
// 11 Finally, you position the chef’s name 10 pixels from the
bottom right.
Page 14 of 26
Notes:
// 1 The Center widget has a Container child widget which has three
properties, the first two being constraints and decoration.
// 2 The third property is child and it has a Column widget. A Column widget
displays its children vertically.
b) In home.dart, replace the second Container with Card2() so it looks like the following:
static List<Widget> pages = <Widget>[
const Card1(),
const Card2(),
Container(color: Colors.blue),
];
c) Import card2 as follows:
import 'card2.dart';
d) Perform hot reload. Tap on Card2 and see the changes as shown in the image below.
e) Next is to add circular author’s avatar. In the lib directory, create a new file called
circle_image.dart. Add the following code:
import 'package:flutter/material.dart';
// 2
final double imageRadius;
Page 15 of 26
final ImageProvider imageProvider;
@override
Widget build(BuildContext context) {
// 3
return CircleAvatar(
backgroundColor: Colors.white,
radius: imageRadius,
// 4
child: CircleAvatar(
radius: imageRadius - 5,
backgroundImage: imageProvider,
),
);
}
}
Notes:
// 1 CircleImage has two parameters: imageProvider and imageRadius.
// 2 The imageRadius and imageProvider property declarations.
// 3 CircleAvatar is a widget provided by the Material library. It’s defined as a
white circle with a radius of imageRadius.
// 4 Within the outer circle is another CircleAvatar, which is a smaller circle
that includes the user’s profile image. Making the inner circle smaller gives
you the white border effect.
f) To set up the author card widget, in the lib directory create new file named
author_card.dart. Add the following code:
import 'package:flutter/material.dart';
import 'fooderlich_theme.dart';
import 'circle_image.dart';
const AuthorCard({
Key? key,
required this.authorName,
required this.title,
required this.imageProvider,
}) : super(key: key);
// 2
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
child: Row(
children: [],
),
);
Page 16 of 26
}
}
Notes:
// 1 AuthorCard has three properties: authorName, the author’s job title
and the profile image, which imageProvider handles.
// 2 Remember that AuthorCard is grouped in a container and uses a Row
widget to lay out the other widgets horizontally.
g) To add AuthorCard widget to Card2, open card2dart and add the following import:
import 'author_card.dart';
h) Then, locate // TODO 1: add author information and replace it with the following:
const AuthorCard(
authorName: 'Mike Katz',
title: 'Smoothie Connoisseur',
imageProvider: AssetImage('assets/author_katz.jpeg')),
i) Open author_card.dart and replace return Container(...); in build() with the following:
return Container(
padding: const EdgeInsets.all(16),
child: Row(
// TODO 3: add alignment
children: [
// 1
Row(children: [
CircleImage(imageProvider: imageProvider,
imageRadius:28),
// 2
const SizedBox(width: 8),
// 3
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
authorName,
style:
FooderlichTheme.lightTextTheme.headline2,
),
Text(
title,
style:
FooderlichTheme.lightTextTheme.headline3,
)
],
),
]),
// TODO 2: add IconButton
],
),
);
j) Perform hot reload. Tap on Card2 and see the changes as shown in the image
below.
Page 17 of 26
Notes:
// 1 The inner Row groups the CircleImage and the author’s
Text information.
// 2 Applies 8 pixels of padding between the image and the text.
// 3 Lays out the author’s name and job title vertically using a
Column.
k) Next is to add IconButton widget. Locate // TODO 2: add IconButton and replace it
with the code below:
IconButton(
// 4
icon: const Icon(Icons.favorite_border),
iconSize: 30,
color: Colors.grey[400],
// 5
onPressed: () {
const snackBar = SnackBar(content: Text('Press Favorite'));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}),
l) Perform hot reload. Tap on Card2 then tap on heart shaped on the screen. You’ll get
output like the figure below:
Page 18 of 26
Notes:
// 4 Set the icon, size and color of the icon.
// 5 When the user presses the icon, display a snackbar.
Notes:
The outer Row widget applies a spaceBetween alignment. This adds extra space
evenly between the outer row’s children, placing the IconButton at the far right of the
screen.
n) Perform hot reload. Tap on Card2 then observe the extra space that has been added.
o) Last element to add on Card2 is text. Go to card2.dart and add the theme import:
import 'fooderlich_theme.dart';
p) Then locate // TODO 4: add Positioned text and replace it with the following:
// 1
Expanded(
// 2
child: Stack(
children: [
// 3
Positioned(
bottom: 16,
right: 16,
child: Text(
'Recipe',
style:
FooderlichTheme.lightTextTheme.headline1,
Page 19 of 26
),
),
// 4
Positioned(
bottom: 70,
left: 16,
child: RotatedBox(
quarterTurns: 3,
child: Text(
'Smoothies',
style:
FooderlichTheme.lightTextTheme.headline1,
),
),
),
],
),
),
q) Perform hot reload. Tap on Card2 you’ll get output as shown in figure below.
Notes:
// 1 With Expanded, you fill in the remaining available space.
// 2 Apply a Stack widget to position the texts on top of each
other.
// 3 Position the first text 16 pixels from the bottom and 16
pixels from the right.
// 4 Finally, position the second text 70 pixels from the
bottom and 16 pixels from the left. Also apply a
RotatedBox widget, which rotates the text clockwise
three quarterTurns. This makes it appear vertical.
Page 20 of 26
13. Composing Card3: The Explore Card. This card lets the user explore trends to find the recipes
they want to try.
a) In the lib directory, create a new file called card3.dart. Add the following code:
import 'package:flutter/material.dart';
import 'fooderlich_theme.dart';
b) In home.dart, replace the final Container with Card3() so it looks like the code shown
below:
static List<Widget> pages = <Widget>[
const Card1(),
const Card2(),
const Card3(),
];
c) Then, add import 'card3.dart'; in home.dart file.
d) Perform hot reload. Tap on Card3 and you’ll get the output like the figure in the next
page:
Page 21 of 26
e) Next, let’s compose a dark overlay image. This help in making white text stand out. In
card3.dart, locate // TODO 5: add dark overlay BoxDecoration add replace it with the
following code in the Stack:
Container(
decoration: BoxDecoration(
// 1
color: Colors.black.withOpacity(0.6),
// 2
borderRadius: const BorderRadius.all(Radius.circular(10.0)),
),
),
Notes:
// 1 You add a container with a colour overlay with a 60% semi-transparent
background to make the image appear darker.
// 2 This gives the appearance of rounded image corners.
f) Perform hot reload. Tap on Card3 to see how your image turn into semi-transparent
background.
Page 22 of 26
g) Now for some header, we are going to add Recipe Trends text and icon. To do this,
replace // TODO 6: Add Container, Column, Icon and Text with:
Container(
// 3
padding: const EdgeInsets.all(16),
// 4
child: Column(
// 5
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 6
const Icon(Icons.book, color: Colors.white, size: 40),
// 7
const SizedBox(height: 8),
// 8
Text(
'Recipe Trends',
style: FooderlichTheme.darkTextTheme.headline2),
// 9
const SizedBox(height: 30),
],
),
),
h) Perform hot reload. Tap on Card3 to see your new added header.
Page 23 of 26
Notes:
// 3 Apply padding of 16 pixels on all sides.
// 4 Set up a child Column to lay out the widgets vertically.
// 5 Position all the widgets to the left of the column.
// 6 Add a book icon.
// 7 Apply an 8-pixel space vertically.
// 8 Add the text widget.
// 9 Apply a 30-pixel space vertically.
i) Next, composing the chips for recipe categories. Locate // TODO 7: Add Center
widget with Chip widget children and replace it with the following:
// 10
Center(
// 11
child: Wrap(
// 12
alignment: WrapAlignment.start,
// 13
spacing: 12,
// 14
children: [
Chip(
label: Text('Healthy',
style:
FooderlichTheme.darkTextTheme.bodyText1),
backgroundColor: Colors.black.withOpacity(0.7),
onDeleted: () {
print('delete');
},
),
Chip(
label: Text('Vegan',
style:
FooderlichTheme.darkTextTheme.bodyText1),
backgroundColor:Colors.black.withOpacity(0.7),
onDeleted: () {
print('delete');
},
),
Page 24 of 26
Chip(
label: Text('Carrots',
style:
FooderlichTheme.darkTextTheme.bodyText1),
backgroundColor:Colors.black.withOpacity(0.7),
),
],
),
),
Notes:
Page 25 of 26
k) Add more chips by duplicating the Chip()
code in (i). This gives you the chance to
see the Wrap layout widget in action, as
shown in this figure.
Page 26 of 26