Controlling State from outside of a StatefulWidget

前端 未结 7 1007
耶瑟儿~
耶瑟儿~ 2020-11-29 18:44

I\'m trying to understand the best practice for controlling a StatefulWidget\'s state outside of that Widgets State.

I have the following interface defined.

<
相关标签:
7条回答
  • 2020-11-29 19:05

    You can use eventify

    This library provide mechanism to register for event notifications with emitter or publisher and get notified in the event of an event.

    You can do something like:

    // Import the library
    import 'package:eventify/eventify.dart';
    final EventEmitter emitter = new EventEmitter();
    
    var controlNumber = 50;
    
    List<Widget> buttonsGenerator() {
        final List<Widget> buttons = new List<Widget>();
        for (var i = 0; i < controlNumber; i++) {
            widgets.add(new MaterialButton(
                      // Generate 10 Buttons afterwards
                      onPressed: () {
                        controlNumber = 10;
                        emitter.emit("updateButtonsList", null, "");
                      },
                    );
        }
    }
    
    class AState extends State<ofYourWidget> {
        @override
        Widget build(BuildContext context) {
            List<Widget> buttons_list = buttonsGenerator();
            emitter.on('updateButtonsList', null, (event, event_context) {
                setState(() {
                    buttons_list = buttonsGenerator();
                });
            });
        }
        ...
    }
    

    I can't think of anything which can't be achieved by event driven programming. You are limitless!

    "Freedom cannot be bestowed — it must be achieved." - Elbert Hubbard

    0 讨论(0)
  • 2020-11-29 19:09

    There is another common used approach to have access to State's properties/methods:

    class StartupPage extends StatefulWidget {
      StartupPage({Key key}) : super(key: key);
    
      @override
      StartupPageState createState() => StartupPageState();
    }
    
    // Make class public!
    class StartupPageState extends State<StartupPage> {
      int someStateProperty;
    
      void someStateMethod() {}
    }
    
    // Somewhere where inside class where `StartupPage` will be used
    final startupPageKey = GlobalKey<StartupPageState>();
    
    // Somewhere where the `StartupPage` will be opened
    final startupPage = StartupPage(key: startupPageKey);
    Navigator.push(context, MaterialPageRoute(builder: (_) => startupPage);
    
    // Somewhere where you need have access to state
    startupPageKey.currentState.someStateProperty = 1;
    startupPageKey.currentState.someStateMethod();
    
    0 讨论(0)
  • 2020-11-29 19:15

    I do:

    class StartupPage extends StatefulWidget {
      StartupPageState state;
    
      @override
      StartupPageState createState() {
        this.state = new StartupPageState();
    
        return this.state;
      }
    }
    
    class DetectedAnimationState extends State<DetectedAnimation> {
    

    And outside just startupPage.state

    0 讨论(0)
  • 2020-11-29 19:20

    While trying to solve a similar problem, I discovered that ancestorStateOfType() and TypeMatcher have been deprecated. Instead, one has to use findAncestorStateOfType(). However as per the documentation, "calling this method is relatively expensive". The documentation for the findAncestorStateOfType() method can be found here.

    In any case, to use findAncestorStateOfType(), the following can be implemented (this is a modification of the correct answer using the findAncestorStateOfType() method):

    class StartupPage extends StatefulWidget {
      static _StartupPageState of(BuildContext context) => context.findAncestorStateOfType<_StartupPageState>();
    
      @override
      _StartupPageState createState() => new _StartupPageState();
    }
    
    class _StartupPageState extends State<StartupPage> {
      ...
    }
    

    The state can be accessed in the same way as described in the correct answer (using StartupPage.of(context).yourFunction()). I wanted to update the post with the new method.

    0 讨论(0)
  • 2020-11-29 19:23

    You can expose the state's widget with a static method, a few of the flutter examples do it this way and I've started using it as well:

    class StartupPage extends StatefulWidget {
      static _StartupPageState of(BuildContext context) => context.ancestorStateOfType(const TypeMatcher<_StartupPageState>());
    
      @override
      _StartupPageState createState() => new _StartupPageState();
    }
    
    class _StartupPageState extends State<StartupPage> {
      ...
    }
    

    You can then access the state by calling StartupPage.of(context).doSomething();.

    The caveat here is that you need to have a BuildContext with that page somewhere in its tree.

    0 讨论(0)
  • 2020-11-29 19:23

    There are multiple ways to interact with other stateful widgets.

    1. findAncestorStateOfType

    The first and most straightforward is through context.findAncestorStateOfType method.

    Usually wrapped in a static method of the Stateful subclass like this :

    class MyState extends StatefulWidget {
      static of(BuildContext context, {bool root = false}) => root
          ? context.findRootAncestorStateOfType<_MyStateState>()
          : context.findAncestorStateOfType<_MyStateState>();
    
      @override
      _MyStateState createState() => _MyStateState();
    }
    
    class _MyStateState extends State<MyState> {
      @override
      Widget build(BuildContext context) {
        return Container();
      }
    }
    

    This is how Navigator works for example.

    Pro:

    • Easiest solution

    Con:

    • Tempted to access State properties or manually call setState
    • Requires to expose State subclass

    Don't use this method when you want to access a variable. As your widget may not reload when that variable change.

    2. Listenable, Stream and/or InheritedWidget

    Sometimes instead of a method, you may want to access some properties. The thing is, you most likely want your widgets to update whenever that value changes over time.

    In this situation, dart offer Stream and Sink. And flutter adds on the top of it InheritedWidget and Listenable such as ValueNotifier. They all do relatively the same thing: subscribing to a value change event when coupled with a StreamBuilder/context.dependOnInheritedWidgetOfExactType/AnimatedBuilder.

    This is the go-to solution when you want your State to expose some properties. I won't cover all the possibilities but here's a small example using InheritedWidget :

    First, we have an InheritedWidget that expose a count :

    class Count extends InheritedWidget {
      static of(BuildContext context) =>
          context.dependOnInheritedWidgetOfExactType<Count>();
    
      final int count;
    
      Count({Key key, @required Widget child, @required this.count})
          : assert(count != null),
            super(key: key, child: child);
    
      @override
      bool updateShouldNotify(Count oldWidget) {
        return this.count != oldWidget.count;
      }
    }
    

    Then we have our State that instantiate this InheritedWidget

    class _MyStateState extends State<MyState> {
      int count = 0;
    
      @override
      Widget build(BuildContext context) {
        return Count(
          count: count,
          child: Scaffold(
            body: CountBody(),
            floatingActionButton: FloatingActionButton(
              onPressed: () {
                setState(() {
                  count++;
                });
              },
            ),
          ),
        );
      }
    }
    

    Finally, we have our CountBody that fetch this exposed count

    class CountBody extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Center(
          child: Text(Count.of(context).count.toString()),
        );
      }
    }
    

    Pros:

    • More performant than findAncestorStateOfType
    • Stream alternative is dart only (works with web) and is strongly integrated in the language (keywords such as await for or async*)
    • Automic reload of the children when the value change

    Cons:

    • More boilerplate
    • Stream can be complicated

    3. Notifications

    Instead of directly calling methods on State, you can send a Notification from your widget. And make State subscribe to these notifications.

    An example of Notification would be :

    class MyNotification extends Notification {
      final String title;
    
      const MyNotification({this.title});
    }
    

    To dispatch the notification simply call dispatch(context) on your notification instance and it will bubble up.

    MyNotification(title: "Foo")..dispatch(context)
    

    Note: you need put above line of code inside a class, otherwise no context, can NOT call notification.

    Any given widget can listen to notifications dispatched by their children using NotificationListener<T> :

    class _MyStateState extends State<MyState> {
      @override
      Widget build(BuildContext context) {
        return NotificationListener<MyNotification>(
          onNotification: onTitlePush,
          child: Container(),
        );
      }
    
      bool onTitlePush(MyNotification notification) {
        print("New item ${notification.title}");
        // true meaning processed, no following notification bubbling.
        return true;
      }
    }
    

    An example would be Scrollable, which can dispatch ScrollNotification including start/end/overscroll. Then used by Scrollbar to know scroll information without having access to ScrollController

    Pros:

    • Cool reactive API. We don't directly do stuff on State. It's State that subscribes to events triggered by its children
    • More than one widget can subscribe to that same notification
    • Prevents children from accessing unwanted State properties

    Cons:

    • May not fit your use-case
    • Requires more boilerplate
    0 讨论(0)
提交回复
热议问题