第2章:第一个Flutter应用 —— 2.1 计数器应用示例

66 阅读4分钟

2.1 计数器应用示例

📚 核心知识点

  1. Flutter应用的基本结构
  2. StatefulWidget 和 State 的使用
  3. Widget树的构建过程
  4. setState() 状态管理机制

💡 核心概念

1. Widget是什么?

Widget是Flutter中UI的基本构建块,理解为"UI的描述"。

关键特点:

  • 不可变(immutable) :创建后不能修改
  • 轻量级:只是配置信息,不是真正的UI元素
  • 可复用:同一个Widget可以在多处使用

类比理解:

Widget = 建筑图纸
Element = 施工队(负责实际建造)
RenderObject = 真正的建筑物

2. StatelessWidget vs StatefulWidget

特性StatelessWidgetStatefulWidget
状态无状态有状态
重建不会重建可以多次重建
适用场景静态UI动态UI
示例Text, IconCheckbox, TextField

判断标准:

  • 需要改变UI → StatefulWidget
  • 不需要改变UI → StatelessWidget

3. State生命周期

stateDiagram-v2
    [*] --> createState: Widget创建
    createState --> initState: State初始化
    initState --> build: 首次构建UI
    build --> mounted: 等待交互
    
    mounted --> setState: 状态改变
    setState --> build: 重新构建UI
    build --> mounted: 更新完成
    
    mounted --> dispose: Widget销毁
    dispose --> [*]: 清理完成

关键方法:

  • createState() - 创建State对象(只调用一次)
  • initState() - 初始化数据、订阅事件(只调用一次)
  • build() - 构建UI(每次状态改变都执行)
  • dispose() - 清理资源(只调用一次)

4. setState() 工作原理

void _incrementCounter() {
  setState(() {
    _counter++;  // 1. 修改状态
  });
  // 2. 标记widget为dirty
  // 3. 在下一帧调用build()
  // 4. 使用diff算法更新UI
}

为什么必须用setState()?

// ❌ 错误:UI不会更新
void _incrementCounter() {
  _counter++;  // 只修改了数据,Flutter不知道要更新UI
}
​
// ✅ 正确:UI会更新
void _incrementCounter() {
  setState(() {
    _counter++;  // Flutter知道状态改变了,会触发UI更新
  });
}

5. setState()执行流程

sequenceDiagram
    participant User as 用户
    participant Widget as Widget
    participant State as State
    participant Flutter as Flutter框架
    participant UI as UI界面
    
    User->>Widget: 点击按钮
    Widget->>State: 触发 _incrementCounter()
    
    rect rgb(255, 243, 224)
        Note over State: setState() 开始
        State->>State: 1. 执行回调函数
        State->>State: 2. _counter++
        State->>Flutter: 3. 标记为dirty
    end
    
    Flutter->>Flutter: 等待下一帧
    Flutter->>State: 调用 build()
    State->>Flutter: 返回新Widget树
    
    rect rgb(232, 245, 233)
        Note over Flutter: Diff算法
        Flutter->>Flutter: 对比新旧树
        Flutter->>Flutter: 找出差异
    end
    
    Flutter->>UI: 只更新变化部分
    UI->>User: 显示新的计数器值

6. 为什么build()在State中?

原因1:状态访问方便

// 如果build在StatefulWidget中:
Widget build(BuildContext context, State state) {
  return Text('${state.counter}');  // ❌ 需要公开状态,破坏封装
}
​
// build在State中:
Widget build(BuildContext context) {
  return Text('$_counter');  // ✅ 直接访问私有状态
}

原因2:继承灵活性

如果build在StatefulWidget中,子类需要访问父类的State,会导致复杂的状态传递机制。build在State中,子类只关注自己的逻辑。


🎨 Widget树结构

计数器应用的Widget树:

graph TD
    A[MaterialApp] --> B[MyHomePage<br/>StatefulWidget]
    B --> C[_MyHomePageState<br/>State]
    C --> D[Scaffold]
    D --> E[AppBar]
    D --> F[body: Center]
    D --> G[FloatingActionButton]
    
    E --> E1[Text 标题]
    
    F --> F1[Column]
    F1 --> F11[Text 提示]
    F1 --> F12[Text 计数]
    F1 --> F13[Padding]
    
    G --> G1[Icon +]
    
    style A fill:#E1F5FE,stroke:#01579B
    style B fill:#F3E5F5,stroke:#4A148C
    style C fill:#FCE4EC,stroke:#880E4F
    style D fill:#FFF3E0,stroke:#E65100

🔍 Flutter声明式UI

核心理念: UI = f(State)

graph TB
    A[State 状态] -->|f 函数| B[UI 界面]
    B -->|用户交互| C[事件触发]
    C -->|修改| A
    
    style A fill:#4CAF50,stroke:#2E7D32,stroke-width:3px,color:#fff
    style B fill:#2196F3,stroke:#1565C0,stroke-width:3px,color:#fff
    style C fill:#FF9800,stroke:#E65100,stroke-width:3px,color:#fff

状态改变驱动UI更新,而不是手动操作DOM。


📝 常见问题

Q1: StatefulWidget和State为什么要分开?

A:

  • StatefulWidget是配置:不可变,可以频繁重建
  • State是状态持有者:可变,在重建过程中保持不变
  • 好处:配置和状态分离,性能更好,逻辑更清晰

Q2: setState()能在build()方法中调用吗?

A: 不能!会导致无限循环:

Widget build(BuildContext context) {
  setState(() {});  // ❌ 错误!
  // build → setState → build → setState → ...
  return Container();
}

Q3: 可以不用setState()直接修改状态吗?

A: 可以修改变量值,但UI不会更新:

void _increment() {
  _counter++;  // ✅ 变量值改了
               // ❌ 但屏幕上的数字不会变
}

原理: Flutter不会主动检测状态变化,必须通过setState()告诉框架。

Q4: 什么时候用StatelessWidget?

A:

Text('Hello')           // ← StatelessWidget(文本不变)
Checkbox(value: _flag)  // ← StatefulWidget(选中状态会变)

🎯 核心总结

  1. Widget - UI的描述,不可变、轻量级
  2. StatelessWidget - 无状态,build一次就不变
  3. StatefulWidget - 有状态,可以多次rebuild
  4. State - 可变状态的持有者,在widget重建时保持不变
  5. setState() - 通知框架状态改变,触发UI更新
  6. build()在State中 - 方便访问状态、保持封装、灵活继承

🎓 跟着做练习

练习1:修改初始值 ⭐

目标: 将计数器的初始值改为 10

步骤:

  1. 找到 _MyHomePageState 类中的 _counter 变量
  2. int _counter = 0; 改为 int _counter = 10;
  3. 运行查看效果

完整代码:

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 10;  // ← 改这里
  
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }
  // ... 其他代码不变
}

练习2:添加减少按钮 ⭐⭐

目标: 在页面上添加一个减少按钮,点击后计数器减1

步骤:

  1. 先写一个减少的方法(仿照 _incrementCounter
  2. body 中添加一个按钮

完整代码:

class _CounterPageState extends State<CounterPage> {
  int _counter = 0;
​
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }
​
  // ← 新增:减少方法
  void _decrementCounter() {
    setState(() {
      _counter--;
    });
  }
​
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(...),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('你已经点击按钮这么多次:'),
            Text('$_counter', style: Theme.of(context).textTheme.headlineLarge),
            
            const SizedBox(height: 20),
            
            // ← 新增:两个按钮
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                // 减少按钮
                FloatingActionButton(
                  onPressed: _decrementCounter,
                  tooltip: '减少',
                  child: const Icon(Icons.remove),
                ),
                
                const SizedBox(width: 20),
                
                // 增加按钮
                FloatingActionButton(
                  onPressed: _incrementCounter,
                  tooltip: '增加',
                  child: const Icon(Icons.add),
                ),
              ],
            ),
          ],
        ),
      ),
      // ← 删除原来的 floatingActionButton(因为已经放到 body 里了)
    );
  }
}

练习3:添加重置按钮 ⭐⭐

目标: 在 AppBar 右侧添加一个重置按钮,点击后归零

步骤:

  1. 写一个重置方法
  2. 在 AppBar 的 actions 里添加按钮

完整代码:

class _CounterPageState extends State<CounterPage> {
  int _counter = 0;
​
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }
​
  // ← 新增:重置方法
  void _resetCounter() {
    setState(() {
      _counter = 0;
    });
  }
​
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        centerTitle: true,
        title: const Text('2.1 计数器应用示例'),
        // ← 新增:右侧按钮
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            tooltip: '重置',
            onPressed: _resetCounter,
          ),
        ],
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('你已经点击按钮这么多次:'),
            Text('$_counter', style: Theme.of(context).textTheme.headlineLarge),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: '增加',
        child: const Icon(Icons.add),
      ),
    );
  }
}

练习4:数字颜色随奇偶变化 ⭐⭐⭐

目标: 偶数显示蓝色,奇数显示红色

关键知识:

  • 奇偶判断:_counter % 2 == 0 (偶数为true)
  • 三元运算符:条件 ? 值1 : 值2

完整代码:

Text(
  '$_counter',
  style: TextStyle(
    fontSize: 48,
    fontWeight: FontWeight.bold,
    // ← 关键代码:根据奇偶设置颜色
    color: _counter % 2 == 0 ? Colors.blue : Colors.red,
  ),
),

练习5:修改主题颜色 ⭐

目标: 将应用主题色改为绿色

步骤: 找到 CounterApp 中的 seedColor,改为 Colors.green

完整代码:

class CounterApp extends StatelessWidget {
  const CounterApp({super.key});
​
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter 计数器示例',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.green,  // ← 改这里
        ),
        useMaterial3: true,
      ),
      home: const CounterPage(),
    );
  }
}

💡 练习建议

  1. 一个一个来:先完成练习1,运行看效果,再做练习2
  2. 对照代码:把答案和原代码对比,看看改了哪里
  3. 理解原理:不要只复制粘贴,想想为什么这么写
  4. 多次运行:每次修改后都运行一下,立即看到效果
  5. 尝试变化:完成后可以试试改变颜色、图标等