เปลี่ยนรูปแบบสตรีมข้อมูล (มุมมอง)

แนวคิดและการใช้งาน Jetpack Compose

เมื่อทำงานกับข้อมูลที่แบ่งหน้า คุณมักจะต้อง เปลี่ยนรูปแบบสตรีมข้อมูลขณะโหลด เช่น คุณอาจต้องกรองรายการ หรือแปลงรายการเป็นประเภทอื่นก่อนที่จะแสดงใน UI กรณีการใช้งานทั่วไปอีกอย่างหนึ่งสำหรับการเปลี่ยนรูปแบบสตรีมข้อมูลคือการเพิ่มตัวคั่นรายการ

โดยทั่วไปแล้ว การใช้การเปลี่ยนรูปแบบกับสตรีมข้อมูลโดยตรงจะช่วยให้คุณ แยกโครงสร้างที่เก็บและโครงสร้าง UI ออกจากกันได้

หน้านี้ถือว่าคุณคุ้นเคยกับการใช้งานพื้นฐานของไลบรารีการแบ่งหน้า

ใช้การเปลี่ยนรูปแบบพื้นฐาน

เนื่องจาก 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 ได้

กรณีการใช้งานทั่วไปอย่างหนึ่งคือการแมปออบเจ็กต์เลเยอร์เครือข่ายหรือฐานข้อมูลกับออบเจ็กต์ที่ใช้ในเลเยอร์ UI โดยเฉพาะ ตัวอย่างด้านล่างแสดงวิธี ใช้การดำเนินการแมปประเภทนี้

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

การแปลงข้อมูลที่พบบ่อยอีกอย่างคือการรับอินพุตจากผู้ใช้ เช่น สตริงการค้นหา แล้วแปลงเป็นเอาต์พุตคำขอเพื่อแสดง การตั้งค่านี้ ต้องคอยรับฟังและบันทึกข้อมูลคำค้นหาของผู้ใช้ ทำการ ขอ และส่งผลการค้นหากลับไปยัง UI

คุณสามารถฟังอินพุตการค้นหาโดยใช้ Stream API เก็บข้อมูลอ้างอิงสตรีม ไว้ใน ViewModel เลเยอร์ UI ไม่ควรมีสิทธิ์เข้าถึงโดยตรง แต่ควร กำหนดฟังก์ชันเพื่อแจ้ง 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)
}

เมื่อค่าการค้นหาเปลี่ยนแปลงในสตรีมข้อมูล คุณสามารถดำเนินการเพื่อ แปลงค่าการค้นหาเป็นประเภทข้อมูลที่ต้องการและแสดงผลลัพธ์ไปยังเลเยอร์ UI ฟังก์ชัน Conversion ที่เฉพาะเจาะจงจะขึ้นอยู่กับภาษาและเฟรมเวิร์ก ที่ใช้ แต่ทั้งหมดจะให้ฟังก์ชันการทำงานที่คล้ายกัน

Java

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

Java

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

การใช้การดำเนินการอย่าง flatMapLatest หรือ switchMap จะช่วยให้มั่นใจได้ว่าระบบจะแสดงเฉพาะผลลัพธ์ล่าสุดใน UI หากผู้ใช้เปลี่ยนอินพุตคำค้นหาก่อนที่การดำเนินการฐานข้อมูลจะเสร็จสมบูรณ์ การดำเนินการเหล่านี้จะทิ้งผลลัพธ์จากคำค้นหาเก่าและเปิดการค้นหาใหม่ทันที

กรองข้อมูล

การดำเนินการอีกอย่างที่พบบ่อยคือการกรอง คุณสามารถกรองข้อมูลตามเกณฑ์ จากผู้ใช้ หรือนำข้อมูลออกจาก UI ได้หากควรซ่อนข้อมูลตาม เกณฑ์อื่นๆ

คุณต้องวางการดำเนินการตัวกรองเหล่านี้ไว้ภายในเรียกใช้ map() เนื่องจากตัวกรองใช้กับออบเจ็กต์ PagingData เมื่อกรองข้อมูลออกจาก PagingDataแล้ว ระบบจะส่งอินสแตนซ์ PagingData ใหม่ไปยังเลเยอร์ UI เพื่อ แสดง

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 มีให้

ขั้นตอนในการแทรกตัวคั่นลงในรายการแบบแบ่งหน้ามี 3 ขั้นตอนดังนี้

  1. แปลงโมเดล UI เพื่อรองรับรายการตัวคั่น
  2. เปลี่ยนสตรีมข้อมูลเพื่อเพิ่มตัวคั่นระหว่างการโหลดข้อมูลและการนำเสนอข้อมูลแบบไดนามิก
  3. อัปเดต UI เพื่อจัดการรายการตัวคั่น

แปลงโมเดล UI

ไลบรารีการแบ่งหน้าจะแทรกตัวคั่นรายการลงใน RecyclerView เป็นรายการจริง แต่ต้องแยกรายการตัวคั่นออกจากรายการข้อมูลในรายการได้เพื่อให้ผูกกับ ViewHolder ประเภทอื่นที่มี UI ที่แตกต่างกันได้ วิธีแก้คือการสร้างคลาสที่ปิดผนึกของ 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;
        }
      });
  });

จัดการตัวคั่นใน UI

ขั้นตอนสุดท้ายคือการเปลี่ยน UI เพื่อรองรับประเภทรายการตัวคั่น สร้างเลย์เอาต์และตัวยึดมุมมองสำหรับรายการตัวคั่น แล้วเปลี่ยน List Adapter ให้ใช้ RecyclerView.ViewHolder เป็นประเภทตัวยึดมุมมอง เพื่อให้สามารถจัดการตัวยึดมุมมองได้มากกว่า 1 ประเภท หรือจะกำหนดคลาสพื้นฐานทั่วไปที่ทั้งคลาสที่ยึดตามรายการและคลาสที่ยึดตามตัวคั่นขยายก็ได้

นอกจากนี้ คุณยังต้องทำการเปลี่ยนแปลงต่อไปนี้กับตัวดัดแปลงรายการด้วย

  • เพิ่มเคสไปยังเมธอด 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);
  }
}

หลีกเลี่ยงการทำงานซ้ำ

ปัญหาสำคัญที่ควรหลีกเลี่ยงคือการให้แอปทำงานที่ไม่จำเป็น การดึงข้อมูลเป็น การดำเนินการที่ใช้ทรัพยากรมาก และการเปลี่ยนรูปแบบข้อมูลก็อาจใช้เวลาอันมีค่าของคุณด้วย เมื่อโหลดและเตรียมข้อมูลเพื่อแสดงใน UI แล้ว คุณควรบันทึกข้อมูลไว้ ในกรณีที่เกิดการเปลี่ยนแปลงการกำหนดค่าและต้องสร้าง UI ใหม่

การดำเนินการ 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 ได้ที่แหล่งข้อมูลเพิ่มเติมต่อไปนี้

Codelabs