Android 利用Jetpack Compose +Paging3+swiperefresh实现分页加载,下拉上拉效果

3,484 阅读4分钟

简述:

Paging是google官方推出的一个Android分页加载库,配合RecyclerView可以很方便实现RecyelerView的Footer和Header。可以使用在Java或Kotlin项目中,借助PagingDataAdapter帮助RecyclerView实现分页加载。

Paging中最重要的两个类是 PagingSourceLoadStatePagingDataAdapter:

  • PagingDataAdapter是对RecyclerView.Adapter的封装, 管理着数据的消费。
    
  • PaingSource负责数据分页,管理着数据的生产。
    
  • LoadState可以实时监控加载状态,可以根据它的值做一些UI动效:Loading,NoLoading, Refresh(初次加载或刷新),Append(加载下一页)
    

Jetpack Compose 提供了Flutter类似的功能API,非常方便,简单举个例子:

   TextButton(
       onClick = { retry() }, 
       modifier = Modifier .padding(20.dp) .width(80.dp) .height(30.dp), 
       shape = RoundedCornerShape(6.dp), 
       contentPadding = PaddingValues(3.dp),
       colors = textButtonColors(backgroundColor = gray300), 
       elevation = elevation( 
           defaultElevation = 2.dp,
           pressedElevation = 4.dp, 
           ),
      ) {
          Text(text = "请重试", color = gray600) 
       }


#### 本文主要介绍JetCompose 项目中 Paging3分页的使用,下拉刷新上拉加载动效。效果如下:

11-20_225027

一、 Paging3分页加载

  1. 引入依赖:

      //Paging 3.0
      implementation 'androidx.paging:paging-compose:1.0.0-alpha14'
      implementation "androidx.paging:paging-runtime-ktx:3.1.0-rc01"
    
  2. Paging实现分页加载,简单快捷可定制,内部管理了分页逻辑和异常处理,而分页规则需要自己定义。主要代码:

    
    class ExamSource(private val repository: Repository) : PagingSource<Int, Question>() {
    
        private val TAG = "--ExamSource"
    
        override fun getRefreshKey(state: PagingState<Int, Question>): Int? {
            return null
        }
    
        override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Question> {
    
            return try {
                val currentPage = params.key ?: 1
                val pageSize = params.loadSize
                Log.d(TAG, "currentPage: $currentPage")
                Log.d(TAG, "pageSize: $pageSize")
    
                // 传入当前页码,每页大小,然后请求数据。网络请求封装在repository
                val responseList = repository.getExamList(currentPage, pageSize = pageSize)
                    .result?.resultData?.questionList ?: emptyList<Question>()
    
                // 加载分页
                val everyPageSize = 4
                val initPageSize = 8
                // 前一页
                val preKey = if (currentPage == 1) null else currentPage.minus(1)
                // 下一页
                var nextKey: Int? = if (currentPage == 1) {
                    initPageSize / everyPageSize
                } else {
                    currentPage.plus(1)
                }
                Log.d(TAG, "preKey: $preKey")
                Log.d(TAG, "nextKey: $nextKey")
                if (responseList.isEmpty()) {
                    nextKey = null
                }
                Log.d(TAG, "final nextKey: $nextKey")
    
                LoadResult.Page(
                    data = responseList,
                    prevKey = preKey,
                    nextKey = nextKey
                )
            } catch (e: Exception) {
                e.printStackTrace()
                LoadResult.Error(e)
            }
        }
    }
    

二、 沉浸式状态栏

// 状态栏相关
implementation "com.google.accompanist:accompanist-insets:0.21.2-beta"
implementation "com.google.accompanist:accompanist-insets-ui:0.21.2-beta"
implementation "com.google.accompanist:accompanist-systemuicontroller:0.21.2-beta"
    1. 设置 WindowCompat.setDecorFitsSystemWindows(window, false),默认是false
 /**
 * 设置decorView是否适配windowsetscompat的WindowInsetsCompat根视图。
 * 若置为false,将不适配内容视图的insets,只会适配内容视图。
 * 请注意:在应用程序中使用View.setSystemUiVisibility(int) API可能会与此方法冲突。应停止使用View.setSystemUiVisibility(int)。
 */
 public static void setDecorFitsSystemWindows(@NonNull Window window,
             final boolean decorFitsSystemWindows) {
         if (Build.VERSION.SDK_INT >= 30) {
             Impl30.setDecorFitsSystemWindows(window, decorFitsSystemWindows);
         } else if (Build.VERSION.SDK_INT >= 16) {
             Impl16.setDecorFitsSystemWindows(window, decorFitsSystemWindows);
         }
     }
    1. 状态栏透明 拿到SystemUiController的setStatusBarColor()方法来改变状态栏,也可以修改底部导航栏颜色。

  setContent {
       UseComposeTheme {
            // 状态栏改为透明,参数:color(状态栏颜色),darkIcons(是否为深色图标)
             rememberSystemUiController().setStatusBarColor(
                   Color.Transparent, darkIcons = MaterialTheme.colors.isLight
             )
 
             // 底部导航栏颜色
             rememberSystemUiController().setNavigationBarColor(
                  Color.Transparent, darkIcons = MaterialTheme.colors.isLight
             )
 
             // ...
       }
  }
    1. 调整以适配状态栏高度

    需要用到ProvideWindowInsets,在显示内容的外围包一层ProvideWindowInsets,在Theme节点以下包裹ProvideWindowInsets以便取得状态栏的高度,也就是保证能正常获得statusBarsHeight()的值,给页面设置等高的Spacer,从而留出状态栏的高度。

     setContent {
          UseComposeTheme {
                     // 加入ProvideWindowInsets
                     ProvideWindowInsets {
                         // 状态栏改为透明
                         rememberSystemUiController().setStatusBarColor(
                             Color.Transparent, darkIcons = MaterialTheme.colors.isLight
                         )
    
                         Surface(color = MaterialTheme.colors.background) {
                             Scaffold(
                                 modifier = Modifier.fillMaxSize()
                             ) {
                                 Column {
                                       // 填充留白状态栏高度
                                      Spacer(modifier = Modifier
                                          .statusBarsHeight()
                                          .fillMaxWidth()
                                      )
    
                                      // 你的业务 Composable
                                 )
                             }
                         }
                     }
                 }
          }
    
    • 效果
修改前FitsSystemWindows :false修改颜色适配高度
11-20_171011.jpg11-20_171902.jpg11-20_173513.jpg11-20_184244.jpg

三、 下拉刷新

构建错误:是com.google.accompanist:accompanist:xxx 相关库的版本不兼容,需要依赖相同的版本

   21:35:51.503 7789-7789/com.jesen.driverexampaging E/AndroidRuntime: FATAL EXCEPTION: main
       Process: com.jesen.driverexampaging, PID: 7789
       java.lang.NoSuchMethodError: No interface method startReplaceableGroup(ILjava/lang/String;)V in class Landroidx/compose/runtime/Composer; or its super classes (declaration of 'androidx.compose.runtime.Composer' appears in /data/app/~~4FT0iYbXWuoHva-X3Y0lBg==/com.jesen.driverexampaging-4uB2hZ7cDclbzM5qgmkttA==/base.apk)
           at com.google.accompanist.swiperefresh.SwipeRefreshKt.rememberSwipeRefreshState(Unknown Source:5)
           at com.jesen.driverexampaging.common.SwipeRefreshListKt.SwipeRefreshList(SwipeRefreshList.kt:31)
           at com.jesen.driverexampaging.ui.composeview.RefreshExamListScreenKt.RefreshExamListScreen(RefreshExamListScreen.kt:33)
           at com.jesen.driverexampaging.Main2Activity$onCreate$1$1$1$1$1.invoke(Main2Activity.kt:54)
           at com.jesen.driverexampaging.Main2Activity$onCreate$1$1$1$1$1.invoke(Main2Activity.kt:47)
           at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:116)
           at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
           at androidx.compose.material.ScaffoldKt$ScaffoldLayout$1$1$1$bodyContentPlaceables$1.invoke(Scaffold.kt:316)
           at androidx.compose.material.ScaffoldKt$ScaffoldLayout$1$1$1$bodyContentPlaceables$1.invoke(Scaffold.kt:314)
  1. 基本用法:

    • 接入依赖:

      // 下拉刷新
          implementation "com.google.accompanist:accompanist-swiperefresh:0.21.2-beta"
      
    • 设置下拉刷新加载更多并判断状态

      @Composable
      fun refreshLoadUse(viewModel: ExamViewModel) {
          // Swipe 的状态
          val refreshState = rememberSwipeRefreshState(isRefreshing = false)
          val collectAsLazyPagingItems = viewModel.examList.collectAsLazyPagingItems()
      
          SwipeRefresh(state = refreshState, onRefresh = {
              collectAsLazyPagingItems.refresh()
          }) {
              LazyColumn(
                  modifier = Modifier
                      .fillMaxWidth()
                      .fillMaxHeight(),
                  content = {
                      itemsIndexed(collectAsLazyPagingItems) { _, refreshData ->//每个item的展示
                          Box(
                              modifier = Modifier
                                  .padding(horizontal = 14.dp, vertical = 4.dp)
                                  .fillMaxWidth()
                                  .height(50.dp)
                                  .background(Color.Green, shape = RoundedCornerShape(8.dp))
                                  .border(
                                      width = 1.dp,
                                      color = Color.Red,
                                      shape = RoundedCornerShape(8.dp)
                                  )
                                  .padding(start = 10.dp),
                              contentAlignment = Alignment.CenterStart
                          ) {
                              Text(text = refreshData?.data ?: "")
                          }
                      }
                      // append 标识非第一页,也就是指下一页或加载更多
                      when (collectAsLazyPagingItems.loadState.append) {
                          is LoadState.Loading -> {
                              //加载中的尾部item展示
                              item {
                                  Box(
                                      modifier = Modifier
                                          .fillMaxWidth()
                                          .height(50.dp),
                                      contentAlignment = Alignment.Center
                                  ) {
                                      Text(text = "加载中。。。")
                                  }
                              }
                          }
                          is LoadState.Error -> {
                              //更多,加载错误展示的尾部item
                              item {
                                  Box(
                                      modifier = Modifier
                                          .fillMaxWidth()
                                          .height(50.dp),
                                      contentAlignment = Alignment.Center
                                  ) {
                                      Text(text = "--加载错误--")
                                  }
                              }
                          }
                      }
                  }
              )
          }
      }
      
  2. 简单封装

    参数1:LazyPagingItems包装的请求结果,可以存储在ViewModel,从ViewMode获取
    参数2:列表内容 listContent 需要外部传入需要携带上下文LazyListScope,可复用
    
    /**
     * 下拉加载封装
     *
     * implementation "com.google.accompanist:accompanist-swiperefresh:xxx"
     * */
    @Composable
    fun <T : Any> SwipeRefreshList(
        collectAsLazyPagingItems: LazyPagingItems<T>,
        listContent: LazyListScope.() -> Unit,
    ) {
    
        val rememberSwipeRefreshState = rememberSwipeRefreshState(isRefreshing = false)
    
        SwipeRefresh(
            state = rememberSwipeRefreshState,
            onRefresh = { collectAsLazyPagingItems.refresh() }
        ) {
    
            rememberSwipeRefreshState.isRefreshing =
                collectAsLazyPagingItems.loadState.refresh is LoadState.Loading
                             
            // lazyColumn的状态属性
            val lazyListState = rememberLazyListState()
    
            // 定义一个协程作用域用来跳到列表顶部
            val coroutineScope = rememberCoroutineScope() 
    
            LazyColumn(
                state = lazyListState,
                modifier = Modifier
                    .fillMaxWidth()
                    .fillMaxHeight(),
    
                ) {
                
                    // 具体的列表内容,从父节点参数传入
                    listContent()
                
                    collectAsLazyPagingItems.apply {
                        when {
                            loadState.append is LoadState.Loading -> {
                                //加载更多,底部loading
                                item { LoadingItem() }
                            }
                            loadState.append is LoadState.Error -> {
                                //加载更多异常
                                item {
                                    ErrorMoreRetryItem() {
                                        collectAsLazyPagingItems.retry()
                                    }
                                }
                            }
                        
                        
                          loadState.append == LoadState.NotLoading(endOfPaginationReached = true) -> {
                                // 已经没有更多数据了
                                item {
                                        NoMoreDataFindItem(onClick = {
                                             // 点击事件 跳到列表顶部
                                             coroutineScope.launch {
                                                    lazyListState.animateScrollToItem(0)
                                             }
                                         })
                               }
                         }
    
                        
                        loadState.refresh is LoadState.Error -> {
                            if (collectAsLazyPagingItems.itemCount <= 0) {
                                // 刷新的时候,如果itemCount小于0,第一次加载异常
                                item {
                                    ErrorContent() {
                                        collectAsLazyPagingItems.retry()
                                    }
                                }
                            } else {
                                item {
                                    ErrorMoreRetryItem() {
                                        collectAsLazyPagingItems.retry()
                                    }
                                }
                            }
                        }
                        loadState.refresh is LoadState.Loading -> {
                            // 第一次加载且正在加载中
                            if (collectAsLazyPagingItems.itemCount == 0) {
                            }
                        }
                    }
                }
    
            }
        }
    }
    
    /**
     * 底部加载更多失败处理
     * */
    @Composable
    fun ErrorMoreRetryItem(retry: () -> Unit) {
        Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
            TextButton(
                onClick = { retry() },
                modifier = Modifier
                    .padding(20.dp)
                    .width(80.dp)
                    .height(30.dp),
                shape = RoundedCornerShape(6.dp),
                contentPadding = PaddingValues(3.dp),
                colors = textButtonColors(backgroundColor = gray300),
                elevation = elevation(
                    defaultElevation = 2.dp,
                    pressedElevation = 4.dp,
                ),
            ) {
                Text(text = "请重试", color = gray600)
            }
        }
    }
    
    /**
     * 页面加载失败处理
     * */
    @Composable
    fun ErrorContent(retry: () -> Unit) {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(top = 100.dp),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Image(
                modifier = Modifier.padding(top = 80.dp),
                painter = painterResource(id = R.drawable.ic_default_empty),
                contentDescription = null
            )
            Text(text = "请求失败,请检查网络", modifier = Modifier.padding(8.dp))
            TextButton(
                onClick = { retry() },
                modifier = Modifier
                    .padding(20.dp)
                    .width(80.dp)
                    .height(30.dp),
                shape = RoundedCornerShape(10.dp),
                contentPadding = PaddingValues(5.dp),
                colors = textButtonColors(backgroundColor = gray300),
                elevation = elevation(
                    defaultElevation = 2.dp,
                    pressedElevation = 4.dp,
                )
                //colors = ButtonDefaults
            ) { Text(text = "重试", color = gray700) }
        }
    }
    
    /**
     * 底部加载更多正在加载中...
     * */
    @Composable
    fun LoadingItem() {
        Row(
            modifier = Modifier
                .height(34.dp)
                .fillMaxWidth()
                .padding(5.dp),
            horizontalArrangement = Arrangement.Center
        ) {
            CircularProgressIndicator(
                modifier = Modifier
                    .size(24.dp),
                color = gray600,
                strokeWidth = 2.dp
            )
            Text(
                text = "加载中...",
                color = gray600,
                modifier = Modifier
                    .fillMaxHeight()
                    .padding(start = 20.dp),
                fontSize = 18.sp,
            )
        }
    }
    
      /**
     * 没有更多数据了
     * */
     @Composable
     fun NoMoreDataFindItem(onClick: () -> Unit) {
         Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
                TextButton(
                  onClick = { onClick() },
                  modifier = Modifier
                  .padding(20.dp)
                  .width(80.dp)
                  .height(30.dp),
                  shape = RoundedCornerShape(6.dp),
                  contentPadding = PaddingValues(3.dp),
                  colors = textButtonColors(backgroundColor = gray300),
                  elevation = elevation(
                      defaultElevation = 2.dp,
                      pressedElevation = 4.dp,
                  ),
          ) {
              Text(text = "已经没有更多数据啦 ~~ Click to top", color = gray600)
          }
        }
      }
    
    • 用法:

      1. 列表布局:

        /**
         * 首页列表加载 ---下拉刷新,加载更多动效
         * */
        @Composable
        fun RefreshExamListScreen(
            viewModel: ExamViewModel,
            context: Context,
        ) {
        
            val collectAsLazyPagingIDataList = viewModel.examList.collectAsLazyPagingItems()
        
            SwipeRefreshList(
                collectAsLazyPagingItems = collectAsLazyPagingIDataList
            ) {
        
                itemsIndexed(collectAsLazyPagingIDataList) { index, data ->
                    // 列表Item,对应的实体数据是data
                    QItemView(
                        index = index,
                        que = data,
                        onClick = { Toast.makeText(context, "ccc", Toast.LENGTH_SHORT).show() },
                    )
                }
            }
        }
        
      2. ViewModel,包括Paging3的配置:

      class ExamViewModel : ViewModel() {
      
          val examList = Pager(
              config = PagingConfig(
                  pageSize = 4,    // 每一页个数,必须
                  initialLoadSize = 8, // 第一次加载数量,非必须
                  prefetchDistance = 2, // 距离下一页请求的距离,非必须
              )
          ) {
              // 此类处理了分页功能,前有提到
              ExamSource(Repository)
          }.flow.cachedIn(viewModelScope)
      }
      

代码示例:驾校考题列表 https://github.com/Jesen0823/UseCompose/tree/main/DriverExamPaging