stateful_props 0.2.0 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 extendStatefulPropsWidget
- call
useProp
oruseDependentProp
for each piece of state you need - override
buildWithProps
instead of thanbuild
- (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 nosetState
is required in the widget itself. This can be disabled this withIntProp(autoBuild: false)
. All the primitives props share this behavior. - A
TapProp
is used, which will wrap the view in aGestureDetector
with anonTap
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 thelate
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 inStatefulPropsWidget
, 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
andTextEditingControllerProp
, which properly clean themselves up when their context is destroyed. - Wrapper props like
LayoutProp
which wraps aLayoutBuilder
, orTapDetectorProp
which wraps aGestureDetector
, 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!