ActionViews atau bagaimana saya tidak suka boilerplate sejak kecil

Halo, Habr! Pada artikel ini saya ingin berbagi pengalaman membuat mekanisme sendiri untuk mengotomatiskan tampilan berbagai jenis Tampilan: ContentView, LoadingView, NoInternetView, EmptyContentView, ErrorView.





Sudah jauh. Jalur coba-coba, penghitungan metode dan opsi, malam tanpa tidur dan pengalaman yang tak ternilai yang ingin saya bagikan dan dengar kritik, yang pasti akan saya perhitungkan.


Saya akan segera mengatakan bahwa saya akan mempertimbangkan untuk mengerjakan RxJava, karena untuk coroutine saya tidak melakukan mekanisme seperti itu - tangan saya tidak mencapai. Dan untuk alat serupa lainnya (Loader, AsyncTask, dll.), Tidak masuk akal untuk menggunakan mekanisme saya, karena paling sering RxJava atau coroutine yang digunakan.


Tinjauan tindakan


Seorang kolega saya mengatakan bahwa tidak mungkin untuk membakukan perilaku Pandangan, tetapi saya masih mencoba melakukannya. Dan berhasil.


Layar aplikasi standar, data yang diambil dari server, setidaknya harus memproses 5 status:


  • Tampilan data
  • Memuat
  • Kesalahan - kesalahan apa pun yang tidak dijelaskan di bawah ini
  • Kurangnya internet adalah kesalahan global
  • Layar kosong - permintaan terlewati, tetapi tidak ada data
  • Keadaan lain adalah bahwa data dimuat dari cache, tetapi permintaan pembaruan dikembalikan dengan kesalahan, yaitu, menampilkan data yang sudah usang (lebih baik daripada tidak sama sekali) - Perpustakaan tidak mendukung ini.

Dengan demikian, untuk setiap keadaan seperti itu harus ada Pandangannya sendiri.


Saya menyebut View ini - ActionViews , karena mereka menanggapi semacam tindakan. Bahkan, jika Anda bisa menentukan dengan tepat pada titik apa Tampilan Anda harus ditampilkan, dan kapan harus menyembunyikan, maka itu juga bisa menjadi ActionView.


Ada satu (atau mungkin tidak satu) cara standar untuk bekerja dengan Tampilan tersebut.


Dalam metode yang berisi pekerjaan dengan RxJava, Anda perlu menambahkan argumen masukan untuk semua jenis ActionViews dan menambahkan beberapa logika ke panggilan ini untuk menampilkan dan menyembunyikan ActionViews, seperti yang dilakukan di sini:


public void getSomeData(LoadingView loadingView, ErrorView errorView, NoInternetView noInternetView, EmptyContentView emptyContentView) { mApi.getProjects() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnSubscribe(disposable -> { loadingView.show(); noInternetView.hide(); emptyContentView.hide(); }) .doFinally(loadingView::hide) .flatMap(projectResponse -> { /*    */ }) .subscribe( response -> {/*   */}, throwable -> { if (ApiUtils.NETWORK_EXCEPTIONS .contains(throwable.getClass())) noInternetView.show(); else errorView.show(throwable.getMessage()); } ); } 

Tetapi metode ini mengandung sejumlah besar boilerplate, dan secara default kami tidak menyukainya. Jadi saya mulai mengurangi kode rutin.


Naik level


Langkah pertama dalam meningkatkan cara standar untuk bekerja dengan ActionViews adalah mengurangi boilerplate dengan menempatkan logika di kelas utilitas. Kode di bawah ini tidak ditemukan oleh saya. Saya seorang penjiplakan dan memata-matai satu rekan yang masuk akal. Terima kasih Arutar


Sekarang kode kita terlihat seperti ini:


 public void getSomeData(LoadingView loadingView, ErrorView errorView, NoInternetView noInternetView, EmptyContentView emptyContentView) { mApi.getProjects() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .compose(RxUtil::loading(loadingView)) .compose(RxUtil::emptyContent(emptyContentView)) .compose(RxUtil::noInternet(errorView, noInternetView)) .subscribe(response -> { /*   */ }, RxUtil::error(errorView)); } 

Kode yang kita lihat di atas, meskipun tanpa kode boilerplate, masih tidak menyebabkan kesenangan yang mempesona. Ini sudah menjadi jauh lebih baik, tetapi masih ada masalah meneruskan tautan ke ActionViews di setiap metode di mana ada pekerjaan dengan Rx. Dan mungkin ada banyak metode seperti itu dalam sebuah proyek. Juga menulis ini menulis terus-menerus. Buueee. Siapa yang butuh ini? Hanya pekerja keras, keras kepala, dan tidak malas. Saya tidak seperti itu. Saya penggemar kemalasan dan penggemar menulis kode yang indah dan nyaman, jadi keputusan penting dibuat - untuk menyederhanakan kode dengan cara apa pun.


Titik terobosan


Setelah banyak penulisan ulang mekanisme, saya sampai pada opsi ini:


 public void getSomeData() { execute(() -> mApi.getProjects(), new BaseSubscriber<>(response -> { /*   */ })); } 

Saya menulis ulang mekanisme saya sekitar 10-15 kali, dan setiap kali itu sangat berbeda dari versi sebelumnya. Saya tidak akan menunjukkan kepada Anda semua versi, mari kita fokus pada dua versi terakhir. Yang pertama Anda lihat.


Setuju, ini terlihat cantik? Saya bahkan akan mengatakan sangat cantik. Saya memperjuangkan keputusan seperti itu. Dan benar-benar semua ActionViews kami akan berfungsi dengan benar pada saat kami membutuhkannya. Saya dapat mencapai ini dengan menulis sejumlah besar bukan kode yang paling indah. Kelas yang memungkinkan mekanisme seperti itu mengandung banyak logika yang kompleks, dan saya tidak menyukainya. Dalam sebuah kata - sayang, yang merupakan monster di bawah tenda.





Kode seperti itu di masa depan akan semakin sulit untuk dipertahankan, dan kode itu sendiri mengandung kelemahan dan masalah yang cukup serius:


  • Apa yang terjadi jika Anda perlu menampilkan beberapa LoadingViews di layar? Bagaimana cara memisahkannya? Bagaimana cara memahami LoadingView yang harus ditampilkan kapan?
  • Pelanggaran konsep Rx - semuanya harus dalam satu aliran (stream). Ini tidak terjadi di sini.
  • Kompleksitas kustomisasi. Perilaku dan logika yang dijelaskan sangat sulit diubah untuk pengguna akhir dan, karenanya, sulit untuk menambahkan perilaku baru.
  • Anda harus menggunakan Tampilan khusus agar mekanisme berfungsi. Ini diperlukan agar mekanisme memahami ActionView mana yang termasuk tipe apa. Misalnya, jika Anda ingin menggunakan ProgressBar, maka itu harus mengandung implementasi LoadingView.
  • ID untuk ActionView kami harus cocok dengan yang ditentukan dalam kelas dasar untuk menyingkirkan pelat ketel. Ini sangat tidak nyaman, meskipun Anda bisa menerima ini.
  • Refleksi Ya, dia ada di sini, dan karena dia, mekanismenya jelas membutuhkan optimasi.

Tentu saja, saya punya solusi untuk masalah ini, tetapi semua solusi ini memunculkan masalah lain. Saya mencoba untuk menyingkirkan yang paling kritis sebanyak mungkin, dan sebagai hasilnya, hanya persyaratan yang diperlukan untuk menggunakan perpustakaan tetap.


Selamat tinggal Jawa!


Setelah beberapa waktu, saya duduk di rumah, mencoba-coba Saya bermain-main dan tiba-tiba saya menyadari bahwa saya harus mencoba Kotlin dan memaksimalkan ekstensi, nilai default, lambda dan delegasi.


Awalnya dia tidak terlihat banyak. Tetapi sekarang ia kehilangan hampir semua kekurangan, yang pada prinsipnya bisa.


Ini kode kami sebelumnya, tetapi dalam versi final:


 fun getSomeData() { api.getProjects() .withActionViews(view) .execute(onComplete = { /*   */ }) } 

Berkat Extensions, saya bisa melakukan semua pekerjaan dalam satu utas tanpa melanggar konsep dasar pemrograman reaktif. Saya juga meninggalkan kesempatan untuk menyesuaikan perilaku. Jika Anda ingin mengubah tindakan di awal atau di akhir acara, Anda bisa meneruskan fungsi ke metode, dan semuanya akan berfungsi:


 fun getSomeData() { api.getProjects() .withActionViews( view, doOnLoadStart = { /* */ }, doOnLoadEnd = { /* */ }) .execute(onComplete = { /*   */ }) } 

Perubahan perilaku juga tersedia untuk ActionViews lainnya. Jika Anda ingin menggunakan perilaku standar, tetapi Anda tidak memiliki ActionViews default, Anda bisa menentukan Tampilan mana yang harus menggantikan ActionView kami:


 fun getSomeData(projectLoadingView: LoadingView) { mApi.getPosts(1, 1) .withActionViews( view, loadingView = projectLoadingView ) .execute(onComplete = { /*   */ }) } 

Saya tunjukkan sangat krim mekanisme ini, tetapi juga memiliki harga sendiri.
Pertama, Anda harus membuat CustomViews agar ini berfungsi:


 class SwipeRefreshLayout : android.support.v4.widget.SwipeRefreshLayout, LoadingView { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet) : super(context, attrs) } 

Bahkan mungkin tidak perlu melakukannya. Saat ini, saya sedang mengumpulkan umpan balik dan menerima saran untuk meningkatkan mekanisme ini. Alasan utama kita perlu menggunakan CustomViews adalah untuk mewarisi dari antarmuka yang memberitahukan jenis ActionView yang dimilikinya. Ini untuk keamanan, karena Anda mungkin secara tidak sengaja melakukan kesalahan saat menentukan jenis tampilan dalam metode withActionsViews.


Inilah yang tampak seperti metode withActionsViews:


 fun <T> Observable<T>.withActionViews( view: ActionsView, contentView: View = view.contentActionView, loadingView: LoadingView? = view.loadingActionView, noInternetView: NoInternetView? = view.noInternetActionView, emptyContentView: EmptyContentView? = view.emptyContentActionView, errorView: ErrorView = view.errorActionView, doOnLoadStart: () -> Unit = { doOnLoadSubscribe(contentView, loadingView) }, doOnLoadEnd: () -> Unit = { doOnLoadComplete(contentView, loadingView) }, doOnStartNoInternet: () -> Unit = { doOnNoInternetSubscribe(contentView, noInternetView) }, doOnNoInternet: (Throwable) -> Unit = { doOnNoInternet(contentView, errorView, noInternetView) }, doOnStartEmptyContent: () -> Unit = { doOnEmptyContentSubscribe(contentView, emptyContentView) }, doOnEmptyContent: () -> Unit = { doOnEmptyContent(contentView, errorView, emptyContentView) }, doOnError: (Throwable) -> Unit = { doOnError(errorView, it) } ) { /**/ } 

Itu terlihat menakutkan, tetapi nyaman dan cepat! Seperti yang Anda lihat, dalam parameter input yang diterimanya loadingView: LoadingView? .. Ini menjamin kami dari kesalahan dengan tipe ActionView.


Oleh karena itu, agar mekanisme bekerja, Anda perlu mengambil beberapa langkah sederhana:


  • Tambahkan ke tata letak kami ActionViews kami, yang merupakan kebiasaan. Saya sudah membuat beberapa dari mereka, dan Anda bisa menggunakannya.
  • Terapkan antarmuka HasActionsView dan timpa variabel default yang bertanggung jawab untuk ActionViews dalam kode:
     override var contentActionView: View by mutableLazy { recyclerView } override var loadingActionView: LoadingView? by mutableLazy { swipeRefreshLayout } override var noInternetActionView: NoInternetView? by mutableLazy { noInternetView } override var emptyContentActionView: EmptyContentView? by mutableLazy { emptyContentView } override var errorActionView: ErrorView by mutableLazy { ToastView(baseActivity) } 
  • Atau mewarisi dari kelas di mana ActionViews kami sudah ditimpa. Dalam hal ini, Anda harus menggunakan id yang ditentukan secara ketat di tata letak Anda:


     abstract class ActionsFragment : Fragment(), HasActionsView { override var contentActionView: View by mutableLazy { findViewById<View>(R.id.contentView) } override var loadingActionView: LoadingView? by mutableLazy { findViewByIdNullable<View>(R.id.loadingView) as LoadingView? } override var noInternetActionView: NoInternetView? by mutableLazy { findViewByIdNullable<View>(R.id.noInternetView) as NoInternetView? } override var emptyContentActionView: EmptyContentView? by mutableLazy { findViewByIdNullable<View>(R.id.emptyContentView) as EmptyContentView? } override var errorActionView: ErrorView by mutableLazy { ToastView(baseActivity) } } 

  • Nikmati pekerjaan tanpa pelat baja!

Jika Anda akan menggunakan Ekstensi Kotlin, maka jangan lupa bahwa Anda dapat mengubah nama impor menjadi nama yang nyaman bagi Anda:


 import kotlinx.android.synthetic.main.fr_gifts.contentView as recyclerView 

Apa selanjutnya


Ketika saya mulai mengerjakan mekanisme ini, saya tidak memikirkan tentang apa perpustakaan itu nantinya. Tetapi kebetulan saya ingin membagikan kreasi saya, dan sekarang hal termanis menunggu saya - menerbitkan perpustakaan, mengumpulkan masalah, menerima umpan balik, menambah / meningkatkan fungsi dan memperbaiki bug.


Sementara saya sedang menulis artikel ...


Saya berhasil mengatur semuanya dalam bentuk perpustakaan:



Perpustakaan dan mekanisme itu sendiri tidak mengklaim harus dimiliki dalam proyek Anda. Saya hanya ingin membagikan ide saya, mendengarkan kritik, komentar, dan meningkatkan mekanisme saya sehingga menjadi lebih nyaman, digunakan, dan praktis. Mungkin Anda bisa membuat mekanisme seperti itu lebih baik dari saya. Saya hanya akan senang. Saya sangat berharap artikel saya menginspirasi Anda untuk membuat sesuatu milik Anda sendiri, mungkin bahkan serupa dan lebih ringkas.


Jika Anda memiliki saran dan rekomendasi untuk meningkatkan fungsi dan pengoperasian mekanisme itu sendiri, saya akan senang mendengarkannya. Selamat datang di komentar dan, untuk jaga-jaga, Telegram saya: @tanchuev


PS Saya sangat senang bahwa saya menciptakan sesuatu yang berguna dengan tangan saya sendiri. Mungkin ActionViews tidak akan diminati, tetapi pengalaman dan buzz dari ini tidak akan kemana-mana.


PPS Agar ActionViews berubah menjadi perpustakaan bekas yang lengkap, Anda perlu mengumpulkan umpan balik dan, mungkin, memperbaiki fungsionalitas atau secara fundamental mengubah pendekatan itu sendiri jika semuanya berjalan sangat buruk.


PPPS Jika Anda tertarik dengan pekerjaan saya, maka kita dapat membahasnya secara pribadi pada 28 September di Moskow pada Konferensi Pengembang Seluler Internasional MBLT DEV 2018 . Ngomong-ngomong, tiket early bird sudah habis!

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


All Articles