The basic concept

Responsive programming

Reactive programming refers to a programming paradigm oriented toward data flow and change propagation. Using the reactive programming paradigm means that static or dynamic data flows can be expressed more easily in a programming language, and the associated computational model automatically propagates the changing values through the data flows.

Responsive programming was originally developed as a way to simplify the creation of interactive user interfaces and the drawing of real-time system animations. It was designed to simplify MVC software architectures. In object-oriented programming languages, responsive programming is usually presented as an extension of the observer pattern. You can also compare the reactive flow pattern to the iterator pattern, with one major difference being that iterators are based on “pull” while reactive flows are based on “push”.

Using iterators is imperative programming where the developer decides when to access the next() element in the sequence. In a reactive stream, the equivalent of iterable-iterator is publisher-subscriber. Publishers notify subscribers when new elements are available, and this push is key to the response. In addition, the operations applied to push elements are declarative rather than imperative: the programmer’s job is to express the logic of the computation, not to describe the precise flow of control.

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 error signals and completion signals terminate the sequence.

Reactive programming is so flexible that it supports use cases with no value, one value, or N values (including infinite sequences) that a large amount of application development is now quietly developing using this popular pattern.

Stream

In Dart, Stream and Future are the two core apis for asynchronous programming. They are used to handle asynchronous or delayed tasks and return Future objects. The difference is that a Future is used to represent data acquired asynchronously once, whereas a Stream can retrieve data or error exceptions by firing success or failure events multiple times.

Stream is a subscription management tool that Dart provides. Similar to EventBus or RxBus on Android, a Stream can receive any object, including another Stream. In the Stream model of a Flutter, the publish object adds data to the Stream via the StreamController’s sink, and then sends data to the Stream via the StreamController. The subscriber listens by calling the Stream’s Listen () method, which returns a StreamSubscription object that allows suspending, resuming, and canceling operations on the Stream.

Stream data streams can be divided into single-subscription streams and multi-subscription streams according to the number of listeners. The so-called single subscription stream means that only one listener is allowed to exist in the whole life cycle. If the listener is cancelled, the listener cannot continue to listen, and the scenarios used include file IO stream reading, etc. The so-called broadcast subscription stream refers to the application that allows multiple listeners during its life cycle. When listeners are added, the data stream can be monitored. This type is suitable for scenarios requiring multiple listeners.

For example, here is an example of data listening using the single-subscription mode of Stream.

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(); }}Copy the code

Running the above code outputs 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}

Copy the code

Unlike single-subscription streams, multi-subscription streams allow multiple subscribers and are broadcast whenever new data is in the data stream. The process for using a multi-subscription Stream is the same as for a single-subscription Stream, except that the Stream controller is created differently, 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(); }}Copy the code

Allow the above code, and the output log is shown below.

I/flutter ( 3519): Listener: B
I/flutter ( 3519): Listener: C
Copy the code

However, a single-subscription Stream sends data only when there is a listener, while a broadcast Stream does not. When the listener calls pause, any stream of any type will stop sending data, and when the listener resumes, all data stored in the stream will be sent.

Sink can accept any type of data and restrict incoming data through generics, Such as the us to specify the types of StreamController StreamController _controller = StreamController. Broadcast (); Since there is no restriction on the type of Sink, you can still add type parameters other than int, but when running, an error will be reported. _controller determines the type of the parameter you passed and refuses to enter.

StremTransformer is also provided for the Stream to process the monitored data. For example, if we send 20 data values ranging from 0 to 19 and accept only the first 5 data values greater than 10, we can do the following for the Stream.

_subscription = _controller.stream
    .where((value) => value > 10)
    .take(5)
    .listen((data) => print('Listen: $data'));

List.generate(20, (index) => _sink.add(index));
Copy the code

In addition to WHERE and take, there are many transformers, such as Map and Skip, that readers can explore on their own.

In the Stream model, the Stream notifies subscribers when the data source changes, changing control state and enabling page refreshes. To reduce developer interference with Stream data streams, Flutter provides a StreamBuilder component to assist Stream data Stream operations. Its constructor is shown below.

StreamBuilder({
  Key key,
  this.initialData,
  Stream<T> stream,
  @required this.builder,
})

Copy the code

In fact, StreamBuilder is a StatefulWidget that monitors and displays changes in the Stream data Stream. It keeps track of the latest data in the Stream and automatically calls the Builder () method to rebuild the view when the Stream changes. For example, here is an update to the official counter application using StreamController and StreamBuider instead of using setState to refresh the page.

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 voiddispose() { controller.close(); super.dispose(); }}Copy the code

As you can see, StreamBuilder is a big improvement over the traditional setState approach, because it does not require forced reconstruction of the entire component tree and its children, just the components wrapped in StreamBuilder. The reason for using the StatefulWidget in the example is that you need to release the StreamController object in the component’s Dispose () method.

BLoC mode

Introduction of BLoC

BLoC is an acronym for Business Logic Component. It is a way of building applications using responsive programming. BLoC was originally designed and developed by Paolo Soares and Cong Hui of Google to separate the page view from the business logic. An architecture diagram of an application using BLoC mode is shown below.

As shown in the figure above, the component sends an event to Bloc by Sink. Bloc receives the event and performs internal logical processing, and notifies the component subscribing to the event stream with the result of processing. In BLoC’s workflow, Sink accepts input, BLoC processes the received content, and finally outputs it in the form of flow. It can be found that BLoC is a typical observer mode. To understand how Bloc works, you need to focus on a few objects: events, states, transitions and flows.

  • Events: In Bloc, events are input to Bloc by Sink, usually in response to user interaction or life cycle events.
  • State: Something that represents the output of Bloc and is part of the application state. It can notify UI components and rebuild parts of itself based on the current state.
  • Transitions: Changes from one state to another are called transitions, and transitions usually consist of the current state, events, and the next state.
  • Stream: Represents a series of asynchronously spaced data. Bloc is based on a stream. Also, Bloc relies on RxDart, which encapsulates the low-level detail implementation of Dart in terms of flow.

BLoC Widget

Bloc is both an architectural pattern in software development and a software programming idea. Bloc mode in Flutter application development requires the introduction of the Flutter_bloc library. With the basic components provided by Flutter_BLOC, developers can quickly and efficiently implement responsive programming. Common components provided by Flutter_bloc are BlocBuilder, BlocProvider, BlocListener, and BlocConsumer.

BlocBuilder

BlocBuilder is a basic component provided by Flutter_BLOC to build a component and respond to its new state. It usually requires both Bloc and Builder parameters. BlocBuilder serves the same purpose as StreamBuilder, but it simplifies the implementation details of StreamBuilder, eliminating some of the required template code. The Builder () method returns a component view that is potentially triggered multiple times in response to changes in the component state. BlocBuilder’s constructor is shown below.

const BlocBuilder({
    Key key,
    @required this.builder,
    B bloc,
    BlocBuilderCondition<S> condition,
  })
Copy the code

As you can see, there are three arguments in the BlocBuilder constructor, and Builder is a mandatory argument. In addition to the Builder and Bloc parameters, there is a condition parameter, which is used to provide optional conditions to the BlocBuilder for careful control of the Builder function.

BlocBuilder<BlocA, BlocAState>(condition: (previousState, state) { (Context, state) {// Build components based on BlocA's state})Copy the code

As shown above, the condition retrieves the state of the previous Bloc and the state of the current Bloc and returns a Boolean value. If the condition attribute returns true, state is called to perform a rebuild of the view. If condition returns false, the rebuild of the view is not performed.

BlocProvider

BlocProvider is a Flutter component that provides a bloc to its children via blocprovider.of (context). When used in practice, it can be injected into the component as a dependency, making a bloc instance available to multiple components in a subtree.

In most cases, you can use BlocProvider to create a new Blocs and provide it to other child components. Since blocS are created by the BlocProvider, closing blocs is also handled by the BlocProvider. In addition, BlocProvider can also be used to provide an existing bloc to a child component. Since a bloc was not created by BlocProvider, it cannot be closed by BlocProvider, as shown below.

BlocProvider.value(
  value: BlocProvider.of<BlocA>(context),
  child: ScreenA(),
);
Copy the code

MultiBlocProvider

MultiBlocProvider is a component used to merge multiple BlocProviders into one BlocProvider. MultiBlocProvider is usually used as an alternative to nesting multiple BlocProviders. This reduces code complexity and improves code readability. For example, here is a scenario with multiple BlocProviders nested.

BlocProvider<BlocA>(
  create: (BuildContext context) => BlocA(),
  child: BlocProvider<BlocB>(
    create: (BuildContext context) => BlocB(),
    child: BlocProvider<BlocC>(
      create: (BuildContext context) => BlocC(),
      child: ChildA(),
    )
  )
)
Copy the code

As you can see in the example, BlocA nested BlocB, and BlocB nested BlocC, the code logic is very complex and unreadable. You can avoid this problem by using the MultiBlocProvider component, which looks like the code below.

MultiBlocProvider(
  providers: [
    BlocProvider<BlocA>(
      create: (BuildContext context) => BlocA(),
    ),
    BlocProvider<BlocB>(
      create: (BuildContext context) => BlocB(),
    ),
    BlocProvider<BlocC>(
      create: (BuildContext context) => BlocC(),
    ),
  ],
  child: ChildA(),
)
Copy the code

BlocListener

BlocListener is a component that receives BlocWidgetListener and an optional Bloc, suitable for scenarios where each state change needs to happen once. The Listener parameter of the BlocListener component can be used to respond to changes in state and can be used to do things other than update the UI view. Unlike the Builder action in BlocBuilder, a state change in the BlocBuilder component calls a listener only once and is an empty function. The BlocListener component is typically used in navigation, SnackBar, and Dialog display scenarios.

BlocListener<BlocA, BlocAState>(bloc: BlocA, listener: (context, state) {// Perform some actions based on the state of the BlocA} Child: Container(),)Copy the code

In addition, you can use conditional attributes to provide more rigorous control over listener functions. The condition property returns a Boolean value by comparing the state of the previous bloc to the state of the current bloc. If the condition is true, the sweat monitor will be called. If the condition is false, the sweat monitor will not be called, as shown below.

BlocListener<BlocA, BlocAState>(condition: (previousState, state) {// ReturnstrueorfalseCall listener: (context, state) {})Copy the code

If you need to listen for the state of multiple bloc 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(), )Copy the code

Other components provided by Flutter_BLOC include BlocConsumer, RepositoryProvider, and MultiRepositoryProvider. When the state changes and you need to do something other than update the UI view, you can use BlocListener. BlocListener contains a listener to do something other than update the UI. This logic cannot be added to the Builder in BlocBuilder because this method will be called multiple times by the Flutter framework. The Builder method should only be a function that returns the Widget.

Flutter_bloc gets started quickly

Before using Flutter_BLOC, library dependencies need to be added to the project’s Pubspec.yaml configuration file, as shown below.

Dependencies: flutter_bloc: ^ 4.0.0Copy the code

The dependencies can be fetched locally using the flutter Packages get command, and then the flutter_bloc library can be used for application development.

Here is an example counter application to illustrate the basic flow of using the Flutter_BLOC library. In the sample program, there are two buttons and a text component that displays the current counter value, and two buttons that increase and decrease the counter value, respectively. According to the basic usage specification of Bloc mode, a new event object should be created first, as shown below.

enum CounterEvent { increment, decrement }
Copy the code

Then, a new Bloc class is created to manage the state of the counter, 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'); }}}Copy the code

In general, both initialState() and mapEventToState() methods must be implemented to inherit Bloc<Event,State>. InitialState () is used to represent the initialState of the event, while mapEventToState() returns the state after the completion of the business logic processing. This method can get the specific event type, and then perform some logical processing according to the event type.

To facilitate Bloc file writing, we can also use the Bloc Code Generator plug-in to assist in Bloc file generation. After the installation is complete, right-click on the project and select [Bloc Generator] -> [New Bloc] in order to create the Bloc file, as shown in the image below.

Bloc ├─ counter_state.dart // All state, Added, Decreased Exercises ── Add, Remove ├── blocCopy the code

Before using Bloc, you need to register Bloc in the uppermost container of the application, in the MaterialApp component. BlocProvider. Of (context) is then used to retrieve the registered Bloc object and process the business logic through Bloc. Changes in receive and response status require the BlocBuilder component. The Builder parameter of the BlocBuilder component returns the component view, as shown below.

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    returnMaterialApp( 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); }, ), ), ], ), ); }}Copy the code

Running the sample code above will add when the increase counter button is clicked, and subtract when the decrease button is clicked, as shown below.

It can be found that using the Flutter_bloc state management framework, the change of data state can be realized 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 of the basics of Bloc. You can check out the official Bloc document for details