Last updated:
0 purchases
inheritable
Inheritable #
Efficient by default, zero-dependency, declarative state/dependency management for flutter.
Examples #
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)';
}
copied to clipboard
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);
}
}
copied to clipboard
This UserName widget is being used something like
@override
Widget build(BuildContext context) {
return Inheritable(
value: User()..fname = 'John'..lname = 'Doe ',
child: const UserName(),
);
}
copied to clipboard
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);
}
}
copied to clipboard
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(),
);
}
copied to clipboard
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.
Description #
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 a Inheritable.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 for User.lname to be changed. However
UserFirstName has only declared User.fname as a dependency. So
User.lname will be updated without causing rebuild for UserFirstName. 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 do fname.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 example AllCapitalText(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 using fname could later replace it by simply doing
context.aspect((User u) => u.lname, key: fname.key);
The widget would then stop listening for fname changes and start listening for
lname 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())
copied to clipboard
In the above case, you are filtering the values of User.lname to not be
null. When it's null you simply won't be notified, it short-circuits the
chain, so the map won't execute. When it's not null,
only then it will be mapped, compared to last value, and you'd be notified
if it was different.
You would then pass the result around or immediately use it like result.of(context)
Composable: create/remove/reuse Inheritables and Aspects. It is encouraged
to create custom implementations of InheritableAspect, such as a custom
SpreadsheetCellAspect rebuilds a cell if any one of the cells in it's
formula changes, So for a cell with value A1 + B1, it will only rebuild if
A1 | 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 and NoAspect
implementations of InheritableAspect
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 if
User didn't have fname, 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 InheritedWidgets 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.
Notes #
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.
Roadmap #
❌ Complete test suite
❌ Whether to support custom implementations of Inheritable
❌ Add examples
❌ Update README with more examples and use cases
License #
MIT
For personal and professional use. You cannot resell or redistribute these repositories in their original state.
There are no reviews.