وقت اینه که دست های خودمون رو با Epoxy رنگی کنیم [سری آموزش پیاده سازی لیست های با ساختار پیچیده با استفاده از کتابخانه Epoxy در Android]


اگر با Epoxy آشنایی ندارید می تونید یه سری به صفحه گیت هاب این کتابخانه بزنید

https://github.com/airbnb/epoxy

یا همین جا بمونید و ادامه بدید !

توجه کنید که در این سری آموزش فرض شده است که با RecyclerView آشنا هستید ، در غیر این صورت می توانید از طریق آموزش های موجود در اینترنت نسبت به یادگیری این موارد اقدام کنید .

What is Epoxy ?

اپوکسی یک لایه Abstraction است که بر روی کتابخانه RecyclerView بنا شده است (اینی که گفتی یعنی چه ؟!!)

بزارید راحت باشیم

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

و برای اجرای این لیست عادی این مراحل رو طی می کنیم : (صرفا به عنوان مثال)

  • داخل لایه activity یک RecyclerView اضافه میکنیم
  • یک لایه برای آیتم های که قراره داخل لیست نشون داده بشن می سازیم
  • یک Adapter از نوع RecylerView.Adapter می سازیم
  • یک layoutManager می سازیم که بسته به نیاز می تونیم LinearyLayoutManager باشه یا GridLayoutManager یا ...
  • و تمام این موارد رو بهم متصل می کنیم تا لیست درست بشه
یک فرایند تکراری به شدت خسته کننده با کدهایی که هر بار مجبوری تکرار کنی (البته روش هایی برای کم کردن میزان این کد ها هست ولی به این سری آموزش ربطی نداره)

چیزی که شاید کمتر بهش فک کنیم اینه که اگر حتی 100,000،000 آیتم به Adapter اضافه کنیم باز هم اسکرول نرم و بدون لگی رو داریم و تعداد آیتم ها هیچ تاثیری نداره (این از برکات استفاده از RecyclerView ِ) .

حالا فرض کنید که ازتون بخوان یک لیست با دو نوع لایه متفاوت بسازید . خوب خیلی راحت می تونید لایه جدید رو داخل فولدر layout تعریف کنید و متد getItemViewType اداپتر رو override کنید و مشخص کنید که برای هر position کدام لایه استفاده بشه و داخل onCreateViewHolder لایه مورد نظر رو inflate کنید . به این صورت :

final int VIEW_TYPE_1 = 0;
final int VIEW_TYPE_2 = 1;

@Override
  public int getItemViewType(int position) {
    if (position % 2 = 0) {
                return 1;
        } 
        return 0;
  }
  
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int                      viewType) {

// لایه نوع یک 
View itemLayoutViewType1 = LayoutInflater.from(parent.getContext()).inflate(
        R.layout.item_type_1, parent, false);

// لایه نوع دو
View itemLayoutViewType2 = LayoutInflater.from(parent.getContext()).inflate(
        R.layout.item_type_2, parent, false);

    switch (viewType) {
      case VIEW_TYPE_1 :
        ViewType1Holder type1Holder = new ViewType1Holder        (itemLayoutViewType1);
        return type1Holder ;
      default :
        ViewType2Holder type2Holder = new ViewType2Holder        (itemLayoutViewType2);
        return type2Holder ;
    }
    
    
class ViewType1Holder extends RecyclerView.ViewHolder {
        ...
}
class ViewType2Holder extends RecyclerView.ViewHolder {
         ...
 }

کار به اینجا ختم نمیشه و باید داخل onBindViewHolder هم منطقی مناسب رو اضافه کنید :

@Override
  public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) {

    if (holder instanceof ViewType1Holder ) {
      ...
    }

     if (holder instanceof ViewType2Holder ) {
      ...
    }
  }

حالا اگر درخواست دیگری مبنی بر اضافه کردن نوع سومی از لایه بشه چی ؟ به زودی می بینید که با اضافه کردن viewType های بیشتر به لیست ، حجم Adapter زیاد میشه و خوانایی کدها و کمتر میشه و نتیجتا نگهداریش سخت میشه . دقت کنید که ممکنه لایه نوع n ام خودش یک لیست باشه (nested lists) و برای این مورد نیاز است کد و اقدامات بیشتری انجام داده بشه . استفاده از nested لیست ها در طراحی برنامه اندرویدی خیلی مرسوم هستند . مثلا اکثر برنامه فروشگاهی مانند google play و دیجی کالا یا موارد دیگر از این ساختار استفاده می کنند .

لیست های تو در تو (nested list) :

تصویر بالا رو ببینید ، لیست اصلی یک لیست عمودی است و آیتم های موجود در این لیست خودشان لیست های افقی هستند . درست کردن یک همچین لیستی کار تقریبا ساده ای است . کافیه دو Adapter جداگانه
تعریف کنیم که یکی از آنها لیست مادر را اداپت کند و اون یکی هم لیست ها فرزند رو . داخل onBindViewHolder لیست مادر باید یک همچین منطقی اضافه کنیم :

childRecyclerView.setHasFixedSize(true);       
childRecyclerView.setLayoutManager(new ChildLayoutManager(mContext, LinearLayoutManager.HORIZONTAL, false));        
childRecyclerView.setAdapter(adapter);        childRecyclerView.setRecycledViewPool(viewPool); 

تعیین کردیم که سایز تمام آیتم ها یکسانه (setHasFixedSize) . اینطوری لیست مادر دیگه لیست لازم نیست با اضافه کردن آیتم جدید به محاسبات بپردازه کافیه از اندازه های موجود استفاده کنه و تمام لیست های فرزند به یک viewPool یکسان متصل هستند اینطوری آیتم های موجود در داخل لیست های فرزند بین اونها به اشتراک گذاشته می شه (recycle میشه). در نگاه اول هیچ مشکلی خاصی با پیاده سازی لیست های تو در تو نداریم . ولی مشکلات به زودی نمایان می شوند .

گفتیم که viewPool آیتم های موجود در لیست های فرزند رو recycle میکنه و دوباره استفاده میکنه . یعنی ممکنه آیتمی که الان داخل لیست n ام داریم می بینیم قبلا داخل لیست m ام بوده باشه . توجه کنید که viewPool وقتی کاربرد داره که آیتم ها یکسان باشند درست مث google play که 90 درصد یا بیشتر آیتم های موجود در لیست های افقی یکسان هستند . وجود آیتم های یکسان باعث میشه که اسکرول نرم و بدون لگی داشته باشیم .

اما با در نظر گرفتن لایه های متفاوت برای هر کدام از لیست های افقی دیگه این اسکرول رو نرم رو نخواهیم داشت و با لگ های طولانی مواجه می شیم . علتش اینکه هیچ view ای بازیافت نمیشه و لیست ها افقی مجبورا هر بار آیتم هاشون رو inflate کنند که زمان بره . مشکل بعدی اینه که state لیست های افقی ذخیره نمیشه و scroll position خودشون رو زمانی که از لیست اصلی detach بشن از دست میدن .البته برای این مورد میتونیم منطقی بنویسیم که state ها رو ذخیره کنه و بعدا اینا رو برگردونیم .

لیست با چند view type
لیست با چند view type


خوب اینجاست که Epoxy خود نمایی میکنه

برای ساخت لیست هایی با آیتم های با ساختار پیچیده و همزمان تعداد viewType های متفاوت (در حد اینکه هیچ آیتم یکسانی نداشته باشیم !) از Epoxy استفاده می کنیم .


ادامه دارد ...