چه تفاوتی بین Recyclerview و Listview وجود دارد؟

در این پست قصد دارم راجع به دو مورد از ابزارهای پرکاربرد اندروید به نام لیست ویو و ریسایکلرویو که برای نمایش لیست ها از آن‌ها استفاده میشود صحبت کنم و تجربه ای را که از کار کردن با این ابزار ها یدست آوردم هر چند هم که کم باشد در اختیارتان بگذارم.

فرض کنید میخواهیم یک دفترچه تلفنی ایجاد کنیم که لیستی از نام مخاطب های شما را نمایش میدهد که میتوانید این لیست رو اسکرول کنید، و با انتخاب هر یک از مخاطب ها جزئیات مربوط به آن را ببینید و... . برای پیاده سازی این دفترچه تلفن از ابزار لیست‌ویو میتوانیم استفاده کنیم، ابزار دیگری که بعد از اندروید ۵ معرفی شده است ریسایکلرویو هست که مشکلاتی که در لیست‌ویو وجود داشت را تا حدود زیادی حل کرده و یک نسخه پیشرفته‌تر و انعطاف ‌پذیرتر از لیست‌ویو هست. در ادامه سعی میکنم هریک از این ابزار ها را با مثالی معرفی کنم و در نهایت تفاوت‌هایشان را شرح دهم که چه فرقی میکند برای پیادده سازی یک دفترچه تلفن از لیست‌ویو اسنغاده کنیم یا از ریسایکلرویو استفاده کنیم؟

همانظور که عرض کردم لیست‌ویو مجموعه‌ای از اطلاعات است که بصورت عمودی روی هم قرار گرفته اند و قابل اسکرول به بالا و پایین هستند.

برای ایجاد دفترچه تلفن اول باید مجوعه داده‌هایی که میخواهیم نشان بدهیم را تعریف کنیم (مثلا لیست افراد و شماره تماس آن ها در یک آرایه رشته‌ای یا دیتابیس و...). سپس یک واسط یا آداپتری تعریف میکنیم که اطلاعات هر یک از آیتم های آرایه را برداشته و در آیتم متناظر در لیست ویو بارگذاری میکند(Bind میکند). مشکلی که در پیاده سازی لیست‌ویو پیش می‌آید این است که ممکن است فضای حافظه برای مجموعه داده هایی که در حافظه بارگذاری می شوند پاسخگو نباشد و با ارور هایی (به اصطلاح out of memory)مواجه شویم. مساله های دیگری هم در مورد لیست ویو وجود دارد که یا پیچیدگی زیادی دارند یا انعطاف لازم را ندارند در ادامه با مثال توضیح میدهم.

همانطور که در تصویر بالا می بینید ما ده تا مخاطب ایجاد کردیم که کد هایش به این ترتیب است:

در فایل xml:

<ListView
    android:id="@+id/list"
    android:layout_width="wrap_content"
    android:laynv h;out_height="wrap_content"/>

در اکتیویتی:

لیست را با آی دی مربوطه تعریف می کنیم:

ListView listView=(ListView) findViewById( R.id. list );

سپس آرایه ای رشته ای شامل نام مخاطب هایمان را تعریف می کنیم و آیتم هایش را اضافه می کنیم

List<String> items=new ArrayList<>();

items.add( "Contact 1" );
items.add( "Contact 2" );
items.add( "Contact 3" );
items.add( "Contact 4" );
items.add( "Contact 5" );
items.add( "Contact 6" );
items.add( "Contact 7" );
items.add( "Contact 8" );
items.add( "Contact 9" );
items.add( "Contact 10" );

حال واسط یا آداپتر را تعریف میکنیم که آرایه مان را به عنوان پارمتر میگیرد و محتوای آن را داخل لیست ویو نشان میدهد.

 ArrayAdapter<String> arrayAdapter=
 new ArrayAdapter<String>( this,R.layout.support_simple_spinner_dropdown_item,items);
listView.setAdapter( arrayAdapter );

پارامتر اول context مربوطه ، پارامتر دوم قالب هر سطر(آیتم)‌‌لیست است که بطور پیش فرض در اندروید تعریف شده و پارمتر سوم منبع داده هاست.

اگر دقت کنید اینجا از ArrayAdapter استفاده کردم چون منبع داده ما از نوع آرایه‌ی رشته ای هست باید از ArrayAdapter استفاده کنیم. نوع دیگه از آداپتر BaseAdapter هست که ArrayAdapter از آن ارث بری می کند استفاده خواهیم کرد و عمومی تر است. یعنی BaseAdapter یک کلاس انتزاعی هست که اینترفیس های Adapter و ListAdapter و spinnerAdapter را پیاده سازی می کند. در جاوا یک کلاس چندین اینترفیس را می تواند پیاده سازی کند ولی یک کلاس فقط میتواند از یک کلاس ارث بری کند. در اینجا ArrayAdapter به جای پیاده سازی اینترفیس های ذکر شده ،از کلاس baseadapter ارث بری می کند. بحث آداپتر ها کمی مفصلتر هست که انشالله در یک پست جداگانه در موردش صحبت می کنیم.

تا اینجا فقط نام مخاطب هایمان را نمایش میدهیم برای اینکه بقیه جزئیات آن ها را نشان دهیم کلاسی برای مخاطب هایمان تعریف می‌کنیم و متغیرها وویژگی های هر مخاطب را تعریف می‌‌ کنیم.در واقع مخاطب ما یک مدل هست(Model) و با ایجاد یک نمونه از این مدل میتوانیم مخاطب هایمان را ایجاد کنیم.

 public class Friend {
  String name;
  String phone;
  
  public Friend(String name,String phone){
  this.name=name;
  this.phone=phone;  }
  
  public String getname(){
  return name;
  }
  public String getPhone(){
    return phone;
}
public void setName(String name){
    this.name=name;}

public void setPhone(String phone){
    this.phone=phone;
}}

من اینجا یک مدل به نام friend برای مخاطبانم تعریف کردم که هر مخاطب دارای یک نام و شماره تماس هست. متدهایی هم برای تنظیم مقدار این متغیرها یا برگرداندن مقدار متغیرها تعریف کردم.مثل مثال قبلی مخاطب هایمان را ایجاد کرده و به لیستمان اضافه می‌کنیم و در نهایت آداپتر را به لیست‌ویو متصل میکنیم.

ListView friendsecoundlist=(ListView)findViewById( R.id.friendsecoundlist );
List<Friend> friendList=new ArrayList<>( );

Friend contact1=new Friend( "Contact 1","0933--------03" );
Friend contact2=new Friend( "Contact 2","0914--------64" );
Friend contact3=new Friend( "Contact 3","0914--------89" );
Friend contact4=new Friend( "Contact 4","0914--------33");
Friend contact5=new Friend( "Contact 5","0336--------05");

friendList.add( contact1);
friendList.add( contact2 );
friendList.add( contact3 );
friendList.add( contact4 );
friendList.add( contact5 );

FriendBaseAdapter  Friendadapter=new FriendBaseAdapter(this,friendList );
friendsecoundlist.setAdapter( Friendadapter );

کلاس آداپتر:

 public class FriendBaseAdapter extends BaseAdapter {

    public List<Friend> friendList;
    public Context context;
 //متد سازنده
public FriendBaseAdapter(Context mcontext,List<Friend> friendList ){
  this.friendList=friendList;
  this.context=mcontext;  }
 //مجوع آیتم های موجود در آداپتر را به شما نشان میدهد
@Override
  public int getCount() {
  return friendList.size();  }
//اطلاعات آیتم مربوطه که به عنوان پارمتر به متد داده میشود 
@Override
  public Object getItem(int position) {
  return friendList.get( position );  }
 //ترتیب نمایش آیتم ها را  نشان میدهد که ممکن است با همون ترتیبی باشد که در منبع داده مان هست
 @Override
  public long getItemId(int position) {
  return position;  }
 // ویوی هر آیتم را فراهم میکند ممکن  است این ویو یه تکست ویوی ساده باشد یا یک لیوت پیچیده باشه. پارامتر دوم  برای این هست که ویویی که ایجاد می کنیم بتوانیم  دوباره استفاده کنیم ولی در ابتدا
  null 
 . هست
@Override
  public View getView(int position,View convertView,ViewGroup parent) {
  Friend contact = (Friend) getItem(position);
  View v;
  if (convertView == null) {
  v = LayoutInflater.from(context).inflate(R.layout.contact_item_layout, null);}
  else {v = convertView;  }
  TextView tvName = (TextView) v.findViewById(R.id.name);
  TextView tvphone = (TextView) v.findViewById(R.id.phone);  tvName.setText(contact.getname());
        tvphone.setText(contact.getPhone());
        return v;
    }
}

نتیجه:

البته نوع دیگری از لیست ویو ووجود دارد به نام HorizontalListview که این امکان را به ما میدهد که لیست ویومان بصورت افقی باشد.ولی به دلیل محدودیت ها و پیاده سازی پیچیده امروزه به ندرت از آن استفاده می شود.

ریسایکلرویو

ریسایکلرویو یک نسخه پیشرفته‌تر و انعطاف‌پذیرتر از ListView هست در RecyclerView، شما می توانید با چند مولفه(view) مختلف و محدود داده‌های زیادی را به راحتی نمایش دهید. ریسایکلر ویو خودش را با ویو هایی که از یک layout managers که شما بهش دادید میگیرد و پر میکند. شما می توانید از یکی از layout managers های استاندارد مانند LinearLayoutManager یا GridLayoutManager و... استفاده کنید یا خودتان پیاده سازی کنید.

LinearLayoutManager mylayoutManager =
 new LinearLayoutmanager(this,LinearLayoutManager.vertical,false)

در کد بالا LiniearLayoutManager را تعریف می کنیم که پارمتر اول context مربوطه ، پارمتر دوم نحوه ی نمایس که در اینجا عمودی هست و مفدار دوم افقی هم می تواند باشد، پارمتر سوم ترتیب نمایش هست که اگر false یاشد به ترتیب نمایش خواهد داد.

ویو ها در لیست توسط اشیاء viewHolder نمایش داده میشوند این اشیاء نمونه هایی از کلاسی هستند که ازRecyclerView.ViewHolder ارث بری می کند.

هر viewholder مسئول نمایش یک آیتم واحد با یک ویو هست. مثلا در مثال خودمان که قبلا ذکر کردم هر ویوهلدر یک مخاطب را نمایش میدهد.ریسایکلرویو تنها به تعدادی ویو هلدر ایجاد می کند که برای نمایش در بخش صفحه اسکرین مورد نیاز است. یعنی تنها آیتم های در حال نمایش را در حافظه بارگذاری می کند. زمانی که کاربر لیست را اسکرول می کند ریسایکلرویو ویوهای خارج از صفحه را می گیرد و محتویات قدیمی آن‌ها را با محتویات جدید جایگزین میکند. با اینکه کار با ریسایکلرویو کمی سختتر و پیچیده تر است ولی کارایی و انعطافش از لیست ویو بیشتر ست. از جمله کارهای بهینه سازی که ریسایکلرویو خودش انجام میدهد:

  • ·زمانیکه لیست برای اولین بار پر می شود تعدادی ویوهلدر را در هر دو طرف لیست ایجاد و بایند میکنه.به عنوان مثال، اگر ویو، موقعیت۰ تا ۹ را نمایش بدهد ریسایکلرویو ویوهلدرهای ۰ تا ۹ رو ایجاد و بایند میکند و همچنین ممکن است viewHolder را برای موقعیت ۱۰ هم ایجاد و بایند کند به این ترتیب، اگر کاربر لیست را اسکرول کند، آیتم بعدی آماده‌ی نمایش است.
  • زمانیکه کاربر لیست را اسکرول میکند ریسایکلرویو ویوهلدر های جدید را در صورت لزوم ایجاد می کند و هم چنین ویوهلدرهایی که از صفحه نمایش خارج شده‌اند را ذخیره می کند بنابراین آنها میتوانند دوباره استفاده شوند. اگر کاربر لیست را اسکرول کند، ویوهلدرهایی که مدت طولانی از صفحه نمایش خارج شده اند با داده های جدید مجددا جایگزین می شوند و لازم نیست ویو هلدر از اول ایجاد شود و فقط محتوای ویو را به روز می‌کند تا مطابق با آیتم جدید نمایش داده شود.
  • هنگامی که در آیتم های نمایش داده شده تغییراتی اتفاق می افتد (مثلا اضافه کردن آیتم به لیست علاقمندی ها) شما میتوانید با استفاده از متد adapter.notifiydatasetchang به آداپتر اعلام کنید که آداپتر فقط آیتم های تغییر یافته را بایند کند و محتوایشان را به روز کند.

شکل پایین بطور کلی نحوه ی کار ریسایکلرویو را نمایش می دهد.

نحوه ی استفاده

اول از همه کتابخانه مورد نیاز را در فایل گریدل اضافه کنید سپس گزینه synk را بزنید تا فایل های مورد نیاز به پروژه تان اضافه شود.

dependencies {
  implementation 'com.android.support:recyclerview-v7:27.1.0'
}

سپس در فایل xml مورد نظر خود کدهای زیر را اضافه کنید.

<android.support.v7.widget.RecyclerView
    android:id="@+id/my_recycler_view"
    android:scrollbars="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

در ادامه در فایل جاوای خود ریسایکلر ویو رو با آی دی مورد نیاز یافته وآداپتر را به آن متصل میکنیم.

public class MyActivity extends Activity {

    private RecyclerView mRecyclerView;
    private RecyclerView.Adapter mAdapter;
    private RecyclerView.LayoutManager mLayoutManager;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.my_activity);
  mRecyclerView = (RecyclerView) findViewById(R.id.my_recycler_view);
  mLayoutManager = new LinearLayoutManager(this,LinearLayoutManager.Vertical,false);          
  mRecyclerView.setLayoutManager(mLayoutManager); 
//این تنظیم برای بهبود عملکرد ریسایکلرویو استفاده می شود تا زمانی که اندازه محتوای 
  ریسایکلرویو تغییر می‌کند اندازه لیوت مربوطه ثابت می ماند
  در غیر اینصورت هر بار که آیتمی به اداپتر اضافه یا حذف می شود ریسایکلرویو باید بررسی کند تا اندازه اش را تغییر دهد در صورنی که  مقدار 
  true‌
  را به این متد دهیم درواقع اندازه ریسایکلرویو به اندازه محتوای آداپتر وایسته نیست.
 mRecyclerView.setHasFixedSize(true);
List<Friend> friendList=new ArrayList<>( );

Friend contact1=new Friend( "Contact 1","0933----03" );
Friend contact2=new Friend( "Contact 2","0914----64" );
Friend contact3=new Friend( "Contact 3","0914----89" );
Friend contact4=new Friend( "Contact 4","0914----33");
Friend contact5=new Friend( "Contact 5","0336----05");

friendList.add( contact1);
friendList.add( contact2 );
friendList.add( contact3 );
friendList.add( contact4 );
friendList.add( contact5 );

تعریف و مشخص کردن آداپتر

  mAdapter = new MyAdapter (this,friendList);
  Friendadapter.setClickListener( new View.OnClickListener() {
  
    @Override
    public void onClick(View v) {
   Toast.makeText( FriendSecoundActivity.this,"Contact",Toast.LENGTH_SHORT ).show();
    }
} );
  mRecyclerView.setAdapter(mAdapter);      } 
          ... 

اضافه کردن آداپتر

برای انتقال تمام اطلاعات خود به لیست شما باید از کلاس Recyclerview.Adapter ارث بری کنید.این شی ویوی آیتم های لیست را درست میکند و با محتوای مربوط به هر آیتم پر می کند.

مثلا در کد زیر نمایش می دهد که چطور هر آیتم از مخاطب‌ها را در هر سطر ریسایکلرویو نمایش می‌دهد .هر آیتم ریسایکلرویو در این مثال شامل یک textview برای نام و یک textview برای تلقن هست که این ویوها برای هر آیتم در برنامه های مختلف می تواند متفاوت باشد.

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> { 
public   List<Friend> friendList;
private Context context;


    آماده سازی مرجع برای ویو هر آیتم//

 برای هر ایتم می باشد در آیتم های پیچیده نیازمند بیش از یک ویو  //
و شما در ویوهلدر دسترسی به همه ویوهای ایتم مورد نظر را فراهم میکنید.//

  public static class ViewHolder extends RecyclerView.ViewHolder {
 TextView tvName;
 TextView tvphone;
   public ViewHolder(TextView v) {
            super(v);
tvName = (TextView) itemView.findViewById(R.id.name);
tvphone = (TextView) itemView.findViewById(R.id.phone)}}

   آماده سازی سازنده مناسب(متناسب با هر نوع داده)//
    public MyAdapter(Context mcontext ,List< Friend > friendList) { 
      this. friendList = friendList;
       This.context= mcontext;    }
//توسط layout managerمورد استفاده قرار ی گیرد
    @Override
    public MyAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
     (ایجاد ویوی جدید)//
    TextView v = (TextView) LayoutInflater.from(parent.getContext())
    .inflate(R.layout.my_item_view, parent, false);
        ...
        ViewHolder vh = new ViewHolder(v);
        return vh;
    }
    // جایگزین کردن محتوای ویو ها ( استفاده می شودlayout managerتوسط )
    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
   آیتم  مربوط به این پوزیشن را  از منبع داده میگیرد//     
محتوای ویو را با این ایتم جایگزین میکند// 

final Friend contact =  friendList.get(position);
 holder.tvName.setText(contact.getname());
holder.tvphone.setText(contact.getPhone());
    }
   اندازه منبع داده را برمیگرداند//
    @Override
    public int getItemCount() {   return friendList.size();   }  }     

ابتدا LayoutManager متد onCreateViewHolder رو فراخوانی می کنه این متد برای ساختن RecyclerView.ViewHolder و تنظیم ویوهای مورد استفاده برای نمایش محتواها نیاز است.

سپس LayoutManager ویو را به داده هایش متصل می کند این کار را با فراخوانی متد onBindViewHolder انجام می دهد. این متد داده متناسب را آورده و در ویو موردنظر پر می کند. مثلا اگر ریسایکلر ویو لیستی از نام‌ها را نمایش می‌دهد این متد نام متناسب را در لیست پیدا می‌کند و آن را در Textviewی ViewHolder پر می کند.

نتیجه این مثال هم مثل مثال قبلی به این شکل خواهد بود:

اگر بخواهیم آیتم ها را بصورت افقی اسکرول کنیم در قسمت تعریف LayoutManager باید نحوه ی نمایش را horizontal انتخاب کنیم.به این شکل:

LinearLayoutManager mylayoutManager = new LinearLayoutManager( this,LinearLayoutManager.HORIZONTAL,false );

همونطور که می بینید ما یک لیست قابل پیمایش را از دو طریق می توانیم انجام دهیم.این بستگی دارد که واقعا چیکار میخواهیم بکنیم از یکی از ابزارهای ریسایکلر ویو یا بیست ویو استفاده می کنیم

و حالا سوالی که پیش می‌آید این است که تفاوت بین لیست‌ویو و ریسایکلر‌ویو در چیست؟

  • ریسایکلرویو از یک ViewHolder برای ذخیره سازی رفرنس ها به ویوهای مرتبط استفاده می کند. درنتیجه باعث میشه تا ما کمتر از متد findviewbyid استفاده کنیم.بنابراین ویوهلدر اجازه میدهد تا عملیات اسکرول لیستمان به آرامی اتفاق بیفتد و لیستمان هنگام اسکرول گیر نکند (lag نزند) آداپترِ recyclerview ما را مجبور میکند تا از الگوی viewholder استفاده کنیم پیداکردن و پرکردن ویوها و هم چنین به روزرسانی ویو‌ها از طریق متدهای onCreateViewHolder() , onBindViewHolder() انجام میگیرد. از طرف دیگر در لیست ویو بدون پیاده سازی Viewholder لیست رو نمایش میدهیم ولی اسکرول در لیست به دلیل بارگذاری تمام داده ها در حافظه ناکارآمدتر خواهد بود.
  • ریسایکلرویو از layout Manager برای تثبیت موقعیت آیتم ها (قرار دادن آیتم ها در مکان مناسب)استفاده میکند و همچنین این کلاس به ما امکانی برای انتخاب روشهایی میدهد که می خواهیم آیتم ها را نمایش دهیم. برای مثال اگر ما بخواهیم لیستمان را بصورت عمودی یا افقی اسکرول کنیم باید از LinearLayoutMsnager استفاده کنیم. , یا برای نمایش بصورت گرید از gridlayoutMaanger استفاده می کنیم.
  • چیدمان و آرایش آیتم ها(itemDecoration) مثلا در ریسایکلر ویو بطور پیش فرض بین سطرها بخش جداکننده وجود ندارد اگر بخواهیم به دلایلی یک بخش جداکننده اضافه کنیم، می توانیم از DividerItemDecoration استفاده کنیم وآن را به RecyclerView اضافه کنیم. در صورتی که از ListView استفاده کنیم، ما باید خودمون آرایش ردیف ها را شکل دهیم. برای این ابزار کلاس کمک کننده دیگری مثل itemDecoration وجود ندارد.
  • انیمیشن های پیش فرض(itemAnimator‌) برای ظاهر شدن یا ناپدید شدن لیست، اضافه کردن به recyclerview یا حذف کردن از آن. بطور پیش فرض انیمیشن لیست ریسایکلرویوها بسیار خوب و نرمه البته با اینکه خیلی آسان نیست ما میتوانیم به انیمیشن مورد نظر خودمان تغییر دهیم. برای انجام راحت این کار میتوانیم از کلاس simpleritemAnimator ارث‌بری کنیم و متد های مورد نیازمان را پیاده سازی کنیم. این کار در لیست ویوو یچیده‌تر و سختتر انجام میگیرد.
  • در آداپتر ریسایکلر ویو متد هایی برای اعلان وجود دارد و برای عناصر متدهای خاص نیز وجود دارد مانند ()notifyItemInserted(), notifyItemRemoved یا ()notifyItemChanged بسته به این که چه اتفاقی در آیتم ها افتاده میتوانیم از آن ها استفده کنیم در لیست ویو فقط می توانیم از notifyDataSetChanged استفاده کنیم و و مجبور هستیم خودمان آن را اداره کنیم.

خلاصه

لیست ویو برای مدت طولانی مورد استفاده قرار گرفته است و می توانستیم بیشتر موارد را با آن پوشش دهیم اما نیازهای کاربران در حال حاضر متفاوت هستند. طراحی لیست‌ها رفته‌رفته پیچیده‌تر می‌شود و لیست‌ویو برای این‌کار کمکی نمی کند. البته Material Design تغییرات خوب دیگری را ایجاد کرد اما پیچیده هست. خوشبختانه Recyclerview معرفی شد و مشکلات زیادی را حل کرده است. ریسایکلرویو به طور پیش‌فرض کارآمدتر هست انیمیشن های آن ساده تر است و طرح‌بندی آن آسان تر است بنابراین همیشه اگه در گیر هستید که از کدام یک از این ابزارها استفاده کنید حتما استفاده از recyclerview را در اولویت قرار دهید.