場景來源
這幾天寫 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);
}),
);
複製程式碼
如有錯誤或更好的方案,歡迎拍磚