Halo, Habr!
Kami mengingatkan Anda bahwa kami telah memiliki
pemesanan di muka untuk
buku yang telah lama ditunggu-tunggu tentang bahasa Kotlin dari seri Big Nerd Ranch Guides yang terkenal. Hari ini kami memutuskan untuk membawa kepada Anda terjemahan dari sebuah artikel yang menceritakan tentang Kotlin coroutine dan tentang pekerjaan yang benar dengan stream di Android. Topik sedang dibahas dengan sangat aktif, oleh karena itu, untuk kelengkapan, kami juga menyarankan agar Anda melihat
artikel ini dari Habr dan
pos terperinci ini dari blog Axmor Software.
Kerangka kerja kompetitif modern di Java / Android menimbulkan neraka pada panggilan balik dan mengarah ke pemblokiran negara, karena Android tidak memiliki cara yang cukup sederhana untuk menjamin keamanan utas.
Corinthine Kotlin adalah toolkit yang sangat efektif dan lengkap yang membuat mengelola kompetisi menjadi lebih mudah dan lebih produktif.
Jeda dan blokir: apa bedanyaCoroutine tidak mengganti utas, tetapi menyediakan kerangka kerja untuk mengelola mereka. Filosofi dari corutin adalah untuk mendefinisikan konteks yang memungkinkan Anda untuk
menunggu operasi latar belakang untuk menyelesaikan tanpa memblokir utas utama.
Tujuan Corutin dalam hal ini adalah untuk menghilangkan panggilan balik dan menyederhanakan persaingan.
Contoh paling sederhanaUntuk memulainya, mari kita ambil contoh paling sederhana: jalankan coroutine dalam konteks
Main
(utas utama). Di dalamnya, kami akan mengekstrak gambar dari aliran
IO
dan mengirim gambar ini untuk diproses kembali ke
Main
.
launch(Dispatchers.Main) { val image = withContext(Dispatchers.IO) { getImage() }
Kode ini sederhana sebagai fungsi single-threaded. Selain itu, sementara
getImage
dieksekusi di kumpulan thread yang dialokasikan
IO
, utas utama gratis dan dapat mengambil tugas lain! Fungsi withContext menjeda coroutine saat ini saat aksinya berjalan (
getImage()
). Segera setelah
getImage()
kembali dan
looper
dari utas utama tersedia, coroutine akan melanjutkan pekerjaan di utas utama dan memanggil
imageView.setImageBitmap(image)
.
Contoh kedua: sekarang kita perlu menyelesaikan 2 tugas latar belakang agar bisa digunakan. Kami akan menggunakan duet async / await sehingga kedua tugas ini dilakukan secara paralel, dan menggunakan hasilnya di utas utama segera setelah kedua tugas siap:
val job = launch(Dispatchers.Main) { val deferred1 = async(Dispatchers.Default) { getFirstValue() } val deferred2 = async(Dispatchers.IO) { getSecondValue() } useValues(deferred1.await(), deferred2.await()) } job.join()
async
mirip dengan
launch
, tetapi mengembalikan
deferred
(entitas Kotlin setara dengan
Future
), sehingga hasilnya dapat diperoleh menggunakan
await()
. Ketika dipanggil tanpa parameter, ia berfungsi dalam konteks default untuk lingkup saat ini.
Sekali lagi, utas utama tetap bebas sementara kami menunggu 2 nilai kami.
Seperti yang Anda lihat, fungsi
launch
mengembalikan
Job
, yang dapat digunakan untuk menunggu sampai operasi selesai - ini dilakukan menggunakan fungsi
join()
. Ini bekerja seperti dalam bahasa lain, dengan peringatan bahwa itu hanya
menunda coroutine, dan tidak menghalangi aliran .
PengirimanPengiriman adalah konsep kunci ketika bekerja dengan coroutine. Tindakan ini memungkinkan Anda untuk "melompat" dari satu utas ke utas lainnya.
Pertimbangkan seperti apa persamaan untuk pengiriman di
Main
seperti di java, yaitu,
runOnUiThread: public final void runOnUiThread(Runnable action) { if (Thread.currentThread() != mUiThread) { mHandler.post(action);
Implementasi Konteks
Main
untuk Android adalah dispatcher berbasis
Handler
. Jadi ini memang implementasi yang sangat cocok:
launch(Dispatchers.Main) { ... } vs launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED) { ... }
launch(Dispatchers.Main)
mengirimkan
Runnable
ke
Handler
, sehingga kodenya tidak segera dieksekusi.
launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED)
akan segera menjalankan ekspresi lambda di utas saat ini.
Dispatchers.Main
memastikan bahwa ketika coroutine melanjutkan bekerja, itu akan diarahkan ke utas utama ; selain itu, Handler digunakan di sini sebagai implementasi Android asli untuk mengirim ke loop acara aplikasi.
Implementasi yang tepat terlihat seperti ini:
val Main: HandlerDispatcher = HandlerContext(mainHandler, "Main")
Berikut ini adalah artikel yang bagus untuk membantu Anda memahami seluk-beluk pengiriman di Android:
Memahami Android Core: Looper, Handler, dan HandlerThread .
Konteks CoroutineKonteks coroutine (juga dikenal sebagai manajer coroutine) menentukan di mana utas kode akan dieksekusi, apa yang harus dilakukan jika pengecualian dilemparkan, dan merujuk ke konteks induk untuk menyebarkan pembatalan.
val job = Job() val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> whatever(throwable) } launch(Disaptchers.Default+exceptionHandler+job) { ... }
job.cancel()
akan membatalkan semua coroutine yang orang tuanya adalah
job
. Pengecualian Handler akan menerima semua pengecualian yang dilemparkan ke coroutine ini.
LingkupAntarmuka
coroutineScope
menyederhanakan penanganan kesalahan:
Jika salah satu coroutine putrinya gagal, maka seluruh ruang lingkup dan semua coroutine anak juga akan dibatalkan.
Dalam contoh
async
, jika tidak mungkin untuk mengekstraksi nilai, sementara tugas lain terus bekerja, kami memiliki keadaan yang rusak, dan kami perlu melakukan sesuatu tentang hal itu.
Saat bekerja dengan
coroutineScope
, fungsi
useValues
akan dipanggil hanya jika ekstraksi kedua nilai berhasil. Juga, jika
deferred2
gagal,
deferred1
akan dibatalkan.
coroutineScope { val deferred1 = async(Dispatchers.Default) { getFirstValue() } val deferred2 = async(Dispatchers.IO) { getSecondValue() } useValues(deferred1.await(), deferred2.await()) }
Anda juga dapat “memasukkan ruang lingkup” seluruh kelas untuk mengatur
CoroutineContext
default untuk itu dan menggunakannya.
Kelas contoh yang mengimplementasikan antarmuka
CoroutineScope
:
open class ScopedViewModel : ViewModel(), CoroutineScope { protected val job = Job() override val coroutineContext = Dispatchers.Main+job override fun onCleared() { super.onCleared() job.cancel() } }
Menjalankan Corutin di
CoroutineScope
:
launch
default atau manajer
async
sekarang menjadi manajer lingkup saat ini.
launch { val foo = withContext(Dispatchers.IO) { … }
Peluncuran otonom coroutine (di luar CoroutineScope apa pun):
GlobalScope.launch(Dispatchers.Main) {
Anda bahkan dapat menentukan ruang lingkup aplikasi dengan menetapkan operator
Main
:
object AppScope : CoroutineScope by GlobalScope { override val coroutineContext = Dispatchers.Main.immediate }
Komentar- Coroutines membatasi interoperabilitas dengan Java
- Batasi mutabilitas untuk menghindari kunci
- Coroutine dirancang untuk menunggu, bukan untuk mengatur utas
- Hindari I / O di
Dispatchers.Default
(dan Main
...) - inilah gunanya Dispatchers.IO - Streaming memakan sumber daya, sehingga konteks single-threaded digunakan
Dispatchers.Default
didasarkan pada ForkJoinPool
, diperkenalkan di Android 5+- Coroutine dapat digunakan melalui saluran
Menyingkirkan kunci dan panggilan balik menggunakan saluranDefinisi saluran dari dokumentasi JetBrains:
Saluran Channel
konseptual sangat mirip dengan BlockingQueue
. Perbedaan utama adalah bahwa ia tidak memblokir operasi put, ia menyediakan untuk send
ditangguhkan (atau penawaran yang tidak menghalangi), dan alih-alih memblokir operasi take, ia menyediakan untuk receive
penundaan.
AktorPertimbangkan alat sederhana untuk bekerja dengan saluran:
Actor
.
Actor
, sekali lagi, sangat mirip dengan
Handler
: kita mendefinisikan konteks coroutine (yaitu, thread di mana kita akan melakukan tindakan) dan bekerja dengannya secara berurutan.
Perbedaannya, tentu saja, adalah bahwa corutin digunakan di sini;
Anda dapat menentukan kekuatan, dan kode yang dijalankan - jeda .
Pada prinsipnya,
actor
akan mengarahkan ulang perintah apa pun ke saluran coroutine. Ini
menjamin eksekusi perintah dan membatasi operasi dalam konteksnya . Pendekatan ini sangat membantu menghilangkan
synchronize
panggilan dan menjaga semua utas gratis!
protected val updateActor by lazy { actor<Update>(capacity = Channel.UNLIMITED) { for (update in channel) when (update) { Refresh -> updateList() is Filter -> filter.filter(update.query) is MediaUpdate -> updateItems(update.mediaList as List<T>) is MediaAddition -> addMedia(update.media as T) is MediaListAddition -> addMedia(update.mediaList as List<T>) is MediaRemoval -> removeMedia(update.media as T) } } }
Dalam contoh ini, kami menggunakan kelas Kotlin tersegel, memilih tindakan mana yang harus dilakukan.
sealed class Update object Refresh : Update() class Filter(val query: String?) : Update() class MediaAddition(val media: Media) : Update()
Selain itu, semua tindakan ini akan antri, mereka tidak akan pernah dieksekusi secara paralel. Ini adalah cara yang mudah untuk mencapai
batas variabilitas .
Android Life Cycle + CoroutinesAktor juga dapat sangat berguna untuk mengendalikan antarmuka pengguna Android, menyederhanakan pembatalan tugas, dan mencegah kelebihan utas.
Mari kita implementasikan ini dan panggil
job.cancel()
ketika aktivitas dihancurkan.
class MyActivity : AppCompatActivity(), CoroutineScope { protected val job = SupervisorJob()
Kelas
SupervisorJob
mirip dengan
Job
reguler dengan satu pengecualian bahwa pembatalan hanya meluas ke arah hilir.
Karenanya, kami tidak membatalkan semua coroutine dalam suatu
Activity
ketika salah satu dari mereka gagal.
Hal-hal sedikit lebih baik dengan
fungsi ekstensi yang memungkinkan Anda untuk mengakses
CoroutineContext
ini dari
View
apa pun di
CoroutineScope
.
val View.coroutineContext: CoroutineContext? get() = (context as? CoroutineScope)?.coroutineContext
Sekarang kita bisa menggabungkan semua ini, fungsi
setOnClick
membuat aktor gabungan untuk mengontrol tindakan
onClick
-nya. Dalam hal beberapa ketukan, tindakan lanjutan akan diabaikan, sehingga menghilangkan kesalahan PPA (aplikasi tidak merespons), dan tindakan ini akan dilakukan dalam lingkup
Activity
. Karena itu, ketika aktivitas dihancurkan, semua ini akan dibatalkan.
fun View.setOnClick(action: suspend () -> Unit) {
Dalam contoh ini, kami mengatur
Channel
ke
Conflated
sehingga mengabaikan beberapa peristiwa jika ada terlalu banyak. Anda dapat menggantinya dengan
Channel.UNLIMITED
jika Anda lebih memilih untuk mengantri acara tanpa kehilangan salah satunya, tetapi tetap ingin melindungi aplikasi dari kesalahan ANR.
Anda juga dapat menggabungkan kerangka kerja coroutine dan Siklus Hidup untuk mengotomatiskan pembatalan tugas yang terkait dengan antarmuka pengguna:
val LifecycleOwner.untilDestroy: Job get() { val job = Job() lifecycle.addObserver(object: LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) fun onDestroy() { job.cancel() } }) return job }
Sederhanakan situasi dengan panggilan balik (bagian 1)Berikut cara mengubah penggunaan API berbasis panggilan balik dengan
Channel
.
API berfungsi seperti ini:
requestBrowsing(url, listener)
mem-parsing folder yang terletak di url
.listener
menerima onMediaAdded(media: Media)
untuk semua file media yang ditemukan di folder ini.listener.onBrowseEnd()
dipanggil saat parsing folder
Ini adalah fungsi
refresh
lama di penyedia konten untuk browser VLC:
private val refreshList = mutableListOf<Media>() fun refresh() = requestBrowsing(url, refreshListener) private val refreshListener = object : EventListener{ override fun onMediaAdded(media: Media) { refreshList.add(media)) } override fun onBrowseEnd() { val list = refreshList.toMutableList() refreshList.clear() launch { dataset.value = list parseSubDirectories() } } }
Bagaimana cara memperbaikinya?Buat saluran yang akan dijalankan dalam
refresh
. Sekarang panggilan balik browser hanya akan mengarahkan media ke saluran ini, dan kemudian menutupnya.
Sekarang fungsi
refresh
menjadi lebih jelas. Dia membuat saluran, memanggil browser VLC, lalu membentuk daftar file media dan memprosesnya.
Alih-alih
select
atau
consumeEach
Anda dapat menggunakan
for
menunggu media, dan loop ini akan pecah segera setelah
browserChannel
ditutup.
private lateinit var browserChannel : Channel<Media> override fun onMediaAdded(media: Media) { browserChannel.offer(media) } override fun onBrowseEnd() { browserChannel.close() } suspend fun refresh() { browserChannel = Channel(Channel.UNLIMITED) val refreshList = mutableListOf<Media>() requestBrowsing(url)
Menyederhanakan situasi dengan callback (bagian 2): RetrofitPendekatan kedua: kami sama sekali tidak menggunakan kotlinx coroutine, tetapi kami menggunakan kerangka inti coroutine.
Lihat bagaimana sebenarnya coroutine bekerja!
Fungsi
retrofitSuspendCall
membungkus permintaan
Retrofit Call
untuk menjadikannya fungsi
suspend
.
Menggunakan
suspendCoroutine
kami memanggil metode
Call.enqueue
dan menjeda coroutine. Callback yang disediakan dengan cara ini akan memanggil
continuation.resume(response)
untuk melanjutkan coroutine dengan respons dari server segera setelah diterima.
Selanjutnya, kita hanya perlu menggabungkan fungsi Retrofit kita ke
retrofitSuspendCall
untuk mengembalikan hasil kueri menggunakannya.
suspend inline fun <reified T> retrofitSuspendCall(request: () -> Call <T> ) : Response <T> = suspendCoroutine { continuation -> request.invoke().enqueue(object : Callback<T> { override fun onResponse(call: Call<T>, response: Response<T>) { continuation.resume(response) } override fun onFailure(call: Call<T>, t: Throwable) { continuation.resumeWithException(t) } }) } suspend fun browse(path: String?) = retrofitSuspendCall { ApiClient.browse(path) }
Jadi, panggilan yang memblokir jaringan dibuat dalam utas Retrofit khusus, coroutine ada di sini, menunggu respons dari server, dan tidak ada tempat untuk menggunakannya dalam aplikasi!
Implementasi ini terinspirasi oleh
perpustakaan gildor / kotlin-coroutines-retrofit .
Ada juga
JakeWharton / retrofit2-kotlin-coroutines-adapter dengan implementasi lain yang memberikan hasil yang serupa.
EpilogChannel
dapat digunakan dengan banyak cara lain; Lihat
BroadcastChannel untuk implementasi yang lebih kuat yang mungkin berguna bagi Anda.
Anda juga dapat membuat saluran menggunakan fungsi
Produce .
Akhirnya, menggunakan saluran akan lebih mudah untuk mengatur komunikasi antara komponen-komponen UI: adaptor dapat mengirimkan peristiwa klik ke fragmen / aktivitasnya melalui
Channel
atau, misalnya, melalui
Actor
.