تبدیل جریان‌های داده (Views)

مفاهیم و پیاده‌سازی Jetpack Compose

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

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

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

اعمال تبدیل‌های اولیه

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

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

جاوا

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

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

جاوا

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

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

شما می‌توانید با استفاده از یک API جریانی (stream API) به ورودی کوئری گوش دهید. مرجع جریان را در ViewModel خود نگه دارید. لایه رابط کاربری (UI layer) نباید مستقیماً به آن دسترسی داشته باشد؛ در عوض، تابعی تعریف کنید تا ViewModel را از کوئری کاربر مطلع کند.

جاوا

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

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

جاوا

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

جاوا

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

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

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

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

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

جاوا

// 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. مدل رابط کاربری را طوری تغییر دهید که آیتم‌های جداکننده را در خود جای دهد.
  2. جریان داده را طوری تغییر دهید که به صورت پویا جداکننده‌ها را بین بارگذاری داده‌ها و ارائه داده‌ها اضافه کند.
  3. رابط کاربری را برای مدیریت آیتم‌های جداکننده به‌روزرسانی کنید.

تبدیل مدل رابط کاربری

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

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

جاوا

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

جاوا

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

مدیریت جداکننده‌ها در رابط کاربری

مرحله آخر، تغییر رابط کاربری (UI) برای تطبیق با نوع آیتم جداکننده است. یک طرح‌بندی (layout) و یک نگهدارنده نما (view holder) برای آیتم‌های جداکننده خود ایجاد کنید و آداپتور لیست (list adapter) را طوری تغییر دهید که از RecyclerView.ViewHolder به عنوان نوع نگهدارنده نما (view holder) استفاده کند تا بتواند بیش از یک نوع نگهدارنده نما (view holder) را مدیریت کند. به عنوان یک روش جایگزین، می‌توانید یک کلاس پایه مشترک تعریف کنید که هم کلاس‌های نگهدارنده آیتم و هم کلاس‌های نگهدارنده نمای جداکننده شما از آن ارث‌بری کنند.

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

  • برای در نظر گرفتن آیتم‌های لیست جداکننده، به متدهای onCreateViewHolder() و onBindViewHolder() موارد (case) اضافه کنید.
  • یک مقایسه‌گر جدید پیاده‌سازی کنید.

جاوا

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

از کارهای تکراری پرهیز کنید

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

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

جاوا

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

کدلبز

{% کلمه به کلمه %} {% فعل کمکی %} {% کلمه به کلمه %} {% فعل کمکی %}