In the development of Flutter, everyone could not get around the Widget refresh, and setState() was the easiest way to use it. However, as app interactions become more complex, the number of setstates will increase significantly, and each setState will re-call the build method, which will have an impact on performance and the readability of the code. How elegant solution to this problem, have to mention StreamBuilder StreamBuilder is asynchronous build core components in Flutter. Many well-known open source frameworks such as Bloc are based on this implementation.
If StreamBuilder has the understanding can directly look at the second part
Git link: github.com/canoninmajo…
A key StreamBuilder for local refresh
-
setState()
We now have two numbers on the page, key1 and key2, to show. When we click on the top button, we change the value of key1 or key2 accordingly.Using setState(), we know it’s easy to create local variables key1, key2, and then display them directly in the corresponding Text. We increment the local variables key1 and key2 when we click the button, and then call setState().
But when I refresh Key1, I refactor both texts displayed by Key2 at the same time, even though my Key2 hasn’t changed, which is obviously not a reasonable thing to do.
There is also a powerful component in Flutter, the SteamBuilder, to help us handle the refresh build of controls.
-
StreamBuilder
StreamBuidler is based on Stream, one of the asynchronous cores in DART. It takes the observer mode. The sender sends data through StreamControll, and the observation object constructs its own content after receiving the data.
StreamBuilder accepts two parameters, a stream, which represents the stream we are listening to (a StreamBuilder listens to a stream, but a stream can be listened to by multiple widgets). The Builder passes in the contentWidget that we need to build.
The Widget is built entirely by Stream, the controls do not need to setState themselves, and its build is completely data-driven and a form of responsive programming. It is also the core principle of many open source frameworks such as Bloc.
Going back to the example above, the above example becomes very clear when we use StreamBuilder. We create two StreamControlers and then wrap the two groups of texts in the figure key1 and Key2 with two StreamBuilders.Add data to Stream during key1’s click event. This creates a Stream of data on key1. After receiving the data, the corresponding listener only updates its own content and does not rebuild other regions.
How to optimize StreamBuilder use DataLine
So we know that. StreamBuilder can perfectly solve the problem of local refresh, but StreamBuilder also has the same obvious disadvantages, it is very troublesome to use, need to manually create their own streams, controls with StreamBuilder wrapped structure.
When our pages need more than one local refresh, the Stream can be cumbersome to write. A solution like Provide would also require setting up the top-level Widget, then wrapping the child controls with consumer, calling updates, and so on.
Are there ways we can simplify our use?
Note that the StreamBuilder needs to listen on a stream, which is usually from the StreamControler. For each StreamControler, it is like a one-to-many DataLine in life.At the heart of this DataLine are two methods
1. Add an observer (wrapping the actual contentWidget in a StreamBuilder) : similar to connecting a phone with a cable
2. Sending data: similar to charging a phone through a data cable (because it is a one-to-many process)
Based on this idea, a SingleDataLine is designed. For this “data line”, T limits the data type used by this line. CurrentData can help us get the latest data, and setData(T T) sends data.
The core is in our addObserver, which needs to pass in a method that returns a Widget Function(BuildContext Context, T data) observer. This incoming method is the Widget constructor that we need to build.
class SingleDataLine<T> {
StreamController<T> _stream;
// Get the latest data
T currentData;
SingleDataLine([T initData]) {
currentData = initData;
_stream = initData == null
? BehaviorSubject<T>()
: BehaviorSubject<T>.seeded(initData);
}
get outer => _stream.stream;
get inner => _stream.sink;
void setData(T t) {
// Same value filtering
if (t == currentData) return;
// Prevent shutdown
if (_stream.isClosed) return;
currentData = t;
inner.add(t);
}
Widget addObserver(
Widget Function(BuildContext context, T data) observer,
) {
return DataObserverWidget<T>(this, observer);
}
voiddispose() { _stream.close(); }}Copy the code
This addObserver method returns a DataObserverWidget that encapsulates StreamBuilder and simplifies StreamBuilder use.
class _DataObserverWidgetState<T> extends State<DataObserverWidget<T>> {
@override
Widget build(BuildContext context) {
// TODO: implement build
return StreamBuilder(
stream: widget._dataLine.outer,
builder: (context, AsyncSnapshot<T> snapshot) {
if(snapshot ! =null&& snapshot.data ! =null) {
print(
" ${context.widget.toString()}Steam received data once${snapshot.data}");
return widget._builder(context, snapshot.data);
} else {
returnRow(); }}); }@override
void dispose() {
super.dispose();
widget._dataLine.dispose();
}
Copy the code
How does DataBus resolve multiple Stream bindings
SingDataLine simplifies StreamBuilder usage, but when there are multiple SingleDataLine pages, it can be a hassle to create and manage it. Based on this, a dataBus bus management is designed.We store each key and the corresponding DataLine into the Map for management, and directly call getLine(key) to obtain and create DataLine. And since MultDataLine is a mixin definition,So we can mix methods into any class. For example, you can mix the class changes directly into the Widget and call the getLine method to get the StreamBuilder.
import 'package:flutter_dpluse_package/common/widgets/Page/data_line.dart';
mixin MultDataLine {
final Map<String, SingleDataLine> dataBus = Map(a); SingleDataLine<T> getLine<T>(String key) {
if(! dataBus.containsKey(key)) { SingleDataLine<T> dataLine =new SingleDataLine<T>();
dataBus[key] = dataLine;
}
return dataBus[key];
}
voiddispose() { dataBus.values.forEach((f) => f.dispose()); dataBus.clear(); }}Copy the code
Going back to the example above, using DataBus, the building of the page will be extremely simple, where the core of sending data and listening we do through getLine.
ListView(children: <Widget>[
GestureDetector(
child: Container(
width: 150,
height: 60,
child: Center(
child: Text(
'Trigger of key1',
style: TextStyle(color: Colors.white, fontSize: 20),
)),
decoration: BoxDecoration(color: Colors.grey),
),
onTap: () {
// Send a datagetLine(KEY1).setData(key1++); },),// Bind the listening object
getLine(KEY1).addObserver((context, data) {
// The actual observer
return Text(
'The current data of key1 is$data',
style: TextStyle(
fontSize: 19, color: Colors.green, fontWeight: FontWeight.w600),
);
}),
getLine(KEY1).addObserver(
(context, data) {
return Text(
'The current data of key1 is$data',
style: TextStyle(
fontSize: 19, color: Colors.blue, fontWeight: FontWeight.w600), ); }),]);Copy the code
Four,
StreamBuilder is used in DataBus as a build method, and there are a few lightweight observation pattern components to choose from, such as ChangNotify, but if you use these components alone, the observations will inevitably be scattered throughout the page and not easy to manage. DataBus is an individual’s development practice of a minimalist UI-model binding approach based on which a common page framework has been implemented for several complex pages. The DataBus core aims to solve two problems: 1. Simplify the binding of observed objects and observed objects; 2. Manage the lifetime of all bindings uniformly github.com/canoninmajo…