Skip to content

evanswanyoike/eventual_neo

# Eventual for Flutter Eventual provides a flexible and composable toolkit to perform State Management. Track the status of **eventual** data, **notify** the UI when changes occur and **rebuild** the relevant widget subtree accordingly. This package allows for great flexibility in complex scenarios while keeping the focus on a clean and simple approach. With Eventual you can: - Split the data lifecycle from the UI - Keep the UI simple and always in sync - Work with composable and straightforward data repositories - Render collections and deep structures efficiently - Track the availability of data and show only relevant pixels - Track whether values are fresh or need updating - Get leaner stateful widgets than `StatefulWidget` ## Getting Started ### Setting some eventual data Create your first `EventualNotifier` wrapping an `int`: ```dart export "package:eventual/eventual.dart"; void main() { // No value by default final someScore = EventualNotifier(); print(someScore.value); // null print(someScore.hasValue); // false // With a default value final userScore = EventualNotifier(42); print(userScore.value); // 42 } ``` Set the status to `loading` before you fetch data from somewhere: ```dart { // ... // Set to loading, but keep the value userScore.loading = true; print(userScore.value); // 42 print(userScore.isLoading); // true final newValue = await successfulNumberFetch(/*...*/); } ``` Get the new value and update it (stops `loading`) ```dart { // ... // Set the new value userScore.value = newValue; print(userScore.value); // 110 print(userScore.hasValue); // true print(userScore.isLoading); // false } ``` Set to `loading` again, and fetch a value that may fail: ```dart { // ... userScore.loadingMessage = "Please, wait..."; try { // This would update the value if successful, but... userScore.value = await failingNumberFetch(/*...*/); } catch (err) { // Stops loading, keeps the previous value // and sets the error message userScore.error = "Something went wrong"; } print(userScore.value); // 42 print(userScore.isLoading); // false print(userScore.hasError); // true print(userScore.errorMessage); // "Something went wrong" } ``` ### Displaying our eventual notifiers Now we have an `EventualNotifier` with data. Let's consume it on our Widget tree with an `EventualBuilder`: ```dart class MyWidget extends StatelessWidget { final EventualNotifier userScore = EventualNotifier(); const MyWidget(this.userScore) { refreshScore(); } @override Widget build(BuildContext context) { // The widget consumes our eventual data from userScore return EventualBuilder( notifier: userScore, // notifiers: [userScore, ...], builder: (context, notifiers, child) { // This builder reruns every time that `userScore` changes // Is it still loading? if (userScore.loading) return Text(userScore.loadingMessage ?? "Loading..."); // Is there an error? if (!userScore.hasValue) return Text("The user has no score"); else if (userScore.hasError) return Text(userScore.errorMessage); // All good, use the value return Text("The user has a score of ${userScore.value}"); } ); } // Eventual data being updated refreshScore() { userScore.loading = true; fetchNewScore(...) .then((newValue) => userScore.value = newValue) .catchError((err) => userScore.error = err.toString()); } } ``` `EventualNotifier`'s can be from a global repository or be created locally. In either case, the widget will rebuild to reflect the latest version of the value. ### Single notifier widget While `EventualNotifier` supports one or more notifiers, you can also use `EventualSingleBuilder` if you only need to consume one. This provides a cleaner way to achieve a similar result: ```dart class MyWidget extends StatelessWidget { final EventualNotifier userScore; const MyWidget(this.userScore) { refreshScore(); } @override Widget build(BuildContext context) { // The widget consumes our eventual data from userScore return EventualSingleBuilder( notifier: userScore, // optional builder loadingBuilder: (ctx, notifier, child) => Text(userScore.loadingMessage ?? "Loading..."), // optional builder errorBuilder: (ctx, notifier, child) => Text(userScore.errorMessage), // optional builder emptyBuilder: (ctx, notifier, child) => Text("The user has no score"), // required builder: (ctx, notifier, child) { // All good, use the value return Text("The user has a score of ${userScore.value}"); } ); } // Eventual data being updated refreshScore() { userScore.loading = true; fetchNewScore(...) .then((newValue) => userScore.value = newValue) .catchError((err) => userScore.error = err.toString()); } } ``` ## Collections and data structures Working with collections of objects is as simple as using a `List<*>` as the actual `value`, whereas struct's can be managed with `Map<*, *>`'s or custom classes. `EventualNotifier` does a shallow comparison once a new value is pushed to it. However, **tracking the inner changes** within the `value` itself is **out of the reach** of an `EventualNotifier`. In such case, an explicit notification would be needed with `notifyChange()`. ### Nested change notifications A better way to be notified when a **nested item changes** is by using `EventualNotifier`'s in layers. Below is an example that uses `List>` instead of a `List`. ```dart void main() { // A few notifiers final num1 = �EventualNotifier(10); final num2 = �EventualNotifier(); final num3 = �EventualNotifier().setError("Invalid number"); final num4 = �EventualNotifier().setToLoading(); // A list of notifiers final numberList = �EventualNotifier>>([num1, num2]); // Updating the list, emits an event to all `numberList` consumers numberList.value = [num1, num2, num3, num4]; // => EMIT EVENT // Updating the item, emits an event to all `num1` consumers, // and not `numberList` final notifierItem = numberList.first; notifierItem.value = 200; // => EMIT EVENT } ``` Here is a UI example: ```dart void main() { // Set an empty list final userList = �EventualNotifier>>(); userList.loadingMessage = "Please, wait..."; // Load some dynaimc data myFetchUserList(...).then((users) { // Update the list value // => Triggers MyUserList > build > EventualBuilder > builder() userList.value = users; }).catchError((err) { // Update the list state // => Triggers MyUserList > build > EventualBuilder > builder() userList.error = err.toString(); }); // Paint the list runApp(MaterialApp( title: 'Eventual', home: Scaffold( appBar: AppBar(title: Text('Eventual')), body: MyUserList(userList), ), )); } // The collection class MyUserList extends StatelessWidget { final �EventualNotifier>> userList; MyUserList(this.userList); @override Widget build(BuildContext context) { // Consume the user list return EventualSingleBuilder( notifier: userList, loadingBuilder: (ctx, notifier, child) => Text(userList.loadingMessage ?? "Loading users..."), errorBuilder: (ctx, notifier, child) => Text(userList.errorMessage), emptyBuilder: (ctx, notifier, child) => Text("There are no users yet"), builder: (ctx, notifier, child) { // All good, use the value final items = userList.value; // Build individual children return ListView.builder( itemCount: items.length, itemBuilder: (BuildContext ctx, int index) => MyUserCard(items[index]), ); } ); } } // An item class MyUserCard extends StatelessWidget { final �EventualNotifier userName; MyUserCard(this.userName); @override Widget build(BuildContext context) { // Consume an inner notifier return EventualSingleBuilder( notifier: userName, loadingBuilder: (ctx, notifier, child) => Text(userList.loadingMessage ?? "Loading the user's name..."), errorBuilder: (ctx, notifier, child) => Text(userList.errorMessage), emptyBuilder: (ctx, notifier, child) => Text("This user has no name yet"), builder: (ctx, notifier, child) { // When `userList[index]` is updated, the appropriate builder reruns // All good, use the list value final name = userName.value; return InkWell( child: Text("I am $name\n\nTap to update me"), onTap: () { // Update the user value userName.loading = "This is a fake loading request"; userName.setError = "Something wrong happened"; // ... userName.value = "I have been tapped"; } ); } ); } } ``` This approach allows to efficiently update only the widgets that are using a certain part of the data. - If the whole list changes, then `MyUserList > ListView.builder()` will rebuild a few rows. - But if we update the second list element, then only the affected `MyUserCard` will rebuild (if visible). This is useful for apps with complex and large collections and data to avoid useless repaint work. ## Leaner Stateful Widgets With Eventual, you can turn code like this: ```dart class HomePage extends StatefulWidget { final List userList; // External data final List imageList; // External data HomePage(this.userList, this.imageList); // External data @override _HomePageState createState() => _HomePageState(); } class _HomePageState extends State { int counter = 0; @override Widget build(BuildContext context) { return Scaffold( body: Column( children: [ buildHeader(), buildCounter(), // The only state-bound branch buildUserList(widget.userList), // expensive build buildImageList(widget.imageList), // expensive build buildFooter() ], ), floatingActionButton: FloatingActionButton( onPressed: () { this.setState(() => counter++); // << Update Counter }, child: Icon(Icons.navigation), ), ); } Widget buildCounter () { return Text("Counter: $counter"); } // ... } ``` Into a more efficient `StatelessWidget` like this: ```dart class HomePage extends StatelessWidget { final �EventualNotifier counter = �EventualNotifier(0); // << Our stateful value final List userList; // External data final List imageList; // External data HomePage(this.userList, this.imageList); // External data @override Widget build(BuildContext context) { return Scaffold( body: Column( children: [ buildHeader(), buildCounter(), // The only state-bound branch buildUserList(widget.userList), // expensive build buildImageList(widget.imageList), // expensive build buildFooter() ], ), floatingActionButton: FloatingActionButton( onPressed: () { counter.value = counter.value + 1; // << Update Counter }, child: Icon(Icons.navigation), ), ); } Widget buildCounter () { return EventualBuilder( notifier: counter, builder: (context, _, __) { return Text("Counter: $counter"); }, ); } } ``` You may have spotted a few differences: - The `StatefulWidget` rebuilds the entire widget even if only `counter` changes - The "eventual" version calls `build()` just once and `buildCounter()` if needed - One class vs two classes for one Widget ## �EventualNotifier When using a `EventualNotifier` or a `EventualValue` you can query the following fields: ```dart void main() { final someNumber = �EventualNotifier(110); // The raw value or `null` print(someNumber.value); // Whether a non-null value is set print(someNumber.hasValue); // Whether the value is set to be loading print(someNumber.isLoading); // Whether loading was called up to 10 seconds ago // Customizable, see below print(someNumber.isLoadingFresh); // An optional message about what is being loaded or null print(someNumber.loadingMessage); // Whether an error is set print(someNumber.hasError); // The message of the last error print(someNumber.errorMessage); // DateTime when the value was set print(someNumber.lastUpdated); // DateTime when the error was set print(someNumber.lastError); // Whether the value was set 10 or more seconds ago // Customizable, see below print(someNumber.isFresh); // Modifiers final someName = �EventualNotifier("hello") // Consider the value obsolete after 60 seconds .withFreshnessTimeout(Duration(seconds: 60)) // Consider the loading stale after 5 seconds .withLoadingTimeout(Duration(seconds: 5)); // Whether the value was set 10 or more seconds ago // Customizable, see below print(someName.isFresh); // Whether loading was called up to 10 seconds ago // Customizable, see below print(someName.isLoadingFresh); // Combine them all sequentially final someDate = �EventualNotifier() .setDefaultValue(DateTime.now()) .withFreshnessTimeout(Duration(seconds: 1)) .withLoadingTimeout(Duration(milliseconds: 50)) .setError("Invalid date") .setToLoading("Determining the current date") .setValue(DateTime.now()); print(someDate.value); // DateTime(...) } ``` ## Misc Eventual is inspired on the core concepts behind [Option](https://doc.rust-lang.org/rust-by-example/std/option.html) and [Result](https://doc.rust-lang.org/rust-by-example/std/result.html) from [Rust](https://www.rust-lang.org/); It also extends many concepts from `ValueNotifier` in [Flutter](https://flutter.dev). ## Future work - [X] Provide a widget with builders for `loading`, `error` and `value` cases - [ ] Add a wrapper for lists and maps # eventual_neo

About

No description, website, or topics provided.

Resources

License

Code of conduct

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages