property_change_notifier 0.1.5

  • Readme
  • Changelog
  • Example
  • Installing
  • 84

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.

  1. Replace ChangeNotifier with PropertyChangeNotifier in your model.
  2. Update your model to include the property name when calling notifyListeners().
  3. When ready, update existing listeners to observe only specific properties.

Usage in Dart #

Model implementation #

class MyModel with PropertyChangeNotifier<String> {
  int _foo = 0;
  int _bar = 0;

  int get foo => _foo;
  int get bar => _bar;

  set foo(int value) {
    _foo = value;
    notifyListeners('foo');
  }

  set bar(int 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(int 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(int 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.

0.1.5 #

  • Updated tests and example project

0.1.4 #

  • Readme updates

0.1.3 #

  • Fixed package declaration

0.1.2 #

  • Updated package structure

0.1.1 #

  • Removed readme from example project

0.1.0 #

  • Initial Open Source release

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:property_change_notifier/property_change_notifier.dart';

class MyModel with PropertyChangeNotifier<String> {
  int _foo = 0;
  int _bar = 0;

  int get foo => _foo;
  int get bar => _bar;

  set foo(int value) {
    _foo = value;
    notifyListeners('foo');
  }

  set bar(int value) {
    _bar = value;
    notifyListeners('bar');
  }
}

void main() => runApp(MyApp());

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final model = MyModel();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Property Change Notifier',
      home: Scaffold(
        body: PropertyChangeProvider(
          value: model,
          child: Center(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                GlobalListener(),
                FooListener(),
                BarListener(),
                FooUpdater(),
                BarUpdater(),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class GlobalListener extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return PropertyChangeConsumer<MyModel>(
      builder: (context, model, property) {
        if (property == null) return Container();
        return Text('$property changed');
      },
    );
  }
}

class FooListener extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return PropertyChangeConsumer<MyModel>(
      properties: ['foo'],
      builder: (context, model, property) {
        return Text('Foo is ${model.foo}');
      },
    );
  }
}

class BarListener extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return PropertyChangeConsumer<MyModel>(
      properties: ['bar'],
      builder: (context, model, property) {
        return Text('Bar is ${model.bar}');
      },
    );
  }
}

class FooUpdater extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final model = PropertyChangeProvider.of<MyModel>(context, listen: false).value;
    return RaisedButton(
      child: Text('Update foo'),
      onPressed: () => model.foo++
    );
  }
}

class BarUpdater extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final model = PropertyChangeProvider.of<MyModel>(context, listen: false).value;
    return RaisedButton(
      child: Text('Update bar'),
      onPressed: () => model.bar++
    );
  }
}

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  property_change_notifier: ^0.1.5

2. Install it

You can install packages from the command line:

with Flutter:


$ flutter pub get

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:property_change_notifier/property_change_notifier.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
67
Health:
Code health derived from static analysis. [more]
100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
100
Overall:
Weighted score of the above. [more]
84
Learn more about scoring.

We analyzed this package on Nov 15, 2019, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.6.0
  • pana: 0.12.21
  • Flutter: 1.9.1+hotfix.6

Platforms

Detected platforms: Flutter

References Flutter, and has no conflicting libraries.

Health suggestions

Format lib/property_change_notifier.dart.

Run flutter format to format lib/property_change_notifier.dart.

Format lib/src/property_change_consumer.dart.

Run flutter format to format lib/src/property_change_consumer.dart.

Format lib/src/property_change_notifier.dart.

Run flutter format to format lib/src/property_change_notifier.dart.

Format lib/src/property_change_provider.dart.

Run flutter format to format lib/src/property_change_provider.dart.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.1.0 <3.0.0
flutter 0.0.0
Transitive dependencies
collection 1.14.11 1.14.12
meta 1.1.7 1.1.8
sky_engine 0.0.99
typed_data 1.1.6
vector_math 2.0.8
Dev dependencies
flutter_test
pedantic ^1.7.0