[Flutter]從Obx一個報錯初探其原理

傘菌發表於2021-05-10

引言

在Flutter開發圈內,隨著Get框架的認知度日漸提高,選擇嘗試它的小夥伴也越來越多。嘗試新框架時,被各種毒打也是家常便飯,但這也是保障其被正確使用所必不可少的。

2DE69AE6525237123B88215ED07CE0E8.jpg

Get提供了多種根據Rx變數構建Widget的小元件,如ObxGetXGetBuilder等。其中最簡潔的莫屬Obx,但是對於新手,十有八九遇到過這樣的錯誤:

The following message was thrown building Obx(dirty, state: _ObxState#777c8):
      [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.
複製程式碼

錯誤說明很明確,ObxGetX元件錯誤使用,因為檢測到在其中沒有使用observable變數。明白意思後在其中使用相應變數便可以順利解決錯誤。但是你是否想過Get是如何知道我們沒有使用observable變數的呢?

初探

class Obx extends ObxWidget {
  final WidgetCallback builder;

  const Obx(this.builder);

  @override
  Widget build() => builder();
}
複製程式碼

Obx的定義很簡單,它僅接受一個返回Widget的閉包作為引數。既然這裡看不出個所以,那麼就繼續分析他的父類:

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(() {});
    }
  }

  // ...

  Widget get notifyChilds {
    final observer = RxInterface.proxy; // 2
    RxInterface.proxy = _observer; // 3
    final result = widget.build();
    // 1
    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; // 4
    return result;
  }

  @override
  Widget build(BuildContext context) => notifyChilds;
}
複製程式碼

ObxWidget裡,我們馬上在1處看到了最終拋給我們的錯誤提示,看來可以在這裡找到我們想要的答案。ObxWidget繼承了StatefulWidget,但是和我們平時的使用方式不同,它在widget的部分也包含了一個build()方法,只是少接收一個BuildContext引數。2處儲存當前proxy,並在 4處還原, 3處切換為當前的RxNotifier,這一系列操作的作用我們稍後分析。

可以發現由於檢查到了_observer!.canUpdatefalse才丟擲的錯誤,所以Get一定是通過它檢測我們有沒有使用observable變數,其命名canUpdate也印證了我們的猜測(沒有可觀測的變數,自然也就沒法響應式更新)。由於Obx是為了響應式更新而專門設計的,而我們的使用方式違背了設計初衷,所以此處丟擲錯誤。

繼續檢視canUpdate的定義,這裡我們可能會被IDE帶偏到RxInterface的定義,它是一個抽象類,被所有Reactive類所繼承,並沒有具體實現。回看上一步程式碼,我們發現_observer其實是一個RxNotifier

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;

  /// This is an internal method.
  /// Subscribe to changes on the inner stream.
  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);
    }
  }

  // ...
}
複製程式碼

可以發現,canUpdate最終是由NotifyManager實現的,canUpdate檢查的是當前例項中流的訂閱個數是否為0,即是否監聽了observable變數。繼續觀察,只發現addListner能夠向_subscriptions中新增新的entry,那麼誰使用了這個方法呢?直接使用IDE搜尋Usage(IDEA系使用Alt/Option+F7)沒有搜尋到相關程式碼,只能由我們自己猜測了。

換一條思路,回到最初的問題,Get檢測了我們在元件內是否使用了observable變數,是否是使用本身呼叫了addListener呢?

當我們使用Get提供的擴充方法.obs建立observable變數時,其實是建立了一個Rx<T>變數,Rx變數是Get響應式元件的核心,Get借鑑了RxDart使用了自己的一套實現。沿著某一種Rx變數(如RxString),我們一路向上追蹤,RxString -> Rx -> _RxImpl -> RxObjectMixin,最終可以發現如下程式碼:

mixin RxObjectMixin<T> on NotifyManager<T> {
  // ...
  T get value {
    if (RxInterface.proxy != null) {
      RxInterface.proxy!.addListener(subject);
    }
    return _value;
  }
  // ...
}
複製程式碼

這裡就是每次使用時會呼叫的程式碼,正如我們預料的,在這裡變數被記錄到了_subscriptions中。

回顧下正確使用的整個build流程:

  1. 建立Obx,儲存當前proxy,切換當前proxy為此Obx持有的RxNotifier

  2. 使用了.value方法,使用proxy記錄下使用記錄,並listen這個變數。

  3. build時檢查監測到有正在watch的變數,通過檢查。

  4. 還原第一步儲存的proxy

瞭解了build的全貌,我們可以猜測出1、4步這一系列操作保證了遞迴build過程中正確的proxy記錄正確的變數。

常見錯誤案例

final controller = Get.put(HomeController());

@override
Widget build(BuildContext){
  final goods = controller.goods.value;
  return Obx(() => Text(goods.name));
}
複製程式碼

瞭解了原理後我們可以清楚知道這樣的程式碼問題點:在訪問value時,Obx尚未建立,也沒有對應的RxNotifier可供記錄和監聽用,於是便會丟擲文章開始的錯誤。

簡單修改:

final controller = Get.put(HomeController());

@override
Widget build(BuildContext){
  return Obx(() => Text(controller.goods.value.name));
}
複製程式碼

新人第一次投稿,如有錯誤請指正

相關文章