Menjalankan tugas Android di thread latar belakang

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 antarmuka BlockingQueue. Agar cocok dengan persyaratan aplikasi, Anda dapat memilih dari implementasi 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.

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.