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

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

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

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

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

بما أنّ 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.

مصادر إضافية

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

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