WorkManager dan Pengujian Lanjutan

1. Pengantar

Di codelab Pekerjaan Latar Belakang dengan WorkManager, Anda telah mempelajari cara mengeksekusi pekerjaan di latar belakang (bukan di thread utama) menggunakan WorkManager. Dalam codelab ini, Anda akan terus mempelajari fungsi WorkManager untuk memastikan pekerjaan unik, memberi tag pekerjaan, membatalkan pekerjaan, dan batasan kerja. Codelab akan selesai setelah Anda mempelajari cara menulis pengujian otomatis untuk memverifikasi bahwa pekerja Anda berfungsi dengan benar dan menampilkan hasil yang diharapkan. Anda juga akan mempelajari cara menggunakan Background Task Inspector, yang disediakan oleh Android Studio untuk memeriksa pekerja dalam antrean.

Yang akan Anda build

Dalam codelab ini, Anda akan memastikan pekerjaan unik, memberi tag pekerjaan, membatalkan pekerjaan, dan menerapkan batasan pekerjaan. Kemudian, Anda akan mempelajari cara menulis pengujian UI otomatis untuk aplikasi Blur-O-Matic yang memverifikasi fungsi tiga pekerja yang dibuat di codelab Pekerjaan Latar Belakang dengan WorkManager:

  • BlurWorker
  • CleanupWorker
  • SaveImageToFileWorker

Yang akan Anda pelajari

  • Memastikan pekerjaan unik.
  • Cara membatalkan pekerjaan.
  • Cara menentukan batasan pekerjaan.
  • Cara menulis pengujian otomatis untuk memverifikasi fungsi Pekerja.
  • Dasar-dasar memeriksa pekerja dalam antrean dengan Background Task Inspector.

Yang Anda butuhkan

2. Mempersiapkan

Mendownload Kode

Klik link berikut guna mendownload semua kode untuk codelab ini:

Atau jika mau, Anda dapat meng-clone kode dari GitHub:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-workmanager.git
$ cd basic-android-kotlin-compose-training-workmanager
$ git checkout intermediate

Buka project di Android Studio.

3. Memastikan pekerjaan unik

Setelah Anda mengetahui cara merantai pekerja, kini saatnya menangani fitur canggih lain dari WorkManager: urutan pekerjaan unik.

Terkadang, Anda hanya ingin menjalankan satu rantai pekerjaan dalam satu waktu. Misalnya, mungkin Anda memiliki rantai pekerjaan yang menyinkronkan data lokal dengan server. Anda mungkin ingin sinkronisasi data pertama selesai sebelum memulai yang baru. Untuk melakukannya, gunakan beginUniqueWork(), bukan beginWith(), dan berikan nama String yang unik. Input ini memberikan nama ke seluruh rantai permintaan pekerjaan sehingga Anda dapat merujuk dan mengkuerinya bersama-sama.

Anda juga harus meneruskan objek ExistingWorkPolicy. Objek ini memberi tahu Android OS apa yang terjadi jika pekerjaan sudah ada. Kemungkinan nilai ExistingWorkPolicy adalah REPLACE, KEEP, APPEND, atau APPEND_OR_REPLACE.

Di aplikasi ini, Anda perlu menggunakan REPLACE karena jika pengguna memutuskan untuk memburamkan gambar lain sebelum gambar saat ini selesai, Anda dapat menghentikan gambar saat ini dan mulai memburamkan gambar baru.

Anda juga perlu memastikan bahwa jika pengguna mengklik Start ketika permintaan pekerjaan sudah ada dalam antrean, aplikasi akan mengganti permintaan pekerjaan sebelumnya dengan permintaan baru tersebut. Tidak masuk akal untuk terus mengerjakan permintaan sebelumnya karena aplikasi akan tetap menggantinya dengan permintaan baru.

Di file data/WorkManagerBluromaticRepository.kt, di dalam metode applyBlur(), selesaikan langkah-langkah berikut:

  1. Hapus panggilan ke fungsi beginWith() dan tambahkan panggilan ke fungsi beginUniqueWork().
  2. Untuk parameter pertama ke fungsi beginUniqueWork(), teruskan IMAGE_MANIPULATION_WORK_NAME yang konstan.
  3. Untuk parameter kedua, parameter existingWorkPolicy, teruskan ExistingWorkPolicy.REPLACE.
  4. Untuk parameter ketiga, buat OneTimeWorkRequest baru untuk CleanupWorker.

data/WorkManagerBluromaticRepository.kt

import androidx.work.ExistingWorkPolicy
import com.example.bluromatic.IMAGE_MANIPULATION_WORK_NAME
...
// REPLACE THIS CODE:
// var continuation = workManager.beginWith(OneTimeWorkRequest.from(CleanupWorker::class.java))
// WITH
var continuation = workManager
    .beginUniqueWork(
        IMAGE_MANIPULATION_WORK_NAME,
        ExistingWorkPolicy.REPLACE,
        OneTimeWorkRequest.from(CleanupWorker::class.java)
    )
...

Blur-O-Matic kini hanya memburamkan satu gambar dalam satu waktu.

4. Memberi tag dan mengupdate UI berdasarkan status Pekerjaan

Perubahan berikutnya yang Anda lakukan adalah hal-hal yang ditampilkan aplikasi saat Pekerjaan dieksekusi. Informasi yang ditampilkan tentang pekerjaan dalam antrean menentukan cara UI berubah.

Tabel ini menunjukkan tiga metode berbeda yang dapat Anda panggil untuk mendapatkan informasi pekerjaan:

Jenis

Metode WorkManager

Deskripsi

Mendapatkan pekerjaan menggunakan id

getWorkInfoByIdLiveData()

Fungsi ini menampilkan satu LiveData<WorkInfo> untuk WorkRequest tertentu berdasarkan ID-nya.

Mendapatkan pekerjaan menggunakan nama rantai unik

getWorkInfosForUniqueWorkLiveData()

Fungsi ini menampilkan LiveData<List<WorkInfo>> untuk semua pekerjaan dalam rantai WorkRequest yang unik.

Mendapatkan pekerjaan menggunakan tag

getWorkInfosByTagLiveData()

Fungsi ini menampilkan LiveData<List<WorkInfo>> untuk tag.

Objek WorkInfo berisi detail tentang status WorkRequest saat ini, termasuk:

Metode ini menampilkan LiveData. LiveData adalah holder data yang dapat diamati dan berbasis siklus proses. Kita mengonversinya menjadi Flow objek WorkInfo dengan memanggil .asFlow().

Karena Anda ingin tahu kapan gambar akhir disimpan, Anda perlu menambahkan tag ke WorkRequest SaveImageToFileWorker agar bisa mendapatkan WorkInfo-nya dari metode getWorkInfosByTagLiveData().

Opsi lainnya adalah menggunakan metode getWorkInfosForUniqueWorkLiveData(), yang menampilkan informasi tentang ketiga WorkRequests (CleanupWorker, BlurWorker, dan SaveImageToFileWorker). Kelemahan dari metode ini adalah Anda memerlukan kode tambahan untuk secara khusus menemukan informasi SaveImageToFileWorker yang diperlukan.

Memberi tag pada permintaan pekerjaan

Pemberian tag pada pekerjaan dilakukan dalam file data/WorkManagerBluromaticRepository.kt di dalam fungsi applyBlur().

  1. Saat Anda membuat permintaan pekerjaan SaveImageToFileWorker, beri tag pada pekerjaan dengan memanggil metode addTag() dan meneruskan konstanta String TAG_OUTPUT.

data/WorkManagerBluromaticRepository.kt

import com.example.bluromatic.TAG_OUTPUT
...
val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
    .addTag(TAG_OUTPUT) // <- Add this
    .build()

Alih-alih menggunakan ID WorkManager, Anda dapat menggunakan tag untuk memberi label pekerjaan karena jika pengguna memburamkan beberapa gambar, semua WorkRequest gambar yang disimpan akan memiliki tag yang sama, tetapi bukan ID yang sama.

Mendapatkan WorkInfo

Anda menggunakan informasi WorkInfo dari permintaan pekerjaan SaveImageToFileWorker dalam logika untuk menentukan composable mana yang akan ditampilkan di UI berdasarkan BlurUiState.

ViewModel menggunakan informasi ini dari variabel outputWorkInfo repositori.

Setelah memberi tag pada permintaan pekerjaan SaveImageToFileWorker, Anda dapat menyelesaikan langkah-langkah berikut untuk mengambil informasinya:

  1. Di file data/WorkManagerBluromaticRepository.kt, panggil metode workManager.getWorkInfosByTagLiveData() untuk mengisi variabel outputWorkInfo.
  2. Teruskan konstanta TAG_OUTPUT untuk parameter metode.

data/WorkManagerBluromaticRepository.kt

...
override val outputWorkInfo: Flow<WorkInfo?> =
    workManager.getWorkInfosByTagLiveData(TAG_OUTPUT)
...

Panggilan metode getWorkInfosByTagLiveData() akan menampilkan LiveData. LiveData adalah holder data yang dapat diamati dan berbasis siklus proses. Fungsi .asFlow() akan mengonversinya menjadi Flow.

  1. Rantai panggilan ke fungsi .asFlow() untuk mengonversi metode menjadi Flow. Anda harus mengonversi metode tersebut agar aplikasi dapat berfungsi dengan Flow Kotlin, bukan LiveData.

data/WorkManagerBluromaticRepository.kt

import androidx.lifecycle.asFlow
...
override val outputWorkInfo: Flow<WorkInfo?> =
    workManager.getWorkInfosByTagLiveData(TAG_OUTPUT).asFlow()
...
  1. Rantai panggilan ke fungsi transformasi .mapNotNull() untuk memastikan Flow berisi nilai.
  2. Untuk aturan transformasi, jika elemen tidak kosong, pilih item pertama dalam koleksi. Jika tidak, tampilkan nilai null. Fungsi transformasi akan menghapusnya kemudian jika nilainya null.

data/WorkManagerBluromaticRepository.kt

import kotlinx.coroutines.flow.mapNotNull
...
    override val outputWorkInfo: Flow<WorkInfo?> =
        workManager.getWorkInfosByTagLiveData(TAG_OUTPUT).asFlow().mapNotNull {
            if (it.isNotEmpty()) it.first() else null
        }
...
  1. Karena fungsi transformasi .mapNotNull() menjamin adanya nilai, Anda dapat menghapus ? dari jenis Flow dengan aman karena tidak perlu lagi berupa jenis nullable.

data/WorkManagerBluromaticRepository.kt

...
    override val outputWorkInfo: Flow<WorkInfo> =
...
  1. Anda juga harus menghapus ? dari antarmuka BluromaticRepository.

data/BluromaticRepository.kt

...
interface BluromaticRepository {
//    val outputWorkInfo: Flow<WorkInfo?>
    val outputWorkInfo: Flow<WorkInfo>
...

Informasi WorkInfo ditampilkan sebagai Flow dari repositori. ViewModel kemudian memakainya.

Mengubah BlurUiState

ViewModel menggunakan WorkInfo yang dimunculkan oleh repositori dari Flow outputWorkInfo untuk menetapkan nilai variabel blurUiState.

Kode UI menggunakan nilai variabel blurUiState untuk menentukan composable yang akan ditampilkan.

Selesaikan langkah-langkah berikut untuk mengubah blurUiState:

  1. Isi variabel blurUiState dengan Flow outputWorkInfo dari repositori.

ui/BlurViewModel.kt

// ...
// REMOVE
// val blurUiState: StateFlow<BlurUiState> = MutableStateFlow(BlurUiState.Default)

// ADD
val blurUiState: StateFlow<BlurUiState> = bluromaticRepository.outputWorkInfo
// ...
  1. Selanjutnya, Anda perlu memetakan nilai dalam Flow ke status BlurUiState, bergantung pada status pekerjaannya.

Setelah pekerjaan selesai, tetapkan variabel blurUiState ke BlurUiState.Complete(outputUri = "").

Setelah tugas dibatalkan, tetapkan variabel blurUiState ke BlurUiState.Default.

Jika tidak, tetapkan variabel blurUiState ke BlurUiState.Loading.

ui/BlurViewModel.kt

import androidx.work.WorkInfo
import kotlinx.coroutines.flow.map
// ...

    val blurUiState: StateFlow<BlurUiState> = bluromaticRepository.outputWorkInfo
        .map { info ->
            when {
                info.state.isFinished -> {
                    BlurUiState.Complete(outputUri = "")
                }
                info.state == WorkInfo.State.CANCELLED -> {
                    BlurUiState.Default
                }
                else -> BlurUiState.Loading
            }
        }

// ...
  1. Karena Anda tertarik dengan StateFlow, konversikan Flow dengan merantai panggilan ke fungsi .stateIn().

Panggilan ke fungsi .stateIn() memerlukan tiga argumen:

  1. Untuk parameter pertama, teruskan viewModelScope, yang merupakan cakupan coroutine yang terkait dengan ViewModel.
  2. Untuk parameter kedua, teruskan SharingStarted.WhileSubscribed(5_000). Parameter ini mengontrol kapan proses berbagi dimulai dan dihentikan.
  3. Untuk parameter ketiga, teruskan BlurUiState.Default, yang merupakan nilai awal alur status.

ui/BlurViewModel.kt

import kotlinx.coroutines.flow.stateIn
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
// ...

    val blurUiState: StateFlow<BlurUiState> = bluromaticRepository.outputWorkInfo
        .map { info ->
            when {
                info.state.isFinished -> {
                    BlurUiState.Complete(outputUri = "")
                }
                info.state == WorkInfo.State.CANCELLED -> {
                    BlurUiState.Default
                }
                else -> BlurUiState.Loading
            }
        }.stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = BlurUiState.Default
        )

// ...

ViewModel mengekspos informasi status UI sebagai StateFlow melalui variabel blurUiState. Flow dikonversi dari Flow dingin ke StateFlow panas dengan memanggil fungsi stateIn().

Mengupdate UI

Dalam file ui/BluromaticScreen.kt, Anda mendapatkan status UI dari variabel blurUiState ViewModel dan mengupdate UI.

Blok when mengontrol UI aplikasi. Blok when ini memiliki cabang untuk masing-masing dari tiga status BlurUiState.

UI diupdate dalam composable BlurActions di dalam composable Row. Selesaikan langkah berikut:

  1. Hapus kode Button(onStartClick) di dalam Composable Row dan ganti dengan blok when dengan blurUiState sebagai argumennya.

ui/BluromaticScreen.kt

...
    Row(
        modifier = modifier,
        horizontalArrangement = Arrangement.Center
    ) {
        // REMOVE
        // Button(
        //     onClick = onStartClick,
        //     modifier = Modifier.fillMaxWidth()
        // ) {
        //     Text(stringResource(R.string.start))
        // }
        // ADD
        when (blurUiState) {
        }
    }
...

Saat aplikasi terbuka, aplikasi berada dalam status default. Status ini dalam kode direpresentasikan sebagai BlurUiState.Default.

  1. Di dalam blok when, buat cabang untuk status ini seperti ditunjukkan dalam contoh kode berikut:

ui/BluromaticScreen.kt

...
    Row(
        modifier = modifier,
        horizontalArrangement = Arrangement.Center
    ) {
        when (blurUiState) {
            is BlurUiState.Default -> {}
        }
    }
...

Untuk status default, aplikasi menampilkan tombol Start.

  1. Untuk parameter onClick dalam status BlurUiState.Default, teruskan variabel onStartClick, yang diteruskan ke composable.
  2. Untuk parameter stringResourceId, teruskan ID resource string R.string.start.

ui/BluromaticScreen.kt

...
    Row(
        modifier = modifier,
        horizontalArrangement = Arrangement.Center
    ) {
        when (blurUiState) {
            is BlurUiState.Default -> {
                Button(
                    onClick = onStartClick,
                    modifier = Modifier.fillMaxWidth()
                ) {
                    Text(stringResource(R.string.start))
                }
        }
    }
...

Saat aplikasi memburamkan gambar secara aktif, itulah status BlurUiState.Loading. Untuk status ini, aplikasi menampilkan tombol Cancel Work dan indikator progres melingkar.

  1. Untuk parameter onClick tombol dalam status BlurUiState.Loading, teruskan variabel onCancelClick, yang diteruskan ke composable.
  2. Untuk parameter stringResourceId tombol, teruskan ID resource string R.string.cancel_work.

ui/BluromaticScreen.kt

import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.FilledTonalButton
...
    Row(
        modifier = modifier,
        horizontalArrangement = Arrangement.Center
    ) {
        when (blurUiState) {
            is BlurUiState.Default -> {
                Button(onStartClick) { Text(stringResource(R.string.start)) }
            }
            is BlurUiState.Loading -> {
               FilledTonalButton(onCancelClick) { Text(stringResource(R.string.cancel_work)) }
               CircularProgressIndicator(modifier = Modifier.padding(dimensionResource(R.dimen.padding_small)))
            }
        }
    }
...

Status terakhir yang akan dikonfigurasi adalah status BlurUiState.Complete, yang terjadi setelah gambar diburamkan dan disimpan. Saat ini, aplikasi hanya menampilkan tombol Start.

  1. Untuk parameter onClick dalam status BlurUiState.Complete, teruskan variabel onStartClick.
  2. Untuk parameter stringResourceId, teruskan ID resource string R.string.start.

ui/BluromaticScreen.kt

...
    Row(
        modifier = modifier,
        horizontalArrangement = Arrangement.Center
    ) {
        when (blurUiState) {
            is BlurUiState.Default -> {
                Button(onStartClick) { Text(stringResource(R.string.start)) }
            }
            is BlurUiState.Loading -> {
                FilledTonalButton(onCancelClick) { Text(stringResource(R.string.cancel_work)) }
                CircularProgressIndicator(modifier = Modifier.padding(dimensionResource(R.dimen.padding_small)))
            }
            is BlurUiState.Complete -> {
                Button(onStartClick) { Text(stringResource(R.string.start)) }
            }
        }
    }
...

Menjalankan aplikasi

  1. Jalankan aplikasi Anda, lalu klik Start.
  2. Lihat jendela Background Task Inspector untuk melihat bagaimana berbagai status sesuai dengan UI yang ditampilkan.

SystemJobService adalah komponen yang bertanggung jawab untuk mengelola eksekusi Pekerja.

Saat worker berjalan, UI akan menampilkan tombol Cancel Work dan indikator progres melingkar.

3395cc370b580b32.png

c5622f923670cf67.png

Setelah worker selesai, UI akan diupdate untuk menampilkan tombol Start seperti yang diharapkan.

97252f864ea042aa.png

81ba9962a8649e70.png

5. Menampilkan output akhir

Di bagian ini, Anda akan mengonfigurasi aplikasi untuk menampilkan tombol berlabel See File setiap kali ada gambar buram yang siap ditampilkan.

Membuat tombol See File

Tombol See File hanya muncul jika BlurUiState adalah Complete.

  1. Buka file ui/BluromaticScreen.kt dan buka composable BlurActions.
  2. Untuk menambahkan spasi antara tombol Start dan tombol See File, tambahkan composable Spacer dalam blok BlurUiState.Complete.
  3. Tambahkan composable FilledTonalButton baru.
  4. Untuk parameter onClick, teruskan onSeeFileClick(blurUiState.outputUri).
  5. Tambahkan composable Text untuk parameter konten Button.
  6. Untuk parameter text Text, gunakan ID resource string R.string.see_file.

ui/BluromaticScreen.kt

import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width

// ...
is BlurUiState.Complete -> {
    Button(onStartClick) { Text(stringResource(R.string.start)) }
    // Add a spacer and the new button with a "See File" label
    Spacer(modifier = Modifier.width(dimensionResource(R.dimen.padding_small)))
    FilledTonalButton({ onSeeFileClick(blurUiState.outputUri) })
    { Text(stringResource(R.string.see_file)) }
}
// ...

Mengupdate blurUiState

Status BlurUiState ditetapkan di ViewModel dan bergantung pada status permintaan pekerjaan dan mungkin variabel bluromaticRepository.outputWorkInfo.

  1. Di file ui/BlurViewModel.kt, di dalam transformasi map(), buat variabel baru outputImageUri.
  2. Isi URI gambar tersimpan variabel baru ini dari objek data outputData.

Anda dapat mengambil string ini dengan kunci KEY_IMAGE_URI.

ui/BlurViewModel.kt

import com.example.bluromatic.KEY_IMAGE_URI

// ...
.map { info ->
    val outputImageUri = info.outputData.getString(KEY_IMAGE_URI)
    when {
// ...
  1. Jika pekerja telah selesai dan variabel telah diisi, ini menunjukkan bahwa ada gambar yang diburamkan untuk ditampilkan.

Anda dapat memeriksa apakah variabel ini diisi dengan memanggil outputImageUri.isNullOrEmpty().

  1. Perbarui cabang isFinished untuk juga memeriksa apakah variabel telah diisi, lalu teruskan variabel outputImageUri ke objek data BlurUiState.Complete.

ui/BlurViewModel.kt

// ...
.map { info ->
    val outputImageUri = info.outputData.getString(KEY_IMAGE_URI)
    when {
        info.state.isFinished && !outputImageUri.isNullOrEmpty() -> {
            BlurUiState.Complete(outputUri = outputImageUri)
        }
        info.state == WorkInfo.State.CANCELLED -> {
// ...

Membuat kode peristiwa klik See File

Saat pengguna mengklik tombol See File, pengendali onClick akan memanggil fungsi yang ditetapkan. Fungsi ini akan diteruskan sebagai argumen dalam panggilan ke composable BlurActions().

Tujuan fungsi ini adalah menampilkan gambar yang disimpan dari URI-nya. Fungsi ini memanggil fungsi bantuan showBlurredImage() dan meneruskan URI. Fungsi bantuan membuat intent dan menggunakannya untuk memulai aktivitas baru guna menampilkan gambar yang disimpan.

  1. Buka file ui/BluromaticScreen.kt.
  2. Pada fungsi BluromaticScreenContent(), dalam panggilan ke fungsi composable BlurActions(), mulai buat fungsi lambda untuk parameter onSeeFileClick yang menggunakan satu parameter bernama currentUri. Pendekatan ini menyimpan URI gambar tersimpan.

ui/BluromaticScreen.kt

// ...
BlurActions(
    blurUiState = blurUiState,
    onStartClick = { applyBlur(selectedValue) },
    onSeeFileClick = { currentUri ->
    },
    onCancelClick = { cancelWork() },
    modifier = Modifier.fillMaxWidth()
)
// ...
  1. Di dalam isi fungsi lambda, panggil fungsi bantuan showBlurredImage().
  2. Untuk parameter pertama, teruskan variabel context.
  3. Untuk parameter kedua, teruskan variabel currentUri.

ui/BluromaticScreen.kt

// ...
BlurActions(
    blurUiState = blurUiState,
    onStartClick = { applyBlur(selectedValue) },
    // New lambda code runs when See File button is clicked
    onSeeFileClick = { currentUri ->
        showBlurredImage(context, currentUri)
    },
    onCancelClick = { cancelWork() },
    modifier = Modifier.fillMaxWidth()
)
// ...

Menjalankan aplikasi

Jalankan aplikasi Anda. Sekarang Anda akan melihat tombol See File baru yang dapat diklik, yang akan mengarahkan Anda ke file tersimpan:

9d76d5d7f231c6b6.png

926e532cc24a0d4f.png

6. Membatalkan pekerjaan

5cec830cc8ef647e.png

Sebelumnya, tombol Cancel Work telah ditambahkan, jadi Anda kini dapat menambahkan kode untuk membuatnya melakukan sesuatu. Dengan WorkManager, Anda dapat membatalkan pekerjaan menggunakan ID, tag, dan nama rantai unik.

Dalam hal ini, Anda perlu membatalkan pekerjaan dengan nama rantai uniknya karena Anda ingin membatalkan semua pekerjaan dalam rantai, bukan hanya langkah tertentu.

Membatalkan pekerjaan menurut nama

  1. Buka file data/WorkManagerBluromaticRepository.kt.
  2. Di fungsi cancelWork(), panggil fungsi workManager.cancelUniqueWork().
  3. Teruskan nama rantai unik IMAGE_MANIPULATION_WORK_NAME sehingga panggilan hanya membatalkan pekerjaan terjadwal dengan nama tersebut.

data/WorkManagerBluromaticRepository.kt

override fun cancelWork() {
    workManager.cancelUniqueWork(IMAGE_MANIPULATION_WORK_NAME)
}

Mengikuti prinsip desain pemisahan masalah, fungsi composable tidak boleh berinteraksi langsung dengan repositori. Fungsi composable berinteraksi dengan ViewModel, dan ViewModel berinteraksi dengan repositori.

Pendekatan ini merupakan prinsip desain yang baik untuk diikuti karena perubahan pada repositori tidak mengharuskan Anda mengubah fungsi composable karena tidak berinteraksi langsung.

  1. Buka file ui/BlurViewModel.kt.
  2. Buat fungsi baru bernama cancelWork() untuk membatalkan pekerjaan.
  3. Di dalam fungsi, pada objek bluromaticRepository, panggil metode cancelWork().

ui/BlurViewModel.kt

/**
 * Call method from repository to cancel any ongoing WorkRequest
 * */
fun cancelWork() {
    bluromaticRepository.cancelWork()
}

Menyiapkan peristiwa klik Cancel Work

  1. Buka file ui/BluromaticScreen.kt.
  2. Buka fungsi composable BluromaticScreen().

ui/BluromaticScreen.kt

fun BluromaticScreen(blurViewModel: BlurViewModel = viewModel(factory = BlurViewModel.Factory)) {
    val uiState by blurViewModel.blurUiState.collectAsStateWithLifecycle()
    val layoutDirection = LocalLayoutDirection.current
    Surface(
        modifier = Modifier
            .fillMaxSize()
            .statusBarsPadding()
            .padding(
                start = WindowInsets.safeDrawing
                    .asPaddingValues()
                    .calculateStartPadding(layoutDirection),
                end = WindowInsets.safeDrawing
                    .asPaddingValues()
                    .calculateEndPadding(layoutDirection)
            )
    ) {
        BluromaticScreenContent(
            blurUiState = uiState,
            blurAmountOptions = blurViewModel.blurAmount,
            applyBlur = blurViewModel::applyBlur,
            cancelWork = {},
            modifier = Modifier
                .verticalScroll(rememberScrollState())
                .padding(dimensionResource(R.dimen.padding_medium))
        )
    }
}

Dalam panggilan ke composable BluromaticScreenContent, Anda perlu membuat metode cancelWork() ViewModel berjalan saat pengguna mengklik tombol.

  1. Tetapkan parameter cancelWork untuk nilai blurViewModel::cancelWork.

ui/BluromaticScreen.kt

// ...
        BluromaticScreenContent(
            blurUiState = uiState,
            blurAmountOptions = blurViewModel.blurAmount,
            applyBlur = blurViewModel::applyBlur,
            cancelWork = blurViewModel::cancelWork,
            modifier = Modifier
                .verticalScroll(rememberScrollState())
                .padding(dimensionResource(R.dimen.padding_medium))
        )
// ...

Menjalankan aplikasi dan membatalkan pekerjaan

Jalankan aplikasi Anda. Aplikasi harus mengompilasi dengan baik. Mulailah memburamkan gambar, lalu klik Cancel Work. Seluruh rantai dibatalkan.

81ba9962a8649e70.png

Setelah Anda membatalkan pekerjaan, hanya tombol Start yang akan ditampilkan karena WorkInfo.State adalah CANCELLED. Perubahan ini menyebabkan variabel blurUiState ditetapkan ke BlurUiState.Default, yang mereset UI kembali ke status awal dan hanya menampilkan tombol Start.

Background Task Inspector menampilkan status Cancelled yang diharapkan.

7656dd320866172e.png

7. Batasan pekerjaan

Terakhir, WorkManager mendukung Constraints. Batasan adalah persyaratan yang harus Anda penuhi sebelum WorkRequest dijalankan.

Beberapa contoh batasan adalah requiresDeviceIdle() dan requiresStorageNotLow().

  • Untuk batasan requiresDeviceIdle(), jika diberi nilai true, pekerjaan hanya akan berjalan jika perangkat tidak ada aktivitas.
  • Untuk batasan requiresStorageNotLow(), jika diberi nilai true, pekerjaan hanya akan berjalan jika penyimpanan tidak rendah.

Untuk Blur-O-Matic, tambahkan batasan bahwa level pengisian daya baterai perangkat tidak boleh rendah sebelum menjalankan permintaan pekerjaan blurWorker. Batasan ini berarti permintaan pekerjaan Anda ditunda dan hanya berjalan setelah baterai perangkat tidak rendah.

Membuat baterai tanpa batasan rendah

Di file data/WorkManagerBluromaticRepository.kt, selesaikan langkah-langkah berikut:

  1. Buka metode applyBlur().
  2. Setelah kode mendeklarasikan variabel continuation, buat variabel baru bernama constraints, yang menyimpan objek Constraints untuk batasan yang dibuat.
  3. Buat builder untuk objek Constraints dengan memanggil fungsi Constraints.Builder() dan menetapkannya ke variabel baru.

data/WorkManagerBluromaticRepository.kt

import androidx.work.Constraints

// ...
    override fun applyBlur(blurLevel: Int) {
        // ...

        val constraints = Constraints.Builder()
// ...
  1. Rantai metode setRequiresBatteryNotLow() ke panggilan dan teruskan nilai true sehingga WorkRequest hanya berjalan saat baterai perangkat tidak rendah.

data/WorkManagerBluromaticRepository.kt

// ...
    override fun applyBlur(blurLevel: Int) {
        // ...

        val constraints = Constraints.Builder()
            .setRequiresBatteryNotLow(true)
// ...
  1. Buat objek dengan membuat rantai panggilan ke metode .build().

data/WorkManagerBluromaticRepository.kt

// ...
    override fun applyBlur(blurLevel: Int) {
        // ...

        val constraints = Constraints.Builder()
            .setRequiresBatteryNotLow(true)
            .build()
// ...
  1. Untuk menambahkan objek batasan ke permintaan pekerjaan blurBuilder, rantai panggilan ke metode .setConstraints() dan teruskan objek batasan.

data/WorkManagerBluromaticRepository.kt

// ...
blurBuilder.setInputData(createInputDataForWorkRequest(blurLevel, imageUri))

blurBuilder.setConstraints(constraints) // Add this code
//...

Menguji dengan emulator

  1. Pada emulator, ubah Charge level di jendela Extended Controls menjadi 15% atau lebih rendah untuk menyimulasikan skenario baterai lemah, Charger connection menjadi AC charger, dan Battery status ke Not charging.

9b0084cb6e1a8672.png

  1. Jalankan aplikasi, lalu klik Start untuk mulai memburamkan gambar.

Level pengisian daya baterai emulator disetel ke rendah sehingga WorkManager tidak menjalankan permintaan pekerjaan blurWorker karena batasan tersebut. Pekerjaan itu akan diantrekan, tetapi ditangguhkan hingga batasan terpenuhi. Anda dapat melihat penangguhan ini di tab Background Task Inspector.

7518cf0353d04f12.png

  1. Setelah memastikan pekerjaan tidak berjalan, tingkatkan level pengisian daya baterai secara perlahan.

Batasan ini terpenuhi setelah level pengisian daya baterai mencapai sekitar 25%, dan pekerjaan yang ditangguhkan akan berjalan. Hasil ini akan muncul di tab Background Task Inspector.

ab189db49e7b8997.png

8. Menulis pengujian untuk implementasi Pekerja

Cara menguji WorkManager

Menulis pengujian untuk Pekerja dan pengujian menggunakan WorkManager API mungkin berlawanan. Pekerjaan yang dilakukan di Pekerja tidak memiliki akses langsung ke UI. Itu hanyalah logika bisnis. Biasanya, Anda menguji logika bisnis dengan pengujian unit lokal. Namun, Anda mungkin ingat dari codelab Pekerjaan Latar Belakang dengan WorkManager bahwa WorkManager memerlukan Konteks Android untuk berjalan. Secara default, konteks tidak tersedia dalam pengujian unit lokal. Oleh karena itu, Anda harus menguji pengujian Pekerja dengan pengujian UI, meskipun tidak ada elemen UI langsung untuk diuji.

Menyiapkan dependensi

Anda perlu menambahkan tiga dependensi gradle ke project. Dua yang pertama mengaktifkan JUnit dan espresso untuk pengujian UI. Dependensi ketiga menyediakan API pengujian pekerjaan.

app/build.gradle.kts

dependencies {
    // Espresso
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
    // Junit
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    // Work testing
    androidTestImplementation("androidx.work:work-testing:2.8.1")
}

Anda harus menggunakan versi rilis stabil work-runtime-ktx terbaru di aplikasi Anda. Jika Anda mengubah versi, pastikan untuk mengklik Sync Now guna menyinkronkan project Anda dengan file gradle yang telah diperbarui.

Membuat class pengujian

  1. Buat direktori untuk pengujian UI Anda di direktori app > src. a7768e9b6ea994d3.png

20cc54de1756c884.png

  1. Buat class Kotlin baru di direktori androidTest/java yang diberi nama WorkerInstrumentationTest.

Menulis pengujian CleanupWorker

Ikuti langkah-langkah untuk menulis pengujian guna memverifikasi implementasi CleanupWorker. Coba terapkan verifikasi ini sendiri berdasarkan petunjuk. Solusi diberikan di akhir langkah.

  1. Di WorkerInstrumentationTest.kt, buat variabel lateinit untuk menyimpan instance Context.
  2. Buat metode setUp() yang dianotasi dengan @Before.
  3. Dalam metode setUp(), lakukan inisialisasi variabel konteks lateinit dengan konteks aplikasi dari ApplicationProvider.
  4. Buat fungsi pengujian bernama cleanupWorker_doWork_resultSuccess().
  5. Dalam pengujian cleanupWorker_doWork_resultSuccess(), buat instance CleanupWorker.

WorkerInstrumentationTest.kt

class WorkerInstrumentationTest {
   private lateinit var context: Context

   @Before
   fun setUp() {
       context = ApplicationProvider.getApplicationContext()
   }

   @Test
   fun cleanupWorker_doWork_resultSuccess() {
   }
}

Saat menulis aplikasi Blur-O-Matic, Anda menggunakan OneTimeWorkRequestBuilder untuk membuat pekerja. Pengujian Pekerja memerlukan builder pekerjaan yang berbeda. WorkManager API menyediakan dua builder yang berbeda:

Kedua builder ini memungkinkan Anda menguji logika bisnis pekerja Anda. Untuk CoroutineWorkers, seperti CleanupWorker, BlurWorker, dan SaveImageToFileWorker, gunakan TestListenableWorkerBuilder untuk pengujian karena mampu menangani kompleksitas threading dari coroutine.

  1. CoroutineWorker berjalan secara asinkron, mengingat penggunaan coroutine. Untuk menjalankan pekerja secara paralel, gunakan runBlocking. Berikan isi lambda kosong untuk memulai, tetapi Anda akan menggunakan runBlocking untuk memerintahkan pekerja ke doWork() secara langsung, bukan mengantrekan pekerja.

WorkerInstrumentationTest.kt

class WorkerInstrumentationTest {
   private lateinit var context: Context

   @Before
   fun setUp() {
       context = ApplicationProvider.getApplicationContext()
   }

   @Test
   fun cleanupWorker_doWork_resultSuccess() {
       val worker = TestListenableWorkerBuilder<CleanupWorker>(context).build()
       runBlocking {
       }
   }
}
  1. Dalam isi lambda runBlocking, panggil doWork() pada instance CleanupWorker yang Anda buat pada langkah 5 dan simpan sebagai nilai.

Anda mungkin ingat bahwa CleanupWorker akan menghapus semua file PNG yang disimpan dalam struktur file aplikasi Blur-O-Matic. Proses ini melibatkan input/output file, yang berarti bahwa pengecualian dapat ditampilkan saat mencoba menghapus file. Karena alasan ini, upaya untuk menghapus file digabungkan dalam blok try.

CleanupWorker.kt

...
            return@withContext try {
                val outputDirectory = File(applicationContext.filesDir, OUTPUT_PATH)
                if (outputDirectory.exists()) {
                    val entries = outputDirectory.listFiles()
                    if (entries != null) {
                        for (entry in entries) {
                            val name = entry.name
                            if (name.isNotEmpty() && name.endsWith(".png")) {
                                val deleted = entry.delete()
                                Log.i(TAG, "Deleted $name - $deleted")
                            }
                        }
                    }
                }
                Result.success()
            } catch (exception: Exception) {
                Log.e(
                    TAG,
                    applicationContext.resources.getString(R.string.error_cleaning_file),
                    exception
                )
                Result.failure()
            }

Perlu diketahui bahwa di akhir blok try, Result.success() akan ditampilkan. Jika kode sampai ke Result.success(), tidak ada error saat mengakses direktori file.

Sekarang saatnya membuat pernyataan yang menunjukkan bahwa pekerja telah berhasil.

  1. Nyatakan bahwa hasil pekerja ListenableWorker.Result.success().

Lihat kode solusi berikut:

WorkerInstrumentationTest.kt

class WorkerInstrumentationTest {
   private lateinit var context: Context

   @Before
   fun setUp() {
       context = ApplicationProvider.getApplicationContext()
   }

   @Test
   fun cleanupWorker_doWork_resultSuccess() {
       val worker = TestListenableWorkerBuilder<CleanupWorker>(context).build()
       runBlocking {
           val result = worker.doWork()
           assertTrue(result is ListenableWorker.Result.Success)
       }
   }
}

Menulis pengujian BlurWorker

Ikuti langkah-langkah berikut untuk menulis pengujian guna memverifikasi implementasi BlurWorker. Coba terapkan verifikasi ini sendiri berdasarkan petunjuk. Solusi diberikan di akhir langkah.

  1. Di WorkerInstrumentationTest.kt, buat fungsi pengujian baru bernama blurWorker_doWork_resultSuccessReturnsUri().

BlurWorker memerlukan gambar untuk diproses. Oleh karena itu, mem-build instance BlurWorker memerlukan beberapa data input yang menyertakan gambar tersebut.

  1. Di luar fungsi pengujian, buat input URI tiruan. URI tiruan adalah pasangan yang berisi kunci dan nilai URI. Gunakan kode contoh berikut untuk pasangan nilai kunci:
KEY_IMAGE_URI to "android.resource://com.example.bluromatic/drawable/android_cupcake"
  1. Build BlurWorker di dalam fungsi blurWorker_doWork_resultSuccessReturnsUri() dan pastikan untuk meneruskan input URI tiruan yang Anda buat sebagai data pekerjaan melalui metode setInputData().

Serupa dengan pengujian CleanupWorker, Anda harus memanggil implementasi pekerja di dalam runBlocking.

  1. Buat blok runBlocking.
  2. Panggil doWork() di dalam blok runBlocking.

Tidak seperti CleanupWorker, BlurWorker memiliki beberapa data output yang siap untuk pengujian.

  1. Untuk mengakses data output, ekstrak URI dari hasil doWork().

WorkerInstrumentationTest.kt

@Test
fun blurWorker_doWork_resultSuccessReturnsUri() {
    val worker = TestListenableWorkerBuilder<BlurWorker>(context)
        .setInputData(workDataOf(mockUriInput))
        .build()
    runBlocking {
        val result = worker.doWork()
        val resultUri = result.outputData.getString(KEY_IMAGE_URI)
    }
}
  1. Buat pernyataan bahwa pekerja berhasil. Misalnya, lihat kode berikut dari BlurWorker:

BlurWorker.kt

val resourceUri = inputData.getString(KEY_IMAGE_URI)
val blurLevel = inputData.getInt(BLUR_LEVEL, 1)

...
val picture = BitmapFactory.decodeStream(
    resolver.openInputStream(Uri.parse(resourceUri))
)

val output = blurBitmap(picture, blurLevel)

// Write bitmap to a temp file
val outputUri = writeBitmapToFile(applicationContext, output)

val outputData = workDataOf(KEY_IMAGE_URI to outputUri.toString())

Result.success(outputData)
...

BlurWorker mengambil URI dan tingkat blur dari data input serta membuat file sementara. Jika operasi berhasil, pasangan nilai kunci yang berisi URI akan ditampilkan. Untuk memeriksa apakah konten output sudah benar, buat pernyataan bahwa data output berisi kunci KEY_IMAGE_URI.

  1. Buat pernyataan bahwa data output berisi URI yang dimulai dengan string "file:///data/user/0/com.example.bluromatic/files/blur_filter_outputs/blur-filter-output-"
  1. Periksa pengujian Anda dengan kode solusi berikut:

WorkerInstrumentationTest.kt

    @Test
    fun blurWorker_doWork_resultSuccessReturnsUri() {
        val worker = TestListenableWorkerBuilder<BlurWorker>(context)
            .setInputData(workDataOf(mockUriInput))
            .build()
        runBlocking {
            val result = worker.doWork()
            val resultUri = result.outputData.getString(KEY_IMAGE_URI)
            assertTrue(result is ListenableWorker.Result.Success)
            assertTrue(result.outputData.keyValueMap.containsKey(KEY_IMAGE_URI))
            assertTrue(
                resultUri?.startsWith("file:///data/user/0/com.example.bluromatic/files/blur_filter_outputs/blur-filter-output-")
                    ?: false
            )
        }
    }

Menulis pengujian SaveImageToFileWorker

Sesuai dengan namanya, SaveImageToFileWorker menulis file ke disk. Ingat bahwa di WorkManagerBluromaticRepository, Anda menambahkan SaveImageToFileWorker ke WorkManager sebagai kelanjutan setelah BlurWorker. Oleh karena itu, kode itu memiliki data input yang sama. Kode itu mengambil URI dari data input, membuat bitmap, lalu menulis bitmap tersebut ke disk sebagai file. Jika operasi berhasil, output yang dihasilkan adalah URL gambar. Pengujian untuk SaveImageToFileWorker sangat mirip dengan pengujian BlurWorker, satu-satunya perbedaan adalah data output.

Lihat apakah Anda dapat menulis sendiri pengujian untuk SaveImageToFileWorker. Setelah selesai, Anda dapat memeriksa solusi di bawah ini. Ingat kembali pendekatan yang Anda ambil untuk pengujian BlurWorker:

  1. Build pekerja, dengan meneruskan data input.
  2. Buat blok runBlocking.
  3. Panggil doWork() pada pekerja.
  4. Periksa apakah hasilnya berhasil.
  5. Periksa output untuk menemukan kunci dan nilai yang benar.

Berikut solusinya:

@Test
fun saveImageToFileWorker_doWork_resultSuccessReturnsUrl() {
    val worker = TestListenableWorkerBuilder<SaveImageToFileWorker>(context)
        .setInputData(workDataOf(mockUriInput))
        .build()
    runBlocking {
        val result = worker.doWork()
        val resultUri = result.outputData.getString(KEY_IMAGE_URI)
        assertTrue(result is ListenableWorker.Result.Success)
        assertTrue(result.outputData.keyValueMap.containsKey(KEY_IMAGE_URI))
        assertTrue(
            resultUri?.startsWith("content://media/external/images/media/")
                ?: false
        )
    }
}

9. Men-debug WorkManager dengan Background Task Inspector

Memeriksa Pekerja

Pengujian otomatis adalah cara yang bagus untuk memverifikasi fungsi Pekerja Anda. Namun, pengujian itu tidak menyediakan utilitas yang cukup banyak saat Anda mencoba men-debug Pekerja. Untungnya, Android Studio memiliki alat yang memungkinkan Anda memvisualisasikan, memantau, dan men-debug Pekerja secara real time. Background Task Inspector berfungsi untuk emulator dan perangkat yang menjalankan API level 26 atau yang lebih tinggi.

Di bagian ini, Anda akan mempelajari beberapa fitur yang disediakan Background Task Inspector untuk memeriksa pekerja di Blur-O-Matic.

  1. Luncurkan aplikasi Blur-O-Matic pada perangkat atau emulator.
  2. Buka View > Tool Windows > App Inspection.

798f10dfd8d74bb1.png

  1. Pilih tab Background Task Inspector.

d601998f3754e793.png

  1. Jika perlu, pilih perangkat dan proses yang berjalan dari menu drop-down.

Dalam gambar contoh, prosesnya adalah com.example.bluromatic. Sistem mungkin akan otomatis memilih prosesnya untuk Anda. Jika sistem memilih proses yang salah, Anda dapat mengubahnya.

6428a2ab43fc42d1.png

  1. Klik menu drop-down Workers. Saat ini tidak ada worker yang berjalan, dan hal itu wajar karena belum ada upaya untuk memburamkan gambar.

cf8c466b3fd7fed1.png

  1. Di aplikasi, pilih More blurred dan klik Start. Anda akan segera melihat beberapa konten di drop-down Workers.

Sekarang Anda melihat sesuatu seperti ini di drop-down Workers.

569a8e0c1c6993ce.png

Tabel Worker menunjukkan nama Worker, Service (dalam kasus ini SystemJobService), status masing-masing, dan stempel waktu. Pada screenshot dari langkah sebelumnya, perhatikan bahwa BlurWorker dan CleanupWorker telah berhasil menyelesaikan pekerjaannya.

Anda juga dapat membatalkan pekerjaan menggunakan pemeriksa.

  1. Pilih pekerja dalam antrean, lalu klik Cancel Selected Worker 7108c2a82f64b348.png dari toolbar.

Memeriksa detail tugas

  1. Klik worker dalam tabel Workers. 97eac5ad23c41127.png

Tindakan ini akan memunculkan jendela Task Details.

9d4e17f7d4afa6bd.png

  1. Tinjau informasi yang ditampilkan di Task Details. 59fa1bf4ad8f4d8d.png

Detail menampilkan kategori berikut:

  • Description: Bagian ini mencantumkan nama class Pekerja dengan paket yang sepenuhnya memenuhi syarat, tag yang ditetapkan, dan UUID pekerja itu.
  • Execution: Bagian ini menampilkan batasan pekerja (jika ada), frekuensi yang berjalan, statusnya, serta class mana yang membuat dan mengantrekan pekerja itu. Memanggil ulang BlurWorker memiliki batasan yang akan mencegahnya dieksekusi saat baterai lemah. Saat Anda memeriksa Pekerja yang memiliki batasan, Pekerja tersebut akan muncul di bagian ini.
  • WorkContinuation: Bagian ini menampilkan lokasi pekerja tersebut dalam rantai pekerjaan. Untuk memeriksa detail pekerja lain dalam rantai pekerjaan, klik UUID-nya.
  • Results: Bagian ini menampilkan waktu mulai, jumlah percobaan ulang, dan data output dari pekerja yang dipilih.

Tampilan grafik

Ingat bahwa pekerja di Blur-O-Matic dirantai. Background Task Inspector menawarkan tampilan grafik yang merepresentasikan dependensi pekerja secara visual.

Di pojok jendela Background Task Inspector, ada dua tombol untuk beralih antara — Show Graph View dan Show List View.

4cd96a8b2773f466.png

  1. Klik Show Graph View 6f871bb00ad8b11a.png:

ece206da18cfd1c9.png

Tampilan grafik secara akurat menunjukkan dependensi Worker yang diimplementasikan di aplikasi Blur-O-Matic.

  1. Klik Show List View 669084937ea340f5.png untuk keluar dari tampilan grafik.

Fitur tambahan

Aplikasi Blur-O-Matic hanya mengimplementasikan Pekerja untuk menyelesaikan tugas latar belakang. Namun, Anda dapat membaca selengkapnya tentang alat yang tersedia untuk memeriksa jenis pekerjaan latar belakang lainnya dalam dokumentasi untuk Background Task Inspector.

10. Mendapatkan kode solusi

Untuk mendownload kode codelab yang sudah selesai, Anda dapat menggunakan perintah berikut:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-workmanager.git
$ cd basic-android-kotlin-compose-training-workmanager
$ git checkout main

Atau, Anda dapat mendownload repositori sebagai file ZIP, lalu mengekstraknya, dan membukanya di Android Studio.

Jika Anda ingin melihat kode solusi untuk codelab ini, lihat kode tersebut di GitHub.

11. Selamat

Selamat! Anda telah mempelajari fungsi tambahan WorkManager, menulis pengujian otomatis untuk pekerja Blur-O-Matic, dan menggunakan Background Task Inspector untuk memeriksanya. Dalam codelab ini, Anda telah mempelajari:

  • Menamai rantai WorkRequest unik.
  • Memberi tag pada WorkRequest.
  • Mengupdate UI berdasarkan WorkInfo.
  • Membatalkan WorkRequest.
  • Menambahkan batasan ke WorkRequest.
  • API pengujian WorkManager.
  • Cara mendekati implementasi pekerja pengujian.
  • Cara menguji CoroutineWorker.
  • Cara memeriksa pekerja dan memverifikasi fungsinya secara manual.