context_plus 5.0.0 copy "context_plus: ^5.0.0" to clipboard
context_plus: ^5.0.0 copied to clipboard

Convenient value propagation and observing for Flutter. Utilize existing reactive value types more easily and with less boilerplate.

context_plus.webp

Visit context-plus.sonerik.dev for more information and interactive examples.

context_plus #

context_plus context_plus context_plus context_plus

This package combines context_ref and context_watch into a single, more convenient package.

It adds .watch(), .watchOnly(), .watchEffect() extension methods on the Refs to supported observable types, allowing you to write

final changeNotifier = changeNotifierRef.watch(context);
final value = listenableRef.watchOnly(context, (it) => it.someValue);
streamRef.watchEffect(context, (...) => ...);

instead of

final changeNotifier = changeNotifierRef.of(context).watch(context);
final value = listenableRef.of(context).watchOnly(context, (it) => it.someValue);
streamRef.of(context).watchEffect(context, (...) => ...);

Table of Contents #

  1. Installation
  2. Example
  3. Features
  4. Supported observable types for .watch()
  5. 3rd party supported observable types for .watch() via separate packages
  6. API

Installation #

  1. Add context_plus to your pubspec.yaml:
    flutter pub add context_plus
    
  2. Wrap your app in ContextPlus.root:
    ContextPlus.root(
      child: MaterialApp(...),
    );
    
  3. (Optional, but recommended) Wrap default error handlers with ContextPlus.errorWidgetBuilder() and ContextPlus.onError() to get better hot reload related error messages:
    void main() {
      ErrorWidget.builder = ContextPlus.errorWidgetBuilder(ErrorWidget.builder);
      FlutterError.onError = ContextPlus.onError(FlutterError.onError);
    }
    
  4. (Optional) Remove context_ref and context_watch from your pubspec.yaml if you have them.

Example #

final _userId = Ref<String>;
final _statusStream = Ref<Stream<UserStatus>>();

class UserSection extends StatelessWidget {
  const UserSection({
    required this.userId,
  });

  final String userId;

  @override
  Widget build(BuildContext context) {
    // Bind the userId to a Ref to make it accessible without piping
    // it through the widget tree as a parameter
    _userId.bindValue(context, userId);
    // The stream subscription will be initialized only once, unless
    // the userId changes.
    _statusStream.bind(
      context,
      () => repository.getStatusStream(userId: userId),
      key: userId,
    ).watchEffect(context, (snapshot) {
      if (snapshot.error) {
        ScaffoldMessenger.of(context).showSnackBar(...);
      }
    });

    // Child widgets can stay const, reducing their unnecessary rebuilds
    return ...
             ...
               ...
                 child: const _UserStatus()
                   ...
                     ...
                     ...
                       ...
                         child: const _UserAvatar()
                           ...
  }
}

class _UserStatus extends StatelessWidget {
  const _UserStatus();

  @override
  Widget build(BuildContext context) {
    // Get user ID from the ref, just as if it was an InheritedWidget
    final userId = _userId.of(context);
    // Rebuild the widget whenever the status stream notifies
    final statusSnapshot = _statusStream.watch(context);
    ...
  }
}

class _UserAvatar extends StatelessWidget {
  const _UserAvatar();

  static final _opacityAnimController = Ref<AnimationController>();
  static final _avatarBytesFuture = Ref<Future<Uint8List>>();

  @override
  Widget build(BuildContext context) {
    final userId = _userIdRef.of(context);

    // Use Ref to initialize an AnimationController
    _opacityAnimController.bind(
      context,
      (vsync) => AnimationController(vsync, ...),
      key: userId,
    );

    // Use Ref to perform a network call and trigger the animation as a result.
    // Watch the result snapshot right away.
    final avatarBytesSnapshot = _avatarBytesFuture.bind(
      context,
      () async {
        final bytes = await repository.fetchUserAvatar(userId: userId);
        if (context.mounted) {
          // BuildContext is available here, can do UI stuff
          _opacityAnimController.of(context).forward();
        }
        return bytes;
      },
      key: userId,
    ).watch(context);

    if (avatarBytesSnapshot.data != null) {
      return Image.memory(
        avatarBytesSnapshot.data!,
        opacity: _opacityAnimController.of(context),
      );
    }
    ...
  }
}

Features #

  • Ref<T> - a reference to a value of type T bound to a context or multiple contexts.
  • Listenable/ValueListenable/Future/Stream (and more) or Ref of any of these types:
    • .watch(context) - rebuild the context whenever the observable notifies of a change. Returns the current value or AsyncSnapshot for corresponding types.

    • .watchOnly(context, (...) => ...) - rebuild the context whenever the observable notifies of a change, but only if selected value has changed.

    • .watchEffect(context, (...) => ...) - execute the provided callback whenever the observable notifies of a change without rebuilding the context.

    • Multi-value observing of up to 4 values:

      // Observe multiple values from observable objects
      final (value, futureSnap, streamSnap) =
          (valueListenable, future, stream).watch(context);
                                           // or
                                           .watchOnly(context, (...) => ...);
                                           // or
                                           .watchEffect(context, (...) => ...);
      // Observe multiple values from Refs to observable objects
      final (streamSnap, value, futureSnap) =
          (streamRef, valueListenableRef, futureRef).watch(context);
                                                    // or
                                                    .watchOnly(context, (...) => ...);
                                                    // or
                                                    .watchEffect(context, (...) => ...);
      

      All three methods are available for all combinations of observables and observable Refs.

      ** Note: IDE suggestions for watch*() methods on records work only with Dart 3.6 and newer (see dart-lang/sdk#56572).

Supported observable types for Observable.watch() and Ref<Observable>.watch(): #

  • Listenable, ValueListenable:
    • ChangeNotifier
    • ValueNotifier
    • AnimationController
    • ScrollController
    • TabController
    • TextEditingController
    • FocusNode
    • PageController
    • RefreshController
    • ... and any other Listenable derivatives
  • Future, SynchronousFuture
  • Stream
  • ValueStream (from rxdart)
  • AsyncListenable (from async_listenable)

3rd party supported observable types for Observable.watch() via separate packages: #

API #

Ref #

Ref<T> is a reference to a value of type T provided by a parent BuildContext.

It behaves similarly to InheritedWidget with a single value property and provides a conventional .of(context) method to access the value in descendant widgets.

Ref<AnyObservableType> also provides .watch() and .watchOnly() methods to observe the value conveniently.

Ref can be bound only to a single value per BuildContext. Child contexts can override their parents' Ref bindings.

Common places to declare Ref instances are:

  • As a global file-private variable.
    final _value = Ref<ValueType>();
    
    • Useful for sharing values across multiple closely-related widgets (e.g. per-screen values).
  • As a global public variable
    final appTheme = Ref<AppTheme>();
    
    • Useful for sharing values across the entire app.
  • As a static field in a widget class
    class SomeWidget extends StatelessWidget {
      static final _value = Ref<ValueType>();
      ...
    }
    
    • Useful for adding a state to a stateless widget without converting it to a stateful widget. The same applies to all previous examples, but this one is more localized, which improves readability for such use-case.

Ref.bind() #

T Ref<T>.bind(
  BuildContext context,
  T Function() create, {
  void Function(T value)? dispose,
  Object? key,
})
  • Binds a Ref<T> to the value initializer (create) for all descendants of context and context itself.
  • Value initialization happens immediately. Use .bindLazy() if you need it lazy.
  • Value is .dispose()'d automatically when the widget is disposed. Provide a dispose callback to customize the disposal if needed.
  • Similarly to widgets, key parameter allows for updating the value initializer when needed.

Ref.bindLazy() #

void Ref<T>.bindLazy(
  BuildContext context,
  T Function() create, {
  void Function(T value)? dispose,
  Object? key,
})

Same as Ref.bind(), but the value is created only when it's first accessed via Ref.of(context) or Ref.watch()/Ref.watchOnly(), thus not returned immediately.

Ref.bindValue() #

T Ref<T>.bindValue(
  BuildContext context,
  T value,
)
  • Binds a Ref<T> to the value for all descendants of context and context itself.
  • Whenever the value changes, the dependent widgets will be automatically rebuilt.
  • Values provided this way are not disposed automatically.

Ref.of(context) #

T Ref<T>.of(BuildContext context)

Ref<Observable>.watch() and Observable.watch() #

TListenable TListenable.watch(BuildContext context)
TListenable Ref<TListenable>.watch(BuildContext context)
  • Rebuilds the widget whenever the Listenable notifies about changes.
T ValueListenable<T>.watch(BuildContext context)
T Ref<ValueListenable<T>>.watch(BuildContext context)
  • Rebuilds the widget whenever the ValueListenable notifies about changes.
  • Returns the current value of the ValueListenable.
AsyncSnapshot<T> Future<T>.watch(BuildContext context)
AsyncSnapshot<T> Ref<Future<T>>.watch(BuildContext context)

AsyncSnapshot<T> Stream<T>.watch(BuildContext context)
AsyncSnapshot<T> Ref<Stream<T>>.watch(BuildContext context)

AsyncSnapshot<T> AsyncListenable<T>.watch(BuildContext context)
AsyncSnapshot<T> Ref<AsyncListenable<T>>.watch(BuildContext context)
  • Rebuilds the widget whenever the value notifies about changes.
  • Returns and AsyncSnapshot describing the current state of the value.
  • .watch()'ing a SynchronousFuture or ValueStream (from rxdart) will return a AsyncSnapshot with properly initialized data/error field, if initial value/error exists.
  • AsyncListenable can be used for dynamic swapping of the listened-to async value without losing the current state. See the live search example for a practical use-case.

Many popular observable types from 3rd party packages have their own .watch() methods provided by separate packages. See the 3rd party supported observable types for more information.

Ref<Observable>.watchOnly() and Observable.watchOnly() #

R TListenable.watchOnly<R>(
  BuildContext context,
  R Function(TListenable listenable) selector,
)

R ValueListenable<T>.watchOnly<R>(
  BuildContext context,
  R Function(T value) selector,
)
  • Invokes selector whenever the Listenable notifies about changes.
  • Rebuilds the widget whenever the selector returns a different value.
  • Returns the selected value.
R Future<T>.watchOnly<R>(
  BuildContext context,
  R Function(AsyncSnapshot<T> value) selector,
)

R Stream<T>.watchOnly<R>(
  BuildContext context,
  R Function(AsyncSnapshot<T> value) selector,
)
  • Invokes selector whenever the async value notifies about changes.
  • Rebuilds the widget whenever the selector returns a different value.
  • Returns the selected value.

Ref<Observable>.watchEffect() and Observable.watchEffect() #

void TListenable.watchEffect(
  BuildContext context,
  void Function(TListenable listenable) effect, {
  Object? key,
  bool immediate = false,
  bool once = false,
})

void ValueListenable<T>.watchEffect(
  BuildContext context,
  void Function(T value) effect, {
  Object? key,
  bool immediate = false,
  bool once = false,
})

void Future<T>.watchEffect(
  BuildContext context,
  void Function(AsyncSnapshot<T> snapshot) effect, {
  Object? key,
  bool immediate = false,
  bool once = false,
})

void Stream<T>.watchEffect(
  BuildContext context,
  void Function(AsyncSnapshot<T> snapshot) effect, {
  Object? key,
  bool immediate = false,
  bool once = false,
})
  • Invokes effect on each value change notification.
  • Does not rebuild the widget on changes.
  • key parameter allows for uniquely identifying the effect, which is needed for conditional effects, immeadiate and once effects.
  • immediate parameter allows for invoking the effect immediately after binding. Requires a unique key. Can be combined with once.
  • once parameter allows for invoking the effect only once. Requires a unique key. Can be combined with immediate.
  • Can be used conditionally, in which case the .unwatchEffect() usage is recommended as well.

Ref<Observable>.unwatchEffect() and Observable.unwatchEffect()

void Listenable.unwatchEffect(
  BuildContext context, {
  required Object key,
})

void Future.unwatchEffect(
  BuildContext context, {
  required Object key,
})

void Stream.unwatchEffect(
  BuildContext context,
  required Object key,
)
  • Unregisters the effect with the specified key.
  • Useful for conditional effects where removing the effect ASAP is needed:
    if (condition) {
      observable.watchEffect(context, key: 'effect', ...);
    } else {
      observable.unwatchEffect(context, key: 'effect');
    }
    
23
likes
150
points
7.46k
downloads

Publisher

verified publishersonerik.dev

Weekly Downloads

Convenient value propagation and observing for Flutter. Utilize existing reactive value types more easily and with less boilerplate.

Repository (GitHub)
View/report issues

Topics

#reactive #ui #state-management #extension

Documentation

API reference

License

MIT (license)

Dependencies

context_ref, context_watch, context_watch_base, flutter

More

Packages that depend on context_plus