نظرة عامة على المكتبة في صفحة النقل 2 جزء من Android Jetpack.

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

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

ضبط إعدادات

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

رائع

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
}

هندسة المكتبة

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

قائمة صفحات

المكوّن الرئيسي في مكتبة تسجيل الصفحات هو فئة 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.

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

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. وتعمل هذه الفئات معًا لجلب المحتوى وعرضه أثناء تحميله، والجلب المُسبق للمحتوى خارج العرض، والمؤثرات الحركية للمحتوى.

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

دعم بنيات البيانات المختلفة

تتوافق "مكتبة تسجيل الصفحات" مع بُنى البيانات التالية:

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

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

مخططات لتدفقات البيانات
الشكل 1. كيفية تدفق البيانات في كل بنية من البِنى التي تتوافق معها "مكتبة تسجيل الصفحات"

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

الشبكة فقط

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

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

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

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

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

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

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

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

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

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

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

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

إذا كنت تستخدم وظائف مخصّصة لتحميل مجموعات فرعية صغيرة من البيانات من مصدر بيانات تطبيقك، يمكنك استبدال هذا المنطق من الفئة 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 قبل محاولة تضخيم فئة مكتبة إلى هذه العناصر في واجهة المستخدم.

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

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

  • ليس من الضروري أن تكون بياناتك موضعية. تتيح لك مكتبة الترحيل تحميل البيانات مباشرة من الخلفية باستخدام المفاتيح التي توفرها الشبكة.
  • قد تكون بياناتك كبيرة بشكل لا يُحصى. باستخدام مكتبة التقسيم، يمكنك تحميل البيانات إلى الصفحات حتى نفاد أي بيانات.
  • يمكنك تتبُّع بياناتك بسهولة أكبر. يمكن لمكتبة الترحيل تقديم بياناتك التي يحتفظ بها 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.

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

يُرجى مشاركة ملاحظاتك وآرائك معنا من خلال الموارد التالية:

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

مراجع إضافية

لمعرفة المزيد حول مكتبة الترحيل، يمكنك الرجوع إلى الموارد التالية.

عيّنات

الدروس التطبيقية حول الترميز

الفيديوهات الطويلة