“This is the second day of my participation in the First Challenge 2022, for more details: First Challenge 2022”.
It has been more than three years since Flutter was first unveiled at Google’s Developer conference in late 2018, and its development has been in full swing. Small and medium-sized enterprises are very popular, and large factories have also invested in technological research. However, there are still many developers wondering which state management framework to choose for their projects. Today, the author will make a technical analysis and comparison of the relatively popular 🔥 state management libraries (Provider, BLoC, GetX) in the community to help everyone better find the appropriate state management library for their projects.
State Management Principles
In order to improve the maintainability and performance of the project, as well as the separation and synchronization of the page UI from data (local or server-side data), we kept the directory structure clear and enabled the state management library during the development process. The MVVM pattern has become the dominant architecture in front-end projects.
MVVM is mode + View + viewModel
- Model represents the page state (that is, the data required by the page)
- View represents the page view (UI)
- ViewModel is the middle layer, responsible for the two-way communication between Model and View, realizing the page view update drive, and responsible for the processing of business logic (such as: condition judgment, network request, etc.).
MVVM can realize the complete separation of view, data, and business logic, so that the project data flow is clear, improve performance, and improve maintainability. User operations on the page trigger data processing, data changes drive page UI refresh. Therefore, single data source and one-way data flow are the key to state management.
- Single data source: The UI here is bound by a single data source, which is completely controllable and will not be arbitrarily affected by other data;
- One-way data flow: user and system operations trigger data processing and data changes ultimately drive view updates. This flow must be one-way and traceable. That is, no matter how many operations the user and the system do, the final data is processed before the view is updated, and data processing cannot be triggered after the view is updated.
The state management libraries in Flutter follow the MVVM principles, so they are designed to make state management better and easier to use while following this principle. In this article, I mainly compare the following three libraries: Provider, BLoC and GetX.
State management in Flutter
State management has always been an issue in Flutter. The debate about Flutter state management didn’t settle down until Flutter replaced Provide with Provider as the official recommended library for state management. However, the rise of GetX in 2021 has caused many new Flutter users to debate which library to use for state management.
So why is state management so important? Here is a business scenario to give you an idea:
Assume that the server pushes data to the APP every ten seconds through websocket, including the content of the article, as well as the number of views and likes. The APP has two pages. Page A displays the list of articles, and click the list item to enter page B to view the details of the article. The contents of pages A and B need to be updated in real time after the message from the server arrives every ten seconds.
-
Common method: To achieve this scenario, we need to register a WebSocket receiver on each page of Flutter. When each page receives a WebSocket message notification, it updates the page view with setState. If you have 10 pages, you need to define 10 sinks, and each sink needs to process the data separately and then setState updates the view. Poor performance does not say, development efficiency is also greatly discounted, error rate is extremely high.
-
State management: In the example above, we want to receive data in only one place and update views in real time as the data changes without setting state per page. Suppose we have a publisher of update events, and then each page is a listener. When the publisher issues an update event upon receiving the data, the listener’s colleague view is updated immediately (no setState required), and the development experience is perfect. This is the typical publish subscriber pattern, and most of the front-end, including the state management in Flutter, is based on this design pattern.
The publish/subscribe mode in Flutter can use the Stream mechanism. Stream system learning
Take the above requirements for example:1.We need a websocket receiver, after receiving the message through streamController. Skin. Add publishing events;2.Page stream registered listeners streamController. Stream. Listen, through setState refresh view in the listener callback.Copy the code
In fact, current Flutter state management, such as RxDART, BLoC, Fluter_redux, Provider, and GetX, requires encapsulating the stream. In addition, the encapsulation of Flutter InheritedWidget evolved StreamBuilder, BlocBuilder and other layout components, thus achieving the effect of updating views in real time without setState. Evolution of Flutter state management
BLoC
BLoC is a design mode proposed by Google, which uses stream to realize asynchronous rendering and redrawing of the interface. We can achieve the separation of business and interface very smoothly through BLoC. Normally, we will introduce the library Flutter_bloc into the project.
The directory structure
BLoC state management usually has three files: BLoC, Event and State
Easy to use
-
When a component needs to use BLoC state management, it needs to declare the provider of the next BLoC before calling the component, as follows:
BlocProvider<BadgesBloc>(create: (context)=> BadgesBloc(),child:UserPage()); Copy the code
-
When a page has multiple BLoC providers, or a BLoC provider common to the entire App, it can be declared globally in advance before loading the App. You can declare this using MultiBlocProvider as follows:
MultiBlocProvider( providers: [ BlocProvider<BadgesBloc>(create:(context) => BadgesBloc()), BlocProvider(create: (context) =>XXX()), ], child: MaterialApp(), ) Copy the code
-
The page layout uses BlocBuilder to create widgets, and the user initiates events in the page via Blocprovider.of (context).add()
///Sample layout BlocBuilder<BadgesBloc, BadgesState>( // Receive the state returned by bloc and bind the view to the variables in the state builder: (context, state) { var isShowBadge = false; if (state is BadgesInitialState) { isShowBadge = state.unReadNotification; } return Badge( showBadge: isShowBadge, shape: BadgeShape.circle, position: BadgePosition(top: - 3, right: - 3), child: Icon(Icons.notifications_none, color: Color(0xFFFFFFFF),),); })Copy the code
/// Page launch event // An event issued to reset the Badge. The event requires a bool pass BlocProvider.of<BadgesBloc>(context).add(ResetBadgeEvent(true)); Copy the code
-
At this point, the event will be received in bloc, which event in the event will be judged, and then the corresponding state will be returned, as follows:
@override Stream<BadgesState> mapEventToState(BadgesEvent event) async* { if (event is ResetBadgeEvent) { yield BadgesInitialState(event.unReadNotification); } } Stream<BadgesInitialState> _mapGetActivityCountState(isShow) async* { // Change the value of the state here so that the view code above can be updated based on this value yield BadgesInitialState(isShow); } Copy the code
-
Add the code screenshots of Event and State
The advantages and disadvantages
- “Advantages”
BLoC has a clear catalogue structure
, fully in line with MVVM conventions. Would be popular for engineering projects,Working as a team reduces the chance of mistakesEveryone follows a pattern and maintainability improves; - “Advantages”
Clear business flow
. Using the Dart Stream event stream as the basic principle, event and State are both event-driven, events are triggered by user actions, and state flow is generated after event processing.A steady flow of data tends to improve code reliability; - “Defect”
BLoC is relatively complex to use
, you need to create multiple files. Although cubit was officially introduced and events were combined into Bloc file, the strong structure still made it difficult for many beginners to get started. - “Defect”
The control of granularity is relatively difficult.
The view built by BlocBuilder will be rebuilt when state changes, and the only way to control the granularity is to refragment bloc, which will greatly increase the code complexity and workload. This can be done by introducing freezed generation code and then reducing the frequency of view refreshes with buildWhen, for example.
Provider
Provider is officially developed and maintained by Flutter and is the most recommended state management library in recent years. Providers are built on top of inheretedWidgets and encapsulate them, greatly reducing the amount of code we need to write. Its characteristics are: uncomplicated, easy to understand, high degree of control. We will introduce the provider library in our project.
Easy to use
- When a component needs to use Provider status management, you need to declare the Provider before invoking the component.
ChangeNotifierProvider<LoginViewModel>.value(
notifier: LoginViewModel(),
child:LoginPage(),
)
Copy the code
- When a page has multiple Provider providers, or the entire App has several universal Provider providers that need to be used on multiple pages, you can declare globally in advance before loading the App. You can declare it using MultiProvider:
MultiProvider(
providers: [
ChangeNotifierProvider<LoginViewModel>( create: (_) => LoginViewModel(),),
ChangeNotifierProvider<HomeViewModel>( create: (_) => HomeViewModel(),),
],
child: MaterialApp()
)
Copy the code
- For the page layout, create a Provider object and then directly bind the data in the viewModel in the widget or fire the event
/// Creating a Provider object
var loginVM = Provider.of<LoginViewModel>(context);
Column(
children: <Widget>[
new Padding(
padding: EdgeInsets.only(top: 85),
child: new Container(
height: 85.h, width: 486.w,
child: TextFormField(
// Bind viewModel data
controller: loginVM.userNameController,
decoration: InputDecoration(
hintText: "Please enter user name",
icon: Icon(Icons.person),
hintStyle: TextStyle(color: Colors.grey, fontSize: 24.sp),
),
validator: (value) {
return value.trim().length > 0 ? null : "Required options"; }))),new Padding(
padding: EdgeInsets.only(top: 40),
child: new Container(
height: 90.h, width: 486.w,
child: new RaisedButton(
// Click to trigger the method in viewModel
onPressed: () { loginVM.loginHandel(context)},
color: const Color(0xff00b4ed), shape: StadiumBorder(),
child: new Text( "Login",
style: new TextStyle(color: Colors.white, fontSize: 32.sp),
),
),
)
)]
Copy the code
- In viewModel, class must inherit from ChangeNotifier. NotifyListeners () are called whenever data needs to be updated, and the page refreshes
Realize the principle of
- Providers typically encapsulate the InheritedWidget components to make them easier to use, and use ChangeNotifier to process the data, thereby reducing the amount of template code associated with the InheritedWidget.
- From the source we can see
Provider
Directly inherited fromInheritedProvider
, via the factory constructorProvider.value
The Model and child nodes are passed and passedcontext.dependOnInheritedWidgetOfExactType<_InheritedProviderScope<T?>>();
Listen for values. - And _InheritedProviderScope inherits from
InheritedWidget
So the implementation of the Provider is really simple and usefulInheritedWidget
Small partners can go to see the source code.
The advantages and disadvantages
- “Advantages”
Using a simple
. The Model inherits from ChangeNotifier. There are no more layout widgets. Just use context.read/context.watch or listen on the Model. - “Advantages”
Particle size control is simple
. To solve the problem of widgets being rebuilt too often, the official launchcontext.select
To listen for some properties of the object. Also can useConsumer/SelectorMake a layout; - “Advantages”
Encapsulation based on the official InheritedWidget
There is no risk, it is stable and does not burden performance - “Defect”
The context of strong correlation
Those of you who have experience with Flutter development know that most of the time the context is basically available in a widget and is always available elsewhereBuildContext
This is impractical and means that most of the time the information provided by the Provider can only be retrieved at the View layer.
GetX
GetX is a lightweight and powerful state management library that tries to do a lot of things. It not only supports state management, but also routing, internationalization, Theme, and a whole host of other features. GetX is definitely a new force in Flutter state management. Since its release, it has attracted a large number of followers due to its simplicity and comprehensive advantages. I haven’t really studied GetX, but after a brief introduction I don’t like the fact that the whole family bucket library keeps our projects relatively limited and puts developers in a passive position without progress.
Easy to use
Let’s use GetX directly to demonstrate the official example” counter “,
- Each click changes the state
- Switch between different pages
- Share state between different pages
- Separate the business logic from the interface
- Turn the MaterialApp into GetMaterialApp
void main() => runApp(GetMaterialApp(home: Home()));
Copy the code
- Create your business logic class and put variables, methods, and controllers in it. Make variables observable with “.obs”
class Controller extends GetxController{
var count = 0.obs;
increment() => count++;
}
Copy the code
- Create the interface
class Home extends StatelessWidget {
@override
Widget build(context) {
// Instantiate your class with get.put () to make it available to all current child routes.
final Controller c = Get.put(Controller());
return Scaffold(
// Use Obx(()=> update Text() whenever the count changes.
appBar: AppBar(title: Obx(() => Text("Clicks: ${c.count}"))),
// Replace the 8 lines of navigator.push with a simple get.to (), no context!
body: Center(child: ElevatedButton(
child: Text("Go to Other"), onPressed: () => Get.to(Other()))), floatingActionButton: FloatingActionButton(child: Icon(Icons.add), onPressed: c.increment)); }}class Other extends StatelessWidget {
// You can ask Get to find a Controller that is being used by another page and return it to you.
final Controller c = Get.find();
@override
Widget build(context){
// Access the updated count variable
return Scaffold(body: Center(child: Text("${c.count}"))); }}Copy the code
It can be seen that it is indeed very simple to use, and it does not follow MVC and MVVM structure, but the impact is not big, efficient development is the most concerned issue of the domestic team. Read more on GetX ReadMe!
Realize the principle of
The implementation principle, I will simply analyze these three points: ① how to achieve data-driven; ② How to manage routing; ③ How to recycle resources when the context is removed.
- GetX implements subscriptions to variables via.obx or obx (Builder) to notify the view of changes as soon as the data changes. This implementation principle is still inseparable from the Dart stream, and we can see from the source code that both ultimately inherit from
RxNotifier
And theRxNotifier
withNotifyManager
.NotifyManager
Is an extension class that provides streamSubscription;
class RxNotifier<T> = RxInterface<T> with NotifyManager<T>;
mixin NotifyManager<T> {
// Comment: provide onListen via GetStream; onPause; OnResume callback
GetStream<T> subject = GetStream<T>();
// comment: Map object, subsequently notified by key-value pair
final _subscriptions = <GetStream, List<StreamSubscription>>{};
bool get canUpdate => _subscriptions.isNotEmpty;
// Comments: internal methods that subscribe to changes in the internal stream
void addListener(GetStream<T> rxGetx) {
if(! _subscriptions.containsKey(rxGetx)) {final subs = rxGetx.listen((data) {
if(! subject.isClosed) subject.add(data); });finallistSubscriptions = _subscriptions[rxGetx] ?? = <StreamSubscription>[];// Issue a notification
listSubscriptions.add(subs);
}
}
StreamSubscription<T> listen(
void Function(T) onData, {
Function? onError,
void Function()? onDone,
bool? cancelOnError,
}) =>
subject.listen(
onData,
onError: onError,
onDone: onDone,
cancelOnError: cancelOnError ?? false,);/// Note: Close subscription to free resources
void close() {
_subscriptions.forEach((getStream, _subscriptions) {
for (final subscription in_subscriptions) { subscription.cancel(); }}); _subscriptions.clear(); subject.close(); }}Copy the code
- The route management of GetX is also done by encapsulating the Flutter Navigator, such as get.toname (), which is provided globally by GetX
NavigatorState
Still calledpushNamed
;
Future<T? >? toNamed<T>(String page, {
dynamic arguments,
int? id,
bool preventDuplicates = true.Map<String.String>? parameters,
}) {
if (preventDuplicates && page == currentRoute) {
return null;
}
if(parameters ! =null) {
final uri = Uri(path: page, queryParameters: parameters);
page = uri.toString();
}
// Note: Global (ID).CurrentState is the navigatorKey in getMaterialApp. router
returnglobal(id).currentState? .pushNamed<T>( page, arguments: arguments, ); }Copy the code
- From the first point, we know how to manage data state, and NotifyManager also provides the close method to release resources. Here we can’t help asking:When to call close to release resources?
The answer is:The resource is released by calling Close from the Widget's Dispose Life hook.
@override
void dispose() {
if(widget.dispose ! =null) widget.dispose! (this);
if (_isCreator! || widget.assignId) {
if (widget.autoRemove && GetInstance().isRegistered<T>(tag: widget.tag)) {
GetInstance().delete<T>(tag: widget.tag);
}
}
_subs.cancel();
// Comment: Release resources here
_observer.close();
controller = null;
_isCreator = null;
super.dispose();
}
Copy the code
The advantages and disadvantages
- “Advantages”
Easiest to use
It is really simple to use and easy to use; Breaking away from the context and using it whenever and wherever you want, solved the pain points of BLoC and Provider; - “Advantages”
Whole family barrel function
With GetX, we no longer need to do route management, internationalization, themes, global context, etc., and even support server-side development. - The second advantage is also the disadvantage. GetX helps us to encapsulate many of the apis that Flutter provides, reducing a lot of work for developers. This will
Make your project heavily dependent on GetX
Given that The Flutter update iteration is so fast, there is no guarantee that GetX will update at a faster pace. If the update is slow, developers will have to wait for GetX (unless they can participate in the community open source). Also, GetX is so basic to use that while making it easy for beginners,Technology also tends to stay superficial
.
conclusion
In addition to the several schemes of appeal, there are other libraries, such as Redux/fish_redux/ RiverPod. Some of these libraries are too complicated, and some are just released. The author noticed them in the research process but did not use them, and they are indeed less active than the above schemes.
In summary, BLoC is suitable for relatively large engineering project teams with a clear structure; Providers are pure and easy to use; GetX is perfect for novice developers……
Write in the last
There is no definite answer to which state management to choose. Difficulty, maintainability, development cost, performance are all factors to consider, and of course it depends on the team and application scenario. If the business scenario is just an input box and a button, excessive pattern design can be overkill. Choose wisely, and see you in the next article!