নেটওয়ার্ক এবং ডাটাবেস থেকে পৃষ্ঠা

নেটওয়ার্ক সংযোগ অনির্ভরযোগ্য হলে বা ব্যবহারকারী অফলাইনে থাকলেও যেন আপনার অ্যাপটি ব্যবহার করা যায়, তা নিশ্চিত করে ব্যবহারকারীর অভিজ্ঞতা উন্নত করুন। এটি করার একটি উপায় হলো একই সাথে নেটওয়ার্ক এবং লোকাল ডেটাবেস থেকে পেজ করা। এইভাবে, আপনার অ্যাপটি লোকাল ডেটাবেস ক্যাশে থেকে ইউজার ইন্টারফেস (UI) পরিচালনা করে এবং ডেটাবেসে আর কোনো ডেটা না থাকলেই কেবল নেটওয়ার্কে অনুরোধ পাঠায়।

এই নির্দেশিকাটি ধরে নেয় যে আপনি Room persistence লাইব্রেরি এবং Paging লাইব্রেরির প্রাথমিক ব্যবহার সম্পর্কে পরিচিত।

সমন্বয় ডেটা লোড

পেজিং লাইব্রেরি এই ব্যবহারের জন্য RemoteMediator কম্পোনেন্টটি প্রদান করে। অ্যাপের ক্যাশ করা ডেটা শেষ হয়ে গেলে, পেজিং লাইব্রেরি থেকে RemoteMediator একটি সিগন্যাল হিসেবে কাজ করে। আপনি এই সিগন্যালটি ব্যবহার করে নেটওয়ার্ক থেকে অতিরিক্ত ডেটা লোড করে লোকাল ডেটাবেসে সংরক্ষণ করতে পারেন, যেখান থেকে একটি PagingSource তা লোড করে প্রদর্শনের জন্য UI-তে সরবরাহ করতে পারে।

যখন অতিরিক্ত ডেটার প্রয়োজন হয়, তখন পেজিং লাইব্রেরি RemoteMediator ইমপ্লিমেন্টেশন থেকে load() মেথডটিকে কল করে। এটি একটি সাসপেন্ডিং ফাংশন, তাই এখানে দীর্ঘ সময় ধরে চলা কাজ নিরাপদে করা যায়। এই ফাংশনটি সাধারণত কোনো নেটওয়ার্ক সোর্স থেকে নতুন ডেটা সংগ্রহ করে এবং সেটিকে লোকাল স্টোরেজে সংরক্ষণ করে।

এই প্রক্রিয়াটি নতুন ডেটা নিয়ে কাজ করে, কিন্তু সময়ের সাথে সাথে ডাটাবেসে সংরক্ষিত ডেটার বৈধতা বাতিল করার প্রয়োজন হয়, যেমন যখন ব্যবহারকারী নিজে থেকে রিফ্রেশ চালু করেন। এটি load() মেথডে পাস করা LoadType প্রপার্টির মাধ্যমে প্রকাশ করা হয়। ` LoadType RemoteMediator কে জানিয়ে দেয় যে, তাকে বিদ্যমান ডেটা রিফ্রেশ করতে হবে, নাকি বিদ্যমান তালিকার শুরুতে বা শেষে যোগ করার জন্য অতিরিক্ত ডেটা আনতে হবে।

এইভাবে, RemoteMediator নিশ্চিত করে যে আপনার অ্যাপ ব্যবহারকারীরা যা দেখতে চান সেই ডেটা সঠিক ক্রমে লোড করে।

পেজিং জীবনচক্র

নেটওয়ার্ক থেকে সরাসরি পেজিং করার সময়, PagingSource ডেটা লোড করে এবং একটি LoadResult অবজেক্ট রিটার্ন করে। PagingSource ইমপ্লিমেন্টেশনটি pagingSourceFactory প্যারামিটারের মাধ্যমে Pager এ পাস করা হয়।

UI-এর জন্য নতুন ডেটার প্রয়োজন হলে, Pager PagingSource থেকে load() মেথডটি কল করে এবং নতুন ডেটা ধারণকারী PagingData অবজেক্টের একটি স্ট্রিম রিটার্ন করে। প্রতিটি PagingData অবজেক্ট সাধারণত UI-তে প্রদর্শনের জন্য পাঠানোর আগে ViewModel এ ক্যাশ করা হয়।

চিত্র ১. PagingSource এবং RemoteMediator সহ পেজিং-এর জীবনচক্রের ডায়াগ্রাম।

RemoteMediator এই ডেটা প্রবাহ পরিবর্তন করে। একটি PagingSource এখনও ডেটা লোড করে; কিন্তু যখন পেজ করা ডেটা শেষ হয়ে যায়, তখন Paging লাইব্রেরি নেটওয়ার্ক সোর্স থেকে নতুন ডেটা লোড করার জন্য RemoteMediator কে ট্রিগার করে। RemoteMediator নতুন ডেটা লোকাল ডেটাবেসে সংরক্ষণ করে, তাই ViewModel এ একটি ইন-মেমরি ক্যাশের প্রয়োজন হয় না। সবশেষে, PagingSource নিজেকে ইনভ্যালিডেট করে, এবং Pager ডেটাবেস থেকে নতুন ডেটা লোড করার জন্য একটি নতুন ইনস্ট্যান্স তৈরি করে।

মৌলিক ব্যবহার

ধরুন, আপনি চান আপনার অ্যাপটি একটি আইটেম-ভিত্তিক নেটওয়ার্ক ডেটা সোর্স থেকে User আইটেমের পেজগুলো লোড করে একটি রুম ডেটাবেসে সংরক্ষিত লোকাল ক্যাশে রাখবে।

একটি RemoteMediator ইমপ্লিমেন্টেশন নেটওয়ার্ক থেকে পেজ করা ডেটা ডাটাবেসে লোড করতে সাহায্য করে, কিন্তু সরাসরি UI-তে ডেটা লোড করে না। এর পরিবর্তে, অ্যাপটি ডাটাবেসকে তথ্যের মূল উৎস হিসেবে ব্যবহার করে। অন্য কথায়, অ্যাপটি শুধুমাত্র সেই ডেটা প্রদর্শন করে যা ডাটাবেসে ক্যাশ করা হয়েছে। একটি PagingSource ইমপ্লিমেন্টেশন (উদাহরণস্বরূপ, Room দ্বারা জেনারেট করা একটি) ডাটাবেস থেকে ক্যাশ করা ডেটা UI-তে লোড করার কাজটি পরিচালনা করে।

রুম সত্তা তৈরি করুন

প্রথম ধাপ হলো Room পার্সিস্টেন্স লাইব্রেরি ব্যবহার করে একটি ডেটাবেস সংজ্ঞায়িত করা, যা নেটওয়ার্ক ডেটা সোর্স থেকে পেজ করা ডেটার একটি স্থানীয় ক্যাশে ধারণ করবে। “Save data in a local database using Room” অংশে বর্ণিত RoomDatabase এর একটি ইমপ্লিমেন্টেশন দিয়ে শুরু করুন।

এরপরে, "রুম এনটিটি ব্যবহার করে ডেটা সংজ্ঞায়িত করা" অংশে বর্ণিত পদ্ধতি অনুসারে, তালিকার আইটেমগুলোর একটি টেবিল উপস্থাপন করার জন্য একটি রুম এনটিটি সংজ্ঞায়িত করুন। এটিকে প্রাইমারি কী হিসেবে একটি id ফিল্ড দিন, এবং সেইসাথে আপনার তালিকার আইটেমগুলোতে থাকা অন্য যেকোনো তথ্যের জন্য ফিল্ড যুক্ত করুন।

@Entity(tableName = "users")
data class User(val id: String, val label: String)

“রুম ডিএও ব্যবহার করে ডেটা অ্যাক্সেস করা” অংশে বর্ণিত পদ্ধতি অনুসারে আপনাকে অবশ্যই এই রুম এনটিটির জন্য একটি ডেটা অ্যাক্সেস অবজেক্ট (ডিএও) সংজ্ঞায়িত করতে হবে। লিস্ট আইটেম এনটিটির ডিএও-তে নিম্নলিখিত মেথডগুলো অবশ্যই অন্তর্ভুক্ত থাকতে হবে:

  • একটি insertAll() মেথড যা টেবিলে আইটেমের একটি তালিকা সন্নিবেশ করে।
  • এমন একটি মেথড যা প্যারামিটার হিসেবে কোয়েরি স্ট্রিং গ্রহণ করে এবং ফলাফলের তালিকার জন্য একটি PagingSource অবজেক্ট রিটার্ন করে। এর ফলে, একটি Pager অবজেক্ট এই টেবিলটিকে পেজ করা ডেটার উৎস হিসেবে ব্যবহার করতে পারে।
  • একটি clearAll() মেথড যা টেবিলের সমস্ত ডেটা মুছে ফেলে।
@Dao
interface UserDao {
  @Insert(onConflict = OnConflictStrategy.REPLACE)
  suspend fun insertAll(users: List<User>)

  @Query("SELECT * FROM users WHERE label LIKE :query")
  fun pagingSource(query: String): PagingSource<Int, User>

  @Query("DELETE FROM users")
  suspend fun clearAll()
}

একটি রিমোটমিডিয়েটর প্রয়োগ করুন

RemoteMediator এর প্রধান কাজ হলো, যখন Pager ডেটা শেষ হয়ে যায় অথবা বিদ্যমান ডেটা অবৈধ হয়ে যায়, তখন নেটওয়ার্ক থেকে আরও ডেটা লোড করা। এতে একটি load() মেথড রয়েছে, যা লোডিং আচরণ নির্ধারণ করার জন্য আপনাকে অবশ্যই ওভাররাইড করতে হবে।

একটি সাধারণ RemoteMediator বাস্তবায়নে নিম্নলিখিত প্যারামিটারগুলো অন্তর্ভুক্ত থাকে:

  • query : একটি কোয়েরি স্ট্রিং যা নির্ধারণ করে ব্যাকএন্ড পরিষেবা থেকে কোন ডেটা পুনরুদ্ধার করতে হবে।
  • database : রুম ডাটাবেস যা লোকাল ক্যাশ হিসেবে কাজ করে।
  • networkService : ব্যাকএন্ড সার্ভিসের জন্য একটি এপিআই ইনস্ট্যান্স।

একটি RemoteMediator<Key, Value> ইমপ্লিমেন্টেশন তৈরি করুন। Key টাইপ এবং Value টাইপ ঠিক তেমনই হওয়া উচিত, যেমনটি একই নেটওয়ার্ক ডেটা সোর্সের বিপরীতে একটি PagingSource সংজ্ঞায়িত করার ক্ষেত্রে হয়ে থাকে। টাইপ প্যারামিটার নির্বাচন সম্পর্কে আরও তথ্যের জন্য, "Key এবং Value টাইপ নির্বাচন করুন" দেখুন।

@OptIn(ExperimentalPagingApi::class)
class ExampleRemoteMediator(
  private val query: String,
  private val database: RoomDb,
  private val networkService: ExampleBackendService
) : RemoteMediator<Int, User>() {
  val userDao = database.userDao()

  override suspend fun load(
    loadType: LoadType,
    state: PagingState<Int, User>
  ): MediatorResult {
    // ...
  }
}

` load() ` মেথডটি ব্যাকেন্ড ডেটাসেট আপডেট করা এবং PagingSource অবৈধ করার জন্য দায়ী। পেজিং সমর্থনকারী কিছু লাইব্রেরি (যেমন `Room`) তাদের দ্বারা বাস্তবায়িত PagingSource অবজেক্টগুলোকে স্বয়ংক্রিয়ভাবে অবৈধ করে দেয়।

` load() ` মেথডটি দুটি প্যারামিটার গ্রহণ করে:

  • PagingState , যাতে এখন পর্যন্ত লোড হওয়া পেজগুলো, সর্বশেষ অ্যাক্সেস করা ইনডেক্স এবং পেজিং স্ট্রিম শুরু করতে ব্যবহৃত PagingConfig অবজেক্টের তথ্য থাকে।
  • LoadType , যা লোডের ধরণ নির্দেশ করে: REFRESH , APPEND , বা PREPEND

load() ` মেথডের রিটার্ন ভ্যালু হলো একটি MediatorResult অবজেক্ট। MediatorResult হয় MediatorResult.Error (যেটিতে ত্রুটির বিবরণ থাকে) অথবা MediatorResult.Success (যেটিতে আরও ডেটা লোড করার আছে কি না, তা জানানোর জন্য একটি সংকেত থাকে)।

` load() ` মেথডটিকে অবশ্যই নিম্নলিখিত ধাপগুলো সম্পাদন করতে হবে:

  1. লোডের ধরন এবং এ পর্যন্ত লোড হওয়া ডেটার ওপর নির্ভর করে নেটওয়ার্ক থেকে কোন পেজটি লোড করতে হবে তা নির্ধারণ করুন।
  2. নেটওয়ার্ক অনুরোধটি চালু করুন।
  3. লোড অপারেশনের ফলাফলের উপর নির্ভর করে পদক্ষেপ গ্রহণ করুন:
    • যদি লোড সফল হয় এবং প্রাপ্ত আইটেমের তালিকাটি খালি না হয়, তাহলে তালিকার আইটেমগুলো ডেটাবেসে সংরক্ষণ করুন এবং MediatorResult.Success(endOfPaginationReached = false) রিটার্ন করুন। ডেটা সংরক্ষিত হওয়ার পরে, পেজিং লাইব্রেরিকে নতুন ডেটা সম্পর্কে অবহিত করতে ডেটা সোর্সটিকে অবৈধ করে দিন।
    • যদি লোড সফল হয় এবং প্রাপ্ত আইটেমের তালিকাটি খালি থাকে অথবা এটি সর্বশেষ পেজ ইনডেক্স হয়, তাহলে MediatorResult.Success(endOfPaginationReached = true) রিটার্ন করুন। ডেটা সংরক্ষিত হওয়ার পরে, পেজিং লাইব্রেরিকে নতুন ডেটা সম্পর্কে অবহিত করতে ডেটা সোর্সটিকে ইনভ্যালিডেট করুন।
    • অনুরোধটির কারণে কোনো ত্রুটি ঘটলে, MediatorResult.Error রিটার্ন করুন।
override suspend fun load(
  loadType: LoadType,
  state: PagingState<Int, User>
): MediatorResult {
  return try {
    // The network load method takes an optional after=<user.id>
    // parameter. For every page after the first, pass the last user
    // ID to let it continue from where it left off. For REFRESH,
    // pass null to load the first page.
    val loadKey = when (loadType) {
      LoadType.REFRESH -> null
      // In this example, you never need to prepend, since REFRESH
      // will always load the first page in the list. Immediately
      // return, reporting end of pagination.
      LoadType.PREPEND ->
        return MediatorResult.Success(endOfPaginationReached = true)
      LoadType.APPEND -> {
        val lastItem = state.lastItemOrNull()

        // You must explicitly check if the last item is null when
        // appending, since passing null to networkService is only
        // valid for initial load. If lastItem is null it means no
        // items were loaded after the initial REFRESH and there are
        // no more items to load.
        if (lastItem == null) {
          return MediatorResult.Success(
            endOfPaginationReached = true
          )
        }

        lastItem.id
      }
    }

    // Suspending network load via Retrofit. This doesn't need to be
    // wrapped in a withContext(Dispatcher.IO) { ... } block since
    // Retrofit's Coroutine CallAdapter dispatches on a worker
    // thread.
    val response = networkService.searchUsers(
      query = query, after = loadKey
    )

    database.withTransaction {
      if (loadType == LoadType.REFRESH) {
        userDao.deleteByQuery(query)
      }

      // Insert new users into database, which invalidates the
      // current PagingData, allowing Paging to present the updates
      // in the DB.
      userDao.insertAll(response.users)
    }

    MediatorResult.Success(
      endOfPaginationReached = response.nextKey == null
    )
  } catch (e: IOException) {
    MediatorResult.Error(e)
  } catch (e: HttpException) {
    MediatorResult.Error(e)
  }
}

initialize পদ্ধতিটি সংজ্ঞায়িত করুন

RemoteMediator ইমপ্লিমেন্টেশনগুলো initialize() মেথডটিকে ওভাররাইড করে ক্যাশ করা ডেটা পুরোনো হয়ে গেছে কিনা তা পরীক্ষা করতে পারে এবং রিমোট রিফ্রেশ ট্রিগার করা হবে কিনা সেই সিদ্ধান্ত নিতে পারে। এই মেথডটি যেকোনো লোডিং সম্পন্ন হওয়ার আগে রান করে, ফলে আপনি যেকোনো লোকাল বা রিমোট লোড ট্রিগার করার আগে ডাটাবেসকে ম্যানিপুলেট করতে পারেন (উদাহরণস্বরূপ, পুরোনো ডেটা মুছে ফেলার জন্য)।

যেহেতু initialize() একটি অ্যাসিঙ্ক্রোনাস ফাংশন, তাই ডাটাবেসে বিদ্যমান ডেটার প্রাসঙ্গিকতা নির্ধারণ করতে আপনি ডেটা লোড করতে পারেন। সবচেয়ে সাধারণ ক্ষেত্রে, ক্যাশ করা ডেটা শুধুমাত্র একটি নির্দিষ্ট সময়ের জন্য বৈধ থাকে। RemoteMediator পরীক্ষা করে দেখতে পারে যে এই মেয়াদ শেষ হওয়ার সময় পার হয়েছে কিনা, সেক্ষেত্রে Paging লাইব্রেরিকে ডেটা সম্পূর্ণরূপে রিফ্রেশ করতে হবে। initialize() এর ইমপ্লিমেন্টেশনগুলোতে নিম্নলিখিতভাবে একটি InitializeAction রিটার্ন করা উচিত:

  • যেসব ক্ষেত্রে স্থানীয় ডেটা সম্পূর্ণরূপে রিফ্রেশ করার প্রয়োজন হয়, initialize() InitializeAction.LAUNCH_INITIAL_REFRESH রিটার্ন করা উচিত। এর ফলে RemoteMediator ডেটা সম্পূর্ণরূপে পুনরায় লোড করার জন্য একটি রিমোট রিফ্রেশ সম্পাদন করে। যেকোনো রিমোট APPEND বা PREPEND লোড পরবর্তী ধাপে যাওয়ার আগে REFRESH লোডটি সফল হওয়ার জন্য অপেক্ষা করে।
  • যেসব ক্ষেত্রে স্থানীয় ডেটা রিফ্রেশ করার প্রয়োজন হয় না, initialize() InitializeAction.SKIP_INITIAL_REFRESH রিটার্ন করা উচিত। এর ফলে RemoteMediator রিমোট রিফ্রেশ এড়িয়ে যায় এবং ক্যাশ করা ডেটা লোড করে।
override suspend fun initialize(): InitializeAction {
  val cacheTimeout = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS)
  return if (System.currentTimeMillis() - db.lastUpdated() <= cacheTimeout)
  {
    // Cached data is up-to-date, so there is no need to re-fetch
    // from the network.
    InitializeAction.SKIP_INITIAL_REFRESH
  } else {
    // Need to refresh cached data from network; returning
    // LAUNCH_INITIAL_REFRESH here will also block RemoteMediator's
    // APPEND and PREPEND from running until REFRESH succeeds.
    InitializeAction.LAUNCH_INITIAL_REFRESH
  }
}

একটি পেজার তৈরি করুন

অবশেষে, পেজ করা ডেটার স্ট্রিম সেট আপ করার জন্য আপনাকে অবশ্যই একটি Pager ইনস্ট্যান্স তৈরি করতে হবে। এটি একটি সাধারণ নেটওয়ার্ক ডেটা সোর্স থেকে Pager তৈরি করার মতোই, তবে দুটি জিনিস আপনাকে ভিন্নভাবে করতে হবে:

  • সরাসরি PagingSource কনস্ট্রাক্টর পাস করার পরিবর্তে, আপনাকে অবশ্যই সেই কোয়েরি মেথডটি প্রদান করতে হবে যা DAO থেকে একটি PagingSource অবজেক্ট রিটার্ন করে।
  • আপনাকে অবশ্যই remoteMediator প্যারামিটার হিসেবে আপনার RemoteMediator ইমপ্লিমেন্টেশনের একটি ইনস্ট্যান্স প্রদান করতে হবে।
val userDao = database.userDao()
val pager = Pager(
  config = PagingConfig(pageSize = 50)
  remoteMediator = ExampleRemoteMediator(query, database, networkService)
) {
  userDao.pagingSource(query)
}

রেসের শর্তাবলী পরিচালনা করুন

একাধিক উৎস থেকে ডেটা লোড করার সময় আপনার অ্যাপকে যে পরিস্থিতিটি সামলাতে হয়, তার মধ্যে একটি হলো যখন স্থানীয় ক্যাশ করা ডেটা দূরবর্তী ডেটা উৎসের সাথে অসামঞ্জস্যপূর্ণ হয়ে পড়ে।

যখন আপনার RemoteMediator ইমপ্লিমেন্টেশনের initialize() মেথডটি LAUNCH_INITIAL_REFRESH রিটার্ন করে, তখন ডেটা পুরোনো হয়ে যায় এবং এটিকে অবশ্যই নতুন ডেটা দিয়ে প্রতিস্থাপন করতে হবে। যেকোনো PREPEND বা APPEND লোড রিকোয়েস্টকে রিমোট REFRESH লোড সফল হওয়া পর্যন্ত অপেক্ষা করতে বাধ্য করা হয়। যেহেতু PREPEND বা APPEND রিকোয়েস্টগুলো REFRESH রিকোয়েস্টের আগে কিউতে রাখা হয়েছিল, তাই এমন হতে পারে যে ওই লোড কলগুলো রান হওয়ার আগেই সেগুলোতে পাস করা PagingState পুরোনো হয়ে যাবে।

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

ব্যবহারকারীরা যাতে সবচেয়ে প্রাসঙ্গিক ও হালনাগাদ ডেটা দেখতে পান, তা নিশ্চিত করার জন্য এই ডেটা সিঙ্ক্রোনাইজেশন সমস্যার সমাধান করা অপরিহার্য। এর সর্বোত্তম সমাধান মূলত নির্ভর করে নেটওয়ার্ক ডেটা সোর্স কীভাবে ডেটা পেজ করে তার ওপর। যাই হোক, রিমোট কী আপনাকে সার্ভার থেকে অনুরোধ করা সর্বশেষ পেজ সম্পর্কিত তথ্য সংরক্ষণ করার সুযোগ দেয়। আপনার অ্যাপ এই তথ্য ব্যবহার করে পরবর্তী লোডের জন্য সঠিক ডেটা পেজটি শনাক্ত করতে এবং তার জন্য অনুরোধ করতে পারে।

রিমোট কীগুলি পরিচালনা করুন

রিমোট কী হলো এমন কী যা একটি RemoteMediator ইমপ্লিমেন্টেশন ব্যাকএন্ড সার্ভিসকে পরবর্তী কোন ডেটা লোড করতে হবে তা জানানোর জন্য ব্যবহার করে। সবচেয়ে সহজ ক্ষেত্রে, পেজ করা ডেটার প্রতিটি আইটেমের সাথে একটি রিমোট কী থাকে যা আপনি সহজেই রেফারেন্স করতে পারেন। তবে, যদি রিমোট কীগুলো স্বতন্ত্র আইটেমের সাথে সঙ্গতিপূর্ণ না হয়, তাহলে আপনাকে অবশ্যই সেগুলো আলাদাভাবে সংরক্ষণ করতে হবে এবং আপনার load() মেথডে সেগুলো পরিচালনা করতে হবে।

এই বিভাগে বর্ণনা করা হয়েছে কীভাবে সেইসব রিমোট কী সংগ্রহ, সংরক্ষণ এবং আপডেট করতে হয়, যেগুলো পৃথক আইটেমে সংরক্ষিত থাকে না।

আইটেম কী

এই বিভাগে স্বতন্ত্র আইটেমের সাথে সম্পর্কিত রিমোট কী নিয়ে কীভাবে কাজ করতে হয় তা বর্ণনা করা হয়েছে। সাধারণত, যখন কোনো এপিআই স্বতন্ত্র আইটেমকে কী হিসেবে ব্যবহার করে, তখন আইটেম আইডি একটি কোয়েরি প্যারামিটার হিসেবে পাঠানো হয়। প্যারামিটারের নামটি নির্দেশ করে যে সার্ভার প্রদত্ত আইডির আগের বা পরের আইটেমগুলো দিয়ে সাড়া দেবে কিনা। User মডেল ক্লাসের উদাহরণে, অতিরিক্ত ডেটার অনুরোধ করার সময় সার্ভারের id ফিল্ডটি একটি রিমোট কী হিসেবে ব্যবহৃত হয়।

যখন আপনার load() মেথডকে আইটেম-নির্দিষ্ট রিমোট কী পরিচালনা করতে হয়, তখন এই কীগুলো সাধারণত সার্ভার থেকে আনা ডেটার আইডি হয়ে থাকে। রিফ্রেশ অপারেশনের জন্য লোড কী-এর প্রয়োজন হয় না, কারণ এটি কেবল সবচেয়ে সাম্প্রতিক ডেটা নিয়ে আসে। একইভাবে, প্রিপেন্ড অপারেশনের জন্য কোনো অতিরিক্ত ডেটা আনার প্রয়োজন হয় না, কারণ রিফ্রেশ সবসময় সার্ভার থেকে নতুনতম ডেটা নিয়ে আসে।

তবে, অ্যাপেন্ড অপারেশনের জন্য একটি আইডি প্রয়োজন হয়। এর জন্য আপনাকে ডাটাবেস থেকে সর্বশেষ আইটেমটি লোড করতে হবে এবং ডেটার পরবর্তী পৃষ্ঠা লোড করার জন্য এর আইডি ব্যবহার করতে হবে। যদি ডাটাবেসে কোনো আইটেম না থাকে, তাহলে endOfPaginationReached মান true সেট করা হয়, যা নির্দেশ করে যে ডেটা রিফ্রেশ করা প্রয়োজন।

@OptIn(ExperimentalPagingApi::class)
class ExampleRemoteMediator(
  private val query: String,
  private val database: RoomDb,
  private val networkService: ExampleBackendService
) : RemoteMediator<Int, User>() {
  val userDao = database.userDao()

  override suspend fun load(
    loadType: LoadType,
    state: PagingState<Int, User>
  ): MediatorResult {
    return try {
      // The network load method takes an optional String
      // parameter. For every page after the first, pass the String
      // token returned from the previous page to let it continue
      // from where it left off. For REFRESH, pass null to load the
      // first page.
      val loadKey = when (loadType) {
        LoadType.REFRESH -> null
        // In this example, you never need to prepend, since REFRESH
        // will always load the first page in the list. Immediately
        // return, reporting end of pagination.
        LoadType.PREPEND -> return MediatorResult.Success(
          endOfPaginationReached = true
        )
        // Get the last User object id for the next RemoteKey.
        LoadType.APPEND -> {
          val lastItem = state.lastItemOrNull()

          // You must explicitly check if the last item is null when
          // appending, since passing null to networkService is only
          // valid for initial load. If lastItem is null it means no
          // items were loaded after the initial REFRESH and there are
          // no more items to load.
          if (lastItem == null) {
            return MediatorResult.Success(
              endOfPaginationReached = true
            )
          }

          lastItem.id
        }
      }

      // Suspending network load via Retrofit. This doesn't need to
      // be wrapped in a withContext(Dispatcher.IO) { ... } block
      // since Retrofit's Coroutine CallAdapter dispatches on a
      // worker thread.
      val response = networkService.searchUsers(query, loadKey)

      // Store loaded data, and next key in transaction, so that
      // they're always consistent.
      database.withTransaction {
        if (loadType == LoadType.REFRESH) {
          userDao.deleteByQuery(query)
        }

        // Insert new users into database, which invalidates the
        // current PagingData, allowing Paging to present the updates
        // in the DB.
        userDao.insertAll(response.users)
      }

      // End of pagination has been reached if no users are returned from the
      // service
      MediatorResult.Success(
        endOfPaginationReached = response.users.isEmpty()
      )
    } catch (e: IOException) {
      MediatorResult.Error(e)
    } catch (e: HttpException) {
      MediatorResult.Error(e)
    }
  }
}

পৃষ্ঠা কী

এই অংশে এমন রিমোট কীগুলো নিয়ে কীভাবে কাজ করতে হয় তা বর্ণনা করা হয়েছে, যেগুলো কোনো নির্দিষ্ট আইটেমের সাথে সঙ্গতিপূর্ণ নয়।

রিমোট কী টেবিল যোগ করুন

যখন রিমোট কীগুলো তালিকার আইটেমগুলোর সাথে সরাসরি যুক্ত থাকে না, তখন সেগুলোকে স্থানীয় ডেটাবেসের একটি আলাদা টেবিলে সংরক্ষণ করাই শ্রেয়। একটি Room এনটিটি সংজ্ঞায়িত করুন যা রিমোট কীগুলোর একটি টেবিলকে উপস্থাপন করে:

@Entity(tableName = "remote_keys")
data class RemoteKey(val label: String, val nextKey: String?)

আপনাকে RemoteKey এনটিটির জন্য একটি DAO-ও সংজ্ঞায়িত করতে হবে:

@Dao
interface RemoteKeyDao {
  @Insert(onConflict = OnConflictStrategy.REPLACE)
  suspend fun insertOrReplace(remoteKey: RemoteKey)

  @Query("SELECT * FROM remote_keys WHERE label = :query")
  suspend fun remoteKeyByQuery(query: String): RemoteKey

  @Query("DELETE FROM remote_keys WHERE label = :query")
  suspend fun deleteByQuery(query: String)
}

রিমোট কী দিয়ে লোড করুন

যখন আপনার load() মেথডকে রিমোট পেজ কী (remote page keys) পরিচালনা করতে হয়, তখন RemoteMediator এর সাধারণ ব্যবহারের তুলনায় আপনাকে এটিকে নিম্নলিখিত উপায়ে ভিন্নভাবে সংজ্ঞায়িত করতে হবে:

  • আপনার রিমোট কী টেবিলের জন্য DAO-এর একটি রেফারেন্স ধারণ করে এমন একটি অতিরিক্ত প্রপার্টি অন্তর্ভুক্ত করুন।
  • PagingState ব্যবহার না করে, রিমোট কী টেবিল কোয়েরি করে পরবর্তী কোন কী লোড করতে হবে তা নির্ধারণ করুন।
  • পেজ করা ডেটার পাশাপাশি নেটওয়ার্ক ডেটা সোর্স থেকে ফেরত আসা রিমোট কী-টিও সন্নিবেশ বা সংরক্ষণ করুন।
@OptIn(ExperimentalPagingApi::class)
class ExampleRemoteMediator(
  private val query: String,
  private val database: RoomDb,
  private val networkService: ExampleBackendService
) : RemoteMediator<Int, User>() {
  val userDao = database.userDao()
  val remoteKeyDao = database.remoteKeyDao()

  override suspend fun load(
    loadType: LoadType,
    state: PagingState<Int, User>
  ): MediatorResult {
    return try {
      // The network load method takes an optional String
      // parameter. For every page after the first, pass the String
      // token returned from the previous page to let it continue
      // from where it left off. For REFRESH, pass null to load the
      // first page.
      val loadKey = when (loadType) {
        LoadType.REFRESH -> null
        // In this example, you never need to prepend, since REFRESH
        // will always load the first page in the list. Immediately
        // return, reporting end of pagination.
        LoadType.PREPEND -> return MediatorResult.Success(
          endOfPaginationReached = true
        )
        // Query remoteKeyDao for the next RemoteKey.
        LoadType.APPEND -> {
          val remoteKey = database.withTransaction {
            remoteKeyDao.remoteKeyByQuery(query)
          }

          // You must explicitly check if the page key is null when
          // appending, since null is only valid for initial load.
          // If you receive null for APPEND, that means you have
          // reached the end of pagination and there are no more
          // items to load.
          if (remoteKey.nextKey == null) {
            return MediatorResult.Success(
              endOfPaginationReached = true
            )
          }

          remoteKey.nextKey
        }
      }

      // Suspending network load via Retrofit. This doesn't need to
      // be wrapped in a withContext(Dispatcher.IO) { ... } block
      // since Retrofit's Coroutine CallAdapter dispatches on a
      // worker thread.
      val response = networkService.searchUsers(query, loadKey)

      // Store loaded data, and next key in transaction, so that
      // they're always consistent.
      database.withTransaction {
        if (loadType == LoadType.REFRESH) {
          remoteKeyDao.deleteByQuery(query)
          userDao.deleteByQuery(query)
        }

        // Update RemoteKey for this query.
        remoteKeyDao.insertOrReplace(
          RemoteKey(query, response.nextKey)
        )

        // Insert new users into database, which invalidates the
        // current PagingData, allowing Paging to present the updates
        // in the DB.
        userDao.insertAll(response.users)
      }

      MediatorResult.Success(
        endOfPaginationReached = response.nextKey == null
      )
    } catch (e: IOException) {
      MediatorResult.Error(e)
    } catch (e: HttpException) {
      MediatorResult.Error(e)
    }
  }
}

যথাস্থানে রিফ্রেশ করুন

পূর্ববর্তী উদাহরণগুলোর মতো আপনার অ্যাপের যদি শুধু তালিকার শীর্ষ থেকে নেটওয়ার্ক রিফ্রেশ সমর্থন করার প্রয়োজন হয়, তাহলে আপনার RemoteMediator প্রিপেন্ড লোড বিহেভিয়ার সংজ্ঞায়িত করার দরকার নেই।

তবে, যদি আপনার অ্যাপকে নেটওয়ার্ক থেকে স্থানীয় ডেটাবেসে পর্যায়ক্রমে ডেটা লোড করার প্রয়োজন হয়, তাহলে আপনাকে অবশ্যই অ্যাঙ্কর (অর্থাৎ ব্যবহারকারীর স্ক্রোল পজিশন) থেকে পেজিনেশন পুনরায় শুরু করার ব্যবস্থা রাখতে হবে। Room-এর PagingSource ইমপ্লিমেন্টেশন আপনার জন্য এই কাজটি করে দেয়, কিন্তু আপনি যদি Room ব্যবহার না করেন, তাহলে PagingSource.getRefreshKey() ওভাররাইড করে এটি করতে পারেন। getRefreshKey() এর একটি উদাহরণ ইমপ্লিমেন্টেশনের জন্য, "Define the PagingSource" দেখুন।

চিত্র ২-এ প্রথমে স্থানীয় ডেটাবেস থেকে এবং ডেটাবেসে ডেটা শেষ হয়ে গেলে নেটওয়ার্ক থেকে ডেটা লোড করার প্রক্রিয়াটি দেখানো হয়েছে।

PagingSource ডাটাবেস থেকে UI-তে ডেটা লোড করতে থাকে যতক্ষণ না ডাটাবেসের ডেটা শেষ হয়ে যায়। তারপর RemoteMediator নেটওয়ার্ক থেকে ডাটাবেসে ডেটা লোড করে, এবং এরপরে PagingSource আবার লোড হতে থাকে।
চিত্র ২. ডেটা লোড করার জন্য PagingSource এবং RemoteMediator কীভাবে একত্রে কাজ করে, তার একটি ডায়াগ্রাম।

অতিরিক্ত সম্পদ

পেজিং লাইব্রেরি সম্পর্কে আরও জানতে, নিম্নলিখিত অতিরিক্ত রিসোর্সগুলো দেখুন:

বিষয়বস্তু দেখুন

{% হুবহু %} {% endverbatim %} {% হুবহু %} {% endverbatim %}