UI Backend-Driven dengan widget

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:

  1. Daftar akun yang tersedia untuk ditransfer;
  2. Nama jenis terjemahan;
  3. Bidang untuk memasukkan nomor telepon (memiliki topeng untuk memasukkan dan berisi ikon untuk memilih kontak dari perangkat);
  4. 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:

  1. 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)
      }
    }
    

  2. Kelas untuk presenter, di mana logika dasar widget dijelaskan, misalnya:

    1. memuat data dan mengirimkannya untuk render;
    2. 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.
  3. 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 = โ€œโ€
    }
    

  4. Kelas konfigurator untuk mengimplementasikan DI, dengan bantuan dependensi widget yang memiliki cakupan yang diinginkan, dan presenter disuntikkan ke dalam pandangannya.

    class TextInputFieldWidgetConfigurator : WidgetScreenConfigurator() {
    	// logic for components injection
    }
    


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.

  1. 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.

    // TransferFormPresenter
    private val sm = TransferFormScreenModel()
    โ€ฆ
    private fun loadData() {
    	loadDataDisposable.dispose()
      	loadDataDisposable = subscribe(
                  observerDataForTransfer().io(), 
                  { data -> 
                          sm.data = data
                          view.render(sm)
                  },
                  { error -> /* error handling */ }
      	)
     }
    

  2. 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.

    // TransferFormView
    fun render(sm: TransferFormScreenModel) {
        //      
        //   EasyAdapter [3]
        val list = ItemList.create()
        //       Controller,
        //       
    
        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
                        )
                    }
                }
            }
            //     RecyclerView
            adapter.setItems(list)
    }  
    

  3. 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).

    // ViewHolder
    override fun bind(data: TransferFieldUi) {
    	// get initialize params from given data
    	itemView.findViewById(R.id.field_tif).initialize(...)
    }
    

  4. 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.

    // TextInputFieldWidget
    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)
           // other logic...
    }
    // TextInputFieldPresenter
    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.

// CoreFrameLayoutView (      ViewGroup)
public abstract class CoreFrameLayoutView
          extends FrameLayout implements CoreWidgetViewInterface {
โ€ฆ
@Override
protected void onAttachedToWindow() {
   super.onAttachedToWindow();
   if (!isManualInitEnabled) {
        widgetViewDelegate = createWidgetViewDelegate();
        widgetViewDelegate.onCreate();
   }
}

public void onCreate() {
    //empty. define in descendant class if needed
}

// WidgetViewDelegate
public class WidgetViewDelegate {
โ€ฆ
public void onCreate() {
   // other logic of widget initialization
   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.
nilai tarik-turun untuk memilih jenis terjemahanVisibilitas bidang input periode pembayaranvisibilitas bidang input nomor telepon
transfer dengan nomor teleponSalahbenar
pembayaranbenarSalah

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.

// TextInputFieldWidget
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.

// TextInputFieldWidget
private val defaultTextChangedListener = object : OnMaskedValueChangedListener {
        override fun onValueChanged(value: String) {
            presenter.onTextChange(value, id)
        }
}

// Events.kt
sealed class InputValueType(val id: String)

class TextValue(id: String, val value: String) : InputValueType(id)

class DataEvent(val data: InputValueType)

// TextInputFieldPresenter -  
fun onTextChange(value: String, id: String) {
	rxBus.emitEvent(DataEvent(data = TextValue(id = id, value = value)))
}

// TransferFormPresenter -  
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


  1. Kerangka pengembangan aplikasi Android Surf
  2. Modul widget
  3. Implementasi kami dalam membuat daftar sederhana yang rumit
  4. Pola PresentationModel

All Articles