إدارة حالات التحميل وتقديمها

تتتبّع مكتبة Paging حالة طلبات تحميل البيانات المقسّمة إلى صفحات وتعرضها من خلال الفئة LoadState.

يتم توفير إشارة LoadState منفصلة لكل LoadType ونوع مصدر بيانات (إما PagingSource أو RemoteMediator). يوفّر عنصر CombinedLoadStates الذي يوفّره المستمع معلومات عن حالة التحميل من جميع هذه الإشارات. يمكنك استخدام هذه المعلومات التفصيلية لعرض مؤشرات التحميل المناسبة للمستخدمين.

جارٍ تحميل الولايات

تعرض مكتبة Paging حالة التحميل لاستخدامها في واجهة المستخدم من خلال الكائن LoadState. تتّخذ عناصر LoadState أحد الأشكال الثلاثة التالية حسب حالة التحميل الحالية:

  • إذا لم تكن هناك عملية تحميل نشطة ولم يحدث أي خطأ، تكون السمة LoadState عبارة عن عنصر LoadState.NotLoading. تتضمّن هذه الفئة الفرعية أيضًا السمة endOfPaginationReached التي تشير إلى ما إذا تم الوصول إلى نهاية التقسيم على صفحات.
  • إذا كانت هناك عملية تحميل نشطة، تكون LoadState عنصرًا من النوع LoadState.Loading.
  • في حال حدوث خطأ، يكون LoadState كائن LoadState.Error.

يمكنك الوصول إلى هذه الحالات من خلال السمة loadState في برنامج تضمين LazyPagingItems. يمكنك استخدام هذه الحالة بطريقتَين: معالجة مستوى ظهور المحتوى الرئيسي (مثل مؤشر تحميل بملء الشاشة) أو إدراج عناصر التحميل مباشرةً في خلاصة LazyColumn (مثل مؤشر تحميل في التذييل).

الوصول إلى حالة التحميل باستخدام أداة معالجة الأحداث

لمراقبة حالة التحميل في واجهة المستخدم، استخدِم السمة loadState التي يوفّرها برنامج تضمين LazyPagingItems. يؤدي ذلك إلى عرض عنصر CombinedLoadStates يتيح لك التفاعل مع سلوك التحميل لأحداث إعادة التحميل أو الإلحاق أو الإضافة إلى البداية.

In the following example, the UI displays a loading spinner or an error message depending on the current state of the refresh (initial) load:

@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). قد يؤدي ذلك إلى ظهور مؤشر تحميل لفترة وجيزة في واجهة المستخدم حتى عندما تكون البيانات المخزّنة مؤقتًا متاحة على الفور.

للتحكّم بدقة، مثل عرض مؤشر تحميل دوّار فقط عندما تكون قاعدة البيانات المحلية فارغة وتكون مزامنة الشبكة نشطة، يمكنك الوصول إلى السمتَين 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()
    }
}

بما أنّ pagingItems.loadState يُصدر كائن CombinedLoadStates جديدًا كلما تغيّر prepend أو append أثناء الانتقال للأعلى أو للأسفل، يمكنك تضمين عملية التحقّق من إعادة التحميل في 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 عند اكتمال عملية إعادة التحميل أو تعذّرها.

استخدِم snapshotFlow داخل LaunchedEffect لمراقبة تغييرات الحالة كتدفق. يتيح لك ذلك تطبيق عوامل تشغيل Flow العادية، مثل filter وdistinctUntilChanged وdistinctUntilChangedBy لعزل أحداث معيّنة.

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

وبالمثل، يمكنك استخدام distinctUntilChangedBy في loadState لعرض خطأ 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()
      }
    }
}

مراجع إضافية

لمزيد من المعلومات حول مكتبة Paging وحالات التحميل، يُرجى الاطّلاع على المراجع التالية.

الوثائق

محتوى المشاهدات