จัดการและแสดงสถานะการโหลด (มุมมอง)

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

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

ระบบจะให้LoadStateสัญญาณแยกต่างหากสำหรับแต่ละLoadTypeและประเภทแหล่งข้อมูล (ไม่ว่าจะเป็น PagingSource หรือ RemoteMediator) ออบเจ็กต์ CombinedLoadStates ที่ Listener ระบุจะให้ข้อมูลเกี่ยวกับสถานะการโหลด จากสัญญาณทั้งหมดเหล่านี้ คุณสามารถใช้ข้อมูลโดยละเอียดนี้เพื่อแสดงตัวบ่งชี้การโหลดที่เหมาะสมต่อผู้ใช้

สถานะการโหลด

ไลบรารีการแบ่งหน้าจะแสดงสถานะการโหลดเพื่อใช้ใน UI ผ่านออบเจ็กต์ LoadState LoadState จะมีรูปแบบใดรูปแบบหนึ่งใน 3 รูปแบบต่อไปนี้ ขึ้นอยู่กับ สถานะการโหลดปัจจุบัน

  • หากไม่มีการดำเนินการโหลดที่ใช้งานอยู่และไม่มีข้อผิดพลาด LoadState จะเป็นออบเจ็กต์ LoadState.NotLoading คลาสย่อยนี้ยังมีพร็อพเพอร์ตี้ endOfPaginationReached ซึ่งระบุว่าถึงจุดสิ้นสุดของการแบ่งหน้าแล้วหรือไม่
  • หากมีการดำเนินการโหลดที่ใช้งานอยู่ LoadState จะเป็นออบเจ็กต์ LoadState.Loading
  • หากมีข้อผิดพลาด LoadState จะเป็นออบเจ็กต์ LoadState.Error

คุณใช้ LoadState ใน UI ได้ 2 วิธี ได้แก่ การใช้ Listener หรือการใช้อะแดปเตอร์รายการพิเศษเพื่อแสดงสถานะการโหลดโดยตรงในรายการ RecyclerView

เข้าถึงสถานะการโหลดด้วย Listener

หากต้องการรับสถานะการโหลดเพื่อใช้ทั่วไปใน UI ให้ใช้สตรีม loadStateFlow หรือเมธอด addLoadStateListener() ที่ PagingDataAdapter ให้ไว้ กลไกเหล่านี้ให้สิทธิ์เข้าถึงออบเจ็กต์ CombinedLoadStates ซึ่งมีข้อมูลเกี่ยวกับลักษณะการทำงานของ LoadState สำหรับประเภทการโหลดแต่ละประเภท

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

Kotlin

// Activities can use lifecycleScope directly, but Fragments should instead use
// viewLifecycleOwner.lifecycleScope.
lifecycleScope.launch {
  pagingAdapter.loadStateFlow.collectLatest { loadStates ->
    progressBar.isVisible = loadStates.refresh is LoadState.Loading
    retry.isVisible = loadState.refresh !is LoadState.Loading
    errorMsg.isVisible = loadState.refresh is LoadState.Error
  }
}

Java

pagingAdapter.addLoadStateListener(loadStates -> {
  progressBar.setVisibility(loadStates.refresh instanceof LoadState.Loading
    ? View.VISIBLE : View.GONE);
  retry.setVisibility(loadStates.refresh instanceof LoadState.Loading
    ? View.GONE : View.VISIBLE);
  errorMsg.setVisibility(loadStates.refresh instanceof LoadState.Error
    ? View.VISIBLE : View.GONE);
});

Java

pagingAdapter.addLoadStateListener(loadStates -> {
  progressBar.setVisibility(loadStates.refresh instanceof LoadState.Loading
    ? View.VISIBLE : View.GONE);
  retry.setVisibility(loadStates.refresh instanceof LoadState.Loading
    ? View.GONE : View.VISIBLE);
  errorMsg.setVisibility(loadStates.refresh instanceof LoadState.Error
    ? View.VISIBLE : View.GONE);
});

ดูข้อมูลเพิ่มเติมเกี่ยวกับ CombinedLoadStates ได้ที่เข้าถึงข้อมูลสถานะการโหลดเพิ่มเติม

แสดงสถานะการโหลดด้วยอะแดปเตอร์

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

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

Kotlin

class LoadStateViewHolder(
  parent: ViewGroup,
  retry: () -> Unit
) : RecyclerView.ViewHolder(
  LayoutInflater.from(parent.context)
    .inflate(R.layout.load_state_item, parent, false)
) {
  private val binding = LoadStateItemBinding.bind(itemView)
  private val progressBar: ProgressBar = binding.progressBar
  private val errorMsg: TextView = binding.errorMsg
  private val retry: Button = binding.retryButton
    .also {
      it.setOnClickListener { retry() }
    }

  fun bind(loadState: LoadState) {
    if (loadState is LoadState.Error) {
      errorMsg.text = loadState.error.localizedMessage
    }

    progressBar.isVisible = loadState is LoadState.Loading
    retry.isVisible = loadState is LoadState.Error
    errorMsg.isVisible = loadState is LoadState.Error
  }
}

Java

class LoadStateViewHolder extends RecyclerView.ViewHolder {
  private ProgressBar mProgressBar;
  private TextView mErrorMsg;
  private Button mRetry;

  LoadStateViewHolder(
    @NonNull ViewGroup parent,
    @NonNull View.OnClickListener retryCallback) {
    super(LayoutInflater.from(parent.getContext())
      .inflate(R.layout.load_state_item, parent, false));

    LoadStateItemBinding binding = LoadStateItemBinding.bind(itemView);
    mProgressBar = binding.progressBar;
    mErrorMsg = binding.errorMsg;
    mRetry = binding.retryButton;
  }

  public void bind(LoadState loadState) {
    if (loadState instanceof LoadState.Error) {
      LoadState.Error loadStateError = (LoadState.Error) loadState;
      mErrorMsg.setText(loadStateError.getError().getLocalizedMessage());
    }
    mProgressBar.setVisibility(loadState instanceof LoadState.Loading
      ? View.VISIBLE : View.GONE);
    mRetry.setVisibility(loadState instanceof LoadState.Error
      ? View.VISIBLE : View.GONE);
    mErrorMsg.setVisibility(loadState instanceof LoadState.Error
      ? View.VISIBLE : View.GONE);
  }
}

Java

class LoadStateViewHolder extends RecyclerView.ViewHolder {
  private ProgressBar mProgressBar;
  private TextView mErrorMsg;
  private Button mRetry;

  LoadStateViewHolder(
    @NonNull ViewGroup parent,
    @NonNull View.OnClickListener retryCallback) {
    super(LayoutInflater.from(parent.getContext())
      .inflate(R.layout.load_state_item, parent, false));

    LoadStateItemBinding binding = LoadStateItemBinding.bind(itemView);
    mProgressBar = binding.progressBar;
    mErrorMsg = binding.errorMsg;
    mRetry = binding.retryButton;
  }

  public void bind(LoadState loadState) {
    if (loadState instanceof LoadState.Error) {
      LoadState.Error loadStateError = (LoadState.Error) loadState;
      mErrorMsg.setText(loadStateError.getError().getLocalizedMessage());
    }
    mProgressBar.setVisibility(loadState instanceof LoadState.Loading
      ? View.VISIBLE : View.GONE);
    mRetry.setVisibility(loadState instanceof LoadState.Error
      ? View.VISIBLE : View.GONE);
    mErrorMsg.setVisibility(loadState instanceof LoadState.Error
      ? View.VISIBLE : View.GONE);
  }
}

จากนั้นสร้างคลาสที่ใช้ LoadStateAdapter และกำหนดเมธอด onCreateViewHolder() และ onBindViewHolder() เมธอดเหล่านี้จะสร้างอินสแตนซ์ของตัวยึดมุมมองที่กำหนดเองและเชื่อมโยง สถานะการโหลดที่เกี่ยวข้อง

Kotlin

// Adapter that displays a loading spinner when
// state is LoadState.Loading, and an error message and retry
// button when state is LoadState.Error.
class ExampleLoadStateAdapter(
  private val retry: () -> Unit
) : LoadStateAdapter<LoadStateViewHolder>() {

  override fun onCreateViewHolder(
    parent: ViewGroup,
    loadState: LoadState
  ) = LoadStateViewHolder(parent, retry)

  override fun onBindViewHolder(
    holder: LoadStateViewHolder,
    loadState: LoadState
  ) = holder.bind(loadState)
}

Java

// Adapter that displays a loading spinner when
// state is LoadState.Loading, and an error message and retry
// button when state is LoadState.Error.
class ExampleLoadStateAdapter extends LoadStateAdapter<LoadStateViewHolder> {
  private View.OnClickListener mRetryCallback;

  ExampleLoadStateAdapter(View.OnClickListener retryCallback) {
    mRetryCallback = retryCallback;
  }

  @NotNull
  @Override
  public LoadStateViewHolder onCreateViewHolder(@NotNull ViewGroup parent,
    @NotNull LoadState loadState) {
    return new LoadStateViewHolder(parent, mRetryCallback);
  }

  @Override
  public void onBindViewHolder(@NotNull LoadStateViewHolder holder,
    @NotNull LoadState loadState) {
    holder.bind(loadState);
  }
}

Java

// Adapter that displays a loading spinner when
// state is LoadState.Loading, and an error message and retry
// button when state is LoadState.Error.
class ExampleLoadStateAdapter extends LoadStateAdapter<LoadStateViewHolder> {
  private View.OnClickListener mRetryCallback;

  ExampleLoadStateAdapter(View.OnClickListener retryCallback) {
    mRetryCallback = retryCallback;
  }

  @NotNull
  @Override
  public LoadStateViewHolder onCreateViewHolder(@NotNull ViewGroup parent,
    @NotNull LoadState loadState) {
    return new LoadStateViewHolder(parent, mRetryCallback);
  }

  @Override
  public void onBindViewHolder(@NotNull LoadStateViewHolder holder,
    @NotNull LoadState loadState) {
    holder.bind(loadState);
  }
}

หากต้องการแสดงความคืบหน้าในการโหลดในส่วนหัวและส่วนท้าย ให้เรียกใช้เมธอด withLoadStateHeaderAndFooter() จากออบเจ็กต์ PagingDataAdapter

Kotlin

pagingAdapter
  .withLoadStateHeaderAndFooter(
    header = ExampleLoadStateAdapter(adapter::retry),
    footer = ExampleLoadStateAdapter(adapter::retry)
  )

Java

pagingAdapter
  .withLoadStateHeaderAndFooter(
    new ExampleLoadStateAdapter(pagingAdapter::retry),
    new ExampleLoadStateAdapter(pagingAdapter::retry));

Java

pagingAdapter
  .withLoadStateHeaderAndFooter(
    new ExampleLoadStateAdapter(pagingAdapter::retry),
    new ExampleLoadStateAdapter(pagingAdapter::retry));

คุณสามารถเรียกใช้ withLoadStateHeader() หรือ withLoadStateFooter() แทนได้หากต้องการให้รายการ RecyclerView แสดงสถานะการโหลดเฉพาะใน ส่วนหัวหรือส่วนท้ายเท่านั้น

เข้าถึงข้อมูลสถานะการโหลดเพิ่มเติม

ออบเจ็กต์ CombinedLoadStates จาก PagingDataAdapter ให้ข้อมูลเกี่ยวกับ สถานะการโหลดสำหรับการติดตั้งใช้งาน PagingSource และการติดตั้งใช้งาน RemoteMediator หากมี

เพื่อความสะดวก คุณสามารถใช้พร็อพเพอร์ตี้ refresh append และ prepend จาก CombinedLoadStates เพื่อเข้าถึงออบเจ็กต์ LoadState สำหรับ ประเภทการโหลดที่เหมาะสม โดยทั่วไปแล้วพร็อพเพอร์ตี้เหล่านี้จะอ้างอิงสถานะการโหลดจาก การติดตั้งใช้งาน RemoteMediator หากมี ไม่เช่นนั้นพร็อพเพอร์ตี้จะมี สถานะการโหลดที่เหมาะสมจากการติดตั้งใช้งาน PagingSource ดูข้อมูลโดยละเอียดเพิ่มเติมเกี่ยวกับตรรกะพื้นฐานได้ในเอกสารอ้างอิงสำหรับ CombinedLoadStates

Kotlin

lifecycleScope.launch {
  pagingAdapter.loadStateFlow.collectLatest { loadStates ->
    // Observe refresh load state from RemoteMediator if present, or
    // from PagingSource otherwise.
    refreshLoadState: LoadState = loadStates.refresh
    // Observe prepend load state from RemoteMediator if present, or
    // from PagingSource otherwise.
    prependLoadState: LoadState = loadStates.prepend
    // Observe append load state from RemoteMediator if present, or
    // from PagingSource otherwise.
    appendLoadState: LoadState = loadStates.append
  }
}

Java

pagingAdapter.addLoadStateListener(loadStates -> {
  // Observe refresh load state from RemoteMediator if present, or
  // from PagingSource otherwise.
  LoadState refreshLoadState = loadStates.refresh;
  // Observe prepend load state from RemoteMediator if present, or
  // from PagingSource otherwise.
  LoadState prependLoadState = loadStates.prepend;
  // Observe append load state from RemoteMediator if present, or
  // from PagingSource otherwise.
  LoadState appendLoadState = loadStates.append;
});

Java

pagingAdapter.addLoadStateListener(loadStates -> {
  // Observe refresh load state from RemoteMediator if present, or
  // from PagingSource otherwise.
  LoadState refreshLoadState = loadStates.refresh;
  // Observe prepend load state from RemoteMediator if present, or
  // from PagingSource otherwise.
  LoadState prependLoadState = loadStates.prepend;
  // Observe append load state from RemoteMediator if present, or
  // from PagingSource otherwise.
  LoadState appendLoadState = loadStates.append;
});

อย่างไรก็ตาม โปรดทราบว่าเฉพาะPagingSourceสถานะการโหลดเท่านั้นที่รับประกันว่าจะซิงโครไนซ์กับการอัปเดต UI เนื่องจากพร็อพเพอร์ตี้ refresh, append และ prepend อาจรับสถานะการโหลดจาก PagingSource หรือ RemoteMediator จึงไม่รับประกันว่าพร็อพเพอร์ตี้เหล่านี้จะซิงค์กับการอัปเดต UI ซึ่งอาจทำให้เกิดปัญหาเกี่ยวกับ UI ที่การโหลดดูเหมือนจะเสร็จสิ้นก่อนที่จะมีการเพิ่มข้อมูลใหม่ลงใน UI

ด้วยเหตุนี้ ตัวช่วยอำนวยความสะดวกจึงเหมาะสำหรับการแสดงสถานะการโหลดในส่วนหัวหรือส่วนท้าย แต่สำหรับกรณีการใช้งานอื่นๆ คุณอาจต้องเข้าถึงสถานะการโหลดจาก PagingSource หรือ RemoteMediator โดยเฉพาะ CombinedLoadStates มีพร็อพเพอร์ตี้ source และ mediator เพื่อวัตถุประสงค์นี้ พร็อพเพอร์ตี้เหล่านี้แต่ละรายการจะแสดงออบเจ็กต์ LoadStates ที่มีออบเจ็กต์ LoadState สำหรับ PagingSource หรือ RemoteMediator ตามลำดับ

Kotlin

lifecycleScope.launch {
  pagingAdapter.loadStateFlow.collectLatest { loadStates ->
    // Directly access the RemoteMediator refresh load state.
    mediatorRefreshLoadState: LoadState? = loadStates.mediator.refresh
    // Directly access the RemoteMediator append load state.
    mediatorAppendLoadState: LoadState? = loadStates.mediator.append
    // Directly access the RemoteMediator prepend load state.
    mediatorPrependLoadState: LoadState? = loadStates.mediator.prepend
    // Directly access the PagingSource refresh load state.
    sourceRefreshLoadState: LoadState = loadStates.source.refresh
    // Directly access the PagingSource append load state.
    sourceAppendLoadState: LoadState = loadStates.source.append
    // Directly access the PagingSource prepend load state.
    sourcePrependLoadState: LoadState = loadStates.source.prepend
  }
}

Java

pagingAdapter.addLoadStateListener(loadStates -> {
  // Directly access the RemoteMediator refresh load state.
  LoadState mediatorRefreshLoadState = loadStates.mediator.refresh;
  // Directly access the RemoteMediator append load state.
  LoadState mediatorAppendLoadState = loadStates.mediator.append;
  // Directly access the RemoteMediator prepend load state.
  LoadState mediatorPrependLoadState = loadStates.mediator.prepend;
  // Directly access the PagingSource refresh load state.
  LoadState sourceRefreshLoadState = loadStates.source.refresh;
  // Directly access the PagingSource append load state.
  LoadState sourceAppendLoadState = loadStates.source.append;
  // Directly access the PagingSource prepend load state.
  LoadState sourcePrependLoadState = loadStates.source.prepend;
});

Java

pagingAdapter.addLoadStateListener(loadStates -> {
  // Directly access the RemoteMediator refresh load state.
  LoadState mediatorRefreshLoadState = loadStates.mediator.refresh;
  // Directly access the RemoteMediator append load state.
  LoadState mediatorAppendLoadState = loadStates.mediator.append;
  // Directly access the RemoteMediator prepend load state.
  LoadState mediatorPrependLoadState = loadStates.mediator.prepend;
  // Directly access the PagingSource refresh load state.
  LoadState sourceRefreshLoadState = loadStates.source.refresh;
  // Directly access the PagingSource append load state.
  LoadState sourceAppendLoadState = loadStates.source.append;
  // Directly access the PagingSource prepend load state.
  LoadState sourcePrependLoadState = loadStates.source.prepend;
});

โอเปอเรเตอร์ Chain ใน LoadState

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

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

Kotlin

lifecycleScope.launchWhenCreated {
  adapter.loadStateFlow
    // Only emit when REFRESH LoadState for RemoteMediator changes.
    .distinctUntilChangedBy { it.refresh }
    // Only react to cases where REFRESH completes, such as NotLoading.
    .filter { it.refresh is LoadState.NotLoading }
    // Scroll to top is synchronous with UI updates, even if remote load was
    // triggered.
    .collect { binding.list.scrollToPosition(0) }
}

Java

PublishSubject<CombinedLoadStates> subject = PublishSubject.create();
Disposable disposable =
  subject.distinctUntilChanged(CombinedLoadStates::getRefresh)
  .filter(
    combinedLoadStates -> combinedLoadStates.getRefresh() instanceof LoadState.NotLoading)
  .subscribe(combinedLoadStates -> binding.list.scrollToPosition(0));

pagingAdapter.addLoadStateListener(loadStates -> {
  subject.onNext(loadStates);
});

Java

LiveData<CombinedLoadStates> liveData = new MutableLiveData<>();
LiveData<LoadState> refreshLiveData =
  Transformations.map(liveData, CombinedLoadStates::getRefresh);
LiveData<LoadState> distinctLiveData =
  Transformations.distinctUntilChanged(refreshLiveData);

distinctLiveData.observeForever(loadState -> {
  if (loadState instanceof LoadState.NotLoading) {
    binding.list.scrollToPosition(0);
  }
});

ตัวอย่างนี้จะรอจนกว่าสถานะการโหลดการรีเฟรชจะได้รับการอัปเดต แต่จะทริกเกอร์ก็ต่อเมื่อสถานะเป็น NotLoading เท่านั้น ซึ่งจะช่วยให้มั่นใจได้ว่าการรีเฟรชจากระยะไกลจะเสร็จสมบูรณ์ ก่อนที่จะมีการอัปเดต UI

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