کلاس های تولید شده توسط دیتا بایندینگ
کلاس های تولید شده توسط دیتا بایندینگ
2020-05-02
آموزش بایند کردن view های لایه به مولفه های معماری اندروید
آموزش بایند کردن view های لایه به مولفه های معماری اندروید
2020-05-04
آموزش آداپتور های بایندینگ (Binding adapters)

آموزش آداپتور های بایندینگ (Binding adapters)

آداپتور های بایندینگ (Binding adapters) ، آداپتور های بایندینگ برای فراخوانی یک سری عملیات مناسب جهت تنظیم کردن مقدار استفاده می شود.برای مثال تنظیم کردن مقدار می توان به فراخوانی متد setText اشاره کرد و یا حتی برای تنظیم کردن یک شنونده رخداد می توان به متد setOnClickListener اشاره کرد.کتابخانه دیتا بایندینگ به شما این امکان را می دهد تا یک متد مشخص با نام دلخواه پیاده کنید و از آن برای تنظیم کردن مقدار (در زمان فراخوانی آن) استفاده کنید. این متد شما که درواقع به عنوان همان آداپتور های بایندینگ یا Binding adapter شناخته می شود می تواند شامل دستورات دلخواه شما و حتی نوع برگشتی دلخواه شما باشد.

آداپتور های بایندینگ (Binding adapters)

تنظیم مقادیر ویژگی

زمانی که یک مقدار تغییر می کند ،کلاس تولید شده توسط کتابخانه دیتا بایندینگ باید متد setter آن مقدار را جهت اعمال مقدار جدید فراخوانی کند.اما شما می توانید به کتابخانه دیتا بایندینگ این اجازه را بدهید که مستقیما به سمت متد دلخواه شما هدایت شود یا حتی براساس یک منطق خاص ،یک متد انتخاب شده و به سمت آن هدایت شود.

انتخاب متد به صورت اتوماتیک

برای یک attribute با نام example ،کتابخانه دیتا بایندینگ به دنبال متدی مانند setExample(arg) می گردد نوع پارامتر arg آن با نوع مقداری که می خواهد به آن پاس دهد یکی باشد.فضای نام یا namespace آن مهم نیست و فقط نام آن و همچنین type متد مورد بررسی دیتا بایندینگ خواهد بود.

برای مثال اگر درلایه خود عبارت android:text=”@{user.name}” را داشته باشید کتابخانه دیتا بایندینگ به دنبال متد setText(arg) در پروژه می گردد که نوع پارامتری که دریافت می کند با نوع خروجی متد user.getName() که می خواهد به آن (متد setText ) مقدار پاس دهد برابر و یکی باشد .بدین معنا که اگر نوع خروجی متد user.getName() به صورت String باشد ،پس کتابخانه دیتا بایندینگ هم به دنبال متدی setText() ی می گردد که پارامتر String دریافت کند !این در حالی است که اگر نوع بازگشتی متد user.getName() برابر int بود ،پس کتابخانه دیتا بایندینگ به دنبال متد setText() ی می گشت که نوع پارامتر دریافتی آن برابر با int باشد.

پس مشخص شد که متد setter هر attribute باید دقیقا مانند نوع خروجی متد getter ی باشد که می خواهد به متد setter مقدار پاس دهد.

کتابخانه دیتا بایندینگ حتی با attribute هایی که وجود ندارند هم کار می کنند ،این جمله به چه معناست؟

بدین معناست که شما می توانید با استفاده از دیتا بایندینگ attribute بسازید !و دستورات خود را با استفاده از attribute پیاده و اعمال کنید.برای مثال کلاس DrawerLayout هیچ attribute خاصی ندارد اما setter های فراوانی دارد !مثلا در نمونه زیر ما متد setScrimColor(int) و متد setDrawerListener(DrawerLinster) به عنوان setter صفت های app:scripColor و app:drawerListener استفاده کردیم.

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

نکته :
همانطور که متوجه شدید ما از دو پیشوند android: و app: برای فراخوانی attribute ها استفاده می کنیم.
اما تفاوت این دو درچیست؟ android برای صفت ها یا attribute هایی استفاده می شود که مربوط به SDK اندروید هستند ،app برای صفت ها یا attribute هایی استفاده می شود که مربوط به کتابخانه support و یا غیر Android SDK هستند.

مشخص کردن یک متد با نام سفارشی سازی شده

برخی attribute ها هستند که متد setter آنها با نام خود attribute همخوانی ندارند.در این مواقع می توان attribute را به متد setter دیگری با استفاده از annotation یا حاشیه نوشت BindingMethods متصل کرد.درواقع annotation یا حاشیه نوشت BindingMethods می تواند شامل چندین حاشیه نوشت BindingMethod باشد که ( هرکدام برای تغییر نام یک متد استفاده شده ) و با یک کلاس استفاده می شود.توجه داشته باشید که annotation یا حاشیه نوشت BindingMethods می تواند در هر کلاسی از پروژه شما پیاده شود.در مثال زیر attribute یا صفت android:tint به متد setter با نام setImageTintList(ColorStateList) متصل شده و نه به متد setTint() !

جاوا

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

کاتلین

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

اما معمولا نیاز به استفاده از حاشیه نوشت های BindingMethods یا BindingMethod برای تغییر نام متد های setter در فریم ورک اندروید نخواهید داشت.این attribute ها قبلا با استفاده از نام های قراردادی پیاده شدند تا بطور اتوماتیک متدهای مربوط به آنها پیدا و فراخوانی شوند.

فراهم کردن دستورات سفارشی شده توسط برنامه نویس

برخی attribute ها به کدنویسی های سفارشی شده جهت بایند شدن نیاز دارند. برای مثال ما setter خاصی برای android:paddingLeft نداریم و به جای آن متدی بصورت setPadding(left, top, right, bottom) داریم که padding های چپ ،بالا ،راست و پایین را برای view مشخص میکند.

یک متد آداپتور بایندینگ binding adapter به همراه حاشیه نوشت @BindingAdapter این امکان را به ما می دهد که setter را برای هر attribute پیاده کنیم.توجه داشته باشید که attribute های خود کلاس های فریم ورک اندروید annotation های BindingAdapter از پیش ساخته شده دارند.برای مثال نمونه زیر BindingAdapter مربوط به paddingLeft را نمایش می دهد:

جاوا

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

کاتلین

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

این دستور مربوط به کلاس های فریم ورک اندروید است و همانطور که گفته شد این دستورات از پیش نوشته و مشخص شده با annotation یا حاشیه نوشت BindingAdapter است.برای اینکه این دستورات را در پروژه خود ببینید کلید میانبر Ctrl + shift + f را بزنید تا پنجره جستجو در کل پروژه نمایش داده شود و سپس عبارت setPaddingLeft را در حالت scope و AllPlace سرچ کنید. خواهید دید در فایل ViewBindingAdapter این متد و متدهای دیگر attribute ها وجود دارد.

آموزش آداپتور های بایندینگ (Binding adapters)

نوع پارامتر های متد binding adapter بسیار مهم است

پارامتر اول همیشه مشخص کننده نوع view است که می خواهیم attribute مربوط به آ نرا مقداردهی کنیم.نوع پارامتر دوم هم درواقع باید نوعی باشد که توسط پراپرتی های attribute قابل پذیرش باشند.برای مثال در نمونه padding بالا که توضیح دادیم پارامتر دوم از نوع int بود چرا که نوع ورودی attribute اصلی ما یعنی setPadding از نوع int است و ما می خواهیم این مقدار را به آن پاس دهیم پس باید نوع ها یکی باشند.همچنین Binding adapter را می توان برای حالت های شخصی سازی دیگر هم به کار برد.برای مثال می توان یک loader یا لود کننده سفارشی شده برای لود کردن عکس پیاده کنیم.به مثال زیر دقت کنید:

جاوا

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

کاتلین

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

شما می توانید از adapter هایی که پیاده کردید در layout به صورت زیر استفاده کنید:

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

توجه کنید که @drawable/venueError به یک فایل درون پروژه شما اشاره میکند.همچنین توجه کنید که adapter تعریف شده درصورتی فراخوانی می شود که هردو پارامتر imageUrl و error مقدار دهی شده باشند.اما اگر بخواهید با تنظیم کردن هرکدام از آنها adapter فراخوانی شود میتوانید از دستور requireAll = false استفاده کنید تا با تنظیم کردن مقدار هرکدام از پارامتر ها adapter فراخوانی شود.

جاوا

@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);
  }
}

کاتین

@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);
    }
}

زمان هایی که شنونده یا listener شما چندین متد دارد ،متدها باید به چندین شنونده شکسته شوند.برای مثال View.OnAttachStateChangeListener دو متد onViewAttachWindow(View) و onViewDetechedFromWindow(View) دار. کتابخانه دو interface برای تفکیک ویژگی ها و کنترل کننده ها برای آنها فراهم کرده.

جاوا

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

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

کاتلین

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

زیرا تغییر وضعیت هر interface می تواند برروی دیگری تاثیر داشته باشد ،بنابراین شما نیاز به دو آداپتور برای هرکدام به صورت جداگانه و یا یک آداپتور که هردو را مدیریت کند ،دارید.شما می توانید از requireAll = false استفاده کنید تا با تنظیم هرکدام binding adapter فراخوانی شود ( الزاما نیاز به تنظیم کردن هردو پارامتر نباشد و تنها ارسال یک پارامتر هم آداپتور را فراخوانی کند ).

جاوا

@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 &amp;&amp; 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);
        }
    }
}

کاتلین

@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 &amp;&amp; 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)
        }
    }
}

تبدیل شی

تبدیل خودکار شی

زمانی که یک شی از عبارات binding برمی گردد. کتابخانه یک متد را برای دریافت این شی درقالب پارامتر انتخاب می کند و به عبارتی شی به نوع پارامتر متد انتخابی cast یا تبدیل میشود.این عملکرد در برنامه هایی که از ObservableMap استفاده می کنند بسیار مناسب است.

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

نکته:
‌شما همچنین می توانید مستقیم به مقدار (value) درون map به صورت object.key هم دسترسی داشته باشید.برای مثال ما در نمونه فوق بدین صورت یک فیلد map را فراخوانی کردیم @{userMap[“lastName”]} درحالی که می توان بدین صورت هم فیلد‌ lastName را فراخوانی کرد @{userMap.lastName}

همانطور که گفته شد شی userMap مقداری را برمی گرداند که به صورت اتوماتیک به نوع پارامتر متد setText(CharSequence) که وظیفه متصل کردن تکست به ویژگی android:text را دارد ،تبدیل می شود.

تبدیل سفارشی شده

در بعضی مواقع یک تبدیل سفارشی به انواع خاص نیازمند است.برای مثال ویژگی android:background توقع پارامتر از نوع Drawable را دارد اما مقدار color از نوع integer است !در مثال زیر می بینید که چگونه attribute توقع مقدار از نوع Drawable را دارد اما مقدار از نوع integer هم مقدار این attribute را فراهم می کند:

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

هر زمان که نیاز بود پارامتر از نوع Drawable باشد ولی از نوع integer دریافت کرد ،مقدار int باید تبدیل به ColorDrawable شود.این تبدیل با متد static به همراه annotation یا حاشیه نوشت BindingConversion انجام می شود.

جاوا

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

کاتلین

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

یک نکته مهم را فراموش نکنید که شما نمی توانید از نوع های مختلف پارامتر استفاده کنید مانند مثال زیر:

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

متدهای binding adapter می توانند مقدار قدیمی راهم به صورت اختیاری دریافت کنند.توجه کنید که متدی که هم مقادیر old و هم مقادیر new (منظور مقادیر قدیم و جدید است) را می خواهد دریافت کند باید تمامی مقادیر قدیمی را جز پارامتر های اول و سپس مقادیر جدید را مشخص کند.

جاوا

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

کاتلین

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

حتی handler ها می توانند تنها با interface ها یا کلاس های abstract استفاده شوند مانند نمونه زیر:

جاوا

@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);
    }
  }
}

کاتلین

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

و از این handler می توانید به صورت زیر در layout خود استفاده کنید

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

امیدوارم این آموزش برای شما مفید بوده باشد…

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *