تحويل مصادر البيانات

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

بشكل أكثر عمومية، يسمح لك تطبيق التحويلات مباشرةً على مصدر البيانات بالحفاظ على بُنى المستودع وإنشاءات واجهة المستخدم منفصلة.

تفترض هذه الصفحة أنك على دراية بالاستخدام الأساسي لمكتبة تسجيل الصفحات.

تطبيق التحويلات الأساسية

بما أنّ PagingData يتم تضمينها في بث تفاعلي، يمكنك تطبيق عمليات التحويل على البيانات بشكل تدريجي بين تحميل البيانات وعرضها.

لتطبيق التحويلات على كل عنصر PagingData في ساحة المشاركات، يجب وضع عمليات التحويل داخل عملية map() في ساحة المشاركات:

Kotlin

pager.flow // Type is Flow<PagingData<User>>.
  // Map the outer stream so that the transformations are applied to
  // each new generation of PagingData.
  .map { pagingData ->
    // Transformations in this block are applied to the items
    // in the paged data.
}

Java

PagingRx.getFlowable(pager) // Type is Flowable<PagingData<User>>.
  // Map the outer stream so that the transformations are applied to
  // each new generation of PagingData.
  .map(pagingData -> {
    // Transformations in this block are applied to the items
    // in the paged data.
  });

Java

// Map the outer stream so that the transformations are applied to
// each new generation of PagingData.
Transformations.map(
  // Type is LiveData<PagingData<User>>.
  PagingLiveData.getLiveData(pager),
  pagingData -> {
    // Transformations in this block are applied to the items
    // in the paged data.
  });

تحويل البيانات

العملية الأساسية في مجموعة مصادر البيانات هي تحويلها إلى نوع مختلف. بعد أن تتمكّن من الوصول إلى الكائن PagingData، يمكنك تنفيذ عملية map() على كل عنصر فردي في القائمة المقسّمة إلى صفحات ضمن الكائن PagingData.

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

Kotlin

pager.flow // Type is Flow<PagingData<User>>.
  .map { pagingData ->
    pagingData.map { user -> UiModel(user) }
  }

Java

// Type is Flowable<PagingData<User>>.
PagingRx.getFlowable(pager)
  .map(pagingData ->
    pagingData.map(UiModel.UserModel::new)
  )

Java

Transformations.map(
  // Type is LiveData<PagingData<User>>.
  PagingLiveData.getLiveData(pager),
  pagingData ->
    pagingData.map(UiModel.UserModel::new)
)

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

يمكنك رصد إدخالات طلب البحث باستخدام واجهة برمجة تطبيقات البث. احتفِظ بمرجع البث في ViewModel. لا ينبغي أن يكون لطبقة واجهة المستخدم وصول مباشر إليها؛ بدلاً من ذلك، قم بتحديد دالة لإشعار ViewModel عن استعلام المستخدم.

Kotlin

private val queryFlow = MutableStateFlow("")

fun onQueryChanged(query: String) {
  queryFlow.value = query
}

Java

private BehaviorSubject<String> querySubject = BehaviorSubject.create("");

public void onQueryChanged(String query) {
  queryFlow.onNext(query)
}

Java

private MutableLiveData<String> queryLiveData = new MutableLiveData("");

public void onQueryChanged(String query) {
  queryFlow.setValue(query)
}

عندما تتغير قيمة طلب البحث في مصدر البيانات، يمكنك إجراء عمليات لتحويل قيمة طلب البحث إلى نوع البيانات المطلوب وإعادة النتيجة إلى طبقة واجهة المستخدم. تعتمد دالة التحويل المحددة على اللغة وإطار العمل المستخدمين، لكنها توفر جميعها وظائف مماثلة.

Kotlin

val querySearchResults = queryFlow.flatMapLatest { query ->
  // The database query returns a Flow which is output through
  // querySearchResults
  userDatabase.searchBy(query)
}

Java

Observable<User> querySearchResults =
  querySubject.switchMap(query -> userDatabase.searchBy(query));

Java

LiveData<User> querySearchResults = Transformations.switchMap(
  queryLiveData,
  query -> userDatabase.searchBy(query)
);

يضمن استخدام عمليات مثل flatMapLatest أو switchMap عرض أحدث النتائج فقط إلى واجهة المستخدم. إذا غيّر المستخدم إدخال طلب البحث قبل اكتمال عملية قاعدة البيانات، فإن هذه العمليات تتجاهل النتائج من الاستعلام القديم وتبدأ البحث الجديد على الفور.

فلترة البيانات

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

عليك وضع عمليات الفلترة هذه داخل استدعاء map() لأنّ الفلتر ينطبق على الكائن PagingData. بعد فلترة البيانات من PagingData، يتم تمرير مثيل PagingData الجديد إلى طبقة واجهة المستخدم لعرضها.

Kotlin

pager.flow // Type is Flow<PagingData<User>>.
  .map { pagingData ->
    pagingData.filter { user -> !user.hiddenFromUi }
  }

Java

// Type is Flowable<PagingData<User>>.
PagingRx.getFlowable(pager)
  .map(pagingData ->
    pagingData.filter(user -> !user.isHiddenFromUi())
  )
}

Java

Transformations.map(
  // Type is LiveData<PagingData<User>>.
  PagingLiveData.getLiveData(pager),
  pagingData ->
    pagingData.filter(user -> !user.isHiddenFromUi())
)

إضافة فواصل القوائم

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

هناك ثلاث خطوات لإدراج فواصل في القائمة المقسّمة إلى صفحات:

  1. قم بتحويل نموذج واجهة المستخدم لتلائم العناصر الفاصلة.
  2. يمكنك تحويل مصدر البيانات لإضافة الفواصل بشكلٍ ديناميكي بين تحميل البيانات وعرضها.
  3. يجب تعديل واجهة المستخدم لمعالجة العناصر الفاصلة.

تحويل نموذج واجهة المستخدم

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

لنفترض أنّك تريد إضافة فواصل إلى قائمة مقسّمة إلى صفحات من User عنصر. يوضّح المقتطف التالي كيفية إنشاء فئة أساسية حيث يمكن أن تكون المثيلات UserModel أو SeparatorModel:

Kotlin

sealed class UiModel {
  class UserModel(val id: String, val label: String) : UiModel() {
    constructor(user: User) : this(user.id, user.label)
  }

  class SeparatorModel(val description: String) : UiModel()
}

Java

class UiModel {
  private UiModel() {}

  static class UserModel extends UiModel {
    @NonNull
    private String mId;
    @NonNull
    private String mLabel;

    UserModel(@NonNull String id, @NonNull String label) {
      mId = id;
      mLabel = label;
    }

    UserModel(@NonNull User user) {
      mId = user.id;
      mLabel = user.label;
    }

    @NonNull
    public String getId() {
      return mId;
    }

    @NonNull
    public String getLabel() {
      return mLabel;
      }
    }

    static class SeparatorModel extends UiModel {
    @NonNull
    private String mDescription;

    SeparatorModel(@NonNull String description) {
      mDescription = description;
    }

    @NonNull
    public String getDescription() {
      return mDescription;
    }
  }
}

Java

class UiModel {
  private UiModel() {}

  static class UserModel extends UiModel {
    @NonNull
    private String mId;
    @NonNull
    private String mLabel;

    UserModel(@NonNull String id, @NonNull String label) {
      mId = id;
      mLabel = label;
    }

    UserModel(@NonNull User user) {
      mId = user.id;
      mLabel = user.label;
    }

    @NonNull
    public String getId() {
      return mId;
    }

    @NonNull
    public String getLabel() {
      return mLabel;
      }
    }

    static class SeparatorModel extends UiModel {
    @NonNull
    private String mDescription;

    SeparatorModel(@NonNull String description) {
      mDescription = description;
    }

    @NonNull
    public String getDescription() {
      return mDescription;
    }
  }
}

تحويل مصدر البيانات

يجب تطبيق عمليات التحويل على مصدر البيانات بعد تحميله وقبل تقديمه. يجب أن تؤدي عمليات التحويل إلى ما يلي:

  • تحويل عناصر القائمة التي تم تحميلها إلى نوع العنصر الأساسي الجديد
  • استخدِم الإجراء PagingData.insertSeparators() لإضافة فواصل.

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

يوضّح المثال التالي عمليات التحويل لتعديل مصدر بيانات PagingData<User> إلى مصدر بيانات PagingData<UiModel> مع إضافة فواصل:

Kotlin

pager.flow.map { pagingData: PagingData<User> ->
  // Map outer stream, so you can perform transformations on
  // each paging generation.
  pagingData
  .map { user ->
    // Convert items in stream to UiModel.UserModel.
    UiModel.UserModel(user)
  }
  .insertSeparators<UiModel.UserModel, UiModel> { before, after ->
    when {
      before == null -> UiModel.SeparatorModel("HEADER")
      after == null -> UiModel.SeparatorModel("FOOTER")
      shouldSeparate(before, after) -> UiModel.SeparatorModel(
        "BETWEEN ITEMS $before AND $after"
      )
      // Return null to avoid adding a separator between two items.
      else -> null
    }
  }
}

Java

// Map outer stream, so you can perform transformations on each
// paging generation.
PagingRx.getFlowable(pager).map(pagingData -> {
  // First convert items in stream to UiModel.UserModel.
  PagingData<UiModel> uiModelPagingData = pagingData.map(
    UiModel.UserModel::new);

  // Insert UiModel.SeparatorModel, which produces PagingData of
  // generic type UiModel.
  return PagingData.insertSeparators(uiModelPagingData,
    (@Nullable UiModel before, @Nullable UiModel after) -> {
      if (before == null) {
        return new UiModel.SeparatorModel("HEADER");
      } else if (after == null) {
        return new UiModel.SeparatorModel("FOOTER");
      } else if (shouldSeparate(before, after)) {
        return new UiModel.SeparatorModel("BETWEEN ITEMS "
          + before.toString() + " AND " + after.toString());
      } else {
        // Return null to avoid adding a separator between two
        // items.
        return null;
      }
    });
});

Java

// Map outer stream, so you can perform transformations on each
// paging generation.
Transformations.map(PagingLiveData.getLiveData(pager),
  pagingData -> {
    // First convert items in stream to UiModel.UserModel.
    PagingData<UiModel> uiModelPagingData = pagingData.map(
      UiModel.UserModel::new);

    // Insert UiModel.SeparatorModel, which produces PagingData of
    // generic type UiModel.
    return PagingData.insertSeparators(uiModelPagingData,
      (@Nullable UiModel before, @Nullable UiModel after) -> {
        if (before == null) {
          return new UiModel.SeparatorModel("HEADER");
        } else if (after == null) {
          return new UiModel.SeparatorModel("FOOTER");
        } else if (shouldSeparate(before, after)) {
          return new UiModel.SeparatorModel("BETWEEN ITEMS "
            + before.toString() + " AND " + after.toString());
        } else {
          // Return null to avoid adding a separator between two
          // items.
          return null;
        }
      });
  });

معالجة الفواصل في واجهة المستخدم

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

يجب أيضًا إجراء التغييرات التالية على محوّل القائمة:

  • أضِف حالات إلى طريقتَي onCreateViewHolder() وonBindViewHolder() لحساب عناصر القائمة الفاصلة.
  • نفِّذ أداة مقارنة جديدة.

Kotlin

class UiModelAdapter :
  PagingDataAdapter<UiModel, RecyclerView.ViewHolder>(UiModelComparator) {

  override fun onCreateViewHolder(
    parent: ViewGroup,
    viewType: Int
  ) = when (viewType) {
    R.layout.item -> UserModelViewHolder(parent)
    else -> SeparatorModelViewHolder(parent)
  }

  override fun getItemViewType(position: Int) {
    // Use peek over getItem to avoid triggering page fetch / drops, since
    // recycling views is not indicative of the user's current scroll position.
    return when (peek(position)) {
      is UiModel.UserModel -> R.layout.item
      is UiModel.SeparatorModel -> R.layout.separator_item
      null -> throw IllegalStateException("Unknown view")
    }
  }

  override fun onBindViewHolder(
    holder: RecyclerView.ViewHolder,
    position: Int
  ) {
    val item = getItem(position)
    if (holder is UserModelViewHolder) {
      holder.bind(item as UserModel)
    } else if (holder is SeparatorModelViewHolder) {
      holder.bind(item as SeparatorModel)
    }
  }
}

object UiModelComparator : DiffUtil.ItemCallback<UiModel>() {
  override fun areItemsTheSame(
    oldItem: UiModel,
    newItem: UiModel
  ): Boolean {
    val isSameRepoItem = oldItem is UiModel.UserModel
      && newItem is UiModel.UserModel
      && oldItem.id == newItem.id

    val isSameSeparatorItem = oldItem is UiModel.SeparatorModel
      && newItem is UiModel.SeparatorModel
      && oldItem.description == newItem.description

    return isSameRepoItem || isSameSeparatorItem
  }

  override fun areContentsTheSame(
    oldItem: UiModel,
    newItem: UiModel
  ) = oldItem == newItem
}

Java

class UiModelAdapter extends PagingDataAdapter<UiModel, RecyclerView.ViewHolder> {
  UiModelAdapter() {
    super(new UiModelComparator(), Dispatchers.getMain(),
      Dispatchers.getDefault());
  }

  @NonNull
  @Override
  public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
    int viewType) {
    if (viewType == R.layout.item) {
      return new UserModelViewHolder(parent);
    } else {
      return new SeparatorModelViewHolder(parent);
    }
  }

  @Override
  public int getItemViewType(int position) {
    // Use peek over getItem to avoid triggering page fetch / drops, since
    // recycling views is not indicative of the user's current scroll position.
    UiModel item = peek(position);
    if (item instanceof UiModel.UserModel) {
      return R.layout.item;
    } else if (item instanceof UiModel.SeparatorModel) {
      return R.layout.separator_item;
    } else {
      throw new IllegalStateException("Unknown view");
    }
  }

  @Override
  public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder,
    int position) {
    if (holder instanceOf UserModelViewHolder) {
      UserModel userModel = (UserModel) getItem(position);
      ((UserModelViewHolder) holder).bind(userModel);
    } else {
      SeparatorModel separatorModel = (SeparatorModel) getItem(position);
      ((SeparatorModelViewHolder) holder).bind(separatorModel);
    }
  }
}

class UiModelComparator extends DiffUtil.ItemCallback<UiModel> {
  @Override
  public boolean areItemsTheSame(@NonNull UiModel oldItem,
    @NonNull UiModel newItem) {
    boolean isSameRepoItem = oldItem instanceof UserModel
      && newItem instanceof UserModel
      && ((UserModel) oldItem).getId().equals(((UserModel) newItem).getId());

    boolean isSameSeparatorItem = oldItem instanceof SeparatorModel
      && newItem instanceof SeparatorModel
      && ((SeparatorModel) oldItem).getDescription().equals(
      ((SeparatorModel) newItem).getDescription());

    return isSameRepoItem || isSameSeparatorItem;
  }

  @Override
  public boolean areContentsTheSame(@NonNull UiModel oldItem,
    @NonNull UiModel newItem) {
    return oldItem.equals(newItem);
  }
}

Java

class UiModelAdapter extends PagingDataAdapter<UiModel, RecyclerView.ViewHolder> {
  UiModelAdapter() {
    super(new UiModelComparator(), Dispatchers.getMain(),
      Dispatchers.getDefault());
  }

  @NonNull
  @Override
  public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
    int viewType) {
    if (viewType == R.layout.item) {
      return new UserModelViewHolder(parent);
    } else {
      return new SeparatorModelViewHolder(parent);
    }
  }

  @Override
  public int getItemViewType(int position) {
    // Use peek over getItem to avoid triggering page fetch / drops, since
    // recycling views is not indicative of the user's current scroll position.
    UiModel item = peek(position);
    if (item instanceof UiModel.UserModel) {
      return R.layout.item;
    } else if (item instanceof UiModel.SeparatorModel) {
      return R.layout.separator_item;
    } else {
      throw new IllegalStateException("Unknown view");
    }
  }

  @Override
  public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder,
    int position) {
    if (holder instanceOf UserModelViewHolder) {
      UserModel userModel = (UserModel) getItem(position);
      ((UserModelViewHolder) holder).bind(userModel);
    } else {
      SeparatorModel separatorModel = (SeparatorModel) getItem(position);
      ((SeparatorModelViewHolder) holder).bind(separatorModel);
    }
  }
}

class UiModelComparator extends DiffUtil.ItemCallback<UiModel> {
  @Override
  public boolean areItemsTheSame(@NonNull UiModel oldItem,
    @NonNull UiModel newItem) {
    boolean isSameRepoItem = oldItem instanceof UserModel
      && newItem instanceof UserModel
      && ((UserModel) oldItem).getId().equals(((UserModel) newItem).getId());

    boolean isSameSeparatorItem = oldItem instanceof SeparatorModel
      && newItem instanceof SeparatorModel
      && ((SeparatorModel) oldItem).getDescription().equals(
      ((SeparatorModel) newItem).getDescription());

    return isSameRepoItem || isSameSeparatorItem;
  }

  @Override
  public boolean areContentsTheSame(@NonNull UiModel oldItem,
    @NonNull UiModel newItem) {
    return oldItem.equals(newItem);
  }
}

تجنُّب العمل المكرّر

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

تخزِّن العملية cachedIn() مؤقتًا نتائج أي عمليات تحويل تحدث قبلها. ولذلك، يجب أن يكون cachedIn() هو الاستدعاء الأخير في ViewModel.

Kotlin

pager.flow // Type is Flow<PagingData<User>>.
  .map { pagingData ->
    pagingData.filter { user -> !user.hiddenFromUi }
      .map { user -> UiModel.UserModel(user) }
  }
  .cachedIn(viewModelScope)

Java

// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact.
CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel);
PagingRx.cachedIn(
  // Type is Flowable<PagingData<User>>.
  PagingRx.getFlowable(pager)
    .map(pagingData -> pagingData
      .filter(user -> !user.isHiddenFromUi())
      .map(UiModel.UserModel::new)),
  viewModelScope);
}

Java

// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact.
CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel);
PagingLiveData.cachedIn(
  Transformations.map(
    // Type is LiveData<PagingData<User>>.
    PagingLiveData.getLiveData(pager),
    pagingData -> pagingData
      .filter(user -> !user.isHiddenFromUi())
      .map(UiModel.UserModel::new)),
  viewModelScope);

لمزيد من المعلومات حول استخدام cachedIn() مع مصدر بيانات PagingData، يُرجى الاطّلاع على إعداد بث PagingData.

مراجع إضافية

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

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

عيّنات