تحويل مصادر البيانات (طرق العرض)

المفاهيم والتنفيذ في Jetpack Compose

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

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

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

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

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

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

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.

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

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 بطلب البحث الذي أدخله المستخدم.

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)
}

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

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 الجديد إلى طبقة واجهة المستخدم لعرضه.

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())
)

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

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

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

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

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

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

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

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> مع إضافة فواصل:

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() لأخذ عناصر قائمة الفواصل في الاعتبار.
  • تطبيق أداة مقارنة جديدة

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.

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، يُرجى الاطّلاع على المراجع الإضافية التالية:

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