Flutter: Widget 基础
Widget 是 Flutter 的积木块——理解了 Widget,你就掌握了 Flutter 的 DNA。
📋 前置知识:需要先掌握以下内容
- 第2课:Dart 语言速览
1. 你将学到
- Widget-Element-RenderObject 三树架构与性能关系
- StatelessWidget 与 StatefulWidget 生命周期对比
- 常用基础 Widget:Text、Image、Icon、Container、Scaffold
- Key 的作用与使用场景:ValueKey / ObjectKey / GlobalKey
- ShopApp 商品卡片 StatelessWidget 实现
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 三树
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 包实现缓存和加载指示。📖 小节
- Widget(配置)→ Element(管理)→ RenderObject(绘制)三层架构
- Widget 不可变、轻量,可频繁重建;Element 做 diff 决定是否更新 RenderObject
- Key 控制 Element 复用策略:ValueKey 按值匹配,UniqueKey 强制不复用
- Container 是万能容器,Scaffold 是页面脚手架
- 在列表中使用 ValueKey(product.id) 保证 Element 正确匹配
📝 作业
- 基础题(难度⭐):创建一个
UserAvatarStatelessWidget,接收 name 和 imageUrl,显示圆形头像和用户名。 - 进阶题(难度⭐⭐):实现一个
PriceTag组件,支持原价/折扣价显示,折扣价用删除线,原价用绿色大字。 - 挑战题(难度⭐⭐⭐):构建一个可排序的商品列表,拖拽排序时用 ValueKey 保证 State 正确跟随,排序后用 SnackBar 显示新顺序。