文章

受控组件vs非受控组件

受控组件 vs 非受控组件

受控组件vs非受控组件

受控组件 vs 非受控组件

概念理解

🎯 受控组件 (Controlled Component)

  • 数据由父组件控制:组件的值通过 value 属性从外部传入
  • 单向数据流:数据从父组件流向子组件
  • 状态提升:状态保存在父组件中,子组件只是”展示”数据

🎮 非受控组件 (Uncontrolled Component)

  • 数据由组件自己管理:组件内部维护自己的状态
  • 双向数据流:组件内部可以修改自己的状态
  • 状态内聚:状态保存在组件内部

📊 对比表格

特性受控组件非受控组件
数据来源通过 value
从外部传入
组件内部管理
状态位置父组件组件自身
数据流单向双向
可预测性
调试难度
适用场景表单、复杂交互简单独立组件

💡 核心原则

在 Flutter 中,推荐使用受控组件,因为:

  • 数据流更清晰
  • 更容易调试
  • 符合 Flutter 的声明式UI理念
  • 便于状态管理

🛠️ 代码示例

1. 受控组件示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/// 受控输入框组件
class ControlledTextField extends StatelessWidget {
  final String value;
  final ValueChanged<String> onChanged;

  const ControlledTextField({
    super.key,
    required this.value,
    required this.onChanged,
  });

  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: TextEditingController(text: value),
      onChanged: onChanged,
    );
  }
}

/// 使用受控组件
class ParentWidget extends StatefulWidget {
  @override
  State<ParentWidget> createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  String _inputValue = ''; // 状态在父组件

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // 数据流向:父组件 → 子组件
        ControlledTextField(
          value: _inputValue, // 数据从父组件传入
          onChanged: (newValue) {
            setState(() {
              _inputValue = newValue; // 回调通知父组件更新状态
            });
          },
        ),
        Text('当前值: $_inputValue'), // 实时显示,数据一致
      ],
    );
  }
}

2. 非受控组件示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/// 非受控输入框组件
class UncontrolledTextField extends StatefulWidget {
  final String initialValue;
  final ValueChanged<String>? onChanged;

  const UncontrolledTextField({
    super.key,
    this.initialValue = '',
    this.onChanged,
  });

  @override
  State<UncontrolledTextField> createState() => _UncontrolledTextFieldState();
}

class _UncontrolledTextFieldState extends State<UncontrolledTextField> {
  late TextEditingController _controller; // 状态在组件内部

  @override
  void initState() {
    super.initState();
    _controller = TextEditingController(text: widget.initialValue);
  }

  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: _controller,
      onChanged: (value) {
        widget.onChanged?.call(value); // 可选的回调
      },
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

/// 使用非受控组件
class ParentWidget extends StatefulWidget {
  @override
  State<ParentWidget> createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  String _displayValue = '';

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        UncontrolledTextField(
          initialValue: '初始值',
          onChanged: (value) {
            setState(() {
              _displayValue = value; // 只能通过回调获取值
            });
          },
        ),
        Text('显示值: $_displayValue'), // 可能不同步!

        ElevatedButton(
          onPressed: () {
            // 问题:无法直接获取输入框的当前值
            // _displayValue 可能不是最新的
          },
          child: Text('提交'),
        ),
      ],
    );
  }
}

🔄 数据流对比

受控组件数据流

1
2
3
父组件状态  value属性  子组件显示  用户操作  onChanged回调  更新父组件状态
  
  数据始终同步

非受控组件数据流

初始值 → 子组件内部状态 → 用户操作 → 内部状态变化 → 可选回调通知父组件
     ↓
数据可能不同步

🎯 实际场景示例

场景:表单提交

受控组件方式(推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class UserForm extends StatefulWidget {
  @override
  State<UserForm> createState() => _UserFormState();
}

class _UserFormState extends State<UserForm> {
  // 所有状态在父组件统一管理
  String _name = '';
  String _email = '';
  bool _subscribe = false;

  void _submitForm() {
    // 直接使用状态,数据保证是最新的
    final userData = {
      'name': _name,
      'email': _email,
      'subscribe': _subscribe,
    };
    print('提交数据: $userData');
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextFieldFormItem(
          label: '姓名',
          value: _name,
          onChanged: (value) => setState(() => _name = value),
        ),
        TextFieldFormItem(
          label: '邮箱', 
          value: _email,
          onChanged: (value) => setState(() => _email = value),
        ),
        SwitchFormItem(
          label: '订阅通知',
          value: _subscribe,
          onChanged: (value) => setState(() => _subscribe = value),
        ),
        ElevatedButton(
          onPressed: _submitForm,
          child: Text('提交'),
        ),
      ],
    );
  }
}

非受控组件方式(不推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class UserForm extends StatefulWidget {
  @override
  State<UserForm> createState() => _UserFormState();
}

class _UserFormState extends State<UserForm> {
  // 需要维护两套状态
  String _name = '';
  String _email = '';
  bool _subscribe = false;

  // 还需要引用子组件来获取值
  final _nameFieldKey = GlobalKey<_UncontrolledTextFieldState>();
  final _emailFieldKey = GlobalKey<_UncontrolledTextFieldState>();
  final _subscribeSwitchKey = GlobalKey<_UncontrolledSwitchState>();

  void _submitForm() {
    // 需要通过各种方式获取子组件的状态
    final name = _nameFieldKey.currentState?._controller.text ?? '';
    final email = _emailFieldKey.currentState?._controller.text ?? '';
    final subscribe = _subscribeSwitchKey.currentState?._value ?? false;

    final userData = {
      'name': name,
      'email': email, 
      'subscribe': subscribe,
    };
    print('提交数据: $userData');
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        UncontrolledTextField(
          key: _nameFieldKey,
          initialValue: _name,
          onChanged: (value) => setState(() => _name = value),
        ),
        // ... 其他类似
        ElevatedButton(
          onPressed: _submitForm,
          child: Text('提交'),
        ),
      ],
    );
  }
}

📝 总结

方面受控组件非受控组件
数据一致性✅ 保证同步❌ 可能不同步
调试难度✅ 容易❌ 困难
代码复杂度✅ 简单清晰❌ 复杂混乱
可测试性✅ 容易测试❌ 测试困难
推荐程度⭐⭐⭐⭐⭐⭐☆

建议:在 Flutter 开发中,除非有特殊需求,否则始终使用受控组件。这样可以让你的应用数据流更清晰、更易于维护和调试。


🚀 最佳实践

  1. 状态提升:将状态提升到最近的共同祖先
  2. 单一数据源:每个状态只有一个”真相来源”
  3. 不可变数据:使用 setState 创建新状态而不是修改旧状态
  4. 明确的数据流:数据向下流动,事件向上传递

记住这个口诀:"状态上提,数据下传,事件回调"

本文由作者按照 CC BY 4.0 进行授权