(awalnya diterbitkan di
Medium )
Coroutine Kotlin jauh lebih dari sekadar benang ringan - mereka adalah paradigma baru yang membantu pengembang untuk berurusan dengan konkurensi dengan cara
terstruktur dan idiomatis.
Ketika mengembangkan aplikasi Android, seseorang harus mempertimbangkan banyak hal yang berbeda: mengambil operasi jangka panjang dari utas UI, menangani acara siklus hidup, membatalkan langganan, beralih kembali ke utas UI untuk memperbarui antarmuka pengguna. Dalam beberapa tahun terakhir RxJava menjadi salah satu kerangka kerja yang paling umum digunakan untuk menyelesaikan serangkaian masalah ini. Pada artikel ini saya akan memandu Anda melalui migrasi fitur end-to-end dari RxJava ke coroutine.
Fitur
Fitur yang akan kita konversi ke coroutine cukup sederhana: ketika pengguna mengirimkan suatu negara kita membuat panggilan API untuk memeriksa apakah negara tersebut memenuhi syarat untuk pencarian rincian bisnis melalui penyedia seperti
Companies House . Jika panggilan berhasil, kami akan menampilkan respons, jika tidak - pesan kesalahan.
Migrasi

Kita akan memigrasi kode kita dalam pendekatan bottom-up dimulai dengan layanan Retrofit, naik ke lapisan Repositori, lalu ke lapisan Interactor dan akhirnya ke ViewModel.
Fungsi yang saat ini kembali Tunggal harus menjadi fungsi yang ditangguhkan dan fungsi yang kembali Dapat diamati harus mengembalikan Aliran. Dalam contoh khusus ini kita tidak akan melakukan apa pun dengan Flows.
Layanan retrofit
Mari kita langsung masuk ke kode dan refactor metode businessLookupEligibility di BusinessLookupService ke coroutine. Seperti ini tampilannya sekarang.
interface BusinessLookupService { @GET("v1/eligibility") fun businessLookupEligibility( @Query("countryCode") countryCode: String ): Single<NetworkResponse<BusinessLookupEligibilityResponse, ErrorResponse>> }
Langkah-langkah refactoring:
- Dimulai dengan versi 2.6.0 Retrofit mendukung pengubah penundaan. Mari kita ubah metode businessLookupEligibility menjadi fungsi penangguhan.
- Hapus bungkus Tunggal dari jenis kembali.
interface BusinessLookupService { @GET("v1/eligibility") suspend fun businessLookupEligibility( @Query("countryCode") countryCode: String ): NetworkResponse<BusinessLookupEligibilityResponse, ErrorResponse> }
NetworkResponse adalah kelas tertutup yang mewakili BusinessLookupEligibilityResponse atau ErrorResponse. NetworkResponse dibangun dalam adaptor panggilan Retrofit kustom. Dengan cara ini kami membatasi aliran data hanya pada dua kasus yang mungkin - sukses atau kesalahan, sehingga konsumen BusinessLookupService tidak perlu khawatir tentang penanganan pengecualian.
Repositori
Mari kita beralih dan melihat apa yang kita miliki di BusinessLookupRepository. Dalam badan metode businessLookupEligibility kami memanggil businessLookupService.businessLookupEligibility (yang baru saja kami refactored) dan menggunakan operator peta RxJava untuk mengubah NetworkResponse menjadi Hasil dan memetakan model respons ke model domain. Hasil adalah kelas tertutup lain yang mewakili Result.Success dan berisi objekBusinessLookupEligibility jika panggilan jaringan berhasil. Jika ada kesalahan dalam panggilan jaringan, pengecualian deserialisasi atau sesuatu yang salah, kami membangun Hasil. Kegagalan dengan pesan kesalahan yang bermakna (ErrorMessage adalah
typealias untuk String).
class BusinessLookupRepository @Inject constructor( private val businessLookupService: BusinessLookupService, private val businessLookupApiToDomainMapper: BusinessLookupApiToDomainMapper, private val responseToString: Mapper, private val schedulerProvider: SchedulerProvider ) { fun businessLookupEligibility(countryCode: String): Single<Result<BusinessLookupEligibility, ErrorMessage>> { return businessLookupService.businessLookupEligibility(countryCode) .map { response -> return@map when (response) { is NetworkResponse.Success -> { val businessLookupEligibility = businessLookupApiToDomainMapper.map(response.body) Result.Success<BusinessLookupEligibility, ErrorMessage>(businessLookupEligibility) } is NetworkResponse.Error -> Result.Failure<BusinessLookupEligibility, ErrorMessage>( responseToString.transform(response) ) } }.subscribeOn(schedulerProvider.io()) } }
Langkah-langkah refactoring:
- businessLookupEligibility menjadi fungsi menangguhkan.
- Hapus bungkus Tunggal dari jenis kembali.
- Metode dalam repositori biasanya melakukan tugas yang sudah berjalan lama seperti panggilan jaringan atau permintaan db. Merupakan tanggung jawab repositori untuk menentukan di utas mana pekerjaan ini harus dilakukan. Dengan subscribeOn (schedulerProvider.io ()) kami memberi tahu RxJava bahwa pekerjaan harus dilakukan pada utas io. Bagaimana hal yang sama dapat dicapai dengan coroutine? Kita akan menggunakan withContext dengan dispatcher khusus untuk menggeser eksekusi blok ke utas yang berbeda dan kembali ke dispatcher asli ketika eksekusi selesai. Ini adalah praktik yang baik untuk memastikan bahwa suatu fungsi aman-utama dengan menggunakan withContext. Konsumen BusinessLookupRepository tidak boleh memikirkan thread mana yang harus mereka gunakan untuk menjalankan metode businessLookupEligibility, harus aman untuk menyebutnya dari utas utama.
- Kami tidak memerlukan operator peta lagi karena kami dapat menggunakan hasil businessLookupService.businessLookupEligibility di badan fungsi yang ditangguhkan.
class BusinessLookupRepository @Inject constructor( private val businessLookupService: BusinessLookupService, private val businessLookupApiToDomainMapper: BusinessLookupApiToDomainMapper, private val responseToString: Mapper, private val dispatcherProvider: DispatcherProvider ) { suspend fun businessLookupEligibility(countryCode: String): Result<BusinessLookupEligibility, ErrorMessage> = withContext(dispatcherProvider.io) { when (val response = businessLookupService.businessLookupEligibility(countryCode)) { is NetworkResponse.Success -> { val businessLookupEligibility = businessLookupApiToDomainMapper.map(response.body) Result.Success<BusinessLookupEligibility, ErrorMessage>(businessLookupEligibility) } is NetworkResponse.Error -> Result.Failure<BusinessLookupEligibility, ErrorMessage>( responseToString.transform(response) ) } } }
Interactractor
Dalam contoh khusus ini BusinessLookupEligibilityInteractor tidak mengandung logika tambahan dan berfungsi sebagai proksi ke BusinessLookupRepository. Kami menggunakan pemohon
berlebih dari operator sehingga interaksor dapat dipanggil sebagai fungsi.
class BusinessLookupEligibilityInteractor @Inject constructor( private val businessLookupRepository: BusinessLookupRepository ) { operator fun invoke(countryCode: String): Single<Result<BusinessLookupEligibility, ErrorMessage>> = businessLookupRepository.businessLookupEligibility(countryCode) }
Langkah-langkah refactoring:
- panggilan menyenangkan operator menjadi menunda panggilan menyenangkan operator.
- Hapus bungkus Tunggal dari jenis kembali.
class BusinessLookupEligibilityInteractor @Inject constructor( private val businessLookupRepository: BusinessLookupRepository ) { suspend operator fun invoke(countryCode: String): Result<BusinessLookupEligibility, ErrorMessage> = businessLookupRepository.businessLookupEligibility(countryCode) }
ViewModel
Di BusinessProfileViewModel kami menyebutnya BusinessLookupEligibilityInteractor yang mengembalikan Single. Kami berlangganan aliran dan mengamatinya di utas UI dengan menentukan penjadwal UI. Jika Berhasil, kami menetapkan nilai dari model domain ke LiveView data businessViewState. Dalam hal Kegagalan kami menetapkan pesan kesalahan.
Kami menambahkan setiap langganan ke CompositeDisposable dan membuangnya dalam metode onCleared () dari siklus hidup ViewModel.
class BusinessProfileViewModel @Inject constructor( private val businessLookupEligibilityInteractor: BusinessLookupEligibilityInteractor, private val schedulerProvider: SchedulerProvider ) : ViewModel() { private val disposables = CompositeDisposable() internal val businessViewState: MutableLiveData<ViewState> = LiveDataFactory.createDefault("Loading...") fun onCountrySubmit(country: Country) { disposables.add(businessLookupEligibilityInteractor(country.countryCode) .observeOn(schedulerProvider.ui()) .subscribe { state -> return@subscribe when (state) { is Result.Success -> businessViewState.value = state.entity.provider is Result.Failure -> businessViewState.value = state.failure } }) } @Override protected void onCleared() { super.onCleared(); disposables.clear(); } }
Langkah-langkah refactoring:
- Di awal artikel saya telah menyebutkan salah satu keunggulan utama coroutine - concurrency terstruktur. Dan di sinilah ia berperan. Setiap coroutine memiliki ruang lingkup. Ruang lingkup memiliki kendali atas coroutine melalui tugasnya. Jika suatu pekerjaan dibatalkan maka semua coroutine dalam lingkup yang sesuai akan dibatalkan juga. Anda bebas membuat cakupan Anda sendiri, tetapi dalam hal ini kita akan memanfaatkan viewModelScope yang sadar siklus hidup. Kami akan memulai coroutine baru di viewModelScope menggunakan viewModelScope.launch. Coroutine akan diluncurkan di utas utama karena viewModelScope memiliki dispatcher default - Dispatchers.Main. Coroutine dimulai pada Dispatcher. Main tidak akan memblokir utas saat ditangguhkan. Karena kami baru saja meluncurkan coroutine, kami dapat meminta operator suspense businessLookupEligibilityInteractor dan mendapatkan hasilnya. businessLookupEligibilityInteractor panggilan BusinessLookupRepository.businessLookupEligibility apa yang menggeser eksekusi ke Dispatchers.IO dan kembali ke Dispatchers.Main. Karena kami berada di utas UI, kami dapat memperbarui businessViewState LiveData dengan memberikan nilai.
- Kita dapat menyingkirkan barang sekali pakai karena viewModelScope terikat pada siklus hidup ViewModel. Setiap coroutine yang diluncurkan dalam lingkup ini secara otomatis dibatalkan jika ViewModel dihapus.
class BusinessProfileViewModel @Inject constructor( private val businessLookupEligibilityInteractor: BusinessLookupEligibilityInteractor ) : ViewModel() { internal val businessViewState: MutableLiveData<ViewState> = LiveDataFactory.createDefault("Loading...") fun onCountrySubmit(country: Country) { viewModelScope.launch { when (val state = businessLookupEligibilityInteractor(country.countryCode)) { is Result.Success -> businessViewState.value = state.entity.provider is Result.Failure -> businessViewState.value = state.failure } } } }
Takeaways kunci
Membaca dan memahami kode yang ditulis dengan coroutine cukup mudah, namun itu adalah perubahan paradigma yang memerlukan beberapa upaya untuk belajar bagaimana mendekati menulis kode dengan coroutine.
Pada artikel ini saya tidak membahas pengujian. Saya menggunakan perpustakaan
mockk karena saya memiliki masalah pengujian coroutine menggunakan Mockito.
Semua yang saya tulis dengan Rx Java, saya temukan cukup mudah diterapkan dengan coroutine,
Flows dan
Channels . Salah satu kelebihan coroutine adalah coroutine adalah fitur bahasa Kotlin dan berkembang bersama dengan bahasa tersebut.