Semua aplikasi Android menggunakan thread utama untuk menangani operasi UI. Memanggil operasi yang berjalan lama dari thread utama ini dapat menyebabkannya berhenti berfungsi dan tidak responsif. Misalnya, jika aplikasi Anda membuat permintaan jaringan dari thread utama, UI aplikasi akan berhenti berfungsi hingga aplikasi itu menerima respons jaringan. Anda dapat membuat thread latar belakang tambahan untuk menangani operasi yang berjalan lama saat thread utama terus menangani update UI.
Panduan ini menunjukkan kepada developer Bahasa Pemrograman Kotlin dan Java tentang cara 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 dengan salah satu thread ini dan thread utama.
Ringkasan contoh
Berdasarkan Panduan arsitektur aplikasi, contoh dalam topik ini membuat permintaan jaringan dan menampilkan hasilnya ke thread utama, tempat aplikasi kemudian dapat menampilkan hasilnya di layar.
Secara khusus, ViewModel
memanggil lapisan repositori pada thread
utama untuk memicu permintaan jaringan. Lapisan repositori bertanggung jawab memindahkan eksekusi permintaan jaringan dari thread utama dan
memposting hasilnya kembali ke thread utama menggunakan callback.
Untuk mengeluarkan 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 dijalankan pada thread yang ada ketika thread tersebut
tidak ada aktivitas. Untuk mengirim tugas ke kumpulan thread, gunakan
antarmuka
ExecutorService
. Perhatikan bahwa ExecutorService
tidak ada hubungannya dengan
Layanan,
komponen aplikasi Android.
Membuat thread itu mahal, jadi Anda harus membuat kumpulan thread
sekali saja ketika aplikasi Anda melakukan inisialisasi. Pastikan Anda menyimpan instance
ExecutorService
di
class Application
atau di
container injeksi dependensi.
Contoh berikut membuat kumpulan thread dari empat thread yang dapat
kita gunakan untuk menjalankan tugas latar belakang.
Kotlin
class MyApplication : Application() { val executorService: ExecutorService = Executors.newFixedThreadPool(4) }
Java
public class MyApplication extends Application { ExecutorService executorService = Executors.newFixedThreadPool(4); }
Ada cara lain yang dapat Anda gunakan untuk mengonfigurasi kumpulan thread, bergantung pada beban kerja yang diharapkan. Lihat Mengonfigurasi kumpulan thread untuk informasi selengkapnya.
Menjalankan eksekusi pada thread latar belakang
Membuat permintaan jaringan di thread utama akan menyebabkan thread menunggu,
atau memblokir, hingga thread itu menerima respons. Karena thread diblokir,
OS tidak dapat memanggil onDraw()
, dan aplikasi Anda berhenti berfungsi, dan berpotensi memunculkan
dialog Aplikasi Tidak Merespons (ANR). Sebaiknya, kita jalankan operasi
ini di thread latar belakang.
Pertama, mari kita lihat class Repository
dan lihat caranya membuat
permintaan jaringan:
Kotlin
sealed class Result<out R> { data class Success<out T>(val data: T) : Result<T>() data class Error(val exception: Exception) : Result<Nothing>() } 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 ): Result<LoginResponse> { 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 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 mencontohkan
respons permintaan jaringan, kita memiliki class Result
sendiri.
ViewModel
memicu permintaan jaringan saat pengguna mengetuk,
misalnya, sebuah tombol:
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); } }
Dengan kode sebelumnya, LoginViewModel
memblokir thread utama
saat membuat permintaan jaringan. Kita bisa menggunakan kumpulan thread yang telah kita
buat instance untuk memindahkan eksekusi ke thread latar belakang. Pertama,
mengikuti prinsip injeksi dependensi,
LoginRepository
mengambil instance
Executor
dan bukan
ExecutorService
karena itu bukan kode eksekusi dan tidak mengelola thread:
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; } ... }
Metode execute()
Executor
mengambil
Runnable
.
Runnable
adalah antarmuka Metode Abstrak Tunggal (SAM) dengan
metode run()
yang dieksekusi dalam thread saat dipanggil.
Mari kita buat fungsi lain bernama makeLoginRequest()
, yang memindahkan
eksekusi ke thread latar belakang dan mengabaikan respons untuk saat ini:
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 ): Result<LoginResponse> { ... // HttpURLConnection logic } }
Java
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 eksekusi 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 di aplikasi Anda dapat dijalankan secara paralel dengan thread lain, termasuk thread utama, jadi Anda harus memastikan kode Anda aman bagi thread. Perhatikan bahwa dalam contoh ini, kita menghindari penulisan ke variabel yang dibagikan antar-thread, tetapi meneruskan data yang permanen. Ini praktik yang baik, karena setiap thread berfungsi dengan instance datanya sendiri, dan kita menghindari kompleksitas sinkronisasi.
Jika Anda perlu berbagi status antar-thread, Anda harus berhati-hati dalam mengelola akses dari thread menggunakan mekanisme sinkronisasi seperti penguncian. Hal ini tidak termasuk dalam cakupan panduan ini. Secara umum, Anda harus menghindari berbagi status yang dapat diubah antara thread jika memungkinkan.
Berkomunikasi dengan thread utama
Dalam langkah sebelumnya, kita mengabaikan respons permintaan jaringan. Untuk menampilkan
hasil di layar, LoginViewModel
perlu mengetahuinya. Kita bisa melakukannya
dengan 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 urutan yang lebih tinggi. Namun, di Java, kita harus
membuat antarmuka callback baru agar memiliki fungsionalitas yang sama:
Kotlin
class LoginRepository( private val responseParser: LoginResponseParser private val executor: Executor ) { fun makeLoginRequest( jsonBody: String, callback: (Result<LoginResponse>) -> Unit ) { executor.execute { try { val response = makeSynchronousLoginRequest(jsonBody) callback(response) } catch (e: Exception) { val errorResult = Result.Error(e) callback(errorResult) } } } ... }
Java
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:
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.Success<LoginResponse> -> // 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 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 dalam thread panggilan, yang merupakan thread latar belakang. Ini berarti Anda tidak dapat memodifikasi atau berkomunikasi langsung dengan lapisan UI sampai Anda beralih kembali ke thread utama.
Menggunakan handler
Anda dapat menggunakan Handler
untuk mengantrekan
tindakan agar dilakukan pada thread lainnya. Untuk menentukan thread
tempat menjalankan tindakan, buat Handler
menggunakan
Looper
untuk thread itu. Looper
adalah
objek yang menjalankan loop pesan untuk thread terkait. Setelah
membuat Handler
, Anda dapat menggunakan
metode post(Runnable)
untuk menjalankan blok kode di thread yang sesuai.
Looper
mencakup fungsi helper,
getMainLooper()
,
yang mengambil Looper
thread utama. Anda dapat menjalankan kode di
thread utama menggunakan Looper
ini untuk membuat Handler
. Karena ini
sesuatu yang mungkin sering Anda lakukan, Anda juga dapat menyimpan instance
Handler
di tempat yang sama dengan tempat Anda menyimpan 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()); }
Sangat disarankan untuk menginjeksikan handler ke Repository
, karena memberikan
fleksibilitas yang lebih besar. Misalnya, di masa mendatang Anda mungkin ingin memasukkan
Handler
yang berbeda untuk menjadwalkan tugas di thread terpisah. Jika Anda
selalu berkomunikasi kembali ke thread yang sama, Anda dapat memasukkan Handler
ke dalam konstruktor Repository
, seperti ditunjukkan dalam contoh berikut.
Kotlin
class LoginRepository( ... private val resultHandler: Handler ) { fun makeLoginRequest( jsonBody: String, callback: (Result<LoginResponse>) -> 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 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); } }); } ... }
Selain itu, jika Anda ingin lebih fleksibel, Anda dapat memasukkan Handler ke setiap fungsi:
Kotlin
class LoginRepository(...) { ... fun makeLoginRequest( jsonBody: String, resultHandler: Handler, callback: (Result<LoginResponse>) -> 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 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. Ini berarti Anda dapat memodifikasi langsung
UI dari callback atau menggunakan LiveData.setValue()
untuk berkomunikasi dengan UI.
Mengonfigurasi kumpulan thread
Anda dapat membuat kumpulan thread menggunakan salah satu
fungsi helper Executor
dengan setelan yang telah ditentukan, seperti yang ditunjukkan dalam contoh kode sebelumnya.
Selain itu, jika Anda 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 ketika thread tetap tidak ada aktivitas sebelum dinonaktifkan.
- Antrean input yang berisi tugas
Runnable
. Antrean ini harus mengimplementasikan antarmukaBlockingQueue
. Agar cocok dengan persyaratan aplikasi, Anda dapat memilih dari implementasi antrean yang tersedia. Untuk mempelajari lebih lanjut, lihat ringkasan class untukThreadPoolExecutor
.
Berikut adalah contoh yang menentukan ukuran kumpulan thread berdasarkan jumlah total core prosesor, waktu keep alive satu detik, dan antrean input.
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: BlockingQueue<Runnable> = LinkedBlockingQueue<Runnable>() // 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 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 ); ... }
Library serentak
Penting untuk memahami dasar-dasar threading dan mekanisme yang mendasarinya. Namun, ada banyak library populer yang menawarkan abstraksi tingkat tinggi terhadap konsep ini dan utilitas yang siap pakai untuk memasukkan data antara thread. Library ini mencakup Guava dan RxJava untuk pengguna Bahasa Pemrograman Java dan coroutine, yang kami sarankan untuk pengguna Kotlin.
Dalam praktiknya, Anda harus memilih salah satu yang paling cocok untuk aplikasi dan tim pengembangan Anda, meskipun aturan threading tetap sama.
Lihat juga
Untuk informasi selengkapnya tentang proses dan thread di Android, lihat Ringkasan proses dan thread.