Chapters

Hide chapters

Flutter Apprentice

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

Section III: Navigating Between Screens

Section 3: 3 chapters
Show chapters Hide chapters

Section IV: Networking, Persistence and State

Section 4: 7 chapters
Show chapters Hide chapters

Appendices

Section 6: 2 chapters
Show chapters Hide chapters

3. Basic Widgets
Written by Vincent Ngo

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

As you know, everything in Flutter is a widget. But how do you know which widget to use when? In this chapter, you’ll explore three categories of basic widgets, which you can use for:

  • Structure and navigation
  • Displaying information
  • Positioning widgets

By the end of this chapter, you’ll use those different types of widgets to build the foundation of an app called Fooderlich, a social recipe app. You’ll build out the app’s structure and learn how to create three different recipe cards: the main recipe card, an author card and an explore card.

Ready? Dive in by taking a look at the starter project.

Getting started

Start by downloading the https://github.com/raywenderlich/flta-materials from the book materials repo.

Locate the projects folder and open starter. If your IDE has a banner that reads ‘Pub get’ has not been run, click Get dependencies to resolve the issue.

Run the app from Android Studio and you’ll see an app bar and some simple text:

main.dart is the starting point for any Flutter app. Open it and you’ll see the following:

void main() {
  // 1
  runApp(const Fooderlich());
}

class Fooderlich extends StatelessWidget {
  const Fooderlich({Key key}) : super(key: key);
  // 2
  @override
  Widget build(BuildContext context) {
    // 3
    return MaterialApp(
      title: 'Fooderlich',
      // 4
      home: Scaffold(
        // 5
        appBar: AppBar(title: const Text('Fooderlich')),
        body: const Center(child: Text('Let\'s get cooking 👩‍🍳')),
      ),
    );
  }
}

Take a moment to explore what the code does:

  1. Everything in Flutter starts with a widget. runApp takes in the root widget Fooderlich.
  2. Every widget must override a 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.

Styling your app

Since Flutter is cross-platform, it’s only natural for Google’s UI Toolkit to support the visual design systems of both Android and iOS.

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

Setting a theme

You might notice the current app looks a little boring with the default blue, so you’ll spice it up with a custom theme! Your first step is to select the font for your app to use.

Using Google fonts

Thegoogle_fonts package supports over 977 fonts to help you style your text. It’s already included lib/pubspec.yaml and you have already added it to the app when clicking Pub Get before. You’ll use this package to apply a custom font to your theme class.

Defining a theme class

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),
  );

  // 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,
        textSelectionColor: Colors.green,
        textTheme: lightTextTheme,
    );
  }

  // 4
  static ThemeData dark() {
    return ThemeData(
        brightness: Brightness.dark,
        primaryColor: Colors.grey[900],
        accentColor: Colors.green[600],
        textTheme: darkTextTheme,
    );
  }
}

Using the theme

In main.dart, import your theme by adding the following beneath the existing import statement:

import 'fooderlich_theme.dart';
final theme = FooderlichTheme.dark();
theme: theme,
appBar: AppBar(title: Text('Fooderlich',
            style: theme.textTheme.headline6),),
body: Center(child: Text('Let\'s get cooking 👩‍🍳',
              style: theme.textTheme.headline1),),
// 1
import 'fooderlich_theme.dart';

void main() {
  runApp(const Fooderlich());
}

class Fooderlich extends StatelessWidget {
  const Fooderlich({Key key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    // 2
    final theme = FooderlichTheme.dark();
    return MaterialApp(
      // 3
      theme: theme,
      title: 'Fooderlich',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Fooderlich',
              // 4
              style: theme.textTheme.headline6),
        ),
        body: Center(
          child: Text('Let\'s get cooking 👩‍🍳',
              // 5
              style: theme.textTheme.headline1),
        ),
      ),
    );
  }
}

App structure & navigation

Establishing your app’s structure from the beginning is important for the user experience. Applying the right navigation structure makes it easy for your users to navigate the information in your app.

Implementing Scaffold

The Scaffold widget implements all your basic visual layout structure needs. It’s composed of the following parts:

Setting up the Home widget

As you build large-scale apps, you’ll start to compose a staircase of widgets. Widgets composed of other widgets can get really long and messy. It’s a good idea to break your widgets into separate files for readability.

import 'package:flutter/material.dart';

// 1
class Home extends StatefulWidget {
  const Home({Key key}) : super(key: key);

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

class _HomeState extends State<Home> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(
          'Fooderlich',
          // 2
          style: Theme.of(context).textTheme.headline6)),
      body: Center(
        child: Text(
          'Let\'s get cooking 👩‍🍳',
          // 3
          style: Theme.of(context).textTheme.headline1)),
    );
  }
}
import 'home.dart';
return MaterialApp(
  theme: theme,
  title: 'Fooderlich',
  home: const Home(),
);

Adding a BottomNavigationBar

Your next step is to add a bottom navigation bar to the scaffold. This will let your users navigate between cards.

// 4
bottomNavigationBar: BottomNavigationBar(
  // 5
  selectedItemColor: Theme.of(context).textSelectionColor,
  // 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'),
  ]
)

Navigating between items

Before you can let the user switch between tab bar items, you need to know which index they selected.

// 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;
  });
}
body: pages[_selectedIndex],

Indicating the selected tab bar item

Now, you want to indicate to the user which tab bar item they currently have selected.

// 10
currentIndex: _selectedIndex,
// 11
onTap: _onItemTapped,

Creating custom recipe cards

In this section, you’ll compose three recipe cards by combining a mixture of display and layout widgets.

Composing Card1: the main recipe card

The first card you’ll compose looks like this:

import 'package:flutter/material.dart';

class Card1 extends StatelessWidget {
  const Card1({Key key}) : super(key: key);
  // 1
  final String category = 'Editor\'s Choice';
  final String title = 'The Art of Dough';
  final String description = 'Learn to make the perfect bread.';
  final String chef = 'Ray Wenderlich';

  // 2
  @override
  Widget build(BuildContext context) {
    // 3
    return Center(
      child: Container(),
    );
  }
}
static List<Widget> pages = <Widget>[
    const Card1(),
    Container(color: Colors.green),
    Container(color: Colors.blue),
];

Adding the image

Switch 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)),
  ),
)

Adding the text

You’re going to add three lines of text describing what the card does. Start by adding the following import statement to the top of the card1.dart file so that you can use your Theme:

import 'fooderlich_theme.dart';
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),
        ],
       ),

Positioning the text

Replace the Stack with the following:

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,)
  ],
),

Composing Card2: the author card

It’s time to start composing the next card, the author card. Here’s how it will look by the time you’re done:

import 'package:flutter/material.dart';

class Card2 extends StatelessWidget {
  const Card2({Key key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Center(
      // 1
      child: Container(
        constraints: const BoxConstraints.expand(width: 350, height: 450),
        decoration: const BoxDecoration(
          image: DecorationImage(
            image: AssetImage('assets/mag5.png'),
            fit: BoxFit.cover,
          ),
          borderRadius: BorderRadius.all(Radius.circular(10.0)),
        ),
        // 2
        child: Column(
          children: [
            // TODO 1: add author information
            // TODO 4: add Positioned text
        ],
        ),
      ),
    );
  }
}

static List<Widget> pages = <Widget>[
    const Card1(),
    const Card2(),
    Container(color: Colors.blue),
];
import 'card2.dart';

Composing the author card

The following widgets make up the AuthorCard:

Creating a circular avatar widget

Your first step is to create the author’s circular avatar.

import 'package:flutter/material.dart';

class CircleImage extends StatelessWidget {
  // 1
  const CircleImage({
    Key key,
    this.imageProvider,
    this.imageRadius = 20,
  }) : super(key: key);

  // 2
  final double imageRadius;
  final ImageProvider imageProvider;

  @override
  Widget build(BuildContext context) {
    // 3
    return CircleAvatar(
      backgroundColor: Colors.white,
      radius: imageRadius,
      // 4
      child: CircleAvatar(
        radius: imageRadius - 5,
        backgroundImage: imageProvider,
      ),
    );
  }
}

Setting up the AuthorCard widget

In the lib directory, create a new file called author_card.dart. Add the following code:

import 'package:flutter/material.dart';
import 'fooderlich_theme.dart';
import 'circle_image.dart';

class AuthorCard extends StatelessWidget {
  // 1
  final String authorName;
  final String title;
  final ImageProvider imageProvider;

  const AuthorCard({
    Key key,
    this.authorName,
    this.title,
    this.imageProvider,
  }) : super(key: key);

  // 2
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(16),
      child: Row(
        children: [],
      ),
    );
  }
}

Adding the AuthorCard widget to Card2

Open card2.dart and add the following imports:

import 'author_card.dart';
const AuthorCard(
  authorName: 'Mike Katz',
  title: 'Smoothie Connoisseur',
  imageProvider: AssetImage('assets/author_katz.jpeg')),

Composing the AuthorCard widget

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
       ],
     ),
    );
import 'circle_image.dart';
import 'fooderlich_theme.dart';

Adding the IconButton widget

Next, you need to add the heart-shaped IconButton widget after the inner Row widget. The user will click this icon when they want to favorite a recipe.

IconButton(
  // 4
  icon: const Icon(Icons.favorite_border),
  iconSize: 30,
  color: Colors.grey[400],
  // 5
  onPressed: () {
    const snackBar = SnackBar(content: Text('Press Favorite'));
    Scaffold.of(context).showSnackBar(snackBar);
  }),

mainAxisAlignment: MainAxisAlignment.spaceBetween,

Composing the text

Return to card2.dart, and add the theme import:

import 'fooderlich_theme.dart';
// 1
Expanded(
  // 2
  child: Stack(
    children: [
      // 3
      Positioned(
        bottom: 16,
        right: 16,
        child: Text(
          'Recipe',
          style: FooderlichTheme.lightTextTheme.headline1,
          ),
      ),
      // 4
      Positioned(
        bottom: 70,
        left: 16,
        child: RotatedBox(
          quarterTurns: 3,
          child: Text(
            'Smoothies',
            style: FooderlichTheme.lightTextTheme.headline1,
          ),
        ),
      ),
      ],
    ),
  ),

Composing Card3: the explore card.

ExploreCard is the last card you’ll create for this chapter. This card lets the user explore trends to find the recipes they want to try.

import 'package:flutter/material.dart';

class Card3 extends StatelessWidget {
  const Card3({Key key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        constraints: const BoxConstraints.expand(width: 350, height: 450),
        decoration: const BoxDecoration(
          image: DecorationImage(image: AssetImage('assets/mag2.png'),
                                 fit: BoxFit.cover),
          borderRadius: BorderRadius.all(Radius.circular(10.0)),
        ),
        child: Stack(
          children: [
            // TODO 5: add dark overlay BoxDecoration
            // TODO 6: Add Container, Column, Icon and Text
            // TODO 7: Add Center widget with Chip widget children
          ],
        ),
      ),
    );
  }
}
static List<Widget> pages = <Widget>[
    const Card1(),
    const Card2(),
    const Card3(),
];
import 'explore_card.dart';

Composing the dark overlay

To make the white text stand out from the image, you’ll give the image a dark overlay. Just as you’ve done before, you’ll use Stack to overlay other widgets on top of the image.

Container(
  decoration: BoxDecoration(
    // 1
    color: Colors.black.withOpacity(0.6),
    // 2
    borderRadius: const BorderRadius.all(Radius.circular(10.0)),
  ),
),

Composing the header

The next thing you want to do is to add the 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),
    ],
  ),
),
import 'fooderlich_theme.dart';

Composing the chips

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');
          },
      ),
      Chip(
        label: Text('Carrots',
            style: FooderlichTheme.darkTextTheme.bodyText1),
        backgroundColor:Colors.black.withOpacity(0.7),
      ),
    ],
  ),
),

Key points

  • Three main categories of widgets are: structure and navigation; displaying information; and, positioning widgets.
  • There are two main visual design systems available in Flutter, Material and Cupertino. They help you build apps that look native on Android and iOS, respectively.
  • Using the Material theme, you can build quite varied user interface elements to give your app a custom look and feel.
  • It’s generally a good idea to establish a common theme object for your app, giving you a single source of truth for your app’s style.
  • The Scaffold widget implements all your basic visual layout structure needs.
  • The Container widget can be used to group other widgets together.
  • The Stack widget layers child widgets on top of each other.

Where to go from here?

There’s a wealth of Material Design widgets to play with, not to mention other types of widgets — too many to cover in a single chapter.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

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

Unlock now