Tổng Hợp

Running Android tasks in background threads

All Android apps use a main thread to handle UI operations. Calling
long-running operations from this main thread can lead to freezes and
unresponsiveness. For example, if your app makes a network request from
the main thread, your app’s UI is frozen until it receives the network
response. You can create additional
background threads to handle long-running operations while the main
thread continues to handle UI updates.

Note:

If you’re writing your app in Kotlin, we strongly recommend coroutines as a lightweight solution for background tasks. Coroutines include features such as structured concurrency, built-in cancellation support, Jetpack integration, and more.

This guide shows both Kotlin and Java Programming Language developers
how to use a thread pool to set up and use multiple threads in an
Android app. This guide also shows you how to define code to run on
a thread and how to communicate between one of these threads and the
main thread.

Examples overview

Based on the Guide to app architecture, the examples
in this topic make a network request and return the result to the main
thread, where the app then might display that result on the screen.

Specifically, the ViewModel calls the repository layer on the main
thread to trigger the network request. The repository layer is in charge
of moving the execution of the network request off the main thread and
posting the result back to the main thread using a callback.

To move the execution of the network request off the main thread, we
need to create other threads in our app.

Creating multiple threads

A thread pool
is a managed collection of threads that runs tasks in parallel from a
queue. New tasks are executed on existing threads as those threads
become idle. To send a task to a thread pool, use the
ExecutorService
interface. Note that ExecutorService has nothing to do with
Services, the
Android application component.

Creating threads is expensive, so you should create a thread pool only
once as your app initializes. Be sure to save the instance of the
ExecutorService either
in your Application class or in a
dependency injection container.
The following example creates a thread pool of four threads that we can
use to run background tasks.

Kotlin

class MyApplication : Application() {
    val executorService: ExecutorService = Executors.newFixedThreadPool(4)
}

Java

public class MyApplication extends Application {
    ExecutorService executorService = Executors.newFixedThreadPool(4);
}

There are other ways you can configure a thread pool depending on
expected workload. See
Configuring a thread pool for more information.

Executing in a background thread

Making a network request on the main thread causes the thread to wait,
or block, until it receives a response. Since the thread is blocked,
the OS can’t call onDraw(), and your app freezes, potentially leading
to an Application Not Responding (ANR) dialog. Instead, let’s run this
operation on a background thread.

First, let’s take a look at our Repository class and see how it’s making
the network request:

Kotlin

sealed class Resultandlt;out Randgt; {
    data class Successandlt;out Tandgt;(val data: T) : Resultandlt;Tandgt;()
    data class Error(val exception: Exception) : Resultandlt;Nothingandgt;()
}

class LoginRepository(private val responseParser: LoginResponseParser) {
    private const val loginUrl = "https://example.com/login"

    // Function that makes the network request, blocking the current thread
    fun makeLoginRequest(
        jsonBody: String
    ): Resultandlt;LoginResponseandgt; {
        val url = URL(loginUrl)
        (url.openConnection() as? HttpURLConnection)?.run {
            requestMethod = "POST"
            setRequestProperty("Content-Type", "application/json; charset=utf-8")
            setRequestProperty("Accept", "application/json")
            doOutput = true
            outputStream.write(jsonBody.toByteArray())

            return Result.Success(responseParser.parse(inputStream))
        }
        return Result.Error(Exception("Cannot open HttpURLConnection"))
    }
}

Java

// Result.java
public abstract class Resultandlt;Tandgt; {
    private Result() {}

    public static final class Successandlt;Tandgt; extends Resultandlt;Tandgt; {
        public T data;

        public Success(T data) {
            this.data = data;
        }
    }

    public static final class Errorandlt;Tandgt; extends Resultandlt;Tandgt; {
        public Exception exception;

        public Error(Exception exception) {
            this.exception = exception;
        }
    }
}

// LoginRepository.java
public class LoginRepository {

    private final String loginUrl = "https://example.com/login";
    private final LoginResponseParser responseParser;

    public LoginRepository(LoginResponseParser responseParser) {
        this.responseParser = responseParser;
    }

    public Resultandlt;LoginResponseandgt; makeLoginRequest(String jsonBody) {
        try {
            URL url = new URL(loginUrl);
            HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
            httpConnection.setRequestMethod("POST");
            httpConnection.setRequestProperty("Content-Type", "application/json; charset=utf-8");
            httpConnection.setRequestProperty("Accept", "application/json");
            httpConnection.setDoOutput(true);
            httpConnection.getOutputStream().write(jsonBody.getBytes("utf-8"));

            LoginResponse loginResponse = responseParser.parse(httpConnection.getInputStream());
            return new Result.Successandlt;LoginResponseandgt;(loginResponse);
        } catch (Exception e) {
            return new Result.Errorandlt;LoginResponseandgt;(e);
        }
    }
}

makeLoginRequest() is synchronous and blocks the calling thread. To model
the response of the network request, we have our own Result class.

Xem Thêm :   Cách Lập Fanpage 2021 ❤️ 4 Bước Lập Trang FB Nhanh Chóng

Xem thêm :  Cách Cắt Ảnh Trong Photoshop Theo Ý Muốn Dễ Nhất [2020]

The ViewModel triggers the network request when the user taps,
for example, on a button:

Kotlin

class LoginViewModel(
    private val loginRepository: LoginRepository
) {
    fun makeLoginRequest(username: String, token: String) {
        val jsonBody = "{ username: "$username", token: "$token"}"
        loginRepository.makeLoginRequest(jsonBody)
    }
}

Java

public class LoginViewModel {

    private final LoginRepository loginRepository;

    public LoginViewModel(LoginRepository loginRepository) {
        this.loginRepository = loginRepository;
    }

    public void makeLoginRequest(String username, String token) {
        String jsonBody = "{ username: "" + username + "", token: "" + token + "" }";
        loginRepository.makeLoginRequest(jsonBody);
    }
}

With the previous code, LoginViewModel is blocking the main thread
when making the network request. We can use the thread pool that we’ve
instantiated to move the execution to a background thread. First,
following the principles of dependency injection,
LoginRepository takes an instance of
Executor as opposed to
ExecutorService because it’s executing code and not managing threads:

Kotlin

class LoginRepository(
    private val responseParser: LoginResponseParser
    private val executor: Executor
) { ... }

Java

public class LoginRepository {
    ...
    private final Executor executor;

    public LoginRepository(LoginResponseParser responseParser, Executor executor) {
        this.responseParser = responseParser;
        this.executor = executor;
    }
    ...
}

The Executor’s
execute()
method takes a
Runnable.
A Runnable is a Single Abstract Method (SAM) interface with a
run() method that is executed in a thread when invoked.

Let’s create another function called makeLoginRequest() that moves the
execution to the background thread and ignores the response for now:

Kotlin

class LoginRepository(
    private val responseParser: LoginResponseParser
    private val executor: Executor
) {

    fun makeLoginRequest(jsonBody: String) {
        executor.execute {
            val ignoredResponse = makeSynchronousLoginRequest(url, jsonBody)
        }
    }

    private fun makeSynchronousLoginRequest(
        jsonBody: String
    ): Resultandlt;LoginResponseandgt; {
        ... // HttpURLConnection logic
    }
}

Java

public class LoginRepository {
    ...
    public void makeLoginRequest(final String jsonBody) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Resultandlt;LoginResponseandgt; ignoredResponse = makeSynchronousLoginRequest(jsonBody);
            }
        });
    }

    public Resultandlt;LoginResponseandgt; makeSynchronousLoginRequest(String jsonBody) {
        ... // HttpURLConnection logic
    }
    ...
}

Inside the execute() method, we create a new Runnable with the
block of code we want to execute in the background thread—in our case,
the synchronous network request method. Internally, the ExecutorService
manages the Runnable and executes it in an available thread.

Note:

In Kotlin, you can use a lambda expression to create an anonymous
class that implements the SAM interface.

Considerations

Any thread in your app can run in parallel to other threads, including the
main thread, so you should ensure that your code is thread-safe. Notice that
in our example that we avoid writing to variables shared between threads,
passing immutable data instead. This is a good practice, because each thread
works with its own instance of data, and we avoid the complexity of
synchronization.

If you need to share state between threads, you must be
careful to manage access from threads using synchronization mechanisms
such as locks. This is outside of the scope of this guide. In general
you should avoid sharing mutable state between threads whenever possible.

Communicating with the main thread

In the previous step, we ignored the network request response. To display
the result on the screen, LoginViewModel needs to know about it. We can do
that by using callbacks.

The function makeLoginRequest() should take a callback as a parameter
so that it can return a value asynchronously. The callback with the
result is called whenever the network request completes or a failure occurs.
In Kotlin, we can use a higher-order function. However, in Java, we have to
create a new callback interface to have the same functionality:

Xem Thêm :   Kiến thức khởi nghiệp là gì ? Kiến thức cở bản cho người mới bắt đầu – Ý Tưởng Kinh Doanh

Xem thêm :  Cách Làm Sữa Chua Dẻo Tại Nhà Bằng Sữa Ông Thọ Ngon Như Nhà Làm

Kotlin

class LoginRepository(
    private val responseParser: LoginResponseParser
    private val executor: Executor
) {

    fun makeLoginRequest(
        jsonBody: String,
        callback: (Resultandlt;LoginResponseandgt;) -> Unit
    ) {
        executor.execute {
            try {
                val response = makeSynchronousLoginRequest(jsonBody)
                callback(response)
            } catch (e: Exception) {
                val errorResult = Result.Error(e)
                callback(errorResult)
            }
        }
    }
    ...
}

Java

interface RepositoryCallbackandlt;Tandgt; {
    void onComplete(Resultandlt;Tandgt; result);
}

public class LoginRepository {
    ...
    public void makeLoginRequest(
        final String jsonBody,
        final RepositoryCallbackandlt;LoginResponseandgt; callback
    ) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Resultandlt;LoginResponseandgt; result = makeSynchronousLoginRequest(jsonBody);
                    callback.onComplete(result);
                } catch (Exception e) {
                    Resultandlt;LoginResponseandgt; errorResult = new Result.Errorandlt;>(e);
                    callback.onComplete(errorResult);
                }
            }
        });
    }
  ...
}

The ViewModel needs to implement the callback now. It can perform different
logic depending on the result:

Kotlin

class LoginViewModel(
    private val loginRepository: LoginRepository
) {
    fun makeLoginRequest(username: String, token: String) {
        val jsonBody = "{ username: "$username", token: "$token"}"
        loginRepository.makeLoginRequest(jsonBody) { result ->
            when(result) {
                is Result.Successandlt;LoginResponseandgt; -> // Happy path
                else -> // Show error in UI
            }
        }
    }
}

Java

public class LoginViewModel {
    ...
    public void makeLoginRequest(String username, String token) {
        String jsonBody = "{ username: "" + username + "", token: "" + token + "" }";
        loginRepository.makeLoginRequest(jsonBody, new RepositoryCallbackandlt;LoginResponseandgt;() {
            @Override
            public void onComplete(Resultandlt;LoginResponseandgt; result) {
                if (result instanceof Result.Success) {
                    // Happy path
                } else {
                    // Show error in UI
                }
            }
        });
    }
}

In this example, the callback is executed in the calling thread, which is
a background thread. This means that you cannot modify or communicate
directly with the UI layer until you switch back to the main thread.

Note:View from the ViewModel layer, use
LiveData as recommended in the
MutableLiveData.postValue()
to communicate with the UI layer.

To communicate with thefrom thelayer, useas recommended in the Guide to app architecture . If the code is being executed on a background thread, you can callto communicate with the UI layer.

Using handlers

You can use a Handler to enqueue an
action to be performed on a different thread. To specify the thread on
which to run the action, construct the Handler using a
Looper for the thread. A Looper is an
object that runs the message loop for an associated thread. Once you’ve
created a Handler, you can then use the
post(Runnable)
method to run a block of code in the corresponding thread.

Looper includes a helper function,
getMainLooper(),
which retrieves the Looper of the main thread. You can run code in the
main thread by using this Looper to create a Handler. As this is
something you might do quite often, you can also save an instance of
the Handler in the same place you saved the ExecutorService:

Kotlin

class MyApplication : Application() {
    val executorService: ExecutorService = Executors.newFixedThreadPool(4)
    val mainThreadHandler: Handler = HandlerCompat.createAsync(Looper.getMainLooper())
}

Java

public class MyApplication extends Application {
    ExecutorService executorService = Executors.newFixedThreadPool(4);
    Handler mainThreadHandler = HandlerCompat.createAsync(Looper.getMainLooper());
}

It’s a good practice to inject the handler to the Repository, as it gives
you more flexibility. For example, in the future you might want to pass in
a different Handler to schedule tasks on a separate thread. If you’re
always communicating back to the same thread, you can pass the Handler
into the Repository constructor, as shown in the following example.

Kotlin

class LoginRepository(
    ...
    private val resultHandler: Handler
) {

    fun makeLoginRequest(
        jsonBody: String,
        callback: (Resultandlt;LoginResponseandgt;) -> Unit
    ) {
          executor.execute {
              try {
                  val response = makeSynchronousLoginRequest(jsonBody)
                  resultHandler.post { callback(response) }
              } catch (e: Exception) {
                  val errorResult = Result.Error(e)
                  resultHandler.post { callback(errorResult) }
              }
          }
    }
    ...
}

Java

public class LoginRepository {
    ...
    private final Handler resultHandler;

    public LoginRepository(LoginResponseParser responseParser, Executor executor,
            Handler resultHandler) {
        this.responseParser = responseParser;
        this.executor = executor;
        this.resultHandler = resultHandler;
    }

    public void makeLoginRequest(
        final String jsonBody,
        final RepositoryCallbackandlt;LoginResponseandgt; callback
    ) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Resultandlt;LoginResponseandgt; result = makeSynchronousLoginRequest(jsonBody);
                    notifyResult(result, callback);
                } catch (Exception e) {
                    Resultandlt;LoginResponseandgt; errorResult = new Result.Errorandlt;>(e);
                    notifyResult(errorResult, callback);
                }
            }
        });
    }

    private void notifyResult(
        final Resultandlt;LoginResponseandgt; result,
        final RepositoryCallbackandlt;LoginResponseandgt; callback,
    ) {
        resultHandler.post(new Runnable() {
            @Override
            public void run() {
                callback.onComplete(result);
            }
        });
    }
    ...
}

Alternatively, if you want more flexibility, you can pass in a Handler to
each function:

Xem Thêm :   Trọn bộ tài liệu Autocad 2D-3D miễn phí

Xem thêm :  Cách Bo Góc Trong Photoshop, Hướng Dẫn Bo Tròn 4 Góc Trong Photoshop

Kotlin

class LoginRepository(...) {
    ...
    fun makeLoginRequest(
        jsonBody: String,
        resultHandler: Handler,
        callback: (Resultandlt;LoginResponseandgt;) -> Unit
    ) {
        executor.execute {
            try {
                val response = makeSynchronousLoginRequest(jsonBody)
                resultHandler.post { callback(response) }
            } catch (e: Exception) {
                val errorResult = Result.Error(e)
                resultHandler.post { callback(errorResult) }
            }
        }
    }
}

Java

public class LoginRepository {
    ...

    public void makeLoginRequest(
        final String jsonBody,
        final RepositoryCallbackandlt;LoginResponseandgt; callback,
        final Handler resultHandler,
    ) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Resultandlt;LoginResponseandgt; result = makeSynchronousLoginRequest(jsonBody);
                    notifyResult(result, callback, resultHandler);
                } catch (Exception e) {
                    Resultandlt;LoginResponseandgt; errorResult = new Result.Errorandlt;>(e);
                    notifyResult(errorResult, callback, resultHandler);
                }
            }
        });
    }

    private void notifyResult(
        final Resultandlt;LoginResponseandgt; result,
        final RepositoryCallbackandlt;LoginResponseandgt; callback,
        final Handler resultHandler
    ) {
        resultHandler.post(new Runnable() {
            @Override
            public void run() {
                callback.onComplete(result);
            }
        });
    }
}

In this example, the callback passed into the Repository’s makeLoginRequest
call is executed on the main thread. That means you can directly modify the
UI from the callback or use LiveData.setValue() to communicate with the UI.

Configuring a thread pool

You can create a thread pool using one of the
Executor helper functions
with predefined settings, as shown in the previous example code.
Alternatively, if you want to customize the details of the thread
pool, you can create an instance using
ThreadPoolExecutor
directly. You can configure the following details:

  • Initial and maximum pool size.
  • Keep alive time and time unit. Keep alive time is the maximum duration
    that a thread can remain idle before it shuts down.
  • An input queue that holds Runnable tasks. This queue must implement the
    BlockingQueue interface.
    To match the requirements of your app, you can choose from the available
    queue implementations. To learn more, see the class overview for
    ThreadPoolExecutor.

Here’s an example that specifies thread pool size based on the total number
of processor cores, a keep alive time of one second, and an input queue.

Kotlin

class MyApplication : Application() {
    /*
     * Gets the number of available cores
     * (not always the same as the maximum number of cores)
     */
    private val NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors()

    // Instantiates the queue of Runnables as a LinkedBlockingQueue
    private val workQueue: BlockingQueueandlt;Runnableandgt; =
            LinkedBlockingQueueandlt;Runnableandgt;()

    // Sets the amount of time an idle thread waits before terminating
    private const val KEEP_ALIVE_TIME = 1L
    // Sets the Time Unit to seconds
    private val KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS
    // Creates a thread pool manager
    private val threadPoolExecutor: ThreadPoolExecutor = ThreadPoolExecutor(
            NUMBER_OF_CORES,       // Initial pool size
            NUMBER_OF_CORES,       // Max pool size
            KEEP_ALIVE_TIME,
            KEEP_ALIVE_TIME_UNIT,
            workQueue
    )
}

Java

public class MyApplication extends Application {
    /*
     * Gets the number of available cores
     * (not always the same as the maximum number of cores)
     */
    private static int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();

    // Instantiates the queue of Runnables as a LinkedBlockingQueue
    private final BlockingQueueandlt;Runnableandgt; workQueue = new LinkedBlockingQueueandlt;Runnableandgt;();

    // Sets the amount of time an idle thread waits before terminating
    private static final int KEEP_ALIVE_TIME = 1;
    // Sets the Time Unit to seconds
    private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;

    // Creates a thread pool manager
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            NUMBER_OF_CORES,       // Initial pool size
            NUMBER_OF_CORES,       // Max pool size
            KEEP_ALIVE_TIME,
            KEEP_ALIVE_TIME_UNIT,
            workQueue
    );
    ...
}

Concurrency libraries

It’s important to understand the basics of threading and its underlying
mechanisms. There are, however, many popular libraries that offer
higher-level abstractions over these concepts and ready-to-use utilities
for passing data between threads. These libraries include
Guava
and RxJava
for the Java Programming Language users and
coroutines, which we recommend for Kotlin users.

In practice, you should pick the one that works best for your app and
your development team, though the rules of threading remain the same.

See also

For more information about processes and threads in Android, see
Process and threads overview.

Xem thêm bài viết thuộc chuyên mục: Kĩ Năng Sống

Related Articles

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *

Back to top button