stateful_props 0.2.0 copy "stateful_props: ^0.2.0" to clipboard
stateful_props: ^0.2.0 copied to clipboard

Provides a simpler way to create stateful components in flutter. Similar in concept to "hooks" but with a more object oriented flavor.

Provides a simpler way to create stateful components in flutter. Similar in concept to "hooks" but with a more object oriented flavor.

🔨 Installation #

dependencies:
  stateful_props: ^0.2.0

⚙ Import #

import 'package:stateful_props/stateful_props.dart';

Have you ever written Flutter code like this?

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> with TickerProviderStateMixin {
  late TextEditingController textController = TextEditingController();
  late AnimationController fadeInAnim = AnimationController(vsync: this, duration: Duration(seconds: 1));
  late AnimationController scaleAnim = AnimationController(vsync: this, duration: Duration(seconds: 1));
  late FocusNode focusNode = FocusNode(descendantsAreFocusable: false);

  int _count = 0;
  int get count => _count;
  set count(int count) => setState(() => _count = count);

  @override
  void dispose() {
    fadeInAnim.dispose();
    scaleAnim.dispose();
    textController.dispose();
    focusNode.dispose();
    super.dispose();
  }

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

With StatefulPropsMixin you can represent the same thing like this:

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> with StatefulPropsMixin {
  late TextEditingController textController = useProp(TextEditingControllerProp());
  late AnimationController fadeInAnim = useProp(AnimationControllerProp(1));
  late AnimationController scaleAnim = useProp(AnimationControllerProp(1));
  late FocusNode focusNode = useProp(FocusNodeProp(descendantsAreFocusable: false));
  late IntProp counter = useProp(IntProp());

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

In the above example, each 'prop' wraps a certain type of state, FocusNodeProp contains a FocusNode, IntProp contains an Int etc. Each prop gets hooked into the Widget lifecycle, can call setState, and is responsible for disposing and updating itself. All the developer has to do is use it!

Getting a little crazier, if you would like to get rid of StatefulWidget all together, you can do that with the StatefulPropsWidget:

class MyWidget extends StatefulPropsWidget {
  late final textController = useProp('text', () => TextEditingControllerProp());
  late final fadeInAnim = useProp('fade', () => AnimationControllerProp(1, autoStart: false));
  late final scaleAnim = useProp('scale', () => AnimationControllerProp(1, autoStart: false));
  late final focusNode = useProp('focus', () => FocusNodeProp(descendantsAreFocusable: false));
  late IntProp counter = useProp('counter', () => IntProp());

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

Under the hood this works almost identically to StatefulPropsMixin but each prop requires a unique key when it is declared, and use a builder when providing the initial value. In this example we're using strings as the keys, but you could use integers, enums, static objects, valueKeys etc.

NOTE: In our opinion it is probably best to use the mixin in most cases. It has a more succinct syntax and less room for errors as no id is required.

🕹ī¸ Usage #

To use StatefulProps:

  • add the StatefulPropsMixin to your state or extend StatefulPropsWidget
  • call useProp or useDependentProp for each piece of state you need
  • override buildWithProps instead of than build
  • (optionally) override initProps for first-run behavior

As a basic example, we can get a FocusNode by calling useProp with a FocusNodeProp. The prop handles creation and disposal of the node, and exposes various configuration options such as skipTraversal, canRequestFocus etc

class _FocusTestState extends State<FocusTest> with StatefulPropsMixin {
  late final _focus = useProp(FocusNodeProp(skipTraversal: true));

  @override
  Widget buildWithProps(BuildContext context) =>  TextField(focusNode: _focus.node);
}

As mentioned above, props are not limited to stateful objects. They can also be used to reduce nesting with non-visual widgets like LayoutBuilder or GestureDetector. For example, here he will combine an IntProp with a TapDetectorProp to implement a basic counter example:

class _CounterTestState extends State<CounterTest> with StatefulPropsMixin {
  late final _counter = useProp(IntProp());

  // optionally override initProps to implement first-run behavior or
  // declare props that are not referenced inside of build
  void initProps() => useProp(TapDetectorProp(() => _counter.value++));

  @override
  Widget buildWithProps(BuildContext context) {
    return Text('${_counter.value}');
  }
}

In the above example:

  • By default IntProp will rebuild the widget when its value changes, so no setState is required in the widget itself. This can be disabled this with IntProp(autoBuild: false). All the primitives props share this behavior.
  • A TapProp is used, which will wrap the view in a GestureDetector with an onTap handler
  • We must use initProps to declare the tap handler here, since it is never referenced inside of build. Otherwise the prop would never be instantiated. This is because the late keyword uses lazy instantiation (https://blog.gskinner.com/archives/2021/03/flutter-lazy-instantiation-with-the-late-keyword.html).

StatefulPropsMixin vs StatefulPropsWidget #

As mentioned above, there are two primary ways to use StatefulProps. There are some key differences to be aware of:

  • Props used within StatefulPropsWidget require a unique id for each prop, while props used with the mixin do not.
  • Props used within StatefulPropsWidget require a builder, while props used with the mixin do not.
  • useProp can be called within the build method in StatefulPropsWidget, but should be avoided when using the mixin.

Lets take a look at an example of each:

StatefulPropsWidget

class CounterTest extends StatefulPropsWidget {
  late final _counter = useProp('counter', () => IntProp());

  @override
  Widget buildWithProps(BuildContext context) {
    useProp('tap', () => TapDetectorProp(() => _counter.value++));
    return Text('${_counter.value}');
  }
}

StatefulPropsMixin

class _CounterTestState extends State<CounterTest> with StatefulPropsMixin {
  late final _counter = useProp(IntProp());

  @override
  void initProps() => useProp(TapDetectorProp(() => _counter.value++));

  @override
  Widget buildWithProps(BuildContext context) => Text('${_counter.value}');
}

As you can see it there is not much difference between the two. StatefulPropsWidget requires the use of an id and builders which is slightly more awkward and less robust, while StatefulPropsMixin comes with the small cost of an extra class declaration and a few lines of boilerplate.

Managing dependencies #

In a normal StatefulWidget, didUpdateWidget and didChangeDependencies would be called when the widget configuration changes, or an inherited widget has changed. It is normally up to the developer to override these methods, and ensure that they are updating any of their internal state to match the latest external values. This process is quite bug-prone and is often forgotten, resulting in hard to spot bugs or hot-reload that appears to be broken.

With StatefulProps this is all handled for you. If you have a prop that depends on an external value, you can use the useDependentProp method. For example, lets say the duration of our animation, depends on the widget configuration, you could write:

late final _anim = useDependentProp(create: (context, widget){
  return AnimationControllerProp(widget.duration.seconds)
});

Or, if you depended on an InheritedWidget, use the context for lookup instead:

late final _anim = useDependentProp(create: (context, widget){
  return AnimationControllerProp(context.watch<Duration>().seconds)
});

You just need to provide the create method and StatefulProps handles the rest. Each prop is automatically notified when dependencies have changed, can diff the new values, and decide how to handle them. For example, the AnimationControllerProp internally performs a diff that looks something like:

void didChangeDependencies(AnimationControllerProp newProp) {
    if (duration != newProp.duration) {
      _controller.duration = newProp.duration;
    }
    if (vsync != newProp.vsync) {
      vsync = newProp.vsync;
      _controller.resync(vsync ?? this);
    }
    // Callbacks
    onTick = newProp.onTick;
  }

With those code centralized within the prop, it prevents bugs and eliminates significant boilerplate.

State Restoration #

The core library of props support state restoration by default. Simply provide an id to your props, and add a RestorationMixin to your state. Currently this is only supported when using StatefulPropsMixin but we hope to get it working with StatefulPropsWidget in the future.

The core props are opinionated... #

You may already have noticed that the core props are not pedantic. They are opinionated and favor readability and convenience over strict adherence to the underlying objects. For example, AnimationControllerProp accepts a double seconds rather than Duration, it also auto-starts automatically as this is the most common case in our experience.

In another case, LayoutProp exposes a measureContext setting, which will cause the prop to measure the RenderBox and expose the resulting size value. This is not something that is part of the LayoutBuilder but it is often needed when reacting to layout changes so the functionality was added for convenience.

This philosophy is carried across all of the core props but in most cases these are only defaults, and the behavior can be configured as needed.

... if you don't like them, make your own, it's super easy! #

If you would prefer something more strict, you could easily sub-class existing props to change their defaults, or just make your own from scratch in a few minutes. We welcome all contributions from the community in this area and expect that the collection of props will grow in the future.

Creating New Props #

To create a new prop extend the StatefulProp class, and override any of the optional methods:

class MyProp extends StatefulProp<MyProp> {
  @override
  void initState();

  @override
  ChildBuilder getBuilder(ChildBuilder childBuild);

  @override
  void didChangeDependencies(MyProp newProp);

  @override
  void dispose();
}

Because each prop is a proper class, and can create child props, they fully support inheritence and composition, allowing you to easily mix and match existing props to create new types. For example, if you check the source code, you'll see that a single ValueProp<T> is used as the base class for all the primitives (IntProp, BoolProp, StringProp and DoubleProp). Additionally, looking at the FutureProp class, you can see that internally it uses a ValueProp<Future> to store its current future, providing automatic rebuilds each time the future is changed. This shows both how one prop can rely on another, and also how ValueProp<T> can be used to store any stateful object you wish.

🐞 Bugs/Requests #

If you encounter any problems please open an issue. If you feel the library is missing a feature, please raise a ticket on Github and we'll look into it. Pull request are welcome.

📃 License #

MIT License

TODO: Move to blog post: #

The problem: #

Re-using stateful behaviors is harder than it should be in Flutter. Developers must remember to call dispose() or cancel() as well as override didUpdateWidget or didChangeDependency to manually diff dependencies. If any of these things are neglected, bugs are created.

  • Builders solve this, but at the cost of readability, as your tree quickly becomes overly nested.
  • Mixins can also be used to solve this, but their lack of encapsulation makes them ill-suited for wide spread use.

For an exhaustive discussion on this topic, see this issue: https://github.com/flutter/flutter/issues/51752#

A solution #

StatefulProps attempts to solve this, but taking a different approach to state. What if we used many small state objects rather than a single large one?

  • Each of these objects can:
    • initialize, update and dispose itself
    • request a repaint of the current widget (aka setState)
    • create additional child props
    • wrap additional widgets around the build method

This concept of "micro-states" or "stateful properties" is the core concept behind StatefulProps. Each Widget can build up a list of props that stay in memory between builds. Each of these props is fully self-contained, and can respond to lifecycle hooks like init or dispose. The provides a highly effective way of encapsulating re-useable behaviors across widgets.

While it's easy to create custom props (more on this later), this package comes with a full set of defaults out of the box[https://github.com/gskinnerTeam/flutter-stateful-props/tree/master/lib/props]. These include:

  • Primitive props like BoolProp, IntProp etc which maintain a single value and rebuild the widget when changed.
  • Controller props like AnimationControllerProp, TimerProp and TextEditingControllerProp, which properly clean themselves up when their context is destroyed.
  • Wrapper props like LayoutProp which wraps a LayoutBuilder, or TapDetectorProp which wraps a GestureDetector, these help reduce nesting and reduce line count significantly.

As a quick example, here's how you could create a simple blink-forever effect with StatefulProps:

class AnimationTest extends StatefulPropsWidget {
  late final _anim = useProp('anim', () => AnimationControllerProp(1, autoStart: false));

  @override
  Widget buildWithProps(BuildContext context) {
    return Opacity(opacity: _anim.controller.value, child: Text('Hello!'));
  }
}

This probably looks quite foreign, but don't get angry! We'll explain it all in a bit...For now, just note the absense of setState, addListener, forward or dispose. Even our old friend TickerProviderStateMixin is gone. The prop is handling it all!

21
likes
140
points
43
downloads

Publisher

verified publishergskinner.com

Weekly Downloads

Provides a simpler way to create stateful components in flutter. Similar in concept to "hooks" but with a more object oriented flavor.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

flutter

More

Packages that depend on stateful_props