【问题标题】:What's the design benefit of Flutter's (Widget)State/StatefulWidget pattern?Flutter 的 (Widget)State/StatefulWidget 模式的设计优势是什么?
【发布时间】:2025-11-25 09:30:02
【问题描述】:

我的文档和 Flutter 视频,StatefulWidget (+(Widget)State) 的设计解释是这样的:

  1. 提倡声明式设计(好)
  2. 正式化了 Flutter 有效决定哪些组件需要重新渲染的过程(也很好)

从例子:

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {...}
}

但是:

  1. 既然我们必须明确记住调用setState 以使状态无效,这真的是声明式设计吗?
  2. Flutter 不会自动检测 State 对象中的更改并决定调用 build(尽管它可能有),因此它并没有真正形式化/自动化/确保视图组件的失效。既然我们必须显式调用setState,那么Flutter 的(Widget)State/StatefulWidget 模式有什么好处,比方说:
class MyHomePage extends StatefulWidget // Define dirty method
{
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  int _counter = 0;

  _incrementCounter() { 
    _counter++;
    this.dirty();  // Require the view to be rebuilt. Arranges generateView to be called.
  }


  @override
  Widget generateView(BuildContext context) {return ... rendering description containing updated counter ... ;}
}

...这会给程序员带来同样的标记 UI 脏的负担,同样具有贬义性,并且避免了混淆程序意图的额外抽象。

我错过了什么?在 Flutter 中将 StatefulWidget(Widget)State 分开有什么好处?

[在人们加入 MVC cmets 之前,请注意,Flutter 模型相当明确地只管理小部件的状态,并且它通过 build 方法与 UI 的小部件紧密耦合 - 这里没有关注点分离,也没有对于未附加到视图的更大应用程序状态有很多话要说。]

[还有,版主,这些问题不一样:Why does Flutter State object require a Widget?What is the relation between stateful and stateless widgets in Flutter?。我的问题是关于目前设计的好处是什么,而不是这个设计是如何工作的。]

更新: @Rémi Rousselet -- 这是一个声明性示例,只需要声明一个新的状态类。通过一些工作,您甚至可以摆脱它(尽管它可能不会更好)。

这种声明与需要交互的方式不需要(用户)声明两个新的循环类型引用类,并且响应状态而改变的小部件与状态解耦(它构造了一个纯函数状态,不需要分配状态)。

这种做事方式无法承受热重载。 (悲伤的脸)。 我怀疑这更多是热重载的问题,但如果有办法让它工作,那就太好了,

import 'dart:collection';
import 'package:flutter/material.dart';

////////////////////////////////
// Define some application state

class MyAppState with ChangeSubscribeable<MyAppState> {
  /***
   * TODO. Automate notifyListeners on setter.
   * Binds changes to the widget
   */
  int _counter;

  get counter => _counter;

  set counter(int c) {
    _counter = c;
    notifyListeners(); // <<<<<< ! Calls ... .setState to invalidate widget
  }

  increment() {
    counter = _counter + 1;
  }

  MyAppState({int counter: 0}) {
    _counter = counter;
  }
}

void main() => runApp(MyApp5());

class MyApp5 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Declare the mutable state.
    // Note because the state is not coupled to any particular widget
    // its possible to easily share the state between concerned.
    // StateListeningWidgets register for, and are notified on changes to
    // the state.
    var state = new MyAppState(counter: 5);

    return MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
          appBar: AppBar(
            title: Text('Flutter Demo'),
          ),
          body: Center(
              child: Column(
            children: [
              // When the button is click, increment the state
              RaisedButton(
                onPressed: () => {
                  state.increment(),
                  print("Clicked. New state: ${state.counter}")
                },
                child: Text('Click me'),
              ),

              // Listens for changes in state.
              StateListeningWidget(
                state,
                // Construct the actual widget based on the current state
                // A pure function of the state.
                // However, is seems closures are not hot-reload.
                (context, s) => new Text("Counter4 : ${s.counter}"),
              ),
            ],
          ))),
    );
  }
}

// //////////////////////
// Implementation

// This one is the onChange callback should accept the state.
//typedef OnChangeFunc<ARG0> = void Function(ARG0);
typedef OnChangeFunc = void Function();

mixin ChangeSubscribeable<STATE> {
  final _listener2Notifier =
      new LinkedHashMap<Object, OnChangeFunc>(); // VoidFunc1<STATE>>();
  List<OnChangeFunc> get _listeners => List.from(_listener2Notifier.values);

  void onChange(listenerKey, OnChangeFunc onChange) {
//    onChange(listenerKey, VoidFunc1<STATE> onChange) {
    assert(!_listener2Notifier.containsKey(listenerKey));
    _listener2Notifier[listenerKey] = onChange;
    print("Num listeners: ${_listener2Notifier.length}");
  }

  void removeOnChange(listenerKey) {
    if (_listener2Notifier.containsKey(listenerKey)) {
      _listener2Notifier.remove(listenerKey);
    }
  }

  void notifyListeners() {
    //    _listener2Notifier.forEach((key, value)=>value(state));
    // Safer, in-case state-update triggers add/remove onChange:
    // Call listener
    _listeners.forEach((value) => value());
  }
}

typedef StateToWidgetFunction<WIDGET extends Widget,
        STATE extends ChangeSubscribeable>
    = WIDGET Function(BuildContext, STATE);

void noOp() {}

class _WidgetFromStateImpl<WIDGET extends Widget,
    STATE extends ChangeSubscribeable> extends State<StatefulWidget> {
  STATE _state;

  // TODO. Make Widget return type more specific.
  StateToWidgetFunction<WIDGET, STATE> stateToWidgetFunc;

  _WidgetFromStateImpl(this.stateToWidgetFunc, this._state) {
    updateState(){setState(() {});}
    this._state.onChange(this, updateState);
  }

  @override
  Widget build(BuildContext context) => stateToWidgetFunc(context, this._state);

  @override
  dispose() {
    _state.removeOnChange(this);
    super.dispose();
  }
}

class StateListeningWidget<WIDGET extends Widget,
    STATE extends ChangeSubscribeable> extends StatefulWidget {
  STATE _watched_state;
  StateToWidgetFunction<WIDGET, STATE> stateToWidgetFunc;

  StateListeningWidget(this._watched_state, this.stateToWidgetFunc) {}

  @override
  State<StatefulWidget> createState() {
    return new _WidgetFromStateImpl<WIDGET, STATE>(
        stateToWidgetFunc, _watched_state);
  }
}

我一直针对 ChangeProvider 模式:https://github.com/flutter/samples/blob/master/provider_counter/lib/main.dart

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Flutter Demo Home Page'),),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('You have pushed the button this many times:'),
            Consumer<Counter>(  // <<< Pure. Hidden magic mutable parameter
              builder: (context, counter, child) => Text(
                '${counter.value}',
                style: Theme.of(context).textTheme.display1,
              ),),],),),
      floatingActionButton: FloatingActionButton(
        onPressed: () =>  
            // <<< Also a hidden magic parameter
            Provider.of<Counter>(context, listen: false).increment(),
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

...但这也有问题:

  • 读者不清楚状态要求是什么或如何提供它们 - 接口(至少在这个 github 示例主页中)示例不需要 Counter 作为形式参数。这里我们有 new HomePage(),其参数中没有提供配置 - 这种类型的访问遇到与全局变量类似的问题。

  • 对状态的访问是通过类类型,而不是对象引用 - 因此,如果您想要两个相同类型的对象(例如 shippingAddress、billingAddress),则不清楚(或至少直截了当)该怎么做该模型。为了解决这个问题,可能需要重构状态模型。

【问题讨论】:

  • 除了将setState 重命名为dirty 和将build 重命名为generateView 之外,当前的StatefulWidget 和您的版本有什么区别?
  • 额外的抽象是混淆
  • ... 没有目的的地方
  • 使用您的变体,更改参数(传递给构造函数)将使您的状态 (_counter) 被销毁。您可以通过将道具或状态提取到另一个类中来使其工作,但您又拥有两个类。
  • 修改 InheritedWidget 从其后代中是不可行的,使其确实是纯粹的。但那是题外话。您可以使用ChangeNotifier 而不使用InheritedWidget。这就是为什么我之前也提到了AnimatedBuilder

标签: flutter


【解决方案1】:

我想我支持 user48956。 (顺便提一下名字)。 不幸的是,Flutter 作者似乎在他们的 View 类后面加上了“State”这个词。这相当混淆了整个 Flutter 状态管理讨论。

我认为这两个类的目的实际上是为了让绘画更高效,但它给我们开发人员带来了非常沉重的管道成本。

关于命名约定: 脏标志方法允许小部件绘制器在不知道我们的状态的情况下优化它们的绘制,从而减轻对两个类的需求。 generateView() 也很有意义(当然,除非您开始使用这些小部件来保存模型片段(根据 Package:provider)。

【讨论】:

  • 谢谢 -- Package:provider 很有帮助。
  • 我还发现了 mobx - 它似乎是一个很好的状态依赖跟踪器,缺点是需要构建插件来生成样板。