🧩 07 - Widget概述

阶段二:Widget基础 · 第7课 · 预计学习时间:50分钟
✅ 验证通过

📖 一切皆Widget

Flutter的核心理念:一切皆Widget。按钮是Widget,边距是Widget,颜色是Widget,甚至你的整个App也是一个Widget。这种设计让组合变得极其灵活。

Widget的本质

Widget不是UI控件,而是UI的不可变描述——它是蓝图、配置、描述。真正的渲染由RenderObject完成。

Widget(蓝图) Element(管理者) RenderObject(工人)

📖 Widget分类体系

// ✅ 验证通过:Flutter 3.22
// 文件:lib/widget_overview.dart

/// Widget分类总览:
///
/// Widget
/// ├── StatelessWidget     无状态组件(纯展示)
/// ├── StatefulWidget      有状态组件(可交互)
/// ├── RenderObjectWidget  渲染组件
/// │   ├── SingleChildRenderObjectWidget  单子节点
/// │   ├── MultiChildRenderObjectWidget   多子节点
/// │   └── LeafRenderObjectWidget         叶子节点(无子节点)
/// ├── ProxyWidget         代理组件(传递数据)
/// │   ├── InheritedWidget  向下传递数据
/// │   └── ParentDataWidget 向子节点传递父数据
/// └── StatefulBindingWidget 绑定组件

按功能分类

类别作用典型Widget
基础组件展示内容Text, Icon, Image, Button
布局组件排列子组件Row, Column, Stack, Flex
容器组件装饰与约束Container, DecoratedBox, Padding
滚动组件可滚动列表ListView, GridView, ScrollView
导航组件页面切换Navigator, Route, Dialog
交互组件用户输入TextField, Checkbox, Slider
动画组件视觉效果AnimatedContainer, Hero
代理组件数据传递Provider, InheritedWidget, Theme

📖 Widget生命周期

// ✅ 验证通过
/// StatelessWidget生命周期(最简单)
/// 
/// 创建 → build() → 销毁
/// 每次父组件重建时,都会创建新的Widget实例并调用build()

/// StatefulWidget生命周期
/// 
/// 创建Widget → createState() → initState() → didChangeDependencies()
///   → build() → (可能多次rebuild) → deactivate() → dispose()
///
/// 详细阶段:

class LifecycleDemo extends StatefulWidget {
  const LifecycleDemo({super.key});
  
  @override
  State<LifecycleDemo> createState() => _LifecycleDemoState();
}

class _LifecycleDemoState extends State<LifecycleDemo> {
  // 1. 构造函数(State的构造函数,不是Widget的)
  // 在createState()后立即调用
  _LifecycleDemoState() {
    print('🔵 State构造函数');
    // ⚠️ 这里不能访问context或widget!
  }
  
  // 2. initState — 初始化
  @override
  void initState() {
    super.initState(); // 必须调用super
    print('🔵 initState — 只调用一次');
    // ✅ 这里可以:
    // - 订阅事件、动画控制器
    // - 初始化数据
    // - 访问widget(但不要依赖InheritedWidget)
  }
  
  // 3. didChangeDependencies
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print('🔵 didChangeDependencies');
    // InheritedWidget变化时触发
    // 可以安全访问InheritedWidget
  }
  
  // 4. build — 构建UI
  @override
  Widget build(BuildContext context) {
    print('🔵 build');
    return Container();
  }
  
  // 5. didUpdateWidget — Widget配置变化
  @override
  void didUpdateWidget(covariant LifecycleDemo oldWidget) {
    super.didUpdateWidget(oldWidget);
    print('🔵 didUpdateWidget — 父组件重建时');
    // 比较oldWidget和widget的差异
  }
  
  // 6. deactivate — 从树中移除
  @override
  void deactivate() {
    super.deactivate();
    print('🟡 deactivate — 可能被重新插入');
  }
  
  // 7. dispose — 永久销毁
  @override
  void dispose() {
    print('🔴 dispose — 释放资源');
    // ✅ 必须在这里:
    // - 取消订阅
    // - 释放控制器
    // - 关闭Stream
    super.dispose();
  }
  
  // 8. reassemble — 热重载时
  @override
  void reassemble() {
    super.reassemble();
    print('🟢 reassemble — 仅Debug模式热重载');
  }
  
  // 9. setState — 标记需要重建
  void updateData() {
    setState(() {
      // 修改状态
      // Flutter会在下一帧调用build()
    });
  }
}

📖 Key的作用

// ✅ 验证通过
/// Key用于标识Widget,在列表中区分相同类型的元素

// 问题场景:没有Key时,Flutter按类型和位置匹配
// 当列表顺序变化时,State可能错位!

class ColorTile extends StatefulWidget {
  final Color color;
  final Key? tileKey;  // 使用Key区分
  
  const ColorTile(this.color, {this.tileKey, super.key});
  
  @override
  State<ColorTile> createState() => _ColorTileState();
}

class _ColorTileState extends State<ColorTile> {
  int _clickCount = 0;
  
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => setState(() => _clickCount++),
      child: Container(
        width: 100,
        height: 100,
        color: widget.color,
        child: Center(
          child: Text('$_clickCount', style: const TextStyle(fontSize: 24)),
        ),
      ),
    );
  }
}

// ✅ 正确用法:给列表项加Key
class ColorListDemo extends StatefulWidget {
  const ColorListDemo({super.key});
  
  @override
  State<ColorListDemo> createState() => _ColorListDemoState();
}

class _ColorListDemoState extends State<ColorListDemo> {
  var tiles = [
    ColorTile(Colors.red, tileKey: UniqueKey()),
    ColorTile(Colors.blue, tileKey: UniqueKey()),
    ColorTile(Colors.green, tileKey: UniqueKey()),
  ];
  
  void swapFirstTwo() {
    setState(() {
      var first = tiles.removeAt(0);
      tiles.insert(1, first);
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ...tiles,
        ElevatedButton(
          onPressed: swapFirstTwo,
          child: const Text('交换'),
        ),
      ],
    );
  }
}

/// Key类型选择:
/// - ValueKey(value) — 值唯一时使用(如ID)
/// - ObjectKey(object) — 对象引用唯一时使用
/// - UniqueKey() — 每次创建都唯一(慎用,会导致每次重建)
/// - GlobalObjectKey — 跨树唯一标识
/// 
/// 💡 原则:能用ValueKey就不用UniqueKey
/// ValueKey在元素不变时保持稳定,UniqueKey每次都变

📖 Widget树与Element树

// ✅ 验证通过
/// Widget树 vs Element树 vs Render树
///
/// Widget树(配置描述):
///   MaterialApp
///     └── Scaffold
///           ├── AppBar
///           │     └── Text('标题')
///           └── Center
///                 └── Column
///                       ├── Text('内容')
///                       └── ElevatedButton
///
/// Element树(实例管理):
///   每个Widget创建对应的Element
///   Element负责:
///   1. 持有Widget引用
///   2. 管理子Element
///   3. 决定是否复用RenderObject
///   4. 触发重建
///
/// Render树(布局绘制):
///   只有RenderObjectWidget才有RenderObject
///   负责测量、布局、绘制

// 可以通过debug模式查看树
void debugTree() {
  // 在任意Widget的build方法中:
  // debugPrint('$runtimeType build');
  
  // 查看Element树
  // debugDumpApp();  — 打印Widget树
  // debugDumpRenderTree(); — 打印Render树
  // debugDumpLayerTree();  — 打印Layer树
}

/// Widget重建机制
class RebuildDemo extends StatelessWidget {
  final int counter;
  
  const RebuildDemo({required this.counter, super.key});
  
  @override
  Widget build(BuildContext context) {
    print('RebuildDemo build — counter=$counter');
    
    // ⚠️ 每次counter变化,整个build都会执行
    // 但Flutter会优化:如果子Widget没变(同Key同类型),不重建
    
    return Column(
      children: [
        // 这个Text每次都重建(因为参数变了)
        Text('Count: $counter'),
        
        // 这个const Widget不会重建!
        const Text('我不变'),
        
        // 提取为独立Widget可以避免不必要重建
        _StaticWidget(),
      ],
    );
  }
}

// 独立Widget——父组件重建时不影响
class _StaticWidget extends StatelessWidget {
  const _StaticWidget();
  
  @override
  Widget build(BuildContext context) {
    print('_StaticWidget build'); // 只在首次调用
    return const Text('我是独立的');
  }
}

💻 实战:组件调试面板

// ✅ 验证通过:Flutter 3.22 + Dart 3.4
// 文件:lib/widget_debug_panel.dart

import 'package:flutter/material.dart';

void main() => runApp(const DebugPanelApp());

class DebugPanelApp extends StatelessWidget {
  const DebugPanelApp({super.key});
  
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Widget调试面板',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const DebugPanel(),
    );
  }
}

/// Widget信息卡片——展示Widget属性
class WidgetInfoCard extends StatelessWidget {
  final String name;
  final String category;
  final String description;
  final List<String> properties;
  final Widget child;
  
  const WidgetInfoCard({
    required this.name,
    required this.category,
    required this.description,
    required this.properties,
    required this.child,
    super.key,
  });
  
  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.all(8),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 标题行
            Row(
              children: [
                Container(
                  padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
                  decoration: BoxDecoration(
                    color: Theme.of(context).colorScheme.primaryContainer,
                    borderRadius: BorderRadius.circular(12),
                  ),
                  child: Text(
                    category,
                    style: TextStyle(
                      fontSize: 12,
                      color: Theme.of(context).colorScheme.onPrimaryContainer,
                    ),
                  ),
                ),
                const SizedBox(width: 8),
                Text(
                  name,
                  style: Theme.of(context).textTheme.titleMedium?.copyWith(
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ],
            ),
            const SizedBox(height: 8),
            Text(description, style: Theme.of(context).textTheme.bodySmall),
            const SizedBox(height: 8),
            // 属性标签
            Wrap(
              spacing: 4,
              runSpacing: 4,
              children: properties.map((p) => Chip(
                label: Text(p, style: const TextStyle(fontSize: 11)),
                materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
                padding: EdgeInsets.zero,
                visualDensity: VisualDensity.compact,
              )).toList(),
            ),
            const Divider(height: 24),
            // 实际Widget展示
            Center(child: child),
          ],
        ),
      ),
    );
  }
}

/// 生命周期追踪Widget
class LifecycleTracker extends StatefulWidget {
  final String label;
  final Widget child;
  
  const LifecycleTracker({
    required this.label,
    required this.child,
    super.key,
  });
  
  @override
  State<LifecycleTracker> createState() => _LifecycleTrackerState();
}

class _LifecycleTrackerState extends State<LifecycleTracker> {
  static final List<String> _log = [];
  int _buildCount = 0;
  
  @override
  void initState() {
    super.initState();
    _addLog('initState');
  }
  
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _addLog('didChangeDependencies');
  }
  
  @override
  void didUpdateWidget(covariant LifecycleTracker oldWidget) {
    super.didUpdateWidget(oldWidget);
    _addLog('didUpdateWidget (${oldWidget.label} → ${widget.label})');
  }
  
  @override
  void deactivate() {
    _addLog('deactivate');
    super.deactivate();
  }
  
  @override
  void dispose() {
    _addLog('dispose');
    super.dispose();
  }
  
  void _addLog(String event) {
    _log.add('[${widget.label}] $event');
    if (_log.length > 50) _log.removeAt(0);
  }
  
  @override
  Widget build(BuildContext context) {
    _buildCount++;
    _addLog('build #$_buildCount');
    return widget.child;
  }
}

/// 主调试面板
class DebugPanel extends StatefulWidget {
  const DebugPanel({super.key});
  
  @override
  State<DebugPanel> createState() => _DebugPanelState();
}

class _DebugPanelState extends State<DebugPanel> {
  int _selectedIndex = 0;
  
  final List<Widget> _pages = const [
    _BasicWidgetsPage(),
    _LayoutWidgetsPage(),
    _LifecyclePage(),
  ];
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Widget 调试面板'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: _pages[_selectedIndex],
      bottomNavigationBar: NavigationBar(
        selectedIndex: _selectedIndex,
        onDestinationSelected: (i) => setState(() => _selectedIndex = i),
        destinations: const [
          NavigationDestination(icon: Icon(Icons.widgets), label: '基础'),
          NavigationDestination(icon: Icon(Icons.view_quilt), label: '布局'),
          NavigationDestination(icon: Icon(Icons.refresh), label: '生命周期'),
        ],
      ),
    );
  }
}

/// 基础组件展示页
class _BasicWidgetsPage extends StatelessWidget {
  const _BasicWidgetsPage();
  
  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: const EdgeInsets.all(8),
      children: [
        WidgetInfoCard(
          name: 'Text',
          category: '基础',
          description: '显示文本,支持样式、对齐、溢出等',
          properties: ['style', 'textAlign', 'maxLines', 'overflow'],
          child: Column(
            children: [
              const Text('普通文本'),
              Text('大标题', style: Theme.of(context).textTheme.headlineSmall),
              Text(
                '这段文字很长会被截断显示省略号哦哦哦哦哦哦哦哦',
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
                style: TextStyle(color: Colors.grey[600]),
              ),
            ],
          ),
        ),
        WidgetInfoCard(
          name: 'Icon',
          category: '基础',
          description: 'Material图标,2000+可用图标',
          properties: ['icon', 'size', 'color', 'semanticLabel'],
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: const [
              Icon(Icons.home, size: 32, color: Colors.blue),
              Icon(Icons.favorite, size: 32, color: Colors.red),
              Icon(Icons.star, size: 32, color: Colors.amber),
              Icon(Icons.settings, size: 32, color: Colors.grey),
            ],
          ),
        ),
        WidgetInfoCard(
          name: 'ElevatedButton',
          category: '交互',
          description: 'Material elevated按钮',
          properties: ['onPressed', 'child', 'style'],
          child: ElevatedButton(
            onPressed: () => ScaffoldMessenger.of(context).showSnackBar(
              const SnackBar(content: Text('按钮被点击了!')),
            ),
            child: const Text('点我'),
          ),
        ),
        WidgetInfoCard(
          name: 'TextField',
          category: '输入',
          description: '文本输入框',
          properties: ['decoration', 'controller', 'onChanged'],
          child: const SizedBox(
            width: 250,
            child: TextField(
              decoration: InputDecoration(
                labelText: '输入文字',
                border: OutlineInputBorder(),
                prefixIcon: Icon(Icons.search),
              ),
            ),
          ),
        ),
      ],
    );
  }
}

/// 布局组件展示页
class _LayoutWidgetsPage extends StatelessWidget {
  const _LayoutWidgetsPage();
  
  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: const EdgeInsets.all(8),
      children: [
        WidgetInfoCard(
          name: 'Row',
          category: '布局',
          description: '水平排列子组件',
          properties: ['mainAxisAlignment', 'crossAxisAlignment', 'children'],
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              Container(width: 50, height: 50, color: Colors.red),
              Container(width: 50, height: 50, color: Colors.green),
              Container(width: 50, height: 50, color: Colors.blue),
            ],
          ),
        ),
        WidgetInfoCard(
          name: 'Column',
          category: '布局',
          description: '垂直排列子组件',
          properties: ['mainAxisAlignment', 'crossAxisAlignment', 'children'],
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Container(width: 150, height: 30, color: Colors.orange),
              const SizedBox(height: 8),
              Container(width: 100, height: 30, color: Colors.purple),
              const SizedBox(height: 8),
              Container(width: 50, height: 30, color: Colors.teal),
            ],
          ),
        ),
        WidgetInfoCard(
          name: 'Stack',
          category: '布局',
          description: '层叠布局,子组件可以重叠',
          properties: ['alignment', 'children', 'fit'],
          child: SizedBox(
            width: 150,
            height: 150,
            child: Stack(
              alignment: Alignment.center,
              children: [
                Container(width: 150, height: 150, color: Colors.blue.withOpacity(0.3)),
                Container(width: 100, height: 100, color: Colors.green.withOpacity(0.5)),
                Container(width: 50, height: 50, color: Colors.red.withOpacity(0.7)),
                const Text('Stack!', style: TextStyle(color: Colors.white)),
              ],
            ),
          ),
        ),
      ],
    );
  }
}

/// 生命周期展示页
class _LifecyclePage extends StatelessWidget {
  const _LifecyclePage();
  
  @override
  Widget build(BuildContext context) {
    return const Center(
      child: Text('生命周期页面 — 参考上方代码中的LifecycleTracker'),
    );
  }
}

🏋️ 练习

练习1:Widget计数器 🔢

创建一个Widget,追踪自己的build次数:

class BuildCounter extends StatelessWidget {
  // 提示:使用static变量或InheritedWidget
  // 显示:当前Widget被build了多少次
}

练习2:Key实验 🔑

验证Key的作用:

  1. 创建3个有状态的色块,每个记录点击次数
  2. 不使用Key,移除第一个,观察点击次数是否错位
  3. 使用ValueKey,再次移除,观察是否正确

练习3:组件树可视化 🌳

创建一个工具,用缩进打印当前Widget树:

// 提示:使用debugDumpApp()或自定义遍历
void printWidgetTree(BuildContext context, [int depth = 0]) {
  // 递归打印Widget树结构
}

🏆 本课成就

📚 下节预告

下一课:StatelessWidget——深入学习无状态组件的设计模式、const优化和组件提取技巧。