Semua aplikasi Android menggunakan thread utama untuk menangani operasi UI. Memanggil operasi yang berjalan lama dari thread utama ini dapat menyebabkan aplikasi berhenti berfungsi dan tidak responsif. Misalnya, jika aplikasi Anda membuat permintaan jaringan dari thread utama, UI aplikasi akan dibekukan hingga menerima respons jaringan. Jika menggunakan Java, Anda dapat membuat thread latar belakang tambahan untuk menangani operasi yang berjalan lama saat thread utama terus menangani update UI.
Panduan ini menunjukkan cara developer yang menggunakan Bahasa Pemrograman Java dapat menggunakan kumpulan thread untuk menyiapkan dan menggunakan beberapa thread di aplikasi Android. Panduan ini juga menunjukkan cara menentukan kode yang akan dijalankan di thread dan cara berkomunikasi antara salah satu thread ini dan thread utama.
Library serentak
Penting untuk memahami dasar-dasar threading dan mekanisme yang mendasarinya. Namun, ada banyak library populer yang menawarkan abstraksi tingkat lebih tinggi atas konsep ini dan utilitas yang siap digunakan untuk meneruskan data antar-thread. Library ini mencakup Guava dan RxJava untuk pengguna Bahasa Pemrograman Java dan Coroutine, yang kami rekomendasikan bagi pengguna Kotlin.
Dalam praktiknya, Anda harus memilih salah satu yang paling sesuai untuk aplikasi dan tim pengembangan, meskipun aturan threading tetap sama.
Ringkasan contoh
Berdasarkan Panduan arsitektur aplikasi, contoh dalam topik ini membuat permintaan jaringan dan menampilkan hasilnya ke thread utama, tempat aplikasi kemudian mungkin menampilkan hasilnya di layar.
Secara khusus, ViewModel
memanggil lapisan data pada thread utama untuk
memicu permintaan jaringan. Lapisan data bertanggung jawab memindahkan
eksekusi permintaan jaringan dari thread utama dan memposting hasilnya kembali
ke thread utama menggunakan callback.
Untuk memindahkan eksekusi permintaan jaringan dari thread utama, kita perlu membuat thread lain di aplikasi.
Membuat beberapa thread
Kumpulan thread adalah kumpulan thread terkelola yang menjalankan tugas secara paralel dari antrean. Tugas baru akan dijalankan pada thread yang ada saat thread
tersebut tidak ada aktivitas. Untuk mengirim tugas ke kumpulan thread, gunakan
antarmuka ExecutorService
. Perlu diperhatikan bahwa ExecutorService
tidak ada hubungannya
dengan Layanan, komponen aplikasi Android.
Membuat thread itu mahal, jadi Anda harus membuat kumpulan thread hanya sekali saat
aplikasi melakukan inisialisasi. Pastikan Anda menyimpan instance ExecutorService
di class Application
atau dalam penampung injeksi dependensi.
Contoh berikut membuat kumpulan thread dari empat thread yang dapat kita gunakan untuk
menjalankan tugas latar belakang.
public class MyApplication extends Application {
ExecutorService executorService = Executors.newFixedThreadPool(4);
}
Ada cara lain untuk mengonfigurasi kumpulan thread, bergantung pada beban kerja yang diharapkan. Lihat Mengonfigurasi kumpulan thread untuk informasi selengkapnya.
Menjalankan di thread latar belakang
Membuat permintaan jaringan di thread utama akan menyebabkan thread menunggu, atau
memblokir, hingga thread tersebut menerima respons. Karena thread diblokir, OS tidak dapat
memanggil onDraw()
, dan aplikasi Anda berhenti berfungsi, yang berpotensi menyebabkan dialog Aplikasi
Tidak Merespons (ANR). Sebagai gantinya, mari kita jalankan operasi ini di thread
latar belakang.
Membuat Permintaan
Pertama, mari kita lihat class LoginRepository
dan lihat caranya membuat
permintaan jaringan:
// Result.java
public abstract class Result<T> {
private Result() {}
public static final class Success<T> extends Result<T> {
public T data;
public Success(T data) {
this.data = data;
}
}
public static final class Error<T> extends Result<T> {
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 Result<LoginResponse> 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.Success<LoginResponse>(loginResponse);
} catch (Exception e) {
return new Result.Error<LoginResponse>(e);
}
}
}
makeLoginRequest()
bersifat sinkron dan memblokir thread pemanggil. Untuk membuat model respons permintaan jaringan, kita memiliki class Result
sendiri.
Memicu permintaan
ViewModel
memicu permintaan jaringan saat pengguna mengetuk, misalnya, sebuah tombol:
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);
}
}
Dengan kode sebelumnya, LoginViewModel
memblokir thread utama saat membuat
permintaan jaringan. Kita dapat menggunakan kumpulan thread yang telah dibuat instance-nya untuk memindahkan
eksekusi ke thread latar belakang.
Menangani injeksi dependensi
Pertama, mengikuti prinsip injeksi dependensi, LoginRepository
mengambil instance Executor, bukan ExecutorService
, karena
mengeksekusi kode dan tidak mengelola thread:
public class LoginRepository {
...
private final Executor executor;
public LoginRepository(LoginResponseParser responseParser, Executor executor) {
this.responseParser = responseParser;
this.executor = executor;
}
...
}
Metode execute() Executor mengambil Runnable. Runnable
adalah
antarmuka Single Abstract Method (SAM) dengan metode run()
yang dieksekusi di
thread saat dipanggil.
Menjalankan di latar belakang
Mari kita buat fungsi lain bernama makeLoginRequest()
, yang memindahkan
eksekusi ke thread latar belakang dan mengabaikan respons untuk saat ini:
public class LoginRepository {
...
public void makeLoginRequest(final String jsonBody) {
executor.execute(new Runnable() {
@Override
public void run() {
Result<LoginResponse> ignoredResponse = makeSynchronousLoginRequest(jsonBody);
}
});
}
public Result<LoginResponse> makeSynchronousLoginRequest(String jsonBody) {
... // HttpURLConnection logic
}
...
}
Dalam metode execute()
, kita membuat Runnable
baru dengan blok kode
yang ingin kita jalankan di thread latar belakang—dalam kasus kita, metode permintaan
jaringan sinkron. Secara internal, ExecutorService
mengelola Runnable
dan
mengeksekusinya dalam thread yang tersedia.
Pertimbangan
Setiap thread dalam aplikasi dapat berjalan secara paralel dengan thread lain, termasuk thread utama, jadi Anda harus memastikan bahwa kode Anda aman untuk thread. Perhatikan bahwa dalam contoh ini, kita menghindari penulisan ke variabel yang dibagikan antar-thread, dan meneruskan data yang tidak dapat diubah. Ini adalah praktik yang baik, karena setiap thread berfungsi dengan instance datanya sendiri, dan kita menghindari kompleksitas sinkronisasi.
Jika perlu berbagi status antar-thread, Anda harus berhati-hati dalam mengelola akses dari thread menggunakan mekanisme sinkronisasi seperti kunci. Hal ini berada di luar cakupan panduan ini. Secara umum, Anda harus menghindari berbagi status yang dapat diubah antar-thread jika memungkinkan.
Berkomunikasi dengan thread utama
Dalam langkah sebelumnya, kita mengabaikan respons permintaan jaringan. Untuk menampilkan
hasil di layar, LoginViewModel
perlu mengetahuinya. Kita dapat melakukannya menggunakan
callback.
Fungsi makeLoginRequest()
harus mengambil callback sebagai parameter agar dapat menampilkan nilai secara asinkron. Callback dengan hasilnya akan dipanggil setiap kali permintaan jaringan selesai atau terjadi kegagalan. Di Kotlin, kita dapat
menggunakan fungsi tingkat tinggi. Namun, dalam Java, kita harus membuat antarmuka callback baru agar memiliki fungsionalitas yang sama:
interface RepositoryCallback<T> {
void onComplete(Result<T> result);
}
public class LoginRepository {
...
public void makeLoginRequest(
final String jsonBody,
final RepositoryCallback<LoginResponse> callback
) {
executor.execute(new Runnable() {
@Override
public void run() {
try {
Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
callback.onComplete(result);
} catch (Exception e) {
Result<LoginResponse> errorResult = new Result.Error<>(e);
callback.onComplete(errorResult);
}
}
});
}
...
}
ViewModel
perlu mengimplementasikan callback sekarang. Sistem ini dapat menjalankan logika
yang berbeda, bergantung pada hasilnya:
public class LoginViewModel {
...
public void makeLoginRequest(String username, String token) {
String jsonBody = "{ username: \"" + username + "\", token: \"" + token + "\" }";
loginRepository.makeLoginRequest(jsonBody, new RepositoryCallback<LoginResponse>() {
@Override
public void onComplete(Result<LoginResponse> result) {
if (result instanceof Result.Success) {
// Happy path
} else {
// Show error in UI
}
}
});
}
}
Dalam contoh ini, callback dieksekusi di thread panggilan, yang merupakan thread latar belakang. Artinya, Anda tidak dapat memodifikasi atau berkomunikasi langsung dengan lapisan UI hingga Anda beralih kembali ke thread utama.
Menggunakan pengendali
Anda dapat menggunakan Handler untuk mengantrekan tindakan agar dilakukan pada thread lain. Untuk menentukan thread tempat menjalankan tindakan, buat
Handler
menggunakan Looper untuk thread. Looper
adalah objek yang menjalankan loop pesan untuk thread terkait. Setelah membuat Handler
, Anda
kemudian dapat menggunakan metode post(Runnable) untuk menjalankan blok kode di
thread yang sesuai.
Looper
menyertakan fungsi bantuan, getMainLooper(), yang mengambil Looper
thread utama. Anda dapat menjalankan kode di thread utama dengan menggunakan
Looper
ini untuk membuat Handler
. Karena ini mungkin cukup sering Anda lakukan,
Anda juga dapat menyimpan instance Handler
di tempat yang sama dengan tempat Anda menyimpan
ExecutorService
:
public class MyApplication extends Application {
ExecutorService executorService = Executors.newFixedThreadPool(4);
Handler mainThreadHandler = HandlerCompat.createAsync(Looper.getMainLooper());
}
Sebaiknya masukkan pengendali ke dalam repositori karena akan memberikan
lebih banyak fleksibilitas. Misalnya, di masa mendatang, Anda mungkin ingin meneruskan
Handler
yang berbeda untuk menjadwalkan tugas di thread terpisah. Jika selalu
berkomunikasi kembali ke thread yang sama, Anda dapat meneruskan Handler
ke
konstruktor repositori, seperti yang ditunjukkan pada contoh berikut.
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 RepositoryCallback<LoginResponse> callback
) {
executor.execute(new Runnable() {
@Override
public void run() {
try {
Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
notifyResult(result, callback);
} catch (Exception e) {
Result<LoginResponse> errorResult = new Result.Error<>(e);
notifyResult(errorResult, callback);
}
}
});
}
private void notifyResult(
final Result<LoginResponse> result,
final RepositoryCallback<LoginResponse> callback,
) {
resultHandler.post(new Runnable() {
@Override
public void run() {
callback.onComplete(result);
}
});
}
...
}
Atau, jika menginginkan fleksibilitas lebih besar, Anda dapat meneruskan Handler
ke setiap fungsi:
public class LoginRepository {
...
public void makeLoginRequest(
final String jsonBody,
final RepositoryCallback<LoginResponse> callback,
final Handler resultHandler,
) {
executor.execute(new Runnable() {
@Override
public void run() {
try {
Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
notifyResult(result, callback, resultHandler);
} catch (Exception e) {
Result<LoginResponse> errorResult = new Result.Error<>(e);
notifyResult(errorResult, callback, resultHandler);
}
}
});
}
private void notifyResult(
final Result<LoginResponse> result,
final RepositoryCallback<LoginResponse> callback,
final Handler resultHandler
) {
resultHandler.post(new Runnable() {
@Override
public void run() {
callback.onComplete(result);
}
});
}
}
Dalam contoh ini, callback yang diteruskan ke panggilan makeLoginRequest
Repositori akan dieksekusi di thread utama. Artinya, Anda dapat langsung memodifikasi UI
dari callback atau menggunakan LiveData.setValue()
untuk berkomunikasi dengan UI.
Mengonfigurasi kumpulan thread
Anda dapat membuat kumpulan thread menggunakan salah satu fungsi bantuan Executor dengan setelan yang telah ditentukan, seperti ditunjukkan dalam kode contoh sebelumnya. Atau, jika ingin menyesuaikan detail kumpulan thread, Anda dapat membuat instance menggunakan ThreadPoolExecutor secara langsung. Anda dapat mengonfigurasi detail berikut:
- Ukuran kumpulan awal dan maksimum.
- Waktu keep alive dan satuan waktu. Waktu keep alive adalah durasi maksimum thread yang dapat tetap tidak ada aktivitas sebelum dinonaktifkan.
- Antrean input yang berisi tugas
Runnable
. Antrean ini harus menerapkan antarmuka BlockingQueue. Agar cocok dengan persyaratan aplikasi, Anda dapat memilih dari penerapan antrean yang tersedia. Untuk mempelajari lebih lanjut, lihat ringkasan class untuk ThreadPoolExecutor.
Berikut adalah contoh yang menentukan ukuran kumpulan thread berdasarkan jumlah total core prosesor, waktu keep alive satu detik, dan antrean input.
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 BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>();
// 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
);
...
}