Coroutine adalah alat yang ampuh untuk eksekusi kode asinkron. Mereka bekerja secara paralel, berkomunikasi satu sama lain, dan menghabiskan sedikit sumber daya. Tampaknya tanpa rasa takut, coroutine dapat dimasukkan ke dalam produksi. Tapi ada ketakutan dan mereka ikut campur.
Laporan Vladimir Ivanov tentang
AppsConf adalah tentang fakta bahwa iblis tidak begitu mengerikan dan Anda dapat menggunakan coroutine sekarang:
Tentang pembicara : Vladimir Ivanov (
dzigoro ) adalah pengembang Android terkemuka di
EPAM dengan pengalaman 7 tahun, sangat menyukai Arsitektur Solusi, React Native dan pengembangan iOS, dan juga memiliki sertifikasi
Google Cloud Architect .
Semua yang Anda baca adalah produk dari pengalaman produksi dan berbagai studi, jadi anggaplah apa adanya, tanpa jaminan apa pun.
Coroutines, Kotlin dan RxJava
Sebagai informasi: status corutin saat ini dalam rilis, kiri Beta.
Kotlin 1.3 dirilis, coroutine dinyatakan stabil dan ada perdamaian di dunia.

Baru-baru ini saya melakukan survei di Twitter bahwa orang-orang menggunakan coroutine:
- 13% coroutine dalam makanan. Semuanya baik-baik saja;
- 25% mencobanya di proyek hewan peliharaan;
- 24% - Apa Kotlin?
- Sebagian besar 38% RxJava ada di mana-mana.
Statistik tidak senang. Saya percaya bahwa
RxJava adalah alat yang terlalu kompleks untuk tugas-tugas yang biasa digunakan oleh pengembang. Coroutine lebih cocok untuk mengendalikan operasi asinkron.
Dalam laporan saya sebelumnya, saya berbicara tentang cara refactor dari RxJava ke coroutine di Kotlin, jadi saya tidak akan membahas hal ini secara rinci, tetapi hanya mengingat poin-poin utama.
Mengapa kita menggunakan coroutine?
Karena jika kita menggunakan RxJava, maka contoh implementasi yang biasa terlihat seperti ini:
interface ApiClientRx { fun login(auth: Authorization) : Single<GithubUser> fun getRepositories (reposUrl: String, auth: Authorization) : Single<List<GithubRepository>> }
Kami memiliki antarmuka, misalnya, kami menulis klien GitHub dan ingin melakukan beberapa operasi untuk itu:
- Pengguna login.
- Dapatkan daftar repositori GitHub.
Dalam kedua kasus, fungsi akan mengembalikan objek bisnis tunggal: GitHubUser atau daftar GitHubRepository.
Kode implementasi untuk antarmuka ini adalah sebagai berikut:
private fun attemptLoginRx () { showProgress(true) compositeDisposable.add(apiClient.login(auth) .flatMap { user -> apiClient.getRepositories(user.repos_url, auth) } .map { list -> list.map { it.full_name } } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doFinally { showProgress(false) } .subscribe( { list -> showRepositories(this, list) }, { error -> Log.e("TAG", "Failed to show repos", error) } )) }
- Kami menggunakan
compositeDisposable sehingga tidak ada kebocoran memori.
- Tambahkan panggilan ke metode pertama.
- Kami menggunakan operator yang nyaman untuk mendapatkan pengguna, misalnya
flatMap .
- Kami mendapat daftar tempat penyimpanannya.
- Kami menulis
Boilerplate sehingga
berjalan di utas kanan.
- Ketika semuanya sudah siap, kami menampilkan daftar repositori untuk pengguna yang login.
Kesulitan Kode RxJava:- Kompleksitas Menurut pendapat saya, kode ini terlalu rumit untuk tugas sederhana dari dua panggilan jaringan dan menampilkan sesuatu pada UI .
- Jejak tumpukan tidak terikat. Jejak tumpukan hampir tidak terkait dengan kode yang Anda tulis.
- Menguasai sumber daya . RxJava menghasilkan banyak objek di bawah tenda dan kinerja dapat menurun.
Apa yang akan menjadi kode yang sama dengan coroutine hingga versi 0.26?Pada 0,26, API telah berubah, dan kita berbicara tentang produksi. Belum ada yang berhasil menerapkan 0,26 di prod, tetapi kami sedang mengusahakannya.
Dengan coroutine, antarmuka kita akan berubah cukup signifikan . Fungsi akan berhenti mengembalikan Singles dan objek pembantu lainnya. Mereka akan segera mengembalikan objek bisnis: GitHubUser dan daftar GitHubRepository. Fungsi GitHubUser dan GitHubRepository akan
menangguhkan pengubah. Ini bagus, karena menunda hampir tidak mengharuskan kita untuk apa pun:
interface ApiClient { suspend fun login(auth: Authorization) : GithubUser suspend fun getRepositories (reposUrl: String, auth: Authorization) : List<GithubRepository> }
Jika Anda melihat kode yang sudah menggunakan implementasi antarmuka ini, itu akan berubah secara signifikan dibandingkan dengan RxJava:
private fun attemptLogin () { launch(UI) { val auth = BasicAuthorization(login, pass) try { showProgress(true) val userlnfo = async { apiClient.login(auth) }.await() val repoUrl = userlnfo.repos_url val list = async { apiClient.getRepositories(repoUrl, auth) }.await() showRepositories( this, list.map { it -> it.full_name } ) } catch (e: RuntimeException) { showToast("Oops!") } finally { showProgress(false) } } }
- Tindakan utama terjadi di mana kita memanggil
async buildout coroutine , menunggu respons dan dapatkan
userlnfo .
- Kami menggunakan data dari objek ini.
- Lakukan panggilan
async lain dan panggilan
tunggu .
Semuanya tampak seolah-olah tidak ada pekerjaan asinkron terjadi, dan kami hanya menulis perintah di kolom dan dieksekusi. Pada akhirnya, kami melakukan apa yang perlu dilakukan di UI.
Mengapa coroutine lebih baik?- Kode ini lebih mudah dibaca. Ini ditulis seolah-olah konsisten.
- Kemungkinan besar kinerja kode ini lebih baik daripada di RxJava.
- Sangat mudah untuk menulis tes, tetapi kita akan mendapatkannya nanti.
2 langkah ke samping
Mari ngelantur sedikit, ada beberapa hal yang masih perlu dibahas.
Langkah 1. withContext vs launch / async
Selain
coroutine builder async, ada
coroutine builder withContext .
Luncurkan atau
async buat
konteks Coroutine baru, yang tidak selalu diperlukan. Jika Anda memiliki konteks Coroutine yang ingin Anda gunakan di seluruh aplikasi, maka Anda tidak perlu membuatnya kembali. Anda cukup menggunakan kembali yang sudah ada. Untuk melakukan ini, Anda memerlukan pembuat coroutine withContext. Itu hanya menggunakan kembali konteks Coroutine yang ada. Ini akan menjadi 2-3 kali lebih cepat, tetapi sekarang ini adalah pertanyaan yang tidak berprinsip. Jika angka pastinya menarik, maka
inilah pertanyaan tentang
stackoverflow dengan tolok ukur dan detail.
Aturan umum: Gunakan withContext tanpa ragu di mana itu cocok secara semantik. Tetapi jika Anda membutuhkan pemuatan paralel, misalnya beberapa gambar atau potongan data, maka async / tunggu adalah pilihan Anda.
Langkah 2. Refactoring
Bagaimana jika Anda memperbaiki rantai RxJava yang sangat kompleks? Saya menemukan ini dalam produksi:
observable1.getSubject().zipWith(observable2.getSubject(), (t1, t2) -> {
Saya memiliki rantai rumit dengan
subjek publik , dengan
zip dan
efek samping di setiap
ritsleting yang mengirim sesuatu yang lain ke bus acara. Tugas paling tidak adalah menyingkirkan bus acara. Saya duduk selama sehari, tetapi tidak bisa memperbaiki kode untuk menyelesaikan masalah.
Keputusan yang tepat ternyata membuang semuanya dan menulis ulang kode pada coroutine dalam 4 jam .
Kode di bawah ini sangat mirip dengan yang saya dapatkan:
try { val firstChunkJob = async { call1 } val secondChunkJob = async { call2 } val thirdChunkJob = async { call3 } return Result( firstChunkJob.await(), secondChunkJob.await(), thirdChunkJob.await()) } catch (e: Exception) {
- Kami melakukan async untuk satu tugas, untuk yang kedua dan ketiga.
- Kami menunggu hasilnya dan memasukkan semuanya ke dalam objek.
- Selesai!
Jika Anda memiliki rantai yang rumit dan ada coroutine, maka cukup refactor saja. Ini sangat cepat.
Apa yang mencegah pengembang menggunakan coroutine dalam produk?
Menurut pendapat saya, kami, sebagai pengembang, saat ini dicegah menggunakan coroutine hanya karena takut akan sesuatu yang baru:
- Kita tidak tahu apa yang harus dilakukan dengan siklus hidup , aktivitas , dan siklus hidup fragmen. Bagaimana cara bekerja dengan coroutine dalam kasus ini?
- Tidak ada pengalaman dalam menyelesaikan tugas-tugas kompleks harian dalam produksi menggunakan corutin.
- Alat tidak cukup. Banyak perpustakaan dan fungsi telah ditulis untuk RxJava. Misalnya RxFCM . RxJava sendiri memiliki banyak operator, yang bagus, tetapi bagaimana dengan coroutine?
- Kami tidak benar-benar mengerti cara menguji coroutine.
Jika kita menghilangkan empat ketakutan ini, kita dapat tidur dengan tenang di malam hari dan menggunakan coroutine dalam produksi.
Mari poin demi poin.
1. Manajemen siklus hidup
- Coroutine dapat bocor sekali pakai atau AsyncTask . Masalah ini harus diselesaikan secara manual.
- Untuk menghindari Pengecualian Null Pointer acak , coroutine harus dihentikan.
Berhenti
Apakah Anda terbiasa dengan
Thread.stop () ? Jika Anda menggunakannya, maka tidak lama. Di
JDK 1.1, metode ini segera dinyatakan usang, karena tidak mungkin untuk mengambil dan menghentikan kode tertentu dan tidak ada jaminan bahwa itu akan selesai dengan benar. Kemungkinan besar Anda hanya akan mendapatkan
kerusakan memori .
Karenanya,
Thread.stop () tidak berfungsi . Anda perlu pembatalan agar kooperatif, yaitu kode di sisi lain untuk mengetahui bahwa Anda membatalkannya.
Bagaimana kami menerapkan pemberhentian dengan RxJava:
private val compositeDisposable = CompositeDisposable() fun requestSmth() { compositeDisposable.add( apiClientRx.requestSomething() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(result -> {}) } override fun onDestroy() { compositeDisposable.dispose() }
Dalam RxJava kita
menggunakan CompositeDisposable .
- Tambahkan variable
compositeDisposable ke aktivitas di fragmen atau di presenter, di mana kita menggunakan RxJava.
- Dalam
onDestro tambahkan
Buang dan semua pengecualian hilang dengan sendirinya.
Kira-kira prinsipnya sama dengan coroutine:
private val job: Job? = null fun requestSmth() { job = launch(UI) { val user = apiClient.requestSomething() β¦ } } override fun onDestroy() { job?.cancel() }
Pertimbangkan contoh
tugas sederhana .
Biasanya,
pembangun coroutine mengembalikan
pekerjaan , dan dalam beberapa kasus
ditangguhkan .
- Kita bisa mengingat pekerjaan ini.
- Berikan perintah
"luncurkan" pembangun coroutine . Proses dimulai, sesuatu terjadi, hasil eksekusi diingat.
- Jika kami tidak meneruskan hal lain, maka "luncurkan" memulai fungsinya dan mengembalikan kami tautan ke pekerjaan itu.
- Pekerjaan diingat, dan dalam onDestroy kita katakan
"batal" dan semuanya bekerja dengan baik.
Apa masalah dari pendekatan ini? Setiap pekerjaan membutuhkan bidang. Anda perlu mempertahankan daftar pekerjaan untuk membatalkan semuanya. Pendekatan ini mengarah pada duplikasi kode, jangan lakukan itu.
Berita baiknya adalah kita memiliki
alternatif : pekerjaan
CompositeJob dan
Lifecycle-aware .
CompositeJob adalah analog dari compositeDisposable. Itu terlihat seperti ini
: private val job: CompositeJob = CompositeJob() fun requestSmth() { job.add(launch(UI) { val user = apiClient.requestSomething() ... }) } override fun onDestroy() { job.cancel() }
- Untuk satu fragmen kami memulai satu pekerjaan.
- Kami menempatkan semua
pekerjaan di CompositeJob dan memberikan perintah:
"job.cancel () untuk semua orang!" .
Pendekatan ini mudah diimplementasikan dalam 4 baris, tidak termasuk deklarasi kelas:
Class CompositeJob { private val map = hashMapOf<String, Job>() fun add(job: Job, key: String = job.hashCode().toString()) = map.put(key, job)?.cancel() fun cancel(key: String) = map[key]?.cancel() fun cancel() = map.forEach { _ ,u -> u.cancel() } }
Anda akan membutuhkan:
-
peta dengan kunci string,
-
tambahkan metode, di mana Anda akan menambahkan pekerjaan,
- parameter
kunci opsional.
Jika Anda ingin menggunakan kunci yang sama untuk pekerjaan yang sama - silakan. Jika tidak, maka
hashCode akan menyelesaikan masalah kita. Tambahkan pekerjaan ke peta, yang kami lewati, dan batalkan yang sebelumnya dengan kunci yang sama. Jika kami memenuhi tugas secara berlebihan, maka hasil sebelumnya tidak menarik bagi kami. Kami membatalkannya dan mengendarainya lagi.
Batalkan itu sederhana: kami mendapatkan pekerjaan dengan kunci dan membatalkan. Pembatalan kedua untuk seluruh peta membatalkan semuanya. Semua kode ditulis dalam setengah jam dalam empat baris dan berfungsi. Jika Anda tidak ingin menulis, ambil contoh di atas.
Pekerjaan sadar siklus
Sudahkah Anda menggunakan
Siklus Hidup Android ,
pemilik atau
pengamat Siklus Hidup ?
Aktivitas dan
fragmen kami memiliki status tertentu. Sorotan:
dibuat, dimulai dan
dilanjutkan . Ada transisi yang berbeda antar negara.
LifecycleObserver memungkinkan Anda untuk berlangganan transisi ini dan melakukan sesuatu ketika salah satu transisi terjadi.
Ini terlihat sangat sederhana:
public class MyObserver implements LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) public void connectListener() { ... } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) public void disconnectListener() { β¦ } }
Anda menutup anotasi dengan beberapa parameter pada metode, dan disebut dengan transisi yang sesuai. Cukup gunakan pendekatan ini untuk coroutine:
class AndroidJob(lifecycle: Lifecycle) : Job by Job(), LifecycleObserver { init { lifecycle.addObserver(this) } @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) fun destroy() { Log.d("AndroidJob", "Cancelling a coroutine") cancel() } }
- Anda dapat menulis
AndroidJob kelas dasar.
- Kami akan mentransfer
Lifecycle ke kelas.
- Antarmuka
LifecycleObserver akan mengimplementasikan pekerjaan itu.
Yang kita butuhkan:
- Dalam konstruktor tambahkan ke Lifecycle sebagai Pengamat.
- Berlangganan
ON_DESTROY atau apa pun yang menarik minat kami.
- Buat pembatalan di ON_DESTROY.
-
Dapatkan satu
parentJob di fragmen Anda.
- Panggil
pekerjaan Joy konstruktor atau
siklus hidup fragmen aktivitas Anda. Tidak ada perbedaan.
- Lulus
parentJob ini sebagai
orangtua .
Kode yang sudah selesai terlihat seperti ini:
private var parentJob = AndroidJob(lifecycle) fun do() { job = launch(UI, parent = parentJob) {
Ketika Anda membatalkan orang tua, semua coroutine anak dibatalkan dan Anda tidak perlu lagi menulis apa pun di fragmen. Semuanya terjadi secara otomatis, tidak ada lagi ON_DESTROY. Yang utama jangan lupa lewat
parent = parentJob .
Jika Anda menggunakan, Anda dapat menulis aturan serat sederhana yang akan menyoroti Anda: "Oh, Anda lupa orang tua Anda!"
Dengan
Manajemen siklus hidup beres. Kami memiliki beberapa alat yang memungkinkan Anda melakukan ini dengan mudah dan nyaman.
Bagaimana dengan skenario kompleks dan tugas-tugas non-sepele dalam produksi?
2. Kasus penggunaan yang kompleks
Skenario kompleks dan tugas non-sepele adalah:
-
Operator - operator kompleks di RxJava: flatMap, debounce, dll.
-
Penanganan kesalahan - penanganan kesalahan yang kompleks. Bukan hanya
coba..tangkap , tapi bersarang misalnya.
-
Caching adalah tugas yang tidak sepele. Dalam produksi, kami mengalami cache dan ingin mendapatkan alat untuk dengan mudah menyelesaikan masalah caching dengan coroutine.
Ulangi
Ketika kami memikirkan operator untuk coroutine, opsi pertama adalah
repeatWhen () .
Jika ada yang tidak beres dan Corutin tidak dapat menjangkau server di dalam, maka kami ingin mencoba lagi beberapa kali dengan semacam fallon eksponensial. Mungkin alasannya adalah koneksi yang buruk dan kami akan mendapatkan hasil yang diinginkan dengan mengulangi operasi beberapa kali.
Dengan coroutine, tugas ini mudah diimplementasikan:
suspend fun <T> retryDeferredWithDelay( deferred: () -> Deferred<T>, tries: Int = 3, timeDelay: Long = 1000L ): T { for (i in 1..tries) { try { return deferred().await() } catch (e: Exception) { if (i < tries) delay(timeDelay) else throw e } } throw UnsupportedOperationException() }
Implementasi operator:
- Dia mengambil
Ditangguhkan .
- Anda harus memanggil
async untuk mendapatkan objek ini.
- Alih-alih
Ditangguhkan, Anda dapat melewati kedua blok penangguhan dan umumnya semua
fungsi penangguhan.- Loop
for - Anda sedang menunggu hasil coroutine Anda. Jika sesuatu terjadi dan penghitung pengulangan tidak habis, coba lagi melalui
Penundaan . Jika tidak, maka tidak.
Fungsi ini dapat dengan mudah disesuaikan: menempatkan Delay eksponensial atau meneruskan fungsi lambda yang akan menghitung Delay tergantung pada keadaan.
Gunakan, itu berhasil!
Ritsleting
Kami juga sering menjumpai mereka. Di sini sekali lagi, semuanya sederhana:
suspend fun <T1, T2, R> zip( source1: Deferred<T1>, source2: Deferred<T2>, zipper: BiFunction<T1, T2, R>): R { return zipper.apply(sourcel.await(), source2.await()) } suspend fun <T1, T2, R> Deferred<T1>.zipWith( other: Deferred<T2>, zipper: BiFunction<T1, T2, R>): R { return zip(this, other, zipper) }
- Gunakan
ritsleting dan panggilan menunggu ditangguhkan Anda.
- Alih-alih Ditangguhkan, Anda dapat menggunakan fungsi menangguhkan dan pembangun coroutine dengan withContext. Anda akan menyampaikan konteks yang Anda butuhkan.
Ini kembali berfungsi dan saya harap saya menghilangkan rasa takut ini.
Cache
Apakah Anda memiliki implementasi cache dalam produksi dengan RxJava? Kami menggunakan RxCache.

Di diagram di sebelah kiri:
View and
ViewModel . Di sebelah kanan adalah sumber data: panggilan jaringan dan database.
Jika kita ingin sesuatu di-cache, maka cache akan menjadi sumber data lain.
Jenis Cache:
- Sumber Jaringan untuk panggilan jaringan.
- Cache dalam memori .
- Cache persisten dengan kedaluwarsa untuk disimpan di disk sehingga cache bisa selamat dari restart aplikasi.
Mari kita tulis
cache sederhana dan primitif untuk kasus ketiga. Pembangun Coroutine withContext datang untuk menyelamatkan lagi.
launch(UI) { var data = withContext(dispatcher) { persistence.getData() } if (data == null) { data = withContext(dispatcher) { memory.getData() } if (data == null) { data = withContext(dispatcher) { network.getData() } memory.cache(url, data) persistence.cache(url, data) } } }
- Anda menjalankan setiap operasi dengan withContext dan melihat apakah ada data yang datang.
- Jika data dari
kegigihan tidak datang, maka Anda mencoba untuk mendapatkannya dari
memory.cache .
- Jika tidak ada memory.cache, maka hubungi
sumber jaringan dan dapatkan data Anda. Jangan lupa, tentu saja, untuk menaruh semua cache.
Ini adalah implementasi yang agak primitif dan ada banyak pertanyaan, tetapi metode ini berfungsi jika Anda memerlukan cache di satu tempat. Untuk tugas-tugas produksi, cache ini tidak cukup. Dibutuhkan sesuatu yang lebih rumit.
Rx memiliki RxCache
Bagi yang masih menggunakan RxJava, Anda bisa menggunakan RxCache. Kami masih menggunakannya juga.
RxCache adalah perpustakaan khusus. Memungkinkan Anda untuk menyimpan data dan mengatur siklus hidupnya.
Misalnya, Anda ingin mengatakan bahwa data ini akan kedaluwarsa setelah 15 menit: "Tolong, setelah periode waktu ini jangan mengirim data dari cache, tetapi kirimkan saya data baru."
Perpustakaannya luar biasa karena secara deklaratif mendukung tim. Deklarasi ini sangat mirip dengan apa yang Anda lakukan dengan
Retrofit :
public interface FeatureConfigCacheProvider { @ProviderKey("features") @LifeCache(duration = 15, timeUnit = TimeUnit.MINUTES) fun getFeatures( result: Observable<Features>, cacheName: DynamicKey ): Observable<Reply<Features>> }
- Anda mengatakan bahwa Anda memiliki
CacheProvider .
- Mulai metode dan katakan bahwa masa pakai
LifeCache adalah 15 menit. Kunci yang akan tersedia adalah
Fitur .
- Returns
Observable <Balas , di mana
Balas adalah objek perpustakaan bantu untuk bekerja dengan cache.
Penggunaannya cukup sederhana:
val restObservable = configServiceRestApi.getFeatures() val features = featureConfigCacheProvider.getFeatures( restObservable, DynamicKey(CACHE_KEY) )
- Dari cache Rx, akses
RestApi .
-
Beralih ke
CacheProvider .
- Beri dia sebuah Observable.
- Perpustakaan itu sendiri akan mencari tahu apa yang harus dilakukan: pergi ke cache atau tidak, jika waktu habis, beralih ke
Diamati dan melakukan operasi lain.
Menggunakan perpustakaan sangat mudah dan saya ingin mendapatkan yang serupa untuk coroutine.
Cache Coroutine dalam pengembangan
Di dalam EPAM, kami sedang menulis pustaka
Coroutine Cache , yang akan melakukan semua fungsi RxCache. Kami menulis versi pertama dan menjalankannya di dalam perusahaan. Segera setelah rilis pertama keluar, saya akan dengan senang hati mempostingnya di Twitter saya. Ini akan terlihat seperti ini:
val restFunction = configServiceRestApi.getFeatures() val features = withCache(CACHE_KEY) { restFunction() }
Kami akan memiliki fungsi penangguhan
getFeatures . Kami akan meneruskan fungsi sebagai blok ke fungsi tingkat tinggi khusus
denganCache , yang akan
mencari tahu apa yang perlu dilakukan.
Mungkin kita akan membuat antarmuka yang sama untuk mendukung fungsi deklaratif.
Menangani kesalahan

Penanganan kesalahan sederhana sering ditemukan oleh pengembang dan biasanya diselesaikan dengan cukup sederhana. Jika Anda tidak memiliki hal-hal yang rumit, maka Anda dapat menangkap
pengecualian dan melihat apa yang terjadi di sana, menulis ke log atau menunjukkan kesalahan kepada pengguna. Di UI, Anda dapat dengan mudah melakukan ini.
Dalam kasus sederhana, semuanya diharapkan sederhana - penanganan kesalahan dengan coroutine dilakukan melalui
try-catch-akhirnya .
Dalam produksi, selain kasus-kasus sederhana, ada:
-
Mencoba- bersarang,
- Berbagai macam
pengecualian ,
- Kesalahan dalam jaringan atau dalam logika bisnis,
- Kesalahan pengguna. Dia kembali melakukan kesalahan dan harus disalahkan atas segalanya.
Kita harus siap untuk ini.
Ada 2 solusi:
CoroutineExceptionHandler dan pendekatan dengan
kelas Hasil .
Penangan Pengecualian Coroutine
Ini adalah kelas khusus untuk menangani kasus kesalahan yang kompleks.
ExceptionHandler memungkinkan Anda untuk menganggap
Exception sebagai suatu kesalahan dan menanganinya.
Bagaimana biasanya kita menangani kesalahan kompleks?
Pengguna menekan sesuatu, tombolnya tidak bekerja. Dia perlu mengatakan apa yang salah dan mengarahkannya ke tindakan tertentu: periksa Internet, Wi-Fi, coba nanti atau hapus aplikasi dan jangan pernah menggunakannya lagi. Mengatakan ini kepada pengguna bisa sangat sederhana:
val handler = CoroutineExceptionHandler(handler = { , error -> hideProgressDialog() val defaultErrorMsg = "Something went wrong" val errorMsg = when (error) { is ConnectionException -> userFriendlyErrorMessage(error, defaultErrorMsg) is HttpResponseException -> userFriendlyErrorMessage(Endpoint.EndpointType.ENDPOINT_SYNCPLICITY, error) is EncodingException -> "Failed to decode data, please try again" else -> defaultErrorMsg } Toast.makeText(context, errorMsg, Toast.LENGTH_SHORT).show() })
- Mari kita pesan default: "Ada yang salah!" dan menganalisis pengecualian.
- Jika ini adalah
ConnectionException, maka kami mengambil pesan lokal dari sumber daya: βMan, nyalakan Wi-Fi dan masalah Anda akan hilang. Saya jamin itu. "
- Jika
server mengatakan sesuatu yang salah , maka Anda harus memberi tahu klien: "Keluar dan masuk lagi", atau "Jangan lakukan ini di Moskow, lakukan di negara lain", atau "Maaf, kawan. Yang bisa saya lakukan hanyalah mengatakan ada yang tidak beres. β
- Jika ini adalah
kesalahan yang sama sekali
berbeda , misalnya,
kehabisan memori , kami mengatakan: "Ada yang salah, maafkan aku."
- Semua pesan ditampilkan.
Apa yang Anda tulis ke
CoroutineExceptionHandler akan dieksekusi pada
Dispatcher yang sama di mana Anda menjalankan coroutine. Karena itu, jika Anda memberikan perintah "luncurkan" UI, maka semuanya terjadi pada UI. Anda tidak perlu
pengiriman terpisah
, yang sangat nyaman.
Gunakan sederhana:
launch(uiDispatcher + handler) { ... }
Ada operator
plus . Dalam konteks Coroutine, tambahkan
handler dan semuanya berfungsi, yang sangat nyaman. Kami menggunakan ini untuk sementara waktu.
Kelas hasil
Kemudian kami menyadari bahwa CoroutineExceptionHandler mungkin hilang. Hasilnya, yang dibentuk oleh karya coroutine, dapat terdiri dari beberapa data, dari bagian yang berbeda atau proses beberapa situasi.
Pendekatan
kelas hasil membantu untuk mengatasi masalah ini:
sealed class Result { data class Success(val payload: String) : Result() data class Error(val exception: Exception) : Result() }
- Dalam logika bisnis Anda, Anda memulai
kelas Hasil .
- Tandai sebagai
disegel .
- Anda mewarisi dari kelas dua kelas data lainnya:
Sukses dan
Kesalahan .
β
Success , .
β
Error exception.
- :
override suspend fun doTask(): Result = withContext(CommonPool) { if ( !isSessionValidForTask() ) { return@withContext Result.Error(Exception()) } β¦ try { Result.Success(restApi.call()) } catch (e: Exception) { Result.Error(e) } }
Coroutine context β Coroutine builder withContex .
, :
β , error. .
β RestApi -.
β ,
Result.Success .
β ,
Result.Error .
- , ExceptionHandler .
Result classes , . Result classes, ExceptionHandler try-catch.
3.
, .
unit- , , . unit-.
, . , unit-, 2 :
- Replacing context . , ;
- Mocking coroutines . .
Replacing context
presenter:
val login() { launch(UI) { β¦ } }
,
login , UI-. , ,
. , , unit-.
:
val login (val coroutineContext = UI) { launch(coroutineContext) { ... } }
β login coroutineContext. , . Kotlin , UI .
β Coroutine builder Coroutine Contex, .
unit- :
fun testLogin() { val presenter = LoginPresenter () presenter.login(Unconfined) }
β
LoginPresenter login - , , Unconfined.
β
Unconfined , , . .
Mocking coroutines
β .
Mockk unit-. unit- Kotlin, . suspend-
coEvery -.
login
githubUser :
coEvery { apiClient.login(any()) } returns githubUser
Mockito-kotlin , β . , , :
given { runBlocking { apiClient.login(any()) } }.willReturn (githubUser)
runBlocking .
given- , .
Presenter :
fun testLogin() { val githubUser = GithubUser('login') val presenter = LoginPresenter(mockApi) presenter.login (Unconfined) assertEquals(githubUser, presenter.user()) }
β -, ,
GitHubUser .
β LoginPresenter API, . .
β
presenter.login Unconfined , Presenter , .
Dan itu saja! .
- Rx- . . , RxJava RxJava. - β , .
- . , . Unit- β , , , . β welcome!
- . , , , , . .
Tautan yang bermanfaat
Berita
30 Mail.ru . , .
AppsConf , .
, , , .
youtube- AppsConf 2018 β :)