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

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

এই গাইড ধরে নেয় যে আপনি রুম পারসিসটেন্স লাইব্রেরি এবং পেজিং লাইব্রেরির প্রাথমিক ব্যবহার সম্পর্কে জানেন।

ডেটা লোড করার কাজ কোঅর্ডিনেট করা

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

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

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

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

পেজিং লাইফসাইকেল

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

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

ছবি ১. PagingSource ও RemoteMediator-এর সাথে পেজিংয়ের লাইফসাইকেলের ডায়াগ্রাম।

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

প্রাথমিক ব্যবহার

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

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

রুম এন্টিটি তৈরি করুন

প্রথম ধাপ হল রুম পারসিসটেন্স লাইব্রেরি ব্যবহার করে এমন একটি ডেটাবেস সংজ্ঞায়িত করা যা নেটওয়ার্ক ডেটা সোর্স থেকে পেজ করা ডেটার লোকাল ক্যাশে ধরে রাখে। রুম ব্যবহার করে স্থানীয় ডেটাবেসে ডেটা সেভ করুন লিঙ্কে যেভাবে বর্ণনা করা হয়েছে সেই অনুযায়ী RoomDatabase প্রয়োগ করে শুরু করুন।

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

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

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

  • একটি 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 প্রয়োগ করা

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

সাধারণত RemoteMediator ইমপ্লিমেন্টেশনে নিম্নলিখিত প্যারামিটার অন্তর্ভুক্ত থাকে:

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

RemoteMediator<Key, Value> ইমপ্লিমেন্টেশন তৈরি করুন। Key-এর ধরন এবং Value-এর ধরন একই হতে হবে, যেমনটা আপনি একই নেটওয়ার্ক ডেটা সোর্সের জন্য PagingSource নির্ধারণ করার সময় করতেন। টাইপ প্যারামিটার বেছে নেওয়ার ব্যাপারে আরও তথ্যের জন্য, কী ও ভ্যালুর ধরন বেছে নিন দেখুন।

@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 বাতিল করার জন্য দায়ী। পেজিং (যেমন, রুম) সাপোর্ট করে এমন কিছু লাইব্রেরি অটোমেটিক 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)
  }
}

ইনিশিয়ালাইজ মেথড সংজ্ঞায়িত করুন

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

initialize() একটি অ্যাসিঙ্ক্রোনাস ফাংশন হওয়ার কারণে, ডেটাবেসে আগে থেকে থাকা ডেটার প্রাসঙ্গিকতা নির্ধারণ করতে আপনি ডেটা লোড করতে পারবেন। সবচেয়ে সাধারণ ক্ষেত্রে ক্যাশে করা ডেটা শুধুমাত্র নির্দিষ্ট সময়সীমার জন্য বৈধ থাকে। RemoteMediator চেক করে দেখতে পারে যে এই মেয়াদ শেষ হওয়ার সময় পেরিয়ে গেছে কিনা, সেক্ষেত্রে পেজিং লাইব্রেরিকে ডেটা সম্পূর্ণভাবে রিফ্রেশ করতে হবে। 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 রিটার্ন করলে, ডেটা পুরনো হয়ে যায় এবং তা নতুন ডেটা দিয়ে পরিবর্তন করতে হয়। REFRESH লোড সফল হওয়ার জন্য যেকোনও PREPEND বা APPEND লোডের অনুরোধকে রিমোটের জন্য অপেক্ষা করতে বাধ্য করা হয়। যেহেতু REFRESH অনুরোধের আগে PREPEND বা APPEND অনুরোধগুলি সারিবদ্ধ করা হয়েছিল, তাই এটি সম্ভব যে সেই লোড কলগুলিতে পাস করা PagingState রান করার সময় পর্যন্ত পুরনো হয়ে যাবে।

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

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

রিমোট 'কী' ম্যানেজ করুন

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

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

আইটেম 'কী'

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

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

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

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

পৃষ্ঠা কী

এই বিভাগে বর্ণনা করা হয়েছে যে কীভাবে রিমোট কী ব্যবহার করতে হয় যা স্বতন্ত্র আইটেমের সাথে সম্পর্কিত নয়।

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

রিমোট কী সরাসরি তালিকা আইটেমের সাথে যুক্ত না থাকলে, লোকাল ডেটাবেসে আলাদা টেবিলে সেগুলি স্টোর করা সবচেয়ে ভাল। রিমোট কী-এর টেবিল দেখায় এমন রুম এন্টিটি সংজ্ঞায়িত করুন:

@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() পদ্ধতিকে রিমোট পৃষ্ঠা কী ম্যানেজ করতে হলে, আপনাকে এটি 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-কে প্রিপেন্ড লোড বিহেভিয়ার সংজ্ঞায়িত করতে হবে না।

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

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

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

অতিরিক্ত রিসোর্স

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

কন্টেন্ট দেখে