Cara menggunakan coroutine dalam makanan dan tidur nyenyak di malam hari

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>> } //RxJava 2 implementation 

Kami memiliki antarmuka, misalnya, kami menulis klien GitHub dan ingin melakukan beberapa operasi untuk itu:

  1. Pengguna login.
  2. 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> } //Base interface 

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) -> { // side effects return true; }).doOnError { // handle errors } .zipWith(observable3.getSubject(), (t3, t4) -> { // side effects return true; }).doOnComplete { // gather data } .subscribe() 

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) { // handle errors } 

- 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) { // code } } 

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 :

  1. Replacing context . , ;
  2. 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 β€” :)

Source: https://habr.com/ru/post/id429908/


All Articles