Chapters

Hide chapters

Flutter Apprentice

Fourth Edition · Flutter 3.16.9 · Dart 3.2.6 · Android Studio 2023.1.1

Section II: Everything’s a Widget

Section 2: 5 chapters
Show chapters Hide chapters

Section IV: Networking, Persistence & State

Section 4: 6 chapters
Show chapters Hide chapters

3. Basic Widgets
Written by Vincent Ngo

Dive into the world of Flutter, where everything is a widget! This chapter unveils three fundamental widget categories essential for:

  • Structure and navigation
  • Displaying information
  • Positioning widgets

By the end of the chapter, you’ll construct a social food app called Yummy. You’ll use various widgets to create three distinct tabs: Category, Post, and Restaurant.

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

Getting Started

Start by downloading this chapter’s project from the book materials repo https://github.com/kodecocodes/flta-materials.

Locate the projects folder and open starter. Navigate to pubspec.yaml and tap Pub get to get all your flutter dependencies.

Run the app, and you’ll see an app bar and a simple text:

lib/main.dart serves as the launchpad of any Flutter application. Open it, and you’ll see the following:

import 'package:flutter/material.dart';

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

class Yummy extends StatelessWidget {
  // TODO: Setup default theme

  // 2
  const Yummy({super.key});

  // TODO: Add changeTheme above here

  @override
  Widget build(BuildContext context) {
    const appTitle = 'Yummy';

    // TODO: Setup default theme

    //3
    return MaterialApp(
      title: appTitle,
      //debugShowCheckedModeBanner: false, // Uncomment to remove Debug banner
      
      // TODO: Add theme
      
      // TODO: Replace Scaffold with Home widget
      // 4
      home: Scaffold(
        appBar: AppBar(
          // TODO: Add action buttons
          elevation: 4.0,
          title: const Text(
            appTitle,
            style: TextStyle(fontSize: 24.0),
          ),
        ),
        body: const Center(
          child: Text(
            'You Hungry?😋',
            style: TextStyle(fontSize: 30.0),
          ),
        ),
      ),
    );
  }
}

Take a moment to explore what the code does:

  1. Widget Initialization: Every journey with Flutter commences with a widget. The runApp() function initializes the app by accepting the root widget, in this case, an instance of Yummy.
  2. Every widget must override the build() method.
  3. The Yummy widget starts by composing a MaterialApp widget to give it a Material Design system look and feel. See https://material.io for more details.
  4. Scaffold defines the app’s visual structure, containing an AppBar and a body for starts.

Styling Your App

Flutter, being cross-platform, supports Android’s Material Design and iOS’s Cupertino design systems.

Android uses the Material Design system, which you’d import like this:

import 'package:flutter/material.dart';

iOS uses the Cupertino system. Here’s how you’d import it:

import 'package:flutter/cupertino.dart';

Throughout this book, you’ll learn to use the Material Design system. You’ll find the look and feel quite customizable!

Note: Switching between Material and Cupertino is beyond the scope of this book. For more information about what these packages offer in terms of UI components, check out:

Now that you’ve settled on a design, you’ll set a theme for your app in the next section.

Defining a Theme Class

Spice up your app with a custom theme! With Material 3, theme management is streamlined, focusing on defining color variations.

Open lib/constants.dart and examine the code included in your starter project:

import 'package:flutter/material.dart';


enum ColorSelection {
  // 1
  deepPurple('Deep Purple', Colors.deepPurple),
  purple('Purple', Colors.purple),
  indigo('Indigo', Colors.indigo),
  blue('Blue', Colors.blue),
  teal('Teal', Colors.teal),
  green('Green', Colors.green),
  yellow('Yellow', Colors.yellow),
  orange('Orange', Colors.orange),
  deepOrange('Deep Orange', Colors.deepOrange),
  pink('Pink', Colors.pink);

  // 2
  const ColorSelection(
    this.label, 
    this.color,
  );

  final String label;
  final Color color;
}

ColorSelection enum enables users to select and customize the app’s appearance with:

  1. Structured color options. The name listed (e.g. Deep Purple) is what will be displayed.
  2. Each has a label and a color object.

Now, you’ll learn to apply the color themes to your app.

Applying the Theme

In main.dart, import your predefined color themes:

import 'constants.dart';

Locate // TODO: Setup default theme and replace it with the following code to establish your default theme mode and primary color, ignore the red squiggles:

ThemeMode themeMode = ThemeMode.light; // Manual theme toggle
ColorSelection colorSelected = ColorSelection.pink;

Next, locate the comment // TODO: Add theme and insert the subsequent code to apply your theme configurations:

themeMode: themeMode,
theme: ThemeData(
  colorSchemeSeed: colorSelected.color,
  useMaterial3: true,
  brightness: Brightness.light,
),
darkTheme: ThemeData(
  colorSchemeSeed: colorSelected.color,
  useMaterial3: true,
  brightness: Brightness.dark,
),

This code snippet sets the global theme mode. It defines both light and dark themes utilizing the color you previously specified, ensuring a cohesive and adaptive visual appearance across your app.

Since the theme can change, you need to remove const from the following two locations:

runApp(const Yummy());

...

const Yummy({super.key});

Save your changes and perform a hot restart.

Locate // Manual theme toggle and change light to dark to observe theme variations. Make sure you do a hot restart.

The two themes look like this:

Next, you’ll create a way to enable users to toggle between light and dark modes and select a custom color theme.

Switching Themes

To enable theme switching within your app, you need to manage state by converting the Yummy widget to a StatefulWidget. The good news is that instead of converting manually, you can just use a right-click menu shortcut to do it automatically.

Right-click the class name Yummy. Then click Show Context Actions from the menu that pops up:

Select Convert to StatefulWidget.

There are now two classes:

class Yummy extends StatefulWidget {
  ...

  @override
  State<Yummy> createState() => _YummyState_();
}

class _YummyState extends State<Yummy> {
  ...
  @override
  Widget build(BuildContext context) {
    ...
  }

A couple of things to notice in the code above:

  • The refactor converted Yummy from a StatelessWidget into a StatefulWidget. It added a createState() implementation.
  • The refactor also created the _YummyState state class. It stores mutable data that can change over the lifetime of the widget.

Don’t you love it when there’s an automatic way to save time? Next, you’re going to implement the theme state changes.

Implementing Theme State Changes

Within _YummyState class, locate // TODO: Add changeTheme above here and replace it with the following functions:

void changeThemeMode(bool useLightMode) {
  setState(() {
    // 1
    themeMode = useLightMode
      ? ThemeMode.light //
      : ThemeMode.dark;
  });
}

void changeColor(int value) {
  setState(() {
    // 2
    colorSelected = ColorSelection.values[value];
  });
}

Here’s how the code works:

  1. Update theme mode based on user selection.
  2. Update theme color based on user selection.

Calling these functions will update the theme or color of your app.

Now, you need to create custom buttons to update the theme.

Creating Custom Buttons to Switch Color and Mode

Now it’s time to create two buttons that will allow your users to:

  • Switch between light and dark mode.
  • Select the color theme of the entire app.

Creating a Theme Button

You’ll create a button to toggle between light and dark mode. In lib directory, create a new folder called components and create a new file called theme_button.dart in that directory, add the following code to it:

import 'package:flutter/material.dart';

class ThemeButton extends StatelessWidget {
  // 1
  const ThemeButton({
    Key? key,
    required this.changeThemeMode,
  }) : super(key: key);

  // 2
  final Function changeThemeMode;

  @override
  Widget build(BuildContext context) {
    // 3
    final isBright = Theme.of(context).brightness == Brightness.light;
    // 4
    return IconButton(
      icon: isBright
          ? const Icon(Icons.dark_mode_outlined) //
          : const Icon(Icons.light_mode_outlined),
      // 5
      onPressed: () => changeThemeMode(!isBright),
    );
  }
}

Take a moment to go over the code:

  1. The ThemeButton widget is initialized with a constructor requiring a function changeThemeMode parameter.
  2. changeThemeMode is a callback function passed as a parameter to be called when the user presses the button. This function notifies the parent widget about the brightness change, enabling it to adjust the theme accordingly.
  3. isBright is a Boolean that checks whether the current theme brightness is light.
  4. An IconButton widget that will display light or dark mode icon based on the isBright Boolean.
  5. IconButton, when pressed, toggles the theme brightness by invoking changeThemeMode.

Next, you’ll create a button for users to select their favorite color to apply the entire app theme.

Creating the Color Button

In lib/components directory, create a new file called color_button.dart and add the following code to it:

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

class ColorButton extends StatelessWidget {
  // 1
  const ColorButton({
    super.key,
    required this.changeColor,
    required this.colorSelected,
  });

  // 2
  final void Function(int) changeColor;
  final ColorSelection colorSelected;

  @override
  Widget build(BuildContext context) {
    // 3
    return PopupMenuButton(
      icon: Icon(
        Icons.opacity_outlined,
        color: Theme.of(context).colorScheme.onSurfaceVariant,
      ),
      // 4
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(10),
      ),
      // 5
      itemBuilder: (context) {
        // 6
        return List.generate(
          ColorSelection.values.length,
          (index) {
            final currentColor = ColorSelection.values[index];
            // 7
            return PopupMenuItem(
              value: index,
              enabled: currentColor != colorSelected,
              child: Wrap(
                children: [
                  Padding(
                    padding: const EdgeInsets.only(left: 10),
                    child: Icon(
                      Icons.opacity_outlined,
                      color: currentColor.color,
                    ),
                  ),
                  Padding(
                    padding: const EdgeInsets.only(left: 20),
                    child: Text(currentColor.label),
                  ),],),);},);},
      // 8
      onSelected: changeColor,
    );
  }
}

Take a moment to go over the code:

  1. Initializes ColorButton with the required callback and color.
  2. Property changeColor is a callback to handle the color selection, and colorSelected is the currently selected color.
  3. Creates a button that displays a menu.
  4. Applies rounded corners to the popup menu.
  5. Generates the menu items.
  6. Creates a list of color options from ColorSelection.
  7. Configures each menu item with an icon and text.
  8. Calls changeColor when an item is selected.

Now that you’ve created the buttons, it’s time to add them to your app.

Adding Action Buttons to the App Bar

In main.dart add the following imports:

import 'components/theme_button.dart';
import 'components/color_button.dart';

Next, locate // TODO: Add action buttons and replace it with the following code:

actions: [
  ThemeButton(
    changeThemeMode: changeThemeMode,
  ),
  ColorButton(
    changeColor: changeColor,
    colorSelected: colorSelected,
  ),
],

With a hot restart, you should see the two new buttons on the top right. Try to switch between light and dark mode and change the color theme.

Next, you’ll learn about an important aspect of building an app — understanding which app structure to use.

Understanding App Structure and 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.

Yummy uses the Scaffold widget for its starting app structure. Scaffold is one of the most commonly used Material widgets in Flutter. Next, you’ll learn how to implement it in your app.

Using Scaffold

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

  • AppBar
  • BottomSheet
  • BottomNavigationBar
  • Drawer
  • FloatingActionButton
  • SnackBar

Scaffold has a lot of functionality out of the box!

The following diagram represents some of the previously mentioned items as well as showing left and right nav options:

App bar/ primary tool bar Content Area left nav right nav Bottom Bar FloatingActionButton

For more information, check out Flutter’s documentation on Material Components widgets, including app structure and navigation: https://flutter.dev/docs/development/ui/widgets/material

Now, it’s time to add more functionality.

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.

To avoid making your code overly complicated, you’ll create the first of these separate files now.

Your next step is to move code out of main.dart into a new StatefulWidget named Home.

In the lib directory, create a new file called home.dart and add the following:

import 'package:flutter/material.dart';
import 'components/theme_button.dart';
import 'components/color_button.dart';
import 'constants.dart';

class Home extends StatefulWidget {
  const Home({
    super.key,
    required this.changeTheme,
    required this.changeColor,
    required this.colorSelected,
  });

  final void Function(bool useLightMode) changeTheme;
  final void Function(int value) changeColor;
  final ColorSelection colorSelected;

  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  // TODO: Track current tab

  // TODO: Define tab bar destinations

  @override
  Widget build(BuildContext context) {
    // TODO: Define pages

    return Scaffold(
      appBar: AppBar(
        elevation: 4.0,
        backgroundColor: Theme.of(context).colorScheme.background,
        actions: [
          ThemeButton(
            changeThemeMode: widget.changeTheme,
          ),
          ColorButton(
            changeColor: widget.changeColor,
            colorSelected: widget.colorSelected,
          ),
        ],
      ),
      // TODO: Switch between pages
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Text(
            'You Hungry?😋',
            style: Theme.of(context).textTheme.displayLarge,
          ),
      ),
      // TODO: Add bottom navigation bar
    );
  }
}

You simply copied the main.dart Scaffold into a new widget in home.dart.

Note: Remember if you see your widget tree starting to get too big, it’s a good idea to break it apart into separate widgets.

Go back to main.dart to update it so it can use the new Home widget. At the top, add the following import statement:

import 'home.dart';

Next, locate TODO: Apply Home widget and replace it and the whole home: Scaffold(...), with the following:

home: Home(
  changeTheme: changeThemeMode,
  changeColor: changeColor,
  colorSelected: colorSelected,
),

Finally, remove the following imports in main.dart:

import 'components/theme_button.dart';
import 'components/color_button.dart';

These aren’t used anymore. With that done, you’ll move on to addressing Scaffold’s bottom navigation.

Adding a BottomNavigationBar

Open home.dart, locate // TODO: Track current tab and replace it with the following:

int tab = 0;

tab property will be used to keep track of the current tab the user is on.

Your next step is to define a list of tabs the user can navigate between. Locate // TODO: Define tab bar destinations and replace it with the following code:

List<NavigationDestination> appBarDestinations = const [
  NavigationDestination(
    icon: Icon(Icons.credit_card),
    label: 'Category',
    selectedIcon: Icon(Icons.credit_card),
  ),
  NavigationDestination(
    icon: Icon(Icons.credit_card),
    label: 'Post',
    selectedIcon: Icon(Icons.credit_card),
  ),
  NavigationDestination(
    icon: Icon(Icons.credit_card),
    label: 'Restaurant',
    selectedIcon: Icon(Icons.credit_card),
  ),
];

You’ll have a total of three tabs where you’ll create three distinct card widgets.

Finally, locate the comment // TODO: Add bottom navigation bar and replace it with the following:

// 1
bottomNavigationBar: NavigationBar(
  // 2
  selectedIndex: tab,
  // 3
  onDestinationSelected: (index) {
    setState(() {
      tab = index;
    });
  },
  // 4
  destinations: appBarDestinations,
),

Here’s how the code works:

  1. Assigns NavigationBar to bottomNavigationBar.
  2. Sets the active tab using selectedIndex.
  3. Updates the active tab on user selection.
  4. Defines the list of tabs with appBarDestinations.

With that complete, your app should look like this:

Now that you’ve set up the bottom navigation bar, you need to implement the navigation between pages.

Navigating Between Pages

To navigate between pages, you first need to define the list of pages the user may potentially navigate to. Still in home.dart, locate the comment // TODO: Define pages and replace it with the following:

final pages = [
  // TODO: Replace with Category Card
  Container(color: Colors.red),
  // TODO: Replace with Post Card
  Container(color: Colors.green),
  // TODO: Replace with Restaurant Landscape Card
  Container(color: Colors.blue)
];

This contains a list of containers with different colors. You’ll replace each one with a unique card soon.

Next, locate the comment // TODO: Switch between pages and replace it and all the body: Padding(...) code with the following:

body: IndexedStack(
  index: tab, 
  children: pages,
),

IndexedStack stacks and displays one widget from pages based on the tab index, preserving the state of all widgets in the stack.

After restarting, your app will look different for each tab item, like this:

Now that you’ve set up your tab navigation, it’s time to create beautiful cards!

Creating Custom Cards

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

Note: To help construct these cards, the models folder already contains models and mock data for each model to use to display in your custom widgets. Have a look at food_category.dart, post.dart, and restaurant.dart to learn more!

Display widgets handle what the user sees onscreen. Examples of display widgets include:

  • Text
  • Image
  • Button

Layout widgets help with the arrangement of widgets. Examples of layout widgets include:

  • Container
  • Padding
  • Stack
  • Column
  • SizedBox
  • Row

Note: Flutter has a plethora of layout widgets to choose from, but this chapter only covers the most common. For more examples, check out https://flutter.dev/docs/development/ui/widgets/layout.

Composing Category Card

The first card you’ll compose looks like this:

CategoryCard is composed of the following widgets:

  • Card: A material design container that holds content and actions about a single subject.
  • Column: Vertically arranges its widget children, here, a StackandListTile.
  • Stack: Overlays multiple widgets, here used to layer text over an image.
    • ClipRRect: Applies rounded corners to its child, an Image.asset widget in this case.
    • Positioned: Places its child, Text widgets, within the Stack.
    • RotatedBox: Rotates a Text widget by the value you pass to it.
  • ListTile: A widget that contains title and subtitle text.

In the lib/components directory, create a new file called category_card.dart and add the following code to it:

import 'package:flutter/material.dart';
import '../models/food_category.dart';

class CategoryCard extends StatelessWidget {
  // 1
  final FoodCategory category;

  const CategoryCard({
    super.key,
    required this.category,
  });

  @override
  Widget build(BuildContext context) {
    // TODO: Get text theme

    // TODO: Replace with Card widget
    return Container(); // 2
  }
}

Here you set up the basic structure for the CategoryCard widget.

  1. It simply takes in a FoodCategory object, which you’ll use later to display data in your UI.
  2. Returns an empty Container.

Next, open home.dart and add the following import:

import 'components/category_card.dart';
import 'models/food_category.dart';

Locate // TODO: Replace with Category Card and replace the first container with the following:

// 1
Center(
  // 2
  child: ConstrainedBox(
    constraints: const BoxConstraints(maxWidth: 300),
    // 3
    child: CategoryCard(category: categories[0]),),),

Take a moment to go over how CategoryCard is laid out on the page.

  1. Center widget ensures the card widget is centered on the screen.
  2. Applies a maximum width of 300 pixels to the card widget.
  3. Set CategoryCard widget as the child, and pass the first mock category to be displayed.

You’ve now set up CategoryCard. Hot restart and the app will currently look like this:

It’s a little bland, isn’t it? For your next step, you’ll spice it up with an image.

Constructing the Widget

Switch to category_card.dart. Locate // TODO: Get text theme and replace it with the following:

final textTheme = Theme.of(context)
    .textTheme
    .apply(displayColor: Theme.of(context).colorScheme.onSurface);

Here, you get the text theme, which you’ll later use to apply to text widgets.

Next, locate // TODO: Replace with Card widget and replace it and the code below with the following:

// 1
return Card(
  // 2
  child: Column(
    mainAxisSize: MainAxisSize.min,
    children: [
      // TODO: Add Stack Widget
      // TODO: Add ListTile widget
    ],
  ),
);

You replace the Container widget with a Card widget, and within the card, you use a Column widget to vertically arrange the child widgets.

Adding Stacked Elements to the Card

Adding the first widget to the column. Locate // TODO: Add Stack Widget and replace it with the following:

Stack(
  children: [
    // 1
    ClipRRect(
      borderRadius: const BorderRadius.vertical(
        top: Radius.circular(8.0)),
      child: Image.asset(category.imageUrl),
    ),
    // 2
    Positioned(
      left: 16.0,
      top: 16.0,
      child: Text(
        'Yummy',
        style: textTheme.headlineLarge,
      ),
    ),
    // 3
    Positioned(
      bottom: 16.0,
      right: 16.0,
      child: RotatedBox(
        quarterTurns: 1,
        child: Text(
          'Smoothies',
          style: textTheme.headlineLarge,
        ),
      ),
    ),
  ],
),

Recall that the Stack widget allows you to overlay widgets on top of each other. Here’s what’s overlayed in the stack:

  1. Add a ClipRRect widget, which clips the image with rounded corners at the top.
  2. Position the text “Yummy” on the top-left.
  3. Rotate the text “Smoothies” 90 degrees and place it at the bottom-right.

After a hot restart, the CategoryCard now look like this:

Adding a Footer to the Card

Adding the second widget to the column. Locate // TODO: Add ListTile widget and replace it with the following:

ListTile(
  // 1
  title: Text(
      category.name,
      style: textTheme.titleSmall,),
  // 2
  subtitle: Text(
      '${category.numberOfRestaurants} places',
      style: textTheme.bodySmall,),),

Take a moment to go over the code:

  1. Display the category name with a smaller title style.
  2. Display the number of restaurants in a small body text style

After these updates, the final CategoryCard looks like this:

Great, you finished the first card! It’s time to move on to the next!

Tip: Leverage Material 3’s typography text theme for consistent text styles across your app, avoiding hardcoded font sizes and colors.

Composing Post Card

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

PostCard is composed of the following widgets:

  • Card: Provides a material design card that can hold related pieces of information or content.
  • Padding: Adds a uniform padding of 16.0 pixels around the content inside it to provide some spacing.
  • Row: Arranges its children widgets in a horizontal line. CircleAvatar: Displays the profile image in a circular shape.
    • SizedBox: Provides a horizontal spacing of 16.0 pixels between the avatar and the text.
    • Expanded: Takes the remaining space in the row to avoid overflow and ensures the child widget utilizes the available horizontal space.
      • Column: Arranges its children widgets in a vertical line.
        • Text (Comment): Displays the post comment, limiting it to 2 lines and truncating any overflow.
        • Text (Timestamp): Displays how long ago the post was made.

This structure ensures a clean, organized layout where the user’s avatar is displayed alongside their post content, with the post comment and timestamp neatly presented below it.

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

import 'package:flutter/material.dart';
import '../models/post.dart';

class PostCard extends StatelessWidget {
  final Post post;

  const PostCard({
    super.key,
    required this.post,
  });

  @override
  Widget build(BuildContext context) {
    final textTheme = Theme.of(context)
        .textTheme
        .apply(
          displayColor: Theme.of(context).colorScheme.onSurface,
        );

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // TODO: Add CircleAvatar
            // TODO: Add spacing
            // TODO: Add Expanded Widget
          ],
        ),
      ),
    );
  }
}

Here, you set up the basic structure for the PostCard widget. It simply takes in a Post object, which you’ll use later to display data in your UI.

Next, open home.dart and add the following import:

import 'components/post_card.dart';
import 'models/post.dart';

Locate // TODO: Replace with Post Card and replace the container beneath it with the following:

Center(child: Padding(
  padding: const EdgeInsets.all(16.0),
  child: PostCard(post: posts[0]),
),),

Then, perform a hot restart.

Tap the Post tab bar item. Your app should look like this:

Adding the Child Widgets

Here’s how PostCard’s layout will look after you’ve added the Row’s children widgets:

In post_card.dart locate // TODO: Add CircleAvatar, and replace it with the following:

CircleAvatar(
  radius: 25,
  backgroundImage: AssetImage(post.profileImageUrl),
),

CircleAvatar is often used to display a profile image or user’s avatar in a circular shape.

Next, locate // TODO: Add spacing and replace it with the following:

const SizedBox(
  width: 16.0,
),

Add 16-pixel padding between the two widgets.

Finally, locate // TODO: Add Expanded Widget and replace it with the following:

// 1
Expanded(
  // 2
  child: Column(
    mainAxisSize: MainAxisSize.min,
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      // 3
      Text(
        post.comment,
        maxLines: 2,
        overflow: TextOverflow.ellipsis,
        style: textTheme.titleMedium),
      Text(
        '${post.timestamp} mins ago',
        style: textTheme.bodySmall,
      ),],),),

Here’s what you’ve added:

  1. Expanded widget makes the child occupy all available space.
  2. Column widget vertically stacks children. MainAxisSize.min aligns them to occupy minimum space. CrossAxisAlignment.start horizontally aligns the child widgets to the left side.
  3. Display two Text widgets, the post contents followed by the post’s timestamp.

After a hot restart, your PostCard widget should look like this:

And that’s all you need to do for the post card. Next, you’ll move on to the final one.

Composing Restaurant Landscape Card

RestaurantLandscapeCard is the last card you’ll create for this chapter. This card lets the user explore popular restaurant trends and order food.

The following widgets compose RestaurantLandscapeCard:

  • Card: A material design card that contains related pieces of information.
  • Column: Arranges its children widgets in a vertical line.
  • ClipRRect: Clips its child with a rounded rectangle border.
  • AspectRatio: Constrains the child’s aspect ratio.
  • Image: Displays the restaurant’s image, covering the available space.
  • ListTile: A widget that contains title and subtitle text.

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

import 'package:flutter/material.dart';

import '../models/restaurant.dart';

class RestaurantLandscapeCard extends StatelessWidget {
  final Restaurant restaurant;

  const RestaurantLandscapeCard({
    super.key,
    required this.restaurant,
  });

  @override
  Widget build(BuildContext context) {
    final textTheme = Theme.of(context)
        .textTheme
        .apply(
          displayColor: Theme.of(context)
          .colorScheme
          .onSurface);
    return Card(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          // TODO: Add Image
          // TODO: Add ListTile
        ],),);}}

Here, you set up the basic structure for the RestaurantLandscapeCard widget. It simply takes in an instance of Restaurant, which you’ll use later to display data in your UI.

Next, open home.dart and add the following import:

import 'components/restaurant_landscape_card.dart';
import 'models/restaurant.dart';

Locate // TODO: Replace with Restaurant Landscape Card and replace the container beneath it with the following:

// 1
Center(
  //2
  child: ConstrainedBox(
    constraints: const BoxConstraints(maxWidth: 400),
    // 3
    child: RestaurantLandscapeCard(
      restaurant: restaurants[0],),),),

Take a moment to go over how RestaurantLandscapeCard is laid out on the page:

  1. Center widget ensures the card widget is centered on the screen.
  2. Applies a maximum width of 400 pixels to the card widget.
  3. Set RestaurantLandscapeCard widget as the child, and pass the first mock restaurant to be displayed.

Now you’ve set up RestaurantLandscapeCard, perform a hot restart. Tap the Restaurant tab bar item. Your app should look like this:

Composing Restaurant’s Child Widgets

Open restaurant_landscape_card.dart, locate // TODO: Add Image and replace it with the following:

ClipRRect(
  // 1
  borderRadius:
      const BorderRadius.vertical(top: Radius.circular(8.0),),
  // 2
  child: AspectRatio(
      aspectRatio: 2,
      child: Image.asset(restaurant.imageUrl, fit: BoxFit.cover,),),),

Here is how the image is formed:

  1. borderRadius rounds the top corners with an 8.0 unit radius.
  2. AspectRatio displays an image with a 2:1 width-to-height ratio. The image scales to fit its container.

Next, locate // TODO: Add ListTile and replace it with the following:

ListTile(
  // 1
  title: Text(restaurant.name, style: textTheme.titleSmall,),
  // 2
  subtitle: Text(restaurant.attributes,
      maxLines: 1, style: textTheme.bodySmall,),
  // 3
  onTap: () {
    // ignore: avoid_print
    print('Tap on ${restaurant.name}');
  },),

The code represents a ListTile in Flutter:

  1. title shows the restaurant’s name with a specific style.
  2. subtitle displays the restaurant’s attributes, truncated if more than one line.
  3. onTap prints the restaurant’s name to the console when tapped.

Save your changes and hot restart. Now, your card looks like this:

Now that Yummy is a StatefulWidget you can add back the const declarations. Open main.dart and change:

runApp(Yummy());

to

runApp(const Yummy());

Then change:

Yummy({super.key});

to

const Yummy({super.key});

When you finish your app, there is one last step - remove the Debug label.

Still in main.dart find // Uncomment to remove Debug banner and remove the // at the beginning of the line. Hot restart your app, and the banner is gone.

You did it! You’ve finished this chapter. Along the way, you’ve applied three different categories of widgets. You learned how to use structural widgets to organize different screens, and you created three custom cards and applied different widget layouts to each.

Well done!

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.

Fortunately, the Flutter team created a Widget UI component library that shows how each widget works! Check it out here: https://gallery.flutter.dev/

In this chapter, you got started right off with using widgets to build a nice user interface. In the next chapter, you’ll dive into the theory of widgets to help you better understand how to use them.

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.
© 2026 Kodeco Inc.