RecyclerView dengan kecepatan maksimum: parsing libraries


Ilya Nekrasov, Mahtalitet , android-developer KODE
Selama dua setengah tahun dalam pengembangan android, saya berhasil mengerjakan proyek yang sangat berbeda: dari jaringan sosial untuk pengendara dan bank Latvia ke sistem bonus federal dan maskapai ketiga untuk transportasi. Lagi pula, di masing-masing proyek ini, saya menemukan tugas yang membutuhkan pencarian solusi non-klasik saat menerapkan daftar menggunakan kelas RecyclerView.
Artikel ini - buah dari persiapan untuk pertunjukan di DevFest Kaliningrad'18, serta berkomunikasi dengan kolega - akan sangat berguna bagi pengembang pemula dan mereka yang hanya menggunakan salah satu perpustakaan yang ada.


Untuk mulai dengan, kami menggali sedikit ke dalam esensi masalah dan sumber rasa sakit, yaitu, pertumbuhan fungsionalitas dalam pengembangan aplikasi dan kompleksitas daftar yang digunakan.


Bab satu, di mana pelanggan memimpikan aplikasi, dan kami - tentang persyaratan yang jelas


Mari kita bayangkan sebuah situasi ketika seorang pelanggan menghubungi sebuah studio yang menginginkan aplikasi mobile untuk toko bebek karet.


Proyek ini berkembang pesat, ide-ide baru muncul secara teratur dan tidak dibingkai dalam peta jalan jangka panjang.


Pertama, pelanggan meminta kami untuk menampilkan daftar barang yang ada dan, ketika diklik, mengisi permintaan pengiriman. Anda tidak perlu melangkah jauh untuk solusi: kami menggunakan set klasik dari RecyclerView , adaptor yang ditulis sendiri sederhana untuk itu dan Activity .


Untuk adaptor, kami menggunakan data homogen, satu ViewHolder dan logika pengikat sederhana.


Adaptor dengan bebek
class DucksClassicAdapter( private val data: List<Duck>, private val onDuckClickAction: (Duck) -> Unit ) : RecyclerView.Adapter<DucksClassicAdapter.ViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val rubberDuckView = LayoutInflater.from(parent.context).inflate(R.layout.item_rubber_duck, parent, false) return ViewHolder(rubberDuckView) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val duck = data[position] holder.divider.isVisible = position != 0 holder.rubberDuckImage.apply { Picasso.get() .load(duck.icon) .config(Bitmap.Config.ARGB_4444) .fit() .centerCrop() .noFade() .placeholder(R.drawable.duck_stub) .into(this) } holder.clicksHolder.setOnClickListener { onDuckClickAction.invoke(duck) } } override fun getItemCount() = data.count() class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val rubberDuckImage: ImageView = view.findViewById(R.id.rubberDuckImage) val clicksHolder: View = view.findViewById(R.id.clicksHolder) val divider: View = view.findViewById(R.id.divider) } } 



Seiring waktu, pelanggan memiliki ide untuk menambahkan kategori barang lain ke bebek karet, yang berarti bahwa ia harus menambahkan model data baru dan tata letak baru. Tetapi yang paling penting, ViewType lain akan muncul di adaptor, yang dengannya Anda dapat menentukan ViewHolder mana yang akan digunakan untuk item daftar tertentu.


Setelah itu, tajuk ditambahkan ke kategori, yang menurutnya setiap kategori dapat diciutkan dan diperluas untuk menyederhanakan orientasi pengguna di toko. Ini ditambah ViewType lain dan ViewHolder untuk header. Selain itu, Anda harus menyulitkan adaptor, karena Anda perlu menyimpan daftar grup terbuka dan menggunakannya untuk memeriksa kebutuhan untuk menyembunyikan dan menampilkan elemen ini atau itu dengan mengklik pada header.


Adaptor dengan segalanya
 class DucksClassicAdapter( private val onDuckClickAction: (Pair<Duck, Int>) -> Unit, private val onSlipperClickAction: (Duck) -> Unit, private val onAdvertClickAction: (Advert) -> Unit ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { var data: List<Duck> = emptyList() set(value) { field = value internalData = data.groupBy { it.javaClass.kotlin } .flatMap { groupedDucks -> val titleRes = when (groupedDucks.key) { DuckSlipper::class -> R.string.slippers RubberDuck::class -> R.string.rubber_ducks else -> R.string.mixed_ducks } groupedDucks.value.let { listOf(FakeDuck(titleRes, it)).plus(it) } } .toMutableList() duckCountsAdapters = internalData.map { duck -> val rubberDuck = (duck as? RubberDuck) DucksCountAdapter( data = (1..(rubberDuck?.count ?: 1)).map { count -> duck to count }, onCountClickAction = { onDuckClickAction.invoke(it) } ) } } private val advert = DuckMockData.adverts.orEmpty().shuffled().first() private var internalData: MutableList<Duck> = mutableListOf() private var duckCountsAdapters: List<DucksCountAdapter> = emptyList() private var collapsedHeaders: MutableSet<Duck> = hashSetOf() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { VIEW_TYPE_RUBBER_DUCK -> { val view = parent.context.inflate(R.layout.item_rubber_duck, parent) DuckViewHolder(view) } VIEW_TYPE_SLIPPER_DUCK -> { val view = parent.context.inflate(R.layout.item_duck_slipper, parent) SlipperViewHolder(view) } VIEW_TYPE_HEADER -> { val view = parent.context.inflate(R.layout.item_header, parent) HeaderViewHolder(view) } VIEW_TYPE_ADVERT -> { val view = parent.context.inflate(R.layout.item_advert, parent) AdvertViewHolder(view) } else -> throw UnsupportedOperationException("view type $viewType without ViewHolder") } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (holder) { is HeaderViewHolder -> bindHeaderViewHolder(holder, position) is DuckViewHolder -> bindDuckViewHolder(holder, position) is SlipperViewHolder -> bindSlipperViewHolder(holder, position) is AdvertViewHolder -> bindAdvertViewHolder(holder) } } private fun bindAdvertViewHolder(holder: AdvertViewHolder) { holder.advertImage.showIcon(advert.icon) holder.advertTagline.text = advert.tagline holder.itemView.setOnClickListener { onAdvertClickAction.invoke(advert) } } private fun bindHeaderViewHolder(holder: HeaderViewHolder, position: Int) { val item = getItem(position) as FakeDuck holder.clicksHolder.setOnClickListener { changeCollapseState(item, position) } val arrowRes = if (collapsedHeaders.contains(item)) R.drawable.ic_keyboard_arrow_up_black_24dp else R.drawable.ic_keyboard_arrow_down_black_24dp holder.arrow.setImageResource(arrowRes) holder.title.setText(item.titleRes) } private fun changeCollapseState(item: FakeDuck, position: Int) { val isCollapsed = collapsedHeaders.contains(item) if (isCollapsed) { collapsedHeaders.remove(item) } else { collapsedHeaders.add(item) } // 1 to add items after header val startPosition = position + 1 if (isCollapsed) { internalData.addAll(startPosition - ADVERTS_COUNT, item.items) notifyItemRangeInserted(startPosition, item.items.count()) } else { internalData.removeAll(item.items) notifyItemRangeRemoved(startPosition, item.items.count()) } notifyItemChanged(position) } @SuppressLint("SetTextI18n") private fun bindSlipperViewHolder(holder: SlipperViewHolder, position: Int) { val slipper = getItem(position) as DuckSlipper holder.duckSlipperImage.showIcon(slipper.icon) holder.duckSlipperSize.text = ": ${slipper.size}" holder.clicksHolder.setOnClickListener { onSlipperClickAction.invoke(slipper) } } private fun bindDuckViewHolder(holder: DuckViewHolder, position: Int) { val duck = getItem(position) as RubberDuck holder.rubberDuckImage.showIcon(duck.icon) holder.rubberDuckCounts.adapter = duckCountsAdapters[position - ADVERTS_COUNT] val context = holder.itemView.context holder.rubberDuckCounts.layoutManager = LinearLayoutManager(context, HORIZONTAL, false) } override fun getItemViewType(position: Int): Int { if (position == 0) return VIEW_TYPE_ADVERT return when (getItem(position)) { is FakeDuck -> VIEW_TYPE_HEADER is RubberDuck -> VIEW_TYPE_RUBBER_DUCK is DuckSlipper -> VIEW_TYPE_SLIPPER_DUCK else -> throw UnsupportedOperationException("unknown type for $position position") } } private fun getItem(position: Int) = internalData[position - ADVERTS_COUNT] override fun getItemCount() = internalData.count() + ADVERTS_COUNT class DuckViewHolder(view: View) : RecyclerView.ViewHolder(view) { val rubberDuckImage: ImageView = view.findViewById(R.id.rubberDuckImage) val rubberDuckCounts: RecyclerView = view.findViewById(R.id.rubberDuckCounts) } class SlipperViewHolder(view: View) : RecyclerView.ViewHolder(view) { val duckSlipperImage: ImageView = view.findViewById(R.id.duckSlipperImage) val duckSlipperSize: TextView = view.findViewById(R.id.duckSlipperSize) val clicksHolder: View = view.findViewById(R.id.clicksHolder) } class HeaderViewHolder(view: View) : RecyclerView.ViewHolder(view) { val title: TextView = view.findViewById(R.id.headerTitle) val arrow: ImageView = view.findViewById(R.id.headerArrow) val clicksHolder: View = view.findViewById(R.id.clicksHolder) } class AdvertViewHolder(view: View) : RecyclerView.ViewHolder(view) { val advertTagline: TextView = view.findViewById(R.id.advertTagline) val advertImage: ImageView = view.findViewById(R.id.advertImage) } } private class FakeDuck( val titleRes: Int, val items: List<Duck> ) : Duck private fun ImageView.showIcon(icon: String, placeHolderRes: Int = R.drawable.duck_stub) { Picasso.get() .load(icon) .config(Bitmap.Config.ARGB_4444) .fit() .centerCrop() .noFade() .placeholder(placeHolderRes) .into(this) } private class DucksCountAdapter( private val data: List<Pair<Duck, Int>>, private val onCountClickAction: (Pair<Duck, Int>) -> Unit ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val view = parent.context.inflate(R.layout.item_duck_count, parent) return CountViewHolder(view) } override fun getItemCount() = data.count() override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { (holder as CountViewHolder).count.apply { val item = data[position] text = item.second.toString() setOnClickListener { onCountClickAction.invoke(item) } } } class CountViewHolder(view: View) : RecyclerView.ViewHolder(view) { val count: TextView = view.findViewById(R.id.count) } } 

Saya pikir Anda menangkap intinya - tumpukan kecil menyerupai perkembangan yang sehat. Dan di depan ada semakin banyak persyaratan dari pelanggan: untuk memperbaiki spanduk iklan di bagian atas daftar, untuk menyadari kemungkinan memilih jumlah bebek yang dipesan. Hanya tugas-tugas ini yang pada akhirnya akan berubah menjadi adaptor biasa, yang lagi-lagi harus ditulis dari awal.


Proses pengembangan adaptor klasik dalam sejarah github

Ringkasan


Bahkan, gambarannya sama sekali tidak menggembirakan: adaptor individu harus diasah untuk kasus-kasus tertentu. Kita semua mengerti bahwa dalam aplikasi nyata ada puluhan atau bahkan ratusan layar daftar seperti itu. Dan mereka tidak mengandung informasi tentang bebek, tetapi data yang lebih kompleks. Ya, dan desain mereka jauh lebih rumit.


Apa yang salah dengan adaptor kami?


  • jelas, mereka sulit untuk digunakan kembali;
  • di dalam logika bisnis muncul dan seiring waktu menjadi lebih dan lebih;
  • sulit dipertahankan dan dikembangkan;
  • risiko kesalahan tinggi saat memperbarui data;
  • desain tidak jelas.

Bab Dua, di mana Semuanya Bisa Berbeda


Tidak realistis dan tidak masuk akal untuk membayangkan perkembangan aplikasi untuk tahun-tahun mendatang. Setelah beberapa tarian seperti itu dengan rebana seperti pada bab terakhir dan menulis lusinan adaptor, siapa pun akan memiliki pertanyaan "Mungkin ada solusi lain?".



Setelah menyelesaikan Github, kami menemukan bahwa perpustakaan AdapterDelegates pertama kali muncul pada tahun 2015, dan setahun kemudian Groupie dan Epoxy ditambahkan ke gudang pengembang - mereka semua membantu membuat hidup lebih mudah, tetapi masing-masing memiliki spesifik dan perangkap tersendiri.


Ada beberapa pustaka yang serupa (misalnya, FastAdapter), tetapi baik saya maupun rekan saya tidak menggunakannya, jadi kami tidak akan mempertimbangkannya di artikel.

Sebelum membandingkan perpustakaan, kami menganalisis secara singkat kasus yang dijelaskan di atas dengan toko online dengan syarat menggunakan AdapterDelegates - dari perpustakaan yang dibongkar, ini adalah yang paling sederhana dari sudut pandang implementasi internal dan penggunaan (namun, itu tidak maju dalam segala hal, jadi Anda harus menambahkan banyak hal dengan tangan).


Perpustakaan tidak akan sepenuhnya menyelamatkan kita dari adaptor, tetapi perpustakaan itu akan dibentuk dari blok (batu bata), yang dapat dengan aman kita tambahkan atau hapus dari daftar dan tukarkan.


Blok adaptor
 class DucksDelegatesAdapter : ListDelegationAdapter<List<DisplayableItem>>() { init { delegatesManager.addDelegate(RubberDuckDelegate()) } fun setData(items: List<DisplayableItem>) { this.items = items notifyDataSetChanged() } } private class RubberDuckDelegate : AbsListItemAdapterDelegate<RubberDuckItem, DisplayableItem, RubberDuckDelegate.ViewHolder>() { override fun isForViewType(item: DisplayableItem, items: List<DisplayableItem>, position: Int): Boolean { return item is RubberDuckItem } override fun onCreateViewHolder(parent: ViewGroup): ViewHolder { val item = parent.context.inflate(R.layout.item_rubber_duck, parent, false) return ViewHolder(item) } override fun onBindViewHolder(item: RubberDuckItem, viewHolder: ViewHolder, payloads: List<Any>) { viewHolder.apply { rubberDuckImage.showIcon(item.icon) } } class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val rubberDuckImage: ImageView = itemView.findViewById(R.id.rubberDuckImage) } } 

Menggunakan Adaptor Aktivitas
 class DucksDelegatesActivity : AppCompatActivity() { private lateinit var ducksList: RecyclerView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_ducks_delegates) ducksList = findViewById(R.id.duckList) ducksList.apply { layoutManager = LinearLayoutManager(this@DucksDelegatesActivity) adapter = createAdapter().apply { showData() } } } fun createAdapter(): DucksDelegatesAdapter { return DucksDelegatesAdapter() } private fun DucksDelegatesAdapter.showData() { setData(getRubberDucks()) } private fun getRubberDucks(): List<DisplayableItem> { return DuckMockData.ducks.orEmpty().map { RubberDuckItem(it.icon) } } } 

Dari tugas pertama, kita melihat perbedaannya: kita memiliki kelas adaptor yang diwarisi dari perpustakaan. Dan selain itu - batu bata yang sama, yang disebut delegate dan dari mana kita juga mewarisi dan mengimplementasikan bagian dari logika yang kita butuhkan. Selanjutnya kita menambahkan delegasi ke manajer - ini juga kelas perpustakaan. Dan hal terakhir yang Anda butuhkan adalah membuat adaptor dan mengisinya dengan data


Untuk mengimplementasikan kategori kedua toko dan header, kami akan menulis beberapa delegasi lagi, dan animasi muncul berkat kelas DiffUtil .


Di sini saya akan menunjukkan kesimpulan singkat tetapi pasti: penggunaan bahkan perpustakaan ini memecahkan semua masalah yang tercantum yang muncul ketika kita mempersulit aplikasi dalam kasus dengan toko online, tetapi tanpa minus, di tempat lain.


Proses pengembangan adaptor dengan AdapterDelegate dalam riwayat github

Bab Tiga, di mana pengembang menghapus kacamata merah muda dengan membandingkan perpustakaan


Kami akan menyelami lebih detail fungsi dan operasi masing-masing perpustakaan. Dengan satu atau lain cara saya menggunakan ketiga pustaka pada proyek kami, tergantung pada tugas dan kompleksitas aplikasi.


Delegasi Adaptor


Kami menggunakan perpustakaan ini dalam aplikasi salah satu maskapai penerbangan Rusia terbesar. Kami perlu mengganti daftar pembayaran sederhana dengan daftar dengan grup dan sejumlah besar parameter berbeda.


Skema kerja perpustakaan yang disederhanakan terlihat seperti ini:


Kelas utama adalah DelegateAdapter , berbagai "batu bata" adalah "delegasi" yang bertanggung jawab untuk menampilkan tipe data tertentu dan, tentu saja, daftar itu sendiri.


Pro:


  • kesederhanaan pencelupan;
  • adaptor yang mudah digunakan kembali;
  • beberapa metode dan kelas;
  • tidak ada refleksi, pembuatan kode, dan penyatuan data.

Cons:


  • Anda perlu mengimplementasikan logika sendiri, misalnya memperbarui item melalui DiffUti l (sejak versi 3.1.0, Anda dapat menggunakan adaptor AsyncListDifferDelegationAdapter);
  • kode mubazir.

Secara umum, perpustakaan ini menyelesaikan semua kesulitan utama dalam memperluas fungsionalitas aplikasi dan cocok untuk mereka yang belum pernah menggunakan perpustakaan sebelumnya. Tetapi untuk berhenti hanya di atasnya, saya tidak menyarankan.

Groupie


Kami sering menggunakan Groupie , dibuat beberapa tahun lalu oleh Lisa Wray , termasuk menulis aplikasi untuk satu bank Latvia sepenuhnya menggunakannya.


Untuk menggunakan perpustakaan ini, pertama-tama, Anda harus berurusan dengan dependensi . Selain yang utama, Anda dapat menggunakan beberapa opsi untuk dipilih:



Kami memikirkan satu hal dan menentukan ketergantungan yang diperlukan.


Menggunakan contoh toko online dengan bebek, kita perlu membuat Item yang diwarisi dari kelas perpustakaan, menentukan tata letak dan menerapkan pengikatan melalui sintaksis Kotlin. Dibandingkan dengan jumlah kode yang harus saya tulis dengan AdapterDelegates , itu hanya surga dan bumi.


Yang tersisa hanyalah mengatur RecyclerView GroupieAdapter sebagai adaptor, dan memasukkan item yang diolok-olok ke dalamnya.



Dapat dilihat bahwa skema kerjanya lebih besar dan lebih kompleks. Di sini, selain item sederhana, Anda dapat menggunakan seluruh bagian - grup item dan kelas lainnya.


Pro:


  • antarmuka intuitif, meskipun api membuat Anda berpikir;
  • adanya solusi kotak;
  • pengelompokan menjadi kelompok elemen;
  • pilihan antara opsi biasa, Kotlin Extensions dan DataBinding;
  • embed ItemDecoration dan animasi.

Cons:


  • wiki tidak lengkap;
  • dukungan yang buruk oleh pengelola dan masyarakat;
  • bug kecil yang harus dielakkan di versi pertama;
  • diffing di utas utama (untuk saat ini);
  • tidak ada dukungan untuk AndroidX (saat ini, tetapi Anda perlu melacak repositori).

Adalah penting bahwa Groupie, dengan semua kekurangannya, dapat dengan mudah mengganti AdaptorDelegasi , terutama jika Anda berencana untuk membuat daftar lipat tingkat pertama, dan tidak ingin menulis banyak boilerplate.


Menerapkan Daftar Bebek bersama Groupie

Epoksi


Perpustakaan terakhir yang kami mulai gunakan relatif baru-baru ini adalah Epoxy , dikembangkan oleh orang-orang Airbnb . Perpustakaan itu kompleks, tetapi memungkinkan Anda untuk menyelesaikan berbagai tugas. Programmer Airbnb sendiri menggunakannya untuk membuat layar langsung dari server. Epoxy berguna pada salah satu proyek terbaru kami - aplikasi untuk bank di Yekaterinburg.


Untuk mengembangkan layar, kami harus bekerja dengan berbagai jenis data, sejumlah besar daftar. Dan salah satu layar benar-benar tidak ada habisnya. Dan Epoxy membantu kita semua menghadapi ini.


Prinsip pustaka secara keseluruhan mirip dengan dua yang sebelumnya, kecuali bahwa alih-alih adaptor, EpoxyController digunakan untuk membangun daftar, yang memungkinkan Anda untuk menentukan secara deklaratif menentukan struktur adaptor.



Untuk mencapai hal ini, perpustakaan dibangun berdasarkan pembuatan kode. Cara kerjanya - dengan semua nuansa yang dijelaskan dengan baik di wiki dan tercermin dalam sampel .


Pro:


  • model untuk daftar, dihasilkan dari Tampilan biasa, dengan kemungkinan digunakan kembali di layar sederhana;
  • deskripsi deklaratif layar;
  • Pengikatan Data dengan kecepatan maksimum - menghasilkan model langsung dari file tata letak;
  • cukup tampilkan tidak hanya daftar dari blok, tetapi juga layar yang rumit;
  • ViewPool berbagi tentang Aktivitas;
  • asynchronous diffing out of the box (AsyncEpoxyController);
  • tidak perlu dikukus dengan daftar horisontal.

Cons:


  • banyak kelas, prosesor, anotasi;
  • sulit menyelam dari awal;
  • menggunakan plugin ButterKnife untuk menghasilkan file R2 dalam modul;
  • sangat sulit untuk memahami cara bekerja dengan benar dengan Callback (kami sendiri belum mengerti);
  • Ada masalah yang perlu dielakkan: misalnya, kerusakan dengan id yang sama.

Menerapkan Daftar Bebek dengan Epoxy

Ringkasan


Hal utama yang ingin saya sampaikan adalah: jangan tahan dengan kerumitan yang muncul ketika Anda perlu membuat daftar yang rumit dan terus-menerus harus mengulanginya. Dan ini sangat sering terjadi. Dan pada prinsipnya, ketika mereka diterapkan, jika proyek baru saja dimulai, atau Anda terlibat dalam refactoring-nya.


Kenyataannya adalah bahwa tidak perlu mempersulit logika sekali lagi, berpikir bahwa beberapa jenis abstraksi kita sendiri sudah cukup. Mereka cukup pendek Dan bekerja dengan mereka bukan hanya bukan kesenangan, tetapi ada juga godaan untuk mentransfer bagian dari logika ke bagian UI, yang seharusnya tidak ada di sana. Ada alat yang dapat membantu Anda menghindari sebagian besar masalah, dan Anda harus menggunakannya.


Saya mengerti bahwa bagi banyak pengembang berpengalaman (dan tidak hanya) ini jelas atau mereka mungkin tidak setuju dengan saya. Tetapi saya menganggap penting untuk menekankan ini lagi.

Jadi, apa yang harus dipilih?


Memberi saran untuk tetap di satu perpustakaan cukup sulit, karena pilihannya tergantung pada banyak faktor: dari preferensi pribadi hingga ideologi pada proyek.


Saya akan melakukan hal berikut:


  1. Jika Anda baru memulai jalan Anda dalam pengembangan, coba mulai pada proyek kecil dengan AdapterDelegates - ini adalah perpustakaan paling sederhana - tidak diperlukan pengetahuan khusus. Anda akan memahami cara bekerja dengannya dan mengapa lebih nyaman daripada menulis adaptor sendiri.
  2. Groupie cocok untuk mereka yang sudah cukup banyak bermain dengan AdapterDelegates dan sudah lelah menulis banyak boilerplate, atau untuk semua orang yang ingin segera memulai dengan jalan tengah. Dan jangan lupa tentang keberadaan kelompok lipat di luar kotak - ini juga argumen yang bagus untuknya.
  3. Nah, dan Epoxy - bagi mereka yang dihadapkan dengan proyek yang benar-benar kompleks, dengan sejumlah besar data, sehingga kompleksitas dengan perpustakaan yang gemuk akan menjadi masalah yang lebih kecil. Pada awalnya akan sulit, tetapi kemudian implementasi daftar akan tampak seperti hal sepele. Argumen penting yang mendukung Epoxy mungkin adalah kehadiran DataBinding dan MVVM pada proyek - itu secara harfiah dibuat untuk ini, mengingat kemungkinan menghasilkan model dari tata letak yang sesuai.

Jika Anda masih memiliki pertanyaan, Anda dapat melihat tautan untuk sekali lagi melihat kode aplikasi kami dengan detailnya.

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


All Articles