eventual 0.9.0 copy "eventual: ^0.9.0" to clipboard
eventual: ^0.9.0 copied to clipboard

outdated

Data toolkit to manage eventual values and sync UI widgets with ease.

eventual #

Eventual provides a toolkit to manage eventual values, 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 clean and simple approach.

With Eventual you can:

  • Split the data lifecycle from the UI
  • Keep the UI simple and always in sync
  • Build data repositories that work on their own
  • Track the loading, error, non-value and value scenarios
  • Track whether values are fresh or need refreshing
  • Rebuild collections and deep values efficiently

Getting Started #

Check the Installing tab, add eventual as a dependency on pubspec.yaml and import the package within your code.

The lifecycle revolves around 3 classes:

  • EventuaValue<T>
    • Used to manage plain state
  • EventNotifier<T>
    • Manage plain state and emit Listenable events upon change
  • EventualBuilder
    • A Widget that rebuilds whenever the notifier(s) change(s)

Create your first EventNotifier:

export "package:eventual/eventual.dart";

void main() {
    // No value by default
    final someScore = EventNotifier<int>();
    print(someScore.value); // null

    // A default value
    final myScore = EventNotifier<int>(97);
    print(myScore.value); // 97

    // Set to loading, but keep the value
    myScore.loading = true;

    print(myScore.value); // 97
    print(myScore.isLoading); // true

    // Stop loading and set an error message while
    // the old value is still available
    myScore.error = "Something went wrong";

    print(myScore.value); // 97
    print(myScore.isLoading); // false
    print(myScore.hasError); // true
    print(myScore.errorMessage); // "Something went wrong"

    // Set to loading with an optional message
    myScore.loadingMessage = "Please, wait";

    print(myScore.value); // 97
    print(myScore.isLoading); // false
    print(myScore.loadingMessage); // "Please, wait"
    print(myScore.hasError); // false

    // Set a new value
    myScore.value = 110;
    
    // All the available status getters
    print(myScore.value); // 110
    print(myScore.hasValue); // true
    print(myScore.isLoading); // false
    print(myScore.isLoadingFresh); // false
    print(myScore.loadingMessage); // null
    print(myScore.hasError); // false
    print(myScore.errorMessage); // null
    print(myScore.lastUpdated); // DateTime(...)
    print(myScore.lastError); // null
    print(myScore.isFresh); // true
}

Consume the notifier on your Widget tree:

class MyWidget extends StatelessWidget {
    final EventualNotifier<String> userName;
    final EventualNotifier<int> userScore;

    const MyWidget(this.userName, this.userScore);

    @override
    initState() {
        refreshScore();
    }

    @override
    Widget build(BuildContext context) {
        // The widget consumes our eventual data from
        // notifiers [userName, userScore]

        return EventualBuilder(
            notifiers: [userName, userScore],
            builder: (context, notifierList, child) {
                // This builder reruns every time that `userName` or `userScore` change

                // Is it still loading?
                if (userScore.loading) return Text(userScore.loadingMessage ?? "Loading...");
                
                // Does it have an error?
                if (!userScore.hasValue) return Text("${userName.value} has no score");
                else if (userScore.hasError) return Text(userScore.errorMessage);
                
                // All good, use the value
                return Text("${userName.value} has a score of ${userScore.value}");
            }
        );
    }

    // Eventual data being changed
    refreshScore() async {
        userScore.loading = true;
        try {
            final newValue = await fetchNewScore(...),
            userScore.value = newValue; // build is triggered <==
        } catch(err) {
            userScore.error = err.toString(); // build is triggered <==
        }
    }
}

EventualNotifier's can be consumed from a global repository or be created locally. In either case, the widget will update to reflect the latest version of the values.

Collections #

Working with collections of objects is as simple as using a List<*> as the actual value.

However, tracking the state of inner values raises performance concerns and gets out of the scope of an EventualNotifier.

export "package:eventual/eventual.dart";

void main() {
    final numberList = EventNotifier<List<int>>([]);
    print(numberList.value); // []

    // Setting a new list, emits an event
    numberList.value = [1, 2, 3, 4]; // => notifyListeners()
    print(numberList.value); // prints [1, 2, 3, 4]
    // EventualBuilder(builder: ...) would see [1, 2, 3, 4]

    // However, updating the list itself emits no event
    numberList.value.add(5); // => NO EVENT
    print(numberList.value); // prints [1, 2, 3, 4, 5]
    // EventualBuilder(builder: ...) still would see [1, 2, 3, 4]

    // To force a notification, we can call setValue()
    final tempList = numberList.value;
    tempList.add(5);
    numberList.setValue(tempList);
    print(numberList.value); // prints [1, 2, 3, 4, 5]
    // EventualBuilder(builder: ...) now would see [1, 2, 3, 4, 5]

    // Alternatively
    numberList.value.add(6);
    numberList.notifyChange();
    print(numberList.value); // prints [1, 2, 3, 4, 5, 6]
    // EventualBuilder(builder: ...) would also see [1, 2, 3, 4, 5, 6]
}

Is there a way to be notified when an item changes? Depper state can be managed by splitting EventualNotifier's into layers.

In the example above, we could use a List<EventualNotifiers<int>> instead of a List<int>.

void main() {
    final numberList = EventNotifier<List<EventNotifier<int>>>([]);

    final num1 = EventNotifier<int>(10);
    final num2 = EventNotifier<int>();
    final num3 = EventNotifier<int>().setError("Invalid number");
    final num4 = EventNotifier<int>().setToLoading();

    // Updating the list, emits an event to all `numberList` consumers
    numberList.value = [num1, num2, num3, num4]; // NOTIFY

    // Updating the item, emits an event to all `num1` consumers
    final item = numberList.value[0];
    item.value = 200; // NOTIFY
}

Here is a UI example:

void main() {
    // Set an empty list
    final userList = EventNotifier<List<EventNotifier<String>>>();

    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
        // => Triggers MyUserList > build > EventualBuilder > builder()
        userList.error = err.toString();
    });

    runApp(MaterialApp(
        title: 'Eventual',
        home: Scaffold(
            appBar: AppBar(title: Text('Eventual')),
            body: MyUserList(userList),
        ),
    ));
}

class MyUserList extends StatelessWidget {
    final EventNotifier<List<EventNotifier<String>>> userList;

    MyUserList(this.userList);
    
    @override
    Widget build(BuildContext context) {
        // Consume the outer notifier (the list)
        return EventualBuilder(
            notifier: userList,
            builder: (context, _, __) {
                // When `userList` is updated, this builder reruns
                
                // Is the list still loading?
                if (userList.loading)
                    return Text(userList.loadingMessage ?? "Loading users...");
                
                // Does it have an error?
                if (userList.hasError)
                    return Text(userList.errorMessage);
                else if (!userList.hasValue)
                    return Text("There are no users yet");
                
                // All good, use the list value
                final List<EventNotifier<String>> items = userList.value;

                // Build individual children
                return ListView.builder(
                    itemCount: items.length,
                    itemBuilder: (BuildContext ctx, int index) => MyUserCard(items[index]),
                );
            });
    }
}

class MyUserCard extends StatelessWidget {
    final EventNotifier<String> userName;

    MyUserCard(this.userName);
    
    @override
    Widget build(BuildContext context) {
        // Consume an inner notifier
        return EventualBuilder(
            notifier: userName,
            builder: (context, _, __) {
                // When `userList[index]` or `userName` is updated, this builder reruns
                
                // Is the item loading?
                if (userName.loading) return Text(userName.loadingMessage ?? "Loading user name...");
                
                // Does the item have an error?
                if (userName.hasError) return Text(userName.errorMessage);
                else if (!userName.hasValue) return Text("The user has no name yet");
                
                // All good, use the list value
                final EventNotifier<String> name = userName.value;
                return InkWell(
                    child: Text("I am $name"),
                    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 only change an inner item, then only the affected instance of MyUserCard will rebuild.

This is useful for apps with complex and large collections and data to avoid useless repaint work.

Misc #

Eventual is inspired on the core concepts behind Option and Result from Rust;

It also extends many concepts from ValueNotifier<T> in Flutter.

1
likes
0
pub points
0%
popularity

Publisher

unverified uploader

Data toolkit to manage eventual values and sync UI widgets with ease.

Homepage
Repository (GitLab)
View/report issues

License

unknown (LICENSE)

Dependencies

flutter

More

Packages that depend on eventual