Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Controlling State from outside of a StatefulWidget

Tags:

flutter

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.

abstract class StartupView {
  Stream<String> get onAppSelected;

  set showActivity(bool activity);
  set message(String message);
}

I would like to create a StatefulWidget StartupPage that implements this interface. I expect the Widget to do the following:

  1. When a button is pressed it would send an event over the onAppSelected stream. A controller would listen to this even and perform some action ( db call, service request etc ).

  2. The controller can call showActivity or set message to have the view show progress with a message.

Because a Stateful Widget does not expose it's State as a property, I don't know the best approach for accessing and modifying the State's attributes.

The way I would expect to use this would be something like:

Widget createStartupPage() {
    var page = new StartupPage();
    page.onAppSelected.listen((app) {
      page.showActivity = true;
      //Do some work
      page.showActivity = false;
    });
  }

I've thought about instantiating the Widget by passing in the state I want it to return in createState() but that feels wrong.

Some background on why we have this approach: We currently have a Dart web application. For view-controller separation, testability and forward thinking towards Flutter we decided that we would create an interface for every view in our application. This would allow a WebComponent or a Flutter Widget to implement this interface and leave all of the controller logic the same.

like image 446
Matthew Smith Avatar asked Sep 05 '17 14:09

Matthew Smith


People also ask

How do you manage state in flutter?

A state management can be divided into two categories based on the duration the particular state lasts in an application. Ephemeral − Last for a few seconds like the current state of an animation or a single page like current rating of a product. Flutter supports its through StatefulWidget.

Why do you need to call setState (() }) in a StatefulWidget when changing some internal data )?

setState is a way to dynamically change the UI. We call it inside the State Object class of the StatefulWidget. Calling setState marks the corresponding Widget dirty .

How do you use stateful widget flutter?

To create a stateful widget in a flutter, use the createState() method. The stateful widget is the widget that describes part of a user interface by building a constellation of other widgets that represent a user interface more concretely. A stateful Widget means a widget that has a mutable state.

How do you use a controller on flutter?

In flutter, controllers are a means to give control to the parent widget over its child state. The main selling point of controllers is that they remove the need of a GlobalKey to access the widget State. This, in turn, makes it harder to do anti-pattern stuff and increase performances.


4 Answers

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
like image 166
Rémi Rousselet Avatar answered Oct 22 '22 21:10

Rémi Rousselet


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.

like image 50
xqwzts Avatar answered Oct 22 '22 22:10

xqwzts


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();
like image 26
BambinoUA Avatar answered Oct 22 '22 23:10

BambinoUA


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

like image 9
Santi Barbat Avatar answered Oct 22 '22 22:10

Santi Barbat