جریان های داده را تغییر دهید

هنگامی که با داده های صفحه بندی شده کار می کنید، اغلب نیاز دارید که جریان داده را هنگام بارگذاری تغییر دهید. به عنوان مثال، ممکن است لازم باشد فهرستی از موارد را فیلتر کنید یا موارد را قبل از ارائه آنها در UI به نوع دیگری تبدیل کنید. یکی دیگر از موارد استفاده رایج برای تبدیل جریان داده، افزودن جداکننده های لیست است.

به طور کلی تر، اعمال تبدیل ها به طور مستقیم در جریان داده به شما این امکان را می دهد که ساختارهای مخزن و ساختارهای UI را جدا نگه دارید.

این صفحه فرض می کند که شما با استفاده اولیه از کتابخانه Paging آشنا هستید.

تغییرات اساسی را اعمال کنید

از آنجایی که PagingData در یک جریان واکنشی محصور شده است، می توانید عملیات تبدیل را بر روی داده ها به صورت تدریجی بین بارگذاری داده ها و ارائه آن اعمال کنید.

برای اعمال تبدیل‌ها به هر شیء PagingData در جریان، تبدیل‌ها را در یک عملیات map() روی جریان قرار دهید:

کاتلین

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

جاوا

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

جاوا

// 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 انجام دهید.

یکی از موارد استفاده رایج برای این، نگاشت یک شی لایه شبکه یا پایگاه داده بر روی یک شی است که به طور خاص در لایه UI استفاده می شود. مثال زیر نحوه اعمال این نوع عملیات نقشه را نشان می دهد:

کاتلین

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

جاوا

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

جاوا

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

یکی دیگر از تبدیل‌های رایج داده، گرفتن ورودی از کاربر، مانند یک query string، و تبدیل آن به خروجی درخواست برای نمایش است. تنظیم این نیاز به گوش دادن و گرفتن ورودی پرس و جو کاربر، انجام درخواست و بازگرداندن نتیجه پرس و جو به رابط کاربری دارد.

می‌توانید با استفاده از یک API جریان، به ورودی پرس و جو گوش دهید. مرجع جریان را در ViewModel خود نگه دارید. لایه UI نباید دسترسی مستقیم به آن داشته باشد. در عوض، تابعی را تعریف کنید تا ViewModel را از درخواست کاربر مطلع کند.

کاتلین

private val queryFlow = MutableStateFlow("")

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

جاوا

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

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

جاوا

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

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

هنگامی که مقدار پرس و جو در جریان داده تغییر می کند، می توانید عملیات تبدیل مقدار پرس و جو را به نوع داده مورد نظر انجام دهید و نتیجه را به لایه UI برگردانید. تابع تبدیل خاص به زبان و چارچوب مورد استفاده بستگی دارد، اما همه آنها عملکرد مشابهی را ارائه می دهند.

کاتلین

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

جاوا

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

جاوا

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

استفاده از عملیاتی مانند flatMapLatest یا switchMap تضمین می کند که فقط آخرین نتایج به UI بازگردانده می شوند. اگر کاربر قبل از تکمیل عملیات پایگاه داده، ورودی پرس و جو خود را تغییر دهد، این عملیات نتایج جستجوی قدیمی را حذف کرده و جستجوی جدید را بلافاصله راه اندازی می کند.

فیلتر کردن داده ها

یکی دیگر از عملیات رایج فیلتر کردن است. می‌توانید داده‌ها را بر اساس معیارهای کاربر فیلتر کنید، یا اگر بر اساس معیارهای دیگر باید داده‌ها را از رابط کاربری پنهان کنید، می‌توانید آن‌ها را حذف کنید.

شما باید این عملیات فیلتر را داخل فراخوانی map() قرار دهید زیرا فیلتر روی شی PagingData اعمال می شود. هنگامی که داده ها از PagingData فیلتر شدند، نمونه جدید PagingData برای نمایش به لایه UI ارسال می شود.

کاتلین

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

جاوا

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

جاوا

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

جداکننده های لیست را اضافه کنید

کتابخانه Paging از جداکننده های لیست پویا پشتیبانی می کند. می توانید خوانایی لیست را با قرار دادن جداکننده ها به طور مستقیم در جریان داده به عنوان آیتم های لیست RecyclerView بهبود بخشید. در نتیجه، جداکننده‌ها اشیاء ViewHolder با ویژگی‌های کامل هستند که تعامل، فوکوس دسترسی و همه ویژگی‌های دیگر ارائه شده توسط View را امکان‌پذیر می‌کنند.

سه مرحله برای وارد کردن جداکننده ها در لیست صفحه بندی شده شما وجود دارد:

  1. مدل UI را برای قرار دادن موارد جداکننده تبدیل کنید.
  2. جریان داده را تغییر دهید تا جداکننده ها را به صورت پویا بین بارگذاری داده ها و ارائه داده ها اضافه کنید.
  3. برای رسیدگی به موارد جداکننده، رابط کاربری را به‌روزرسانی کنید.

تبدیل مدل UI

کتابخانه Paging جداکننده‌های لیست را به عنوان آیتم‌های فهرست واقعی در RecyclerView وارد می‌کند، اما آیتم‌های جداکننده باید از آیتم‌های داده در لیست قابل تشخیص باشند تا بتوانند به یک نوع ViewHolder متفاوت با یک رابط کاربری مجزا متصل شوند. راه حل این است که یک کلاس مهر و موم شده Kotlin با کلاس های فرعی برای نمایش داده ها و جداکننده های شما ایجاد کنید. یا می توانید یک کلاس پایه ایجاد کنید که توسط کلاس آیتم لیست و کلاس جداکننده شما گسترش می یابد.

فرض کنید می‌خواهید جداکننده‌ها را به فهرست صفحه‌ای از آیتم‌های User اضافه کنید. قطعه زیر نحوه ایجاد یک کلاس پایه را نشان می دهد که در آن نمونه ها می توانند یک UserModel یا یک SeparatorModel باشند:

کاتلین

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

جاوا

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

جاوا

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> با جداکننده های اضافه شده نشان می دهد:

کاتلین

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

جاوا

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

جاوا

// 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 به عنوان نوع نگهدارنده view آن تغییر دهید تا بتواند بیش از یک نوع نگهدارنده نمایش را مدیریت کند. از طرف دیگر، می‌توانید یک کلاس پایه مشترک تعریف کنید که هم کلاس‌های نگهدارنده نمای آیتم و جداکننده شما گسترش می‌یابد.

همچنین باید تغییرات زیر را در آداپتور لیست خود ایجاد کنید:

  • موارد را به متدهای onCreateViewHolder() و onBindViewHolder() اضافه کنید تا موارد لیست جداکننده را حساب کنید.
  • یک مقایسه کننده جدید پیاده سازی کنید.

کاتلین

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
}

جاوا

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

جاوا

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

از کارهای تکراری خودداری کنید

یکی از مسائل کلیدی که باید از آن اجتناب کنید، انجام کارهای غیر ضروری از برنامه است. واکشی داده ها عملیات گران قیمتی است و تبدیل داده ها نیز می تواند زمان ارزشمندی را به خود اختصاص دهد. هنگامی که داده ها بارگیری و برای نمایش در UI آماده شدند، در صورت بروز تغییر پیکربندی و نیاز به ایجاد مجدد رابط کاربری، باید ذخیره شوند.

عملیات cachedIn() نتایج هر تبدیلی که قبل از آن رخ می دهد را در حافظه پنهان ذخیره می کند. بنابراین، cachedIn() باید آخرین تماس در ViewModel شما باشد.

کاتلین

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

جاوا

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

جاوا

// 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، به منابع اضافی زیر مراجعه کنید:

Codelabs

نمونه ها

{% کلمه به کلمه %} {% آخر کلمه %} {% کلمه به کلمه %} {% آخر کلمه %}