3. StatefulWidget实现可切换主题的计数器

为什么计数器需要 StatefulWidget

例1中的默认计数器之所以能刷新界面,是因为它继承自 StatefulWidget。与 StatelessWidget 不同,StatefulWidget 本身不可变,真正保存可变数据的是它配套的 State 对象。当调用 setState() 时,Flutter 会把该 State 标记为“脏”,在下一帧重建其 build(),从而让界面反映新的状态值。

本例在计数器基础上增加一个主题开关:点击按钮切换亮色/暗色主题,同时保留计数功能。这样能同时观察两类状态变化——数值状态(计数)与外观状态(主题),并理解它们都通过 setState() 驱动重建。

状态生命周期与重建关系

下面这张图展示了 StatefulWidget 从创建到销毁的关键阶段,以及 setState() 如何触发 build() 重新执行。理解这条链路,就能明白为什么状态变量必须定义在 State 类中,而不是 StatefulWidget 里。

StatefulWidget 生命周期与 setState 重建链路 createState() 创建 State 对象 initState() 初始化状态变量 build() 根据状态构建 UI setState() 修改状态并标记脏 重建 build() 下一帧刷新界面 dispose() 释放资源 状态变量示例: int _count = 0; bool _isDark = false; ← 两者都定义在 State 类中,通过 setState() 更新

完整实现代码

下面是一个可直接运行的完整示例。它包含计数增减、主题切换两个状态,并用 ThemeData 的 brightness 控制亮暗外观。

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  bool _isDark = false;

  void _toggleTheme() {
    setState(() {
      _isDark = !_isDark;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '主题计数器',
      theme: _isDark
          ? ThemeData.dark(useMaterial3: true)
          : ThemeData.light(useMaterial3: true),
      home: CounterPage(
        isDark: _isDark,
        onToggleTheme: _toggleTheme,
      ),
    );
  }
}

class CounterPage extends StatefulWidget {
  final bool isDark;
  final VoidCallback onToggleTheme;

  const CounterPage({
    super.key,
    required this.isDark,
    required this.onToggleTheme,
  });

  @override
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int _count = 0;

  void _increment() {
    setState(() {
      _count++;
    });
  }

  void _decrement() {
    setState(() {
      if (_count > 0) _count--;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('StatefulWidget 计数器'),
        actions: [
          IconButton(
            icon: Icon(widget.isDark ? Icons.light_mode : Icons.dark_mode),
            onPressed: widget.onToggleTheme,
            tooltip: '切换主题',
          ),
        ],
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              '当前计数',
              style: Theme.of(context).textTheme.titleMedium,
            ),
            const SizedBox(height: 8),
            Text(
              '$_count',
              style: Theme.of(context).textTheme.displayLarge,
            ),
            const SizedBox(height: 24),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                FilledButton.tonal(
                  onPressed: _decrement,
                  child: const Icon(Icons.remove),
                ),
                const SizedBox(width: 16),
                FilledButton(
                  onPressed: _increment,
                  child: const Icon(Icons.add),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

关键点说明

  • _MyAppState 管理主题状态 _isDark,通过 MaterialApp 的 theme 属性切换亮暗主题。
  • _CounterPageState 管理计数状态 _count,通过 setState() 更新并触发重建。
  • 主题切换按钮放在 AppBar 的 actions 中,点击后调用父级传入的 onToggleTheme 回调。
  • 计数减到 0 时不再继续减少,避免出现负数。
验证方式:运行后点击“+”或“−”按钮,数字应立即变化;点击右上角图标,整个界面应在亮色与暗色之间切换。若数字不变,检查是否在 setState() 内修改了 _count;若主题不变,检查 MaterialApp 的 theme 是否依赖了 _isDark。
常见错误:把 _count 定义在 StatefulWidget 子类中而不是 State 子类中。由于 StatefulWidget 本身不可变,每次重建都会创建新实例,定义在其中的变量无法保持状态。