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
- Versi stabil terbaru Android Studio
- Penyelesaian codelab Pekerjaan Latar Belakang dengan WorkManager
- Perangkat Android atau emulator.
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:
- Hapus panggilan ke fungsi
beginWith()
dan tambahkan panggilan ke fungsibeginUniqueWork()
. - Untuk parameter pertama ke fungsi
beginUniqueWork()
, teruskanIMAGE_MANIPULATION_WORK_NAME
yang konstan. - Untuk parameter kedua, parameter
existingWorkPolicy
, teruskanExistingWorkPolicy.REPLACE
. - Untuk parameter ketiga, buat
OneTimeWorkRequest
baru untukCleanupWorker
.
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 | Fungsi ini menampilkan satu LiveData<WorkInfo> untuk WorkRequest tertentu berdasarkan ID-nya. | |
Mendapatkan pekerjaan menggunakan nama rantai unik | Fungsi ini menampilkan LiveData<List<WorkInfo>> untuk semua pekerjaan dalam rantai WorkRequest yang unik. | |
Mendapatkan pekerjaan menggunakan tag | Fungsi ini menampilkan LiveData<List<WorkInfo>> untuk tag. |
Objek WorkInfo
berisi detail tentang status WorkRequest
saat ini, termasuk:
- Apakah pekerjaan tersebut
BLOCKED
,CANCELLED
,ENQUEUED
,FAILED
,RUNNING
, atauSUCCEEDED
- Apakah
WorkRequest
selesai dan semua data output dari pekerjaan.
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()
.
- Saat Anda membuat permintaan pekerjaan
SaveImageToFileWorker
, beri tag pada pekerjaan dengan memanggil metodeaddTag()
dan meneruskan konstantaString
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:
- Di file
data/WorkManagerBluromaticRepository.kt
, panggil metodeworkManager.getWorkInfosByTagLiveData()
untuk mengisi variabeloutputWorkInfo
. - 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.
- 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()
...
- Rantai panggilan ke fungsi transformasi
.mapNotNull()
untuk memastikan Flow berisi nilai. - 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
}
...
- 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> =
...
- Anda juga harus menghapus
?
dari antarmukaBluromaticRepository
.
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
:
- Isi variabel
blurUiState
dengan FlowoutputWorkInfo
dari repositori.
ui/BlurViewModel.kt
// ...
// REMOVE
// val blurUiState: StateFlow<BlurUiState> = MutableStateFlow(BlurUiState.Default)
// ADD
val blurUiState: StateFlow<BlurUiState> = bluromaticRepository.outputWorkInfo
// ...
- 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
}
}
// ...
- Karena Anda tertarik dengan StateFlow, konversikan Flow dengan merantai panggilan ke fungsi
.stateIn()
.
Panggilan ke fungsi .stateIn()
memerlukan tiga argumen:
- Untuk parameter pertama, teruskan
viewModelScope
, yang merupakan cakupan coroutine yang terkait dengan ViewModel. - Untuk parameter kedua, teruskan
SharingStarted.WhileSubscribed(5_000)
. Parameter ini mengontrol kapan proses berbagi dimulai dan dihentikan. - 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:
- Hapus kode
Button(onStartClick)
di dalam ComposableRow
dan ganti dengan blokwhen
denganblurUiState
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
.
- 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.
- Untuk parameter
onClick
dalam statusBlurUiState.Default
, teruskan variabelonStartClick
, yang diteruskan ke composable. - Untuk parameter
stringResourceId
, teruskan ID resource stringR.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.
- Untuk parameter
onClick
tombol dalam statusBlurUiState.Loading
, teruskan variabelonCancelClick
, yang diteruskan ke composable. - Untuk parameter
stringResourceId
tombol, teruskan ID resource stringR.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.
- Untuk parameter
onClick
dalam statusBlurUiState.Complete
, teruskan variabelonStartClick
. - Untuk parameter
stringResourceId
, teruskan ID resource stringR.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
- Jalankan aplikasi Anda, lalu klik Start.
- 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.
Setelah worker selesai, UI akan diupdate untuk menampilkan tombol Start seperti yang diharapkan.
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
.
- Buka file
ui/BluromaticScreen.kt
dan buka composableBlurActions
. - Untuk menambahkan spasi antara tombol Start dan tombol See File, tambahkan composable
Spacer
dalam blokBlurUiState.Complete
. - Tambahkan composable
FilledTonalButton
baru. - Untuk parameter
onClick
, teruskanonSeeFileClick(blurUiState.outputUri)
. - Tambahkan composable
Text
untuk parameter kontenButton
. - Untuk parameter
text
Text
, gunakan ID resource stringR.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
.
- Di file
ui/BlurViewModel.kt
, di dalam transformasimap()
, buat variabel baruoutputImageUri
. - 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 {
// ...
- 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()
.
- Perbarui cabang
isFinished
untuk juga memeriksa apakah variabel telah diisi, lalu teruskan variabeloutputImageUri
ke objek dataBlurUiState.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.
- Buka file
ui/BluromaticScreen.kt
. - Pada fungsi
BluromaticScreenContent()
, dalam panggilan ke fungsi composableBlurActions()
, mulai buat fungsi lambda untuk parameteronSeeFileClick
yang menggunakan satu parameter bernamacurrentUri
. Pendekatan ini menyimpan URI gambar tersimpan.
ui/BluromaticScreen.kt
// ...
BlurActions(
blurUiState = blurUiState,
onStartClick = { applyBlur(selectedValue) },
onSeeFileClick = { currentUri ->
},
onCancelClick = { cancelWork() },
modifier = Modifier.fillMaxWidth()
)
// ...
- Di dalam isi fungsi lambda, panggil fungsi bantuan
showBlurredImage()
. - Untuk parameter pertama, teruskan variabel
context
. - 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:
6. Membatalkan pekerjaan
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
- Buka file
data/WorkManagerBluromaticRepository.kt
. - Di fungsi
cancelWork()
, panggil fungsiworkManager.cancelUniqueWork()
. - 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.
- Buka file
ui/BlurViewModel.kt
. - Buat fungsi baru bernama
cancelWork()
untuk membatalkan pekerjaan. - Di dalam fungsi, pada objek
bluromaticRepository
, panggil metodecancelWork()
.
ui/BlurViewModel.kt
/**
* Call method from repository to cancel any ongoing WorkRequest
* */
fun cancelWork() {
bluromaticRepository.cancelWork()
}
Menyiapkan peristiwa klik Cancel Work
- Buka file
ui/BluromaticScreen.kt
. - 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.
- Tetapkan parameter
cancelWork
untuk nilaiblurViewModel::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.
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.
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 nilaitrue
, pekerjaan hanya akan berjalan jika perangkat tidak ada aktivitas. - Untuk batasan
requiresStorageNotLow()
, jika diberi nilaitrue
, 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:
- Buka metode
applyBlur()
. - Setelah kode mendeklarasikan variabel
continuation
, buat variabel baru bernamaconstraints
, yang menyimpan objekConstraints
untuk batasan yang dibuat. - 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()
// ...
- Rantai metode
setRequiresBatteryNotLow()
ke panggilan dan teruskan nilaitrue
sehinggaWorkRequest
hanya berjalan saat baterai perangkat tidak rendah.
data/WorkManagerBluromaticRepository.kt
// ...
override fun applyBlur(blurLevel: Int) {
// ...
val constraints = Constraints.Builder()
.setRequiresBatteryNotLow(true)
// ...
- Buat objek dengan membuat rantai panggilan ke metode
.build()
.
data/WorkManagerBluromaticRepository.kt
// ...
override fun applyBlur(blurLevel: Int) {
// ...
val constraints = Constraints.Builder()
.setRequiresBatteryNotLow(true)
.build()
// ...
- 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
- 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.
- 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.
- 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.
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
- Buat direktori untuk pengujian UI Anda di direktori app > src.
- Buat class Kotlin baru di direktori
androidTest/java
yang diberi namaWorkerInstrumentationTest
.
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.
- Di
WorkerInstrumentationTest.kt
, buat variabellateinit
untuk menyimpan instanceContext
. - Buat metode
setUp()
yang dianotasi dengan@Before
. - Dalam metode
setUp()
, lakukan inisialisasi variabel kontekslateinit
dengan konteks aplikasi dariApplicationProvider
. - Buat fungsi pengujian bernama
cleanupWorker_doWork_resultSuccess()
. - Dalam pengujian
cleanupWorker_doWork_resultSuccess()
, buat instanceCleanupWorker
.
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.
CoroutineWorker
berjalan secara asinkron, mengingat penggunaan coroutine. Untuk menjalankan pekerja secara paralel, gunakanrunBlocking
. Berikan isi lambda kosong untuk memulai, tetapi Anda akan menggunakanrunBlocking
untuk memerintahkan pekerja kedoWork()
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 {
}
}
}
- Dalam isi lambda
runBlocking
, panggildoWork()
pada instanceCleanupWorker
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.
- 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.
- Di
WorkerInstrumentationTest.kt
, buat fungsi pengujian baru bernamablurWorker_doWork_resultSuccessReturnsUri()
.
BlurWorker
memerlukan gambar untuk diproses. Oleh karena itu, mem-build instance BlurWorker
memerlukan beberapa data input yang menyertakan gambar tersebut.
- 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"
- Build
BlurWorker
di dalam fungsiblurWorker_doWork_resultSuccessReturnsUri()
dan pastikan untuk meneruskan input URI tiruan yang Anda buat sebagai data pekerjaan melalui metodesetInputData()
.
Serupa dengan pengujian CleanupWorker
, Anda harus memanggil implementasi pekerja di dalam runBlocking
.
- Buat blok
runBlocking
. - Panggil
doWork()
di dalam blokrunBlocking
.
Tidak seperti CleanupWorker
, BlurWorker
memiliki beberapa data output yang siap untuk pengujian.
- 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)
}
}
- 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
.
- 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-"
- 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
:
- Build pekerja, dengan meneruskan data input.
- Buat blok
runBlocking
. - Panggil
doWork()
pada pekerja. - Periksa apakah hasilnya berhasil.
- 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.
- Luncurkan aplikasi Blur-O-Matic pada perangkat atau emulator.
- Buka View > Tool Windows > App Inspection.
- Pilih tab Background Task Inspector.
- 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.
- Klik menu drop-down Workers. Saat ini tidak ada worker yang berjalan, dan hal itu wajar karena belum ada upaya untuk memburamkan gambar.
- 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.
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.
- Pilih pekerja dalam antrean, lalu klik Cancel Selected Worker dari toolbar.
Memeriksa detail tugas
- Klik worker dalam tabel Workers.
Tindakan ini akan memunculkan jendela Task Details.
- Tinjau informasi yang ditampilkan di Task Details.
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.
- Klik Show Graph View :
Tampilan grafik secara akurat menunjukkan dependensi Worker yang diimplementasikan di aplikasi Blur-O-Matic.
- Klik Show List View 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.