Android Compose——Paging3的使用以及利用泛型进行封装

效果视频

简述

本Demo采用Hilt+Retrofit+Paging3完成,主要为了演示paging3分页功能的使用,下列为Demo所需要的相关依赖

 //retrofit
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

    //paging
    implementation 'androidx.paging:paging-runtime:3.1.1'
    implementation 'androidx.paging:paging-compose:1.0.0-alpha14'

    //Dagger - Hilt
    implementation("com.google.dagger:hilt-android:2.44")
    kapt("com.google.dagger:hilt-android-compiler:2.44")

    // Compose dependencies
    implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1"
    implementation "androidx.hilt:hilt-navigation-compose:1.0.0"

    // Coroutines
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'

Hilt+Retrofit

访问接口

定义需要访问的接口,此接口是Github api,suspend字段用于提示后续引用,此内容需要在协程中使用

interface GithubService {
    @GET("search/repositories?sort=stars&q=Android")
    suspend fun queryGithubAsync(@Query("per_page")number:Int, @Query("page") page:Int):DetailsBean
}

网络实例

提供三个实例,最终外部需要引用的的为UseCase的实例,具体Hilt依赖注入此处不予说明,有意者可参考Hilt依赖注入

@Module
@InstallIn(SingletonComponent::class)
object AppModule {

    const val BASE_URL:String = "https://api.github.com/"

    @Singleton
    @Provides
    fun providerRetrofit():Retrofit{
        return Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    @Singleton
    @Provides
    fun providerGithubService(retrofit: Retrofit): GithubService {
        return retrofit.create(GithubService::class.java)
    }

    @Singleton
    @Provides
    fun providerUseCase(service: GithubService):UseCase{
        return UseCase(GetProjects(service))
    }
}

在Hilt提供的实例中,UseCase中实现了访问网络接口的任务

data class UseCase(
    val getProjects: GetProjects
)
class GetProjects(private val service: GithubService) {
    suspend operator fun invoke(number:Int,page:Int): DetailsBean {
        return service.queryGithubAsync(number, page)
    }
}

PagingSource

我们主要实现load方法;其中page为当前内容页数,pageSize为每页需要加载的内容数量(可在外部进行定义),repository为获取的网络数据实体,previousPage为前一页,此处做了一个判断,如果为第一页时,则返回null,否则进行滑动至上一页;nextPage为下一页, LoadResult.Page为分页加载所需的内容; LoadResult.Error可捕获异常

class DataPagingSource(private val useCase: UseCase):PagingSource<Int,DetailBean>() {
    override fun getRefreshKey(state: PagingState<Int, DetailBean>): Int? = null

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, DetailBean> {
       return try {
           val page = params.key ?: 1 //当前页,默认第一页
           val pageSize = params.loadSize //每页数据条数
           val repository = useCase.getProjects(page,pageSize) //获取的数据源
           val repositoryItem = repository.beans //获取的数据列表
           val previousPage = if (page > 1) page - 1 else null //前一页
           val nextPage = if (repositoryItem.isNotEmpty()) page+1 else null //下一页
           Log.d("hiltViewModel","page=$page size=$pageSize")
           LoadResult.Page(repositoryItem,previousPage,nextPage)
       }catch (e:Exception){
           LoadResult.Error(e)
       }
    }
}

ViewModel

在构造函数中调用Hilt构造的实例;其中getData方法为获取分页的数据,返回为Flow<PagingData<DetailBean>>类型,其中Flow<PagingData<...>>外部为固定写法,内部可根据需要自行定义,然后PagingConfig的配置中,我们需要配置pageSizeinitialLoadSize,如果不定义后者,则通过每页内容数量会是pageSize的三倍,然后添加我们上述创建的PagingSource;最后转化为流,然后置于协程中,它缓存PagingData,以便此流的任何下游集合都将共享相同的数据

@HiltViewModel
class HomeViewModel @Inject constructor(private val useCase: UseCase):ViewModel() {
    val PAGE_SIZE = 10

    fun getData():Flow<PagingData<DetailBean>>{
        return Pager(
            config = PagingConfig(pageSize = PAGE_SIZE, initialLoadSize = PAGE_SIZE),
            pagingSourceFactory = { DataPagingSource(useCase) }
        ).flow.cachedIn(viewModelScope)
    }
}

View

获取ViewModel中的数据

  val datas = viewModel.getData().collectAsLazyPagingItems()

同时如果需要添加底部刷新状态栏、数据错误等标识,需要监听loadState,其状态总共分为五种:

  • refresh:第一次加载数据触发
  • prepend:滑动上一页触发
  • append:滑动下一页触发
  • source:对应于[PagingSource]中的加载
  • mediator:对应于来自[RemoteMediator]的加载
    我们此处主要使用refreshappend
    其中,在refresh中进行监听,如果然后数据为null,则显示全屏错误提示,此处为第一次加载数据;
    然后,在append中监听loadingError两种状态,在其loading是显示底部加载状态,在Error中显示底部错误提示,此处不同于refreshError状态,因为有了数据,就不在需要显示全屏错误提示,在数据列表底部显示错误状态栏即可

@Composable
fun GithubList(viewModel: HomeViewModel = hiltViewModel()){
    val datas = viewModel.getData().collectAsLazyPagingItems()
    LazyColumn(
        verticalArrangement = Arrangement.spacedBy(10.dp),
        modifier = Modifier
            .background(grey)
            .fillMaxSize()
            .padding(10.dp)
    ){

        when(datas.loadState.refresh){
            is LoadState.Loading-> {item {  loading() }}
            is LoadState.Error-> {
                if (datas.itemCount <= 0){
                    item{
                        /**
                         * 全屏显示错误*/
                        failedScreen() {
                            datas.retry()
                        }
                    }
                }
            }
        }

        itemsIndexed(datas){ _, value ->
            if (value != null){
                GithubItem(value)
            }else{
                empty {
                    datas.retry()
                }
            }
        }

        when(datas.loadState.append){
            is LoadState.NotLoading-> {}
            is LoadState.Loading-> {
                item {
                    loading()
                }
            }
            is LoadState.Error-> {
                if (datas.itemCount > 0){
                    /**
                     * 底部显示加载错误*/
                    item { failed(){datas.retry()} }
                }
            }
        }


    }
}

利用泛型封装一个通用的Paging

上述所阐述的内容是建立一个符合特定类的Paging,随着我们的接口增多,拥有不同的分页加载接口,显然,给每一个接口建立一个DataPagingSource不是最佳的方法,下列简述一种利用协程和泛型建立一种通用的Paging

封装DataPagingSource

下列和DataPagingSource和上述基本没有太大差距,变化在于,将特定返回的数据类型通过泛型代替,同时为了应对不同接口的传入,故定义一个block回调方法,其中拥有俩个形参(可以根据自己需求改变),此处我返回的是offsetlimit

class BaseDataPagingSource<T:Any>(
    private val block:suspend (Int,Int)->List<T>
):PagingSource<Int, T>() {
    override fun getRefreshKey(state: PagingState<Int, T>): Int? {
        return state.anchorPosition?.let { anchorPosition ->
            state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
                ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
        }
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, T> {
        val offset = params.key ?: 0
        val limit = params.loadSize
        return try {
            val response = block(offset,limit)
            val previousPage = if (offset > 0) offset - 1 else null //前一页
            val nextPage = offset + 1 //下一页
            LoadResult.Page(
                data = response,
                prevKey = previousPage,
                nextKey = nextPage
            )
        } catch (e:Exception){
            LoadResult.Error(e)
        }
    }
}

封装Pager

同理,将建立的Pager也进行封装,将接口通过block引入,需注意的是block方法需要加suspend修饰,因为我们的接口一般同需要用suspend修饰,所以此处也需要声明,不然外面引用时会出现错误

const val PAGE_SIZE:Int = 20

fun <T : Any> creator(
    pageSize: Int = PAGE_SIZE,
    enablePlaceholders: Boolean = false,
    block: suspend (Int, Int) -> List<T>
): Pager<Int, T> = Pager(
    config = PagingConfig(
        pageSize = pageSize,
        enablePlaceholders = enablePlaceholders,
        initialLoadSize = pageSize
    ),
    pagingSourceFactory = { BaseDataPagingSource(block = block) }
)

运用

在ViewModel中我们可以定义一个如下方法,以供外部调用,以最后一行作为结果传入上方的BaseDataPagingSourceLoadResult.Page

 fun getSearchSongResult(keywords:String) = creator { offset, limit ->
        val response = service.getSearchSongResult(keywords = keywords,offset = offset*limit,limit = limit)
        response.result.songs
    }.flow.cachedIn(viewModelScope)

在外部就和普通调用一致,之后就可以将数据插入列表组件中

val songs = viewModel.getSearchSongResult(value.keyword).collectAsLazyPagingItems()
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FranzLiszt1847

嘟嘟嘟嘟嘟

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值