property_change_notifier 0.1.0 property_change_notifier: ^0.1.0 copied to clipboard
A drop-in replacement for ChangeNotifier for observing only certain properties of a model.
property_change_notifier #
A drop-in replacement for ChangeNotifier for observing only certain properties of a model.
Why? #
ChangeNotifier is useful for observing changes to a model. The problem is that it takes an all-or-none approach. There is no way to listen only to specific properties. To do so requires every property to be implemented as a ValueNotifier or similar. Even then, it is not possible to listen to multiple properties with a single listener. The Observable package has some powerful tools but they are not backwards-compatible with ChangeNotifier.
PropertyChangeNotifier
is an implementation of a more granular observer pattern similar to PropertyChangeListener in Java and INotifyPropertyChanged in .NET. When a property changes, the name of the property is included in the notification. Listeners can then choose to observe only one or many properties.
How? #
PropertyChangeNotifier
works by extending ChangeNotifier in a way that makes it 100% backwards compatible with existing code.
- Replace
ChangeNotifier
withPropertyChangeNotifier
in your model. - Update your model to include the property name when calling
notifyListeners()
. - When ready, update existing listeners to observe only specific properties.
Usage in Dart #
Model implementation #
class MyModel with PropertyChangeNotifier<String> {
String _foo;
String _bar;
String get foo => _foo;
String get bar => _bar;
set foo(String value) {
_foo = value;
notifyListeners('foo');
}
set bar(String value) {
_bar = value;
notifyListeners('bar');
}
}
You can use PropertyChangeNotifier
as a superclass or a mixin. <String>
is the generic type of the property you provide to notifyListeners()
. This is typically a String
but can be any type.
Listening to a single property #
Provide an additional parameter containing the property name you wish to observe, wrapped in an Iterable (typically a List):
final model = MyModel();
model.addListener(_listener, ['foo']);
void _listener() {
print('foo was changed');
}
Listening to multiple properties #
Provide a Iterable containing the property names you wish to observe. The listener will be invoked when any of the given properties change. If the listener accepts a property parameter, it will be provided the name of the property that changed.
final model = MyModel();
model.addListener(_listener, ['foo', 'bar']);
void _listener(String property) {
print('$property was changed');
}
Listening to all properties #
This is the default behavior of ChangeNotifier
and remains the same, even if you've update your model to invoke notifyListeners()
with a property name. If the listener accepts a property parameter, it will be provided the name of the property that changed.
final model = MyModel();
model.addListener(_listenerOne);
model.addListener(_listenerTwo);
void _listenerOne() {
print('model was changed');
}
void _listenerTwo(String property) {
print('$property was changed');
}
Adding listeners #
Listeners can be added at any time. A listener cannot be null
. Adding a listener with no parameters will cause it to listen to all properties. The same listener can be added to multiple properties. Adding the same listener again is a no-op. It doesn't hurt to add a listener to a non-existent property, but it serves no purpose; PropertyChangeNotifier
has no way of knowing if the property actually exists.
final model = MyModel();
model.addListener(_fooListener, ['foo']);
model.addListener(_bothListener, ['foo', 'bar']);
model.addListener(_allListener);
// _fooListener is listening to foo only.
// _bothListener is listening to foo and bar only.
// _allListener is listening to all properties.
Removing listeners #
Listeners can be removed at any time. A listener can be removed from one or more properties without being removed from other properties. Removing a listener that does not exist is a no-op.
final model = MyModel();
model.addListener(_listener, ['foo', 'bar', 'baz']);
model.removeListener(_listener, ['bar', 'baz']);
// _listener is now listening to foo only.
String constants as property names #
Referring to properties using string literals is error-prone and leads to stringly-typed code. To avoid this, you can reference string constants in both your model and listeners so that they can be safely checked by the compiler:
// Properties
abstract class MyModelProperties {
static String get foo => 'foo';
static String get bar => 'bar';
}
// Model
class MyModel with PropertyChangeNotifier<String> {
set foo(String value) {
_foo = value;
notifyListeners(MyModelProperties.foo);
}
…
}
// Listener
final model = MyModel();
model.addListener(_listener, [MyModelProperties.foo]);
Enum values as property names #
It might be more convenient to use enum values for property names. Remember to pass the property type as a generic argument to the PropertyChangeNotifier
declaration.
// Properties
enum MyModelProperties {
foo,
bar,
}
// Model
class MyModel with PropertyChangeNotifier<MyModelProperties> {
set foo(String value) {
_foo = value;
notifyListeners(MyModelProperties.foo);
}
…
}
// Listener
final model = MyModel();
model.addListener(_listener, [MyModelProperties.foo]);
You can even use your own custom types as property names. They just must extend Object and correctly implement equality using ==
and hashCode
.
Mixin compilation error #
If you are using PropertyChangeNotifier
as a mixin and see the following compilation error:
error: The class 'PropertyChangeNotifier' can't be used as a mixin because it extends a class other than Object.
The solution is to ignore the mixin_inherits_from_not_object
static analyzer rule. You can do this by adding the following line above your model class declaration:
// ignore: mixin_inherits_from_not_object
class MyModel with PropertyChangeNotifier<String> {
...
}
Or you can add the following to your analysis_options.yaml
file:
analyzer:
errors:
mixin_inherits_from_not_object: ignore
Or you can use PropertyChangeNotifier
as a superclass instead (using the extends
keyword).
Usage in Flutter #
PropertyChangeProvider
can be used to expose a PropertyChangeNotifier
instance to descendant widgets, and automatically rebuild them when all or certain properties change. First, create a root PropertyChangeProvider
widget with an instance of your model:
PropertyChangeProvider(
value: MyModel(),
child: MyApp(...)
};
Then, from any descendant widget, listen for changes to all or some properties by using the standard of()
syntax typically used with InheritedWidget
. You can then access either the model itself or its last changed property. Here are a few different examples:
###Rebuilding when any property changes
Just call the static of()
method anywhere from your widget, passing in its BuildContext
.
@override
Widget build(BuildContext context) {
PropertyChangeProvider.of<MyModel>(context);
return Text('MyModel changed!);
}
Rebuilding when a single property changes #
Provide a properties
parameter list with a single value.
@override
Widget build(BuildContext context) {
PropertyChangeProvider.of<MyModel>(context, properties: ['foo']);
return Text('Foo changed!);
}
Rebuilding when multiple properties change #
Provide a properties
parameter list with multiple values.
@override
Widget build(BuildContext context) {
PropertyChangeProvider.of<MyModel>(context, properties: ['foo', 'bar']);
return Text('Foo or Bar changed!);
}
Accessing the model instance #
Call value
on the return value of the of()
method.
@override
Widget build(BuildContext context) {
final model = PropertyChangeProvider.of<MyModel>(context).value;
...
}
Accessing the model instance without rebuilding #
You may want to just access the model without registering for a rebuild. For example, a button that mutates the model does not need to listen for changes. Provide a listen
parameter with a value of false
:
@override
Widget build(BuildContext context) {
final model = PropertyChangeProvider.of<MyModel>(context, listen: false).value;
...
}
Accessing the last changed property #
Useful if you are listening to all or multiple properties and wish to know which property was changed. Call property
on the return value of the of()
method.
@override
Widget build(BuildContext context) {
final property = PropertyChangeProvider.of<MyModel>(context).property;
...
}
Accessing as a widget #
PropertyChangeConsumer
is a widget-based listener for cases where a BuildContext
is hard to access, or if you prefer this kind of API. You can access both the model value and the last changed property via the builder
callback:
@override
Widget build(BuildContext context) {
return PropertyChangeConsumer<MyModel>(
properties: ['foo', 'bar'],
builder: (context, model, property) {
...
},
);
}
Unit Tests #
This library has 100% test coverage.