تحميل المزيد عند السحب للأسفل | Pagination In Firebase

سنقوم في هذا الدرس بشرح عملية Pagination في Firebase باستخدام مكتبة Infinite-Fire

ماهو Pagination؟

لنفترض أنه لديك في قاعدة البيانات على موقع ما مثلا 1000 عنصر وتريد عرضهم جميعهم في RecylcerView ,كم من الوقت ستستغرق عملية التحميل من الإنترنت وعرضها على الجهاز؟
بالتأكيد ستأخد بعض الوقت ولكن ماذا اذا كان عدد العناصر 5000 او اكثر!

هنا يأتي دور Pagination

عملية الPagination هي أن نقوم بتحميل 100 عنصر على سبيل المثال مبدأياً وعندما نعمل Scroll ونصل الى نهاية ال100 عنصر سنقوم بتحميل 100 أخرى من قاعدة البيانات وهكذا ..

لمزيد من المعلومات تابع هذا الشرح من مدونة هينديوير

الPagination غير مدعوم بشكل رسمي من Firebase ولكن سنستعمل مكتبة رائعة تمكننا من استعمال Pagination

تجهيز المشروع

كالعادة نقوم بإنشاء مشروع جديد على Firebase ونقوم بربطه بمشروع Android Studio ونقوم بإضافة مكتبة Infinite-Fire

في gradle

نستبدل هذه الأسطر في ملف build.gradle(project)

allprojects {
    repositories {
        jcenter()
        maven { url "https://jitpack.io"}

    }
}

ثم نضيف المكتبة في ملف build.gradle(app)

compile 'com.github.marcorei:Infinite-Fire:2.0.0'

ونقوم بعمل Sync ل Gradle

 

قمت بتجهيز بعض البيانات الوهمية (Dummy Data) لتجربة الموضوع ووضعتها في Firebase Database

قمت بإنشاء Model

 

وملف row.xml  الذي سيعطى لAdapter ويحتوي على 4 TextViews

إنشاء Adapter

عادةً عندما نقوم بإنشاء RecyclerView Adapter نجعله extends RecyclerView.Adapter

لكن الآن سنجعله يقوم بعمل extends من Adapter خاص بمكتبة Infinite-Fire

public class Adapter extends InfiniteFireRecyclerViewAdapter<Model>

نقوم بعمل Implements للميثود onCreateViewHolder و onBindViewHolder

بعد ذلك ننشئ الConstructor وبدلاً من أن يأخذ List كبارامتر سيأخذ InfiniteFireArray  وتسمى Snapshots  وهو كلاس خاص يحتوي على مصفوفة من Snapshots

 

  public Adapter(InfiniteFireArray snapshots, Context context) {
        super(snapshots, 0, 1);
        this.context = context;
    }

لاحظ أنه تم استدعاء الsuper,اما عن 0 فهو عدد headers و 1 هو عدد Footers

وقمت بإنشاء Context في حال أردت استعماله مستقبلاً

ثم ننشئ كلاس Holder الذي سيقوم بوضع البيانات ل TextViews

  class MyHolder extends RecyclerView.ViewHolder {
        TextView id, name, email, gender;

        public MyHolder(View itemView) {
            super(itemView);
            id = (TextView) itemView.findViewById(R.id.id);
            name = (TextView) itemView.findViewById(R.id.full_name_tv);
            email = (TextView) itemView.findViewById(R.id.email);
            gender = (TextView) itemView.findViewById(R.id.gender);
        }
    }

بعد ذلك ننشئ كلاس Holder آخر ولكن هذا الكلاس سيحتوي فقط على ProgressBar وسيتم عرضها عندما يتم تحميل البيانات

 public  class LoadingHolder extends RecyclerView.ViewHolder {
        public ProgressBar progressBar;

        public LoadingHolder(View view) {
            super(view);
            progressBar = (ProgressBar) view.findViewById(R.id.progress_bar);
        }
    }

ونقوم بإنشاء ملف xml خاص به

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_height="wrap_content"
    android:layout_width="match_parent"
    android:minHeight="1px" >
    <ProgressBar
        android:id="@+id/progress_bar"
        android:layout_height="wrap_content"
        android:layout_width="match_parent"
        android:layout_gravity="center_horizontal" />
</LinearLayout>

ثم نقوم بعمل Override لميثود getItemViewType

@Override
    public int getItemViewType(int position) {
        return super.getItemViewType(position);
    }

هذه الميثود ستكون مسؤولة عن نوع view الذي سيعطى,هل هو Loading أم أنه view البيانات

سنقوم بتعريف متغيرين من نوع int ونعطيهم أي قيمة

   public static final int VIEW_TYPE_CONTENT = 1;
    public static final int VIEW_TYPE_FOOTER = 2;

Content هو للمحتوى و Footer هو ل Loading

نعود الى getItemViewType ونقوم بالتحقق اذا كان position == عدد العناصر كلي -1 (أي انه وصل الى نهاية العناصر المحملة ) فستعود ب FOOTER أي أنه سيقوم بعرض عنصر التحميل

وإلا سيقوم بعرض عنصر Content

    @Override
    public int getItemViewType(int position) {
        if (position == getItemCount() - 1) {
            return VIEW_TYPE_FOOTER;
        }
        return VIEW_TYPE_CONTENT;
    }

الآن حان وقت إدخال بعض الأكواد الى onCreateViewHolder و onBindViewHolder

  @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        RecyclerView.ViewHolder viewHolder;
        View view;
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        switch (viewType) {
            case VIEW_TYPE_CONTENT:
                view = inflater.inflate(R.layout.row, parent, false);
                viewHolder = new MyHolder(view);
                break;
            case VIEW_TYPE_FOOTER:
                view = inflater.inflate(R.layout.list_item_loading, parent, false);
                viewHolder = new LoadingHolder(view);
                break;
            default:
                throw new IllegalArgumentException("Unknown type");
        }
        return viewHolder;
    }

لا شيئ جديد هنا عدا أنه قمنا بالتحقق اذا كان viewType يساوي VIEW_TYPE_CONTENT قم بعمل inflate لملف row.xml وقم بإرجاع ال MyHolder

واذا كان FOOTER قم بعمل inflate لملف list_item_loading وقم بإرجاع LoadingHolder

غير ذلك سنقوم بإظهار خطأ .

 

ونفس الكلام ينطبق على onBindViewHolder

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        int viewType = getItemViewType(position);
        switch (viewType) {
            case VIEW_TYPE_CONTENT:
                Model model = snapshots.getItem(position - indexOffset).getValue();

                MyHolder mHolder = (MyHolder) holder;
                mHolder.id.setText(model.getId().toString());
                mHolder.name.setText(model.getFirstName() + " " + model.getLastName());
                mHolder.email.setText(model.getEmail());
                mHolder.gender.setText(model.getGender());
                break;
            case VIEW_TYPE_FOOTER:
                LoadingHolder footerHolder = (LoadingHolder) holder;
                footerHolder.progressBar.setVisibility((isLoadingMore()) ? View.VISIBLE : View.GONE);
                break;
            default:
                throw new IllegalArgumentException("Unknown type");
        }
    }

في هذا السطر قمنا بأخذ قيمة model من snapshots array

                Model model = snapshots.getItem(position - indexOffset).getValue();

أخيراً نقوم بتعريف متغير boolean isLoadingMore لمعرفة هل هنالك عملية تحميل للداتا أم لا

private boolean loadingMore = false;

ثم ننشئ ميثود لتعيد قيمة هذا المتغير (لنستطيع الوصول اليها من MainActivity)

 public boolean isLoadingMore() {
        return loadingMore;
    }

وميثود أخرى لتغيير قيمة هذا المتغير وتقوم بتنبيه Adapter بأنه تم تغيير بيانات عنصر في المكان الفلاني

 public void setLoadingMore(boolean loadingMore) {
        if (loadingMore == this.isLoadingMore()) return;
        this.loadingMore = loadingMore;
        notifyItemChanged(getItemCount() - 1);
    }

انتهينا من صنع Adapter

أصبح الكود كاملاً كالتالي

public class Adapter extends InfiniteFireRecyclerViewAdapter<Model> {

    private Context context;
    public static final int VIEW_TYPE_CONTENT = 1;
    public static final int VIEW_TYPE_FOOTER = 2;


    /**
     * This is the view holder for the simple header and footer of this example.
     */
    public class LoadingHolder extends RecyclerView.ViewHolder {
        public ProgressBar progressBar;

        public LoadingHolder(View view) {
            super(view);
            progressBar = (ProgressBar) view.findViewById(R.id.progress_bar);
        }
    }

    private boolean loadingMore = false;

    /**
     * @param snapshots data source for this adapter.
     */
    public Adapter(InfiniteFireArray snapshots, Context context) {
        super(snapshots, 0, 1);
        this.context = context;
    }


    /**
     * @return status of load-more loading procedures
     */
    public boolean isLoadingMore() {
        return loadingMore;
    }


    public void setLoadingMore(boolean loadingMore) {
        if (loadingMore == this.isLoadingMore()) return;
        this.loadingMore = loadingMore;
        notifyItemChanged(getItemCount() - 1);
    }

    @Override
    public int getItemViewType(int position) {
        if (position == getItemCount() - 1) {
            return VIEW_TYPE_FOOTER;
        }
        return VIEW_TYPE_CONTENT;
    }


    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        RecyclerView.ViewHolder viewHolder;
        View view;
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        switch (viewType) {
            case VIEW_TYPE_CONTENT:
                view = inflater.inflate(R.layout.row, parent, false);
                viewHolder = new MyHolder(view);
                break;
            case VIEW_TYPE_FOOTER:
                view = inflater.inflate(R.layout.list_item_loading, parent, false);
                viewHolder = new LoadingHolder(view);
                break;
            default:
                throw new IllegalArgumentException("Unknown type");
        }
        return viewHolder;
    }

    class MyHolder extends RecyclerView.ViewHolder {
        TextView id, name, email, gender;

        public MyHolder(View itemView) {
            super(itemView);
            id = (TextView) itemView.findViewById(R.id.id);
            name = (TextView) itemView.findViewById(R.id.full_name_tv);
            email = (TextView) itemView.findViewById(R.id.email);
            gender = (TextView) itemView.findViewById(R.id.gender);
        }
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        int viewType = getItemViewType(position);
        switch (viewType) {
            case VIEW_TYPE_CONTENT:
                Model model = snapshots.getItem(position - indexOffset).getValue();

                MyHolder mHolder = (MyHolder) holder;
                mHolder.id.setText(model.getId().toString());
                mHolder.name.setText(model.getFirstName() + " " + model.getLastName());
                mHolder.email.setText(model.getEmail());
                mHolder.gender.setText(model.getGender());
                break;
            case VIEW_TYPE_FOOTER:
                LoadingHolder footerHolder = (LoadingHolder) holder;
                footerHolder.progressBar.setVisibility((isLoadingMore()) ? View.VISIBLE : View.GONE);
                break;
            default:
                throw new IllegalArgumentException("Unknown type");
        }
    }


}

الآن ننتقل الى MainActivity وننشأ RecyclerView و SwipeRefreshLayout في xml ونربطها عن طريق findViewById

نقوم بتعريف Instance من Firebase Database  داخل query ونقوم بالترتيب orderByKey

 Query query = FirebaseDatabase.getInstance().getReference().orderByKey();

بعد ذلك نعرف اوبجكت من InfiniteFireArray الذي سيحتوي على كافة Snapshots من Firebase

وهي تأخذ التالي:

  • كلاس Model
  • Database Reference او Query
  • Initial Size عدد العناصر عند أول تحميل
  • Page Size عدد العناصر الذي سيتم تحميلها كل مرة
  • limitToFirst اذا كانت true فسيعرض العناصر القديمة في الأعلى,اما اذا كانت False فسيعرف أحدث العناصر في الأعلى
 final InfiniteFireArray<Model> array = new InfiniteFireArray<>(
                Model.class,// Model Class
                query,//Database Ref
                20,// Initial Size
                20, //how many items to Load every time
                true,//limitToFirst //True means the old appears on top
                true
        );

ثم نقوم بتعريف Adapter ونعطيه array و context

        final Adapter adapter = new Adapter(array, this);

ثم نجعل SwipeRefreshLayout تقوم بتحميل البيانات

swipeRefreshLayout.post(new Runnable() {
            @Override
            public void run() {
                swipeRefreshLayout.setRefreshing(true);
            }
        });

ونضع listener لها ,أي أنه عندما يقوم المستخدم بعمل بالسحب من الأعلى للأسفل ستقوم بعمل reset لل array وإعادة تحميل البيانات الجديدة

    swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                array.reset();
            }
        });

ثم نقوم بعمل setAdapter و setLayoutManager

recyclerView.setLayoutManager(layoutManager);
        recyclerView.setAdapter(adapter);

ثم نضع listener لarray

      array.addOnLoadingStatusListener(new InfiniteFireArray.OnLoadingStatusListener() {
            @Override
            public void onChanged(EventType type) {
                switch (type) {
                    case LoadingContent:
                        adapter.setLoadingMore(true);
                        break;
                    case LoadingNoContent:
                        adapter.setLoadingMore(false);
                        break;
                    case Done:
                        swipeRefreshLayout.setRefreshing(false);
                        adapter.setLoadingMore(false);
                        break;
                }
            }
        });

ونقوم بالتحقق في حالة كان النوع هو Loading فسيقوم بتغيير المتغير في adapter isLoadingMore الى true

وفي حالة LoadingNoContent فسيغير نفس المتغير الى false

وفي حالة Done أي أنه تم التحميل عندها سنغير حالة التحميل swipeRefreshLayout الى false ونغير loadingMore الى false

 

أخيراً نقوم بإضافة Listener  لل recyclerView لنجعل array تقوم بتحميل المزيد من البيانات عند الوصول الى نهاية العناصر المحملة (عند الوصول الى آخر القائمة)

   recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                if (dy < 0) {
                    return;
                }
                if (layoutManager.findLastVisibleItemPosition() < array.getCount() - 20) {
                    return;
                }

                array.more();
            }
        });

ليصبح الكود كاملاً كالتالي

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);


        Query query = FirebaseDatabase.getInstance().getReference().orderByKey();

        final InfiniteFireArray<Model> array = new InfiniteFireArray<>(
                Model.class,// Model Class
                query,//Database Ref
                20,// Initial Size
                20, //how many items to Load every time
                true,//limitToFirst //True means the old appears on top
                true
        );

        final SwipeRefreshLayout swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipe_to_refresh);
        RecyclerView recyclerView = (RecyclerView) findViewById(rv);
        final LinearLayoutManager layoutManager = new LinearLayoutManager(this);

        final Adapter adapter = new Adapter(array, this);
        swipeRefreshLayout.post(new Runnable() {
            @Override
            public void run() {
                swipeRefreshLayout.setRefreshing(true);
            }
        });

        swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                array.reset();
            }
        });


        recyclerView.setLayoutManager(layoutManager);
        recyclerView.setAdapter(adapter);


        array.addOnLoadingStatusListener(new InfiniteFireArray.OnLoadingStatusListener() {
            @Override
            public void onChanged(EventType type) {
                switch (type) {
                    case LoadingContent:
                        adapter.setLoadingMore(true);
                        break;
                    case LoadingNoContent:
                        adapter.setLoadingMore(false);
                        break;
                    case Done:
                        swipeRefreshLayout.setRefreshing(false);
                        adapter.setLoadingMore(false);
                        break;
                }
            }
        });

        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                if (dy < 0) {
                    return;
                }
                if (layoutManager.findLastVisibleItemPosition() < array.getCount() - 20) {
                    return;
                }

                array.more();
            }
        });
    }
}

حان وقت التجربة 😀

 

رابط المكتبة على Github

 

رابط المشروع على Github

ملاحظة:المشروع على Github للمعاينة فقط ولايمكنك تجربته على Android Studio لعدم وجود google-services.json الخاص بك

عن 3llomi

Just A GEEK :)

شاهد أيضاً

إرسال الإشعارات عبر Cloud Functions وبدون سيرفر خارجي

في هذا الشرح سنقوم بطرح كيفية إرسال الإشعارات الى المستخدمين باستخدام Firebase Cloud Functions بدون …

تعليق واحد

  1. جزاك الله خيرا
    شرحك دائما متقدم وسهل
    نتمني ان تواصل

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *