واجهة مستخدم تستند إلى الواجهة الخلفية مع عناصر واجهة المستخدم

فكر في ميزات هذا النهج وتنفيذنا باستخدام الأدوات ومفهومها ومزاياها واختلافاتها عن طرق العرض الأخرى في Android.



واجهة مستخدم تستند إلى الواجهة الخلفية - نهج يسمح لك بإنشاء مكونات واجهة المستخدم بناءً على استجابة الخادم. يجب أن يحتوي وصف واجهة برمجة التطبيقات على أنواع المكونات وخصائصها ، ويجب أن يعرض التطبيق المكونات اللازمة اعتمادًا على الأنواع والخصائص. بشكل عام ، يمكن دمج منطق المكونات ، وبالنسبة لتطبيقات الهاتف المحمول ، فهي عبارة عن صندوق أسود ، حيث يمكن أن يكون لكل مكون منطق مستقل عن بقية التطبيق ويمكن تكوينه بشكل تعسفي بواسطة الخادم ، اعتمادًا على منطق الأعمال المطلوب. هذا هو السبب في أن هذا النهج يستخدم غالبًا في التطبيقات المصرفية: على سبيل المثال ، عندما تحتاج إلى عرض نموذج ترجمة يحتوي على عدد كبير من الحقول المحددة ديناميكيًا. التطبيق لا يعرف مقدما تكوين النموذج وترتيب الحقول فيه ، وبالتالي ،هذا النهج هو الطريقة الوحيدة لكتابة التعليمات البرمجية دون العكازات. بالإضافة إلى ذلك ، فإنه يضيف مرونة: من جانب الخادم ، يمكنك تغيير النموذج في أي وقت ، وسيكون تطبيق الهاتف المحمول جاهزًا لذلك.

حالة الاستخدام




يتم عرض الأنواع التالية من المكونات أعلاه:

  1. قائمة الحسابات المتاحة للتحويل ؛
  2. اسم نوع الترجمة ؛
  3. حقل لإدخال رقم هاتف (يحتوي على قناع للدخول ويحتوي على رمز لتحديد جهات الاتصال من الجهاز) ؛
  4. حقل لإدخال مبلغ التحويل.

أيضًا في النموذج ، أي عدد من المكونات الأخرى المضمنة في منطق الأعمال وتحديدها في مرحلة التصميم ممكن. يجب أن تستوفي المعلومات حول كل مكون يأتي في استجابة الخادم المتطلبات ، ويجب أن يتوقع التطبيق المحمول كل مكون لمعالجته بشكل صحيح.



تحتوي حقول الإدخال المختلفة على أقنعة وقواعد تحقق مختلفة ؛ قد يحتوي الزر على رسم متحرك وميض في وقت التمهيد ؛ يمكن أن يكون لعنصر واجهة مستخدم لتحديد حساب خصم الرسوم المتحركة رسوم متحركة عند التمرير ، وما إلى ذلك.

جميع مكونات واجهة المستخدم مستقلة عن بعضها البعض ، ويمكن أخذ المنطق في طرق عرض منفصلة مع مجالات مسؤولية مختلفة - دعنا نطلق عليها عناصر واجهة المستخدم. تتلقى كل أداة تكوينها في استجابة الخادم وتغلف منطق العرض ومعالجة البيانات.

عند تنفيذ الشاشة ، يكون RecyclerView هو الأنسب ، وسوف تحتوي عناصرها على عناصر واجهة المستخدم. سيقوم ViewHolder لكل عنصر قائمة فريد بتهيئة الأداة وإعطائها البيانات التي يحتاجها لعرضها.

مفهوم القطعة


دعونا ننظر في الحاجيات بمزيد من التفصيل. القطعة هي في جوهرها طريقة عرض مخصصة "بأقصى سرعة". يمكن أن تحتوي طريقة العرض المخصصة العادية أيضًا على البيانات ومنطق العرض الخاص بها ، ولكن عنصر واجهة المستخدم يعني شيئًا أكثر - يحتوي على مقدم ، ونموذج شاشة وله نطاق DI خاص به.

قبل الغوص في تفاصيل تنفيذ الحاجيات ، نناقش مزاياها:

  • , , «» , — UI- , .
  • , , .
  • , — : , , .


لتنفيذ عناصر واجهة استخدام قابلة للتطوير ، نستخدم الفئات الأساسية لمجموعة ViewGroups الأكثر استخدامًا ، والتي لها نطاقات DI الخاصة بها. جميع الفئات الأساسية ، بدورها ، موروثة من الواجهة المشتركة ، والتي تحتوي على كل ما هو ضروري لتهيئة الأدوات.

أسهل طريقة لاستخدام الأدوات هي طرق العرض الثابتة ، المحددة مباشرة في التخطيط. بعد تنفيذ فئات عنصر واجهة المستخدم ، يمكنك إضافته بأمان إلى تخطيط XML ، دون نسيان تحديد معرفه في التخطيط (بناءً على المعرف ، سيتم تشكيل نطاق DI لعنصر واجهة مستخدم).

في هذه المقالة ، نعتبر الحاجيات الديناميكية بمزيد من التفصيل ، حيث يتم حل حالة نموذج الترجمة الموضح أعلاه مع مجموعة عشوائية من الحقول بمساعدتهم.

لا تختلف أي أداة ، سواء ثابتة أو ديناميكية ، في تنفيذنا تقريبًا عن العرض العادي من حيث MVP. عادة ، هناك حاجة إلى 4 فئات لتنفيذ عنصر واجهة المستخدم:

  1. عرض الفصل الدراسي ، حيث يحدث تخطيط التخطيط وعرض المحتوى ؛

    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. فئة للمقدم ، حيث يتم وصف المنطق الأساسي لعنصر واجهة التعامل ، على سبيل المثال:

    1. تحميل البيانات وإرسالها لتقديمها ؛
    2. الاشتراك في الأحداث المختلفة وانبعاث أحداث تغييرات إدخال القطعة ؛

    @PerScreen
    class TextInputFieldPresenter @Inject constructor(
            basePresenterDependency: BasePresenterDependency,
            rxBus: RxBus
    ) : BaseInputFieldPresenter<TextInputFieldWidget>(
           basePresenterDependency, rxBus
    ) {
    private val sm = TextInputFieldScreenModel()
    ...
    }
    

    في تطبيقنا ، فإن فئة RxBus عبارة عن ناقل يستند إلى PublishSubject لإرسال الأحداث والاشتراك فيها.
  3. فئة لنموذج الشاشة ، بمساعدة يتلقى مقدم البيانات البيانات ونقلها لعرضها في طريقة عرض (من حيث نمط نموذج العرض) ؛

    class TextInputFieldScreenModel : ScreenModel() {
    	val value = String = “”
    	val hint = String = “”
    	val errorText = String = “”
    }
    

  4. فئة مكون لتنفيذ DI ، بمساعدة يتم تسليم تبعيات عنصر واجهة المستخدم التي لها النطاق المطلوب ، ويتم إدخال مقدم العرض في طريقة العرض الخاصة به.

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


الاختلاف الوحيد بين الأدوات وتطبيقنا للشاشات الكاملة (النشاط ، جزء) هو أن الأداة لا تحتوي على العديد من طرق دورة الحياة (onStart ، onResume ، onPause). لديها فقط طريقة onCreate ، والتي تظهر أن الأداة قد أنشأت نطاقها حاليًا ، ويتم تدمير المجرفة في طريقة onDetachedFromWindow. ولكن من أجل الراحة والاتساق ، يحصل مقدم الأداة على نفس أساليب دورة الحياة مثل باقي الشاشات. يتم إرسال هذه الأحداث إليه تلقائيًا من الوالد. وتجدر الإشارة إلى أن الفئة الأساسية لمقدم الأداة هي نفس الفئة الأساسية لمقدمي الشاشات الأخرى.

باستخدام الحاجيات الديناميكية


دعنا ننتقل إلى تنفيذ الحالة الموضحة في بداية المقالة.

  1. في مقدم الشاشة ، يتم تحميل بيانات نموذج الترجمة ، ويتم نقل البيانات إلى العرض لتقديمها. في هذه المرحلة ، لا يهمنا ما إذا كان عرض شاشة النشاط عبارة عن جزء أو أداة. نحن مهتمون فقط بامتلاك RecyclerView وتقديم شكل ديناميكي معه.

    // 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. يتم نقل بيانات النموذج إلى محول القائمة ويتم تقديمها باستخدام أدوات موجودة في ViewHolder لكل عنصر نموذج فريد. يتم تحديد ViewHolder المطلوب لعرض المكون بناءً على الأنواع المحددة مسبقًا لمكونات النموذج.

    // 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. تتم تهيئة عنصر واجهة المستخدم في طريقة ربط ViewHolder. بالإضافة إلى إرسال البيانات للعرض ، من المهم أيضًا تعيين معرف فريد لعنصر واجهة المستخدم ، والذي سيتم على أساسه تشكيل نطاق DI الخاص به. في حالتنا ، كان لكل عنصر في النموذج معرف فريد ، كان مسؤولاً عن تعيين المدخلات وجاء استجابة بالإضافة إلى نوع العنصر (يمكن تكرار الأنواع في النموذج).

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

  4. يقوم أسلوب التهيئة بتهيئة بيانات عرض عنصر واجهة المستخدم ، والتي يتم إرسالها بعد ذلك إلى المقدم باستخدام طريقة دورة حياة onCreate ، حيث يتم تعيين قيم الحقول على نموذج عنصر واجهة المستخدم وعرضه.

    // 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)
    }
    


صخور تحت الماء


كما يتبين من الوصف ، فإن التنفيذ بسيط للغاية وبديهي. ومع ذلك ، هناك فروق دقيقة تحتاج إلى النظر فيها.

خذ بعين الاعتبار دورة حياة الحاجيات


نظرًا لأن الفئات الأساسية لعناصر واجهة المستخدم هي ورثة لـ ViewGroups المستخدمة بشكل شائع ، فإننا نعرف أيضًا دورة حياة الأدوات. عادة ، تتم تهيئة عناصر واجهة المستخدم في ViewHolder عن طريق استدعاء طريقة خاصة حيث يتم نقل البيانات ، كما هو موضح في الفقرة السابقة. تتم التهيئة الأخرى في onCreate (على سبيل المثال ، تعيين مستمعين للنقر) - تسمى هذه الطريقة بعد onAttachedToWindow باستخدام مفوض خاص يتحكم في الكيانات الرئيسية لمنطق الأداة.

// 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();
}

قم دائمًا بتنظيف المستمعين


إذا كانت هناك حقول تابعة في النموذج ، فقد نحتاج إلى onDetachedFromWindow. خذ بعين الاعتبار الحالة التالية: يحتوي نموذج الترجمة على العديد من الحقول ، من بينها قائمة منسدلة. بناءً على القيمة المحددة في القائمة ، قد يصبح حقل إدخال نموذج إضافي مرئيًا أو قد يختفي حقل موجود.
قائمة منسدلة لاختيار نوع الترجمةوضوح حقل إدخال فترة الدفعرؤية حقل إدخال رقم الهاتف
التحويل برقم الهاتفخاطئةصحيح
دفعصحيحخاطئة

في الحالة الموضحة أعلاه ، من المهم جدًا مسح جميع مستمعي الأداة في طريقة onDetachedFromWindow ، نظرًا لأنه إذا قمت بإضافة الأداة إلى القائمة مرة أخرى ، فستتم إعادة تهيئة جميع المستمعين.

// TextInputFieldWidget
override fun onCreate() {
     initListeners()
}

override fun onDetachedFromWindow() {
      super.onDetachedFromWindow()
      clearListeners()
}

التعامل مع اشتراكات حدث القطعة بشكل صحيح


يجب إخطار عرض الشاشة التي تحتوي على عناصر واجهة المستخدم بالتغييرات في إدخال كل عنصر واجهة مستخدم. التنفيذ الأكثر وضوحًا لكل أداة باستخدام الأحداث المنبعثة والاشتراك في جميع الأحداث باستخدام مقدم شاشة. يجب أن يحتوي الحدث على معرف القطعة وبياناتها. من الأفضل تنفيذ هذا المنطق بحيث يتم حفظ قيم الإدخال الحالية في طراز الشاشة ، وعند النقر فوق الزر ، يتم إرسال البيانات النهائية في الطلب. باستخدام هذا الأسلوب ، من الأسهل تنفيذ التحقق من صحة النموذج: يحدث عند النقر فوق زر ، وفي حالة عدم وجود أخطاء ، يتم إرسال الطلب مع بيانات النموذج المحفوظة مسبقًا.

// 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()
}

هناك خيار تنفيذ ثانٍ ، حيث لا تأتي الأحداث من الأدوات مع بياناتها إلا بعد النقر على الزر ، وليس أثناء الكتابة ، أي أننا نجمع جميع البيانات مباشرة قبل إرسال الطلب. باستخدام هذا الخيار ، سيكون هناك عدد أقل من الأحداث ، ولكن من الجدير بالذكر أن هذا التطبيق قد يتحول إلى غير بديهي في الممارسة العملية ، وستكون هناك حاجة إلى منطق إضافي للتحقق مما إذا تم استلام جميع الأحداث.

توحيد جميع المتطلبات


أود أن أشير مرة أخرى إلى أن الحالة الموصوفة ممكنة فقط بعد تنسيق المتطلبات مع الواجهة الخلفية.

ما هي المتطلبات التي يجب توحيدها:

  • أنواع الحقول. يجب أن يتوقع التطبيق المحمول كل حقل لعرضه ومعالجته بشكل صحيح.
  • — , , , .
  • , .
  • . , : , , — , -, .



يعد ذلك ضروريًا حتى يمكن معرفة المكون الذي تم تلقيه في الاستجابة لتطبيقات الهاتف المحمول من أجل العرض الصحيح ومعالجة المنطق.

الفروق الدقيقة الثانية هي أن مكونات النموذج نفسها مستقلة بشكل عام عن بعضها البعض ، ومع ذلك ، بعض السيناريوهات ممكنة عندما ، على سبيل المثال ، تعتمد رؤية عنصر على حالة عنصر آخر ، كما هو موضح أعلاه. لتنفيذ هذا المنطق ، من الضروري أن تجتمع العناصر التابعة دائمًا دائمًا ، ويجب أن تحتوي الاستجابة على وصف للمنطق ، والمكونات التي تعتمد على بعضها البعض وكيف. وبالطبع ، يجب الاتفاق على كل هذا مع فريق الخادم قبل بدء التطوير.

استنتاج


وبالتالي ، عند تنفيذ مثل هذه الحالة القياسية مثل ملء قائمة ديناميكية ، لا يمكنك دائمًا التوقف عند الحلول الموجودة بالفعل. بالنسبة لنا ، كان هذا مفهومًا جديدًا ، سمح لنا باختيار الأجزاء الذرية من المنطق والتمثيل من الشاشات الضخمة وتمكنا من الحصول على حل قابل للتوسيع يعمل بسهولة الصيانة بسبب تشابه الحاجيات مع وجهات النظر الأخرى. في تطبيقنا ، تم تطوير الأدوات من حيث نمط RxPM - بعد إضافة المجلدات ، أصبح من الأسهل استخدام الأدوات ، ولكن هذه قصة مختلفة تمامًا.

روابط مفيدة


  1. إطار تطوير تطبيقات Android Surf
  2. وحدة القطعة
  3. لدينا تنفيذ بسيطة تجعل من القوائم المعقدة
  4. نموذج العرض التقديمي

All Articles