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

Issue

UPDATE (15 july 2020)

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


QUESTION

I have to ask you what is the best approach for doing what i’m trying to do:

  1. Calling a web service in async mode
  2. Parsing response
  3. Performing massive database operations

All of this without freezing progress animation, like indeterminate progress bar.

There isn’t problem at first and second point. Problem occurs at third, when a massive database insert is in act. And i don’t understand yet how is the right way for implementing this stuff.

Some pseudo piece of code for clarify

UI (Dialog is shown and progress bar runs…)

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

CONNECTION (Freeze doesn’t occur on progress bar)

   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 (Freeze occurs on progress bar)

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

NOT WORKING ALTERNATIVE

I saw "compute" function approach too, but it seems there is a problem in sqflite package, when i call a db operation. For example:

  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)'

And also explicitly calling the WidgetsFlutterBinding.ensureInitialized() as first in runApp(), as suggested in the error log, nothing happens.

Solution

The problem is that Flutter is Single Threaded, so once you get a heavy process running, your Single Thread will block anything else.

The solution is to be smart on how to use that Single Thread.

Dart will have an Event Queue with a bunch of Futures waiting to be processed. Once the Dart engine sees an await it will let another Future grab hold of the Single Thread and let it run. This way, one Future will run at a time inside an Isolate.

So if we get smart about it, we let everyone play at it’s own time, in other words, we break down our tasks into smaller tasks, so the Dart engine won’t starve other Futures, and all the processes awaiting for running can have their time.

The equivalent for your code would be something like this (assuming the for is what takes lots of time to execute, because of a large collection, and not it’s individual steps):

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

This will fragment each step of your for loop into a Future, so at each step your UI will have the opportunity to execute whatever it needs to execute to keep your animations smooth. Keep in mind though that this will have the side effect of slowing down _parseResponse as putting each for step into the Future Event Queue will have an additional cost, so you might want to optimize this further for your particular use case.

Answered By – Michel Feinstein

Answer Checked By – Clifford M. (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published.