ניהול והצגה של מצבי הטעינה

ספריית Paging עוקבת אחרי הסטטוס של בקשות הטעינה של נתונים בדפים, וחושפת אותו באמצעות המחלקה LoadState.

אות LoadState נפרד מסופק לכל LoadType ולכל סוג של מקור נתונים (PagingSource או RemoteMediator). האובייקט CombinedLoadStates שמסופק על ידי המאזין מספק מידע על מצב הטעינה מכל האותות האלה. אתם יכולים להשתמש במידע המפורט הזה כדי להציג למשתמשים את אינדיקטורי הטעינה המתאימים.

מצבי טעינה

ספריית Paging חושפת את מצב הטעינה לשימוש בממשק המשתמש דרך האובייקט LoadState. אובייקטים מסוג LoadState יכולים להיות באחת משלוש צורות, בהתאם למצב הטעינה הנוכחי:

  • אם אין פעולת טעינה פעילה ואין שגיאה, אז LoadState הוא אובייקט מסוג LoadState.NotLoading. תת-המחלקה הזו כוללת גם את המאפיין endOfPaginationReached, שמציין אם הגעתם לסוף העמודים.
  • אם יש פעולת טעינה פעילה, אז LoadState הוא אובייקט LoadState.Loading.
  • אם יש שגיאה, LoadState הוא אובייקט LoadState.Error.

אפשר לגשת למצבים האלה דרך המאפיין loadState של ה-wrapper של LazyPagingItems. אפשר להשתמש במצב הזה בשתי דרכים: טיפול בנראות של התוכן הראשי (כמו ספינר רענון במסך מלא) או הוספת פריטי טעינה ישירות לסטרים של LazyColumn (כמו ספינר בכותרת התחתונה).

גישה למצב הטעינה באמצעות מאזין (listener)

כדי לעקוב אחרי מצב הטעינה בממשק המשתמש, משתמשים במאפיין loadState שסופק על ידי ה-wrapper‏ LazyPagingItems. הפעולה הזו מחזירה אובייקט CombinedLoadStates שמאפשר לכם להגיב להתנהגות הטעינה של אירועי רענון, הוספה לסוף או הוספה להתחלה.

בדוגמה הבאה, בממשק המשתמש מוצג סימן גרפי של טעינה או הודעת שגיאה, בהתאם לסטטוס הנוכחי של טעינת הרענון (הראשונית):

@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). כך אפשר למנוע מעדכונים של scroll-edge ליצור מחדש היקף הורה שרק מחליף את האינדיקטור של רענון המסך המלא:

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 ועל מצבי טעינה, אפשר לעיין במקורות המידע הבאים.

תיעוד

צפייה בתוכן