Efficient by default, zero-dependency, declarative state/dependency management for flutter.
Consider the following mutable value of User
class User {
String fname;
String lname;
// <proper Object.hashCode & Object.== implementations>
@override
String toString() {
return 'User($fname $lname)';
}
Assume that there is widget to show the user's full name in ALL_CAPS.
class UserName extends StatelessWidget {
@override
Widget build(BuildContext context) {
final fullName = context.aspect((User u) => '${u.fname} ${u.lname}'.trim().toUpperCase());
return Text(fullName);
}
}
This UserName
widget is being used something like
@override
Widget build(BuildContext context) {
return Inheritable(
value: User()..fname = 'John'..lname = 'Doe ',
child: const UserName(),
);
}
For a User(John Doe )
, the Widget would display JOHN DOE
. The widget is
currently listening for changes to the nearest User
, if the user changes to
User(JOHN DOE)
which is essentially the same as what the widget would display
anyway, Inheritable
skips informing the widget about that change. However if
the user were to change to User(John Doe2)
, the final result display to the
user would change to JOHN DOE2
, which is different from the last value, here
Inheritable
will rebuild the UserName
widget.
In the previous example the UserName
widget was using all available aspects of
User
i.e. fname
and lname
. Now consider,
class UserFirstName extends StatelessWidget {
@override
Widget build(BuildContext context) {
final fName = context.aspect((User u) => u.fname.trim().toUpperCase());
return Text(fName);
}
}
Similar to UserName
, UserFirstName
is being used something like
@override
Widget build(BuildContext context) {
return Inheritable(
value: User()..fname = 'John'..lname = 'Doe ',
child: const UserFirstName(),
);
}
Now UserFirstName
would display JOHN
, if the user were to change to
User(John Doe2)
, the UserFirstName
would not be notified, since it only
cares about the first name and Inheritable
know about this. Not only that,
Inheritable also knows that UserFirstName
doesn't care about being notified
until the trimmed and ALL_CAPS value of User.fname
changes to something other
than JOHN
.
Checkout the tests for more examples.
Inheritable was initially made for an internal app, but extracted out to be a separate open-source package.
Inheritable is based on the pre-existing InheritableModel
of flutter. The
concepts are very similar.
Inheritable however has some advantages
-
Moves the decision of whether or not a widget should rebuild to itself. Even if the value held by
Inheritable
changes, along with the aspect, it's still up to the dependent to decide whether to rebuild. -
Optionally allow sending updates e.g.
context.aspect.update(User()..fname = 'Josh' ..lname = 'Doe')
Notice that
update
directly takes the value you want to update, If there is aInheritable.mutable
available, it supplies the new value to it. That's all. Just like whether to rebuild or not is up to the dependent, whether to update the value or not is up to the owner of that value. -
Update a value without depending on it. From the above examples
UserFirstName
could send an update forUser.lname
to be changed. HoweverUserFirstName
has only declaredUser.fname
as a dependency. SoUser.lname
will be updated without causing rebuild forUserFirstName
. This allows for interesting scenarios such as sending data to siblings, parents or children widgets -
Dynamically add aspects to listen for changes
-
Stop listening to changes at a later point.
-
Reuse aspects in multiple widgets. One could create the following aspect and pass it around to multiple widgets
var fname = Aspect((Useru) => u.fname)
The widget would then simply dofname.of(context)
to get the value. Generally these would be dumb widgets that are only used for presentation purposes of a certain value type for exampleAllCapitalText(fname)
would be a widget that requires some string aspect of some value, and display that in ALL_CAPS -
Replace existing dependencies using
Key
. e.g.var fname = Aspect((Useru) => u.fname, key: Key('user.fname'))
A widget usingfname
could later replace it by simply doingcontext.aspect((User u) => u.lname, key: fname.key);
The widget would then stop listening forfname
changes and start listening forlname
changes. This works because the keys for both aspects are same. -
Chaining aspects. One could also do
var result = Aspect((User u) => u.lname) .where((lname) => lname!=null) .map((lname) => lname.trim().toUpperCase())
In the above case, you are filtering the values of
User.lname
to not benull
. When it'snull
you simply won't be notified, it short-circuits the chain, so themap
won't execute. When it's notnull
, only then it will bemapped
, compared to last value, and you'd be notified if it was different. You would then pass the result around or immediately use it likeresult.of(context)
-
Composable: create/remove/reuse
Inheritable
s andAspect
s. It is encouraged to create custom implementations ofInheritableAspect
, such as a customSpreadsheetCellAspect
rebuilds a cell if any one of the cells in it's formula changes, So for a cell with valueA1 + B1
, it will only rebuild ifA1 | B1
changes. And if you aren't building offstage widgets, it would make your spreadsheet even more efficient. -
User definable behaviour for aspects. See
Aspect
andNoAspect
implementations ofInheritableAspect
-
Get asynchronous values in a synchronous fashion
-
Get Static/Compile-time errors for aspects that couldn't exist on a value. Contrary to
InheritableModel
's example use case instead of specifying"fname"
aspect you specify(User u) => u.fname
which wouldn't work ifUser
didn't havefname
, whereas"fname"
would've silently been allowed, until you get a runtime error. -
Short-circuit unnecessary work.
The idea behind Inheritable
is that you specify your dependencies "declaratively"
in a type-safe manner. More often than not I see dependencies being
registered/declared/requested in a non-auto-completable fashion. If it's
type-safe, it can be auto-completed.
Allow Presentation of a value via multiple widgets without causing unnecessary
rebuilds. A User
could be presented by UFistName
& ULastName
widgets. While
it's possible to create InheritedWidget for User.fname
and User.lname
separately both of them will have same runtimeType
. Which turns out to be a
limitation of InheritedWidget
. It doesn't allow multiple InheritedWidget
s of
same runtimeType
to be available at the same time. Which, if you think about it,
is fine, because the users often don't know how to distinctively request for 1
of them and not the other.
Since you'd either have to create 1
InheritedWidget<String>
subclass for supplying String
values to widgets (which
would fail, since the last widget in hierarchy overrides the String
value) or create 2
separate classes InheritedWidget<UserFirstName>
&
InheritedWidget<UserLastName>
which is too verbose and very little reusability.
A single default implementation of Inheritable
would be
enough in this case. However it would be possible to even go further and define
custom behaviour using custom implementations of Inheritable
to allow various
hierarchies, but it remains to be decided whether I want to support that and to
what extent. Custom implementations of InheritableAspect
should be enough in
most cases. Allowing custom Inheritable
implementations would only complicate things.
-
Keys are useful but not required in most common cases
-
"Dependency" is used in the sense of state management, and not in the sense of dependency-injection. However is could be made possible to use
Inheritable
for such use cases -
Using
Inheritable
won't magically make you app perform better and rebuild efficiently, However it will allow you to do just that. It depends on how well you understand flutter, dart, inheritable & most importantly your use case.
- Complete test suite
- Whether to support custom implementations of
Inheritable
- Add examples
- Update README with more examples and use cases
MIT