Flutter: Widget 基础

Widget 是 Flutter 的积木块——理解了 Widget,你就掌握了 Flutter 的 DNA。

📋 前置知识:需要先掌握以下内容

1. 你将学到


2. 一个前端转 Flutter 的真实故事

(1) 痛点:DOM 思维的陷阱

Bob 从 React 转 Flutter,习惯性地用"组件渲染 DOM"的思维理解 Widget。他发现列表滚动时商品卡片频繁重建,1000 个商品页面滚动卡顿到 30fps。更困惑的是,给列表加 key 后性能反而更差——因为他用的是 UniqueKey

(2) 三树架构的解法

Flutter 不是"组件→DOM"的简单映射,而是"Widget→Element→RenderObject"三层架构。Widget 是配置描述(轻量),Element 是实例管理(diff 核心),RenderObject 是布局绘制(重量)。理解三树后,Bob 知道该在 Element 层做 diff 优化,而不是在 Widget 层重建。

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

// Lightweight widget: just configuration
const ProductCard({required this.product, super.key});

// Flutter internally creates Element and RenderObject
// Widget rebuilds are cheap - Element diffing avoids RenderObject rebuild
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

(3) 收益:60fps 流畅滚动

Bob 改用 ValueKey(product.id) 后,Element diff 精准匹配,1000 商品列表稳定 60fps。


3. Widget-Element-RenderObject 三树

100%
graph LR
    W[Widget Tree] --> E[Element Tree]
    E --> R[RenderObject Tree]
    W -.->|canUpdate| E
    E -.->|adoptChild| R
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

(1) 三树职责

层级 创建方式 职责 重量
Widget 开发者 build() 不可变配置描述 极轻
Element Framework 自动创建 管理树结构、diff 更新 中等
RenderObject Framework 自动创建 测量、布局、绘制 重量

(2) canUpdate 规则

Element 通过 Widget.canUpdate(oldWidget, newWidget) 决定是否复用:

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

// canUpdate logic
static bool canUpdate(Widget oldWidget, Widget newWidget) {
  return oldWidget.runtimeType == newWidget.runtimeType
      && oldWidget.key == newWidget.key;
}
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
情况 runtimeType key 结果
同类型同 key 相同 相同 复用 Element
同类型不同 key 相同 不同 重建 Element
不同类型 不同 - 重建 Element

4. 常用基础 Widget

(1) Text 与样式

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:Text 多种样式

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

// Basic text
const Text('ShopApp')

// Styled text
Text(
  'Flash Sale!',
  style: TextStyle(
    fontSize: 24,
    fontWeight: FontWeight.bold,
    color: Colors.red,
    letterSpacing: 1.5,
  ),
)

// Rich text with multiple styles
Text.rich(
  TextSpan(
    text: 'Price: ',
    style: const TextStyle(fontSize: 16, color: Colors.grey),
    children: [
      TextSpan(
        text: '\$99.99',
        style: const TextStyle(
          fontSize: 20,
          fontWeight: FontWeight.bold,
          color: Colors.green,
        ),
      ),
      TextSpan(
        text: ' \$149.99',
        style: TextStyle(
          fontSize: 14,
          decoration: TextDecoration.lineThrough,
          color: Colors.grey[400],
        ),
      ),
    ],
  ),
)
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

(2) Container:万能容器

Container 属性 作用 类型
padding 内边距 EdgeInsets
margin 外边距 EdgeInsets
decoration 背景装饰 BoxDecoration
constraints 约束 BoxConstraints
alignment 子组件对齐 Alignment

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:Container 商品卡片

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

Container(
  margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
  padding: const EdgeInsets.all(12),
  decoration: BoxDecoration(
    color: Colors.white,
    borderRadius: BorderRadius.circular(12),
    boxShadow: [
      BoxShadow(
        color: Colors.black.withOpacity(0.1),
        blurRadius: 8,
        offset: const Offset(0, 2),
      ),
    ],
  ),
  child: Row(
    children: [
      ClipRRect(
        borderRadius: BorderRadius.circular(8),
        child: Image.network(
          'https://cdn.shopapp.com/products/laptop.jpg',
          width: 80,
          height: 80,
          fit: BoxFit.cover,
        ),
      ),
      const SizedBox(width: 12),
      Expanded(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text('Pro Laptop', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
            const SizedBox(height: 4),
            Text('\$1,299.99', style: const TextStyle(fontSize: 18, color: Colors.green)),
          ],
        ),
      ),
    ],
  ),
)
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

(3) Icon 与 IconButton

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:Icon 在电商场景

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

// Icon with color and size
const Icon(Icons.shopping_cart, color: Colors.blue, size: 32)

// IconButton for actions
IconButton(
  icon: const Icon(Icons.favorite_border),
  onPressed: () {
    // Add to wishlist
  },
  tooltip: 'Add to Wishlist',
)

// Badge icon for cart count
Stack(
  children: [
    const Icon(Icons.shopping_cart, size: 28),
    Positioned(
      right: 0,
      top: 0,
      child: Container(
        padding: const EdgeInsets.all(2),
        decoration: const BoxDecoration(color: Colors.red, shape: BoxShape.circle),
        constraints: const BoxConstraints(minWidth: 16, minHeight: 16),
        child: const Text('3', style: TextStyle(fontSize: 10, color: Colors.white),
          textAlign: TextAlign.center),
      ),
    ),
  ],
)
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

5. Key 机制

Key 用于控制 Element 的复用策略,在列表更新时尤为关键。

(1) Key 类型

Key 类型 判断依据 适用场景
ValueKey(value) 值相等 列表项有唯一标识(如 product.id)
ObjectKey(object) 对象引用 对象实例唯一
UniqueKey() 每次创建都不同 强制不复用(极少用)
GlobalKey 全局唯一 跨 Widget 访问 State

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:Key 在列表中的作用

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

// 简化类定义
class Product {
  final int id;
  final String name;
  final double price;
  const Product({required this.id, required this.name, required this.price});
}

// BAD: No key, Flutter matches by position
ListView(
  children: [
    ProductTile(product: products[0]), // index 0
    ProductTile(product: products[1]), // index 1
  ],
)

// When products reorder, Element mismatches State
// Solution: Use ValueKey with unique id

// GOOD: ValueKey enables correct Element-Widget matching
ListView(
  children: [
    ProductTile(key: ValueKey(products[0].id), product: products[0]),
    ProductTile(key: ValueKey(products[1].id), product: products[1]),
  ],
)
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

6. 完整示例:ShopApp 商品卡片组件

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

class Product {
  final int id;
  final String name;
  final double price;
  final String imageUrl;
  final double rating;
  final int reviewCount;

  const Product({
    required this.id,
    required this.name,
    required this.price,
    required this.imageUrl,
    this.rating = 0.0,
    this.reviewCount = 0,
  });
}

class ProductCard extends StatelessWidget {
  final Product product;
  final VoidCallback? onAddToCart;
  final VoidCallback? onTap;

  const ProductCard({
    super.key,
    required this.product,
    this.onAddToCart,
    this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onTap,
      child: Card(
        clipBehavior: Clip.antiAlias,
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
        elevation: 2,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Expanded(
              flex: 3,
              child: Stack(
                children: [
                  Image.network(product.imageUrl, fit: BoxFit.cover,
                    width: double.infinity),
                  if (product.price < 50)
                    Positioned(top: 8, left: 8,
                      child: _buildBadge('SALE', Colors.red)),
                ],
              ),
            ),
            Expanded(
              flex: 2,
              child: Padding(
                padding: const EdgeInsets.all(8.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(product.name, maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                      style: const TextStyle(fontWeight: FontWeight.w600)),
                    const SizedBox(height: 4),
                    Text('\$${product.price.toStringAsFixed(2)}',
                      style: const TextStyle(color: Colors.green, fontSize: 16)),
                    const Spacer(),
                    Align(
                      alignment: Alignment.centerRight,
                      child: IconButton(
                        icon: const Icon(Icons.add_shopping_cart, size: 20),
                        onPressed: onAddToCart,
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildBadge(String text, Color color) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
      decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(4)),
      child: Text(text, style: const TextStyle(color: Colors.white, fontSize: 10)),
    );
  }
}
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

❓ 常见问题

Q StatelessWidget 的 build 方法什么时候调用?
A 父组件重建时,或调用 setState 触发父组件重建时。StatelessWidget 本身不能主动触发重建。
Q 为什么 Widget 设计为不可变?
A 不可变意味着可以频繁创建而不担心副作用。Flutter 框架可以安全地对比新旧 Widget 来决定是否更新 Element。
Q 什么时候需要用 Key?
A 当列表中 Widget 类型相同但数据不同,且可能发生增删或重排时。静态列表通常不需要 Key。
Q GlobalKey 能跨 Widget 访问 State,这是否破坏了封装?
A 是的,GlobalKey 是"逃生舱",应谨慎使用。常见场景:Form 验证、ScrollController 管理。优先通过回调传递数据。
Q Container 和 Scaffold 有什么区别?
A Container 是通用装饰容器;Scaffold 是 Material Design 页面脚手架,提供 AppBar/Body/FAB/Drawer/BottomNav 等槽位。
Q Image.network 加载失败怎么办?
A 使用 errorBuilder 参数显示占位图,配合 cached_network_image 包实现缓存和加载指示。

📖 小节


📝 作业

  1. 基础题(难度⭐):创建一个 UserAvatar StatelessWidget,接收 name 和 imageUrl,显示圆形头像和用户名。
  2. 进阶题(难度⭐⭐):实现一个 PriceTag 组件,支持原价/折扣价显示,折扣价用删除线,原价用绿色大字。
  3. 挑战题(难度⭐⭐⭐):构建一个可排序的商品列表,拖拽排序时用 ValueKey 保证 State 正确跟随,排序后用 SnackBar 显示新顺序。

← 上一课 | 下一课 →

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

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

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