পৃষ্ঠাভুক্ত ডেটা (ভিউ) লোড এবং প্রদর্শন করুন

ধারণা এবং জেটপ্যাক কম্পোজ বাস্তবায়ন

পেজিং লাইব্রেরি একটি বৃহত্তর ডেটাসেট থেকে পেজ করা ডেটা লোড এবং প্রদর্শন করার জন্য শক্তিশালী সক্ষমতা প্রদান করে। এই নির্দেশিকাটিতে দেখানো হয়েছে কিভাবে পেজিং লাইব্রেরি ব্যবহার করে একটি নেটওয়ার্ক ডেটা সোর্স থেকে পেজ করা ডেটার একটি স্ট্রিম সেট আপ করতে হয় এবং সেটিকে একটি RecyclerView তে প্রদর্শন করতে হয়।

একটি ডেটা উৎস সংজ্ঞায়িত করুন

প্রথম ধাপ হলো ডেটা সোর্স শনাক্ত করার জন্য একটি PagingSource ইমপ্লিমেন্টেশন সংজ্ঞায়িত করা। PagingSource API ক্লাসে load মেথডটি অন্তর্ভুক্ত থাকে, যা আপনি সংশ্লিষ্ট ডেটা সোর্স থেকে পেজ করা ডেটা কীভাবে পুনরুদ্ধার করবেন তা নির্দেশ করার জন্য ওভাররাইড করতে পারেন।

অ্যাসিঙ্ক লোডিংয়ের জন্য কোটলিন কোরাউটিন ব্যবহার করতে সরাসরি PagingSource ক্লাসটি ব্যবহার করুন। Paging লাইব্রেরিটি অন্যান্য অ্যাসিঙ্ক ফ্রেমওয়ার্ক সমর্থন করার জন্য ক্লাসও সরবরাহ করে:

  • RxJava ব্যবহার করতে হলে, এর পরিবর্তে RxPagingSource প্রয়োগ করুন।
  • Guava থেকে ListenableFuture ব্যবহার করতে হলে, এর পরিবর্তে ListenableFuturePagingSource ইমপ্লিমেন্ট করুন।

কী এবং ভ্যালু টাইপ নির্বাচন করুন

PagingSource<Key, Value> দুটি টাইপ প্যারামিটার আছে: Key এবং Value । Key প্যারামিটারটি ডেটা লোড করার জন্য ব্যবহৃত আইডেন্টিফায়ারকে সংজ্ঞায়িত করে এবং Value হলো ডেটার নিজস্ব টাইপ। উদাহরণস্বরূপ, যদি আপনি Retrofit-Int পেজ নম্বর পাস করে নেটওয়ার্ক থেকে User অবজেক্টের পেজ লোড করেন, তাহলে Key টাইপ হিসেবে Int এবং Value টাইপ হিসেবে User নির্বাচন করুন।

পেজিং উৎস সংজ্ঞায়িত করুন

নিম্নলিখিত উদাহরণটি একটি PagingSource বাস্তবায়ন করে যা পৃষ্ঠা নম্বর অনুসারে আইটেমের পৃষ্ঠাগুলি লোড করে। Key টাইপটি হল Int এবং Value টাইপটি হল User

জাভা (আরএক্সজাভা)

class ExamplePagingSource extends RxPagingSource<Integer, User> {
  @NonNull
  private ExampleBackendService mBackend;
  @NonNull
  private String mQuery;

  ExamplePagingSource(@NonNull ExampleBackendService backend,
    @NonNull String query) {
    mBackend = backend;
    mQuery = query;
  }

  @NotNull
  @Override
  public Single<LoadResult<Integer, User>> loadSingle(
    @NotNull LoadParams<Integer> params) {
    // Start refresh at page 1 if undefined.
    Integer nextPageNumber = params.getKey();
    if (nextPageNumber == null) {
      nextPageNumber = 1;
    }

    return mBackend.searchUsers(mQuery, nextPageNumber)
      .subscribeOn(Schedulers.io())
      .map(this::toLoadResult)
      .onErrorReturn(LoadResult.Error::new);
  }

  private LoadResult<Integer, User> toLoadResult(
    @NonNull SearchUserResponse response) {
    return new LoadResult.Page<>(
      response.getUsers(),
      null, // Only paging forward.
      response.getNextPageNumber(),
      LoadResult.Page.COUNT_UNDEFINED,
      LoadResult.Page.COUNT_UNDEFINED);
  }

  @Nullable
  @Override
  public Integer getRefreshKey(@NotNull PagingState<Integer, User> state) {
    // Try to find the page key of the closest page to anchorPosition from
    // either the prevKey or the nextKey; you need to handle nullability
    // here.
    //  * prevKey == null -> anchorPage is the first page.
    //  * nextKey == null -> anchorPage is the last page.
    //  * both prevKey and nextKey are null -> anchorPage is the
    //    initial page, so return null.
    Integer anchorPosition = state.getAnchorPosition();
    if (anchorPosition == null) {
      return null;
    }

    LoadResult.Page<Integer, User> anchorPage = state.closestPageToPosition(anchorPosition);
    if (anchorPage == null) {
      return null;
    }

    Integer prevKey = anchorPage.getPrevKey();
    if (prevKey != null) {
      return prevKey + 1;
    }

    Integer nextKey = anchorPage.getNextKey();
    if (nextKey != null) {
      return nextKey - 1;
    }

    return null;
  }
}

জাভা (গুয়াভা/লাইভডেটা)

class ExamplePagingSource extends ListenableFuturePagingSource<Integer, User> {
  @NonNull
  private ExampleBackendService mBackend;
  @NonNull
  private String mQuery;
  @NonNull
  private Executor mBgExecutor;

  ExamplePagingSource(
    @NonNull ExampleBackendService backend,
    @NonNull String query, @NonNull Executor bgExecutor) {
    mBackend = backend;
    mQuery = query;
    mBgExecutor = bgExecutor;
  }

  @NotNull
  @Override
  public ListenableFuture<LoadResult<Integer, User>> loadFuture(@NotNull LoadParams<Integer> params) {
    // Start refresh at page 1 if undefined.
    Integer nextPageNumber = params.getKey();
    if (nextPageNumber == null) {
      nextPageNumber = 1;
    }

    ListenableFuture<LoadResult<Integer, User>> pageFuture =
      Futures.transform(mBackend.searchUsers(mQuery, nextPageNumber),
      this::toLoadResult, mBgExecutor);

    ListenableFuture<LoadResult<Integer, User>> partialLoadResultFuture =
      Futures.catching(pageFuture, HttpException.class,
      LoadResult.Error::new, mBgExecutor);

    return Futures.catching(partialLoadResultFuture,
      IOException.class, LoadResult.Error::new, mBgExecutor);
  }

  private LoadResult<Integer, User> toLoadResult(@NonNull SearchUserResponse response) {
    return new LoadResult.Page<>(response.getUsers(),
    null, // Only paging forward.
    response.getNextPageNumber(),
    LoadResult.Page.COUNT_UNDEFINED,
    LoadResult.Page.COUNT_UNDEFINED);
  }

  @Nullable
  @Override
  public Integer getRefreshKey(@NotNull PagingState<Integer, User> state) {
    // Try to find the page key of the closest page to anchorPosition from
    // either the prevKey or the nextKey; you need to handle nullability
    // here.
    //  * prevKey == null -> anchorPage is the first page.
    //  * nextKey == null -> anchorPage is the last page.
    //  * both prevKey and nextKey are null -> anchorPage is the
    //    initial page, so return null.
    Integer anchorPosition = state.getAnchorPosition();
    if (anchorPosition == null) {
      return null;
    }

    LoadResult.Page<Integer, User> anchorPage = state.closestPageToPosition(anchorPosition);
    if (anchorPage == null) {
      return null;
    }

    Integer prevKey = anchorPage.getPrevKey();
    if (prevKey != null) {
      return prevKey + 1;
    }

    Integer nextKey = anchorPage.getNextKey();
    if (nextKey != null) {
      return nextKey - 1;
    }

    return null;
  }
}

একটি সাধারণ PagingSource ইমপ্লিমেন্টেশন কোনো কোয়েরির জন্য উপযুক্ত ডেটা লোড করতে এর কনস্ট্রাক্টরে প্রদত্ত প্যারামিটারগুলোকে load মেথডে পাস করে। উপরের উদাহরণে, সেই প্যারামিটারগুলো হলো:

  • backend : ব্যাকএন্ড সার্ভিসের একটি ইনস্ট্যান্স যা ডেটা সরবরাহ করে।
  • query : backend দ্বারা নির্দেশিত সার্ভিসে পাঠানোর জন্য সার্চ কোয়েরি।

LoadParams অবজেক্টটিতে যে লোড অপারেশনটি করা হবে সে সম্পর্কিত তথ্য থাকে। এর মধ্যে অন্তর্ভুক্ত রয়েছে যে কী-টি লোড করতে হবে এবং আইটেমের সংখ্যা।

LoadResult অবজেক্টটিতে লোড অপারেশনের ফলাফল থাকে। LoadResult একটি সিলড ক্লাস যা load কলটি সফল হয়েছে কিনা তার উপর নির্ভর করে দুটি রূপের মধ্যে একটি গ্রহণ করে:

  • লোড সফল হলে, একটি LoadResult.Page অবজেক্ট রিটার্ন করুন।
  • লোড সফল না হলে, একটি LoadResult.Error অবজেক্ট রিটার্ন করুন।

নিম্নলিখিত চিত্রটি দেখায় যে কীভাবে এই উদাহরণে load ফাংশনটি প্রতিটি লোডের জন্য কী গ্রহণ করে এবং পরবর্তী লোডের জন্য কী সরবরাহ করে।

প্রতিটি লোড কলে, ExamplePagingSource বর্তমান কী-টি গ্রহণ করে এবং লোড করার জন্য পরবর্তী কী-টি ফেরত দেয়।
চিত্র ১. load কীভাবে কী ব্যবহার ও আপডেট করে, তা দেখানো ডায়াগ্রাম।

PagingSource ইমপ্লিমেন্টেশনকে অবশ্যই একটি getRefreshKey মেথডও ইমপ্লিমেন্ট করতে হবে, যা প্যারামিটার হিসেবে একটি PagingState অবজেক্ট গ্রহণ করে। প্রাথমিক লোডের পর ডেটা রিফ্রেশ বা ইনভ্যালিডেট করা হলে, এটি load মেথডে পাস করার জন্য কী (key) রিটার্ন করে। Paging লাইব্রেরি পরবর্তী ডেটা রিফ্রেশের সময় স্বয়ংক্রিয়ভাবে এই মেথডটি কল করে।

ত্রুটিগুলি পরিচালনা করুন

বিভিন্ন কারণে ডেটা লোড করার অনুরোধ ব্যর্থ হতে পারে, বিশেষ করে নেটওয়ার্কের মাধ্যমে লোড করার সময়। লোড করার সময় যে ত্রুটিগুলো দেখা দেয়, তা রিপোর্ট করতে load মেথড থেকে একটি LoadResult.Error অবজেক্ট রিটার্ন করুন।

উদাহরণস্বরূপ, পূর্ববর্তী উদাহরণের ExamplePagingSource এর load মেথডে নিম্নলিখিতটি যোগ করে আপনি লোডিং ত্রুটিগুলি ধরতে এবং রিপোর্ট করতে পারেন:

জাভা (আরএক্সজাভা)

return backend.searchUsers(searchTerm, nextPageNumber)
  .subscribeOn(Schedulers.io())
  .map(this::toLoadResult)
  .onErrorReturn(LoadResult.Error::new);

জাভা (গুয়াভা/লাইভডেটা)

ListenableFuture<LoadResult<Integer, User>> pageFuture = Futures.transform(
  backend.searchUsers(query, nextPageNumber), this::toLoadResult,
  bgExecutor);

ListenableFuture<LoadResult<Integer, User>> partialLoadResultFuture = Futures.catching(
  pageFuture, HttpException.class, LoadResult.Error::new,
  bgExecutor);

return Futures.catching(partialLoadResultFuture,
  IOException.class, LoadResult.Error::new, bgExecutor);

Retrofit ত্রুটিগুলি পরিচালনা করার বিষয়ে আরও তথ্যের জন্য, PagingSource API রেফারেন্সে থাকা নমুনাগুলি দেখুন।

PagingSource LoadResult.Error অবজেক্টগুলো সংগ্রহ করে UI-তে পৌঁছে দেয়, যাতে আপনি সেগুলোর উপর ভিত্তি করে ব্যবস্থা নিতে পারেন। UI-তে লোডিং অবস্থা প্রকাশ করার বিষয়ে আরও তথ্যের জন্য, “লোডিং অবস্থা পরিচালনা ও উপস্থাপন” দেখুন।

পেজিং ডেটার একটি প্রবাহ সেট আপ করুন

এরপরে, আপনার PagingSource ইমপ্লিমেন্টেশন থেকে পেজ করা ডেটার একটি স্ট্রিম প্রয়োজন। আপনার ViewModel এ ডেটা স্ট্রিমটি সেট আপ করুন। Pager ক্লাসটি এমন কিছু মেথড সরবরাহ করে যা একটি PagingSource থেকে PagingData অবজেক্টের একটি রিঅ্যাক্টিভ স্ট্রিম প্রকাশ করে। Paging লাইব্রেরিটি Flow, LiveData এবং RxJava-এর FlowableObservable টাইপ সহ বিভিন্ন ধরণের স্ট্রিম ব্যবহার সমর্থন করে।

আপনার রিঅ্যাক্টিভ স্ট্রিম সেট আপ করার জন্য যখন আপনি একটি Pager ইনস্ট্যান্স তৈরি করেন, তখন আপনাকে অবশ্যই ইনস্ট্যান্সটিকে একটি PagingConfig কনফিগারেশন অবজেক্ট এবং এমন একটি ফাংশন প্রদান করতে হবে, যা Pager বলে দেবে কীভাবে আপনার PagingSource ইমপ্লিমেন্টেশনের একটি ইনস্ট্যান্স পেতে হবে:

জাভা (আরএক্সজাভা)

// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact.
CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel);
Pager<Integer, User> pager = Pager<>(
  new PagingConfig(/* pageSize = */ 20),
  () -> ExamplePagingSource(backend, query));

Flowable<PagingData<User>> flowable = PagingRx.getFlowable(pager);
PagingRx.cachedIn(flowable, viewModelScope);

জাভা (গুয়াভা/লাইভডেটা)

// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact.
CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel);
Pager<Integer, User> pager = Pager<>(
  new PagingConfig(/* pageSize = */ 20),
  () -> ExamplePagingSource(backend, query));

PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), viewModelScope);

cachedIn অপারেটরটি ডেটা স্ট্রিমকে শেয়ারযোগ্য করে তোলে এবং প্রদত্ত CoroutineScope ব্যবহার করে লোড করা ডেটা ক্যাশে করে রাখে। এই উদাহরণটিতে lifecycle-viewmodel-ktx আর্টিফ্যাক্ট দ্বারা প্রদত্ত ` viewModelScope ব্যবহার করা হয়েছে।

Pager অবজেক্টটি PagingSource অবজেক্টের load মেথডকে কল করে, এটিকে LoadParams অবজেক্টটি প্রদান করে এবং বিনিময়ে LoadResult অবজেক্টটি গ্রহণ করে।

একটি RecyclerView অ্যাডাপ্টার সংজ্ঞায়িত করুন

আপনার RecyclerView লিস্টে ডেটা গ্রহণ করার জন্য একটি অ্যাডাপ্টারও সেট আপ করতে হবে। এই উদ্দেশ্যে Paging লাইব্রেরি PagingDataAdapter ক্লাসটি প্রদান করে।

এমন একটি ক্লাস সংজ্ঞায়িত করুন যা PagingDataAdapter এক্সটেন্ড করে। এই উদাহরণে, UserAdapter ক্লাসটি PagingDataAdapter এক্সটেন্ড করে User টাইপের লিস্ট আইটেমগুলোর জন্য একটি RecyclerView অ্যাডাপ্টার প্রদান করে এবং UserViewHolder ভিউ হোল্ডার হিসেবে ব্যবহার করে।

কোটলিন (কোরুটিন)

class UserAdapter(diffCallback: DiffUtil.ItemCallback<User>) :
  PagingDataAdapter<User, UserViewHolder>(diffCallback) {
  override fun onCreateViewHolder(
    parent: ViewGroup,
    viewType: Int
  ): UserViewHolder {
    return UserViewHolder(parent)
  }

  override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
    val item = getItem(position)
    // Note that item can be null. ViewHolder must support binding a
    // null item as a placeholder.
    holder.bind(item)
  }
}

জাভা (আরএক্সজাভা)

class UserAdapter extends PagingDataAdapter<User, UserViewHolder> {
  UserAdapter(@NotNull DiffUtil.ItemCallback<User> diffCallback) {
    super(diffCallback);
  }

  @NonNull
  @Override
  public UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    return new UserViewHolder(parent);
  }

  @Override
  public void onBindViewHolder(@NonNull UserViewHolder holder, int position) {
    User item = getItem(position);
    // Note that item can be null. ViewHolder must support binding a
    // null item as a placeholder.
    holder.bind(item);
  }
}

জাভা (গুয়াভা/লাইভডেটা)

class UserAdapter extends PagingDataAdapter<User, UserViewHolder> {
  UserAdapter(@NotNull DiffUtil.ItemCallback<User> diffCallback) {
    super(diffCallback);
  }

  @NonNull
  @Override
  public UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    return new UserViewHolder(parent);
  }

  @Override
  public void onBindViewHolder(@NonNull UserViewHolder holder, int position) {
    User item = getItem(position);
    // Note that item can be null. ViewHolder must support binding a
    // null item as a placeholder.
    holder.bind(item);
  }
}

আপনার অ্যাডাপ্টারকে অবশ্যই onCreateViewHolder এবং onBindViewHolder মেথডগুলো সংজ্ঞায়িত করতে হবে এবং একটি DiffUtil.ItemCallback নির্দিষ্ট করতে হবে। এটি সাধারণত RecyclerView লিস্ট অ্যাডাপ্টার সংজ্ঞায়িত করার মতোই কাজ করে:

কোটলিন (কোরুটিন)

object UserComparator : DiffUtil.ItemCallback<User>() {
  override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
    // Id is unique.
    return oldItem.id == newItem.id
  }

  override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
    return oldItem == newItem
  }
}

জাভা (আরএক্সজাভা)

class UserComparator extends DiffUtil.ItemCallback<User> {
  @Override
  public boolean areItemsTheSame(@NonNull User oldItem,
    @NonNull User newItem) {
    // Id is unique.
    return oldItem.id.equals(newItem.id);
  }

  @Override
  public boolean areContentsTheSame(@NonNull User oldItem,
    @NonNull User newItem) {
    return oldItem.equals(newItem);
  }
}

জাভা (গুয়াভা/লাইভডেটা)

class UserComparator extends DiffUtil.ItemCallback<User> {
  @Override
  public boolean areItemsTheSame(@NonNull User oldItem,
    @NonNull User newItem) {
    // Id is unique.
    return oldItem.id.equals(newItem.id);
  }

  @Override
  public boolean areContentsTheSame(@NonNull User oldItem,
    @NonNull User newItem) {
    return oldItem.equals(newItem);
  }
}

আপনার UI-তে পৃষ্ঠাবদ্ধ ডেটা প্রদর্শন করুন।

এখন যেহেতু আপনি একটি PagingSource নির্ধারণ করেছেন, আপনার অ্যাপের জন্য PagingData এর একটি স্ট্রিম তৈরি করার উপায় বানিয়েছেন এবং একটি PagingDataAdapter সংজ্ঞায়িত করেছেন, আপনি এই উপাদানগুলোকে একসাথে সংযুক্ত করতে এবং আপনার অ্যাক্টিভিটিতে পেজ করা ডেটা প্রদর্শন করতে প্রস্তুত।

আপনার অ্যাক্টিভিটির onCreate অথবা ফ্র্যাগমেন্টের onViewCreated মেথডে নিম্নলিখিত ধাপগুলো অনুসরণ করুন:

  1. আপনার PagingDataAdapter ক্লাসের একটি ইনস্ট্যান্স তৈরি করুন।
  2. যে RecyclerView লিস্টে আপনি আপনার পেজ করা ডেটা প্রদর্শন করতে চান, সেখানে PagingDataAdapter ইনস্ট্যান্সটি পাস করুন।
  3. PagingData স্ট্রিমটি পর্যবেক্ষণ করুন এবং প্রতিটি জেনারেট হওয়া ভ্যালু আপনার অ্যাডাপ্টারের submitData() মেথডে পাস করুন।

কোটলিন (কোরুটিন)

val viewModel by viewModels<ExampleViewModel>()

val pagingAdapter = UserAdapter(UserComparator)
val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
recyclerView.adapter = pagingAdapter

// Activities can use lifecycleScope directly; fragments use
// viewLifecycleOwner.lifecycleScope.
lifecycleScope.launch {
  viewModel.flow.collectLatest { pagingData ->
    pagingAdapter.submitData(pagingData)
  }
}

জাভা (আরএক্সজাভা)

ExampleViewModel viewModel = new ViewModelProvider(this)
  .get(ExampleViewModel.class);

UserAdapter pagingAdapter = new UserAdapter(new UserComparator());
RecyclerView recyclerView = findViewById<RecyclerView>(
  R.id.recycler_view);
recyclerView.adapter = pagingAdapter

viewModel.flowable
  // Using AutoDispose to handle subscription lifecycle.
  // See: https://github.com/uber/AutoDispose.
  .to(autoDisposable(AndroidLifecycleScopeProvider.from(this)))
  .subscribe(pagingData -> pagingAdapter.submitData(lifecycle, pagingData));

জাভা (গুয়াভা/লাইভডেটা)

ExampleViewModel viewModel = new ViewModelProvider(this)
  .get(ExampleViewModel.class);

UserAdapter pagingAdapter = new UserAdapter(new UserComparator());
RecyclerView recyclerView = findViewById<RecyclerView>(
  R.id.recycler_view);
recyclerView.adapter = pagingAdapter

// Activities can use getLifecycle() directly; fragments use
// getViewLifecycleOwner().getLifecycle().
viewModel.liveData.observe(this, pagingData ->
  pagingAdapter.submitData(getLifecycle(), pagingData));

RecyclerView তালিকাটি এখন ডেটা সোর্স থেকে পৃষ্ঠাবদ্ধ ডেটা প্রদর্শন করে এবং প্রয়োজনে স্বয়ংক্রিয়ভাবে আরেকটি পৃষ্ঠা লোড করে।