context_plus 5.0.0
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.
Visit context-plus.sonerik.dev for more information and interactive examples.
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 #
- Installation
- Example
- Features
- Supported observable types for
.watch() - 3rd party supported observable types for
.watch()via separate packages - API
Installation #
- Add
context_plusto yourpubspec.yaml:flutter pub add context_plus - Wrap your app in
ContextPlus.root:ContextPlus.root( child: MaterialApp(...), ); - (Optional, but recommended) Wrap default error handlers with
ContextPlus.errorWidgetBuilder()andContextPlus.onError()to get better hot reload related error messages:void main() { ErrorWidget.builder = ContextPlus.errorWidgetBuilder(ErrorWidget.builder); FlutterError.onError = ContextPlus.onError(FlutterError.onError); } - (Optional) Remove
context_refandcontext_watchfrom yourpubspec.yamlif 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 typeTbound to acontextor multiplecontexts..bind(context, () => ...)- create and bind value to acontext. Automaticallydispose()the value uponcontextdisposal..bindLazy(context, () => ...)- same as.bind(), but the value is created only when it's first accessed..bindValue(context, ...)- bind an already created value to acontext. The value is not disposed automatically..of(context)- get the value bound to thecontextor its nearest ancestor.
Listenable/ValueListenable/Future/Stream(and more) orRefof any of these types:-
.watch(context)- rebuild thecontextwhenever the observable notifies of a change. Returns the current value orAsyncSnapshotfor corresponding types. -
.watchOnly(context, (...) => ...)- rebuild thecontextwhenever 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 thecontext. -
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:ChangeNotifierValueNotifierAnimationControllerScrollControllerTabControllerTextEditingControllerFocusNodePageControllerRefreshController- ... and any other
Listenablederivatives
Future,SynchronousFutureStreamValueStream(from rxdart)AsyncListenable(from async_listenable)
3rd party supported observable types for Observable.watch() via separate packages: #
Bloc,Cubit(from bloc, using context_watch_bloc)Observable(from mobx, using context_watch_mobx)Rx(from get, using context_watch_getx)Signal(from signals, using context_watch_signals)
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 ofcontextandcontextitself. - Value initialization happens immediately. Use
.bindLazy()if you need it lazy. - Value is
.dispose()'d automatically when the widget is disposed. Provide adisposecallback to customize the disposal if needed. - Similarly to widgets,
keyparameter 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 thevaluefor all descendants ofcontextandcontextitself. - 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)
- Fetches the value of type
Tprovided by theRef<T>.bind()/Ref<T>.bindLazy()/Ref<T>.bindValue()in the nearest ancestor ofcontext. - Rebuilds the widget whenever the value provided with
Ref<T>.bindValue()changes. - Throws an exception if the value is not provided in the ancestor tree.
Ref<Observable>.watch() and Observable.watch() #
TListenable TListenable.watch(BuildContext context)
TListenable Ref<TListenable>.watch(BuildContext context)
- Rebuilds the widget whenever the
Listenablenotifies about changes.
T ValueListenable<T>.watch(BuildContext context)
T Ref<ValueListenable<T>>.watch(BuildContext context)
- Rebuilds the widget whenever the
ValueListenablenotifies 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
AsyncSnapshotdescribing the current state of the value. .watch()'ing aSynchronousFutureorValueStream(from rxdart) will return aAsyncSnapshotwith properly initializeddata/errorfield, if initial value/error exists.AsyncListenablecan 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
selectorwhenever theListenablenotifies about changes. - Rebuilds the widget whenever the
selectorreturns 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
selectorwhenever the async value notifies about changes. - Rebuilds the widget whenever the
selectorreturns 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
effecton each value change notification. - Does not rebuild the widget on changes.
keyparameter allows for uniquely identifying the effect, which is needed for conditional effects,immeadiateandonceeffects.immediateparameter allows for invoking the effect immediately after binding. Requires a uniquekey. Can be combined withonce.onceparameter allows for invoking the effect only once. Requires a uniquekey. Can be combined withimmediate.- 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'); }
