Elementary-helper

Elementary Logo

Owner Pub Version Coverage Status Pub points Pub Likes Downloads Contributors License

Description

To make Elementary easier to use, some helpers have been added. Among them are custom implementations of the observer pattern and wrappers to facilitate easier and more testable interactions with Flutter from the WidgetModel.

StateNotifier

The behavior is similar to ValueNotifier but with no requirement to set an initial value. Due to this, the returned value is nullable. StateNotifier's subscribers are notified whenever a state change occurs. Additionally, the subscriber is called for the first time at the moment of subscription. Use accept to emit a new value.

final _somePropertyWithIntegerValue = StateNotifier<int>();

void someFunctionChangeValue() {
  // do something, get new value
  // ...............................................................
  final newValue = _doSomething();
  // and then change value of property
  _somePropertyWithIntegerValue.accept(newValue);
}

EntityStateNotifier

A variant of ValueNotifier that uses a special EntityState object as the state value. EntityState has three states: content, loading, and error. All states can contain data, for cases when you want to keep previous values, such as pagination.

final _countryListState = EntityStateNotifier<Iterable<Country>>();

Future<void> _loadCountryList() async {
  final previousData = _countryListState.value?.data;

  // set property to loading state and use previous data for this state
  _countryListState.loading(previousData);

  try {
    // await the result
    final res = await model.loadCountries();
    // set property to content state, use new data
    _countryListState.content(res);
  } on Exception catch (e) {
    // set property to error state
    _countryListState.error(e, previousData);
  }
}

StateNotifierBuilder

The StateNotifierBuilder is a widget that uses a StateNotifier as its data source. A builder function of the StateNotifierBuilder must return a widget based on the current value passed.

void somewhereInTheBuildFunction() {
  // ......
  StateNotifierBuilder<String>(
    listenableState: someListenableState,
    builder: (ctx, value) {
      return Text(value);
    },
  );
  // ......
}

EntityStateNotifierBuilder

The EntityStateNotifierBuilder is a widget that uses an EntityStateNotifier as its data source. Depending on the state, different builders are called: errorBuilder for error, loadingBuilder for loading, and builder for content.

@override
Widget build(ICountryListWidgetModel wm) {
  return Scaffold(
    appBar: AppBar(
      title: const Text('Country List'),
    ),
    body: EntityStateNotifierBuilder<Iterable<Country>>(
      listenableEntityState: wm.countryListState,
      loadingBuilder: (_, __) => const _LoadingWidget(),
      errorBuilder: (_, __, ___) => const _ErrorWidget(),
      builder: (_, countries) =>
          _CountryList(
            countries: countries,
            nameStyle: wm.countryNameStyle,
          ),
    ),
  );
}

DoubleSourceBuilder

One of the multi-source builders, it uses two ListenableState objects as sources of data. The builder function is called whenever any of the sources change.

void somewhereInTheBuildFunction() {
  // ......
  DoubleSourceBuilder<String, TextStyle>(
    firstSource: captionListenableState,
    secondSource: captionStyleListenableState,
    builder: (ctx, value, style) {
      return Text(value, style: style);
    },
  );
  // ......
}

DoubleValueListenableBuilder

One of the multi-source builders, it uses two ValueListenable objects as sources of data. The builder function is called whenever any of the sources change.

void somewhereInTheBuildFunction() {
  // ......
  DoubleValueListenableBuilder<String, TextStyle>(
    firstValue: captionListenableState,
    secondValue: captionStyleListenableState,
    builder: (ctx, value, style) {
      return Text(value, style: style);
    },
  );
  // ......
}

TripleSourceBuilder

One of the multi-source builders, it uses three ListenableState objects as sources of data. The builder function is called whenever any of the sources change.

void somewhereInTheBuildFunction() {
  // ......
  TripleSourceBuilder<String, int, TextStyle>(
    firstSource: captionListenableState,
    secondSource: valueListenableState,
    thirdSource: captionStyleListenableState,
    builder: (ctx, title, value, style) {
      return Text('$title: ${value ?? 0}', style: style);
    },
  );
  // ......
}

TripleValueListenableBuilder

One of the multi-source builders, it uses three ValueListenable objects as sources of data. The builder function is called whenever any of the sources change.

void somewhereInTheBuildFunction() {
  // ......
  TripleSourceBuilder<String, int, TextStyle>(
    firstSource: captionListenableState,
    secondSource: valueListenableState,
    thirdSource: captionStyleListenableState,
    builder: (ctx, title, value, style) {
      return Text('$title: ${value ?? 0}', style: style);
    },
  );
  // ......
}

MultiListenerRebuilder

A widget that rebuilds part of the UI when one of the Listenable objects changes. The builder function in this widget does not take any values as parameters; you need to get the values directly within the function's body.

void somewhereInTheBuildFunction() {
  // ......
  MultiListenerRebuilder(
    listenableList: [
      firstListenable,
      secondListenable,
      thirdListenable,
    ],
    builder: (ctx) {
      final title = firstListenable.value;
      final value = secondListenable.value;
      final style = thirdListenable.value;
      return Text('$title: ${value ?? 0}', style: style);
    },
  );
  // ......
}

Executor

Executor is a mixin for ElementaryModel that wraps async operations with error handling and lifecycle management. Errors are automatically forwarded to handleError, and in-flight task results are ignored once the model is disposed (the underlying async operations are not cooperatively cancelled).

class MyModel extends ElementaryModel with Executor {
  Future<List<Item>> loadItems() => exec(() async {
    return await _repository.fetchItems();
  });
}

The returned Future completes with the result on success, or completes with the error (and additionally calls handleError) on failure. Tasks that are still running when the model is disposed will never complete their associated Futures — those futures remain pending (even though the underlying async operations may continue running) and are collected by the GC together with the model.

Note: Do not await the result of exec from outside the wm/model's own scopes (e.g. from a global service). Such a caller would be permanently suspended after disposal and would not be collected by the GC as part of the model's lifecycle.

SequentialExecutor

A sequential variant of Executor. Operations submitted via exec are guaranteed to run one at a time, in the order they were submitted. A new operation does not start until the previous one has finished (even if the previous one threw).

class MyModel extends ElementaryModel with SequentialExecutor {
  Future<void> submitForm(FormData data) => exec(() async {
    await _repository.save(data);
  });
}

This is useful when you run a series of operations that mutate state and need a guarantee that each one finishes before the next begins, avoiding race conditions and ensuring the state is updated consistently.

If the handler is disposed while operations are queued, the queued tasks are discarded and will not run.

ExecutionHandler

A standalone class that provides the same behaviour as the Executor mixin, but without any dependency on ElementaryModel. Use it when you need execution handling outside of the Elementary MVVM stack.

SequentialExecutionHandler

A standalone class that provides the same behaviour as the SequentialExecutor mixin, but without any dependency on ElementaryModel. Operations are executed one at a time in submission order.

Maintainer

Maintainer avatar

Mikhail Zotyev

Contributors thanks

Big thanks to all these people, who put their effort into helping the project.

contributors

Special thanks to:

Dmitry Krutskikh, Konoshenko Vlad, and Denis Grafov for the early adoption and the first production feedback;

Alex Bukin for IDE plugins;

All members of the Surf Flutter Team for actively using and providing feedback.

Sponsorship & Support

Special sponsor of the project:

Surf

For all questions regarding sponsorship/collaboration connect with Mikhail Zotyev.

We appreciate any form of support, whether it's a financial donation, a public sharing, or a star on GitHub and a like on Pub. If you want to provide financial support, there are several ways you can do it:

Thank you for all your support!

Libraries

elementary_helper