نظرة عامة على مكتبة Paging 2   جزء من Android Jetpack.

تساعدك مكتبة Paging في تحميل أجزاء صغيرة من البيانات وعرضها في كل مرة. يؤدي تحميل البيانات الجزئية عند الطلب إلى تقليل استخدام معدل نقل البيانات في الشبكة وموارد النظام.

يقدّم هذا الدليل عدة أمثلة مفاهيمية للمكتبة، بالإضافة إلى نظرة عامة على آلية عملها. للاطّلاع على أمثلة كاملة حول طريقة عمل هذه المكتبة، يمكنك تجربة الدرس التطبيقي حول الترميز والنماذج من قسم المراجع الإضافية.

الإعداد

لاستيراد مكوّنات Paging إلى تطبيق Android، أضِف التبعيات التالية إلى ملف build.gradle في تطبيقك:

Groovy

dependencies {
  def paging_version = "2.1.2"

  implementation "androidx.paging:paging-runtime:$paging_version" // For Kotlin use paging-runtime-ktx

  // alternatively - without Android dependencies for testing
  testImplementation "androidx.paging:paging-common:$paging_version" // For Kotlin use paging-common-ktx

  // optional - RxJava support
  implementation "androidx.paging:paging-rxjava2:$paging_version" // For Kotlin use paging-rxjava2-ktx
}

Kotlin

dependencies {
  val paging_version = "2.1.2"

  implementation("androidx.paging:paging-runtime:$paging_version") // For Kotlin use paging-runtime-ktx

  // alternatively - without Android dependencies for testing
  testImplementation("androidx.paging:paging-common:$paging_version") // For Kotlin use paging-common-ktx

  // optional - RxJava support
  implementation("androidx.paging:paging-rxjava2:$paging_version") // For Kotlin use paging-rxjava2-ktx
}

بنية المكتبة

يصف هذا القسم المكوّنات الرئيسية لمكتبة Paging ويعرضها.

PagedList

المكوّن الرئيسي لمكتبة Paging هو فئة PagedList التي تحمّل أجزاء من بيانات تطبيقك أو صفحاته. وعند الحاجة إلى المزيد من البيانات، يتم نقلها إلى كائن PagedList الحالي. في حال تغيّر أي بيانات تم تحميلها، يتم إرسال مثيل جديد من PagedList إلى حاوية البيانات القابلة للتتبّع من كائن LiveData أو كائن مستند إلى RxJava2. عند إنشاء عناصر PagedList، تعرض واجهة مستخدم تطبيقك محتواها، مع مراعاة دورات حياة وحدات التحكّم في واجهة المستخدم.

يوضّح مقتطف الرمز التالي كيفية ضبط نموذج العرض في تطبيقك لتحميل البيانات وعرضها باستخدام حاوية LiveData لعناصر PagedList:

Kotlin

class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
    val concertList: LiveData<PagedList<Concert>> =
            concertDao.concertsByDate().toLiveData(pageSize = 50)
}

Java

public class ConcertViewModel extends ViewModel {
    private ConcertDao concertDao;
    public final LiveData<PagedList<Concert>> concertList;

    // Creates a PagedList object with 50 items per page.
    public ConcertViewModel(ConcertDao concertDao) {
        this.concertDao = concertDao;
        concertList = new LivePagedListBuilder<>(
                concertDao.concertsByDate(), 50).build();
    }
}

البيانات

يحمّل كل مثيل من PagedList لقطة محدّثة لبيانات تطبيقك من كائن DataSource المقابل. تتدفق البيانات من الواجهة الخلفية أو قاعدة البيانات الخاصة بتطبيقك إلى عنصر PagedList.

يستخدم المثال التالي مكتبة Room لاستدامة البيانات لتنظيم بيانات تطبيقك، ولكن إذا أردت تخزين بياناتك باستخدام وسيلة أخرى، يمكنك أيضًا توفير مصنع مصدر البيانات الخاص بك.

Kotlin

@Dao
interface ConcertDao {
    // The Int type parameter tells Room to use a PositionalDataSource object.
    @Query("SELECT * FROM concerts ORDER BY date DESC")
    fun concertsByDate(): DataSource.Factory<Int, Concert>
}

Java

@Dao
public interface ConcertDao {
    // The Integer type parameter tells Room to use a
    // PositionalDataSource object.
    @Query("SELECT * FROM concerts ORDER BY date DESC")
    DataSource.Factory<Integer, Concert> concertsByDate();
}

لمزيد من المعلومات حول كيفية تحميل البيانات إلى كائنات PagedList، يُرجى الاطّلاع على الدليل حول كيفية تحميل البيانات المقسّمة على صفحات.

واجهة المستخدم

يعمل الصنف PagedList مع PagedListAdapter لتحميل العناصر إلى RecyclerView. تعمل هذه الفئات معًا لجلب المحتوى وعرضه أثناء تحميله، وجلب المحتوى غير المرئي مسبقًا، وتحريك تغييرات المحتوى.

لمزيد من المعلومات، يُرجى الاطّلاع على الدليل حول كيفية عرض القوائم المقسّمة إلى صفحات.

إتاحة بنيات بيانات مختلفة

تتوافق مكتبة Paging مع بُنى البيانات التالية:

  • يتم عرضها من خادم خلفية فقط.
  • يتم تخزينها في قاعدة بيانات على الجهاز فقط.
  • مزيج من المصادر الأخرى، باستخدام قاعدة البيانات على الجهاز فقط كذاكرة تخزين مؤقت.

يوضّح الشكل 1 كيفية تدفق البيانات في كل سيناريو من سيناريوهات البنية هذه. في حالة الحلول التي تعتمد على الشبكة فقط أو قاعدة البيانات فقط، تتدفق البيانات مباشرةً إلى نموذج واجهة المستخدم في تطبيقك. إذا كنت تستخدم نهجًا مدمجًا، تتدفق البيانات من خادمك الخلفي إلى قاعدة بيانات على الجهاز فقط، ثم إلى نموذج واجهة المستخدم في تطبيقك. بين الحين والآخر، تنفد البيانات من نقطة نهاية كل تدفق بيانات، وعند هذه النقطة تطلب المزيد من البيانات من المكون الذي قدم البيانات. على سبيل المثال، عندما تنفد البيانات من قاعدة بيانات على الجهاز فقط، فإنها تطلب المزيد من البيانات من الخادم.

تدفُّق البيانات في بنى مكتبة Paging المتوافقة
الشكل 1: كيفية تدفق البيانات من خلال كل من البُنى التي تتيحها مكتبة Paging

يقدّم الجزء المتبقي من هذا القسم اقتراحات لضبط كل حالة استخدام لتدفق البيانات.

الشبكة فقط

لعرض البيانات من خادم خلفي، استخدِم الإصدار المتزامن من واجهة برمجة التطبيقات Retrofit لتحميل المعلومات إلى كائن DataSource المخصّص.

قاعدة البيانات فقط

يجب إعداد RecyclerView لمراقبة مساحة التخزين المحلية، ويُفضَّل استخدام مكتبة Room لاستدامة البيانات. بهذه الطريقة، كلما تم إدراج بيانات أو تعديلها في قاعدة بيانات تطبيقك، ستظهر هذه التغييرات تلقائيًا في RecyclerView الذي يعرض هذه البيانات.

الشبكة وقاعدة البيانات

بعد بدء مراقبة قاعدة البيانات، يمكنك الاستماع إلى وقت نفاد البيانات من قاعدة البيانات باستخدام PagedList.BoundaryCallback. يمكنك بعد ذلك جلب المزيد من العناصر من شبكتك وإدراجها في قاعدة البيانات. إذا كانت واجهة المستخدم تراقب قاعدة البيانات، فهذا كل ما عليك فعله.

التعامل مع أخطاء الشبكة

عند استخدام شبكة لجلب البيانات التي تعرضها أو تقسيمها إلى صفحات باستخدام "مكتبة تقسيم الصفحات"، من المهم عدم التعامل مع الشبكة على أنّها "متاحة" أو "غير متاحة" طوال الوقت، لأنّ العديد من الاتصالات تكون متقطّعة أو غير مستقرة:

  • قد يتعذّر على خادم معيّن الاستجابة لطلب شبكة.
  • قد يكون الجهاز متصلاً بشبكة بطيئة أو ضعيفة.

بدلاً من ذلك، يجب أن يتحقّق تطبيقك من كل طلب بحثًا عن حالات الفشل والاسترداد بأكبر قدر ممكن من السلاسة في الحالات التي لا تتوفّر فيها الشبكة. على سبيل المثال، يمكنك توفير زر "إعادة المحاولة" للمستخدمين لتحديده إذا لم تنجح خطوة تحديث البيانات. في حال حدوث خطأ أثناء خطوة تقسيم البيانات إلى صفحات، من الأفضل إعادة محاولة طلبات التقسيم إلى صفحات تلقائيًا.

تحديث تطبيقك الحالي

إذا كان تطبيقك يستهلك بيانات من قاعدة بيانات أو مصدر خلفي، يمكنك ترقيته مباشرةً إلى الوظائف التي توفّرها مكتبة Paging. يوضّح هذا القسم كيفية ترقية تطبيق يتضمّن تصميمًا شائعًا حاليًا.

حلول مخصّصة للتنقّل بين الصفحات

إذا كنت تستخدم وظائف مخصّصة لتحميل مجموعات فرعية صغيرة من البيانات من مصدر بيانات تطبيقك، يمكنك استبدال هذا المنطق بمنطق من فئة PagedList. توفّر مثيلات PagedList اتصالات مدمجة بمصادر البيانات الشائعة. وتوفّر هذه النُسخ أيضًا محوّلات لعناصر RecyclerView التي قد تدرجها في واجهة مستخدم تطبيقك.

يتم تحميل البيانات باستخدام القوائم بدلاً من الصفحات

إذا كنت تستخدم قائمة في الذاكرة كبنية بيانات احتياطية لمحوّل واجهة المستخدم، ننصحك بمراقبة تحديثات البيانات باستخدام فئة PagedList إذا كان عدد العناصر في القائمة كبيرًا. يمكن أن تستخدم مثيلات PagedList إما LiveData<PagedList> أو Observable<List> لتمرير تحديثات البيانات إلى واجهة مستخدم تطبيقك، مما يقلل من أوقات التحميل واستخدام الذاكرة. والأفضل من ذلك أنّ استبدال عنصر List بعنصر PagedList في تطبيقك لا يتطلّب إجراء أي تغييرات على بنية واجهة المستخدم في تطبيقك أو منطق تعديل البيانات.

ربط مؤشر بيانات بعرض على شكل قائمة باستخدام CursorAdapter

قد يستخدم تطبيقك CursorAdapter لربط البيانات من Cursor بـ ListView. في هذه الحالة، ستحتاج عادةً إلى نقل البيانات من ListView إلى RecyclerView كحاوية لواجهة مستخدم قائمة تطبيقك، ثم استبدال مكوّن Cursor إما بـ Room أو PositionalDataSource، بناءً على ما إذا كانت مثيلات Cursor تصل إلى قاعدة بيانات SQLite.

في بعض الحالات، مثل عند العمل مع مثيلات Spinner، يمكنك تقديم المحوّل نفسه فقط. بعد ذلك، تأخذ المكتبة البيانات التي تم تحميلها في هذا المحوّل وتعرض البيانات لك. في هذه الحالات، غيِّر نوع بيانات المحوِّل إلى LiveData<PagedList>، ثم غلِّف هذه القائمة في كائن ArrayAdapter قبل محاولة جعل فئة المكتبة تضخِّم هذه العناصر في واجهة مستخدم.

تحميل المحتوى بشكل غير متزامن باستخدام AsyncListUtil

إذا كنت تستخدم عناصر AsyncListUtil لتحميل مجموعات من المعلومات وعرضها بشكل غير متزامن، تعمل مكتبة Paging على تبسيط تحميل البيانات:

  • لا يلزم أن تكون بياناتك موضعية. تتيح لك مكتبة Paging تحميل البيانات مباشرةً من الخلفية باستخدام المفاتيح التي توفّرها الشبكة.
  • يمكن أن تكون بياناتك كبيرة بشكل لا يُحصى. باستخدام مكتبة Paging، يمكنك تحميل البيانات في صفحات حتى لا تتبقّى أي بيانات.
  • يمكنك مراقبة بياناتك بسهولة أكبر. يمكن لمكتبة Paging عرض بياناتك التي يحتفظ بها ViewModel الخاص بتطبيقك في بنية بيانات قابلة للتتبّع.

أمثلة على قواعد البيانات

توضّح مقتطفات الرموز التالية عدة طرق ممكنة لدمج جميع الأجزاء معًا.

مراقبة البيانات المقسّمة إلى صفحات باستخدام LiveData

يوضّح مقتطف الرمز التالي كيفية عمل جميع الأجزاء معًا. عند إضافة أحداث الحفلات الموسيقية أو إزالتها أو تغييرها في قاعدة البيانات، يتم تحديث المحتوى في RecyclerView تلقائيًا وبكفاءة:

Kotlin

@Dao
interface ConcertDao {
    // The Int type parameter tells Room to use a PositionalDataSource
    // object, with position-based loading under the hood.
    @Query("SELECT * FROM concerts ORDER BY date DESC")
    fun concertsByDate(): DataSource.Factory<Int, Concert>
}

class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
    val concertList: LiveData<PagedList<Concert>> =
            concertDao.concertsByDate().toLiveData(pageSize = 50)
}

class ConcertActivity : AppCompatActivity() {
    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Use the 'by viewModels()' Kotlin property delegate
        // from the activity-ktx artifact
        val viewModel: ConcertViewModel by viewModels()
        val recyclerView = findViewById(R.id.concert_list)
        val adapter = ConcertAdapter()
        viewModel.concertList.observe(this, PagedList(adapter::submitList))
        recyclerView.setAdapter(adapter)
    }
}

class ConcertAdapter() :
        PagedListAdapter<Concert, ConcertViewHolder>(DIFF_CALLBACK) {
    fun onBindViewHolder(holder: ConcertViewHolder, position: Int) {
        val concert: Concert? = getItem(position)

        // Note that "concert" is a placeholder if it's null.
        holder.bindTo(concert)
    }

    companion object {
        private val DIFF_CALLBACK = object :
                DiffUtil.ItemCallback<Concert>() {
            // Concert details may have changed if reloaded from the database,
            // but ID is fixed.
            override fun areItemsTheSame(oldConcert: Concert,
                    newConcert: Concert) = oldConcert.id == newConcert.id

            override fun areContentsTheSame(oldConcert: Concert,
                    newConcert: Concert) = oldConcert == newConcert
        }
    }
}

Java

@Dao
public interface ConcertDao {
    // The Integer type parameter tells Room to use a PositionalDataSource
    // object, with position-based loading under the hood.
    @Query("SELECT * FROM concerts ORDER BY date DESC")
    DataSource.Factory<Integer, Concert> concertsByDate();
}

public class ConcertViewModel extends ViewModel {
    private ConcertDao concertDao;
    public final LiveData<PagedList<Concert>> concertList;

    public ConcertViewModel(ConcertDao concertDao) {
        this.concertDao = concertDao;
        concertList = new LivePagedListBuilder<>(
            concertDao.concertsByDate(), /* page size */ 50).build();
    }
}

public class ConcertActivity extends AppCompatActivity {
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ConcertViewModel viewModel =
                new ViewModelProvider(this).get(ConcertViewModel.class);
        RecyclerView recyclerView = findViewById(R.id.concert_list);
        ConcertAdapter adapter = new ConcertAdapter();
        viewModel.concertList.observe(this, adapter::submitList);
        recyclerView.setAdapter(adapter);
    }
}

public class ConcertAdapter
        extends PagedListAdapter<Concert, ConcertViewHolder> {
    protected ConcertAdapter() {
        super(DIFF_CALLBACK);
    }

    @Override
    public void onBindViewHolder(@NonNull ConcertViewHolder holder,
            int position) {
        Concert concert = getItem(position);
        if (concert != null) {
            holder.bindTo(concert);
        } else {
            // Null defines a placeholder item - PagedListAdapter automatically
            // invalidates this row when the actual object is loaded from the
            // database.
            holder.clear();
        }
    }

    private static DiffUtil.ItemCallback<Concert> DIFF_CALLBACK =
            new DiffUtil.ItemCallback<Concert>() {
        // Concert details may have changed if reloaded from the database,
        // but ID is fixed.
        @Override
        public boolean areItemsTheSame(Concert oldConcert, Concert newConcert) {
            return oldConcert.getId() == newConcert.getId();
        }

        @Override
        public boolean areContentsTheSame(Concert oldConcert,
                Concert newConcert) {
            return oldConcert.equals(newConcert);
        }
    };
}

مراقبة البيانات المقسّمة إلى صفحات باستخدام RxJava2

إذا كنت تفضّل استخدام RxJava2 بدلاً من LiveData، يمكنك إنشاء عنصر Observable أو Flowable:

Kotlin

class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
    val concertList: Observable<PagedList<Concert>> =
            concertDao.concertsByDate().toObservable(pageSize = 50)
}

Java

public class ConcertViewModel extends ViewModel {
    private ConcertDao concertDao;
    public final Observable<PagedList<Concert>> concertList;

    public ConcertViewModel(ConcertDao concertDao) {
        this.concertDao = concertDao;

        concertList = new RxPagedListBuilder<>(
                concertDao.concertsByDate(), /* page size */ 50)
                        .buildObservable();
    }
}

يمكنك بعد ذلك بدء مراقبة البيانات وإيقافها باستخدام الرمز في المقتطف التالي:

Kotlin

class ConcertActivity : AppCompatActivity() {
    private val adapter = ConcertAdapter()

    // Use the 'by viewModels()' Kotlin property delegate
    // from the activity-ktx artifact
    private val viewModel: ConcertViewModel by viewModels()

    private val disposable = CompositeDisposable()

    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val recyclerView = findViewById(R.id.concert_list)
        recyclerView.setAdapter(adapter)
    }

    override fun onStart() {
        super.onStart()
        disposable.add(viewModel.concertList
                .subscribe(adapter::submitList)))
    }

    override fun onStop() {
        super.onStop()
        disposable.clear()
    }
}

Java

public class ConcertActivity extends AppCompatActivity {
    private ConcertAdapter adapter = new ConcertAdapter();
    private ConcertViewModel viewModel;

    private CompositeDisposable disposable = new CompositeDisposable();

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        RecyclerView recyclerView = findViewById(R.id.concert_list);

        viewModel = new ViewModelProvider(this).get(ConcertViewModel.class);
        recyclerView.setAdapter(adapter);
    }

    @Override
    protected void onStart() {
        super.onStart();
        disposable.add(viewModel.concertList
                .subscribe(adapter.submitList(flowableList)
        ));
    }

    @Override
    protected void onStop() {
        super.onStop();
        disposable.clear();
    }
}

رمز ConcertDao وConcertAdapter هو نفسه بالنسبة إلى الحلّ المستند إلى RxJava2 والحلّ المستند إلى LiveData.

تقديم ملاحظات

يمكنك مشاركة ملاحظاتك وأفكارك معنا من خلال المراجع التالية:

Issue Tracker
يمكنك الإبلاغ عن المشاكل لنتمكّن من إصلاح الأخطاء.

مراجع إضافية

لمزيد من المعلومات حول مكتبة Paging، يُرجى الاطّلاع على المراجع التالية.

نماذج

اختبارات الرموز

الفيديوهات