পৃষ্ঠা ৩ এর সংক্ষিপ্ত বিবরণ

এই নির্দেশিকাটি Jetpack Compose ব্যবহার করে কীভাবে পেজিং ৩ প্রয়োগ করতে হয় তা ব্যাখ্যা করে, যেখানে Room ডেটাবেস সহ এবং ছাড়া উভয় প্রয়োগই অন্তর্ভুক্ত রয়েছে। পেজিনেশন হলো বিশাল ডেটাসেট পরিচালনা করার একটি কৌশল, যেখানে পুরো ডেটাসেটকে একবারে লোড না করে, সেগুলোকে পেজ নামক ছোট ও সহজে পরিচালনাযোগ্য খণ্ডে লোড ও প্রদর্শন করা হয়।

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

ডেটা পেজিনেশন পরিচালনার জন্য পেজিং ৩ লাইব্রেরি হলো প্রস্তাবিত সমাধান। আপনি যদি পেজিং ২ থেকে মাইগ্রেট করেন, তবে নির্দেশনার জন্য ‘পেজিং ৩-এ মাইগ্রেট করুন’ দেখুন।

পূর্বশর্ত

এগিয়ে যাওয়ার আগে, নিম্নলিখিত বিষয়গুলো জেনে নিন:

  • অ্যান্ড্রয়েডে নেটওয়ার্কিং (আমরা এই ডকুমেন্টে Retrofit ব্যবহার করেছি, কিন্তু Paging 3 যেকোনো লাইব্রেরির সাথেই কাজ করে, যেমন Ktor )।
  • কম্পোজ UI টুলকিট।

নির্ভরতা সেট আপ করুন

আপনার অ্যাপ-স্তরের build.gradle.kts ফাইলে নিম্নলিখিত ডিপেন্ডেন্সিগুলো যোগ করুন।

dependencies {
  val paging_version = "3.4.0"

  // Paging Compose
  implementation("androidx.paging:paging-compose:$paging_version")

  // Networking dependencies used in this guide
  val retrofit = "3.0.0"
  val kotlinxSerializationJson = "1.9.0"
  val retrofitKotlinxSerializationConverter = "1.0.0"
  val okhttp = "4.12.0"

  implementation("com.squareup.retrofit2:retrofit:$retrofit")
  implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializationJson")
  implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:$retrofitKotlinxSerializationConverter")
  implementation(platform("com.squareup.okhttp3:okhttp-bom:$okhttp"))
  implementation("com.squareup.okhttp3:okhttp")
  implementation("com.squareup.okhttp3:logging-interceptor")
}

Pager ক্লাস সংজ্ঞায়িত করুন

Pager ক্লাসটি হলো পেজিনেশনের প্রধান প্রবেশপথ। এটি PagingData এর একটি রিঅ্যাক্টিভ স্ট্রিম তৈরি করে। আপনার ViewModel মধ্যে Pager ইনস্ট্যানশিয়েট করে পুনরায় ব্যবহার করা উচিত।

ডেটা কীভাবে সংগ্রহ ও উপস্থাপন করা হবে তা নির্ধারণ করার জন্য Pager একটি PagingConfig প্রয়োজন।

// Configs for pagination
val PAGING_CONFIG = PagingConfig(
    pageSize = 50, // Items requested from data source
    enablePlaceholders = false,
    initialLoadSize = 50,
    prefetchDistance = 10 // Items from the end that trigger the next fetch
)

আপনি Pager দুটি উপায়ে প্রয়োগ করতে পারেন: ডাটাবেস ছাড়া (শুধুমাত্র নেটওয়ার্ক) অথবা ডাটাবেস সহ (রুম ব্যবহার করে)।

ডাটাবেস ছাড়া বাস্তবায়ন করুন

যখন ডাটাবেস ব্যবহার করা হয় না, তখন চাহিদা অনুযায়ী ডেটা লোড করার জন্য আপনার একটি PagingSource<Key, Value> প্রয়োজন হয়। এই উদাহরণে, কী (key) হলো Int এবং ভ্যালু (value) হলো একটি Product

আপনাকে আপনার PagingSource এ দুটি অ্যাবস্ট্রাক্ট মেথড ইমপ্লিমেন্ট করতে হবে:

  • load : একটি সাসপেন্ডিং ফাংশন যা LoadParams গ্রহণ করে। Refresh , Append , বা Prepend অনুরোধের জন্য ডেটা আনতে এটি ব্যবহার করুন।

  • getRefreshKey : পেজারটি অবৈধ হয়ে গেলে ডেটা পুনরায় লোড করার জন্য ব্যবহৃত কী (key) প্রদান করে। এই পদ্ধতিটি ব্যবহারকারীর বর্তমান স্ক্রোল পজিশনের ( state.anchorPosition ) উপর ভিত্তি করে কী-টি গণনা করে।

নিম্নলিখিত কোড উদাহরণটি দেখায় কিভাবে ProductPagingSource ক্লাসটি ইমপ্লিমেন্ট করতে হয়, যা লোকাল ডাটাবেস ছাড়া পেজিং ৩ ব্যবহার করার সময় ডেটা ফেচিং লজিক সংজ্ঞায়িত করার জন্য প্রয়োজনীয়।

class ProductPagingSource : PagingSource<Int, Product>() {
    override fun getRefreshKey(state: PagingState<Int, Product>): Int {

// This is called when the Pager needs to load new data after invalidation
      // (for example, when the user scrolls quickly or the data stream is
      // manually refreshed).

      // It tries to calculate the page key (offset) that is closest to the
      // item the user was last viewing (`state.anchorPosition`).

        return ((state.anchorPosition ?: 0) - state.config.initialLoadSize / 2).coerceAtLeast(0)
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Product> {
        return when (params) {
                // Case 1: The very first load or a manual refresh. Start from
                // offset 0.
            is LoadParams.Refresh<Int> -> {
                fetchProducts(0, params.loadSize)
            }
                // Case 2: User scrolled to the end of the list. Load the next
                // 'page' using the stored key.
            is LoadParams.Append<Int> -> {
                fetchProducts(params.key, params.loadSize)
            }
                // Case 3: Loading backward. Not supported in this
                // implementation.
            is LoadParams.Prepend<Int> -> LoadResult.Invalid()
        }
    }
// Helper function to interact with the API service and map the response
//  into a [LoadResult.Page] or [LoadResult.Error].
    private suspend fun fetchProducts(key: Int, limit: Int): LoadResult<Int, Product> {
        return try {
            val response = productService.fetchProducts(limit, key)

            LoadResult.Page(
                data = response.products,
                prevKey = null,
                nextKey = (key + response.products.size).takeIf { nextKey ->
                    nextKey < response.total
                }
            )
        } catch (e: Exception) {
                // Captures network failures or JSON parsing errors to display
                // in the UI.
            LoadResult.Error(e)
        }
    }
}

আপনার ViewModel ক্লাসে Pager তৈরি করুন:

val productPager = Pager(
    //  Configuration: Defines page size, prefetch distance, and placeholders.
    config = PAGING_CONFIG,
    //  Initial State: Start loading data from the very first index (offset 0).
    initialKey = 0,
    //  Factory: Creates a new instance of the PagingSource whenever the
    // data is invalidated (for example, calling pagingSource.invalidate()).
    pagingSourceFactory = { ProductPagingSource() }
).flow.cachedIn(viewModelScope)

ডাটাবেস দিয়ে বাস্তবায়ন করুন

Room ব্যবহার করার সময়, ডাটাবেস স্বয়ংক্রিয়ভাবে PagingSource ক্লাসটি তৈরি করে। তবে, নেটওয়ার্ক থেকে কখন আরও ডেটা আনতে হবে তা ডাটাবেস জানে না। এটি সামাল দিতে, একটি RemoteMediator ইমপ্লিমেন্ট করুন।

RemoteMediator.load() মেথডটি loadType ( Append , Prepend , বা Refresh ) এবং state প্রদান করে। এটি একটি MediatorResult রিটার্ন করে, যা সফল বা ব্যর্থ হওয়ার পাশাপাশি পেজিনেশনের শেষ পর্যায়ে পৌঁছেছে কিনা তা নির্দেশ করে।

@OptIn(ExperimentalPagingApi::class)
@OptIn(ExperimentalPagingApi::class)
class ProductRemoteMediator : RemoteMediator<Int, Product>() {
    override suspend fun load(
        loadType: LoadType,
        state: PagingState<Int, Product>
    ): MediatorResult {
        return try {
            // Get the count of loaded items to calculate the skip value
            val skip = when (loadType) {
                LoadType.REFRESH -> 0
                LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
                LoadType.APPEND -> {
                    InMemoryDatabaseProvider.INSTANCE.productDao().getCount()
                }
            }

            val response = productService.fetchProducts(
                state.config.pageSize,
                skip
            )

            InMemoryDatabaseProvider.INSTANCE.productDao().apply {
                insertAll(response.products)
            }

            MediatorResult.Success(
                endOfPaginationReached = response.skip + response.limit >= response.total
            )
        } catch (e: Exception) {
            MediatorResult.Error(e)
        }
    }
}

আপনার ViewModel এ এর বাস্তবায়ন উল্লেখযোগ্যভাবে সহজ হয়ে যায়, কারণ Room নিজেই PagingSource ক্লাসটি পরিচালনা করে:

val productPager = ProductRepository().fetchProducts().flow.cachedIn(viewModelScope)

নেটওয়ার্ক সেটআপ

পূর্ববর্তী উদাহরণগুলো একটি নেটওয়ার্ক পরিষেবার উপর নির্ভরশীল। এই বিভাগে api.example.com/products এন্ডপয়েন্ট থেকে ডেটা আনার জন্য ব্যবহৃত রেট্রোফিট এবং সিরিয়ালাইজেশন সেটআপ প্রদান করা হয়েছে।

ডেটা ক্লাস

নিম্নলিখিত কোড উদাহরণটিতে ProductResponse এবং Product দুটি ডেটা ক্লাস কীভাবে সংজ্ঞায়িত করতে হয় তা দেখানো হয়েছে, যেগুলো নেটওয়ার্ক পরিষেবা থেকে প্রাপ্ত পেজিনেটেড JSON প্রতিক্রিয়া পার্স করার জন্য kotlinx.serialization সাথে ব্যবহৃত হয়।

@Serializable
data class ProductResponse(
    val products: List<Product>,
    val total: Int,
    val skip: Int,
    val limit: Int
)

@Serializable
data class Product(
    val id: Int,
    var title: String = "",
    // ... other fields (description, price, etc.)
    val thumbnail: String = ""
)

রেট্রোফিট পরিষেবা

নিম্নলিখিত কোড উদাহরণটি দেখায় কিভাবে শুধুমাত্র নেটওয়ার্ক-ভিত্তিক বাস্তবায়নের জন্য রেট্রোফিট সার্ভিস ইন্টারফেস ( ProductService ) সংজ্ঞায়িত করতে হয়, যেখানে ডেটা পেজ আনার জন্য পেজিং ৩ লাইব্রেরির প্রয়োজনীয় এন্ডপয়েন্ট ( @GET("/products") ) এবং পেজিনেশন প্যারামিটার ( limit ) ও ( skip ) উল্লেখ করা হয়েছে।

interface ProductService {
    @GET("/products")
    suspend fun fetchProducts(
        @Query("limit") limit: Int,
        @Query("skip") skip: Int
    ): ProductResponse
}

// Setup logic (abbreviated)
val jsonConverter = Json { ignoreUnknownKeys = true }
val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com")
    .addConverterFactory(jsonConverter.asConverterFactory("application/json".toMediaType()))
    // ... client setup
    .build()

কম্পোজে ডেটা ব্যবহার করুন

আপনার Pager সেট আপ করার পরে, আপনি আপনার UI-তে ডেটা প্রদর্শন করতে পারবেন।

  1. ফ্লো সংগ্রহ করুন : ফ্লোটিকে একটি স্টেট-অ্যাওয়ার লেজি পেজিং আইটেমস অবজেক্টে রূপান্তর করতে collectAsLazyPagingItems() ব্যবহার করুন।

    val productPagingData = mainViewModel.productPager.collectAsLazyPagingItems()
    

    ফলস্বরূপ প্রাপ্ত LazyPagingItems অবজেক্টটি আইটেমের সংখ্যা এবং ইনডেক্সড অ্যাক্সেস প্রদান করে, যার ফলে তালিকার আইটেমগুলো রেন্ডার করার জন্য LazyColumn মেথডটি এটিকে সরাসরি ব্যবহার করতে পারে।

  2. LazyColumn সাথে সংযুক্ত করুন : ডেটা একটি LazyColumn লিস্টে পাঠান। আপনি যদি RecyclerView লিস্ট থেকে মাইগ্রেট করে থাকেন, তাহলে আপনার লিস্টের উপরে বা নীচে লোডিং স্পিনার অথবা এরর রিট্রাই বাটন দেখানোর জন্য withLoadStateHeaderAndFooter ব্যবহার করার সাথে আপনি পরিচিত থাকতে পারেন।

    Compose-এ এর জন্য কোনো বিশেষ অ্যাডাপ্টারের প্রয়োজন নেই। আপনি আপনার মূল items {} ব্লকের আগে বা পরে শর্তসাপেক্ষে একটি item {} ব্লক যোগ করে হুবহু একই আচরণ অর্জন করতে পারেন, যা সরাসরি prepend (হেডার) এবং append (ফুটার) লোড স্টেটের প্রতি সাড়া দেয়।

    LazyColumn {
        // --- HEADER (Equivalent to loadStateHeader) ---
        // Reacts to 'prepend' states when scrolling towards the top
        if (productPagingData.loadState.prepend is LoadState.Loading) {
            item {
                Box(modifier = Modifier.fillMaxWidth(), contentAlignment =
                Alignment.Center) {
                    CircularProgressIndicator(modifier = Modifier.padding(16.dp))
                }
            }
        }
        if (productPagingData.loadState.prepend is LoadState.Error) {
            item {
                ErrorHeader(onRetry = { productPagingData.retry() })
            }
        }
    
        // --- MAIN LIST ITEMS ---
        items(count = productPagingData.itemCount) { index ->
            val product = productPagingData[index]
            if (product != null) {
                UserPagingListItem(product = product)
            }
        }
    
        // --- FOOTER (Equivalent to loadStateFooter) ---
        // Reacts to 'append' states when scrolling towards the bottom
        if (productPagingData.loadState.append is LoadState.Loading) {
            item {
                Box(modifier = Modifier.fillMaxWidth(), contentAlignment =
                Alignment.Center) {
                    CircularProgressIndicator(modifier = Modifier.padding(16.dp))
                }
            }
        }
        if (productPagingData.loadState.append is LoadState.Error) {
            item {
               ErrorFooter(onRetry = { productPagingData.retry() })
            }
        }
    }
    

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

লোড অবস্থা পরিচালনা করুন

PagingData অবজেক্টটি লোডিং অবস্থার তথ্য একত্রিত করে। বিভিন্ন অবস্থার ( refresh , append বা prepend ) জন্য লোডিং স্পিনার বা ত্রুটির বার্তা দেখানোর জন্য আপনি এটি ব্যবহার করতে পারেন।

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

আপনি snapshotFlow ব্যবহার করে স্টেট পর্যবেক্ষণ করে এবং distinctUntilChangedBy প্রপার্টির মতো ফ্লো অপারেটর প্রয়োগ করে এটিকে অপ্টিমাইজ করতে পারেন। খালি স্টেট প্রদর্শন করার সময় বা এরর স্নাকবারের মতো সাইড-ইফেক্ট ট্রিগার করার সময় এটি বিশেষভাবে কার্যকর।

val snackbarHostState = remember { SnackbarHostState() }

LaunchedEffect(productPagingData.loadState) {
    snapshotFlow { productPagingData.loadState }
        // Filter out updates that don't change the refresh state
        .distinctUntilChangedBy { it.refresh }
        // Only react when the state is an Error
        .filter { it.refresh is LoadState.Error }
        .collect { loadState ->
            val error = (loadState.refresh as LoadState.Error).error
            snackbarHostState.showSnackbar(
                message = "Data failed to load: ${error.localizedMessage}",
                actionLabel = "Retry"
            )
        }
}

পূর্ণ-স্ক্রিন লোডিং স্পিনার দেখানোর জন্য রিফ্রেশ অবস্থা পরীক্ষা করার সময়, অপ্রয়োজনীয় পুনর্গঠন এড়াতে derivedStateOf ব্যবহার করুন।

এছাড়াও, যদি আপনি একটি RemoteMediator ব্যবহার করেন (যেমনটি আগে Room ডাটাবেস ইমপ্লিমেন্টেশনে করা হয়েছিল), তাহলে সুবিধাজনক loadState.refresh প্রপার্টির পরিবর্তে অন্তর্নিহিত ডেটা সোর্সের লোডিং অবস্থা ( loadState.source.refresh ) স্পষ্টভাবে পরীক্ষা করুন। এই সুবিধাজনক প্রপার্টিটি হয়তো রিপোর্ট করতে পারে যে, ডাটাবেস UI-তে নতুন আইটেমগুলো যোগ করা শেষ করার আগেই নেটওয়ার্ক ফেচ সম্পন্ন হয়ে গেছে। source পরীক্ষা করলে এটি নিশ্চিত হয় যে UI স্থানীয় ডাটাবেসের সাথে সম্পূর্ণরূপে সিঙ্ক করা আছে, যা লোডারকে খুব তাড়াতাড়ি অদৃশ্য হয়ে যাওয়া থেকে বিরত রাখে।

// Safely check the refresh state for a full-screen spinner
// without triggering unnecessary recompositions
val isRefreshing by remember {
    derivedStateOf { productPagingData.loadState.source.refresh is LoadState.Loading }
}
if (isRefreshing) {
    // Show UI for refreshing (for example, full screen spinner)
    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        CircularProgressIndicator()
    }
}

ব্যবহারকারীকে রিট্রাই বাটন বা ত্রুটির বার্তা দেখানোর জন্য আপনি LoadState.Error ও ব্যবহার করে দেখতে পারেন। আমরা LoadState.Error ব্যবহার করার পরামর্শ দিই, কারণ এটি অন্তর্নিহিত এক্সেপশনটি প্রকাশ করে এবং ব্যবহারকারীকে পরিস্থিতি থেকে পুনরুদ্ধারের জন্য বিল্ট-ইন retry() ফাংশনটি সক্রিয় করে।

if (refreshState is LoadState.Error) {
   val e = refreshState as LoadState.Error

   // This composable should ideally replace the entire list if the initial load
   // fails.
   ErrorScreen(
       message = "Data failed to load: ${e.error.localizedMessage}",
       onClickRetry = { productPagingData.retry() }
   )
}

আপনার বাস্তবায়ন পরীক্ষা করুন

আপনার পেজিনেশন ইমপ্লিমেন্টেশন পরীক্ষা করলে নিশ্চিত হওয়া যায় যে ডেটা সঠিকভাবে লোড হচ্ছে, ট্রান্সফরমেশনগুলো প্রত্যাশা অনুযায়ী প্রয়োগ হচ্ছে এবং স্টেট পরিবর্তনের সাথে UI যথাযথভাবে সাড়া দিচ্ছে। এই প্রক্রিয়াটিকে সহজ করার জন্য Paging 3 লাইব্রেরি একটি বিশেষ টেস্টিং আর্টিফ্যাক্ট ( androidx.paging:paging-testing ) প্রদান করে।

প্রথমে, আপনার build.gradle ফাইলে টেস্টিং ডিপেন্ডেন্সিটি যোগ করুন:

testImplementation("androidx.paging:paging-testing:$paging_version")

ডেটা স্তর পরীক্ষা করুন

আপনার PagingSource সরাসরি পরীক্ষা করতে, TestPager ব্যবহার করুন। এই ইউটিলিটিটি Paging 3-এর অন্তর্নিহিত কার্যপ্রণালী পরিচালনা করে এবং একটি সম্পূর্ণ Pager সেটআপের প্রয়োজন ছাড়াই আপনাকে প্রাথমিক লোড (রিফ্রেশ), ডেটা যুক্ত করা বা সামনে যোগ করার মতো প্রান্তিক পরিস্থিতিগুলো স্বাধীনভাবে যাচাই করতে দেয়।

@Test
fun testProductPagingSource() = runTest {
    val pagingSource = ProductPagingSource(mockApiService)

    // Create a TestPager to interact with the PagingSource
    val pager = TestPager(
        config = PAGING_CONFIG,
        pagingSource = pagingSource
    )

    // Trigger an initial load
    val result = pager.refresh() as PagingSource.LoadResult.Page

    // Assert the data size and edge cases like next/prev keys
    assertEquals(50, result.data.size)
    assertNull(result.prevKey)
    assertEquals(50, result.nextKey)
}

ViewModel লজিক এবং রূপান্তর পরীক্ষা করুন

যদি আপনার ViewModel PagingData ফ্লো-তে ডেটা ট্রান্সফরমেশন (যেমন .map অপারেশন) প্রয়োগ করে, তাহলে আপনি asPagingSourceFactory এবং asSnapshot() ব্যবহার করে এই লজিকটি পরীক্ষা করতে পারেন।

asPagingSourceFactory এক্সটেনশনটি একটি স্ট্যাটিক লিস্টকে PagingSource এ রূপান্তর করে, যার ফলে রিপোজিটরি লেয়ারকে মক করা সহজ হয়। asSnapshot() এক্সটেনশনটি PagingData স্ট্রিমকে একটি স্ট্যান্ডার্ড কোটলিন List সংগ্রহ করে, যা আপনাকে রূপান্তরিত ডেটার উপর স্ট্যান্ডার্ড অ্যাসারশন চালাতে দেয়।

@Test
fun testViewModelTransformations() = runTest {
    // 1. Mock your initial data using asPagingSourceFactory
    val mockProducts = listOf(Product(1, "A"), Product(2, "B"))
    val pagingSourceFactory = mockProducts.asPagingSourceFactory()

    // 2. Pass the mocked factory to your ViewModel or Pager
    val pager = Pager(
        config = PagingConfig(pageSize = 10),
        pagingSourceFactory = pagingSourceFactory
    )

    // 3. Apply your ViewModel transformations (for example, mapping to a UI
    //    model)
    val transformedFlow = pager.flow.map { pagingData ->
        pagingData.map { product -> product.title.uppercase() }
    }

    // 4. Extract the data as a List using asSnapshot()
    val snapshot: List<String> = transformedFlow.asSnapshot(this)

    // 5. Verify the transformation
    assertEquals(listOf("A", "B"), snapshot)
}

অবস্থা এবং পুনর্গঠন যাচাই করার জন্য UI পরীক্ষা

UI পরীক্ষা করার সময়, যাচাই করুন যে আপনার Compose কম্পোনেন্টগুলো ডেটা সঠিকভাবে রেন্ডার করছে এবং লোড স্টেটের প্রতি যথাযথভাবে সাড়া দিচ্ছে। ডেটা স্ট্রিম অনুকরণ করার জন্য আপনি PagingData.from() এবং flowOf() ব্যবহার করে স্ট্যাটিক PagingData পাস করতে পারেন। এছাড়াও, আপনার Compose কম্পোনেন্টগুলো অপ্রয়োজনীয়ভাবে রিকম্পোজ হচ্ছে না তা নিশ্চিত করতে, পরীক্ষার সময় রিকম্পোজিশনের সংখ্যা ট্র্যাক করার জন্য আপনি একটি SideEffect ব্যবহার করতে পারেন।

নিম্নলিখিত উদাহরণটি দেখায় কিভাবে একটি লোডিং অবস্থা অনুকরণ করতে হয়, লোড হওয়া অবস্থায় রূপান্তরিত হতে হয়, এবং UI নোড ও পুনর্গঠন সংখ্যা উভয়ই যাচাই করতে হয়:

@get:Rule
val composeTestRule = createComposeRule()

@Test
fun testProductList_loadingAndDataStates() {
    val context = InstrumentationRegistry.getInstrumentation().targetContext

    // Create a MutableStateFlow to emit different PagingData states over time
    val pagingDataFlow = MutableStateFlow(PagingData.empty<Product>())
    var recompositionCount = 0

    composeTestRule.setContent {
        val lazyPagingItems = pagingDataFlow.collectAsLazyPagingItems()

        // Track recompositions of this composable
        SideEffect { recompositionCount++ }

        ProductListScreen(lazyPagingItems = lazyPagingItems)
    }

    // 1. Simulate initial loading state
    pagingDataFlow.value = PagingData.empty(
        sourceLoadStates = LoadStates(
            refresh = LoadState.Loading,
            prepend = LoadState.NotLoading(endOfPaginationReached = false),
            append = LoadState.NotLoading(endOfPaginationReached = false)
        )
    )

    // Verify that the loading indicator is displayed
    composeTestRule.onNodeWithTag("LoadingSpinner").assertIsDisplayed()

    // 2. Simulate data loaded state
    val mockItems = listOf(
        Product(id = 1, title = context.getString(R.string.product_a_title)),
        Product(id = 2, title = context.getString(R.string.product_b_title))
    )

    pagingDataFlow.value = PagingData.from(
        data = mockItems,
        sourceLoadStates = LoadStates(
            refresh = LoadState.NotLoading(endOfPaginationReached = false),
            prepend = LoadState.NotLoading(endOfPaginationReached = false),
            append = LoadState.NotLoading(endOfPaginationReached = false)
        )
    )

    // Wait for the UI to settle and verify the items are displayed
    composeTestRule.waitForIdle()
    composeTestRule.onNodeWithText(context.getString(R.string.product_a_title)).assertIsDisplayed()
    composeTestRule.onNodeWithText(context.getString(R.string.product_b_title)).assertIsDisplayed()

    // 3. Verify recomposition counts
    // Assert that recompositions are within expected limits (for example,
    // initial composition + updating to load state + updating to data state)
    assert(recompositionCount <= 3) {
        "Expected less than or equal to 3 recompositions, but got $recompositionCount"
    }
}