Search code examples
androidmultithreadingandroid-asynctaskandroid-viewmodelandroid-camerax

Android inter thread communication best practices with ViewModel and CameraX?


I have the following scenario (view code below):

  1. Get an ImageProxy from the loop of the ImageAnalyzer use case of CameraX
  2. Analyze the image (takes about 20 ms) and return some information, close() the ImageProxy
  3. Analyze the extracted information via AsyncTask (takes about 500 to 3000 ms)
  4. If successful, show results

I have some fragments which are dependent on how successful Task 1. and 2. are (in particular a PreviewView fragment and some result page). Currently they get notified by the ViewModel. When the image gets analyzed by either Task 1. or 2. I set a state with viewModel.analyzerState.postValue(<newState>). This is of course not ideal, since I need to listen to that state in my Tasks as well in case the previous image produced a result that I can show to the user.

My problem is now the following:
When a result is ready, Task 1. or 2. might still be running and produce another result which will then trigger the UI to update twice. I do not want that, but I still want the Tasks to run in parallel in case the previous task failed (which happens quite often). When I'm sure I have a result, I want the Tasks cancelled (I'm already calling cancel() on the AsyncTask, but that's not working). I think my problem is using the ViewModel as a messaging tool for inter thread communication, since I set the state in Task 1. as well as in Task 2. and listen to it in the loop.

The only information I could find was using Kotlin coroutines which apparently are made for working with the ViewModel, but I am using Java.

What is the standard approach for this kind of problem? How do I communicate between the various threads and fragments safely?

Code:

void analyzeImage(ImageProxy imageProxy) {    // loop called by CameraX
    if (viewModel.analyzerState.getValue() == 'done') {
        imageProxy.close();
        return;
    }

    viewModel.analyzerState.postValue('extracting');

    Data data = extractData(imageProxy);

    imageProxy.close();

    if (data.isValid()) {
        viewModel.analyzerState.postValue('analyzing');
        new DataAnalyzeTask.execute(data);
    } else {
        viewModel.analyzerState.postValue('idle');
    }
}

// UI listens with
viewModel.analyzerState.observe(getViewLifecycleOwner(), state -> { /*...*/ } );

Solution

  • It took me some time and a lot of changes, but now I have a solution that is working for my purposes. So instead of using AsyncTask and the ViewModel for communication, I implemented my own Task class which is basically just a Runnable with some callbacks. These callbacks allow me to communicate with the UI. The UI then propagates data changes with the ViewModel (on the main thread). To ensure I'm on the main thread, I execute the callbacks in the main thread handler:

    Handler mainThreadHandler = HandlerCompat.createAsync(Looper.getMainLooper());
    
    // in Task class:
    mainThreadHandler.post(new Runnable() {
        @Override
        public void run() {
            callback.onSomethingHappend(result);
        }
    });
    

    My custom Tasks are executed in a thread pool via an ExecutorService:

    ExecutorService executor = Executors.newFixedThreadPool(4);   // should be called only once when the application launches
    
    executor.execute(new Task(data, callback));
    

    I think there might be a simpler solution using ListenableFuture (since I more or less reimplemented those with my Task and TaskCallback class), but I did not want to add Guava to my project for now.

    To ensure I can cancel my tasks, I used AtomicBoolean and set the value to false once I had some results:

    atomicBoolean.compareAndSet(/*expectedValue=*/false, /*newValue=*/true);
    

    Here are some useful resources:

    https://developer.android.com/guide/background/asynchronous/java-threads https://developer.android.com/guide/background/asynchronous/listenablefuture