Module 04: Widgets 101 — Building Your First Real UI

Dart & Widget Basics

Widgets — the LEGO bricks of Flutter

🎯

Teach: How to build Flutter UIs using StatelessWidget, MaterialApp, Scaffold, and core layout widgets. See: A real app take shape widget by widget, from an empty screen to a polished profile card. Feel: The satisfying click of widgets snapping together into something real and visible.


Everything Is a Widget (For Real This Time)

🎯

Teach: What "everything is a widget" really means now that you understand Dart classes. See: The two fundamental widget types — StatelessWidget and StatefulWidget — and their roles. Feel: The concept of widgets shifting from abstract slogan to concrete understanding.

In Module 00, we said "everything in Flutter is a widget." Now that you know Dart classes, constructors, and inheritance, let's see what that actually means in code.

A widget is just a Dart class that describes a piece of UI. It doesn't draw pixels directly — it describes what should be on screen, and Flutter's rendering engine handles the rest.

There are two fundamental types of widgets: - StatelessWidget — immutable, no internal state, just takes data and renders - StatefulWidget — has mutable state that can change over time (we'll cover this in a future module)

Today, we focus entirely on StatelessWidget. It's simpler, and you'll use it more often than you'd expect.

🎙️

Here's a mental model that helps: a StatelessWidget is like a recipe. You give it ingredients (parameters), and it always produces the same dish (UI). Same ingredients, same result, every time. It doesn't remember what it made yesterday. It doesn't change its mind mid-cook. Pure, predictable, reliable.


StatelessWidget: The Build Method

🎯

Teach: How to create a StatelessWidget with a build() method, const constructor, and final fields. See: A complete widget class dissected line by line. Feel: Confident that you can write a StatelessWidget from memory.

Every StatelessWidget has exactly one job: implement the build() method. This method receives a BuildContext (we'll explore that later) and returns a widget tree.

import 'package:flutter/material.dart';

class Greeting extends StatelessWidget {
  final String name;

  const Greeting({super.key, required this.name});

  @override
  Widget build(BuildContext context) {
    return Text('Hello, $name!');
  }
}

Let's dissect this:

  1. class Greeting extends StatelessWidget — Our widget IS a StatelessWidget
  2. final String name — Data the widget needs (always final in StatelessWidget)
  3. const Greeting({super.key, required this.name}) — Constructor with named params
  4. build(BuildContext context) — The one method you must implement
  5. return Text(...) — Returns the widget tree to display

The key Parameter

Every widget can accept a key. You'll almost always pass it through with super.key. Keys help Flutter efficiently update the widget tree when things change. For now, just include {super.key} in your constructors and don't worry about it — we'll cover keys in depth when we get to lists and state management.

The const Constructor

const Greeting({super.key, required this.name});

Making the constructor const tells Flutter: "If the parameters haven't changed, you can reuse the previous widget instance instead of creating a new one." This is a free performance optimization. Use const constructors whenever all fields are final (which they always should be in a StatelessWidget).

💡

Always make StatelessWidget constructors const and all fields final. This enables Flutter's performance optimizations and ensures your widget is truly stateless.

🎙️

Let's name the five-line ritual you'll write over and over. Extend StatelessWidget. Declare final fields for your inputs. Write a const constructor with super.key and required this.whatever. Override build and return a widget tree. Done. That's the heartbeat of Flutter UI code. Once this pattern is second nature, you stop thinking about it — it's just how you draw things on screen. And every time you add const to your constructor, you're telling Flutter "I promise nothing inside me changes," which unlocks real performance gains in the widget tree.


MaterialApp and Scaffold: The Foundation

🎯

Teach: What MaterialApp and Scaffold provide and why every Flutter app starts with them. See: How the app shell is structured layer by layer. Feel: Oriented — you know where the "frame" of your app comes from.

Every Flutter app needs a root widget. For Material Design apps (which is most apps), that's MaterialApp:

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My App',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const HomeScreen(),
    );
  }
}

MaterialApp provides: - Theme — colors, fonts, and styling for the entire app - Navigation — the ability to push/pop screens - Localization — language and text direction support - MediaQuery — screen size and orientation info

Scaffold: The Screen Frame

Scaffold is the skeleton of a single screen. It provides the standard Material Design layout structure:

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
      ),
      body: const Center(
        child: Text('Hello, World!'),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        child: const Icon(Icons.add),
      ),
    );
  }
}

Scaffold gives you: - appBar — the top bar with title and actions - body — the main content area (this is where your UI goes) - floatingActionButton — the circular action button - drawer — a side navigation panel - bottomNavigationBar — tabs at the bottom

🎙️

Think of MaterialApp as the building and Scaffold as a room. The building provides electricity, plumbing, and an address (theme, navigation, context). Each room has walls, a door, and maybe some furniture (app bar, body, FAB). You'll have one MaterialApp and many Scaffold screens.


The Core Widgets Toolkit

🎯

Teach: The essential widgets you will use daily — Text, Icon, Container, Column, Row, Padding, and SizedBox. See: Code examples for each widget showing common parameters and usage patterns. Feel: Equipped with a practical toolkit you can reach for immediately.

Now for the fun part — the widgets you'll use every single day. These are your bread and butter.

Text

Displays a string with optional styling:

Text('Hello, Flutter!')

Text(
  'Styled text',
  style: TextStyle(
    fontSize: 24,
    fontWeight: FontWeight.bold,
    color: Colors.blue,
    letterSpacing: 1.5,
  ),
)

// Multi-line with overflow handling
Text(
  'This is a very long text that might not fit on one line',
  maxLines: 2,
  overflow: TextOverflow.ellipsis,
)

Icon

Material Design icons built right in:

Icon(Icons.favorite)

Icon(
  Icons.star,
  size: 48,
  color: Colors.amber,
)

Flutter includes thousands of Material icons. Browse them at fonts.google.com/icons.

Container

The Swiss Army knife widget. It can hold a child, add padding/margin, set a background color, add borders, constrain its size, and more:

Container(
  width: 200,
  height: 100,
  padding: const EdgeInsets.all(16),
  margin: const EdgeInsets.symmetric(vertical: 8),
  decoration: BoxDecoration(
    color: Colors.blue.shade50,
    borderRadius: BorderRadius.circular(12),
    border: Border.all(color: Colors.blue, width: 2),
    boxShadow: [
      BoxShadow(
        color: Colors.black26,
        blurRadius: 4,
        offset: Offset(0, 2),
      ),
    ],
  ),
  child: const Text('Inside a container'),
)
🎙️

New Flutter developers tend to wrap everything in a Container. Resist that urge. If you only need padding, use Padding. If you only need a colored box, use ColoredBox. If you only need sizing, use SizedBox. Container is powerful but heavy — use it when you need multiple features at once (padding + color + border, for example).

Column and Row — Layout Workhorses

Column arranges children vertically. Row arranges them horizontally. They're the CSS Flexbox of Flutter.

Column(
  mainAxisAlignment: MainAxisAlignment.center,   // vertical alignment
  crossAxisAlignment: CrossAxisAlignment.start,  // horizontal alignment
  children: [
    Text('First'),
    Text('Second'),
    Text('Third'),
  ],
)

Row(
  mainAxisAlignment: MainAxisAlignment.spaceEvenly,  // horizontal
  crossAxisAlignment: CrossAxisAlignment.center,      // vertical
  children: [
    Icon(Icons.star),
    Icon(Icons.star),
    Icon(Icons.star),
  ],
)

MainAxisAlignment options: - start — pack children at the beginning - end — pack at the end - center — center them - spaceBetween — even space between children, none at edges - spaceEvenly — equal space everywhere - spaceAround — equal space around each child

CrossAxisAlignment options: - start, end, center — align perpendicular to main axis - stretch — stretch children to fill the cross axis

Padding

Adds space inside a widget's bounds:

Padding(
  padding: const EdgeInsets.all(16),
  child: Text('I have 16px of padding on all sides'),
)

Padding(
  padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
  child: Text('Different horizontal and vertical padding'),
)

Padding(
  padding: const EdgeInsets.only(left: 32, top: 8),
  child: Text('Padding only on specific sides'),
)

SizedBox

Forces a specific width and/or height. Also great for adding gaps in Column/Row:

SizedBox(
  width: 200,
  height: 50,
  child: Text('I am constrained to 200x50'),
)

// As a spacer in a Column:
Column(
  children: [
    Text('Above'),
    const SizedBox(height: 16),  // 16px gap
    Text('Below'),
  ],
)
💡

Use SizedBox for gaps between widgets in a Column or Row. It's cleaner and more performant than padding or margin hacks.


Widget Composition: Building Up

🎯

Teach: How to compose small widgets into larger, reusable custom widgets. See: A real UI built step by step from simple pieces. Feel: The composability that makes Flutter development fast and maintainable.

The power of Flutter isn't in any single widget — it's in how you compose them. Let's build a profile card step by step.

Step 1: A Simple InfoRow

class InfoRow extends StatelessWidget {
  final IconData icon;
  final String label;
  final String value;

  const InfoRow({
    super.key,
    required this.icon,
    required this.label,
    required this.value,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4),
      child: Row(
        children: [
          Icon(icon, size: 20, color: Colors.blue),
          const SizedBox(width: 12),
          Text(
            '$label: ',
            style: const TextStyle(
              fontWeight: FontWeight.bold,
              fontSize: 14,
            ),
          ),
          Expanded(
            child: Text(
              value,
              style: const TextStyle(fontSize: 14),
            ),
          ),
        ],
      ),
    );
  }
}

Step 2: The ProfileCard

Now compose InfoRow into a larger card:

class ProfileCard extends StatelessWidget {
  final String name;
  final String role;
  final String email;
  final String location;

  const ProfileCard({
    super.key,
    required this.name,
    required this.role,
    required this.email,
    required this.location,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 4,
      margin: const EdgeInsets.all(16),
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16),
      ),
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            // Avatar
            CircleAvatar(
              radius: 40,
              backgroundColor: Colors.blue.shade100,
              child: Text(
                name[0].toUpperCase(),
                style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
              ),
            ),
            const SizedBox(height: 12),

            // Name
            Text(
              name,
              style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 4),

            // Role chip
            Chip(
              label: Text(role),
              backgroundColor: Colors.blue.shade50,
            ),
            const SizedBox(height: 16),

            // Info rows
            InfoRow(icon: Icons.email, label: 'Email', value: email),
            InfoRow(icon: Icons.location_on, label: 'Location', value: location),
          ],
        ),
      ),
    );
  }
}

Step 3: Wire It All Up

import 'package:flutter/material.dart';

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

class ProfileApp extends StatelessWidget {
  const ProfileApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Profile Card',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const ProfileScreen(),
    );
  }
}

class ProfileScreen extends StatelessWidget {
  const ProfileScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('My Profile'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: const Center(
        child: ProfileCard(
          name: 'Campbell Reed',
          role: 'Flutter Intern',
          email: 'campbell@example.com',
          location: 'Portland, OR',
        ),
      ),
    );
  }
}
🎙️

Look at what just happened. We built InfoRow — a tiny, reusable widget. Then we used InfoRow inside ProfileCard. Then we used ProfileCard inside ProfileScreen. Then ProfileScreen inside ProfileApp. Each piece is small, understandable, and testable on its own. That's composition. That's the Flutter way.

The Expanded Widget

You saw Expanded in the InfoRow example. It's crucial for layout. Expanded tells a Row or Column: "Give this child all the remaining space."

Row(
  children: [
    Text('Label: '),          // Takes only the space it needs
    Expanded(
      child: Text('This text takes up all remaining space'),
    ),
  ],
)

Without Expanded, a long Text in a Row would overflow. Expanded constrains it to the available space.

// Three equal columns:
Row(
  children: [
    Expanded(child: Container(color: Colors.red, height: 50)),
    Expanded(child: Container(color: Colors.green, height: 50)),
    Expanded(child: Container(color: Colors.blue, height: 50)),
  ],
)

// Custom ratios with flex:
Row(
  children: [
    Expanded(flex: 2, child: Container(color: Colors.red, height: 50)),   // 2/4
    Expanded(flex: 1, child: Container(color: Colors.green, height: 50)), // 1/4
    Expanded(flex: 1, child: Container(color: Colors.blue, height: 50)),  // 1/4
  ],
)

There Are No Dumb Questions

🎯

Teach: Answers to common questions about widget choice, extraction, BuildContext, and app structure. See: Clear guidance on when to use Padding vs. Container, when to extract widgets, and more. Feel: Reassured that widget confusion is normal and the decision rules are simple.

Q: Why does Flutter have so many small widgets instead of a few powerful ones? A: Composition over configuration. Instead of one mega-widget with 50 parameters, Flutter gives you small, focused widgets that you combine. Padding does padding. Center does centering. Opacity does opacity. This makes each widget simple to understand and easy to test. It also means you only pay for what you use.

Q: What's the difference between Padding and Container with padding? A: Functionally, they're identical. But Padding is lighter — it only handles padding. Container creates additional objects under the hood even for features you're not using. Use Padding when padding is all you need.

Q: When should I extract a widget into its own class vs. keeping it inline? A: Extract when: (1) you'd reuse it in multiple places, (2) the widget tree is getting deeply nested and hard to read, or (3) you want to give a piece of UI a meaningful name. A good rule of thumb: if you're nesting more than 5-6 levels deep, it's time to extract.

Q: What's BuildContext and why does build() need it? A: BuildContext is a handle to the widget's position in the widget tree. It's how you access things like the current theme (Theme.of(context)), screen size (MediaQuery.of(context)), and navigation (Navigator.of(context)). Think of it as the widget's "address" in the tree. We'll use it more in later modules.

Q: Do I always need MaterialApp and Scaffold? A: MaterialApp is needed if you want Material Design theming, navigation, and other conveniences. There's also CupertinoApp for iOS-style design or WidgetsApp for barebones. Scaffold is just a convenient layout for screens — you can skip it and use any widget as your body, but you'd lose the app bar, FAB, and drawer support.

🎙️

A quick note before I forget — that "Padding versus Container" question gets asked in every Flutter class. The answer is always use the most specific widget. Padding if you only need padding. SizedBox if you only need sizing. ColoredBox if you only need a background color. Container is a Swiss Army knife that exists for when you need several of those things at once. Reaching for Container first is a common beginner habit — catch yourself doing it and ask "could I use something simpler?" Your code gets lighter and your intent becomes clearer.


Sharpen Your Pencil

🎯

Teach: How to build a complete ProfileCard app from scratch using widget composition. See: Your own Flutter app with custom widgets, layout, and styling running on screen. Feel: The satisfying payoff of four modules of Dart knowledge turning into a real, visible app.

🔄

Where this fits: This is it — your first real Flutter UI. Everything from Modules 01-03 (Dart basics, OOP, async) built up to this moment. From here, you'll add state, navigation, and data — but the foundation is widget composition.

Exercise: Build a ProfileCard App

Build the full ProfileCard app from this module. Here's your checklist:

  1. Create a new Flutter project: flutter create profile_card_app

  2. Create the InfoRow widget in its own file lib/widgets/info_row.dart:

  3. Takes IconData icon, String label, and String value
  4. Displays them in a Row with an icon, bold label, and value text
  5. Uses Expanded to prevent text overflow

  6. Create the ProfileCard widget in lib/widgets/profile_card.dart:

  7. Takes name, role, email, and location
  8. Displays a CircleAvatar with the first letter of the name
  9. Shows the name in large bold text
  10. Shows the role in a Chip
  11. Uses InfoRow for email and location

  12. Update lib/main.dart:

  13. Set up MaterialApp with a custom theme using ColorScheme.fromSeed
  14. Create a Scaffold with an AppBar
  15. Place your ProfileCard in the center of the body

  16. Stretch goals (if you want more practice):

  17. Add more InfoRow entries (phone number, website, GitHub)
  18. Add a second ProfileCard and display both in a Column with SingleChildScrollView
  19. Experiment with different BoxDecoration styles on a Container
  20. Try changing the MainAxisAlignment and CrossAxisAlignment of a Row and observe how children move
🎙️

When you run this app and see your ProfileCard on screen — styled, composed, and real — that's the moment Flutter clicks. You wrote classes (Module 02), you understand the structure (Module 00), and now you're seeing your code become pixels. This is just the beginning. Next up: state management, navigation, and dynamic data. But first, enjoy this. You built a Flutter app.


📝 Module Quiz

Test what you learned. 45 seconds per question. Passing score is set per module. Your best attempt is saved in your browser so you can track progress -- nothing is sent to a server.

What's Next?

🎯

Teach: What the next module covers — StatefulWidget and making UIs respond to user interaction. See: A preview of state management and dynamic, interactive Flutter apps. Feel: Eager to bring your static widgets to life with state and interactivity.

You've now built a complete static UI with StatelessWidgets. But apps aren't static — users tap buttons, data changes, screens update. In the next module, we'll tackle StatefulWidget and state management — how to make your UI respond to user interaction and data changes. That's where Flutter really comes alive.

🎙️

Before we move on, take a minute to sit with what just happened. You built a widget that composed smaller widgets. You wrapped it in a scaffold, inside a Material app, and ran it. That is literally how every Flutter app — at any scale — is structured. The next module adds state and interactivity on top of this foundation. If the widget composition we just did feels solid, the rest of the course is just filling in the spaces. We're not learning a hundred new concepts from scratch. We're adding to what you already know.

1 / 1