লোডিং স্ট্যাটাস ম্যানেজ ও উপস্থাপন করুন

পেজিং লাইব্রেরি পেজ করা ডেটার জন্য লোড অনুরোধের অবস্থা ট্র্যাক করে এবং LoadState ক্লাসের মাধ্যমে এটি প্রকাশ করে।

প্রতিটি LoadType এবং ডেটা সোর্সের ধরনের (PagingSource অথবা RemoteMediator) জন্য আলাদা LoadState সিগন্যাল প্রদান করা হয়। লিসনারের মাধ্যমে প্রদান করা CombinedLoadStates অবজেক্ট, এইসব সিগন্যাল থেকে লোডিং স্টেট সম্পর্কে তথ্য প্রদান করে। আপনার ব্যবহারকারীদের উপযুক্ত লোডিং ইন্ডিকেটর দেখানোর জন্য আপনি এই বিস্তারিত তথ্য ব্যবহার করতে পারেন।

রাজ্য লোড করা হচ্ছে

পেজিং লাইব্রেরি LoadState অবজেক্টের মাধ্যমে UI-তে ব্যবহারের জন্য লোডিং স্টেট প্রকাশ করে। বর্তমান লোডিং অবস্থার উপর নির্ভর করে LoadState অবজেক্ট তিনটি ফর্মের মধ্যে একটি নেয়:

  • কোনও অ্যাক্টিভ লোড অপারেশন না থাকলে এবং কোনও সমস্যা না হলে, LoadState হল একটি LoadState.NotLoading অবজেক্ট। এই সাবক্লাসে endOfPaginationReached প্রপার্টিও অন্তর্ভুক্ত থাকে, যা পেজিনেশনের শেষ প্রান্তে পৌঁছেছে কিনা তা নির্দেশ করে।
  • কোনও অ্যাক্টিভ লোড অপারেশন থাকলে, LoadState হল একটি LoadState.Loading অবজেক্ট।
  • কোনও সমস্যা হলে, LoadState হল একটি LoadState.Error অবজেক্ট।

আপনার LazyPagingItems র‍্যাপারের loadState প্রপার্টির মাধ্যমে এইসব স্টেট অ্যাক্সেস করুন। আপনি এই স্টেট দুটি উপায়ে ব্যবহার করতে পারবেন: মূল কন্টেন্টের দৃশ্যমানতা ম্যানেজ করা (যেমন, ফুল-স্ক্রিন রিফ্রেশ স্পিনার) অথবা সরাসরি আপনার LazyColumn স্ট্রিমে লোডিং আইটেম যোগ করা (যেমন, ফুটার স্পিনার)।

লিসনারের মাধ্যমে লোডিং স্টেট অ্যাক্সেস করুন

আপনার UI-তে লোডিং স্টেট মনিটর করতে, LazyPagingItems র‍্যাপারের প্রদান করা loadState প্রপার্টি ব্যবহার করুন। এটি একটি CombinedLoadStates অবজেক্ট রিটার্ন করে যা আপনাকে রিফ্রেশ, অ্যাপেন্ড বা প্রিপেন্ড ইভেন্টের জন্য লোডিং আচরণে প্রতিক্রিয়া জানাতে দেয়।

নিচের উদাহরণে, রিফ্রেশ (প্রাথমিক) লোডের বর্তমান অবস্থার উপর নির্ভর করে UI একটি লোডিং স্পিনার বা একটি ত্রুটি মেসেজ দেখায়:

@Composable
fun UserListScreen(viewModel: UserViewModel) {
  val pagingItems = viewModel.flow.collectAsLazyPagingItems()

  Box(modifier = Modifier.fillMaxSize()) {
    // Show the list content
    LazyColumn {
      items(pagingItems.itemCount) { index ->
        UserItem(pagingItems[index])
      }
    }

    // Handle the loading state
    when (val state = pagingItems.loadState.refresh) {
      is LoadState.Loading -> {
        CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
      }
      is LoadState.Error -> {
        ErrorButton(
          message = state.error.message ?: "Unknown error",
          onClick = { pagingItems.retry() },
          modifier = Modifier.align(Alignment.Center)
        )
      }
      else -> {} // No separate view needed for success/not loading
    }
  }
}

LazyPagingItems সম্পর্কে আরও তথ্যের জন্য, বড় ডেটা-সেট (পেজিং) দেখুন।

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

আপনি CombinedLoadStates অবজেক্ট ব্যবহার করে হেডারের জন্য প্রিপেন্ড স্টেট এবং ফুটারের জন্য অ্যাপেন্ড স্টেট মনিটর করতে পারবেন।

নিচের উদাহরণে, আরও ডেটা ফেচ করার সময় তালিকায় একটি প্রগ্রেস বার বা আবার চেষ্টা করার বোতাম তালিকায় নিচে দেখানো হয়েছে:

@Composable
fun UserList(viewModel: UserViewModel) {
  val pagingItems = viewModel.pager.flow.collectAsLazyPagingItems()

  LazyColumn {
    // 1. Header (Prepend state)
    // Useful if you support bidirectional paging or jumping to the middle
    item {
      val prependState = pagingItems.loadState.prepend
      if (prependState is LoadState.Loading) {
        LoadingItem()
      } else if (prependState is LoadState.Error) {
        ErrorItem(
          message = prependState.error.message ?: "Error",
          onClick = { pagingItems.retry() }
        )
      }
    }

    // 2. Main Data
    items(pagingItems.itemCount) { index ->
      UserItem(pagingItems[index])
    }

    // 3. Footer (Append state)
    // Shows when the user scrolls to the bottom and more data is loading
    item {
      val appendState = pagingItems.loadState.append
      if (appendState is LoadState.Loading) {
        LoadingItem()
      } else if (appendState is LoadState.Error) {
        ErrorItem(
          message = appendState.error.message ?: "Error",
          onClick = { pagingItems.retry() }
        )
      }
    }
  }
}

@Composable
fun LoadingItem() {
  Box(modifier = Modifier.fillMaxWidth().padding(16.dp), contentAlignment = Alignment.Center) {
    CircularProgressIndicator()
  }
}

@Composable
fun ErrorItem(message: String, onClick: () -> Unit) {
  Column(
    modifier = Modifier.fillMaxWidth().padding(16.dp),
    horizontalAlignment = Alignment.CenterHorizontally
  ) {
    Text(text = message, color = Color.Red)
    Button(onClick = onClick) { Text("Retry") }
  }
}

অতিরিক্ত লোডিং স্ট্যাটাস সংক্রান্ত তথ্য অ্যাক্সেস করুন

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

সুনির্দিষ্ট কন্ট্রোলের জন্য, যেমন লোকাল ডেটাবেস খালি থাকলে এবং নেটওয়ার্ক সিঙ্ক অ্যাক্টিভ থাকলে, শুধুমাত্র লোডিং স্পিনার দেখানোর জন্য, আপনার কম্পোজেবলের মধ্যে সরাসরি source এবং mediator প্রপার্টি অ্যাক্সেস করুন।

val loadState = pagingItems.loadState

val isSyncing = loadState.mediator?.refresh is LoadState.Loading

val isLocalEmpty = loadState.source.refresh is LoadState.NotLoading &&
                   pagingItems.itemSnapshotList.items.isEmpty()

if (isSyncing && isLocalEmpty) {
    FullScreenLoading()
} else {
    UserList(pagingItems)

    if (isSyncing) {
        TopOverlaySpinner()
    }
}

যেহেতু স্ক্রল করার সময় prepend বা append পরিবর্তন হলে, pagingItems.loadState একটি নতুন CombinedLoadStates অবজেক্ট এমিট করে, তাই derivedStateOf-এ remember(pagingItems)-এর সাথে রিফ্রেশ চেক র‍্যাপ করুন। এটি স্ক্রল-এজ আপডেটকে পেরেন্ট স্কোপ রিকম্পোজ করা থেকে আটকায় যা শুধুমাত্র ফুল-স্ক্রিন রিফ্রেশ ইন্ডিকেটর টগল করে:

val showFullScreenLoading by remember(pagingItems) {
  derivedStateOf {
    // Avoid recomposing on prepend or append updates while scrolling, and
    // avoid covering existing items during a background refresh.
    pagingItems.loadState.source.refresh is LoadState.Loading &&
      pagingItems.itemCount == 0
  }
}

if (showFullScreenLoading) {
  Box(
    modifier = Modifier.fillMaxSize(),
    contentAlignment = Alignment.Center
  ) {
    CircularProgressIndicator()
  }
}

লোড স্টেট পরিবর্তনের জন্য প্রতিক্রিয়া জানান

লোড স্টেট পরিবর্তনের উপর ভিত্তি করে আপনাকে এককালীন সাইড এফেক্ট ট্রিগার করতে হতে পারে, যেমন তালিকার একদম উপরে স্ক্রল করা বা রিফ্রেশ সম্পূর্ণ বা ব্যর্থ হলে Snackbar দেখানো।

স্ট্যাটাসের পরিবর্তন স্ট্রিম হিসেবে দেখতে LaunchedEffect-এর মধ্যে snapshotFlow ব্যবহার করুন। এটি আপনাকে নির্দিষ্ট ইভেন্ট আলাদা করতে filter, distinctUntilChanged এবং distinctUntilChangedBy-এর মতো স্ট্যান্ডার্ড Flow অপারেটর প্রয়োগ করতে দেয়।

val listState = rememberLazyListState()

LaunchedEffect(pagingItems) {
  // 1. Convert the state to a Flow
  snapshotFlow { pagingItems.loadState.refresh }
    // 2. Filter for the specific event (Refresh completed successfully)
    .distinctUntilChanged()
    .filter { it is LoadState.NotLoading }
    .collect {
      // 3. Trigger the side effect
      listState.animateScrollToItem(0)
    }
}

একইভাবে, আপনি loadState-এ distinctUntilChangedBy ব্যবহার করে Snackbar ত্রুটি দেখাতে পারেন, শুধুমাত্র যখন refresh স্টেট LoadState.Error-এ ট্রানজিশন করে:

val snackbarHostState = remember { SnackbarHostState() }

// Key on pagingItems rather than loadState so state updates do not restart the
// coroutine and reset distinctUntilChangedBy.
LaunchedEffect(pagingItems) {
  snapshotFlow { pagingItems.loadState }
    // Ignore prepend and append updates so scrolling does not re-trigger the
    // refresh error Snackbar.
    .distinctUntilChangedBy { it.refresh }
    .filter { it.refresh is LoadState.Error }
    .collect { loadState ->
      val error = (loadState.refresh as LoadState.Error).error
      val result = snackbarHostState.showSnackbar(
        message = "Data failed to load: ${error.localizedMessage ?: "Unknown error"}",
        actionLabel = "Retry"
      )
      if (result == SnackbarResult.ActionPerformed) {
        pagingItems.retry()
      }
    }
}

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

পেজিং লাইব্রেরি ও লোডিং স্টেট সম্পর্কে আরও তথ্যের জন্য, নিম্নলিখিত রিসোর্স দেখুন।

ডকুমেন্টেশন

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