بسیاری از برنامه ها مجموعه بزرگی از داده ها را به کاربران نمایش می دهند . برای مثال برنامه آمازون را در نظر بگیرید این برنامه لیستی از محصولات را نشان می دهد. محصولات زیادی هم دارد اما تمام محصولات را یکباره بارگیری نمی کند ,برخی از محصولات را نشان می دهد و به محض رسیدن به آخرین مورد از لیست , محصولات بیشتری را بارگیری می کند و به ما نشان می دهد .اینکار صفحه بندی یا پیمایش (Paging) نامیده می شود. در این مقاله از آموزشگاه های برنامه نویسی در مشهد به معرفی این ویژگی می پردازیم .
چرا از Paging استفاده کنیم؟
- فرض کنید بیش از ۱۰۰۰ آیتم برای لیست تان دارید که از یک سرور پشتیبان ,می گیرید. کار اشتباهی است که همه آیتم ها را یکباره خوانده و نمایش دهیم .
معایب عدم استفاده از صفحه بندی (Paging) :
- کاربر همه آیتمها را همزمان نمی بیند اما شما همه آیتم ها را یکدفعه بارگذاری می کنید که در نتیجه پهنای باند بیشتری را بیهوده مصرف خواهید کرد.
- همچنین ایجاد یک لیست بزرگ از منابع سیستم بیشتری استفاده می کند که نتیجه آن یک برنامه کند خواهد بود.
مزایای استفاده از پیمایش:
- شما فقط یک تکه کوچک از مجموع داده های بزرگ را بارگذاری می کنیدکه از پهنای باند کمتری استفاده خواهد شد .
- این برنامه از منابع کمتری استفاده می کند که نتیجه آن یک برنامه سریع و مناسب میباشد.
کتابخانه paging
کتابخانه paging اندروید یک جز از کامپوننت jetpack اندروید هست .به یاد داشته باشید که بطور پیش فرض در دسترس نیست و باید این کتابخانه را به برنامه اضافه کنیم. اینکار به ما کمک می کند که داده هارا به تدریج و به زیبایی در RecyclerView بارگیری کنیم.
Android Jetpack مجموعه ای از کتابخانه ها، ابزار ها و راهنمایی های معماری است که به شما کمک می کند تا سریع و آسان به ساخت برنامه های کاربردی برای اندروید بپردازید. Android Jetpack زیر ساخت ها ی کد را فراهم می کند، بنابراین شما می توانید بر روی آنچه که در برنامه شما مورد نیاز است ، تمرکز کنید.
پیش نیازها
- استفاده از Retrofit در اندروید:از کتابخانه Retrofit برای آوردن داده ها از API backend استفاده خواهیم کرد.
- RecyclerView:بعد از آوردن آیتم ها از سرور آنها را در یک RecyclerView بارگذاری خواهیم کرد.
- Android ViewModel: یک جزء دیگر از Android Jetpack میباشد و به ما کمک می کند اطلاعات مربوط به رابط کاربر(UI) را به روش کارآمدتر نمایش دهیم .
Backend API
مهمترین قسمت Backend API هست . اگر چه شما میتوانید داده ها را از پایگاه داده SQLITE با استفاده از کتابخانه Paging بارگذاری کنید اما اغلب , برنامه نیاز به استخراج داده از Bakend API دارد . شما میتوانید با یادگیری یکسری آموزشها یک API را با استفاده از PHP و MYSQL بسازید .
در این آموزش API را خودمان نخواهیم ساخت و از یک API واقعی از Stackoverflow استفاده کنیم.در زیر لینک API هست :
https://api.stackexchange.com/2.2/answers?page=1&pagesize=50&site=stackoverflow
در نشانی اینترنتی API بالا پارامترهای زیر را داریم:
- Page: شماره صفحه ای که میخواهیم بیاریم.
- Pagesize:تعداد کل آیتم هایی که در صفحه میخواهیم.
- Site:سایتی که میخواهیم داده ها را از آن بیاوریم.
نشانی اینترنتی بالا پاسخ زیر را خواهد داد:
داده ها از Stackoverflow می آیند و این سایت یک مجموعه داده بسیار بزرگی دارد بنابراین ممکنه تعداد نامحدود صفحات داشته باشیم. حال وظیفه ما این است که صفحه ۱ را برداریم و به محض رسیدن کاربر به انتهای فهرست ,صفحه بعدی را بارگذاری کنیم و برای اینکار از کتابخانه paging اندروید استفاده می کنیم.
حالا بیایید وارد کد واقعی شویم.
ایجاد یک پروژه جدید:
- یک پروژه جدید با عنوان Paging Library Tutorial ایجاد میکنیم.
اضافه کردن وابستگی ها (Dependencies)
- به فایل build.gradle بروید و وابستگی های زیر را اضافه کنید:
dependencies { def paging_version = "1.0.0" def view_model_version = "1.1.0" def support_version = "27.1.0" def glide_version = "4.3.1" implementation fileTree(include: ['*.jar'], dir: 'libs') implementation "com.android.support:appcompat-v7:$support_version" implementation 'com.android.support.constraint:constraint-layout:1.1.2' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' //adding retrofit implementation 'com.squareup.retrofit2:retrofit:2.4.0' implementation 'com.squareup.retrofit2:converter-gson:2.4.0' //adding view model implementation "android.arch.lifecycle:extensions:$view_model_version" implementation "android.arch.lifecycle:viewmodel:$view_model_version" //adding paging implementation "android.arch.paging:runtime:$paging_version" //adding recyclerview and cardview implementation "com.android.support:cardview-v7:$support_version" implementation "com.android.support:recyclerview-v7:$support_version" //adding glide implementation "com.github.bumptech.glide:glide:$glide_version" annotationProcessor "com.github.bumptech.glide:compiler:$glide_version" }
- بعد از اضافه کردن وابستگی های مورد نیاز پروژه خود را sync (همگام) کنید.
ما وابستگیهای زیر را اضافه کردیم:
- Retrofit and Gson: برای تجزیه json از URL ( نشانی اینترنتی)
- ViewModel : کامپوننت اندروید برای ذخیره داده ها
- Paging:کتابخانه صفحه بندی (پیمایش)
- RecyclerView and CardView:برای ساختن فهرست
- Glide:برای بارگذاری تصویر از URL
ایجاد کلاس مدل
ما به این کلاس نیاز داریم تا پاسخ Json را بطور خودکار تجزیه کنیم. در کل به کلاسهای زیادی هم نیاز داریم تا پاسخ را بطور خودکار به کلاس جاوا مربوطه متصل کنیم
- یک فایل به نام StackApiResponse.java ایجاد کنید و کد زیر را بنویسید.
package net.simplifiedcoding.androidpagingexample; import java.util.List; class Owner { public int reputation; public long user_id; public String user_type; public String profile_image; public String display_name; public String link; } class Item { public Owner owner; public boolean is_accepted; public int score; public long last_activity_date; public long creation_date; public long answer_id; public long question_id; } public class StackApiResponse { public List<Item> items; public boolean has_more; public int quota_max; public int quota_remaining; }
- کد بالا بسیار ساده است . و در حد امکان کوتاه نوشته شده است . نام متغیرها با کلیدهای Json مطابقت دارد بنابراین Gson داده ها را بر اساس آن نمایش خواهد داد .
- فایل بالا فقط یک کلاس عمومی (public) دارد .(در واقع ما میتوانیم فقط یک کلاس عمومی در یک فایل داشته باشیم)
کلاس به نام StackApiResponse.java شامل Json زیر است:
- در کلاس فوق ما در حال تطبیق دادن داده های Json بالا هستیم به همین دلیل است که فقط ۴ ویژگی در StackApiResponse.java داریم.
- اولین مورد در داخل StackApiResponse.java آیتم (items) هست که شامل یک آرایه از آیتم ها می باشد.به همین دلیل است که یک < List<Item داریم.
سپس has_more از نوع boolean هست و quota_max و quota_remaining از نوع int هستند.
- در حال حاضر داخل Item , ما Json زیر را داریم:
- درون items ما یک شی ء دیگر به نام Owner داریم.به همین دلیل است که یک کلاس دیگر به نام owner ایجاد کرده ایم و یک شیء از نوع owner را داخل کلاس Item تعریف کرده ایم.امیدوارم که فهمیده باشید که چطور کلاس مدل را برای Json مشخص تعریف کنید.
ایجاد کلاس Retrofit
هر بار که میخواهیم داده ها را از یک صفحه جدید دریافت کنیم به شیء Retrofit نیاز داریم. پس ایجاد یک نمونه از Retrofit ایده خوبی هست .
یک کلاس جدید به نام RetrofitClient ایجاد می کنیم.
package net.simplifiedcoding.androidpagingexample; import okhttp3.OkHttpClient; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; public class RetrofitClient { private static final String BASE_URL = "https://api.stackexchange.com/2.2/"; private static RetrofitClient mInstance; private Retrofit retrofit; private RetrofitClient() { retrofit = new Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .build(); } public static synchronized RetrofitClient getInstance() { if (mInstance == null) { mInstance = new RetrofitClient(); } return mInstance; } public Api getApi() { return retrofit.create(Api.class); } }
کد بالا بسیار ساده است اما اگر در درکش مشکل دارید از ابتدا آموزش را دنبال کنید.
ایجاد API
- حالا ما رابط فراخوان API را ایجاد میکنیم.
- یک رابط به نام Api ایجاد کرده و کد زیر را می نویسیم.
package net.simplifiedcoding.androidpagingexample; import retrofit2.Call; import retrofit2.http.GET; import retrofit2.http.Query; public interface Api { @GET("answers") Call<StackApiResponse> getAnswers(@Query("page") int page, @Query("pagesize") int pagesize, @Query("site") String site); }
ایجاد RecyclerView
- همانطور که قبلا گفته بودیم این لیست را در RecyclerView نمایش خواهیم داد . به activity_main.xml بروید و RecyclerView را در اینجا تعریف کنید.
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <android.support.v7.widget.RecyclerView android:id="@+id/recyclerview" android:layout_width="match_parent" android:layout_height="match_parent" /> </android.support.constraint.ConstraintLayout>
- حالا برای RecyclerView یک فایل لایوت به نام recyclerview_users ایجاد کنید و کد xml زیر را بنویسید.
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content"> <ImageView android:id="@+id/imageView" android:layout_width="70dp" android:layout_height="70dp" /> <TextView android:id="@+id/textViewName" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_marginLeft="15dp" android:layout_toRightOf="@id/imageView" android:text="Belal Khan" android:textAppearance="@style/Base.TextAppearance.AppCompat.Large" android:textColor="@color/colorDefault" /> <TextView android:layout_width="match_parent" android:layout_height="1dp" android:layout_below="@id/imageView" android:background="@color/colorDefault" /> </RelativeLayout>
ایجاد PagedListAdapter
برای خواندن صفحه ی داده ها از RecyclerView.Adapter استفاده نخواهیم کرد و به جای آن از pageListAdapter استفاده خواهیم کرد. این کلاسی هست که کارهایی مثل شمارش آیتم , صفحه فراخوانی (callback) و غیره را انجام میدهد . و نکته این است که به عنوان رابط برای RecyclerView ما خواهد بود.
- یک کلاس به نام ItemAdapter ایجاد کنید و کد زیر را بنویسید.
package net.simplifiedcoding.androidpagingexample; import android.arch.paging.PagedListAdapter; import android.content.Context; import android.support.annotation.NonNull; import android.support.v7.util.DiffUtil; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import com.bumptech.glide.Glide; public class ItemAdapter extends PagedListAdapter<Item, ItemAdapter.ItemViewHolder> { private Context mCtx; ItemAdapter(Context mCtx) { super(DIFF_CALLBACK); this.mCtx = mCtx; } @NonNull @Override public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(mCtx).inflate(R.layout.recyclerview_users, parent, false); return new ItemViewHolder(view); } @Override public void onBindViewHolder(@NonNull ItemViewHolder holder, int position) { Item item = getItem(position); if (item != null) { holder.textView.setText(item.owner.display_name); Glide.with(mCtx) .load(item.owner.profile_image) .into(holder.imageView); }else{ Toast.makeText(mCtx, "Item is null", Toast.LENGTH_LONG).show(); } } private static DiffUtil.ItemCallback<Item> DIFF_CALLBACK = new DiffUtil.ItemCallback<Item>() { @Override public boolean areItemsTheSame(Item oldItem, Item newItem) { return oldItem.question_id == newItem.question_id; } @Override public boolean areContentsTheSame(Item oldItem, Item newItem) { return oldItem.equals(newItem); } }; class ItemViewHolder extends RecyclerView.ViewHolder { TextView textView; ImageView imageView; public ItemViewHolder(View itemView) { super(itemView); textView = itemView.findViewById(R.id.textViewName); imageView = itemView.findViewById(R.id.imageView); } } }
آداپتر تقریبا شبیه<> RecyclerView.Adapter است.تنها تغییر اینجا این است که ما پیاده سازی Diff_CallBack را داریم که برایsuper() استفاده میکنیم هستیم . این فراخوانی برای متمایز کردن دو مورد در یک لیست استفاده میشود.
- برای <> pagedListAdapter ما Item و viewholder را تعریف می کنیم . Item موردی هست که شما میخواهید نمایش داده شود و ما یک کلاس به این نام داریم که حاوی اطلاعاتی است که برای نمایش نیاز داریم.
ایجاد منبع داه Item
منبع داده item هایمان از جایی است که داده های واقعی را قرار دارد خوانده می شود و همانطور که میدانید که ما از StackOverFlow Api استفاده میکنیم.
برای ایجاد منبع داده گزینه های زیادی مانند :ItemKeyedDataSource, PageKeyedDataSource, PositionalDataSource داریم.
برای مثال ما قصد داریم ازPageKeyedDataSource استفاده کنیم . و در Api باید برای آوردن صفحه ای که میخواهیم شماره صفحه را بفرستیم . پس در اینجا شماره صفحه می شود کلید صفحه ی ما .
- یک کلاس با نام ItemDataSource ایجاد کنید و کد زیر را بنویسید.
package net.simplifiedcoding.androidpagingexample; import android.arch.paging.PageKeyedDataSource; import android.support.annotation.NonNull; import android.util.Log; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; public class ItemDataSource extends PageKeyedDataSource<Integer, Item> { //the size of a page that we want public static final int PAGE_SIZE = 50; //we will start from the first page which is 1 private static final int FIRST_PAGE = 1; //we need to fetch from stackoverflow private static final String SITE_NAME = "stackoverflow"; //this will be called once to load the initial data @Override public void loadInitial(@NonNull LoadInitialParams<Integer> params, @NonNull final LoadInitialCallback<Integer, Item> callback) { RetrofitClient.getInstance() .getApi().getAnswers(FIRST_PAGE, PAGE_SIZE, SITE_NAME) .enqueue(new Callback<StackApiResponse>() { @Override public void onResponse(Call<StackApiResponse> call, Response<StackApiResponse> response) { if (response.body() != null) { callback.onResult(response.body().items, null, FIRST_PAGE + 1); } } @Override public void onFailure(Call<StackApiResponse> call, Throwable t) { } }); } //this will load the previous page @Override public void loadBefore(@NonNull final LoadParams<Integer> params, @NonNull final LoadCallback<Integer, Item> callback) { RetrofitClient.getInstance() .getApi().getAnswers(params.key, PAGE_SIZE, SITE_NAME) .enqueue(new Callback<StackApiResponse>() { @Override public void onResponse(Call<StackApiResponse> call, Response<StackApiResponse> response) { //if the current page is greater than one //we are decrementing the page number //else there is no previous page Integer adjacentKey = (params.key > 1) ? params.key - 1 : null; if (response.body() != null) { //passing the loaded data //and the previous page key callback.onResult(response.body().items, adjacentKey); } } @Override public void onFailure(Call<StackApiResponse> call, Throwable t) { } }); } //this will load the next page @Override public void loadAfter(@NonNull final LoadParams<Integer> params, @NonNull final LoadCallback<Integer, Item> callback) { RetrofitClient.getInstance() .getApi() .getAnswers(params.key, PAGE_SIZE, SITE_NAME) .enqueue(new Callback<StackApiResponse>() { @Override public void onResponse(Call<StackApiResponse> call, Response<StackApiResponse> response) { if (response.body() != null) { //if the response has next page //incrementing the next page number Integer key = response.body().has_more ? params.key + 1 : null; //passing the loaded data and next page value callback.onResult(response.body().items, key); } } @Override public void onFailure(Call<StackApiResponse> call, Throwable t) { } }); } }
ممکن است کد بالا مشکل به نظر برسد اما مهمترین بخش پروژه ما هست . پس با هم مرور میکنیم.
- کلاس بالا از کلاس < PageKeyedDataSource<Integer, Item ارث بری دارد . Integer در اینجا کلید صفحه را تعریف می کند که در اینجا ما عددی صحیح استفاده می کنیم . هر بار که ما یک صفحه جدید از API میخواهیم باید شماره صفحه ای که میخواهیم که یک عدد صحیح است را بفرستیم . Item موردی است که ما از API بدست خواهیم آورد .از قبل یک کلاس به نام Item داریم.
- سپس اندازه یک صفحه را تعریف کردیم که ۵۰ هست , شماره صفحه اول که ۱ هست و sitename جایی هست که میخواهیم اطلاعات را بدست آوریم اگر میخواهید این مقادیر را تغییر دهید.
- ما سه متد را overridden کردیم :
() loadInitials: این متد داده های اولیه را بار خواهد کرد.
() loadBefore: این متد صفحه قبلی را بار خواهد کرد.
() loadAfter: این متد صفحه بعد را بار خواهد کرد.
ایجاد Item Data Source Factory
ما قصد داریم از <> MutableLiveData برای ذخیره PageKeyedDataSource مان استفاده کنیم و برای اینکار به DataSource.Factory نیاز داریم.
package net.simplifiedcoding.androidpagingexample; import android.arch.lifecycle.MutableLiveData; import android.arch.paging.DataSource; import android.arch.paging.PageKeyedDataSource; public class ItemDataSourceFactory extends DataSource.Factory { //creating the mutable live data private MutableLiveData<PageKeyedDataSource<Integer, Item>> itemLiveDataSource = new MutableLiveData<>(); @Override public DataSource<Integer, Item> create() { //getting our data source object ItemDataSource itemDataSource = new ItemDataSource(); //posting the datasource to get the values itemLiveDataSource.postValue(itemDataSource); //returning the datasource return itemDataSource; } //getter for itemlivedatasource public MutableLiveData<PageKeyedDataSource<Integer, Item>> getItemLiveDataSource() { return itemLiveDataSource; } }
ایجاد ViewModel
- یک کلاس با نام ItemViewModel ایجاد کنید و کد زیر را بنویسید:
package net.simplifiedcoding.androidpagingexample; import android.arch.lifecycle.LiveData; import android.arch.lifecycle.ViewModel; import android.arch.paging.LivePagedListBuilder; import android.arch.paging.PageKeyedDataSource; import android.arch.paging.PagedList; public class ItemViewModel extends ViewModel { //creating livedata for PagedList and PagedKeyedDataSource LiveData<PagedList<Item>> itemPagedList; LiveData<PageKeyedDataSource<Integer, Item>> liveDataSource; //constructor public ItemViewModel() { //getting our data source factory ItemDataSourceFactory itemDataSourceFactory = new ItemDataSourceFactory(); //getting the live data source from data source factory liveDataSource = itemDataSourceFactory.getItemLiveDataSource(); //Getting PagedList config PagedList.Config pagedListConfig = (new PagedList.Config.Builder()) .setEnablePlaceholders(false) .setPageSize(ItemDataSource.PAGE_SIZE).build(); //Building the paged list itemPagedList = (new LivePagedListBuilder(itemDataSourceFactory, pagedListConfig)) .build(); } }
حالا پروژه خود را بازسازی (rebuild) کنید
نمایش Paged List
- در نهایت به MainActivity.java بروید و کد زیر را بنویسید:
package net.simplifiedcoding.androidpagingexample; import android.arch.lifecycle.Observer; import android.arch.lifecycle.ViewModelProvider; import android.arch.lifecycle.ViewModelProviders; import android.arch.paging.PagedList; import android.support.annotation.Nullable; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.widget.Toast; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; public class MainActivity extends AppCompatActivity { //getting recyclerview private RecyclerView recyclerView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //setting up recyclerview recyclerView = findViewById(R.id.recyclerview); recyclerView.setLayoutManager(new LinearLayoutManager(this)); recyclerView.setHasFixedSize(true); //getting our ItemViewModel ItemViewModel itemViewModel = ViewModelProviders.of(this).get(ItemViewModel.class); //creating the Adapter final ItemAdapter adapter = new ItemAdapter(this); //observing the itemPagedList from view model itemViewModel.itemPagedList.observe(this, new Observer<PagedList<Item>>() { @Override public void onChanged(@Nullable PagedList<Item> items) { //in case of any changes //submitting the items to adapter adapter.submitList(items); } }); //setting the adapter recyclerView.setAdapter(adapter); } }
حالا پروژه خود را اجرا کنید.
توجه: اگر هیچ آیتمی را نمی بینید مطمین شوید که نشانی اینترنتی API به درستی کار می کند( با باز کردن نشانی اینترنتی در مرورگرتان ).گاهی اوقات stackoverflow در خواستهای پیوسته به URLs را مسدود می کند.
دانلود پروژه کتابخانه Paging در اندروید
جهت شرکت در دوره های آموزش اندروید مشهد و آموزش طراحی سایت مشهد می توانید از طریق شماره تماس های پایین ما در ارتباط باشید .