Flutter: Material Design and Common Components

Material Design is Flutter's native language — master it and your app will have platform-consistent "native feel."

📋 Prerequisites: You need to have completed the following first

1. What You Will Learn


2. A Story from Design Specs to Code

(1) The Pain: Building UI Components from Scratch Is Time-Consuming

Bob received the ShopApp UI design specs and found that 80% of the components are standard Material Design: AppBar, Card, FAB, BottomSheet. If he hand-coded each one, it would take an estimated 3 weeks of development. Worse, his custom components didn't match the system's native style, and users complained the app "didn't look like a proper app."

(2) The Material Component Library Solution

Flutter includes a complete Material Design component library — use standard components with a single line of code, with built-in animations, accessibility, and theme adaptation.

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

// 5 lines = full Material page
Scaffold(
  appBar: AppBar(title: const Text('ShopApp')),
  body: const ProductList(),
  floatingActionButton: FloatingActionButton(onPressed: () {}, child: const Icon(Icons.add)),
  bottomNavigationBar: BottomNavigationBar(items: [...]),
)
TEXT
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.

(3) The Result: 3x Development Speed Improvement

After using Material components, the detail page went from 3 days to 1 day, with automatic Light/Dark theme and accessibility mode adaptation.


3. Scaffold

Scaffold is the foundational framework for Material Design pages, providing a standardized page structure.

100%
graph TD
    S[Scaffold] --> A[AppBar]
    S --> B[Body]
    S --> F[FAB]
    S --> BN[BottomNavigationBar]
    S --> D[Drawer]
    B --> C[Card]
    B --> LT[ListTile]
    B --> GV[GridView]
TEXT
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.

(1) Scaffold Components

Property Type Description
appBar PreferredSizeWidget Top navigation bar
body Widget Page body
floatingActionButton Widget Bottom-right floating button
bottomNavigationBar Widget Bottom navigation bar
drawer Widget Left drawer menu
bottomSheet Widget Bottom popup panel
snackBar SnackBar Bottom notification bar

▶ Example

TEXT
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.

: ShopApp home page Scaffold

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

Scaffold(
  appBar: AppBar(
    title: const Text('ShopApp'),
    actions: [
      IconButton(icon: const Icon(Icons.search), onPressed: () {}),
      IconButton(icon: const Icon(Icons.shopping_cart), onPressed: () {}),
    ],
  ),
  body: const ProductGrid(),
  floatingActionButton: FloatingActionButton.extended(
    onPressed: () {},
    icon: const Icon(Icons.flash_on),
    label: const Text('Flash Sale'),
  ),
  bottomNavigationBar: NavigationBar(
    destinations: const [
      NavigationDestination(icon: Icon(Icons.home), label: 'Home'),
      NavigationDestination(icon: Icon(Icons.category), label: 'Categories'),
      NavigationDestination(icon: Icon(Icons.favorite), label: 'Wishlist'),
      NavigationDestination(icon: Icon(Icons.person), label: 'Profile'),
    ],
  ),
  drawer: Drawer(
    child: ListView(
      children: [
        const DrawerHeader(
          decoration: BoxDecoration(color: Colors.blue),
          child: Text('ShopApp', style: TextStyle(color: Colors.white, fontSize: 24)),
        ),
        ListTile(leading: const Icon(Icons.home), title: const Text('Home'), onTap: () {}),
        ListTile(leading: const Icon(Icons.settings), title: const Text('Settings'), onTap: () {}),
      ],
    ),
  ),
)
TEXT
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.

4. Card and List Components

(1) Card and ListTile

▶ Example

TEXT
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.

: Product card (Card + ListTile)

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

// Simplified class definition
class Product {
  final String imageUrl;
  final String name;
  final double price;
  final String category;
  const Product({required this.imageUrl, required this.name, required this.price, required this.category});
}

Card(
  margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
  elevation: 2,
  shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
  child: ListTile(
    leading: ClipRRect(
      borderRadius: BorderRadius.circular(8),
      child: Image.network(product.imageUrl, width: 56, height: 56, fit: BoxFit.cover),
    ),
    title: Text(product.name, style: const TextStyle(fontWeight: FontWeight.w600)),
    subtitle: Text('$${product.price.toStringAsFixed(2)} - ${product.category}'),
    trailing: IconButton(
      icon: const Icon(Icons.add_shopping_cart),
      onPressed: () => addToCart(product),
    ),
    onTap: () => navigateToDetail(product),
  ),
)
TEXT
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.

▶ Example

TEXT
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.

: Order list (ListTile + Divider)

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

// Simplified class definition
class Order {
  final int id;
  final double total;
  final String date;
  final String status;
  const Order({required this.id, required this.total, required this.date, required this.status});
}

ListView.separated(
  itemCount: orders.length,
  separatorBuilder: (_, __) => const Divider(height: 1, indent: 16),
  itemBuilder: (context, index) {
    final order = orders[index];
    return ListTile(
      leading: CircleAvatar(
        backgroundColor: order.status == 'Shipped' ? Colors.green : Colors.orange,
        child: Icon(order.status == 'Shipped' ? Icons.local_shipping : Icons.schedule,
          color: Colors.white),
      ),
      title: Text('Order #${order.id}'),
      subtitle: Text('$${order.total.toStringAsFixed(2)} - ${order.date}'),
      trailing: const Icon(Icons.chevron_right),
      onTap: () => navigateToOrderDetail(order),
    );
  },
)
TEXT
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.

5. Dialog Components

(1) Dialog Types Comparison

Component Position Interaction Use Case
AlertDialog Center popup Confirm/Cancel Important action confirmation (delete order)
BottomSheet Slides up from bottom Custom content Size selection, filtering
Snackbar Bottom temporary notification Can include action Lightweight feedback (added to cart)
Dialog Center popup Fully customizable Custom popup content

▶ Example

TEXT
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.

: Delete confirmation AlertDialog

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

// Simplified class definition
class CartItem {
  final String name;
  const CartItem({required this.name});
}

void showDeleteConfirmation(BuildContext context, CartItem item) {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('Remove Item'),
      content: Text('Remove "${item.name}" from your cart?'),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('Cancel'),
        ),
        FilledButton(
          onPressed: () {
            removeItem(item);
            Navigator.pop(context);
          },
          child: const Text('Remove'),
        ),
      ],
    ),
  );
}
TEXT
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.

▶ Example

TEXT
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.

: Size selection BottomSheet

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

// Product class definition shown above

void showSizeSelector(BuildContext context, Product product) {
  showModalBottomSheet(
    context: context,
    isScrollControlled: true,
    shape: const RoundedRectangleBorder(
      borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
    ),
    builder: (context) => DraggableScrollableSheet(
      initialChildSize: 0.5,
      minChildSize: 0.3,
      maxChildSize: 0.8,
      expand: false,
      builder: (context, scrollController) => Column(
        children: [
          // Handle bar
          Center(child: Container(
            margin: const EdgeInsets.symmetric(vertical: 8),
            width: 40, height: 4,
            decoration: BoxDecoration(color: Colors.grey[300], borderRadius: BorderRadius.circular(2)),
          )),
          Padding(
            padding: const EdgeInsets.all(16),
            child: Text('Select Size', style: Theme.of(context).textTheme.titleLarge),
          ),
          Expanded(
            child: ListView(
              controller: scrollController,
              children: ['S', 'M', 'L', 'XL', 'XXL'].map((size) => ListTile(
                title: Text('Size $size'),
                trailing: size == 'M' ? const Icon(Icons.check, color: Colors.green) : null,
                onTap: () => Navigator.pop(context, size),
              )).toList(),
            ),
          ),
        ],
      ),
    ),
  );
}
TEXT
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.

▶ Example

TEXT
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.

: Cart Snackbar feedback

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

// Product class definition shown above

void showCartSnackbar(BuildContext context, Product product) {
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      content: Text('${product.name} added to cart'),
      duration: const Duration(seconds: 2),
      action: SnackBarAction(
        label: 'View Cart',
        onPressed: () => navigateToCart(),
      ),
    ),
  );
}
TEXT
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.

6. Toggle Components

(1) Toggle Components Comparison

Component Interaction Data Type Use Case
Switch Slide toggle bool On/off settings (dark mode)
Checkbox Click to check bool Multi-select confirmation (agree to terms)
Radio Click for single selection T Single selection from a group (payment method)
Slider Drag slider double Range selection (price range)

▶ Example

TEXT
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.

: Payment method radio selection

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

class PaymentSelector extends StatefulWidget {
  const PaymentSelector({super.key});

  @override
  State<PaymentSelector> createState() => _PaymentSelectorState();
}

class _PaymentSelectorState extends State<PaymentSelector> {
  String _payment = 'credit_card';

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        RadioListTile<String>(
          title: const Text('Credit Card'),
          subtitle: const Text('Visa **** 4242'),
          secondary: const Icon(Icons.credit_card),
          value: 'credit_card',
          groupValue: _payment,
          onChanged: (v) => setState(() => _payment = v!),
        ),
        RadioListTile<String>(
          title: const Text('PayPal'),
          secondary: const Icon(Icons.account_balance_wallet),
          value: 'paypal',
          groupValue: _payment,
          onChanged: (v) => setState(() => _payment = v!),
        ),
        RadioListTile<String>(
          title: const Text('Apple Pay'),
          secondary: const Icon(Icons.phone_iphone),
          value: 'apple_pay',
          groupValue: _payment,
          onChanged: (v) => setState(() => _payment = v!),
        ),
      ],
    );
  }
}
TEXT
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.

▶ Example

TEXT
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.

: Price range Slider

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

RangeValues _priceRange = const RangeValues(0, 500);

RangeSlider(
  values: _priceRange,
  min: 0,
  max: 2000,
  divisions: 40,
  labels: RangeLabels(
    '$${_priceRange.start.toStringAsFixed(0)}',
    '$${_priceRange.end.toStringAsFixed(0)}',
  ),
  onChanged: (values) => setState(() => _priceRange = values),
)
TEXT
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.

7. Complete Example: ShopApp Product Detail Page

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

// Simplified class definition
class Product {
  final String name;
  final double price;
  final String imageUrl;
  final double? originalPrice;
  final double rating;
  final int reviewCount;
  const Product({required this.name, required this.price, required this.imageUrl, this.originalPrice, this.rating = 0.0, this.reviewCount = 0});
}

class ProductDetailPage extends StatelessWidget {
  final Product product;
  const ProductDetailPage({super.key, required this.product});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          // Collapsible app bar with product image
          SliverAppBar(
            expandedHeight: 300,
            pinned: true,
            flexibleSpace: FlexibleSpaceBar(
              title: Text(product.name, style: const TextStyle(fontSize: 16)),
              background: Stack(
                fit: StackFit.expand,
                children: [
                  Image.network(product.imageUrl, fit: BoxFit.cover),
                  const DecoratedBox(
                    decoration: BoxDecoration(
                      gradient: LinearGradient(begin: Alignment.bottomCenter, end: Alignment.topCenter,
                        colors: [Colors.black54, Colors.transparent]),
                    ),
                  ),
                ],
              ),
            ),
          ),
          // Product info
          SliverToBoxAdapter(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Row(children: [
                    Text('$${product.price.toStringAsFixed(2)}',
                      style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.green)),
                    const SizedBox(width: 12),
                    if (product.originalPrice != null)
                      Text('$${product.originalPrice!.toStringAsFixed(2)}',
                        style: const TextStyle(decoration: TextDecoration.lineThrough, color: Colors.grey)),
                  ]),
                  const SizedBox(height: 8),
                  Row(children: [
                    Icon(Icons.star, color: Colors.amber[700], size: 18),
                    Text(' ${product.rating} (${product.reviewCount} reviews)'),
                  ]),
                  const SizedBox(height: 16),
                  const Text('Description', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
                  const SizedBox(height: 8),
                  const Text('Premium quality product with fast shipping worldwide. '
                      'Free returns within 30 days. USD settlement supported.'),
                ],
              ),
            ),
          ),
        ],
      ),
      // Bottom action bar
      bottomNavigationBar: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(12),
          child: Row(children: [
            IconButton.outlined(onPressed: () {}, icon: const Icon(Icons.favorite_border)),
            const SizedBox(width: 8),
            Expanded(
              child: FilledButton.icon(
                onPressed: () => _showSizeSelector(context),
                icon: const Icon(Icons.shopping_cart),
                label: const Text('Add to Cart'),
              ),
            ),
          ]),
        ),
      ),
    );
  }

  void _showSizeSelector(BuildContext context) {
    showModalBottomSheet(
      context: context,
      builder: (ctx) => const SizeSelectorSheet(),
    );
  }
}
TEXT
> Output: Run locally with Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly across platforms.

❓ FAQ

Q Can Scaffold's body and bottomNavigationBar be used together?
A Yes, this is standard practice. body takes the main space, bottomNavigationBar is fixed at the bottom.
Q Snackbar not showing?
A You must call ScaffoldMessenger.of(context).showSnackBar() using the Scaffold's context. If you use a Builder or nested Scaffold, the context might be wrong.
Q How to set rounded corners on a BottomSheet?
A Use the shape property: shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20))).
Q What is Radio's groupValue?
A It's the currently selected value, shared by all Radios in the group. When a Radio's value equals groupValue, it shows the selected state.
Q What's the difference between Card and Container?
A Card has default elevation (shadow) and rounded corners, conforming to Material Design specs; Container is more flexible but has no default decoration. Use Card when you need a card appearance.
Q What's the difference between SliverAppBar's pinned and floating?
A pinned=true keeps the AppBar fixed at the top during scrolling; floating=true shows the AppBar immediately when scrolling down. Both can be used together.

📖 Summary


📝 Exercises

  1. Basic (difficulty ⭐): Create a Scaffold with a Drawer containing 5 menu items (Home/Cart/Orders/Settings/About).
  2. Intermediate (difficulty ⭐⭐): Implement a BottomSheet filter panel with a price Slider and category Checkboxes, showing filter results via Snackbar after confirming.
  3. Challenge (difficulty ⭐⭐⭐): Implement a complete product detail page: SliverAppBar with collapsible image + product info + size selection BottomSheet + add-to-cart Snackbar feedback.

← Previous | Next →

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏