Pages

Sunday, October 3, 2021

Migrating Async Tasks - Android 11

Background:
AsyncTask is deprecated from Android 11. Google has recommened to use 
Executors and Handlers to achieve the background computation and UI updates. 

Challenge:
For larger enterprise applications/projects (with more AsyncTasks), 
replacing the asynctask with Executors/Handlers, could be an uphill task.  
It would be even more challenging, if the existing async task does lot 
of complex background computations and more UI interactions and the 
author/SME is not around. 

Migrating them would require more code changes, more testing and 
it involves more risk of breaking the functionality. 

If your project is still in JAVA, please continue reading here. 
For Kotlin, you might want to leverage the benefits of coroutines, 
which is not explained here.

Solution:
Create a base class that exposes similar AsyncTask API but internally 
it uses executors and handlers. For migration, instead of extending 
AsyncTask, just extend the new base class.

GitHub:
https://github.com/sudhans/ExecutorAsyncTask

Limitations/Differences:
The above snippet is not strict like the async task in throwing 
exceptions if the execute() method is called after the asynctask is complete.

Implement a ThreadFactory, if you would like to give the custom 
thread names for the executor.


Code Snippet:
import android.os.Handler;
import android.os.Looper;
import android.util.Log;

import androidx.annotation.AnyThread;
import androidx.annotation.MainThread;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;

public abstract class AsyncTaskV2<Params, Progress, Result> {

    public enum Status {
        FINISHED,
        PENDING,
        RUNNING
    }

    // This handler will be used to communicate with main thread
    private final Handler handler = new Handler(Looper.getMainLooper());
    private final AtomicBoolean cancelled = new AtomicBoolean(false);

    private Result result;
    private Future<Result> resultFuture;
    private ExecutorService executor;
    private Status status = Status.PENDING;

    // Base class must implement this method
     protected abstract Result doInBackground(Params params);

    // Methods with default implementation
    // Base class can optionally override these methods.
    protected void onPreExecute() {}
    protected void onPostExecute(Result result) {}
    protected void onProgressUpdate(Progress progress) {}
    protected void onCancelled() {}
    protected void onCancelled(Result result) {
        onCancelled();
    }


    @MainThread
    public final Future<Result> execute(@Nullable Params params) {
        status = Status.RUNNING;
        onPreExecute();
        try {
            executor = Executors.newSingleThreadExecutor();
            Callable<Result> backgroundCallableTask = () ->  doInBackground(params);
            // Execute the background task
            resultFuture = executor.submit(backgroundCallableTask);
            
            // On the worker thread - wait for the background task to complete
            executor.submit(this::getResult);
            return resultFuture;
        } finally {
            if (executor != null) {
                executor.shutdown();
            }
        }
    }

    private Runnable getResult() {
        return () -> {
            try {
                if (!isCancelled()) {
                    // This will block the worker thread, till the result is available
                    result = resultFuture.get();
                    
                    // Post the result to main thread
                    handler.post(() -> onPostExecute(result));
                } else {
                    // User cancelled the operation, ignore the result
                    handler.post(this::onCancelled);
                }
                status = Status.FINISHED;
            } catch (InterruptedException | ExecutionException e) {
                Log.e("Exception while trying to get result ", e.getMessage());
            }
        };
    }

    @WorkerThread
    public final void publishProgress(Progress progress) {
        if (!isCancelled()) {
            handler.post(() -> onProgressUpdate(progress));
        }
    }

    @MainThread
    public final void cancel(boolean mayInterruptIfRunning) {
        cancelled.set(true);
        if (resultFuture!= null) {
            resultFuture.cancel(mayInterruptIfRunning);
        }
    }

    @AnyThread
    public final boolean isCancelled() {
        return cancelled.get();
    }

    @AnyThread
    public final Status getStatus() {
        return status;
    }

}