The PropertyBuilder
FutureBuilder
and StreamBuilder
are awesome widgets to react to asynchronous changes to an app’s state. However, they are triggered only on changes and if the awaited value arrives before the builder is ready, it is lost.
Therefore, people often use an RX BehaviorSubject
. If you don’t want to depend on RX, though, the following class will provide the same behavior without an additional library.
A Property
encapsulates a value and provides a stream of changes, starting with the current value. You can also read and write the value at any time.
Here is its implementation:
import 'dart:async';class Property<T> {
StreamController<T> _controller;
T _value;T get value => _value;set value(T newValue) {
if (_value != newValue) {
_value = newValue;
_controller.add(newValue);
}
}Stream<T> get stream => _controller.stream;Property(T initialValue) {
_value = initialValue;
_controller = StreamController.broadcast(
sync: true,
onListen: () => _controller.add(_value),
);
}
}
Here is an example how to use a Property
instance:
final p = Property(42);
print(p.value); // prints "42"
p.value += 1;
print(p.value); // prints "43"p.stream.listen((v) => print(v)); // prints "43", again.
p.value -= 1; // prints "42" because of the above listener
To reduce the boilerplate code when using Property
instances, we will also create a PropertyBuilder
which works like a StreamBuilder
. It requires both a property
and a builder
as arguments. The latter is a function that gets the usual BuildContext
as well as the current property value
and must return a Widget
.
Because a Property
always has a current value, the builder
is never called without data. Furthermore, the property’s stream
will never raise an error. Therefore, the implementation of the builder is much simpler.
Here is the implementation:
class PropertyBuilder<T> extends StreamBuilder<T> {
PropertyBuilder({
Key key,
@required Property<T> property,
@required Widget Function(BuildContext, T) builder,
}) : super(
key: key,
stream: property.stream,
initialData: property.value,
builder: (context, snapshot) {
assert(snapshot.hasData);
assert(!snapshot.hasError);
return builder(context, snapshot.data);
},
);
}
Counter example
Last but not least, here is the usual Flutter counter example implemented using Property
and PropertyBuilder
:
final counter = Property(0);class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FloatingActionButton(
onPressed: () => counter.value--,
child: Icon(Icons.remove),
elevation: 0,
),
PropertyBuilder<int>(
property: counter,
builder: (context, snapshot) {
return Text(
"${counter.value}",
style: Theme.of(context).textTheme.display4,
);
},
),
FloatingActionButton(
onPressed: () => counter.value++,
child: Icon(Icons.add),
elevation: 0,
),
],
),
),
),
);
}
}
Of course, instead of using a global object, the counter
property should be used with the BLoC pattern. But that’s a story for a different day.