Flutter Platform Channels - Invoke channel method on android, hangs the ui

3,116

Solution 1

I had the same Issue and fixed it with a MethodCallWrapper in TesseractOcrPlugin.java

This Code works for me (no Dart-code change is needed):

package io.paratoner.tesseract_ocr;

import com.googlecode.tesseract.android.TessBaseAPI;

import android.os.Handler;
import android.os.Looper;
import android.os.AsyncTask;

import java.io.File;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.PluginRegistry.Registrar;

/** TesseractOcrPlugin */
public class TesseractOcrPlugin implements MethodCallHandler {

  private static final int DEFAULT_PAGE_SEG_MODE = TessBaseAPI.PageSegMode.PSM_SINGLE_BLOCK;

  /** Plugin registration. */
  public static void registerWith(Registrar registrar) {
    final MethodChannel channel = new MethodChannel(registrar.messenger(), "tesseract_ocr");
    channel.setMethodCallHandler(new TesseractOcrPlugin());
  }

  // MethodChannel.Result wrapper that responds on the platform thread.
  private static class MethodResultWrapper implements Result {
    private Result methodResult;
    private Handler handler;

    MethodResultWrapper(Result result) {
      methodResult = result;
      handler = new Handler(Looper.getMainLooper());
    }

    @Override
    public void success(final Object result) {
      handler.post(new Runnable() {
        @Override
        public void run() {
          methodResult.success(result);
        }
      });
    }

    @Override
    public void error(final String errorCode, final String errorMessage, final Object errorDetails) {
      handler.post(new Runnable() {
        @Override
        public void run() {
          methodResult.error(errorCode, errorMessage, errorDetails);
        }
      });
    }

    @Override
    public void notImplemented() {
      handler.post(new Runnable() {
        @Override
        public void run() {
          methodResult.notImplemented();
        }
      });
    }
  }

  @Override
  public void onMethodCall(MethodCall call, Result rawResult) {

    Result result = new MethodResultWrapper(rawResult);

    if (call.method.equals("extractText")) {
      final String tessDataPath = call.argument("tessData");
      final String imagePath = call.argument("imagePath");
      String DEFAULT_LANGUAGE = "eng";
      if (call.argument("language") != null) {
        DEFAULT_LANGUAGE = call.argument("language");
      }
      calculateResult(tessDataPath, imagePath, DEFAULT_LANGUAGE, result);
    } else {
      result.notImplemented();
    }
  }

  private void calculateResult(final String tessDataPath, final String imagePath, final String language,
      final Result result) {

    new AsyncTask<Void, Void, Void>() {
      @Override
      protected Void doInBackground(Void... params) {
        final String[] recognizedText = new String[1];
        final TessBaseAPI baseApi = new TessBaseAPI();
        baseApi.init(tessDataPath, language);
        final File tempFile = new File(imagePath);
        baseApi.setPageSegMode(DEFAULT_PAGE_SEG_MODE);
        baseApi.setImage(tempFile);
        recognizedText[0] = baseApi.getUTF8Text();
        baseApi.end();
        result.success(recognizedText[0]);
        return null;
      }

      @Override
      protected void onPostExecute(Void result) {
        super.onPostExecute(result);
      }
    }.execute();
  }

}

Solution 2

By using join you're making the main thread wait for the background thread, blocking it. You have to remove the join and return a result immediately.

So, how do you return the ocr result, which won't be available immediately. When it becomes available, you then call a method from native to dart, passing the result. At the dart end, you then handle the result as any async event.

The point of the last paragraph of your question is that your result will become available on your background thread, so you'd want to call the native to dart method there. You can't. You have to post the method call code to the main looper - you already show some code for posting to the main looper which you can use as an example.

Share:
3,116
Elia Weiss
Author by

Elia Weiss

Updated on December 17, 2022

Comments

  • Elia Weiss
    Elia Weiss over 1 year

    I'm trying to use Tesseract in flutter using the following package https://github.com/arrrrny/tesseract_ocr

    I've download the app and run in.

    The problem is that the extractText hangs the UI.

    Looking at the Java code:

      Thread t = new Thread(new Runnable() {
        public void run() {
          baseApi.setImage(tempFile);
          recognizedText[0] = baseApi.getUTF8Text();
          baseApi.end();
        }
      });
      t.start();
      try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); }
      result.success(recognizedText[0]);
    

    I can see that it is running on a new thread, so I expect it not to hang the app, but it still does.

    I found this example:

                new Handler(Looper.getMainLooper()).post(new Runnable() {
                    @Override
                    public void run() {
                        // Call the desired channel message here.
                        baseApi.setImage(tempFile);
                        recognizedText[0] = baseApi.getHOCRText(0);
                        baseApi.end();
                        result.success(recognizedText[0]);
    
                    }
                });
    

    from https://flutter.dev/docs/development/platform-integration/platform-channels#channels-and-platform-threading

    but it also hangs the UI.

    The docs also say

    **Channels and Platform Threading**
    Invoke all channel methods on the platform’s main thread when writing code on the platform side.
    

    Can someone clarify this sentence?

    According to Richard Heap answer, I tried to call a method from native to dart, passing the result:

    Dart side:

    _channel.setMethodCallHandler((call) {
      print(call);
      switch (call.method) {
        case "extractTextResult":
          final String result = call.arguments;
          print(result);
      }
      var t;
      return t;
    });
    

    Java side:

    channel.invokeMethod("extractTextResult","hello");

    if I call this method from the main thread, this works fine, but then the thread is blocking.

    If I do

                Thread t = new Thread(new Runnable() {
                    public void run() {
                        channel.invokeMethod("extractTextResult","test1231231");
    
                    }
                });
                t.start();
    
                result.success("tst"); // return immediately
    

    Then the app crashes with the following message:

    enter image description here

    I also tried:

               Thread t = new Thread(new Runnable() {
                    public void run() {
                        new Handler(Looper.getMainLooper()).post(new Runnable() {
                            @Override
                            public void run() {
                                // Call the desired channel message here.
                                baseApi.setImage(tempFile);
                                recognizedText[0] = baseApi.getHOCRText(0);
                                baseApi.end();
                                result.success(recognizedText[0]);
                                //                                channel.invokeMethod("extractTextResult", "test1231231");
                            }
                        });
    
                    }
                });
                t.start();
    
                result.success("tst");
    

    which is what I understand that Richard Heap last comment meant, but It still hangs the ui.