Preface
The prejudice in people's hearts is a big mountain, no matter how hard you try, you can't move it.
This is a sentence said by Shen Gongbao in the movie "Nezha", and it is also a theme that implements the whole film; perhaps this sentence has resonated with too many people: 35-year-old workplace crisis, major factory card bachelor degree, no house, no car Marriage is difficult and so on, so this sentence is often mentioned.
At the same time, because of the comments of the author of GetX, some stereotypes have been accompanied by the framework of GetX.
I am writing this article, not for getting the name of
- I ask myself that I am not a big fan of any state framework, Provider and Bloc. I wrote related usage, principle analysis articles and related code generation plugins.
- In my mind, this kind of framework is not so mysterious
- Because it is relatively familiar with its principles, it is easier to get started, so there is not much time cost to switch related frameworks
- So, I don’t need to be a defender
There are many excellent ideas in the overall design of GetX. I hope to show these excellent design ideas to everyone; it may be helpful for you to design your own framework, and it is also a record of your own thinking process.
Pre-knowledge
Before talking about the GetX design ideas, we need to introduce a few knowledge first. In the course of Flutter's strong development, they have left a
InheritedWidget
have to say, this control is really a magical control, it is like a magic weapon
- Swordsman slays the dragon, commands the world, don’t dare not to follow the sky, who will fight
- Relying on the Heaven Sword, the sword hides the "Nine Yin Zhenjing"
- Dragon Sword, "Eighteen Palms of the Dragon Slaying", "Wu Mu's Last Letter"
InheritedWidget hide in this magic weapon?
- Depend on node, data transmission
- Fixed-point refresh mechanism
data transmission
InheritedWidget is a control name we collectively , the essence is still 160f8dea7559dd InheritedElement , the data transfer of InheritedWidget, look at the two processes of saving and fetching
save data
- InheritedWidget stores data, which is a relatively simple operation, which can be stored in InheritedElement
class TransferDataWidget extends InheritedWidget {
TransferDataWidget({required Widget child}) : super(child: child);
@override
bool updateShouldNotify(InheritedWidget oldWidget) => false;
@override
InheritedElement createElement() => TransferDataElement(this);
}
class TransferDataElement extends InheritedElement {
TransferDataElement(InheritedWidget widget) : super(widget);
///随便初始化什么, 设置只读都行
String value = '传输数据';
}
fetch data
- As long as it is TransferDataWidget (a subclass of InheritedWidget above) child node, through the child node's BuildContext (Element is the implementation class of BuildContext), you can seamlessly fetch data
var transferDataElement = context.getElementForInheritedWidgetOfExactType<TransferDataWidget>()
as TransferDataElement?;
var msg = transferDataElement.value;
It can be found that we only need to pass the getElementForInheritedWidgetOfExactType method of Element to get the TransferDataElement instance of the parent node (must inherit InheritedElement)
After getting the instance, you can naturally get the corresponding data very easily
Principle
It can be found that we got the XxxInheritedElement instance, and then got the stored value, so the key is getElementForInheritedWidgetOfExactType<T extends InheritedWidget>()
- The code is very simple, just take the value from the _inheritedWidgets map, the generic T is the key
abstract class Element extends DiagnosticableTree implements BuildContext {
Map<Type, InheritedElement>? _inheritedWidgets;
@override
InheritedElement? getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
return ancestor;
}
...
}
- Next, just figure out how _inheritedWidgets stores the value, then everything will be clear
abstract class ComponentElement extends Element {
@mustCallSuper
void mount(Element? parent, dynamic newSlot) {
...
_updateInheritance();
}
void _updateInheritance() {
assert(_lifecycleState == _ElementLifecycle.active);
_inheritedWidgets = _parent?._inheritedWidgets;
}
...
}
abstract class ProxyElement extends ComponentElement {
...
}
class InheritedElement extends ProxyElement {
InheritedElement(InheritedWidget widget) : super(widget);
@override
void _updateInheritance() {
assert(_lifecycleState == _ElementLifecycle.active);
final Map<Type, InheritedElement>? incomingWidgets = _parent?._inheritedWidgets;
if (incomingWidgets != null)
_inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets);
else
_inheritedWidgets = HashMap<Type, InheritedElement>();
_inheritedWidgets![widget.runtimeType] = this;
}
}
The overall logic is still relatively clear
When a certain node is InheritedWidget , the _inheritedWidgets variable of the parent node will be given to the temporary variable of incomingWidgets
- incomingWidgets is empty: _inheritedWidgets variable of the parent class Element, an instance of a map object
- incomingWidgets is not empty: deep copy all the data of the parent node _inheritedWidgets, return a brand new Map instance (containing all the data of the parent node _inheritedWidgets), and assign it to the _inheritedWidgets variable of the parent class Element
- Assign its own instance to the _inheritedWidgets variable of the parent class Element, the key is the runtimeType of the widget
Why can the _inheritedWidgets variable of any Widget Element instance directly get the parent node InheritedElement instance?
- In Element, a parent node assigns a value to a child node: the entire data transmission chain is clear
abstract class Element extends DiagnosticableTree implements BuildContext {
Map<Type, InheritedElement>? _inheritedWidgets;
void _updateInheritance() {
assert(_lifecycleState == _ElementLifecycle.active);
_inheritedWidgets = _parent?._inheritedWidgets;
}
...
}
- Icon
Refresh mechanism
There are some interactions between InheritedElement and Element, in fact it comes with a set of refresh mechanism
- InheritedElement stores the child nodes Element: _dependents, this variable is used to store the child elements that need to be refreshed
class InheritedElement extends ProxyElement {
InheritedElement(InheritedWidget widget) : super(widget);
final Map<Element, Object?> _dependents = HashMap<Element, Object?>();
@protected
void setDependencies(Element dependent, Object? value) {
_dependents[dependent] = value;
}
@protected
void updateDependencies(Element dependent, Object? aspect) {
setDependencies(dependent, null);
}
}
InheritedElement refresh child Element
- The notifyClients method is to take out all the elements stored in _dependents and pass in notifyDependent
- In the notifyDependent method, the incoming Element calls its own didChangeDependencies() method
- Element’s didChangeDependencies() method will call markNeedsBuild() to refresh itself
class InheritedElement extends ProxyElement {
InheritedElement(InheritedWidget widget) : super(widget);
final Map<Element, Object?> _dependents = HashMap<Element, Object?>();
@protected
void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
dependent.didChangeDependencies();
}
@override
void notifyClients(InheritedWidget oldWidget) {
for (final Element dependent in _dependents.keys) {
...
notifyDependent(oldWidget, dependent);
}
}
}
abstract class Element extends DiagnosticableTree implements BuildContext {
...
@mustCallSuper
void didChangeDependencies() {
assert(_lifecycleState == _ElementLifecycle.active); // otherwise markNeedsBuild is a no-op
assert(_debugCheckOwnerBuildTargetExists('didChangeDependencies'));
markNeedsBuild();
}
...
}
How does the child node of InheritedWidget integrate itself Element
added to the _dependents variable of InheritedElement?
There is a dependOnInheritedElement method in Element
- The dependOnInheritedElement method in Element will pass in InheritedElement instance ancestor
- ancestor calls the updateDependencies method to pass in its own Element instance
- InheritedElement will add this Element to the _dependents variable
abstract class Element extends DiagnosticableTree implements BuildContext {
...
@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object? aspect }) {
assert(ancestor != null);
_dependencies ??= HashSet<InheritedElement>();
_dependencies!.add(ancestor);
ancestor.updateDependencies(this, aspect);
return ancestor.widget;
}
...
}
class InheritedElement extends ProxyElement {
InheritedElement(InheritedWidget widget) : super(widget);
final Map<Element, Object?> _dependents = HashMap<Element, Object?>();
@protected
void setDependencies(Element dependent, Object? value) {
_dependents[dependent] = value;
}
@protected
void updateDependencies(Element dependent, Object? aspect) {
setDependencies(dependent, null);
}
}
The use of dependOnInheritedElement method is also very simple
- Generally speaking, use getElementForInheritedWidgetOfExactType in a Widget to get the InheritedElement of the parent node
- Then pass it into the dependOnInheritedElement method
// 举例
var inheritedElement = context
.getElementForInheritedWidgetOfExactType<ChangeNotifierEasyP<T>>()
as EasyPInheritedElement<T>?;
context.dependOnInheritedElement(inheritedElement);
core principle of 160f8dea756977 Provider is to use the refresh mechanism of InheritedWidget
To learn more about the principles of Provider, please refer to the following article
icon
- Take a look at the icon of InheritedWidget refresh mechanism
Routing knowledge
- The routing Navigator is basically static methods for operating routing, and NavigatorState is the specific logic implemented
When you use InheritedWidget to get data, you may have such a trouble: page A ---> page B ---> page C
If I use InheritedWidget to store data on page A and jump to page B or page C, I will find that the InheritedElement of page A cannot be obtained using context
This side proves that the Navigator routing jump: page A jumps to page B, page B is not a child node of page A
- General structure: It can be barely understood that Navigator is the parent node of all pages, and the relationship between pages is level
Here I drew the general structure, if there is any deviation, please be sure to point it out, and I will modify
of Flutter routing principle, please refer to this article (Why the author does not update the article now~~) : Flutter routing principle analysis
Thinking
InheritedWidget brings us a lot of convenience
- We can get the InheritedElement we want in the scope of a Widget tree (discriminate by generics)
- Various interactions between InheritedElement and Element have also realized a set of extremely simple refresh mechanism
- Carry out some in-depth encapsulation, and even manage the resource release of many controls seamlessly
However, the method of obtaining InheritedElement provided by Element cannot be well integrated with the routing mechanism after all; this is also something that cannot be avoided in module design. Perhaps the optimal solution of some module design is difficult to worry about other modules.
InheritedWidget, a magic weapon, has given us a lot of help in our learning process of Flutter
- However, as the requirements become more complex, your technology keeps improving
- Dragon Sword, this magic weapon, maybe gradually, it’s not suitable for you anymore.
- Sometimes, it even restricts your moves
- A blacksmith who makes magical weapons, in his heart, the best magical weapon may always be the next one
separates the interface layer and the logic layer, and both are the logic layer to handle the refresh of the interface; the logic layer can be handed over to the InheritedWidget storage management; indicating that we can definitely store the management ourselves!
- Manage the logic layer by yourself, you can get rid of the constraints of the Element tree without being trapped in the parent and child nodes of the Element tree
- In the page where the route jumps, you can easily get the previous page, the next page, or the previous page logic layer.
This is also a core idea in GetX. This is not a novel or advanced technology, but I think this is a breakthrough in thinking that can bring more possibilities.
Dependency injection
Description
Dependency injection can be implemented in the following ways (Wikipedia):
- Based on the interface. Implement specific interfaces for external containers to inject objects of dependent types.
- Based on the set method. Implement the public set method of a specific attribute to allow the external container to call the object of the dependent type.
- Based on the constructor. Implement the constructor with specific parameters, and pass in the object of the dependent type when creating an object.
- Based on annotations. Based on Java's annotation function , add "@Autowired" and other annotations in front of the private variable, without explicitly defining the above three codes, you can let the external container pass in the corresponding object. This solution is equivalent to defining a public set method, but because there is no real set method, it will not expose interfaces that should not be exposed for dependency injection (because the set method only wants the container to access for injection and does not want other Rely on such object access).
Strongly coupled type, based on constructor
class Test {
String msg;
Test(String msg) {
this.msg = msg;
}
}
set method
class Test {
String? _msg;
void setMsg(String msg) {
this._msg = msg;
}
}
If in Java, the picture is convenient for a while, pass the value directly in the constructor, and then more and more values are needed, which leads to the need to increase the parameter passing of the constructor, because of the strong coupling of many classes, the constructor is changed, and a lot of popularity (The optional parameters of the Dart constructor have no such problems)
- The GetXController injected by Getx is maintained by the GetX framework itself. What would it look like without the middle layer of GetX?
Introduce GetX as an intermediate layer to manage
- Looking at the picture below, I immediately thought of the mediator mode
- This is also the idea of inversion of control (the control of the created object was originally in his own hands, but now it is handed over to a third party)
Put
Let's take a look at the operation injected by GetX
- put use
var controller = Get.put(XxxGetxController());
Look at internal operations
- Hey, all kinds of show operations
- The main logic is in Inst, which is an extended class of GetInterface
class _GetImpl extends GetInterface {}
final Get = _GetImpl();
extension Inst on GetInterface {
S put<S>(S dependency,
{String? tag,
bool permanent = false,
InstanceBuilderCallback<S>? builder}) =>
GetInstance().put<S>(dependency, tag: tag, permanent: permanent);
}
The main logic seems to be in GetInstance
- You can take a look at the implementation of the singleton in this place. I found that many source codes are written in this way, which is very concise
The global data is stored in _singl, which is a Map
- key: the runtimeType of the object or the Type + tag of the class
- value: _InstanceBuilderFactory class, we pass in the dependedt object will be stored in this class
_singl When storing the value of this map, instead of using put, use putIfAbsent
- If there is data in the map with the same key as the incoming key, the incoming data will not be stored
- In other words, objects of the same class instance will not be overwritten when passed in, only the first piece of data will be stored, and subsequent ones will be discarded
- Finally, use the find method to return the passed instance
class GetInstance {
factory GetInstance() => _getInstance ??= GetInstance._();
const GetInstance._();
static GetInstance? _getInstance;
static final Map<String, _InstanceBuilderFactory> _singl = {};
S put<S>(
S dependency, {
String? tag,
bool permanent = false,
@deprecated InstanceBuilderCallback<S>? builder,
}) {
_insert(
isSingleton: true,
name: tag,
permanent: permanent,
builder: builder ?? (() => dependency));
return find<S>(tag: tag);
}
void _insert<S>({
bool? isSingleton,
String? name,
bool permanent = false,
required InstanceBuilderCallback<S> builder,
bool fenix = false,
}) {
final key = _getKey(S, name);
_singl.putIfAbsent(
key,
() => _InstanceBuilderFactory<S>(
isSingleton,
builder,
permanent,
false,
fenix,
name,
),
);
}
String _getKey(Type type, String? name) {
return name == null ? type.toString() : type.toString() + name;
}
S find<S>({String? tag}) {
final key = _getKey(S, tag);
if (isRegistered<S>(tag: tag)) {
if (_singl[key] == null) {
if (tag == null) {
throw 'Class "$S" is not registered';
} else {
throw 'Class "$S" with tag "$tag" is not registered';
}
}
final i = _initDependencies<S>(name: tag);
return i ?? _singl[key]!.getDependency() as S;
} else {
// ignore: lines_longer_than_80_chars
throw '"$S" not found. You need to call "Get.put($S())" or "Get.lazyPut(()=>$S())"';
}
}
}
find
- The find method is quite simple, that is, the operation of fetching data from the map
S find<S>({String? tag}) => GetInstance().find<S>(tag: tag);
Look at the specific logic
- First judge whether the key data is contained in _singl, take it if it has it, throw an exception if it doesn’t
- Key code: _singl[key]!.getDependency() as S , just use the key to get the value of the map directly
class GetInstance {
factory GetInstance() => _getInstance ??= GetInstance._();
const GetInstance._();
static GetInstance? _getInstance;
static final Map<String, _InstanceBuilderFactory> _singl = {};
String _getKey(Type type, String? name) {
return name == null ? type.toString() : type.toString() + name;
}
bool isRegistered<S>({String? tag}) => _singl.containsKey(_getKey(S, tag));
S find<S>({String? tag}) {
final key = _getKey(S, tag);
if (isRegistered<S>(tag: tag)) {
if (_singl[key] == null) {
if (tag == null) {
throw 'Class "$S" is not registered';
} else {
throw 'Class "$S" with tag "$tag" is not registered';
}
}
final i = _initDependencies<S>(name: tag);
return i ?? _singl[key]!.getDependency() as S;
} else {
// ignore: lines_longer_than_80_chars
throw '"$S" not found. You need to call "Get.put($S())" or "Get.lazyPut(()=>$S())"';
}
}
}
GetBuilder refresh mechanism
use
For the continuity of knowledge, simply write down and use here
- Logic layer
class GetCounterEasyLogic extends GetxController {
var count = 0;
void increase() {
++count;
update();
}
}
- interface
class GetCounterEasyPage extends StatelessWidget {
final GetCounterEasyLogic logic = Get.put(GetCounterEasyLogic());
@override
Widget build(BuildContext context) {
return BaseScaffold(
appBar: AppBar(title: const Text('计数器-简单式')),
body: Center(
child: GetBuilder<GetCounterEasyLogic>(builder: (logic) {
return Text(
'点击了 ${logic.count} 次',
style: TextStyle(fontSize: 30.0),
);
}),
),
floatingActionButton: FloatingActionButton(
onPressed: () => logic.increase(),
child: Icon(Icons.add),
),
);
}
}
GetBuilder
One day, I was lying in bed thinking
- Obx state management, GetXController instance recycling is placed in the route, in many scenarios, there are some limitations
- Later I thought, GetBuilder uses generics, which can get GetxController instance, GetBuilder is StatefulWidget again
- In this way, it can be used to recycle instances, which can solve the problem that GetXController instances cannot be reclaimed in many scenarios (not using Getx routing)
- I opened the Getx project with enthusiasm and prepared to submit a PR, and then found that GetBuilder has written the operation of recycling instances in dispose.
- Gan!
Built-in recycling mechanism
- A lot of code is simplified here, only the code of the recycling mechanism is shown
class GetBuilder<T extends GetxController> extends StatefulWidget {
final GetControllerBuilder<T> builder;
final bool global;
final String? tag;
final bool autoRemove;
final T? init;
const GetBuilder({
Key? key,
this.init,
this.global = true,
required this.builder,
this.autoRemove = true,
this.initState,
this.tag,
}) : super(key: key);
@override
GetBuilderState<T> createState() => GetBuilderState<T>();
}
class GetBuilderState<T extends GetxController> extends State<GetBuilder<T>>
with GetStateUpdaterMixin {
T? controller;
bool? _isCreator = false;
VoidCallback? _remove;
Object? _filter;
@override
void initState() {
super.initState();
widget.initState?.call(this);
var isRegistered = GetInstance().isRegistered<T>(tag: widget.tag);
if (widget.global) {
if (isRegistered) {
controller = GetInstance().find<T>(tag: widget.tag);
} else {
controller = widget.init;
GetInstance().put<T>(controller!, tag: widget.tag);
}
} else {
controller = widget.init;
controller?.onStart();
}
}
@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;
}
@override
Widget build(BuildContext context) {
return widget.builder(controller!);
}
}
The logic in the code is quite clear, initState gets the instance, dispose reclaims the instance
Obtain the corresponding GetXController instance through generics on GetBuilder
- Does not exist: the instance passed in using init
- Exist: use directly; the instance passed by init is invalid
autoRemove can control whether to automatically recycle GetXController instances
- The default is true: automatic recycling is enabled by default
- true: turn on automatic recycling false: turn off automatic recycling
Refresh logic
- Only the relevant code of the refresh logic is retained here, and the code that does not need to be concerned is removed
mixin GetStateUpdaterMixin<T extends StatefulWidget> on State<T> {
void getUpdate() {
if (mounted) setState(() {});
}
}
class GetBuilder<T extends GetxController> extends StatefulWidget {
final GetControllerBuilder<T> builder;
final bool global;
final T? init;
final Object? id;
const GetBuilder({
Key? key,
this.init,
this.id,
this.global = true,
required this.builder,
}) : super(key: key);
@override
GetBuilderState<T> createState() => GetBuilderState<T>();
}
class GetBuilderState<T extends GetxController> extends State<GetBuilder<T>>
with GetStateUpdaterMixin {
T? controller;
@override
void initState() {
super.initState();
...
if (widget.global) {
if (isRegistered) {
controller = GetInstance().find<T>(tag: widget.tag);
} else {
controller = widget.init;
GetInstance().put<T>(controller!, tag: widget.tag);
}
} else {
controller = widget.init;
controller?.onStart();
}
_subscribeToController();
}
void _subscribeToController() {
_remove?.call();
_remove = (widget.id == null)
? controller?.addListener(
_filter != null ? _filterUpdate : getUpdate,
)
: controller?.addListenerId(
widget.id,
_filter != null ? _filterUpdate : getUpdate,
);
}
void _filterUpdate() {
var newFilter = widget.filter!(controller!);
if (newFilter != _filter) {
_filter = newFilter;
getUpdate();
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
widget.didChangeDependencies?.call(this);
}
@override
void didUpdateWidget(GetBuilder oldWidget) {
super.didUpdateWidget(oldWidget as GetBuilder<T>);
if (oldWidget.id != widget.id) {
_subscribeToController();
}
widget.didUpdateWidget?.call(oldWidget, this);
}
@override
Widget build(BuildContext context) {
return widget.builder(controller!);
}
}
key step
- Get the injected GetXController instance through generics
Add monitoring code
- addListener: add listener callback
- addListenerId: To add a listener callback, the id must be set, and the matching id must be written when the update is refreshed
Monitoring code: The core code is the getUpdate method, which is in GetStateUpdaterMixin
- The logic of getUpdate() is setState(), refresh the current GetBuilder
icon
Update
The trigger logic is still very simple, just use update
- Ids: Corresponds to the Getbuilder above, and can refresh the GetBuilder corresponding to the set id
- condition: Whether to refresh a judgment condition, the default is true (assuming that a certain id must be greater than 3 to refresh: update([1, 2, 3, 4], index> 3))
abstract class GetxController extends DisposableInterface with ListNotifier {
void update([List<Object>? ids, bool condition = true]) {
if (!condition) {
return;
}
if (ids == null) {
refresh();
} else {
for (final id in ids) {
refreshGroup(id);
}
}
}
}
Look at the key method refresh(), in the ListNotifier class
- It can be found that generics in _updaters is a method
- The monitor added in GetBuilder is a method parameter, and the method body is setState()
- All alive! GetBuilder add method (method body is setState), update traversal triggers all add methods
typedef GetStateUpdate = void Function();
class ListNotifier implements Listenable {
List<GetStateUpdate?>? _updaters = <GetStateUpdate?>[];
HashMap<Object?, List<GetStateUpdate>>? _updatersGroupIds =
HashMap<Object?, List<GetStateUpdate>>();
@protected
void refresh() {
assert(_debugAssertNotDisposed());
_notifyUpdate();
}
void _notifyUpdate() {
for (var element in _updaters!) {
element!();
}
}
...
}
If the id parameter is added to the update, the refreshGroup method will be used. The logic is almost the same as refresh, the difference is the judgment of the id: if it is available, it will be executed, and if it is not, it will be skipped.
- Traverse all ids, and then execute the refreshGroup method
abstract class GetxController extends DisposableInterface with ListNotifier {
void update([List<Object>? ids, bool condition = true]) {
if (!condition) {
return;
}
if (ids == null) {
refresh();
} else {
for (final id in ids) {
refreshGroup(id);
}
}
}
}
class ListNotifier implements Listenable {
HashMap<Object?, List<GetStateUpdate>>? _updatersGroupIds =
HashMap<Object?, List<GetStateUpdate>>();
void _notifyIdUpdate(Object id) {
if (_updatersGroupIds!.containsKey(id)) {
final listGroup = _updatersGroupIds![id]!;
for (var item in listGroup) {
item();
}
}
}
@protected
void refreshGroup(Object id) {
assert(_debugAssertNotDisposed());
_notifyIdUpdate(id);
}
}
to sum up
- Take a look at the GetBuilder refresh icon
Obx refresh mechanism
This refresh mechanism is different from our commonly used state management framework (provider, bloc) and the above GetBuilder.
Variables: basic types, entities and data types such as lists. The author has encapsulated a set of Rx types, and quickly adds obs after the data.
- For example: RxString msg = "test".obs (var msg = "test".obs)
- Updating: the basic type can directly update the data, and the entity needs to be in the form of .update()
Usage: To use this kind of variable, .value generally added. The author also gives a shortcut variable followed by ()
- I do not recommend adding () form of ongoing maintenance of the project were too unfriendly
Obx refresh mechanism. The most interesting thing is that after the variable is changed, the Obx that wraps the variable will be automatically refreshed! Note that only the Obx that wraps the variable will be refreshed! Other Obx will not refresh.
How is this done?
- In fact, it's very simple to implement
- However, if you haven't come into contact with this idea, I'm afraid it will be hard to come up with a broken head, and you can still play like this. . .
use
Simply look at the use
- logic
class GetCounterRxLogic extends GetxController {
var count = 0.obs;
///自增
void increase() => ++count;
}
- view
class GetCounterRxPage extends StatelessWidget {
final GetCounterRxLogic logic = Get.put(GetCounterRxLogic());
@override
Widget build(BuildContext context) {
return BaseScaffold(
appBar: AppBar(title: const Text('计数器-响应式')),
body: Center(
child: Obx(() {
return Text(
'点击了 ${logic.count.value} 次',
style: TextStyle(fontSize: 30.0),
);
}),
),
floatingActionButton: FloatingActionButton(
onPressed: () => logic.increase(),
child: Icon(Icons.add),
),
);
}
}
Rx class variables
as an example here, take a look at its internal implementation
- Let's first look at the extension obs behind the integer, which is an extension class, 0.obs equivalent to RxInt(0)
extension IntExtension on int {
/// Returns a `RxInt` with [this] `int` as initial value.
RxInt get obs => RxInt(this);
}
- Take a look at RxInt: This place makes it clear that when running with .value, it will automatically return a current instance and modify the corresponding value value
class RxInt extends Rx<int> {
RxInt(int initial) : super(initial);
/// Addition operator.
RxInt operator +(int other) {
value = value + other;
return this;
}
/// Subtraction operator.
RxInt operator -(int other) {
value = value - other;
return this;
}
}
Take a look at the parent class Rx<T>
- A very important class appeared in this place: _RxImpl<T>
class Rx<T> extends _RxImpl<T> {
Rx(T initial) : super(initial);
@override
dynamic toJson() {
try {
return (value as dynamic)?.toJson();
} on Exception catch (_) {
throw '$T has not method [toJson]';
}
}
}
_RxImpl<T> class inherits RxNotifier<T> and with RxObjectMixin<T>
- The content of this class is relatively large, mainly RxNotifier<T> and RxObjectMixin<T> have a lot of content
- There is a lot of code, first show the complete code; it will be simplified in the next description
abstract class _RxImpl<T> extends RxNotifier<T> with RxObjectMixin<T> {
_RxImpl(T initial) {
_value = initial;
}
void addError(Object error, [StackTrace? stackTrace]) {
subject.addError(error, stackTrace);
}
Stream<R> map<R>(R mapper(T? data)) => stream.map(mapper);
void update(void fn(T? val)) {
fn(_value);
subject.add(_value);
}
void trigger(T v) {
var firstRebuild = this.firstRebuild;
value = v;
if (!firstRebuild) {
subject.add(v);
}
}
}
class RxNotifier<T> = RxInterface<T> with NotifyManager<T>;
mixin NotifyManager<T> {
GetStream<T> subject = GetStream<T>();
final _subscriptions = <GetStream, List<StreamSubscription>>{};
bool get canUpdate => _subscriptions.isNotEmpty;
void addListener(GetStream<T> rxGetx) {
if (!_subscriptions.containsKey(rxGetx)) {
final subs = rxGetx.listen((data) {
if (!subject.isClosed) subject.add(data);
});
final listSubscriptions =
_subscriptions[rxGetx] ??= <StreamSubscription>[];
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,
);
void close() {
_subscriptions.forEach((getStream, _subscriptions) {
for (final subscription in _subscriptions) {
subscription.cancel();
}
});
_subscriptions.clear();
subject.close();
}
}
mixin RxObjectMixin<T> on NotifyManager<T> {
late T _value;
void refresh() {
subject.add(value);
}
T call([T? v]) {
if (v != null) {
value = v;
}
return value;
}
bool firstRebuild = true;
String get string => value.toString();
@override
String toString() => value.toString();
dynamic toJson() => value;
@override
bool operator ==(dynamic o) {
if (o is T) return value == o;
if (o is RxObjectMixin<T>) return value == o.value;
return false;
}
@override
int get hashCode => _value.hashCode;
set value(T val) {
if (subject.isClosed) return;
if (_value == val && !firstRebuild) return;
firstRebuild = false;
_value = val;
subject.add(_value);
}
T get value {
if (RxInterface.proxy != null) {
RxInterface.proxy!.addListener(subject);
}
return _value;
}
Stream<T?> get stream => subject.stream;
void bindStream(Stream<T> stream) {
final listSubscriptions =
_subscriptions[subject] ??= <StreamSubscription>[];
listSubscriptions.add(stream.listen((va) => value = va));
}
}
Simplify _RxImpl<T>, the above content is too much, I simplified this place to show the content that needs attention: here are a few points that need to be focused on
- RxInt is a data type with built-in callback (GetStream)
- When the value variable of RxInt changes (set value), subject.add(_value) will be triggered, and the internal logic is an automatic refresh operation
- getting the value variable of RxInt (get value), there will be an operation to add a monitor, which is very important!
abstract class _RxImpl<T> extends RxNotifier<T> with RxObjectMixin<T> {
void update(void fn(T? val)) {
fn(_value);
subject.add(_value);
}
}
class RxNotifier<T> = RxInterface<T> with NotifyManager<T>;
mixin NotifyManager<T> {
GetStream<T> subject = GetStream<T>();
final _subscriptions = <GetStream, List<StreamSubscription>>{};
bool get canUpdate => _subscriptions.isNotEmpty;
void addListener(GetStream<T> rxGetx) {
if (!_subscriptions.containsKey(rxGetx)) {
final subs = rxGetx.listen((data) {
if (!subject.isClosed) subject.add(data);
});
final listSubscriptions =
_subscriptions[rxGetx] ??= <StreamSubscription>[];
listSubscriptions.add(subs);
}
}
}
mixin RxObjectMixin<T> on NotifyManager<T> {
late T _value;
void refresh() {
subject.add(value);
}
set value(T val) {
if (subject.isClosed) return;
if (_value == val && !firstRebuild) return;
firstRebuild = false;
_value = val;
subject.add(_value);
}
T get value {
if (RxInterface.proxy != null) {
RxInterface.proxy!.addListener(subject);
}
return _value;
}
}
Why does the add of GetStream have a refresh operation: a lot of code is deleted, and the key code is retained
- When the add method is called, the _notifyData method will be called
- In the _notifyData method, the _onData list will be traversed, and its generic _data method will be executed according to the conditions
- I guess, the method body in _data must have setState() added somewhere in all likelihood
class GetStream<T> {
GetStream({this.onListen, this.onPause, this.onResume, this.onCancel});
List<LightSubscription<T>>? _onData = <LightSubscription<T>>[];
FutureOr<void> addSubscription(LightSubscription<T> subs) async {
if (!_isBusy!) {
return _onData!.add(subs);
} else {
await Future.delayed(Duration.zero);
return _onData!.add(subs);
}
}
void _notifyData(T data) {
_isBusy = true;
for (final item in _onData!) {
if (!item.isPaused) {
item._data?.call(data);
}
}
_isBusy = false;
}
T? _value;
T? get value => _value;
void add(T event) {
assert(!isClosed, 'You cannot add event to closed Stream');
_value = event;
_notifyData(event);
}
}
typedef OnData<T> = void Function(T data);
class LightSubscription<T> extends StreamSubscription<T> {
OnData<T>? _data;
}
Icon, let’s take a look at the functions of the Rx class
- get value add monitor
- set value executes the added monitor
Obx refresh mechanism
The biggest special feature of Obx should be that when it is used, it does not need to add generics and can be refreshed automatically. How is this done?
Obx: Not much code, but they all have magical uses
- Obx inherits ObxWidget, ObxWidget is actually a StatefulWidget
- The code in _ObxState is the core code
class Obx extends ObxWidget {
final WidgetCallback builder;
const Obx(this.builder);
@override
Widget build() => builder();
}
abstract class ObxWidget extends StatefulWidget {
const ObxWidget({Key? key}) : super(key: key);
@override
_ObxState createState() => _ObxState();
@protected
Widget build();
}
class _ObxState extends State<ObxWidget> {
RxInterface? _observer;
late StreamSubscription subs;
_ObxState() {
_observer = RxNotifier();
}
@override
void initState() {
subs = _observer!.listen(_updateTree, cancelOnError: false);
super.initState();
}
void _updateTree(_) {
if (mounted) {
setState(() {});
}
}
@override
void dispose() {
subs.cancel();
_observer!.close();
super.dispose();
}
Widget get notifyChilds {
final observer = RxInterface.proxy;
RxInterface.proxy = _observer;
final result = widget.build();
if (!_observer!.canUpdate) {
throw """
[Get] the improper use of a GetX has been detected.
You should only use GetX or Obx for the specific widget that will be updated.
If you are seeing this error, you probably did not insert any observable variables into GetX/Obx
or insert them outside the scope that GetX considers suitable for an update
(example: GetX => HeavyWidget => variableObservable).
If you need to update a parent widget and a child widget, wrap each one in an Obx/GetX.
""";
}
RxInterface.proxy = observer;
return result;
}
@override
Widget build(BuildContext context) => notifyChilds;
}
Add listener
If a control wants to refresh, there must be logic to add monitoring, and then manually trigger it somewhere
- Take a look at where the _ObxState class is added to monitor: only the code added by the monitor is displayed
- _ObxState will instantiate an RxNotifier() object when it is initialized, and use the _observer variable to accept: this operation is very important
- A key operation is done in initState. In the listener method of _observer, the _updateTree method is passed in. The logic body in this method is setState()
class _ObxState extends State<ObxWidget> {
RxInterface? _observer;
late StreamSubscription subs;
_ObxState() {
_observer = RxNotifier();
}
@override
void initState() {
subs = _observer!.listen(_updateTree, cancelOnError: false);
super.initState();
}
void _updateTree(_) {
if (mounted) {
setState(() {});
}
}
}
A lot of the above logic is related to the RxNotifier class, take a look at this class
- The RxNotifier class will instantiate a GetStream<T>() object internally and assign it to the subject
- The above assignment _updateTree method is passed into the GetStream<T>() class, and finally _onData is added to the list variable
- Take a glance at the _notifyData method, is it traversing the method that executes the item in the _onData list (item. _data?.call(data))
class RxNotifier<T> = RxInterface<T> with NotifyManager<T>;
mixin NotifyManager<T> {
GetStream<T> subject = GetStream<T>();
final _subscriptions = <GetStream, List<StreamSubscription>>{};
bool get canUpdate => _subscriptions.isNotEmpty;
StreamSubscription<T> listen(
void Function(T) onData, {
Function? onError,
void Function()? onDone,
bool? cancelOnError,
}) =>
subject.listen(
onData,
onError: onError,
onDone: onDone,
cancelOnError: cancelOnError ?? false,
);
}
class GetStream<T> {
void Function()? onListen;
void Function()? onPause;
void Function()? onResume;
FutureOr<void> Function()? onCancel;
GetStream({this.onListen, this.onPause, this.onResume, this.onCancel});
List<LightSubscription<T>>? _onData = <LightSubscription<T>>[];
FutureOr<void> addSubscription(LightSubscription<T> subs) async {
if (!_isBusy!) {
return _onData!.add(subs);
} else {
await Future.delayed(Duration.zero);
return _onData!.add(subs);
}
}
int? get length => _onData?.length;
bool get hasListeners => _onData!.isNotEmpty;
void _notifyData(T data) {
_isBusy = true;
for (final item in _onData!) {
if (!item.isPaused) {
item._data?.call(data);
}
}
_isBusy = false;
}
LightSubscription<T> listen(void Function(T event) onData,
{Function? onError, void Function()? onDone, bool? cancelOnError}) {
final subs = LightSubscription<T>(
removeSubscription,
onPause: onPause,
onResume: onResume,
onCancel: onCancel,
)
..onData(onData)
..onError(onError)
..onDone(onDone)
..cancelOnError = cancelOnError;
addSubscription(subs);
onListen?.call();
return subs;
}
}
- The code flow above is a bit convoluted, a picture is drawn below, I hope to help you
Monitor transfer
made a very important in the _ObxState class, the operation of monitoring object transfer
_observer has got the setState method inside the Obx control, now it needs to be transferred out!
Paste the code to transfer the objects in _observer below: The main logic is in the notifyChilds method
- RxInterface class, this variable is very important, it is a transit variable!
`class _ObxState extends State<ObxWidget> {
RxInterface? _observer;
_ObxState() {
_observer = RxNotifier();
}
Widget get notifyChilds {
final observer = RxInterface.proxy;
RxInterface.proxy = _observer;
final result = widget.build();
if (!_observer!.canUpdate) {
throw """
[Get] the improper use of a GetX has been detected.
You should only use GetX or Obx for the specific widget that will be updated.
If you are seeing this error, you probably did not insert any observable variables into GetX/Obx
or insert them outside the scope that GetX considers suitable for an update
(example: GetX => HeavyWidget => variableObservable).
If you need to update a parent widget and a child widget, wrap each one in an Obx/GetX.
""";
}
RxInterface.proxy = observer;
return result;
}
@override
Widget build(BuildContext context) => notifyChilds;
}
abstract class RxInterface<T> {
bool get canUpdate;
void addListener(GetStream<T> rxGetx);
void close();
static RxInterface? proxy;
StreamSubscription<T> listen(void Function(T event) onData,
{Function? onError, void Function()? onDone, bool? cancelOnError});
}
notifyChilds have profound meaning, and the interpretation of line by line is
- final observer = RxInterface.proxy: RxInterface.proxy is normally empty, but it may be used as an intermediate variable to temporarily store the object. Now temporarily take out his object and store it in the observer variable
RxInterface.proxy = _observer: Assign the address of the RxNotifier() object we instantiated in the _ObxState class to RxInterface.proxy
- Note: Here, the RxNotifier() instance in RxInterface.proxy has the setState() method of the current Obx control
final result = widget.build(): This assignment is very important! Call the Widget we passed in from the outside
- If there is a responsive variable in this Widget, then the variable will be called to get the get value
Remember the code for get value?
mixin RxObjectMixin<T> on NotifyManager<T> { late T _value; T get value { if (RxInterface.proxy != null) { RxInterface.proxy!.addListener(subject); } return _value; } } mixin NotifyManager<T> { GetStream<T> subject = GetStream<T>(); }
Finally a connection is established. The GetStream instance in the variable is added to the RxNotifier() instance in Obx; there is a subject (GetStream) instance in the RxNotifier() instance. The data change in the Rx type will trigger the subject change, and finally the Obx will be refreshed.
mixin NotifyManager<T> { GetStream<T> subject = GetStream<T>(); final _subscriptions = <GetStream, List<StreamSubscription>>{}; bool get canUpdate => _subscriptions.isNotEmpty; void addListener(GetStream<T> rxGetx) { if (!_subscriptions.containsKey(rxGetx)) { //重点 GetStream中listen方法是用来添加监听方法的,add的时候会刷新监听方法 final subs = rxGetx.listen((data) { if (!subject.isClosed) subject.add(data); }); final listSubscriptions = _subscriptions[rxGetx] ??= <StreamSubscription>[]; listSubscriptions.add(subs); } } }
- if (!_observer!.canUpdate) {}: This judgment is very simple. If there is no Rx type variable in the Widget we pass in, the _subscriptions array will be empty, and this judgment will fail
- RxInterface.proxy = observer: Re-assign the original value in RxInterface.proxy to yourself, so far, the _observer object address in _ObxState, after a fantasy tour, ended his mission
Icon
to sum up
Obx's refresh mechanism is quite interesting
- The Rx variable is changed, and the Obx control that wraps its variable is automatically refreshed. Other Obx controls will not be refreshed.
- Use Obx controls, no need to write generics! Niu batch!
However, I think the Obx refresh mechanism also has its own shortcomings. From the perspective of its implementation principle, this is unavoidable.
- Because Obx automatically refreshes, each variable must have its own monitoring trigger mechanism; therefore, all basic types, entities, and lists need to be repackaged, which will cause serious usage impact: variable assignment, type calibration , Refresh is very normal and the writing is different.
- Because of the re-encapsulation of all types, after the above code backtracking, everyone also found that the encapsulation type has quite a lot of code; the encapsulation type must occupy more resources than the dart's own type (this problem can be avoided: encapsulating a reactive variable, It does not necessarily need a lot of code, I give a package reference below)
Hand rub a state management framework
GetX has built-in two sets of state management mechanisms, here will also be based on its refresh mechanism, hand rub the two sets out
I will use extremely simple code to reproduce two sets of classic mechanisms
Dependency injection
Before doing the refresh mechanism, we must first write a dependency injection class, we need to manage those instances of the logic layer by ourselves
- I wrote an extremely simple one here, which only implements three basic functions: inject, get, delete
///依赖注入,外部可将实例,注入该类中,由该类管理
class Easy {
///注入实例
static T put<T>(T dependency, {String? tag}) =>
_EasyInstance().put(dependency, tag: tag);
///获取注入的实例
static T find<T>({String? tag, String? key}) =>
_EasyInstance().find<T>(tag: tag, key: key);
///删除实例
static bool delete<T>({String? tag, String? key}) =>
_EasyInstance().delete<T>(tag: tag, key: key);
}
///具体逻辑
class _EasyInstance {
factory _EasyInstance() => _instance ??= _EasyInstance._();
static _EasyInstance? _instance;
_EasyInstance._();
static final Map<String, _InstanceInfo> _single = {};
///注入实例
T put<T>(T dependency, {String? tag}) {
final key = _getKey(T, tag);
//只保存第一次注入:针对自动刷新机制优化,每次热重载的时候,数据不会重置
_single.putIfAbsent(key, () => _InstanceInfo<T>(dependency));
return find<T>(tag: tag);
}
///获取注入的实例
T find<T>({String? tag, String? key}) {
final newKey = key ?? _getKey(T, tag);
var info = _single[newKey];
if (info?.value != null) {
return info!.value;
} else {
throw '"$T" not found. You need to call "Easy.put($T())""';
}
}
///删除实例
bool delete<T>({String? tag, String? key}) {
final newKey = key ?? _getKey(T, tag);
if (!_single.containsKey(newKey)) {
print('Instance "$newKey" already removed.');
return false;
}
_single.remove(newKey);
print('Instance "$newKey" deleted.');
return true;
}
String _getKey(Type type, String? name) {
return name == null ? type.toString() : type.toString() + name;
}
}
class _InstanceInfo<T> {
_InstanceInfo(this.value);
T value;
}
- Customize a monitoring class, this class is very important, the following two mechanisms need to be used
///自定义个监听触发类
class EasyXNotifier {
List<VoidCallback> _listeners = [];
void addListener(VoidCallback listener) {
_listeners.add(listener);
}
void removeListener(VoidCallback listener) {
for (final entry in _listeners) {
if (entry == listener) {
_listeners.remove(entry);
return;
}
}
}
void dispose() {
_listeners.clear();
}
void notify() {
if (_listeners.isEmpty) return;
for (final entry in _listeners) {
try {
entry.call();
} catch (e) {
print(e.toString());
}
}
}
}
EasyBuilder
achieve
This mode requires a custom base class
- What I wrote here is minimal, and the related life cycles are not added. This is easy to add up. Define each life cycle and trigger it in the Builder control.
- For the sake of code brevity, this will not be shown temporarily
class EasyXController {
EasyXNotifier xNotifier = EasyXNotifier();
///刷新控件
void update() {
xNotifier.notify();
}
}
Let's take a look at the core EasyBuilder control: that's it!
- The realization of the code is extremely simple, I hope everyone can have a clear idea
///刷新控件,自带回收机制
class EasyBuilder<T extends EasyXController> extends StatefulWidget {
final Widget Function(T logic) builder;
final String? tag;
final bool autoRemove;
const EasyBuilder({
Key? key,
required this.builder,
this.autoRemove = true,
this.tag,
}) : super(key: key);
@override
_EasyBuilderState<T> createState() => _EasyBuilderState<T>();
}
class _EasyBuilderState<T extends EasyXController>
extends State<EasyBuilder<T>> {
late T controller;
@override
void initState() {
super.initState();
controller = Easy.find<T>(tag: widget.tag);
controller.xNotifier.addListener(() {
if (mounted) setState(() {});
});
}
@override
void dispose() {
if (widget.autoRemove) {
Easy.delete<T>(tag: widget.tag);
}
controller.xNotifier.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return widget.builder(controller);
}
}
use
- Very simple to use, first look at the logic layer
class EasyXCounterLogic extends EasyXController {
var count = 0;
void increase() {
++count;
update();
}
}
- Interface layer
class EasyXCounterPage extends StatelessWidget {
final EasyXCounterLogic logic = Easy.put(EasyXCounterLogic());
@override
Widget build(BuildContext context) {
return BaseScaffold(
appBar: AppBar(title: const Text('EasyX-自定义EasyBuilder刷新机制')),
body: Center(
child: EasyBuilder<EasyXCounterLogic>(builder: (logic) {
return Text(
'点击了 ${logic.count} 次',
style: TextStyle(fontSize: 30.0),
);
}),
),
floatingActionButton: FloatingActionButton(
onPressed: () => logic.increase(),
child: Icon(Icons.add),
),
);
}
}
- Effect picture
Ebx: automatic refresh mechanism
Automatic refresh mechanism, because there is no generic type, it is impossible to determine which injection instance is used internally. In Getx, these instances are recycled in the routing. However, if you do not use GetX routing and use Obx, you will find , GetXController can't recover automatically! ! !
For this scenario here, I will give a solution
achieve
In the automatic refresh mechanism, the basic types need to be encapsulated
- The main logic is in Rx<T>
- set value and get value are the key
///拓展函数
extension IntExtension on int {
RxInt get ebs => RxInt(this);
}
extension StringExtension on String {
RxString get ebs => RxString(this);
}
extension DoubleExtension on double {
RxDouble get ebs => RxDouble(this);
}
extension BoolExtension on bool {
RxBool get ebs => RxBool(this);
}
///封装各类型
class RxInt extends Rx<int> {
RxInt(int initial) : super(initial);
RxInt operator +(int other) {
value = value + other;
return this;
}
RxInt operator -(int other) {
value = value - other;
return this;
}
}
class RxDouble extends Rx<double> {
RxDouble(double initial) : super(initial);
RxDouble operator +(double other) {
value = value + other;
return this;
}
RxDouble operator -(double other) {
value = value - other;
return this;
}
}
class RxString extends Rx<String> {
RxString(String initial) : super(initial);
}
class RxBool extends Rx<bool> {
RxBool(bool initial) : super(initial);
}
///主体逻辑
class Rx<T> {
EasyXNotifier subject = EasyXNotifier();
Rx(T initial) {
_value = initial;
}
late T _value;
bool firstRebuild = true;
String get string => value.toString();
@override
String toString() => value.toString();
set value(T val) {
if (_value == val && !firstRebuild) return;
firstRebuild = false;
_value = val;
subject.notify();
}
T get value {
if (RxEasy.proxy != null) {
RxEasy.proxy!.addListener(subject);
}
return _value;
}
}
Need to write a very important transfer class, this will also store the listener object of the response variable
- This class has a very core logic: it associates responsive variables with refresh controls!
class RxEasy {
EasyXNotifier easyXNotifier = EasyXNotifier();
Map<EasyXNotifier, String> _listenerMap = {};
bool get canUpdate => _listenerMap.isNotEmpty;
static RxEasy? proxy;
void addListener(EasyXNotifier notifier) {
if (!_listenerMap.containsKey(notifier)) {
//变量监听中刷新
notifier.addListener(() {
//刷新ebx中添加的监听
easyXNotifier.notify();
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。