Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Correct way to communicate the result of a background thread to the Ui Thread in Android

This is one of the most confusing topics for me. So my question is, what is the correct way of communicate the result of background thread when this finish?.

Imagine I want to update some TextView with some information I just downloaded.There is 3 things I use when I need to perform background tasks:

AsyncTask

Very easy to use, this one has the onPostExecute() method that will return the result directly to the UiThread so I can use a callback interface or do whatever I want. I liked this class but it's deprecated.

ThreadPoolExecutor

This is what I actually use when need to perform background tasks and here comes my problem, the moment when I have to give the result to the UiThread. I have informed myself about Looper and Handler classes and about the mainLooper.

So, when I need to return some results I use the method runOnUiThread() that, as I have readed, just get the Looper of the Ui thread and post my Runnable to the queue.

Well this is working and I can communicate with the main thread but, I find it really ugly, and I am sure there is a more elegant way of doing it than populate all my code of "runOnUiThread()" methods. Also, if the background task need too much time, maybe the user already change of Activity or Fragment when the code inside runOnUiThread() runs what will cause Exceptions (I know using LiveData and MVVM pattern would solve this last problem but I am working in a legacy project and I can't refactor all the code so I am working with the clasical Activity mvc pattern)

So, there is another way of doing this? Could you give an example? I really searched a lot but didn't find anything...

Coroutines

I am actually working in a legacy project and I must use Java so can't use Kotlin coroutines, but I find them easy to use and so powerfull.

Any help would be appreciated!

like image 969
Bogdan Android Avatar asked Nov 25 '20 08:11

Bogdan Android


People also ask

How would you update the UI from the background thread?

In this case, to update the UI from a background thread, you can create a handler attached to the UI thread, and then post an action as a Runnable : Handler handler = new Handler(Looper. getMainLooper()); handler. post(new Runnable() { @Override public void run() { // update the ui from here } });

What is UI thread and background thread?

A UI thread is an example of a foreground thread. The difference between background and foreground threads is pretty simple. Background threads don't stop a process from terminating, but foreground threads do. When the last foreground thread stops, then all the background threads are also stopped and the process ends.


1 Answers

Background

In Android, when an application is launched, the system creates a thread of execution for the application, called main thread (also known as the UI thread). Google introduces the main thread and its responsible as below.

The main thread has a very simple design: Its only job is to take and execute blocks of work from a thread-safe work queue until its app is terminated. The framework generates some of these blocks of work from a variety of places. These places include callbacks associated with lifecycle information, user events such as input, or events coming from other apps and processes. In addition, app can explicitly enqueue blocks on their own, without using the framework.

Nearly any block of code your app executes is tied to an event callback, such as input, layout inflation, or draw. When something triggers an event, the thread where the event happened pushes the event out of itself, and into the main thread’s message queue. The main thread can then service the event.

While an animation or screen update is occurring, the system tries to execute a block of work (which is responsible for drawing the screen) every 16ms or so, in order to render smoothly at 60 frames per second. For the system to reach this goal, the UI/View hierarchy must update on the main thread. However, when the main thread’s messaging queue contains tasks that are either too numerous or too long for the main thread to complete the update fast enough, the app should move this work to a worker thread. If the main thread cannot finish executing blocks of work within 16ms, the user may observe hitching, lagging, or a lack of UI responsiveness to input. If the main thread blocks for approximately five seconds, the system displays the Application Not Responding (ANR) dialog, allowing the user to close the app directly.

To update a View, you must do it on the main thread, if you try to update in a background thread, the system will throw CalledFromWrongThreadException.

How to update a View on the main thread from a background thread?

The main thread has a Looper and a MessageQueue assigned with it. To update a View, we need to create a task then put it to the MessageQueue. To do that Android provides Handler API which allows us to send a task to the main thread's MessageQueue for executing later.

// Create a handler that associated with Looper of the main thread
Handler mainHandler = new Handler(Looper.getMainLooper());

// Send a task to the MessageQueue of the main thread
mainHandler.post(new Runnable() {
    @Override
    public void run() {
        // Code will be executed on the main thread
    }
});

To help developers easy to communicate with the main thread from a background thread, Android offers several methods:

  • Activity.runOnUiThread(Runnable)

  • View.post(Runnable)

  • View.postDelayed(Runnable, long)

Under the hood, they use Handler API to do their jobs.

Back to your question

AsyncTask

This is a class that is designed to be a helper class around Thread and Handler. It's responsible for:

  • Create a thread or pool of thread to do a task in the background

  • Create a Handler that associated with the main thread to send a task to the main thread's MessageQueue.

  • It is deprecated from API level 30

ThreadPoolExecutor

Create and handle a thread in Java is sometimes hard and might lead to a lot of bugs if developers do not handle it correctly. Java offers the ThreadPoolExecutor to create and manage threads more efficiently.

This API does not provide any method to update the UI.

Kotlin Coroutines

Coroutines is a solution for asynchronous programming on Android to simplify code that executes asynchronously. But it only available for Kotlin.

So my question is, what is the correct way of communicate the result of background thread when this finish?.

1. Using Handler or mechanism built on Handler

1.1. If a thread is bounded with Activity/Fragment:

  • Activity.runOnUiThread(Runnable)

1.2. If a thread has a reference to a view, such as Adapter class.

  • View.post(Runnable)

  • View.postDelayed(Runnable, long)

1.3. If a thread does not bound to any UI element, then create a Handler on your own.

Handler mainHandler = new Handler(Looper.getMainLooper);

Note: A benefit of using Handler is you can use it to do 2 ways communication between threads. It means from a background thread you can send a task to the main thread's MessageQueue and from the main thread, you can send a task to the background's MessageQueue.

2. Using BroadcastReceiver

This API is designed to allow Android apps can send and receive broadcast messages from the Android system, other apps or components (Activity, Service, etc) inside the app, similar to publish-subscribe design partern.

Because of the BroadcastReceiver.onReceive(Context, Intent) method is called within the main thread by default. So you can use it to update the UI on the main thread. For example.

Send data from a background thread.

// Send result from a background thread to the main thread
Intent intent = new Intent("ACTION_UPDATE_TEXT_VIEW");
intent.putExtra("text", "This is a test from a background thread");
getApplicationContext().sendBroadcast(intent);

Receive data from activity/fragment

// Create a broadcast to receive message from the background thread
private BroadcastReceiver updateTextViewReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        String text = intent.getStringExtra("text");
        myTextView.setText(text);
    }
};

@Override
protected void onStart() {
    super.onStart();
    // Start receiving the message
    registerReceiver(updateTextViewReceiver, new IntentFilter("ACTION_UPDATE_TEXT_VIEW"));
}

@Override
protected void onStop() {
    // Stop receving the message
    unregisterReceiver(updateTextViewReceiver);
    super.onStop();
}

This method is usually used to communicate between Android apps or Android apps with the system. Actually, you can use it to communicate between components in Android app, such as (Activity, Fragment, Service, Thread, etc.), but it requires a lot of code.

If you want a similar solution but less code, easy to use, then you can use the following method.

3. Using EventBus

EventBus is a publish/subscribe event bus for Android and Java. If you want to execute a method that runs on the main thread, just mark it with @Subscribe(threadMode = ThreadMode.MAIN) annotation.

// Step 1. Define events
public class UpdateViewEvent {
    private String text;
    
    public UpdateViewEvent(String text) {
        this.text = text;
    }

    public String getText() {
        return text;
    }
}

// Step 2. Prepare subscriber, usually inside activity/fragment
@Subscribe(threadMode = ThreadMode.MAIN)  
public void onMessageEvent(MessageEvent event) {
    myTextView.setText = event.getText();
};

// Step 3. Register subscriber
@Override
public void onStart() {
    super.onStart();
    EventBus.getDefault().register(this);
}

// Step 4. Unregister subscriber
@Override
public void onStop() {
    super.onStop();
    EventBus.getDefault().unregister(this);
}

// Step 5. Post events from a background thread
UpdateViewEvent event = new UpdateViewEvent("new name");
EventBus.getDefault().post(event);

This is useful when you want to update a View when the activity/fragment is visible to users (they are interacting with your app).

like image 151
Son Truong Avatar answered Sep 29 '22 13:09

Son Truong