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

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

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.

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

Defining a Theme Class

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

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;
}

Applying the Theme

In main.dart, import your predefined color themes:

import 'constants.dart';
ThemeMode themeMode = ThemeMode.light; // Manual theme toggle
ColorSelection colorSelected = ColorSelection.pink;
themeMode: themeMode,
theme: ThemeData(
  colorSchemeSeed: colorSelected.color,
  useMaterial3: true,
  brightness: Brightness.light,
),
darkTheme: ThemeData(
  colorSchemeSeed: colorSelected.color,
  useMaterial3: true,
  brightness: Brightness.dark,
),
runApp(const Yummy());

...

const Yummy({super.key});

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.

class Yummy extends StatefulWidget {
  ...

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

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

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];
  });
}

Creating Custom Buttons to Switch Color and Mode

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

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

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

Adding Action Buttons to the App Bar

In main.dart add the following imports:

import 'components/theme_button.dart';
import 'components/color_button.dart';
actions: [
  ThemeButton(
    changeThemeMode: changeThemeMode,
  ),
  ColorButton(
    changeColor: changeColor,
    colorSelected: colorSelected,
  ),
],

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.

Using Scaffold

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

Ufv fuh/ yberuqn siob bid Joxceln Ogoa wikq hur mapkj buy Lafhih Keb XpoesassOdpuefZopris

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';
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
    );
  }
}
import 'home.dart';
home: Home(
  changeTheme: changeThemeMode,
  changeColor: changeColor,
  colorSelected: colorSelected,
),
import 'components/theme_button.dart';
import 'components/color_button.dart';

Adding a BottomNavigationBar

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

int tab = 0;
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),
  ),
];
// 1
bottomNavigationBar: NavigationBar(
  // 2
  selectedIndex: tab,
  // 3
  onDestinationSelected: (index) {
    setState(() {
      tab = index;
    });
  },
  // 4
  destinations: appBarDestinations,
),

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)
];
body: IndexedStack(
  index: tab, 
  children: pages,
),

Creating Custom Cards

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

Composing Category Card

The first card you’ll compose looks like this:

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
  }
}
import 'components/category_card.dart';
import 'models/food_category.dart';
// 1
Center(
  // 2
  child: ConstrainedBox(
    constraints: const BoxConstraints(maxWidth: 300),
    // 3
    child: CategoryCard(category: categories[0]),),),

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);
// 1
return Card(
  // 2
  child: Column(
    mainAxisSize: MainAxisSize.min,
    children: [
      // TODO: Add Stack Widget
      // TODO: Add ListTile widget
    ],
  ),
);

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

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

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:

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
          ],
        ),
      ),
    );
  }
}
import 'components/post_card.dart';
import 'models/post.dart';
Center(child: Padding(
  padding: const EdgeInsets.all(16.0),
  child: PostCard(post: posts[0]),
),),

Adding the Child Widgets

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

CircleAvatar(
  radius: 25,
  backgroundImage: AssetImage(post.profileImageUrl),
),
const SizedBox(
  width: 16.0,
),
// 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,
      ),],),),

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.

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
        ],),);}}
import 'components/restaurant_landscape_card.dart';
import 'models/restaurant.dart';
// 1
Center(
  //2
  child: ConstrainedBox(
    constraints: const BoxConstraints(maxWidth: 400),
    // 3
    child: RestaurantLandscapeCard(
      restaurant: restaurants[0],),),),

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

runApp(Yummy());
runApp(const Yummy());
Yummy({super.key});
const Yummy({super.key});

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