From 1730edb2ae59f7a3a5c5a4bc184aa1c3e1e9e235 Mon Sep 17 00:00:00 2001 From: devkabiir Date: Sat, 16 Jan 2021 18:51:57 +0530 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=96=20(inheritable)=20Release=20v1.0.0?= =?UTF-8?q?-dev.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - First public release - Introduce `Inheritable`, Allows access to immutable value or selective aspects. Only notifies dependents if the selected aspect changes. - Introduce `Inheritable.mutable`, Allows access to immutable/mutable value or selective aspects. Allows updating the value provided by it. Compatible with `Inheritable.of(context)` usage. --- .gitignore | 4 + CHANGELOG.md | 10 ++ LICENSE | 21 ++++ README.md | 220 +++++++++++++++++++++++++++++++++++++ pubspec.lock | 147 +++++++++++++++++++++++++ pubspec.yaml | 19 ++++ test/inheritable_test.dart | 2 +- 7 files changed, 422 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 pubspec.lock create mode 100644 pubspec.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6af1a66 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +# Pb/Dart/Flutter reproducible artifacts +.dart_tool +build +.packages diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e191064 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +## 1.0.0-dev.1 + +- First public release +- Introduce `Inheritable`, Allows access to immutable value or selective + aspects. Only notifies dependents if the selected aspect changes. +- Introduce `Inheritable.mutable`, Allows access to immutable/mutable value or + selective aspects. Allows updating the value provided by it. Compatible with + `Inheritable.of(context)` usage. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..757eb9e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Dinesh Ahuja + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c3dfe59 --- /dev/null +++ b/README.md @@ -0,0 +1,220 @@ +# Inheritable [pre-release] + +Efficient by default, zero-dependency, declarative state/dependency management for flutter. + +## Examples + +Consider the following mutable value of `User` + +```dart +class User { + String fname; + String lname; + + // + + @override + String toString() { + return 'User($fname $lname)'; +} +``` + +Assume that there is widget to show the user's full name in ALL_CAPS. + +```dart +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 + +```dart + @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, + +```dart +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 + +```dart + @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. + +## 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. Once 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 + + ```dart + 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 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 `Inheritable`s and `Aspect`s. 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`, but `"fname"` would've silently be 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` 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` & +`InheritedWidget` 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 no 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 diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..5668b33 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,147 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.4.2" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.3" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.14.13" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.8" + meta: + dependency: "direct main" + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.8" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.5" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.17" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" +sdks: + dart: ">=2.9.0 <3.0.0" + flutter: ">=1.20.4 <1.26.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..eefbb00 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,19 @@ +name: inheritable +description: Efficient by default zero-dependency declarative state/dependency management for flutter +homepage: https://github.com/devkabiir/inheritable +repository: https://github.com/devkabiir/inheritable +issue_tracker: https://github.com/devkabiir/inheritable/issues +version: 1.0.0-dev.1 + +environment: + sdk: '>=2.9.0 <3.0.0' + flutter: '>=1.20.4 <1.26.0' + +dependencies: + flutter: + sdk: flutter + meta: ^1.1.8 + +dev_dependencies: + flutter_test: + sdk: flutter diff --git a/test/inheritable_test.dart b/test/inheritable_test.dart index 33a0fd2..bbea345 100644 --- a/test/inheritable_test.dart +++ b/test/inheritable_test.dart @@ -1,4 +1,4 @@ -import 'package:elistapp/inheritable.dart'; +import 'package:inheritable/inheritable.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart';