[Flutter actual combat BLoC mode RxDart Provider mode]

Keywords: Flutter architecture

I think the BLoC model

  • Observer mode + thread scheduling + Dart asynchronous syntax feature + fluent encapsulation
    • Dart asynchronous syntax features: dart API, Stream, Future
    • Fluent encapsulation: StreamBuilder (encapsulating: Dart feature + fluent UI)
  • The concept of old students is normal, so the following article focuses on Stream

---------Dividing line 20210927

How to manage status

  • Interactively
  • Data stream processing
  • Status change
  • Interaction between components

give an example

Change the state of the pie chart by sliding the bottom component

interactive

  • Compare Android with a ProgressBar and a pie chart PieChart
  • Sliding the ProgressBar causes the pie chart PieChar data to change

AntiPattern design

  • PieChart sets the Status to Global exposure
  • ProgressBar listens to drag and changes the State of PieChart according to drag. charState.setState( () {charState.dataChange = 2 })

Disadvantages

  • ProgressBar and PieChart are strongly coupled
  • PieChart's Status is globally exposed (Globally tracking state.)
  • Transfer setState outside PieChart

concept

  • UI = f(state)
  • state maps the UI through the Function
  • As can be seen from the formula, there is no findViewById, and no View changes another View.
  • What changes in the life cycle of a component: state

Stream concept review

  • The two main API s of Dart's async library are stream and future. Future represents the result of a single calculation. Stream represents the result of a string.
  • How to create a Stream:
  • Transforming an existing stream
  • Create a stream using the async * method
  • Create a Stream using the StreamController

Transforming an existing Stream

/// Splits a stream of consecutive strings into lines.
///
/// The input string is provided in smaller chunks through
/// the `source` stream.
Stream<String> lines(Stream<String> source) async* {
  // Stores any partial line from the previous chunk.
  var partial = '';
  // Wait until a new chunk is available, then process it.
  await for (var chunk in source) {
    var lines = chunk.split('\n');
    lines[0] = partial + lines[0]; // Prepend partial line.
    partial = lines.removeLast(); // Remove new partial line.
    for (var line in lines) {
      yield line; // Add lines to output stream.
    }
  }
  // Add final partial line to output stream, if any.
  if (partial.isNotEmpty) yield partial;
}

There are also methods supported by Stream:

  • map() map
  • where() filter
  • expand() repeat
  • take() limit
var counterStream =
    Stream<int>.periodic(Duration(seconds: 1), (x) => x).take(15);
///...

counterStream.forEach(print); // Print an integer every second, 15 times.


var doubleCounterStream = counterStream.map((int x) => x * 2);
doubleCounterStream.forEach(print);

///...
.where((int x) => x.isEven) // Retain only even integer events.
.expand((var x) => [x, x]) // Duplicate each event.
.take(5) // Stop after the first five events.

asynchronous generator (async*) function

Stream<int> timedCounter(Duration interval, [int? maxCount]) async* {
  int i = 0;
  while (true) {
    await Future.delayed(interval);
    yield i++;
    if (i == maxCount) break;
  }
}

The asynchronous generator method returns a Stream object

  • When Stream.listen((event) {}) is called, the method body starts running
  • Method body: yield the next number at intervals
  • If there is no maxCount condition, the listener will not stop listening until the listener cancels listening
  • yield is analogous to return in generating a string of results
  • Method, final error or complete
  • When listener cancel s, yield is equivalent to return. Exit directly
Stream<T> streamFromFutures<T>(Iterable<Future<T>> futures) async* {
  for (var future in futures) {
    var result = await future;
    yield result;
  }
}

The async * method is generally less used and is too simple to process multiple data sources, so the StreamController is introduced.

StreamController:

  • Sink sink: entering data
  • Stream flow: output data
  • StreamControllers: manage stream s and sink

Intermediate processing of Stream

  • Stream can also process data before outflow through StreamTransformer. Input stream and output stream.
  • Generally, reuse processing logic can achieve the same effect as Stream.map, Stream.where or Stream.expand
  • It is generally used for filtering, reorganization, change, deletion, caching and other operations
  • StreamSubscription: listener settings. When there is at least one active listener, Stream will generate events to remind StreamSubscription.

Events:

  • some data goes out from the stream,
  • when some error has been sent to the stream,
  • when the stream is closed.

StreamSubscription feature

  • stop listening,
  • pause,
  • resume.

Type of Stream

  • There are two types of streams.
  • The sync property of the StreamController controls whether to call asynchronously.
  • Single-subscription Streams
  • Only one listener is allowed to listen. It is not allowed to listen on the Stream twice, even if the first subscription cancels listening.
  StreamSubscription<List<int>> listen(
    void Function(List<int> event)? onData, {
    Function? onError,
    void Function()? onDone,
    bool? cancelOnError,
  }) {
	 if (_sub == null) {
      throw StateError('Stdin has already been terminated.');
    }
    // ignore: close_sinks
    final controller = _getCurrent();
    if (controller.hasListener) {
      throw StateError(''
          'Subscriber already listening. The existing subscriber must cancel '
          'before another may be added.');
    }
    ///...
import 'dart:async';

void main() {
  //
  // Initialize a "Single-Subscription" Stream controller
  //
  final StreamController ctrl = StreamController();
  
  //
  // Initialize a single listener which simply prints the data
  // as soon as it receives it
  //
  final StreamSubscription subscription = ctrl.stream.listen((data) => print('$data'));

  //
  // We here add the data that will flow inside the stream
  //
  ctrl.sink.add('my name');
  ctrl.sink.add(1234);
  ctrl.sink.add({'a': 'element A', 'b': 'element B'});
  ctrl.sink.add(123.45);
  
  //
  // We release the StreamController
  //
  ctrl.close();
}

Broadcast Streams

You can add any listener to Broadcast Stream

import 'dart:async';

void main() {
  //
  // Initialize a "Broadcast" Stream controller of integers
  //
  final StreamController<int> ctrl = StreamController<int>.broadcast();
  
  //
  // Initialize a single listener which filters out the odd numbers and
  // only prints the even numbers
  //
  final StreamSubscription subscription = ctrl.stream
					      .where((value) => (value % 2 == 0))
					      .listen((value) => print('$value'));

  //
  // We here add the data that will flow inside the stream
  //
  for(int i=1; i<11; i++){
  	ctrl.sink.add(i);
  }
  
  //
  // We release the StreamController
  //
  ctrl.close();
}

RxDart

  • When you mention Streams, you must mention RxDart Package
  • RxDart package is the Dart implementation version of the ReactiveX API, which expands the Dart Streams API to meet the ReactiveX standard
  • RxDart is a reactive functional programming library for Dart language, based on ReactiveX.

Glossary comparison

DartRXDart
StreamStreamController
ObservableSubject

RxDart provides three variants of StreamController.

PublishSubject

PublishSubject and broadcast StreamController variants only return Observable instead of Stream.

BehaviorSubject

The difference between BehaviorSubject and PublishSubject: when subscribing, the latest event before subscription is returned.

ReplaySubject

The difference between ReplaySubject and PublishSubject: when subscribing, all event s before subscription are returned.

be careful

Be sure to free up resources when you don't need them

  • StreamSubscription - when you no longer need to listen to a stream, cancel the subscription;
  • StreamController - when you no longer need a StreamController, close it;
  • the same applies to RxDart Subjects, when you no longer need a BehaviourSubject, a PublishSubject..., close it.

What is Reactive Programming?

  • Reactive programming is programming with asynchronous data streams.
    • Use asynchronous data flow programming.
  • In other words, everything from an event (e.g. tap), changes on a variable, messages, ... to build requests, everything that may change or happen will be conveyed, triggered by a data stream.
    • In other words, any... Is triggered and transmitted by the data flow.

Using reactive programming, the program will:

  • Become asynchronous
  • Is architected around the note of Streams and listeners
  • when something happens somewhere (an event, a change of a variable...) a notification is sent to a Stream, the change (event, variable change) will send a reminder to the stream.
  • if "someone" listens to that Stream, it will be notified and will take appropriate action(s), whatever its location in the application.
  • There is no longer any tight coupling between components. that Widget does only care about its own business, that's all !!
    Components are no longer coupled, and components only care about their own business logic.
  • In short, when a Widget sends something to a Stream, that Widget does no longer need to know:
    • what is going to happen next
      who might use this information (no one, one or several Widgets...)
    • Where this information might be used (nowhere, same screen, other one, severe ones...)
    • When this information might be used (almost directly, after severe seconds, never...)

Benefits:

  • the opportunity to build parts of the application only responsible for specific activities
  • to easily mock some components' behavior to allow more complete tests coverage
  • to easily reuse components (somewhere else in the application or in another application)

BLoC mode

  • The idea is that everything flows. Some components subscribe to events and others respond to events.
  • BLoC manages these sessions centrally.
  • Dart also built streams into the language itself without relying on additional libraries

Abbreviation for Business Logic Components

  • Use Dart stream to control the data flow in components without introducing dependencies
  • The BLoC Pattern has been designed by Paolo Soares and Cong Hui, from Google and first presented during the DartConf 2018 (January 23-24, 2018). See the video on YouTube

framework

Noun interpretation

  • Provider component: bind the UI component of the bloc and provide the method for the child Widget to access the bloc
  • StreamBuilder component: listen to the output of stream and call builder to refresh UI

In fluent:

  • the pipe is called a Stream
  • to control the Stream, we usually(*) use a StreamController
  • to insert something into the Stream, the StreamController exposes the "entrance", called a StreamSink, accessible via the sink property.
  • the way out of the Stream, is exposed by the StreamController via the stream property

No flutter_bloc Library

  ///Network request processing
  final _dataSourceController = StreamController<ModelConfig>();
  StreamSink<ModelConfig> get configSink => _dataSourceController.sink;
	Stream<ModelConfig> get configStream => _dataSourceController.stream;
  ///Click event distribution
  final _eventController = StreamController<UserEvent>();	
	StreamSink<UserEvent> get dispatch => _eventController.sink;
  ///UI content display
  final _displayController = StreamController<String>();
  Stream<String> get displayContent => _displayController.stream;
  
  
  enum UserEvent {
		add, delete,modify
  }
  
  initBloc() {
  	eventController.listen((event) {	
    	Switch (event) {
      	case UserEvent.add:
       		handleUserAdd();
        	break;
		///...
    });
  }
  
  void handleUserAdd() {
  	_displayController.sink.add('User click add button');
  }
  ConfigRepository.instance.get((config) {
		configSink.add(config);
  });
import 'package:flutter/cupertino.dart';
import 'package:flutter_app/bloc/bloc.dart';

class BlocProvider<T extends Bloc> extends StatefulWidget {
  final Widget child;
  final T bloc;

  const BlocProvider({Key? key, required this.bloc, required this.child})
      : super(key: key);

  static T? of<T extends Bloc>(BuildContext context) {
    // final type = _providerType<BlocProvider<T>>();
    final BlocProvider<T>? provider = context.findAncestorWidgetOfExactType<BlocProvider<T>>();
    return provider?.bloc;
  }

  static Type _providerType<T>() => T;

  @override
  State<StatefulWidget> createState() => _BlocProviderState();

}

class _BlocProviderState extends State<BlocProvider> {


  @override
  Widget build(BuildContext context) => widget.child;

  @override
  void dispose() {
    widget.bloc.dispose();
    super.dispose();
  }
}

Create a BLoC and provide a child Widget through the BlocProvider to obtain

void main() {
  runApp(BlocProvider(
    bloc: MainBloc(),
    child: MaterialApp(
      title: 'MaoShan',
      home: RoutingPage()
    ),
  ));
}

Monitor Stream

  StreamSubscription<T> listen(void onData(T event),
      {Function onError, void onDone(), bool cancelOnError}) 

  @override
  Widget build(BuildContext context) {
    final MainBloc? bloc = BlocProvider.of<MainBloc>(context);

    resetController(widget.config);
    return Scaffold(
        appBar: AppBar(
          title: StreamBuilder<String>(
              stream: bloc?.displayContent,
              builder: (BuildContext context, AsyncSnapshot<String?> snapshot) {
                return Text(snapshot?.data ?? '');
              }),
        ),
        body: Container(
            child: ElevatedButton(
                child: Text("Add something"),
                onPressed: () => bloc?.dispatch.add)));
  }

Using fluent_ Bloc Library

BLoC internal architecture:

  • The code architecture is more consistent: UI = f(state), which is suitable for large projects

Installation:

  • Add dependent packages in pubspec.yanml fluent_ bloc: ^7.0.1
  • IDE template plug-in, Android studio search BLoC
  • Create a new bloc and enter the name prefix. Automatically generate 3 files
    • prefix_bloc.dart
    • prefix_state.dart
    • prefix_event.dart

technological process:

bloc content

mapEventToState converts an Event to a State

class MainBloc extends Bloc<MainEvent, MainState> {
  MainBloc() : super(MainInitial());

  @override
  Stream<MainState> mapEventToState(
    MainEvent event,
  ) async* {
    // TODO: implement mapEventToState
  }
}
///...
class MainBloc extends Bloc<MainEvent, MainState> {
  MainBloc() : super(MainState(selectedIndex: 0, isExtended: false));

  @override
  Stream<MainState> mapEventToState(MainEvent event) async* {
    ///main_ The events added in the view will be called back here. After processing the data here, the data will be delivered, and the BlocBuilder will refresh the component
    if (event is SwitchTabEvent) {
      ///Get the value passed by the event event event, and let's get the value and insert it into the MainState
      ///Directly change the internal value on the state, and then yield. The BlocBuilder can only be triggered once. It will internally compare the last MainState object. If it is the same, it will not build
      yield MainState()
        ..selectedIndex = event.selectedIndex
        ..isExtended = state.isExtended;
    } else if (event is IsExtendEvent) {
      yield MainState()
        ..selectedIndex = state.selectedIndex
        ..isExtended = !state.isExtended;
    }
  }
}

main_event: here are all kinds of events executed, a bit similar to fish_ action layer of Redux

@immutable
abstract class MainEvent {}

///...
@immutable
abstract class MainEvent extends Equatable{
  const MainEvent();
}
///Switch the tab of NavigationRail
class SwitchTabEvent extends MainEvent{
  final int selectedIndex;

  const SwitchTabEvent({@required this.selectedIndex});

  @override
  List<Object> get props => [selectedIndex];
}
///Expand NavigationRail. The logic is relatively simple, so there is no need to pass parameters
class IsExtendEvent extends MainEvent{
  const IsExtendEvent();

  @override
  List<Object> get props => [];
}

main_state: The status data is saved here and transferred

@immutable
abstract class MainState {}
///...
class MainState{
   int selectedIndex;
   bool isExtended;
  
   MainState({this.selectedIndex, this.isExtended});
}

UI page writing: send event and change UI

class MainPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return _buildBg(children: [
      //sidebar 
      _buildLeftNavigation(),

      //Main content on the right
      Expanded(child: Center(
        child: BlocBuilder<MainBloc, MainState>(builder: (context, state) {
          return Text(
            "choice Index: " + state.selectedIndex.toString(),
            style: TextStyle(fontSize: 30.0),
          );
        }),
      ))
    ]);
  }

  Widget _buildBg({List<Widget> children}) {
    ///Creating a BlocProvider indicates that the Page uses MainBloc, which is the Bloc belonging to the Page
    return BlocProvider(
      create: (BuildContext context) => MainBloc(),
      child: Scaffold(
        appBar: AppBar(title: Text('Bloc')),
        body: Row(children: children),
      ),
    );
  }

  //Add NavigationRail component as sidebar
  Widget _buildLeftNavigation() {
    return BlocBuilder<MainBloc, MainState>(builder: (context, state) {
      return NavigationRail(
        backgroundColor: Colors.white,
        elevation: 3,
        extended: state.isExtended,
        labelType: state.isExtended
            ? NavigationRailLabelType.none
            : NavigationRailLabelType.selected,
        //item in sidebar
        destinations: [
          NavigationRailDestination(
            icon: Icon(Icons.add_to_queue),
            selectedIcon: Icon(Icons.add_to_photos),
            label: Text("Test one"),
          ),
          NavigationRailDestination(
            icon: Icon(Icons.add_circle_outline),
            selectedIcon: Icon(Icons.add_circle),
            label: Text("Test two"),
          ),
          NavigationRailDestination(
            icon: Icon(Icons.bubble_chart),
            selectedIcon: Icon(Icons.broken_image),
            label: Text("Test three"),
          ),
        ],
        //Top widget
        leading: _buildNavigationTop(),
        //Bottom widget
        trailing: _buildNavigationBottom(),
        selectedIndex: state.selectedIndex,
        onDestinationSelected: (int index) {
          ///Add switch tab event
          BlocProvider.of<MainBloc>(context)
              .add(SwitchTabEvent(selectedIndex: index));
        },
      );
    });
  }

  Widget _buildNavigationTop() {
    return Center(
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Container(
          width: 80,
          height: 80,
          decoration: BoxDecoration(
            shape: BoxShape.circle,
            image: DecorationImage(
              image: NetworkImage(
                "https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=3383029432,2292503864&fm=26&gp=0.jpg",
              ),
              fit: BoxFit.fill,
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildNavigationBottom() {
    return Container(
      child: BlocBuilder<MainBloc, MainState>(
        builder: (context, state) {
          return FloatingActionButton(
            onPressed: () {
              ///Add NavigationRail expansion and contraction events
              BlocProvider.of<MainBloc>(context).add(IsExtendEvent());
            },
            child: Icon(state.isExtended ? Icons.send : Icons.navigation),
          );
        },
      ),
    );
  }
}

API:

BlocWidgets are analogous to StreamBuilder

BlocBuilder<BlocA, BlocAState>(
  bloc: blocA, // provide the local bloc instance
  builder: (context, state) {
    // return widget here based on BlocA's state
  }
)


BlocBuilder<BlocA, BlocAState>(
  buildWhen: (previousState, state) {
    ///return whether to run again
  },
  builder: (context, state) {
    // return widget here based on BlocA's state
  }
)

BlocProvider

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


///Use
{
// with extensions
context.read<BlocA>();

// without extensions
BlocProvider.of<BlocA>(context);
}

MultiBlocProvider

///Rrovider nesting problem

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

BlocListener

listener The method is useless return Value for display SnackBar,Dialog

BlocListener<BlocA, BlocAState>(
  listener: (context, state) {
    // do stuff here based on BlocA's state
  },
  child: Container(),
)

BlocConsumer

It combines BlocListener and BlocBuilder

BlocConsumer<BlocA, BlocAState>(
  listenWhen: (previous, current) {
    // return true/false to determine whether or not
    // to invoke listener with state
  },
  listener: (context, state) {
    // do stuff here based on BlocA's state
  },
  buildWhen: (previous, current) {
    // return true/false to determine whether or not
    // to rebuild the widget with state
  },
  builder: (context, state) {
    // return widget here based on BlocA's state
  }
)

Bloc

enum TestEvent { increment }

class TestBloc extends Bloc<TestEvent, int> {
  TestBloc(int i) : super(i);

  @override
  Stream<int> mapEventToState(TestEvent event) async* {
    switch (event) {
      case TestEvent.increment:
        yield state + 1;
        break;
    }
  }
}

Provider mode

  • Solution package provided by the community: provider (version 2.0 of provide)
  • Simplify BLoC and reduce complexity. You don't have to learn Stream and RX.

Main API

  • ChangeNotifier observed

    • addListener(VoidCallback listener)
    • removeListener(VoidCallback listener)
    • notifyListeners()
  • ChangeNotifierProvider observer, Widget

Key key

@required T value

Widget child

/// ChangeNotifierProvider(/// create: (_) => new MyChangeNotifier(),/// child: .../// )
  • Consumer special observer (consumer)

Better performance (see notes for details)

  1. Encapsulation status to MySchedule
  2. Use ChangeNotifierProvider to wrap the original layout in MyApp (two widgets share a Parent)
  3. Read the status in the child Widget final schedule = Provider.of(context);
  4. MyChart is a StateLessWidget, and consumer is used in build (Builder: (context, schedule,) = > piechart (function (schedule. Statemanagementtime)...))

summary

  • Pay attention to controlling the granularity of data update in BLoC mode
  • The effects of BLoC and Provider are similar, similar to observer mode + thread scheduling
  • However, BLoC is based on stream, which is closer to the characteristics of Dart language. It is conducive to learning Dart asynchrony, and you can also be familiar with another set of concepts.

reference material:

Fill it out later. I wrote it on the internal website. This is copy

  • Very good BLoC quick contact document reactive programming streams BLoC tutorial
    Reactive programming streams bloc code example StateManagement tutorial
    StateManagement code example BLoC official document
    Framework hodgepodge (BLoC, redux, mvc, inherited_widget, mvi) BLoC tutorial Dart Stream tutorial

Posted by sukanya.paul on Sun, 26 Sep 2021 12:18:23 -0700