Pertimbangkan fitur-fitur dari pendekatan ini dan implementasi kami menggunakan widget, konsep, kelebihan, dan perbedaan dari pandangan lain di Android.
Backend-Driven UI - sebuah pendekatan yang memungkinkan Anda membuat komponen UI berdasarkan respons server. Deskripsi API harus berisi jenis komponen dan propertinya, dan aplikasi harus menampilkan komponen yang diperlukan tergantung pada jenis dan propertinya. Secara umum, logika komponen dapat diletakkan, dan untuk aplikasi mobile, mereka adalah kotak hitam, karena masing-masing komponen dapat memiliki logika yang independen dari sisa aplikasi dan dapat dikonfigurasi secara sewenang-wenang oleh server, tergantung pada logika bisnis yang diperlukan. Itulah sebabnya pendekatan ini sering digunakan dalam aplikasi perbankan: misalnya, ketika Anda perlu menampilkan formulir terjemahan dengan sejumlah besar bidang yang ditentukan secara dinamis. Aplikasi tidak tahu sebelumnya komposisi bentuk dan urutan bidang di dalamnya, oleh karena itu,pendekatan ini adalah satu-satunya cara untuk menulis kode tanpa kruk. Selain itu, ini menambah fleksibilitas: dari sisi server, Anda dapat mengubah formulir kapan saja, dan aplikasi seluler akan siap untuk ini.Gunakan kasing
Jenis komponen berikut ini disajikan di atas:- Daftar akun yang tersedia untuk ditransfer;
- Nama jenis terjemahan;
- Bidang untuk memasukkan nomor telepon (memiliki topeng untuk memasukkan dan berisi ikon untuk memilih kontak dari perangkat);
- Bidang untuk memasukkan jumlah transfer.
Juga pada formulir, sejumlah komponen lain yang tertanam dalam logika bisnis dan ditentukan pada tahap desain dimungkinkan. Informasi tentang setiap komponen yang datang dalam respons dari server harus memenuhi persyaratan, dan setiap komponen harus diharapkan oleh aplikasi seluler untuk memprosesnya dengan benar.
Bidang input yang berbeda memiliki masker dan aturan validasi yang berbeda; tombol mungkin memiliki animasi kilau pada saat boot; widget untuk memilih akun charge-off dapat memiliki animasi saat menggulir, dan sebagainya.Semua komponen UI terpisah satu sama lain, dan logikanya dapat dibawa ke tampilan terpisah dengan area tanggung jawab yang berbeda - sebut saja mereka widget. Setiap widget menerima konfigurasinya dalam respons server dan merangkum logika tampilan dan pemrosesan data.Saat mengimplementasikan layar, RecyclerView paling cocok, elemen yang akan berisi widget. ViewHolder dari setiap item daftar unik akan menginisialisasi widget dan memberikannya data yang diperlukan untuk ditampilkan.Konsep widget
Mari kita pertimbangkan widget lebih detail. Pada intinya, widget adalah tampilan khusus "dengan kecepatan maksimum." Tampilan kustom biasa juga dapat berisi data dan logika tampilan mereka, tetapi widget menyiratkan sesuatu yang lebih - itu memiliki presenter, model layar dan memiliki ruang lingkup DI sendiri.Sebelum masuk ke detail penerapan widget, kami membahas keunggulannya:- , , ยซยป , โ UI- , .
- , , .
- , โ : , , .
Untuk mengimplementasikan widget scalable, kami menggunakan kelas dasar untuk ViewGroup yang paling umum digunakan, yang memiliki cakupan DI sendiri. Semua kelas dasar, pada gilirannya, diwarisi dari antarmuka umum, yang berisi semua yang diperlukan untuk menginisialisasi widget.Kasing termudah untuk menggunakan widget adalah tampilan statis, yang ditentukan secara langsung dalam tata letak. Setelah menerapkan kelas widget, Anda dapat dengan aman menambahkannya ke tata letak XML, tanpa lupa untuk menentukan idnya di tata letak (berdasarkan id, lingkup DI widget akan dibentuk).Dalam artikel ini, kami mempertimbangkan widget dinamis lebih detail, karena kasus bentuk terjemahan yang dijelaskan di atas dengan set bidang yang sewenang-wenang diselesaikan dengan mudah dengan bantuan mereka.Widget apa pun, baik statis maupun dinamis, dalam penerapan kami hampir tidak berbeda dengan tampilan biasa dalam hal MVP. Biasanya, 4 kelas diperlukan untuk mengimplementasikan widget:- Lihat kelas, tempat tata letak tata letak dan menampilkan konten terjadi;
class TextInputFieldWidget @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : CoreFrameLayoutView(context, attrs) {
@Inject
lateinit var presenter: TextInputFieldPresenter
โฆ
init {
inflate(context, R.layout.view_field_text_input, this)
}
}
- Kelas untuk presenter, di mana logika dasar widget dijelaskan, misalnya:
- memuat data dan mengirimkannya untuk render;
- Berlangganan berbagai acara dan memancarkan acara perubahan input widget;
@PerScreen
class TextInputFieldPresenter @Inject constructor(
basePresenterDependency: BasePresenterDependency,
rxBus: RxBus
) : BaseInputFieldPresenter<TextInputFieldWidget>(
basePresenterDependency, rxBus
) {
private val sm = TextInputFieldScreenModel()
...
}
Dalam implementasi kami, kelas RxBus adalah bus berbasis PublishSubject untuk mengirim acara dan berlangganan.
- Kelas untuk model layar, dengan bantuan yang presenter menerima data dan mentransfernya untuk rendering dalam tampilan (dalam hal pola Model Presentasi);
class TextInputFieldScreenModel : ScreenModel() {
val value = String = โโ
val hint = String = โโ
val errorText = String = โโ
}
- Kelas konfigurator untuk mengimplementasikan DI, dengan bantuan dependensi widget yang memiliki cakupan yang diinginkan, dan presenter disuntikkan ke dalam pandangannya.
class TextInputFieldWidgetConfigurator : WidgetScreenConfigurator() {
}
Satu-satunya perbedaan antara widget dan penerapan layar penuh kami (Aktivitas, Fragmen) adalah bahwa widget tidak memiliki banyak metode siklus hidup (onStart, onResume, onPause). Ini hanya memiliki metode onCreate, yang menunjukkan bahwa widget saat ini telah menciptakan ruang lingkupnya, dan sendoknya dihancurkan dalam metode onDetachedFromWindow. Tetapi untuk kenyamanan dan konsistensi, penyaji widget mendapatkan metode siklus hidup yang sama dengan sisa layar. Peristiwa ini secara otomatis dikirimkan kepadanya dari orang tua. Perlu dicatat bahwa kelas dasar dari presenter widget adalah kelas dasar yang sama dari presenter layar lain.Menggunakan widget dinamis
Mari kita beralih ke implementasi kasus yang dijelaskan di awal artikel.- Di presenter layar, data untuk formulir terjemahan dimuat, data ditransmisikan ke tampilan untuk rendering. Pada tahap ini, tidak masalah bagi kami apakah tampilan layar aktivitas adalah fragmen atau widget. Kami hanya tertarik memiliki RecyclerView dan merender bentuk dinamis dengannya.
private val sm = TransferFormScreenModel()
โฆ
private fun loadData() {
loadDataDisposable.dispose()
loadDataDisposable = subscribe(
observerDataForTransfer().io(),
{ data ->
sm.data = data
view.render(sm)
},
{ error -> }
)
}
- Data formulir ditransfer ke adaptor daftar dan diterjemahkan menggunakan widget yang ada di ViewHolder untuk setiap elemen formulir unik. ViewHolder yang diinginkan untuk rendering komponen ditentukan berdasarkan tipe komponen form yang telah ditentukan.
fun render(sm: TransferFormScreenModel) {
val list = ItemList.create()
sm.data
.filter { transferField -> transferField.visible }
.forEach { transferField ->
when (transferField.type) {
TransferFieldType.PHONE_INPUT -> {
list.add(
PhoneInputFieldData(transferField),
phoneInputController
)
}
TransferFieldType.MONEY -> {
list.add(
MoneyInputFieldData(transferField),
moneyInputController
)
}
TransferFieldType.BUTTON -> {
list.add(
ButtonInputFieldData(transferField),
buttonController
)
}
else -> {
list.add(
TextInputFieldData(transferField),
textInputController
)
}
}
}
adapter.setItems(list)
}
- Widget diinisialisasi dalam metode mengikat ViewHolder. Selain mentransmisikan data untuk render, penting juga untuk menetapkan id unik untuk widget, atas dasar di mana ruang lingkup DI-nya akan dibentuk. Dalam kasus kami, setiap elemen formulir memiliki id unik, yang bertanggung jawab untuk pengangkatan input dan datang sebagai respons selain jenis elemen (jenis dapat diulang pada formulir).
override fun bind(data: TransferFieldUi) {
itemView.findViewById(R.id.field_tif).initialize(...)
}
- Metode inisialisasi menginisialisasi data tampilan widget, yang kemudian dikirim ke presenter menggunakan metode siklus hidup onCreate, di mana nilai-nilai bidang diatur ke model widget dan render-nya.
fun initialize(
id: String = this.id,
value: String = this.value,
hint: String = this.hint,
errorText: String = this.errorText
) {
this.id = id
this.value = value
this.hint = hint
this.errorText = errorText
}
override fun onCreate() {
presenter.onCreate(value, hint, errorText)
}
fun onCreate(value: String, hint: String, errorText: String) {
sm.value = value
sm.hint = hint
sm.errorText = errorText
view.render(sm)
}
Batuan bawah laut
Seperti dapat dilihat dari uraiannya, implementasinya sangat sederhana dan intuitif. Namun, ada nuansa yang perlu diperhatikan.Pertimbangkan siklus hidup widget
Karena kelas dasar widget adalah pewaris ViewGroup yang umum digunakan, kami juga mengetahui siklus hidup widget. Biasanya, widget diinisialisasi dalam ViewHolder dengan memanggil metode khusus tempat data ditransfer, seperti yang ditunjukkan pada paragraf sebelumnya. Inisialisasi lainnya terjadi di onCreate (misalnya, mengatur pendengar klik) - metode ini dipanggil setelah onAttachedToWindow menggunakan delegasi khusus yang mengontrol entitas kunci dari logika widget.
public abstract class CoreFrameLayoutView
extends FrameLayout implements CoreWidgetViewInterface {
โฆ
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (!isManualInitEnabled) {
widgetViewDelegate = createWidgetViewDelegate();
widgetViewDelegate.onCreate();
}
}
public void onCreate() {
}
public class WidgetViewDelegate {
โฆ
public void onCreate() {
coreWidgetView.onCreate();
}
Selalu bersihkan pendengarnya
Jika ada bidang tergantung pada formulir, kita mungkin perluDetachedFromWindow. Pertimbangkan kasus berikut: formulir terjemahan memiliki banyak bidang, di antaranya ada daftar drop-down. Bergantung pada nilai yang dipilih dalam daftar, bidang input formulir tambahan dapat menjadi terlihat atau yang sudah ada dapat menghilang.Dalam kasus yang dijelaskan di atas, sangat penting untuk menghapus semua pendengar widget dalam metode onDetachedFromWindow, karena jika Anda menambahkan widget ke daftar lagi, semua pendengar akan diinisialisasi ulang.
override fun onCreate() {
initListeners()
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
clearListeners()
}
Tangani langganan acara widget dengan benar
Presentasi layar yang berisi widget harus diberitahu tentang perubahan input setiap widget. Implementasi yang paling jelas dari setiap widget menggunakan acara memancarkan dan berlangganan semua acara dengan presenter layar. Acara harus berisi id widget dan datanya. Yang terbaik adalah menerapkan logika ini sehingga nilai input saat ini disimpan dalam model layar dan ketika Anda mengklik tombol, data yang sudah selesai dikirim dalam permintaan. Dengan pendekatan ini, lebih mudah untuk menerapkan validasi formulir: itu terjadi ketika tombol diklik, dan jika tidak ada kesalahan, permintaan dikirim dengan data formulir disimpan terlebih dahulu.
private val defaultTextChangedListener = object : OnMaskedValueChangedListener {
override fun onValueChanged(value: String) {
presenter.onTextChange(value, id)
}
}
sealed class InputValueType(val id: String)
class TextValue(id: String, val value: String) : InputValueType(id)
class DataEvent(val data: InputValueType)
fun onTextChange(value: String, id: String) {
rxBus.emitEvent(DataEvent(data = TextValue(id = id, value = value)))
}
private fun subscribeToEvents() {
subscribe(rxBus.observeEvents(DataEvent::class.java))
{
handleValue(it.data) // handle data
}
}
private fun handleValue(value: InputValueType) {
val id = value.id
when (value) {
// handle event using its type, saving event value using its id
is TextValue -> {
sm.fieldValuesMap[id] = value.value
}
else -> {
// handle other events
}
}
}
// TransferScreenModel
class TransferScreenModel : ScreenModel() {
// map for form values: key = input id
val fieldValuesMap: MutableMap<String, String> = mutableMapOf()
}
Ada opsi implementasi kedua, di mana acara dari widget dengan datanya datang hanya setelah mengklik tombol, dan bukan saat Anda mengetik, yaitu, kami mengumpulkan semua data segera sebelum mengirim permintaan. Dengan opsi ini, akan ada lebih sedikit peristiwa, tetapi perlu dicatat bahwa implementasi ini dapat berubah menjadi tidak praktis dalam praktiknya, dan logika tambahan akan diperlukan untuk memeriksa apakah semua peristiwa telah diterima.Unify All Requirements
Saya ingin sekali lagi mencatat bahwa kasus yang dijelaskan hanya mungkin setelah mengoordinasikan persyaratan dengan backend.Persyaratan apa yang perlu disatukan:- Jenis bidang. Setiap bidang harus diharapkan oleh aplikasi seluler untuk tampilan dan pemrosesan yang benar.
- โ , , , .
- , .
- . , : , , โ , -, .
Ini diperlukan agar komponen yang diterima dalam respons diketahui oleh aplikasi seluler untuk tampilan dan pemrosesan logika yang benar.Nuansa kedua adalah bahwa komponen-komponen bentuk itu sendiri umumnya tidak tergantung satu sama lain, namun beberapa skenario dimungkinkan ketika, misalnya, visibilitas satu elemen tergantung pada keadaan elemen lainnya, seperti dijelaskan di atas. Untuk mengimplementasikan logika ini, perlu agar elemen-elemen dependen selalu datang bersama-sama, dan respon harus mengandung deskripsi dari logika, komponen mana yang saling bergantung dan bagaimana. Dan tentu saja, semua ini harus disepakati dengan tim server sebelum memulai pengembangan.Kesimpulan
Jadi, ketika menerapkan bahkan kasus standar seperti mengisi daftar dinamis, Anda selalu dapat berhenti pada solusi yang sudah ada. Bagi kami, ini adalah konsep baru, yang memungkinkan kami untuk memilih potongan atom logika dan representasi dari layar besar dan kami berhasil mendapatkan solusi yang dapat diperluas yang mudah dipelihara karena kesamaan widget dengan pandangan lain. Dalam implementasi kami, widget dikembangkan berdasarkan pola RxPM - setelah menambahkan binder, widget menjadi lebih nyaman digunakan, tetapi ini adalah cerita yang sangat berbeda.tautan yang bermanfaat
- Kerangka pengembangan aplikasi Android Surf
- Modul widget
- Implementasi kami dalam membuat daftar sederhana yang rumit
- Pola PresentationModel