flutter边缘实践——实现异步依赖并控制反转IoC(附JS粗制版解决方案)

720 阅读3分钟

场景来源

这几天写 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);
  }),
);

如有错误或更好的方案,欢迎拍砖