“This is the 8th day of my participation in the First Challenge 2022. For details: First Challenge 2022”
GetBuilder is one of the important components of GetX state management. This paper will deeply understand the use and implementation principle of GetBuilder from the introduction of using method and principle analysis.
role
GetBuilder is a Widget component. In the state management of GetX, the main function of GetBuilder is to update interface data together with GetxController. When the Update method of GetxController is called, the Widget wrapped by GetBuilder is refreshed to update the interface data.
use
In the previous article: The use of GetX state management is described in detail. The use of GetBuilder can be used to manage the state of Flutter. Example of implementing an official counter with GetBuilder and GetxController:
CounterBinding:
class CounterBinding extends Bindings {
@override
voiddependencies() { Get.lazyPut(() => CounterController()); }}Copy the code
CounterController:
class CounterController extends GetxController {
int count = 0;
void increase(){
count += 1; update(); }}Copy the code
CounterPage:
class CounterPage extends StatelessWidget {
final controller = Get.find<CounterController>();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Counter"),
),
body: Center(
child: GetBuilder<CounterController>(builder: (logic) {
return Text("${controller.count}", style: const TextStyle(fontSize: 50)); }), ), floatingActionButton: FloatingActionButton( child:constIcon(Icons.add), onPressed: controller.increase, ), ); }}Copy the code
Effect:
Inject the value of the count variable in the CounterController into the CounterPage via dependency injection via CounterBinding. Wrap the Text variable in the GetBuilder to display the value of the count variable in the CounterController. When the plus button is clicked, the increase method of the CounterController is called, which increments the number, and the update method is called to update the interface data, thus implementing the function of the counter.
Related uses of dependency injection can be found in the following article:
- Flutter dissects the implementation of Getx dependency management step by step through the source code
- Use of Flutter GetX dependency injection in detail
- The use of Flutter GetX dependency injection tag is explained
- Detailed explanation of Flutter GetX dependency injection Bindings usage
Source code analysis
The previous introduction of the simple use of GetBuilder, the next will be through the source code analysis of GetBuilder implementation principle and through the source code to further understand the use of GetBuilder.
GetBuilder
Check out GetBuilder’s source code:
class GetBuilder<T extends GetxController> extends StatefulWidget {
final GetControllerBuilder<T> builder;
final bool global;
final Object? id;
final String? tag;
final bool autoRemove;
final bool assignId;
final Object Function(T value)? filter;
final void Function(GetBuilderState<T> state)? initState,
dispose,
didChangeDependencies;
final void Function(GetBuilder oldWidget, GetBuilderState<T> state)?
didUpdateWidget;
final T? init;
const GetBuilder({
Key? key,
this.init,
this.global = true.required this.builder,
this.autoRemove = true.this.assignId = false.this.initState,
this.filter,
this.tag,
this.dispose,
this.id,
this.didChangeDependencies,
this.didUpdateWidget,
}) : super(key: key);
@override
GetBuilderState<T> createState() => GetBuilderState<T>();
}
Copy the code
First, we found that GetBuilder is derived from the StatefulWidget, the StatefulWidget commonly used in Flutter, and has one derived from the GetxController generic. Second GetBuilder in addition to the builder parameters used above, there are a series of parameters, about the specific role and use of parameters will be introduced in the following source analysis process.
GetBuilder doesn’t do any logic, just accepts the parameters that are passed in, and the core code is in State, which is GetBuilderState.
GetBuilderState
GetBuilderState source code:
class GetBuilderState<T extends GetxController> extends State<GetBuilder<T>>
with GetStateUpdaterMixin {
T? controller;
bool? _isCreator = false;
VoidCallback? _remove;
Object? _filter;
@override
voidinitState() {... }void_subscribeToController() {... }void_filterUpdate() {... }@override
voiddispose() {... }@override
voiddidChangeDependencies() {... }@override
voiddidUpdateWidget(GetBuilder oldWidget) {... }@override
Widget build(BuildContext context) {...}
}
Copy the code
The state-related lifecycle methods are implemented in GetBuilderState, along with GetStateUpdaterMixin, and the implementation of each method will be analyzed in the source code below.
build
Let’s first look at the most important method of State, the build method, which creates the display Widget:
@override
Widget build(BuildContext context) {
returnwidget.builder(controller!) ; }Copy the code
The implementation is simple: call the widget. Builder method, which is the Builder parameter passed in when you use GetBuilder, which is the widget that you actually want to display on the interface. The controller parameter is passed in when the widget.builder method is called, and the value of controller is implemented in initState.
initState
The initState method is as follows:
@override
void initState() {
super.initState(); widget.initState? .call(this);
var isRegistered = GetInstance().isRegistered<T>(tag: widget.tag);
if (widget.global) {
if (isRegistered) {
if (GetInstance().isPrepared<T>(tag: widget.tag)) {
_isCreator = true;
} else {
_isCreator = false;
}
controller = GetInstance().find<T>(tag: widget.tag);
} else {
controller = widget.init;
_isCreator = true;
GetInstance().put<T>(controller!, tag: widget.tag);
}
} else {
controller = widget.init;
_isCreator = true; controller? .onStart(); }if(widget.filter ! =null) { _filter = widget.filter! (controller!) ; } _subscribeToController(); }Copy the code
Widget. initState is first called, and widget.initState is the GetBuilder constructor argument, defined as follows:
void Function(GetBuilderState<T> state)? initState
Copy the code
InitState is a function, that is, the initState argument to GetBuilder is a lifecycle callback called in the initState method of State.
Then GetInstance().isregistered
(tag: widget.tag); Check whether the Controller dependencies are registered and pass in a tag. The tag is passed in by the GetBuilder constructor and is used to distinguish the injected Controller dependencies.
For details about isRegistered and tag, see the use of GetX dependency injection for Flutter
Whether widget.global is true, which literally means global. If widget.global is true, whether the Controller dependency is registered. IsPrepared
(tag: widget.tag) isPrepared
(tag: widget.tag)
bool isPrepared<S>({String? tag}) {
final newKey = _getKey(S, tag);
final builder = _getDependency<S>(tag: tag, key: newKey);
if (builder == null) {
return false;
}
if(! builder.isInit) {return true;
}
return false; }}Copy the code
Return false if the dependent Builder is empty or builder.isinit is true that it has been initialized, and true only if the dependency is not initialized.
Return to initState, that is, _isCreator is true when the dependency is not empty and has not been initialized. Please refer to dispose method for the use of _isCreator.
Then get GetInstance().find
(tag: widget.tag); Gets the dependent Controller object.
If isRegistered is false then Controller is not registered:
controller = widget.init;
_isCreator = true; GetInstance().put<T>(controller! , tag: widget.tag);Copy the code
Assign widget.init to the controller, assign _isCreator to true, and finally register the Controller with a dependency via GetInstance().put.
Init: final T? Final T? init; The type is generic T, which is Controller, so the init parameter is passed the initial value of Controller.
The widget.global branch is true, so let’s look at the false branch code:
controller = widget.init;
_isCreator = true; controller? .onStart();Copy the code
When widget.global is false, widget.init is assigned to the Controller, _isCreator is assigned to true, and the controller’s onStart method is called.
The above code for initState is mainly used to get the value of Controller. The init parameter is used as the Controller of GetBuilder if the dependent Controller is unregistered or global is false.
Filter is called if the widget.filter is not empty. As the name indicates, filter is a filter.
final Object Function(T value)? filter;
Copy the code
Passing in the Controller parameter returns an Object value, which will be discussed later.
InitState finally calls the _subscribeToController method, which subscribes to Controller updates.
The initState flow is as follows (click to enlarge) :
_subscribeToController
_subscribeToController Subscribes to Controller update messages with the following source code:
void_subscribeToController() { _remove? .call(); _remove = (widget.id ==null)? controller? .addListener( _filter ! =null? _filterUpdate : getUpdate, ) : controller? .addListenerId( widget.id, _filter ! =null ? _filterUpdate : getUpdate,
);
}
Copy the code
The first call is made if _remove is not empty, and the value of _remove is the return value of the controller.addListener method.
The core code for _subscribeToController is the method to add a listener that calls Controller, which has two listener methods: The addListener and addListenerId methods are called when widget.id is null otherwise addListenerId is called. The source code for the two methods in Controller is as follows:
@override
Disposer addListener(GetStateUpdate listener) {
assert(_debugAssertNotDisposed()); _updaters! .add(listener);return() => _updaters! .remove(listener); } Disposer addListenerId(Object?key, GetStateUpdate listener) { _updatersGroupIds! [key] ?? = <GetStateUpdate>[]; _updatersGroupIds! [key]! .add(listener);return() => _updatersGroupIds! [key]! .remove(listener); }Copy the code
The difference between the two methods is that addListenerId requires a key, which is the previous ID. Both methods require a parameter of type GetStateUpdate, defined as:
typedef GetStateUpdate = void Function(a);Copy the code
Is a function argument, the callback method.
AddListener adds a listener to the List of _updaters and addListenerId to the Map of _updatersGroupIds. The key is the key of the Map, and the Map value is the List. At the same time, both methods finally return a method, the method implementation is to call the corresponding List of remove method.
AddListener and addListenerId add the listener to the collection and return the method to remove the listener from the collection.
The add listener method is called to determine whether _filter is empty. If it is not empty, the _filterUpdate method is passed in, if it is empty, the getUpdate method is passed in, and the value of _filter is the value returned by the widget.filter call. _filterUpdate ();
void _filterUpdate() {
varnewFilter = widget.filter! (controller!) ;if (newFilter != _filter) {
_filter = newFilter;
getUpdate();
}
}
Copy the code
The source code implementation is simple, call widget.filter to get the new value, then compare it with the current _filter value, assign _filter to the new value if it is different, and call getUpdate. If it is null, it is not processed. So ultimately _filterUpdate is still the getUpdate method being called. GetUpdate source code:
mixin GetStateUpdaterMixin<T extends StatefulWidget> on State<T> {
void getUpdate() {
if(mounted) setState(() {}); }}Copy the code
The getUpdate implementation is in GetStateUpdaterMixin, and the code is simple: determine whether the current Widget is mounted and call setState to refresh the Widget if it is. This enables the Widget in the GetBuilder to be refreshed when the data change callback in the Controller listens.
Now that you’ve seen how to add a listener to the Controller in GetBuilder and refresh the Widget after listening for changes, when is the listener callback called in the Controller?
The Widget will refresh when the Controller update is called. Check the Controller update source code:
void update([List<Object>? ids, bool condition = true]) {
if(! condition) {return;
}
if (ids == null) {
refresh();
} else {
for (final id inids) { refreshGroup(id); }}}}Copy the code
Update can pass in a set of ids and call refresh if the set of ids is empty. If the set of ids is not empty, call refresh group.
@protected
void refresh() {
assert(_debugAssertNotDisposed());
_notifyUpdate();
}
@protected
void refreshGroup(Object id) {
assert(_debugAssertNotDisposed());
_notifyIdUpdate(id);
}
Copy the code
The _notifyUpdate and _notifyIdUpdate methods are called again:
void _notifyUpdate() {
for (var element in_updaters!) { element! (a); }}void _notifyIdUpdate(Object id) {
if(_updatersGroupIds! .containsKey(id)) {finallistGroup = _updatersGroupIds! [id]! ;for (var item inlistGroup) { item(); }}}Copy the code
It turns out that the final implementation of calling the _notifyUpdate and _notifyIdUpdate methods is to fetch all listeners from the corresponding collection of listeners, that is, the listeners registered by the addListener and addListenerId methods above, and then call them. The difference is that when an ID is passed in, only the listener for that ID is updated, that is, the Widget is refreshed only when the ID passed in the UPDATE is the same id as the ID passed in the GetBuilder.
Note: From the above source code analysis, you can see that the container with id and without ID store listener is separate, and also separate calls. So when an UPDATE does not pass in an ID, instead of updating all getBuilders, it updates widgets in getBuilders that do not have ids.
dispose
Next look at the Dispose method, an implementation called when the Widget is removed from the render tree:
@override
void dispose() {
super.dispose(); widget.dispose? .call(this);
if (_isCreator! || widget.assignId) {
if(widget.autoRemove && GetInstance().isRegistered<T>(tag: widget.tag)) { GetInstance().delete<T>(tag: widget.tag); } } _remove? .call(); controller =null;
_isCreator = null;
_remove = null;
_filter = null;
}
Copy the code
First, widget.dispose, which is defined as void Function(GetBuilderState
state) of GetBuilder, is called, which is also a lifecycle callback method.
_isCreator and Widget. assignId. Look at _isCreator first. From the previous source analysis of initState, we know that when the Controller dependency builder is empty, Or the current Controller dependency is already initialized, at which point _isCreator will be false.
Widget. assignId is a bool. The default value is false. However, viewing the source code has nothing to do with the id of the parameter, which is only used in Dispose.
Then check whether widget.autoRemove is true. AutoRemove is also a bool and is automatically removed. The default is true. Then check whether the Controller is registered.
After a series of conditional judgments, the GetInstance().delete method is finally executed, which removes the dependency of Controller in GetBuilder from dependency management.
When assignId is set to true and autoRemove is set to true, if the Controller is registered, the Controller will be removed from the dependency when GetBuilder is removed from the rendering tree.
Next call _remove? .call(), according to the analysis of the _subscribeToController and the source code of the Controller, _remove is used to remove the listening in the Controller.
Finally, assign the values of controller, _isCreator, _remove, and _filter to null.
didUpdateWidget
When the parent node calls setState, the child node’s didUpdateWidget method is triggered:
@override
void didUpdateWidget(GetBuilder oldWidget) {
super.didUpdateWidget(oldWidget as GetBuilder<T>);
// to avoid conflicts when modifying a "grouped" id list.
if(oldWidget.id ! = widget.id) { _subscribeToController(); } widget.didUpdateWidget? .call(oldWidget,this);
}
Copy the code
Check whether the old GetBuilder ID is the same as the current ID. If not, re-subscribe Controller. Finally, call widget.didupDateWidget.
didChangeDependencies
In the Widget tree, called when there is a change in the level in the parent structure of a node or in the widget type of any node in the parent structure
@override
void didChangeDependencies() {
super.didChangeDependencies(); widget.didChangeDependencies? .call(this);
}
Copy the code
Do not make other operations, just call the widget. DidChangeDependencies.
conclusion
Through the source code analysis of GetBuilder, the basic understanding of GetBuilder parameters function and implementation principle.
The function of the GetBuilder parameter is summarized as follows:
- Builder: Widget builder that creates widgets to display on the interface
- Init:Initializes the Controller value when
global
Use this value as Controller when false, whenglobal
When true and Controller does not register a dependency, the value of init is injected into the dependency. - Global: whether it is global. Used in Controller initialization and in combination with init
- AutoRemove:Whether to automatically remove Controller dependencies, combined
assignId
Used together - AssignId: True in combination with autoRemove automatically removes Controller dependencies
- Filter: indicates whether to filter by the return value. The interface is refreshed only when the return value changes
- Tag: the Controller dependency injection tag, according to which the Controller instance is obtained
- Id: Refresh flag, used in conjunction with the Controller update to refresh widgets within a specified GetBuilder control
- InitState:Callback function, lifecycle
initState
Method call - The dispose:Callback function, lifecycle
dispose
In the call - DidUpdateWidget:Callback function, lifecycle
didUpdateWidget
In the call - DidChangeDependencies:Callback function, lifecycle
didChangeDependencies
In the call