Basic concepts
Responsive programming
The so-called responsive programming refers to a programming paradigm oriented to data flow and change propagation. The use of responsive programming paradigm means that static or dynamic data streams can be more easily expressed in programming languages, and the relevant computing models will automatically propagate the changing values through the data streams.
The original purpose of reactive programming is to simplify the creation of interactive user interface and the rendering of real-time system animation. It is designed to simplify the MVC software architecture. In object-oriented programming languages, responsive programming is usually presented as an extension of observer mode. You can also compare the responsive flow pattern with the iterator pattern. The main difference is that the iterator is based on "pull" while the responsive flow is based on "push".
Using iterators is imperative programming, where the developer decides when to access the next() element in the sequence. In a responsive flow, the publisher subscriber corresponds to the iteratable iterator. When a new available element appears, the publisher notifies the subscriber that this "push" is the key to the response. In addition, the operation applied to the push element is declarative rather than imperative: what the programmer should do is to express the logic of calculation, rather than describe the precise control process.
In addition to push elements, responsive programming also defines good error handling and completion notification. Publishers can push new elements to subscribers by calling the next() method, send an error signal by calling the onError() method, or send a completion signal by calling onComplete(). Both the error signal and the completion signal terminate the sequence.
Reactive programming is very flexible. It supports use cases with no value, one value or n values (including infinite sequences). Therefore, a large number of application development are quietly using this popular pattern for development.
Stream
In Dart, Stream and Future are two core API s for asynchronous programming. They are mainly used to handle asynchronous or delayed tasks. The return values are Future objects. The difference is that Future is used to represent data obtained asynchronously at one time, while Stream can obtain data or error exceptions by triggering success or failure events multiple times.
Stream is a data stream subscription management tool provided by Dart. Its function is somewhat similar to EventBus or RxBus in Android. A stream can receive any object, including another stream. In Flutter's stream flow model, the publishing object adds data through the sink of the StreamController, and then sends it to the stream through the StreamController. The subscriber listens by calling the listen() method of the stream. The listen() method returns a StreamSubscription object. The StreamSubscription object supports operations such as pausing, resuming, and canceling data streams.
According to the number of Stream listeners, Stream data streams can be divided into single subscription streams and multi subscription streams. The so-called single subscription Stream means that only one listener is allowed in the whole life cycle. If the listener is cancelled, it cannot continue listening. The scenarios used include file IO Stream reading, etc. The so-called broadcast subscription Stream means that multiple listeners are allowed in the life cycle of an application. When listeners are added, they can monitor the data Stream. This type is suitable for scenarios where multiple listeners are required.
For example, the following is an example of data listening using the single subscription mode of Stream. The code is as follows.
class StreamPage extends StatefulWidget { StreamPage({Key key}): super(key: key); @override _StreamPageState createState() => _StreamPageState(); } class _StreamPageState extends State<StreamPage> { StreamController controller = StreamController(); Sink sink; StreamSubscription subscription; @override void initState() { super.initState(); sink = controller.sink; sink.add('A'); sink.add(1); sink.add({'a': 1, 'b': 2}); subscription = controller.stream.listen((data) => print('listener: $data')); } @override Widget build(BuildContext context) { return Center(); } @override void dispose() { super.dispose(); sink.close(); controller.close(); subscription.cancel(); } }
Running the above code will output the following log information on the console.
I/flutter ( 3519): listener: A I/flutter ( 3519): listener: 1 I/flutter ( 3519): listener: {a: 1, b: 2}
Unlike a single subscription Stream, a multi subscription Stream allows multiple subscribers, and broadcasts as long as there is new data in the data Stream. The use process of multi subscription flow is the same as that of single subscription flow, except that the method of creating Stream flow controller is different, as shown below.
class StreamBroadcastPage extends StatefulWidget { StreamBroadcastPage({Key key}): super(key: key); @override _StreamBroadcastPageState createState() => _StreamBroadcastPageState(); } class _StreamBroadcastPageState extends State<StreamBroadcastPage> { StreamController controller = StreamController.broadcast(); Sink sink; StreamSubscription subscription; @override void initState() { super.initState(); sink = controller.sink; sink.add('A'); subscription = controller.stream.listen((data) => print('Listener: $data')); sink.add('B'); subscription.pause(); sink.add('C'); subscription.resume(); } @override Widget build(BuildContext context) { return Center(); } @override void dispose() { super.dispose(); sink.close(); controller.close(); subscription.cancel(); } }
Allow the above code, and the output log is as follows.
I/flutter ( 3519): Listener: B I/flutter ( 3519): Listener: C
However, a single subscription stream sends data only when there is listening. A broadcast subscription stream does not consider this. It sends data when there is data; After the listener calls pause, any type of stream will stop sending data. After resume, all the previously saved data will be sent.
Sink can accept any type of data, and can also restrict the incoming data through generics. For example, we specify the type of StreamController streamcontroller<int>_ Controller = StreamController Broadcast(); Because there is no restriction on the type of sink, you can still add type parameters other than int, but an error will be reported when running_ The controller determines the type of the parameter you passed in and refuses to enter.
At the same time, many streamtransformers are provided in the stream to process the monitored data. For example, if we send 20 data from 0 to 19 and only accept the first 5 data greater than 10, the following operations can be performed on the stream.
_subscription = _controller.stream .where((value) => value > 10) .take(5) .listen((data) => print('Listen: $data')); List.generate(20, (index) => _sink.add(index));
In addition to where and take, there are many transformers, such as map and skip, which readers can study by themselves.
In the Stream flow model, when the data source changes, the Stream will notify the subscriber, so as to change the control state and refresh the page. At the same time, in order to reduce developers' intervention in Stream data flow, fluent provides a StreamBuilder component to assist Stream data flow operations. Its constructor is as follows.
StreamBuilder({ Key key, this.initialData, Stream<T> stream, @required this.builder, })
In fact, StreamBuilder is a StatefulWidget component used to monitor the changes of Stream data flow and display the data changes. It will always record the latest data in the data flow. When the data flow changes, it will automatically call the builder() method to rebuild the view. For example, the following is to use StreamController and StreamBuider to improve the official counter application, instead of using setState to refresh the page. The code is as follows.
class CountPage extends StatefulWidget { @override State<StatefulWidget> createState() { return CountPageState(); } } class CountPageState extends State<CountPage> { int count = 0; final StreamController<int> controller = StreamController(); @override Widget build(BuildContext context) { return Scaffold( body: Container( child: Center( child: StreamBuilder<int>( stream: controller.stream, builder: (BuildContext context, AsyncSnapshot snapshot) { return snapshot.data == null ? Text("0") : Text("${snapshot.data}"); }), ), ), floatingActionButton: FloatingActionButton( child: const Icon(Icons.add), onPressed: () { controller.sink.add(++count); }), ); } @override void dispose() { controller.close(); super.dispose(); } }
It can be seen that compared with the traditional setState method, StreamBuilder is a great progress, because it does not need to forcibly rebuild the entire component tree and its sub components, but only the components wrapped by StreamBuilder. The reason for using StatefulWidget in the example is that the StreamController object needs to be released in the dispose() method of the component.
BLoC mode
About BLoC
BLoC is the English abbreviation of Business Logic Component, which is translated into Business Logic Component in Chinese. It is a way to use responsive programming to build applications. BLoC was first designed and developed by Paolo Soares and Cong Hui of Google. The original intention of the design is to realize the separation of page view and business logic. As shown in the following figure, it is the architecture diagram of the application in BLoC mode.
When using BLoC for state management, all components in the application are regarded as an event flow. Some components can subscribe to events, while others consume events. The engineering process of BLoC is shown in the following figure.
As shown in the above figure, the component sends events to bloc through Sink. After receiving the events, bloc executes internal logic processing, and notifies the processing results to the component subscribing to the event flow through flow. In the workflow of bloc, Sink accepts input, bloc processes the received content, and finally outputs it in the form of stream. It can be found that bloc is a typical observer mode. To understand the operation principle of bloc, you need to focus on several objects, namely events, States, transitions, and flows.
- Events: in the Bloc, events are input into the Bloc through Sink, usually in response to user interaction or life cycle events.
- Status: used to represent the output of Bloc. It is part of the application status. It can notify UI components and rebuild parts of itself based on the current state.
- Transition: the change from one state to another is called transition. Transition usually consists of current state, event and next state.
- Stream: represents a series of asynchronous data. Bloc is based on the stream. Moreover, bloc needs to rely on RxDart, which encapsulates the low-level detail implementation of Dart in terms of flow.
BLoC Widget
Bloc is not only an architecture mode in software development, but also a software programming idea. In the development of flutter application, the use of bloc mode requires the introduction of flutter_bloc library, with the help of fluent_ The basic components provided by bloc enable developers to implement responsive programming quickly and efficiently. Flutter_ The common components provided by bloc include BlocBuilder, BlocProvider, BlocListener, and BlocConsumer.
BlocBuilder
BlocBuilder is flutter_ A basic component provided by Bloc is used to build components and respond to new states of components. It usually requires two parameters, Bloc and builder. BlocBuilder has the same function as StreamBuilder, but it simplifies the implementation details of StreamBuilder and reduces some necessary template code. The builder() method will return a component view, which will be potentially triggered many times to respond to the change of component state. The constructor of BlocBuilder is as follows.
const BlocBuilder({ Key key, @required this.builder, B bloc, BlocBuilderCondition<S> condition, })
It can be found that there are three parameters in the constructor of BlocBuilder, and the builder is a required parameter. In addition to the builder and bloc parameters, there is also a condition parameter, which is used to provide optional conditions for the BlocBuilder and carefully control the builder function.
BlocBuilder<BlocA, BlocAState>( condition: (previousState, state) { //Decide whether to refactor the component according to the returned state }, builder: (context, state) { //Build components based on the status of BlocA } )
As shown above, the condition obtains the status of the previous bloc and the status of the current bloc and returns a Boolean value. If the condition property returns true, state is called to perform the view rebuild. If condition returns false, the view will not be rebuilt.
BlocProvider
The BlocProvider is a fluent component, which can be accessed through the BlocProvider Of <t> (context) provides bloc to its children. In practice, it can be injected into components as dependencies, so that a bloc instance can be provided to multiple components in the subtree.
In most cases, we can use the BlocProvider to create a new blocs and provide it to other subcomponents. Since blocs are created by the BlocProvider, it also needs the BlocProvider to close them. In addition, the BlocProvider can also be used to provide existing blocs to sub components. Since the bloc is not created by the BlocProvider, the bloc cannot be closed through the BlocProvider, as shown below.
BlocProvider.value( value: BlocProvider.of<BlocA>(context), child: ScreenA(), );
MultiBlocProvider
MultiBlocProvider is a component used to combine multiple BlocProviders into a single BlocProvider. MultiBlocProvider is usually used to replace scenarios that need to nest multiple BlocProviders, thus reducing the complexity of the code and improving the readability of the code. For example, the following is a scenario where multiple BlocProviders are nested.
BlocProvider<BlocA>( create: (BuildContext context) => BlocA(), child: BlocProvider<BlocB>( create: (BuildContext context) => BlocB(), child: BlocProvider<BlocC>( create: (BuildContext context) => BlocC(), child: ChildA(), ) ) )
It can be found that in the example, BlocA is nested with BlocB, and BlocB is nested with BlocC. The code logic is very complex and the readability is very poor. If you use the MultiBlocProvider component, you can avoid the above problems. The modified code is as follows.
MultiBlocProvider( providers: [ BlocProvider<BlocA>( create: (BuildContext context) => BlocA(), ), BlocProvider<BlocB>( create: (BuildContext context) => BlocB(), ), BlocProvider<BlocC>( create: (BuildContext context) => BlocC(), ), ], child: ChildA(), )
BlocListener
The BlocListener is a component that receives the BlocWidgetListener and optional blocs. It is applicable to scenarios where every state change needs to occur. The listener parameter of the BlocListener component can be used to respond to changes in state, and can be used to handle other things besides updating the UI view. Different from the builder operation in the BlocBuilder, the status change of the BlocBuilder component will only call the listener once, and it is an empty function. The blocklistener component is typically used in navigation, SnackBar, and Dialog display scenarios.
BlocListener<BlocA, BlocAState>( bloc: blocA, listener: (context, state) { //Perform certain actions based on the status of BlocA } child: Container(), )
In addition, you can use conditional attributes to control listener functions more carefully. The condition attribute will return a Boolean value by comparing the status of the previous bloc with that of the current bloc. If the condition returns true, the listening function will be called. If the condition returns false, the listening function will not be called, as shown below.
BlocListener<BlocA, BlocAState>( condition: (previousState, state) { //Return true or false to determine whether to call listening }, listener: (context, state) { } )
If you need to listen to the status of multiple blocs at the same time, you can use the MultiBlocListener component, as shown below.
BlocListener<BlocA, BlocAState>( MultiBlocListener( listeners: [ BlocListener<BlocA, BlocAState>( listener: (context, state) {}, ), BlocListener<BlocB, BlocBState>( listener: (context, state) {}, ), ... ], child: ChildA(), )
In addition, flutter_ The components provided by bloc include BlocConsumer, RepositoryProvider and MultiRepositoryProvider.
When the status changes, you need to handle other things besides updating the UI view. You can use the BlocListener. The BlocListener contains a listener to do things other than UI update. This logic cannot be put into the builder in the BlocBuilder, because this method will be called many times by the fluent framework. The builder method should only be a function that returns a Widget.
flutter_bloc quick start
Using flutter_ Before bloc, you need to create a Add library dependencies to the yaml configuration file, as shown below.
dependencies: flutter_bloc: ^4.0.0
Use the fluent packages get command to pull the dependent libraries locally, and then you can use fluent_ Bloc library for application development.
The following is an example of a counter application to illustrate the flutter_ Basic usage process of Bloc library. In the sample program, there are two buttons and a text component for displaying the current counter value. The two buttons are used to increase and decrease the counter value respectively. According to the basic usage specification of Bloc mode, you need to create an event object first, as shown below.
enum CounterEvent { increment, decrement }
Then, create a new Bloc class to manage the status of counters, as shown below.
class CounterBloc extends Bloc<CounterEvent, int> { @override int get initialState => 0; @override Stream<int> mapEventToState(CounterEvent event) async* { switch (event) { case CounterEvent.decrement: yield state - 1; break; case CounterEvent.increment: yield state + 1; break; default: throw Exception('oops'); } } }
Generally, inheriting bloc<event, state> must implement initialState() and mapEventToState() methods. Among them, initialState() is used to represent the initial state of the event, while mapEventToState() method returns the state after business logic processing. This method can get the specific event type, and then perform some logic processing according to the event type.
To facilitate the writing of Bloc files, we can also use the Bloc Code Generator plug-in to assist in the generation of Bloc files. After installation, right-click the project and select Bloc generator - > New Bloc to create a Bloc file, as shown in the following figure.
The Bloc files generated by the Bloc Code Generator plug-in are as follows:
bloc ├── counter_bloc.dart // All business logic s, such as addition and subtraction ├── counter_state.dart // All States, such as Added and destroyed ├── counter_event.dart // All event s, such as add, remove └── bloc.dart
Before using Bloc, you need to register it in the top container of the application, that is, in the MaterialApp component. Then, use the blocprovider Of <t> (context) get the registered Bloc object and process the business logic through the Bloc. To receive and respond to changes in status, you need to use the BlocBuilder component. The builder parameter of the BlocBuilder component will return to the component view, as shown below.
void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home:BlocProvider<CounterBloc>( create: (context) => CounterBloc(), child: CounterPage(), ), ); } } class CounterPage extends StatelessWidget { @override Widget build(BuildContext context) { CounterBloc counterBloc = BlocProvider.of<CounterBloc>(context); return Scaffold( appBar: AppBar(title: Text('Bloc Counter')), body: BlocBuilder<CounterBloc, int>( builder: (context, count) { return Center( child: Text('$count', style: TextStyle(fontSize: 48.0)), ); }, ), floatingActionButton: Column( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end, children: <Widget>[ Padding( padding: EdgeInsets.symmetric(vertical: 5.0), child: FloatingActionButton( child: Icon(Icons.add), onPressed: () { counterBloc.add(CounterEvent.increment); }, ), ), Padding( padding: EdgeInsets.symmetric(vertical: 5.0), child: FloatingActionButton( child: Icon(Icons.remove), onPressed: () { counterBloc.add(CounterEvent.decrement); }, ), ), ], ), ); } }
Run the above example code. When you click the increase button of the counter, the addition operation will be executed, and when you click the decrease button, the subtraction operation will be executed, as shown in the following figure.
As you can see, using the shuttle_ The Bloc state management framework can change the data state without calling the setState() method, and the page and logic are separated, which is more suitable for medium and large projects. This article only introduces some basic knowledge of Bloc. You can view the details: Official documents of Bloc