Mutable —— 教你写一个轻量级GetX

1,219 阅读5分钟

Mutable是上篇文章MoLc组件中的一个类,由于和其他组件没有耦合度,所以单拎出来和大家分享一下。
个人并不没有使用过GetX,但社区关于GetX的文章太多了,一不小心就看了一两个。有一篇文章的评论说道,GetX的精髓在于dart单线程特性结合一个proxy的静态变量实现了控制反转。利用这点,我们来复刻一个GetX。

要做到如同GetX一样的响应式刷新,即赋值即刷新,首先需要设计一个GetX中Rx类型和.obs

响应式数据

先上代码:

class Mutable<T> {
  late Queue<T> _data;

  T get value {
    return _data.last;
  }

  set value(T value) {
    if (value == _data.last) return;
    _data.add(value);
  }

  Mutable(T value) : _data = Queue.from([value]);
}

GetX有一个特性是值不变的话不会刷新,这里就想到肯定需要进行对比新老数据的过程,于是存放新老数据的数据结构选用了Queue,然后在set value时加入队列,在get value时取最新的last。这样就实现了一个基本的数据结构。

基本类型转换

这里以String为例:

extension MutableBoolExt on String {
  Mutable<String> get mt => Mutable(this);
}

为String提供一个.mt扩展函数就可以将String转化为Mutable<String>。像这样:

final abc = 'abcText'.mt;

但此时你没办法用abs调用String的函数和操作符, 还需要将String源码复制出来写成Mutable<String>的扩展。

extension MutableStringFunExt on Mutable<String?> {
  String operator +(String val) => (value ?? '') + val;

  bool? endsWith(String other) {
    return value?.endsWith(other);
  }

  bool? startsWith(Pattern pattern, [int index = 0]) {
    return value?.startsWith(pattern, index);
  }

  bool? get isEmpty => value?.isEmpty;

  bool? get isNotEmpty => value?.isNotEmpty;
  ...
}

代理刷新

然后给Mutable数据加上刷新来实现控制反转。也就是实现GetX中的Obx()

abstract class RefreshDelegate {
  static MapEntry<int, RefreshCallback>? _delegate;

  Map<int, RefreshCallback>? _refreshMap;
}

先解释一下上面的两个变量, _delegate是用来临时存放widget的刷新函数,_refreshMap是用来存放所有用到该Mutable值的Widget的刷新函数。看下面代码就清楚了:

class MutableWidget extends StatefulWidget {
  final WidgetBuilder builder;

  const MutableWidget(this.builder);

  @override
  State<StatefulWidget> createState() => _MutableState();
}

class _MutableState extends State<MutableWidget> {
  bool refresh() {
    if (mounted) {
      setState(() {});
    }
    return mounted;
  }

  @override
  Widget build(BuildContext context) {
    RefreshDelegate._delegate = MapEntry(hashCode, refresh);
    return widget.builder(context);
  }
}

MutableWidget是用来包裹Mutable,也就是GetX中的Obx。

Mutable继承RefreshDelegate, 并在get中将静态变量_delegate存到刷新map里

class Mutable<T> extends RefreshDelegate {
  ...

  T get value {
    final entry = RefreshDelegate._delegate!;
    _refreshMap ??= SplayTreeMap();
    _refreshMap![entry.key] = entry.value;
    return _data.last;
  }

  set value(T value) {
    if (value == _data.last) return;
    if (_data.length == _MUTABLE_SIZE) {
      _data.removeFirst();
    }
    _data.add(value);

    final removeSet = Set();
    _refreshMap?.forEach((k, v) {
      if (!v.call()) removeSet.add(k);
    });
    removeSet.forEach((e) {
      _refreshMap?.remove(e);
    });
  }

  ...
}

所以整个流程就是,MutableWidgetbuild的时候,会先将它自身的刷新函数赋值给静态变量_delegate。接下来build中用到的Mutable值会在get时将_delegate也就是刷新函数存入刷新函数Map中。最后在给Mutable赋值也就是set的时候,在刷新map中取出所有的刷新函数并刷新。如果刷新失败,也就是MutableWidget已经被dispose,那么在刷新map中移除该刷新。

使用

大功告成之后我们看下基本使用:

class ExampleWidget extends StatelessWidget {
  final abc = 100.mt;

  @override
  Widget build(BuildContext context) {
    return MutableWidget(
      (context) => Row(
        children: [
          Text('${abc.value}'),
          TextButton(
            onPressed: () {
              abc.value += 1;
            },
            child: Text('+1'),
          ),
        ],
      ),
    );
  }
}

和GetX的.obsObx()类似,关注点几乎只有.mtMutableWidget()MutableWidget内依赖Mutable值的小组件都是响应式刷新的,支持MutableWidget嵌套,也支持一个Mutable值被多个MutableWidget引用。

源码

下面是开箱即用的源码,只有一个类,如果你只是想要实现颗粒度刷新和响应式刷新,可以直接拿到项目中使用,而不必依赖臃肿的GetX。

import 'dart:collection';

import 'package:flutter/widgets.dart';


typedef RefreshCallback = bool Function();

const int _MUTABLE_SIZE = 3;

class Mutable<T> extends RefreshDelegate {
  late Queue<T> _data;

  T get value {
    final entry = RefreshDelegate._delegate!;
    _refreshMap ??= SplayTreeMap();
    _refreshMap![entry.key] = entry.value;
    return _data.last;
  }

  set value(T value) {
    if (value == _data.last) return;
    if (_data.length == _MUTABLE_SIZE) {
      _data.removeFirst();
    }
    _data.add(value);

    final removeSet = Set();
    _refreshMap?.forEach((k, v) {
      if (!v.call()) removeSet.add(k);
    });
    removeSet.forEach((e) {
      _refreshMap?.remove(e);
    });
  }

  Mutable(T value) : _data = Queue.from([value]);

  @override
  String toString() {
    return value.toString();
  }
}

class MutableWidget extends StatefulWidget {
  final WidgetBuilder builder;

  const MutableWidget(this.builder);

  @override
  State<StatefulWidget> createState() => _MutableState();
}

class _MutableState extends State<MutableWidget> {
  bool refresh() {
    if (mounted) {
      setState(() {});
    }
    return mounted;
  }

  @override
  Widget build(BuildContext context) {
    RefreshDelegate._delegate = MapEntry(hashCode, refresh);
    return widget.builder(context);
  }
}

abstract class RefreshDelegate {
  static MapEntry<int, RefreshCallback>? _delegate;

  Map<int, RefreshCallback>? _refreshMap;
}

extension MutableStringExt on String {
  Mutable<String> get mt => Mutable(this);
}

extension MutableBoolExt on bool {
  Mutable<bool> get mt => Mutable(this);
}

extension MutableIntExt on int {
  Mutable<int> get mt => Mutable(this);
}

extension MutableDoubleExt on double {
  Mutable<double> get mt => Mutable(this);
}

extension MutableNumExt on num {
  Mutable<num> get mt => Mutable(this);
}

extension MutableListExtension<T> on List<T> {
  Mutable<List<T>> get mt => Mutable(this);
}

extension MutableT<T> on T {
  Mutable<T> get mt => Mutable<T>(this);
}

extension MutableStringFunExt on Mutable<String?> {
  String operator +(String val) => (value ?? '') + val;

  int? compareTo(String other) {
    return value?.compareTo(other);
  }

  bool? endsWith(String other) {
    return value?.endsWith(other);
  }

  bool? startsWith(Pattern pattern, [int index = 0]) {
    return value?.startsWith(pattern, index);
  }

  int? indexOf(Pattern pattern, [int start = 0]) {
    return value?.indexOf(pattern, start);
  }

  int? lastIndexOf(Pattern pattern, [int? start]) {
    return value?.lastIndexOf(pattern, start);
  }

  bool? get isEmpty => value?.isEmpty;

  bool? get isNotEmpty => value?.isNotEmpty;

  String? substring(int startIndex, [int? endIndex]) {
    return value?.substring(startIndex, endIndex);
  }

  String? trim() {
    return value?.trim();
  }

  String? trimLeft() {
    return value?.trimLeft();
  }

  String? trimRight() {
    return value?.trimRight();
  }

  String? padLeft(int width, [String padding = ' ']) {
    return value?.padLeft(width, padding);
  }

  String? padRight(int width, [String padding = ' ']) {
    return value?.padRight(width, padding);
  }

  bool? contains(Pattern other, [int startIndex = 0]) {
    return value?.contains(other, startIndex);
  }

  String? replaceAll(Pattern from, String replace) {
    return value?.replaceAll(from, replace);
  }

  List<String>? split(Pattern pattern) {
    return value?.split(pattern);
  }

  List<int>? get codeUnits => value?.codeUnits;

  Runes? get runes => value?.runes;

  String? toLowerCase() {
    return value?.toLowerCase();
  }

  String? toUpperCase() {
    return value?.toUpperCase();
  }

  Iterable<Match>? allMatches(String string, [int start = 0]) {
    return value?.allMatches(string, start);
  }

  Match? matchAsPrefix(String string, [int start = 0]) {
    return value?.matchAsPrefix(string, start);
  }
}

extension MutableBoolFunExt on Mutable<bool?> {
  bool get isTrue => value ?? false;

  bool get isFalse => !isTrue;

  bool operator &(bool other) => other && (value ?? false);

  bool operator |(bool other) => other || (value ?? false);

  bool operator ^(bool other) => !other == value;

  Mutable<bool?> toggle() {
    if (value != null) {
      value = !value!;
    }
    return this;
  }
}

extension MutableNumFunExt<T extends num> on Mutable<T?> {
  num? operator *(num other) {
    if (value != null) {
      return value! * other;
    }
  }

  num? operator %(num other) {
    if (value != null) {
      return value! % other;
    }
  }

  double? operator /(num other) {
    if (value != null) {
      return value! / other;
    }
  }

  int? operator ~/(num other) {
    if (value != null) {
      return value! ~/ other;
    }
  }

  num? operator -() {
    if (value != null) {
      return -value!;
    }
  }

  num? remainder(num other) => value?.remainder(other);

  bool? operator <(num other) {
    if (value != null) {
      return value! < other;
    }
  }

  bool? operator <=(num other) {
    if (value != null) {
      return value! <= other;
    }
  }

  bool? operator >(num other) {
    if (value != null) {
      return value! > other;
    }
  }

  bool? operator >=(num other) {
    if (value != null) {
      return value! >= other;
    }
  }

  bool? get isNaN => value?.isNaN;

  bool? get isNegative => value?.isNegative;

  bool? get isInfinite => value?.isInfinite;

  bool? get isFinite => value?.isFinite;

  num? abs() => value?.abs();

  num? get sign => value?.sign;

  int? round() => value?.round();

  int? floor() => value?.floor();

  int? ceil() => value?.ceil();

  int? truncate() => value?.truncate();

  double? roundToDouble() => value?.roundToDouble();

  double? floorToDouble() => value?.floorToDouble();

  double? ceilToDouble() => value?.ceilToDouble();

  double? truncateToDouble() => value?.truncateToDouble();

  num? clamp(num lowerLimit, num upperLimit) =>
      value?.clamp(lowerLimit, upperLimit);

  int? toInt() => value?.toInt();

  double? toDouble() => value?.toDouble();

  String? toStringAsFixed(int fractionDigits) =>
      value?.toStringAsFixed(fractionDigits);

  String? toStringAsExponential([int? fractionDigits]) =>
      value?.toStringAsExponential(fractionDigits);

  String? toStringAsPrecision(int precision) =>
      value?.toStringAsPrecision(precision);
}