flutter邊緣實踐——非同步依賴(附JS粗製版解決方案)

HannibalKcc發表於2020-01-10

場景來源

這幾天寫 flutter 產品給了我一個新需求——在 app 開啟時檢查當前版本是否為最新版本,如果不是則彈窗提示更新。

初步嘗試

一開始想,這需求簡單啊,直接在 main.dart_MyAppState initState 中寫下 showDialogUpdate() 就完事了。

但是被無情打臉,由於專案使用了 ScreenUtil.getInstance().setWidth(w)(一個 flutter 的尺寸適配解決方案 API)幾乎所有的 Widget 都會使用它,而 ScreenUtil.getInstance().setWidth(w) 使用的先決條件是用 ScreenUtil.instance = ScreenUtil(width: 375, height: 667)..init(context) 初始化過一次,否則會丟擲異常。

showDialogUpdate() 會內構建 DialogUpadeWidget ,此時 ScreenUtil 還未被初始化,自然就來異常了。

應急解決

應急解決一下,寫一個計時器就完了,等待 ScreenUtil 初始化再構建 DialogUpadeWidget

分析場景,優化解決方案

寫一個計時器是不優雅且不可靠的。為了日後的維護與擴充套件,需要找到一種語義化、清晰、可靠的解決方案。

在這個場景下我們會發現 showDialogUpdate() 依賴於 ScreenUtil 的初始化(下文簡稱 ScreenUtilInit())。但是 showDialogUpdate()ScreenUtilInit() 寫在不同的檔案中,我們無法便捷知道 ScreenUtilInit()而且程式碼執行順序上 showDialogUpdate() 要優先於 ScreenUtilInit()

也就是說我們無法書寫以下程式碼

/// main.dart
import './FirstPage.dart';
FirstPage.futureScreenUtilInit().then(() => showDialogUpdate());

/// FirstPage.dart
FirstPageState {
  var futureScreenUtilInit;

  Widget build() {
   futureScreenUtilInit = new Future(() => ScreenUtilInit());
   // ...
  }
}
複製程式碼

js解決方案

由於身邊寫 flutter 的大佬不多,這個問題簡化描述後跟身邊的 js 開發者討論,在 js中有 Promise 可以輕鬆解決這個問題。附上初版程式碼 JS粗製解決方案

仔細觀察可以發現核心思路是暴露出 Prmose 例項化提供的 resolve 方法給外部使用

function createResolve() {
  let _resolve;
  return {
    promise: new Promise(resolve => {
      _resolve = resolve; // 核心1
    }),
    resolve: _resolve // 核心2
  };
}
複製程式碼

回到 dart

dart 中跟 js promise 類似的為 Future 但是使用上稍有不同, Future 通過返回值轉變自己的狀態為 success or error。沒有可以暴露出去的 resolve 方法。

仔細查詢發現 google 的開發者已經考慮到這一點了,不過不是 Future 而是隱藏的很深的 Completer,可以暴露的方法是 completer.complete

已在業務上使用的程式碼如下

import 'dart:async';

/// 初始化函式收集者(隨著不斷開發,進行查漏補缺)
/// 對於部分早期呼叫的函式,為了確保其相關依賴者可以正常執行,請讓依賴者等待它們。
/// 而被依賴者執行時請配合 delayRely.complete() 使用,詳情參考 [DelayRely]
class InitCollector {
  /// 初始化 Application.prefs 否則任何的 Application.prefs.get() 均返回 null
  static DelayRely initSharedPreferences = new DelayRely();

  /// 初始化 ScreenUtil ,所有的尺寸 $w() $h() 都依賴此函式
  static DelayRely initScreenUtilInstance = new DelayRely();

  /// 初始化 Application.currentRouteContext,所有的 $t 都依賴此值
  static DelayRely initCurrentRouteContext = new DelayRely();
}

typedef Complete = void Function([FutureOr value]);

/// 延遲依賴
/// # Examples
/// ``` dart
///  var res = new DelayRely();
///  res.future.then((val) => print('then val $val'));
///  res.future.then((val) => print('then2 val $val'));
///  res.future.then((val) => print('then3 val $val'));
///
///  res.complete(
///    new Future.delayed(
///      new Duration(seconds: 3),
///          () {
///        print('3s callback');
///        return 996;
///      },
///    ),
///  );
/// ```
class DelayRely {
  DelayRely() {
    _completer = new Completer();
    _complete = _completer.complete;
    _future = _completer.future;
  }

  Completer _completer;
  Complete _complete;
  Future _future;
  Future get future => _future;

  // 完成 _future 這樣 future.then 就可以開始進入任務佇列了
  void complete<T>(Future<T> future) {
    if (_completer.isCompleted) return;

    future.then((T val) => _complete(val));
  }
}
複製程式碼

您可以在 dartpad 驗證

擷取業務實踐的程式碼片段

/// main.dart
void showDialogUpdate({bool isNeedCheckLastReject = false}) async {
  await Future.wait([
    InitCollector.initSharedPreferences.future,
    InitCollector.initScreenUtilInstance.future,
    InitCollector.initCurrentRouteContext.future,
  ]);

  // ...
}

/// FirstPage.dart
InitCollector.initScreenUtilInstance.complete(
  Future.sync(() {
    Application.mediaQuery = MediaQuery.of(context);
    // 適配初始化
    ScreenUtil.instance = ScreenUtil(width: 375, height: 667)..init(context);
  }),
);
複製程式碼

如有錯誤或更好的方案,歡迎拍磚

相關文章