2

My docs and Flutter videos, the explanation of the design of the StatefulWidget (+(Widget)State) is that it:

  1. promotes a declarative design (good)
  2. formalizes the process by which Flutter to efficiently decide which components need to be re-rendered (also good)

From the example:

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) {...}
}

However:

  1. since we have to explicitly remember call setState in order to invalidate the state, is this really a declarative design?
  2. Flutter doesn't automatically detect changes in the State object and decide to call build (although it could have), and so it doesn't really formalize/automate/make-safe the invalidation of view components. Since we have to explicitly call setState, what's the benefit of the Flutter's (Widget)State/StatefulWidget pattern over, let's say:
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 ... ;}
}

... which would place the same burden of marking the UI dirty on the programmer, is no less decalrative, and avoids additional abstraction that obfuscates the intention of the program.

What have I missed? What's the benefit of separating of StatefulWidget from (Widget)State in Flutter?

[Before people chime in with MVC comments, note that the Flutter model rather explicitly only manages only the widget's state and its tightly coupled to the UI's Widget through the build method - there is no separation of concern here and it doesn't have a lot to say about larger application state that's not attached to a view.]

[Also, moderators, these not the same questions: Why does Flutter State object require a Widget?, What is the relation between stateful and stateless widgets in Flutter?. My question is one about what's the benefit of the present design, not how this design works.]

Update: @Rémi Rousselet -- Here's a declarative example with only a new state class needing to be declared. With some work, you could even get rid of that (though it may not be better).

This way of declaring interaction with need didn't require (the user) declaring two new circularly type-referencing class, and the widget that is changing in response to state is decoupled from the state (its constructed a pure function of the state and does not need to allocate the state).

This way of doing things doesn't survive hot-reload. (sad face). I suspect this is more of an issue with hot-reload, but if there's a way to make it work it would be great,

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);
  }
}

I've been directed at the ChangeProvider pattern: 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),
      ),
    );
  }
}

... but this also suffers problems:

  • its not clear to reader of what the state requirements are or how to provide them -- the interface (at least in this github example HomePage) example does not require Counter as a formal parameter. Here we have new HomePage() that has configuration that is not provided in its parameters - this type of access suffers similar problems to global variables.

  • access to state is by class type, not object reference - so its not clear (or at least straightforward) what to do if you want two objects of the same type (e.g. shippingAddress, billingAddress) that are peers in the model. To resolve this, the state model likely needs to be refactored.

user48956
  • 14,850
  • 19
  • 93
  • 154
  • What's the difference between the current `StatefulWidget` and your version, besides renaming `setState` to `dirty` and `build` to `generateView`? – Rémi Rousselet Sep 15 '19 at 16:54
  • Additional abstraction is obfuscation – user48956 Sep 15 '19 at 16:55
  • ... where it doesn’t serve a purpose – user48956 Sep 15 '19 at 16:56
  • Can you elaborate? Because I could explain how they are strictly equivalent – Rémi Rousselet Sep 15 '19 at 16:57
  • My solution didn’t need to declare a WidgetState class. This delegation is a conceptual burden than isn’t necessary (in this example). What, therefore, is its reason to exist? – user48956 Sep 15 '19 at 17:05
  • 3
    With your variant, changing the parameters (passed to the constructor) would make your state (_counter) to be destroyed. You could make it work by extracting the props or state into another class, but you're having two classes again. – Rémi Rousselet Sep 15 '19 at 17:09
  • That assumes its the widget's responsibility to allocate the state (why?) and there's special state class just for this widget (also, why?) This excludes designs where the state is provided by a parent (which is reasonable, since we want the widget to be pure function of state). Suppose I want two different kinds of state? With the current design the State generates one, and only one type of view. I'm not understanding the benefits of the limitations. – user48956 Sep 17 '19 at 00:20
  • @RémiRousselet Its not true that two classes are needed. Interaction can be specified purely declaratively and the user need only introduce a single class for the state that neither inherits State, nor know anything about its Widget view. I've added an example above. – user48956 Sep 17 '19 at 06:31
  • Like I said this example will make you loose your state when the parameters of your widget changes. How do you change `title` without destroying `_counter`? – Rémi Rousselet Sep 17 '19 at 07:02
  • @RémiRousselet My apologies -- forgot to hit save. I've added the fully declarative example. The state is retained for the duration of the MyApp5 -- and could easily be moved into the global context if needed. (This example does seem to have an issue with hot reloaded -- but I believe its a hot reload limitation). – user48956 Sep 17 '19 at 07:20
  • How is that different from ChangeNotifier + provider/AnimatedBuilder? – Rémi Rousselet Sep 17 '19 at 07:56
  • It is a big improvement over the initial state-modifying examples in the docs. However, Consumer and Producer hide the breadcrumbs -- in the example here: https://github.com/flutter/samples/blob/master/provider_counter/lib/main.dart how can the person reading the HomePage class know how to use it? Counter is not an argument to its interface's constructors or methods. – user48956 Sep 17 '19 at 15:22
  • Also, its not obvious what to do if HomePage should have two counters - the linkage to the state is obfuscated (detailed study is required to know how to reuse HomePage), because the linkage is made magically by type (by methods unknown-to-the-caller, and not-required in the interface), not by requiring a reference to the state parameter as a simple functional+declarative interface might. – user48956 Sep 17 '19 at 15:22
  • I think you're missing the concept of Purity/Immutability. Widgets are immutable and the `build` method is pure. This is what makes widgets composable and easy to maintain. Sure, by removing them you could reduce the boilerplate. But as you've seen yourself you'll have bugs, like undesired state loss (on hot-reload, route push, keyboard open, ...) – Rémi Rousselet Sep 17 '19 at 15:33
  • In https://github.com/flutter/samples/blob/master/provider_counter/lib/main.dart, HomePage is not pure. It has externally provided behavior that's not provided as parameter - its secretly accessed in a manner that invisible to the caller. Its a lot like accessing a global variable. – user48956 Sep 17 '19 at 16:58
  • That's still pure. It doesn't modify any variable in the process. That you dislike InheritedWidgets doesn't make them impure :) – Rémi Rousselet Sep 17 '19 at 17:36
  • Is a function impure if it depends on mutable global state? If note, I'd argue that function that depends upon a parent's widget immutable state is similarly impure. What's itching at me here is that: the limitations of these solutions don't seem necessary i) Provider runs into provides when you can't tell the difference between `homeAddress:Address` and `billingAddress:Address` (actually in very first example I hit this). ii) hides the dependencies of object by not requiring the to be provided - this makes for difficult other to follow code because the widget's contract is not formalized. – user48956 Sep 17 '19 at 17:46
  • 1
    Modifying an InheritedWidget is not feasible from its descendants, making it indeed pure. But that's off-topic. You can use `ChangeNotifier` without using `InheritedWidget`. That's why I mentioned `AnimatedBuilder` previously too. – Rémi Rousselet Sep 17 '19 at 18:26

1 Answers1

2

I think I'm with user48956 on this. (Catchy name by the way). Unfortunately, the Flutter authors seem to have suffixed their View class with the word 'State'. This has rather confused the whole Flutter state management discussions.

I think the purpose of the two classes is actually to make the painting more performant but it comes with a very heavy plumbing cost for us developers.

As to the naming convention: The dirty flag approach allows the widget painter to optimise their painting without knowing about our state, thereby alleviation the need for two classes. Also generateView() is kinda meaningful (unless of course, you start using these widgets to hold model-fragments (as per Package:provider).

Chris Reynolds
  • 5,453
  • 1
  • 15
  • 12