1.数据源
这里采用GitHub REST API的搜索的api来作为数据源:
https://api.github.com/search/repositories?q=compose+language:kotlin&sort=stars&order=desc
大家可以用AS的JsonToKotlinClass插件来把这个响应数据生成data class,这里就不贴代码了。
2.依赖项
//network & serialization
implementation "com.google.code.gson:gson:2.9.0"
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
implementation "com.squareup.retrofit2:retrofit:2.9.0"//
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.3'
//swiperefresh 的compose 版本
implementation "com.google.accompanist:accompanist-swiperefresh:0.23.1"
// paging 3 的compose 版本
implementation "androidx.paging:paging-compose:1.0.0-alpha14"
//这个可以在Compose中得到viewmodel
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.4.1"
//coil Compose 图片加载库
implementation "io.coil-kt:coil-compose:2.0.0-rc01"
compose_version用的是1.1.1版本的这里就不贴了
3.Api调用
interface GithubService {
/**
* 用户列表
*/
@GET("users")
suspend fun getUsers(@Query("since") userId: Int,@Query("per_page") pageSize: Int = 30): List<UserEntity>
/**
* 仓库搜索
*/
@GET("search/repositories")
suspend fun searchRepositors(
@Query("q") words: String,
@Query("page") page: Int = 1,
@Query("per_page") pageSize: Int = 30,
@Query("sort") sort:String = "stars",
@Query("order") order: String = "desc",
): RepositorResult
}
private val service: GithubService by lazy {
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply { setLevel(HttpLoggingInterceptor.Level.BODY) })
.build()
val retrofit = Retrofit.Builder()
.baseUrl("https://api.github.com")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
retrofit.create(GithubService::class.java)
}
fun getGithubService() = service
Retrofit 2.6.0版本及以上就支持kotlin协程了
4.使用paging3 进行分页:
class MyPagingSource(
val githubService: GithubService = getGithubService(),
val words: String,
) : PagingSource<Int, RepositorItem>() {
override fun getRefreshKey(state: PagingState<Int, RepositorItem>): Int? {
return state.anchorPosition?.let {
val anchorPage = state.closestPageToPosition(it)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, RepositorItem> {
try {
val nextPage: Int = params.key ?: 1
val repositorRst = githubService.searchRepositors(words, nextPage, 20)
return LoadResult.Page(
data = repositorRst.items,
prevKey = if (nextPage == 1) null else nextPage - 1,
nextKey = if (repositorRst.items.isEmpty()) null else nextPage + 1
)
}catch (e:Exception){
return LoadResult.Error(e)
}
}
}
5.viewModel:
class GithubViewModel:ViewModel() {
val repositorPager = Pager(config = PagingConfig(pageSize = 6)){
MyPagingSource(getGithubService(),"compose")
}.flow.cachedIn(viewModelScope)
}
6.最后是Compose:
@Composable
fun ListContent() {
val viewModel: GithubViewModel = viewModel()
val lazyPagingItems = viewModel.repositorPager.collectAsLazyPagingItems()
val state: LazyListState = rememberLazyListState()
SwipeRefresh(
state = rememberSwipeRefreshState((lazyPagingItems.loadState.refresh is LoadState.Loading && lazyPagingItems.itemCount > 0)),
onRefresh = { lazyPagingItems.refresh() },
) {
LazyColumn(
state =state,
contentPadding = PaddingValues(10.dp),
verticalArrangement = Arrangement.spacedBy(5.dp)) {
items(items = lazyPagingItems) { item ->
item?.let {
RepositorCard(item)
}
}
if (lazyPagingItems.loadState.append is LoadState.Loading) {
//下一页的load状态
item {
Box(modifier = Modifier
.fillMaxWidth()
.height(50.dp)) {
CircularProgressIndicator(modifier = Modifier.align(alignment = Alignment.Center))
}
}
}
}
}
if (lazyPagingItems.loadState.refresh is LoadState.Loading) {
if (lazyPagingItems.itemCount == 0) {//第一次响应页面加载时的loading状态
Box(modifier = Modifier.fillMaxSize()) {
CircularProgressIndicator(modifier = Modifier.align(alignment = Alignment.Center))
}
}
}else if(lazyPagingItems.loadState.refresh is LoadState.Error){
//加载失败的错误页面
Box(modifier = Modifier.fillMaxSize()) {
Button(modifier = Modifier.align(alignment = Alignment.Center),
onClick = { lazyPagingItems.refresh() }) {
Text(text = "加载失败!请重试")
}
}
}
}
item:
@Composable
fun RepositorCard(repositorItem: RepositorItem) {
Card(modifier = Modifier
.fillMaxWidth()
.padding(8.dp)) {
Row(modifier = Modifier
.fillMaxWidth()
.height(88.dp)) {
Spacer(modifier = Modifier.width(10.dp))
Surface(shape = CircleShape, modifier = Modifier
.size(66.dp)
.align(Alignment.CenterVertically)) {
AsyncImage(model = repositorItem.owner.avatar_url,
contentDescription = "",
contentScale = ContentScale.Crop)
}
Spacer(modifier = Modifier.width(15.dp))
Column(modifier = Modifier.fillMaxWidth()) {
Spacer(modifier = Modifier.height(8.dp))
Text(text = repositorItem.name,
color = MaterialTheme.colors.primary,
style = MaterialTheme.typography.h6)
Text(text = repositorItem.full_name, style = MaterialTheme.typography.subtitle1)
}
}
}
}
最后运行是这样的:
7.自动向上翻页的问题
有的同学想上下都能翻页加载,还是用这个搜索的api来模拟一下,首次进入页面就加载第10页的数据,在 PagingSource这里改一下:
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, RepositorItem> {
try {
val nextPage: Int = params.key ?: 10//改成10 默认首次从第10页加载
val repositorRst = githubService.searchRepositors(words, nextPage, 20)
return LoadResult.Page(
data = repositorRst.items,
prevKey = if (nextPage == 1) null else nextPage - 1,
nextKey = if (repositorRst.items.isEmpty()) null else nextPage + 1
)
}catch (e:Exception){
return LoadResult.Error(e)
}
}
compose里面也改一下去掉SwipeRefresh:
LazyColumn(
state =state,
contentPadding = PaddingValues(10.dp),
verticalArrangement = Arrangement.spacedBy(5.dp)) {
if (lazyPagingItems.loadState.prepend is LoadState.Loading) {
//list 顶部loading
item {
Box(modifier = Modifier
.fillMaxWidth()
.height(50.dp)) {
CircularProgressIndicator(modifier = Modifier.align(alignment = Alignment.Center))
}
}
}
items(items = lazyPagingItems) { item ->
item?.let {
RepositorCard(item)
}
}
if (lazyPagingItems.loadState.append is LoadState.Loading) {
//list 底部loading
item {
Box(modifier = Modifier
.fillMaxWidth()
.height(50.dp)) {
CircularProgressIndicator(modifier = Modifier.align(alignment = Alignment.Center))
}
}
}
}
肿么肥事!! 它怎么自动向上翻页!!! 直到 prevKey为null ,这难道是paging compose版本的bug,后来我想到了 compose lifecycle里面讲到的:
列表加载完初始数据后,因为prevKey不为空,就继续获取上页数据,然后就相当于列表在顶部新增内容,然后LazyColumn自动滑动到顶部,然后又会触发paging加载上一页的数据,如此循环直到prevKey为空。
compose lifecycle后面讲到了可以用key
可组合项,在LazyColumn中提供了key
可组合项的内置支持,我们加上key就行了:
items(items = lazyPagingItems, key = { item -> item.id }) { item ->
item?.let {
RepositorCard(item)
}
}
ok 解决了
这里也可以加上重试按钮,这是底部的:
if (lazyPagingItems.loadState.append is LoadState.Loading) {
// xxxx
}else if(lazyPagingItems.loadState.append is LoadState.Error){
item {
//底部的重试按钮
Box(modifier = Modifier
.fillMaxWidth()
.height(50.dp)) {
Button(modifier = Modifier.align(alignment = Alignment.Center),
onClick = { lazyPagingItems.retry() }) {
Text(text = "重试")
}
}
}
}
顶部的就是
if(lazyPagingItems.loadState.prepend is LoadState.Error)
然后可以在PagingSource load方法里 随机抛出TimeoutException来模拟数据加载失败的情况。
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, RepositorItem> {
try {
val nextPage: Int = params.key ?: 10//改成10 默认从第10页加载
if(Random.nextBoolean()){
throw SocketTimeoutException("SocketTimeout")
}
val repositorRst = githubService.searchRepositors(words, nextPage, 20)
return LoadResult.Page(
data = repositorRst.items,
prevKey = if (nextPage == 1) null else nextPage - 1,
nextKey = if (repositorRst.items.isEmpty()) null else nextPage + 1
)
}catch (e:Exception){
return LoadResult.Error(e)
}
}
有的童靴觉得列表底部加个重试按钮有点丑,然后想能不能当翻页的数据加载失败后滑动到底部自动重试
首先我们加个方法判断是否滑动到底部了:
@Composable
fun LazyListState.isScrollToEnd():Boolean {
var previousIndex by remember(this) { mutableStateOf(firstVisibleItemIndex) }
var previousScrollOffset by remember(this) { mutableStateOf(firstVisibleItemScrollOffset) }
return remember(this) {
derivedStateOf {
if (previousIndex != firstVisibleItemIndex) {
previousIndex < firstVisibleItemIndex
} else {
previousScrollOffset < firstVisibleItemScrollOffset
}.also {
previousIndex = firstVisibleItemIndex
previousScrollOffset = firstVisibleItemScrollOffset
} && layoutInfo.visibleItemsInfo.lastOrNull()?.let { it.index == layoutInfo.totalItemsCount-1 } ?: false
}
}.value
}
然后就是根据条件调用重试了
val state: LazyListState = rememberLazyListState()
LazyColumn(state =state){
xxx
}
//是否滑动到底部
val isScrollToEnd = state.isScrollToEnd()
//是否自动重试
val currectIsNextRetry by rememberUpdatedState(isScrollToEnd &&
lazyPagingItems.loadState.append is LoadState.Error)
LaunchedEffect(isScrollToEnd){
if(currectIsNextRetry){
Log.i(TAG,"retry")
lazyPagingItems.retry()
}
}
文末
我总结了一些Android核心知识点,以及一些最新的大厂面试题、知识脑图和视频资料解析。
需要的直接点击文末小卡片可以领取哦!我免费分享给你,以后的路也希望我们能一起走下去。(谢谢大家一直以来的支持,需要的自己领取)
Android学习PDF+架构视频+面试文档+源码笔记
部分资料一览:
- 330页PDF Android学习核心笔记(内含8大板块)
- Android学习的系统对应视频
- Android进阶的系统对应学习资料
- Android BAT大厂面试题(有解析)
领取地址: