🔄 09 - StatefulWidget

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

📖 State与Widget的关系

StatefulWidget将不可变的配置(Widget)与可变的状态(State)分离。Widget每次重建都是新实例,但State对象会被保留。

// ✅ 验证通过:Flutter 3.22
// Widget是短期对象——每次重建都new一个
// State是长期对象——跨重建保持

class Counter extends StatefulWidget {
  // Widget部分:不可变配置
  final int initialValue;
  final Color color;
  
  const Counter({
    this.initialValue = 0,
    this.color = Colors.blue,
    super.key,
  });
  
  // createState返回State实例
  // 只在Element首次创建时调用
  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  // State部分:可变数据
  late int _count;
  
  // 访问Widget的配置——通过widget属性
  // widget.color → Widget的color
  // widget.initialValue → Widget的initialValue
  
  @override
  void initState() {
    super.initState();
    _count = widget.initialValue; // 使用Widget配置初始化
  }
  
  // 当Widget配置变化时
  @override
  void didUpdateWidget(covariant Counter oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.initialValue != widget.initialValue) {
      _count = widget.initialValue;
    }
  }
  
  void increment() => setState(() => _count++);
  void decrement() => setState(() => _count--);
  void reset() => setState(() => _count = widget.initialValue);
  
  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Text('$_count', style: TextStyle(fontSize: 48, color: widget.color)),
        const SizedBox(height: 16),
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            IconButton(onPressed: decrement, icon: const Icon(Icons.remove)),
            ElevatedButton(onPressed: reset, child: const Text('Reset')),
            IconButton(onPressed: increment, icon: const Icon(Icons.add)),
          ],
        ),
      ],
    );
  }
}

📖 setState深入

// ✅ 验证通过
/// setState做了什么?
/// 1. 执行传入的回调函数(修改状态)
/// 2. 标记当前Element为dirty
/// 3. 在下一帧,Framework调用build()重建UI
/// 4. 比较新旧Widget树,更新Render树

void setStateDeepDive() {
  // ✅ 正确用法
  setState(() {
    _counter++;  // 在回调内修改状态
  });
  
  // ❌ 错误用法1:忘记setState
  _counter++;  // 状态变了,UI不会更新!
  
  // ❌ 错误用法2:在build中调用setState
  @override
  Widget build(BuildContext context) {
    setState(() {}); // 无限循环!build → setState → build → ...
    return Container();
  }
  
  // ❌ 错误用法3:异步回调中使用过期的context
  void onPressed() async {
    await Future.delayed(const Duration(seconds: 1));
    // 如果Widget已经被dispose,setState会抛异常
    if (mounted) {  // ✅ 先检查mounted
      setState(() {
        _data = 'loaded';
      });
    }
  }
  
  // ❌ 错误用法4:setState粒度太粗
  setState(() {
    // 修改了10个字段,但只有2个影响UI
    // 应该只修改影响UI的字段,或者拆分Widget
  });
  
  // ✅ 最佳实践:精细控制setState范围
  // 方案1:拆分为多个小Widget,各自管理状态
  // 方案2:使用ValueNotifier + ValueListenableBuilder
}

/// ValueNotifier——轻量级状态管理
class CounterWithNotifier extends StatefulWidget {
  const CounterWithNotifier({super.key});
  
  @override
  State<CounterWithNotifier> createState() => _CounterWithNotifierState();
}

class _CounterWithNotifierState extends State<CounterWithNotifier> {
  // 只追踪需要的数据
  final _counter = ValueNotifier<int>(0);
  
  @override
  void dispose() {
    _counter.dispose(); // 必须释放
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // 只有这个部分会重建!
        ValueListenableBuilder<int>(
          valueListenable: _counter,
          builder: (context, value, child) {
            return Text('$value', style: const TextStyle(fontSize: 48));
          },
        ),
        // 这部分不会重建
        const Text('计数器示例'),
        Row(
          children: [
            IconButton(
              onPressed: () => _counter.value--,
              icon: const Icon(Icons.remove),
            ),
            IconButton(
              onPressed: () => _counter.value++,
              icon: const Icon(Icons.add),
            ),
          ],
        ),
      ],
    );
  }
}

📖 控制器模式

// ✅ 验证通过
/// TextEditingController — 文本输入控制器
class TextFormDemo extends StatefulWidget {
  const TextFormDemo({super.key});
  
  @override
  State<TextFormDemo> createState() => _TextFormDemoState();
}

class _TextFormDemoState extends State<TextFormDemo> {
  final _nameController = TextEditingController();
  final _emailController = TextEditingController();
  
  @override
  void initState() {
    super.initState();
    // 监听文本变化
    _nameController.addListener(() {
      print('名字变化: ${_nameController.text}');
    });
  }
  
  @override
  void dispose() {
    _nameController.dispose();
    _emailController.dispose();
    super.dispose();
  }
  
  void _submit() {
    print('Name: ${_nameController.text}');
    print('Email: ${_emailController.text}');
  }
  
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          controller: _nameController,
          decoration: const InputDecoration(
            labelText: '姓名',
            hintText: '请输入姓名',
            prefixIcon: Icon(Icons.person),
          ),
        ),
        TextField(
          controller: _emailController,
          decoration: const InputDecoration(
            labelText: '邮箱',
            hintText: '请输入邮箱',
            prefixIcon: Icon(Icons.email),
          ),
          keyboardType: TextInputType.emailAddress,
        ),
        ElevatedButton(
          onPressed: _submit,
          child: const Text('提交'),
        ),
      ],
    );
  }
}

/// AnimationController — 动画控制器
class PulseWidget extends StatefulWidget {
  final Widget child;
  const PulseWidget({required this.child, super.key});
  
  @override
  State<PulseWidget> createState() => _PulseWidgetState();
}

class _PulseWidgetState extends State<PulseWidget> with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 2),
    )..repeat(reverse: true);  // 来回重复
  }
  
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return ScaleTransition(
      scale: Tween<double>(begin: 0.95, end: 1.05).animate(_controller),
      child: widget.child,
    );
  }
}

/// ScrollController — 滚动控制器
class ScrollDemo extends StatefulWidget {
  const ScrollDemo({super.key});
  
  @override
  State<ScrollDemo> createState() => _ScrollDemoState();
}

class _ScrollDemoState extends State<ScrollDemo> {
  final _scrollController = ScrollController();
  bool _showFab = false;
  
  @override
  void initState() {
    super.initState();
    _scrollController.addListener(_onScroll);
  }
  
  void _onScroll() {
    var show = _scrollController.offset > 200;
    if (show != _showFab) {
      setState(() => _showFab = show);
    }
  }
  
  void _scrollToTop() {
    _scrollController.animateTo(
      0,
      duration: const Duration(milliseconds: 500),
      curve: Curves.easeInOut,
    );
  }
  
  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView.builder(
        controller: _scrollController,
        itemCount: 100,
        itemBuilder: (context, index) => ListTile(title: Text('Item $index')),
      ),
      floatingActionButton: _showFab
          ? FloatingActionButton(
              onPressed: _scrollToTop,
              child: const Icon(Icons.arrow_upward),
            )
          : null,
    );
  }
}

💻 实战:交互式待办事项

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

import 'package:flutter/material.dart';

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

class TodoApp extends StatelessWidget {
  const TodoApp({super.key});
  
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '交互式待办',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const TodoPage(),
    );
  }
}

// 数据模型
class Todo {
  final String id;
  String title;
  bool isDone;
  DateTime createdAt;
  Priority priority;
  
  Todo({
    required this.id,
    required this.title,
    this.isDone = false,
    DateTime? createdAt,
    this.priority = Priority.medium,
  }) : createdAt = createdAt ?? DateTime.now();
  
  void toggle() => isDone = !isDone;
}

enum Priority { low, medium, high }

extension PriorityX on Priority {
  String get label => switch (this) {
    Priority.low => '低',
    Priority.medium => '中',
    Priority.high => '高',
  };
  Color get color => switch (this) {
    Priority.low => Colors.green,
    Priority.medium => Colors.orange,
    Priority.high => Colors.red,
  };
}

// 主页面
class TodoPage extends StatefulWidget {
  const TodoPage({super.key});
  
  @override
  State<TodoPage> createState() => _TodoPageState();
}

class _TodoPageState extends State<TodoPage> {
  final List<Todo> _todos = [];
  final _inputController = TextEditingController();
  Priority _selectedPriority = Priority.medium;
  TodoFilter _filter = TodoFilter.all;
  SortMode _sort = SortMode.created;
  
  int get _activeCount => _todos.where((t) => !t.isDone).length;
  int get _doneCount => _todos.where((t) => t.isDone).length;
  
  List<Todo> get _filteredTodos {
    var filtered = switch (_filter) {
      TodoFilter.all => _todos,
      TodoFilter.active => _todos.where((t) => !t.isDone),
      TodoFilter.done => _todos.where((t) => t.isDone),
    };
    
    var sorted = filtered.toList();
    sorted.sort((a, b) => switch (_sort) {
      SortMode.created => b.createdAt.compareTo(a.createdAt),
      SortMode.priority => b.priority.index.compareTo(a.priority.index),
      SortMode.alpha => a.title.compareTo(b.title),
    });
    return sorted;
  }
  
  void _addTodo() {
    var title = _inputController.text.trim();
    if (title.isEmpty) return;
    
    setState(() {
      _todos.insert(0, Todo(
        id: DateTime.now().millisecondsSinceEpoch.toString(),
        title: title,
        priority: _selectedPriority,
      ));
    });
    _inputController.clear();
  }
  
  void _toggleTodo(String id) {
    setState(() {
      var todo = _todos.firstWhere((t) => t.id == id);
      todo.toggle();
    });
  }
  
  void _deleteTodo(String id) {
    setState(() {
      _todos.removeWhere((t) => t.id == id);
    });
  }
  
  void _clearDone() {
    setState(() {
      _todos.removeWhere((t) => t.isDone);
    });
  }
  
  @override
  void dispose() {
    _inputController.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('待办事项'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        actions: [
          PopupMenuButton<SortMode>(
            icon: const Icon(Icons.sort),
            onSelected: (mode) => setState(() => _sort = mode),
            itemBuilder: (_) => [
              const PopupMenuItem(value: SortMode.created, child: Text('按创建时间')),
              const PopupMenuItem(value: SortMode.priority, child: Text('按优先级')),
              const PopupMenuItem(value: SortMode.alpha, child: Text('按名称')),
            ],
          ),
        ],
      ),
      body: Column(
        children: [
          // 输入区域
          _buildInputArea(),
          // 过滤栏
          _buildFilterBar(),
          // 统计
          _buildStats(),
          // 列表
          Expanded(
            child: _filteredTodos.isEmpty
                ? const Center(child: Text('暂无待办'))
                : ListView.builder(
                    itemCount: _filteredTodos.length,
                    itemBuilder: (context, index) {
                      var todo = _filteredTodos[index];
                      return _TodoTile(
                        todo: todo,
                        onToggle: () => _toggleTodo(todo.id),
                        onDelete: () => _deleteTodo(todo.id),
                      );
                    },
                  ),
          ),
        ],
      ),
    );
  }
  
  Widget _buildInputArea() {
    return Padding(
      padding: const EdgeInsets.all(12),
      child: Row(
        children: [
          Expanded(
            child: TextField(
              controller: _inputController,
              decoration: const InputDecoration(
                hintText: '添加新待办...',
                border: OutlineInputBorder(),
              ),
              onSubmitted: (_) => _addTodo(),
            ),
          ),
          const SizedBox(width: 8),
          IconButton(
            onPressed: () => setState(() {
              _selectedPriority = Priority.values[(_selectedPriority.index + 1) % 3];
            }),
            icon: Icon(Icons.flag, color: _selectedPriority.color),
            tooltip: '优先级: ${_selectedPriority.label}',
          ),
          FilledButton(
            onPressed: _addTodo,
            child: const Icon(Icons.add),
          ),
        ],
      ),
    );
  }
  
  Widget _buildFilterBar() {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 12),
      child: Row(
        children: TodoFilter.values.map((filter) {
          var isSelected = _filter == filter;
          return Padding(
            padding: const EdgeInsets.only(right: 8),
            child: FilterChip(
              selected: isSelected,
              label: Text(filter.label),
              onSelected: (_) => setState(() => _filter = filter),
            ),
          );
        }).toList(),
      ),
    );
  }
  
  Widget _buildStats() {
    return Padding(
      padding: const EdgeInsets.all(12),
      child: Row(
        children: [
          Text('总计: ${_todos.length}'),
          const SizedBox(width: 16),
          Text('待办: $_activeCount'),
          const SizedBox(width: 16),
          Text('完成: $_doneCount'),
          const Spacer(),
          if (_doneCount > 0)
            TextButton(
              onPressed: _clearDone,
              child: const Text('清除已完成'),
            ),
        ],
      ),
    );
  }
}

enum TodoFilter { all, active, done }
extension on TodoFilter {
  String get label => switch (this) {
    TodoFilter.all => '全部',
    TodoFilter.active => '待办',
    TodoFilter.done => '已完成',
  };
}

enum SortMode { created, priority, alpha }

// 待办项Widget
class _TodoTile extends StatelessWidget {
  final Todo todo;
  final VoidCallback onToggle;
  final VoidCallback onDelete;
  
  const _TodoTile({
    required this.todo,
    required this.onToggle,
    required this.onDelete,
  });
  
  @override
  Widget build(BuildContext context) {
    return Dismissible(
      key: ValueKey(todo.id),
      direction: DismissDirection.endToStart,
      onDismissed: (_) => onDelete(),
      background: Container(
        color: Colors.red,
        alignment: Alignment.centerRight,
        padding: const EdgeInsets.only(right: 16),
        child: const Icon(Icons.delete, color: Colors.white),
      ),
      child: ListTile(
        leading: Checkbox(
          value: todo.isDone,
          onChanged: (_) => onToggle(),
        ),
        title: Text(
          todo.title,
          style: TextStyle(
            decoration: todo.isDone ? TextDecoration.lineThrough : null,
            color: todo.isDone ? Colors.grey : null,
          ),
        ),
        trailing: Container(
          padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
          decoration: BoxDecoration(
            color: todo.priority.color.withOpacity(0.2),
            borderRadius: BorderRadius.circular(12),
          ),
          child: Text(
            todo.priority.label,
            style: TextStyle(color: todo.priority.color, fontSize: 12),
          ),
        ),
      ),
    );
  }
}

🏋️ 练习

练习1:秒表 ⏱️

使用AnimationController或Timer实现一个秒表:

练习2:可展开列表 📋

使用StatefulWidget实现ExpandableList:

练习3:拖拽排序 🔀

使用ReorderableListView实现可拖拽排序的列表。

🏆 本课成就

📚 下节预告

下一课:布局(Row/Column/Stack)——Flutter三大核心布局组件的深入讲解。