Flutter:在进行大量数据库操作时避免 UI 冻结

Flutter: avoid UI freeze when massive Database operation is in progress

更新(2020 年 7 月 15 日)

mFeinstein's response, for now, is the only answer which gives me the first acceptable solution.


问题

我不得不问你什么是做我想做的事情的最佳方法:

  1. 以异步模式调用 Web 服务
  2. 解析响应
  3. 执行大量数据库操作

所有这些都没有冻结进度动画,就像不确定的进度条。

第一点和第二点没有问题。问题出现在第三个,当大量数据库插入正在进行时。而且我还不明白实施这些东西的正确方法是什么。

一些用于澄清的伪代码

UI(显示对话框并运行进度条...)

void callWS() async {
    MyProgressDialog _dialog = DialogHelper.showMyProgressDialog(_context, "Data", "Loading...");
    await getDataFromService();
    _dialog.close();
  }

CONNECTION(进度条不冻结)

   static Future<void> getDataFromService() async {
    String uri = MY_URI;
    String wsMethod = MY_WS_METHOD;
    String wsContract = MY_WS_CONTRACT;

    SoapObject myRequest = SoapObject.fromSoapObject(namespace: my_namespace, name: wsMethod);

    MyConnectionResult response = await _openMyConnection(myRequest, uri, wsContract, wsMethod);
    if (response.result == MyResultEnum.OK) {
      await _parseResponse(response.data);
    }
  }

DATABASE(冻结出现在进度条上)

  static Future<void> _parseResponse(xml.XmlElement elements) async {
    Database db = await MyDatabaseHelper.openConnection();
    db.transaction((tx) async {
      Batch batch = tx.batch();
      for (xml.XmlElement oi in elements.children) {
        int id = int.parse(oi.findElements("ID").first.text);
        String name = oi.findElements("NAME").first.text;

        DatabaseHelper.insertElement(
          tx,
          id: id,
          name: name,
        );
      }
      batch.commit(noResult: true);
    });
  }

无法替代

我也看到了“计算”函数方法,但是当我调用数据库操作时,sqflite package 中似乎存在问题。例如:

  static Future<void> performDelete() async {
    Database db = await openMyConnection();
    compute(_performDeleteCompute, db);
  }

  static void _performDeleteCompute(Database db) async {
    db.rawQuery("DELETE MYTABLE");
  }

Console error:'
-> Unhandled Exception: Exception: ServicesBinding.defaultBinaryMessenger was accessed before the binding was initialized. 
-> If you are running an application and need to access the binary messenger before runApp() has been called (for example, during plugin initialization),
then you need to explicitly call the WidgetsFlutterBinding.ensureInitialized() first.
-> error defaultBinaryMessenger.<anonymous closure> (package:flutter/src/services/binary_messenger.dart:76:7)
    #1      defaultBinaryMessenger (package:flutter/src/services/binary_messenger.dart:89:4)
    #2      MethodChannel.binaryMessenger (package:flutter/src/services/platform_channel.dart:140:62)
    #3      MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:146:35)
    #4      MethodChannel.invokeMethod (package:flutter/src/services/platform_channel.dart:329:12)
    #5      invokeMethod (package:sqflite/src/sqflite_impl.dart:17:13)
    #6      SqfliteDatabaseFactoryImpl.invokeMethod (package:sqflite/src/factory_impl.dart:31:7)
    #7      SqfliteDatabaseMixin.invokeMethod (package:sqflite_common/src/database_mixin.dart:287:15)
    #8      SqfliteDatabaseMixin.safeInvokeMethod.<anonymous closure> (package:sqflite_common/src/database_mixin.dart:208:43)
    #9      wrapDatabaseException (package:sqflite/src/exception_impl.dart:7:32)
    #10     SqfliteDatabaseFactoryImpl.wrapDatabaseException (package:sqflite/src/factory_impl.dart:27:7)
    #11     SqfliteDatabaseMixin.safeInvokeMethod (package:sqflite_common/src/database_mixin.dart:208:15)
    #12     SqfliteDatabaseMixin.txnRawQuery.<anonymous closure> (package:sqflite_common/src/database_mixin.dart:394:36)
    #13     SqfliteDatabaseMixin.txnSynchronized.<anonymous closure> (package:sqflite_common/src/database_mixin.dart:327:22)
    #14     BasicLock.synchronized (package:synchronized/src/basic_lock.dart:32:26)
    #15     SqfliteDatabaseMixin.txnSynchronized (package:sqflite_common/src/database_mixin.dart:323:33)
    #16     SqfliteDatabaseMixin.txnRawQuery (package:sqflite_common/src/database_mixin.dart:393:12)
    #17     SqfliteDatabaseExecutorMixin._rawQuery (package:sqflite_common/src/database_mixin.dart:126:15)
    #18     SqfliteDatabaseExecutorMixin.rawQuery (package:sqflite_common/src/database_mixin.dart:120:12)
    #19     DatabaseHelper._performDeleteCompute(package:flutter_infocad/Database/DatabaseHelper.dart:368:8)'

并且还明确调用 WidgetsFlutterBinding.ensureInitialized() 作为 runApp() 中的第一个,如错误日志中所建议的那样,没有任何反应。

隔离和计算有时无法与第 3 方库一起使用,您需要使用 flutter_isolate

FlutterIsolate allows creation of an Isolate in flutter that is able to use flutter plugins

问题是 Flutter 是单线程的,所以一旦你得到一个繁重的进程 运行ning,你的单线程就会阻塞其他任何东西。

解决方案是明智地使用单线程。

Dart 将有一个事件队列,其中有一堆 Futures 等待处理。一旦 Dart 引擎看到一个 await,它将让另一个 Future 抓住单线程并让它 运行。这样,一个 Future 将在 Isolate.

中一次 运行

所以如果我们聪明点,我们让每个人都在自己的时间玩,换句话说,我们将我们的任务分解成更小的任务,这样 Dart 引擎就不会饿死其他人 Futures,所有等待 运行ning 的进程都可以有时间了。

你的代码的等价物是这样的(假设 for 是需要大量时间执行的,因为有一个大集合,而不是单独的步骤):

static Future<void> _parseResponse(xml.XmlElement elements) async {
  Database db = await MyDatabaseHelper.openConnection();
  db.transaction((tx) async {
    Batch batch = tx.batch();
    for (xml.XmlElement oi in elements.children) {      
      await Future(() {
        int id = int.parse(oi.findElements("ID").first.text);
        String name = oi.findElements("NAME").first.text;

         DatabaseHelper.insertElement(
          tx,
          id: id,
          name: name,
         );
      );
    }

    batch.commit(noResult: true);
  });
}

这会将你的 for 循环的每一步分割成一个 Future,因此在每一步你的 UI 将有机会执行它需要执行的任何内容以保持你的动画流畅。请记住,这会产生减慢 _parseResponse 的副作用,因为将每个 for 步骤放入 Future 事件队列都会产生额外的成本,因此您可能需要优化它进一步针对您的特定用例。

此任务适用于本机 ios 和 android 代码,那里有真正的多线程。实现解析和插入应该不会花很长时间。