محوِّلات مُلزِمة

تعد محولات الربط مسؤولة عن إجراء استدعاءات إطار العمل المناسبة لتعيين القيم. ومن الأمثلة على ذلك ضبط قيمة سمة، مثل استدعاء طريقة setText(). ومثال آخر هو إعداد أداة معالجة حدث، مثل استدعاء الطريقة setOnClickListener().

تتيح لك "مكتبة ربط البيانات" تحديد الطريقة التي يتم استدعاء استخدامها لتعيين قيمة، وتوفير منطق الربط الخاص بك، وتحديد نوع الكائن المعروض باستخدام المحولات.

ضبط قيم السمات

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

الاختيار التلقائي للطريقة

بالنسبة إلى السمة المسماة example، تعثر المكتبة تلقائيًا على الطريقة setExample(arg) التي تقبل الأنواع المتوافقة كوسيطة. لا يتم مراعاة مساحة الاسم للسمة. يتم استخدام اسم السمة ونوعها فقط عند البحث عن طريقة.

على سبيل المثال، في التعبير android:text="@{user.name}"، تبحث المكتبة عن طريقة setText(arg) التي تقبل النوع الذي يعرضه user.getName(). إذا كان نوع إرجاع user.getName() هو String، تبحث المكتبة عن أسلوب setText() الذي يقبل الوسيطة String. إذا عرض التعبير int، ستبحث المكتبة عن طريقة setText() تقبل وسيطة int. يجب أن يعرض التعبير النوع الصحيح. يمكنك عرض القيمة المعروضة إذا لزم الأمر.

يعمل ربط البيانات حتى في حال عدم وجود سمة بالاسم المعنيّ. يمكنك إنشاء سمات لأي دالة setter باستخدام ربط البيانات. على سبيل المثال، لا تحتوي فئة الدعم DrawerLayout على سمات، ولكنها تحتوي على الكثير من القيم. يستخدم التنسيق التالي تلقائيًا طريقتَي setScrimColor(int) وaddDrawerListener(DrawerListener) كأداة ضبط للسمتَين app:scrimColor وapp:drawerListener على التوالي:

<androidx.drawerlayout.widget.DrawerLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:scrimColor="@{@color/scrim}"
    app:drawerListener="@{fragment.drawerListener}">

تحديد اسم طريقة مخصصة

تحتوي بعض السمات على قيم تعيين لا تتطابق حسب الاسم. في هذه الحالات، يمكن ربط سمة بالدالة setter باستخدام التعليق التوضيحي BindingMethods. يُستخدم التعليق التوضيحي مع فئة ويمكن أن يحتوي على تعليقات BindingMethod توضيحية متعددة، واحدة لكل طريقة تمت إعادة تسميتها. طرق الربط هي تعليقات توضيحية يمكنك إضافتها إلى أي فئة في تطبيقك.

في المثال التالي، ترتبط السمة android:tint بالطريقة setImageTintList(ColorStateList)، وليس بالطريقة setTint():

Kotlin

@BindingMethods(value = [
    BindingMethod(
        type = android.widget.ImageView::class,
        attribute = "android:tint",
        method = "setImageTintList")])

Java

@BindingMethods({
       @BindingMethod(type = "android.widget.ImageView",
                      attribute = "android:tint",
                      method = "setImageTintList"),
})

وعادةً، لا تحتاج إلى إعادة تسمية أدوات الإعداد في فئات إطار عمل Android. سبق أن تم تنفيذ السمات باستخدام اصطلاح الاسم للعثور على طرق المطابقة تلقائيًا.

تقديم منطق مخصّص

تحتاج بعض السمات إلى منطق ربط مخصّص. على سبيل المثال، ما من دالة تعيين مرتبطة للسمة android:paddingLeft. ويتم توفير طريقة setPadding(left, top, right, bottom) بدلاً من ذلك. تتيح لك طريقة محوِّل الربط الثابت ذات التعليق التوضيحي BindingAdapter تخصيص طريقة استدعاء دالة setter للسمة.

تتضمّن سمات فئات إطار عمل Android تعليقات BindingAdapter التوضيحية حاليًا. يعرض المثال التالي محوِّل الربط لسمة paddingLeft:

Kotlin

@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, padding: Int) {
    view.setPadding(padding,
                view.getPaddingTop(),
                view.getPaddingRight(),
                view.getPaddingBottom())
}

Java

@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
  view.setPadding(padding,
                  view.getPaddingTop(),
                  view.getPaddingRight(),
                  view.getPaddingBottom());
}

تعتبر أنواع المَعلمات مهمة. تحدد المعلمة الأولى نوع العرض المرتبط بالسمة. تحدد المعلمة الثانية النوع المقبول في تعبير الربط للسمة المحددة.

تُعد محوِّلات الربط مفيدة أيضًا لأنواع أخرى من التخصيص. على سبيل المثال، يمكن استدعاء برنامج تحميل مخصّص من مؤشر ترابط عامل لتحميل صورة.

يمكنك أيضًا استخدام محوّلات تتلقّى عدة سمات، كما هو موضّح في المثال التالي:

Kotlin

@BindingAdapter("imageUrl", "error")
fun loadImage(view: ImageView, url: String, error: Drawable) {
    Picasso.get().load(url).error(error).into(view)
}

Java

@BindingAdapter({"imageUrl", "error"})
public static void loadImage(ImageView view, String url, Drawable error) {
  Picasso.get().load(url).error(error).into(view);
}

يمكنك استخدام المحوّل في التنسيق، كما هو موضّح في المثال التالي. يُرجى العِلم أنّ السمة @drawable/venueError تشير إلى مورد في تطبيقك. إحاطة المورد بـ @{} يجعله تعبيرًا ربطًا صالحًا.

<ImageView app:imageUrl="@{venue.imageUrl}" app:error="@{@drawable/venueError}" />

يتم استدعاء المحوِّل إذا تم استخدام imageUrl وerror لكائن ImageView، وimageUrl عبارة عن سلسلة، وerror هو Drawable. إذا كنت تريد طلب المحوِّل عند ضبط أي من السمات، اضبط العلامة الاختيارية requireAll للمحوِّل على false، كما هو موضّح في المثال التالي:

Kotlin

@BindingAdapter(value = ["imageUrl", "placeholder"], requireAll = false)
fun setImageUrl(imageView: ImageView, url: String?, placeHolder: Drawable?) {
    if (url == null) {
        imageView.setImageDrawable(placeholder);
    } else {
        MyImageLoader.loadInto(imageView, url, placeholder);
    }
}

Java

@BindingAdapter(value={"imageUrl", "placeholder"}, requireAll=false)
public static void setImageUrl(ImageView imageView, String url, Drawable placeHolder) {
  if (url == null) {
    imageView.setImageDrawable(placeholder);
  } else {
    MyImageLoader.loadInto(imageView, url, placeholder);
  }
}

يمكن لطرق محوِّل الربط أن تأخذ القيم القديمة في معالِجاتها. ويجب أن تعلن الطريقة التي تأخذ القيم القديمة والجديدة عن جميع القيم القديمة للسمات أولاً، متبوعة بالقيم الجديدة، كما هو موضح في المثال التالي:

Kotlin

@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, oldPadding: Int, newPadding: Int) {
    if (oldPadding != newPadding) {
        view.setPadding(newPadding,
                    view.getPaddingTop(),
                    view.getPaddingRight(),
                    view.getPaddingBottom())
    }
}

Java

@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int oldPadding, int newPadding) {
  if (oldPadding != newPadding) {
      view.setPadding(newPadding,
                      view.getPaddingTop(),
                      view.getPaddingRight(),
                      view.getPaddingBottom());
   }
}

لا يمكن استخدام معالِجات الأحداث إلا مع الواجهات أو فئات التجريد من خلال طريقة مجردة واحدة، كما هو موضّح في المثال التالي:

Kotlin

@BindingAdapter("android:onLayoutChange")
fun setOnLayoutChangeListener(
        view: View,
        oldValue: View.OnLayoutChangeListener?,
        newValue: View.OnLayoutChangeListener?
) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        if (oldValue != null) {
            view.removeOnLayoutChangeListener(oldValue)
        }
        if (newValue != null) {
            view.addOnLayoutChangeListener(newValue)
        }
    }
}

Java

@BindingAdapter("android:onLayoutChange")
public static void setOnLayoutChangeListener(View view, View.OnLayoutChangeListener oldValue,
       View.OnLayoutChangeListener newValue) {
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
    if (oldValue != null) {
      view.removeOnLayoutChangeListener(oldValue);
    }
    if (newValue != null) {
      view.addOnLayoutChangeListener(newValue);
    }
  }
}

استخدِم معالج الأحداث هذا في التنسيق على النحو التالي:

<View android:onLayoutChange="@{() -> handler.layoutChanged()}"/>

عندما تكون لدى المستمع عدة طرق، يجب تقسيمه إلى عدة مستمعين. على سبيل المثال، هناك طريقتان لاستخدام View.OnAttachStateChangeListener: onViewAttachedToWindow(View) وonViewDetachedFromWindow(View). توفر المكتبة واجهتين للتمييز بين السمات والمعالجات لها:

Kotlin

// Translation from provided interfaces in Java:
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
interface OnViewDetachedFromWindow {
    fun onViewDetachedFromWindow(v: View)
}

@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
interface OnViewAttachedToWindow {
    fun onViewAttachedToWindow(v: View)
}

Java

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
  void onViewDetachedFromWindow(View v);
}

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
  void onViewAttachedToWindow(View v);
}

ولأن تغيير أحد المستمعين يمكن أن يؤثر على الآخر، فأنت بحاجة إلى محوّل يناسب أي من السمتين أو لكليهما. يمكنك ضبط requireAll على false في التعليق التوضيحي لتحديد أنه لا يجب تعيين تعبير ربط لكل سمة، كما هو موضّح في المثال التالي:

Kotlin

@BindingAdapter(
        "android:onViewDetachedFromWindow",
        "android:onViewAttachedToWindow",
        requireAll = false
)
fun setListener(view: View, detach: OnViewDetachedFromWindow?, attach: OnViewAttachedToWindow?) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
        val newListener: View.OnAttachStateChangeListener?
        newListener = if (detach == null && attach == null) {
            null
        } else {
            object : View.OnAttachStateChangeListener {
                override fun onViewAttachedToWindow(v: View) {
                    attach.onViewAttachedToWindow(v)
                }

                override fun onViewDetachedFromWindow(v: View) {
                    detach.onViewDetachedFromWindow(v)
                }
            }
        }

        val oldListener: View.OnAttachStateChangeListener? =
                ListenerUtil.trackListener(view, newListener, R.id.onAttachStateChangeListener)
        if (oldListener != null) {
            view.removeOnAttachStateChangeListener(oldListener)
        }
        if (newListener != null) {
            view.addOnAttachStateChangeListener(newListener)
        }
    }
}

Java

@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"}, requireAll=false)
public static void setListener(View view, OnViewDetachedFromWindow detach, OnViewAttachedToWindow attach) {
    if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
        OnAttachStateChangeListener newListener;
        if (detach == null && attach == null) {
            newListener = null;
        } else {
            newListener = new OnAttachStateChangeListener() {
                @Override
                public void onViewAttachedToWindow(View v) {
                    if (attach != null) {
                        attach.onViewAttachedToWindow(v);
                    }
                }
                @Override
                public void onViewDetachedFromWindow(View v) {
                    if (detach != null) {
                        detach.onViewDetachedFromWindow(v);
                    }
                }
            };
        }

        OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view, newListener,
                R.id.onAttachStateChangeListener);
        if (oldListener != null) {
            view.removeOnAttachStateChangeListener(oldListener);
        }
        if (newListener != null) {
            view.addOnAttachStateChangeListener(newListener);
        }
    }
}

المثال أعلاه معقّد بعض الشيء لأنّ الفئة View تستخدم طريقتَي addOnAttachStateChangeListener() وremoveOnAttachStateChangeListener() بدلاً من طريقة setter لـ OnAttachStateChangeListener. تساعد فئة android.databinding.adapters.ListenerUtil في تتبُّع هؤلاء المستمعين بحيث يمكن إزالتها في محوِّل الربط.

إحالات ناجحة العناصر

التحويل التلقائي للعناصر

عند عرض Object من تعبير ربط، تختار المكتبة الطريقة المستخدَمة لضبط قيمة السمة. يتم تحويل Object إلى نوع مَعلمة بالطريقة التي اخترتها. هذا السلوك مناسب في التطبيقات التي تستخدم الفئة ObservableMap لتخزين البيانات، كما هو موضّح في المثال التالي:

<TextView
   android:text='@{userMap["lastName"]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content" />

يعرض الكائن userMap في التعبير قيمة يتم إرسالها تلقائيًا إلى نوع المعلَمة الموجود في طريقة setText(CharSequence) المستخدَمة لضبط قيمة السمة android:text. إذا كان نوع المعلمة غامضًا، فعليك تحويل نوع الإرجاع في التعبير.

الإحالات الناجحة المخصّصة

في بعض الحالات، يجب إجراء إحالة ناجحة مخصّصة بين أنواع محدّدة. على سبيل المثال، تتوقّع السمة android:background لإحدى المشاهدات Drawable، إلا أنّ قيمة color المحدّدة هي عدد صحيح. يعرض المثال التالي سمة تتوقّع ظهور Drawable، ولكن يتم توفير عدد صحيح بدلاً من ذلك:

<View
   android:background="@{isError ? @color/red : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

عندما يكون من المتوقّع توفّر Drawable ويتم عرض عدد صحيح، يمكنك تحويل int إلى ColorDrawable. لإجراء الإحالة الناجحة، استخدِم طريقة ثابتة مع التعليق التوضيحي BindingConversion على النحو التالي:

Kotlin

@BindingConversion
fun convertColorToDrawable(color: Int) = ColorDrawable(color)

Java

@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
    return new ColorDrawable(color);
}

ومع ذلك، يجب أن تكون أنواع القيم المقدَّمة في تعبير الربط متسقة. لا يمكنك استخدام أنواع مختلفة في نفس التعبير، كما هو موضح في المثال التالي:

// The @drawable and @color represent different value types in the same
// expression, which causes a build error.
<View
   android:background="@{isError ? @drawable/error : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

مراجع إضافية

لمعرفة المزيد من المعلومات عن ربط البيانات، اطّلِع على المراجع التالية.

عيّنات

الدروس التطبيقية حول الترميز

مشاركات المدونة