1.请求接口地址:https://api.github.com/search/repositories
2.创建网络请求GithubService及ViewMode
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Query
interface GithubService {
/**
* 仓库搜索
*/
@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
GithubViewModel
class GithubViewModel: ViewModel() {
val githubService: GithubService = getGithubService()
val repositorPager = Pager(config = PagingConfig(pageSize = 6)){
MyPagingSource(getGithubService(),"compose")
}.flow.cachedIn(viewModelScope)
val dataLive = MutableLiveData<MutableList<RepositorItem>>()
fun loadData(offset:Int,pageSize:Int){
viewModelScope.launch {
val repositorRst = githubService.searchRepositors("android", offset, pageSize)
dataLive.postValue(repositorRst.items.toMutableList())
}
}
}
3.实体Bean
data class RepositorResult(
@SerializedName("incomplete_results")
var incompleteResults: Boolean,
@SerializedName("items")
var items: List<RepositorItem>,
@SerializedName("total_count")
var totalCount: Int
)
data class Rsp(
@SerializedName("incomplete_results")
var incompleteResults: Boolean,
@SerializedName("items")
var items: List<RepositorItem>,
@SerializedName("total_count")
var totalCount: Int
)
data class RepositorItem(
@SerializedName("allow_forking")
var allowForking: Boolean,
@SerializedName("archive_url")
var archiveUrl: String,
@SerializedName("archived")
var archived: Boolean,
@SerializedName("assignees_url")
var assigneesUrl: String,
@SerializedName("blobs_url")
var blobsUrl: String,
@SerializedName("branches_url")
var branchesUrl: String,
@SerializedName("clone_url")
var cloneUrl: String,
@SerializedName("collaborators_url")
var collaboratorsUrl: String,
@SerializedName("comments_url")
var commentsUrl: String,
@SerializedName("commits_url")
var commitsUrl: String,
@SerializedName("compare_url")
var compareUrl: String,
@SerializedName("contents_url")
var contentsUrl: String,
@SerializedName("contributors_url")
var contributorsUrl: String,
@SerializedName("created_at")
var createdAt: String,
@SerializedName("default_branch")
var defaultBranch: String,
@SerializedName("deployments_url")
var deploymentsUrl: String,
@SerializedName("description")
var description: String,
@SerializedName("disabled")
var disabled: Boolean,
@SerializedName("downloads_url")
var downloadsUrl: String,
@SerializedName("events_url")
var eventsUrl: String,
@SerializedName("fork")
var fork: Boolean,
@SerializedName("forks")
var forks: Int,
@SerializedName("forks_count")
var forksCount: Int,
@SerializedName("forks_url")
var forksUrl: String,
@SerializedName("full_name")
var fullName: String,
@SerializedName("git_commits_url")
var gitCommitsUrl: String,
@SerializedName("git_refs_url")
var gitRefsUrl: String,
@SerializedName("git_tags_url")
var gitTagsUrl: String,
@SerializedName("git_url")
var gitUrl: String,
@SerializedName("has_discussions")
var hasDiscussions: Boolean,
@SerializedName("has_downloads")
var hasDownloads: Boolean,
@SerializedName("has_issues")
var hasIssues: Boolean,
@SerializedName("has_pages")
var hasPages: Boolean,
@SerializedName("has_projects")
var hasProjects: Boolean,
@SerializedName("has_wiki")
var hasWiki: Boolean,
@SerializedName("homepage")
var homepage: String,
@SerializedName("hooks_url")
var hooksUrl: String,
@SerializedName("html_url")
var htmlUrl: String,
@SerializedName("id")
var id: Int,
@SerializedName("is_template")
var isTemplate: Boolean,
@SerializedName("issue_comment_url")
var issueCommentUrl: String,
@SerializedName("issue_events_url")
var issueEventsUrl: String,
@SerializedName("issues_url")
var issuesUrl: String,
@SerializedName("keys_url")
var keysUrl: String,
@SerializedName("labels_url")
var labelsUrl: String,
@SerializedName("language")
var language: String,
@SerializedName("languages_url")
var languagesUrl: String,
@SerializedName("license")
var license: License,
@SerializedName("merges_url")
var mergesUrl: String,
@SerializedName("milestones_url")
var milestonesUrl: String,
@SerializedName("mirror_url")
var mirrorUrl: Any,
@SerializedName("name")
var name: String,
@SerializedName("node_id")
var nodeId: String,
@SerializedName("notifications_url")
var notificationsUrl: String,
@SerializedName("open_issues")
var openIssues: Int,
@SerializedName("open_issues_count")
var openIssuesCount: Int,
@SerializedName("owner")
var owner: Owner,
@SerializedName("private")
var `private`: Boolean,
@SerializedName("pulls_url")
var pullsUrl: String,
@SerializedName("pushed_at")
var pushedAt: String,
@SerializedName("releases_url")
var releasesUrl: String,
@SerializedName("score")
var score: Double,
@SerializedName("size")
var size: Int,
@SerializedName("ssh_url")
var sshUrl: String,
@SerializedName("stargazers_count")
var stargazersCount: Int,
@SerializedName("stargazers_url")
var stargazersUrl: String,
@SerializedName("statuses_url")
var statusesUrl: String,
@SerializedName("subscribers_url")
var subscribersUrl: String,
@SerializedName("subscription_url")
var subscriptionUrl: String,
@SerializedName("svn_url")
var svnUrl: String,
@SerializedName("tags_url")
var tagsUrl: String,
@SerializedName("teams_url")
var teamsUrl: String,
@SerializedName("topics")
var topics: List<String>,
@SerializedName("trees_url")
var treesUrl: String,
@SerializedName("updated_at")
var updatedAt: String,
@SerializedName("url")
var url: String,
@SerializedName("visibility")
var visibility: String,
@SerializedName("watchers")
var watchers: Int,
@SerializedName("watchers_count")
var watchersCount: Int,
@SerializedName("web_commit_signoff_required")
var webCommitSignoffRequired: Boolean
) {
data class License(
@SerializedName("key")
var key: String,
@SerializedName("name")
var name: String,
@SerializedName("node_id")
var nodeId: String,
@SerializedName("spdx_id")
var spdxId: String,
@SerializedName("url")
var url: String
)
data class Owner(
@SerializedName("avatar_url")
var avatarUrl: String,
@SerializedName("events_url")
var eventsUrl: String,
@SerializedName("followers_url")
var followersUrl: String,
@SerializedName("following_url")
var followingUrl: String,
@SerializedName("gists_url")
var gistsUrl: String,
@SerializedName("gravatar_id")
var gravatarId: String,
@SerializedName("html_url")
var htmlUrl: String,
@SerializedName("id")
var id: Int,
@SerializedName("login")
var login: String,
@SerializedName("node_id")
var nodeId: String,
@SerializedName("organizations_url")
var organizationsUrl: String,
@SerializedName("received_events_url")
var receivedEventsUrl: String,
@SerializedName("repos_url")
var reposUrl: String,
@SerializedName("site_admin")
var siteAdmin: Boolean,
@SerializedName("starred_url")
var starredUrl: String,
@SerializedName("subscriptions_url")
var subscriptionsUrl: String,
@SerializedName("type")
var type: String,
@SerializedName("url")
var url: String
)
}
4.MyPagingSource
import androidx.paging.PagingSource
import androidx.paging.PagingState
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.上拉加载及下拉刷新组建SwipeRefreshList
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ButtonDefaults.elevation
import androidx.compose.material.ButtonDefaults.textButtonColors
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.zj.refreshlayout.SwipeRefreshLayout
import kotlinx.coroutines.delay
/**
* 下拉加载封装
*
* implementation 'io.github.shenzhen2017:compose-refreshlayout:1.0.0'
* */
@Composable
fun <T : Any> SwipeRefreshList(
collectAsLazyPagingItems: LazyPagingItems<T>,
listContent: LazyListScope.() -> Unit,
) {
var refreshing by remember { mutableStateOf(false) }
LaunchedEffect(refreshing) {
if (refreshing) {
delay(2000)
collectAsLazyPagingItems.refresh()
refreshing = false
}
}
val rememberSwipeRefreshState = rememberSwipeRefreshState(isRefreshing = false)
SwipeRefreshLayout(isRefreshing = refreshing, onRefresh = {
refreshing = true
}){
rememberSwipeRefreshState.isRefreshing = collectAsLazyPagingItems.loadState.refresh is LoadState.Loading
LazyColumn(
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.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 = Color.White),
elevation = elevation(
defaultElevation = 2.dp,
pressedElevation = 4.dp,
),
) {
Text(text = "加载失败,请重试", color = Color.Gray)
}
}
}
/**
* 页面加载失败处理
* */
@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.loading_big_13),
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 = Color.White),
elevation = elevation(
defaultElevation = 2.dp,
pressedElevation = 4.dp,
)
//colors = ButtonDefaults
) { Text(text = "重试", color = Color.Gray) }
}
}
/**
* 底部加载更多正在加载中...
* */
@Composable
fun LoadingItem() {
Row(
modifier = Modifier
.height(34.dp)
.fillMaxWidth()
.padding(5.dp),
horizontalArrangement = Arrangement.Center
) {
CircularProgressIndicator(
modifier = Modifier
.size(24.dp),
color = Color.Gray,
strokeWidth = 2.dp
)
Text(
text = "加载中...",
color = Color.Gray,
modifier = Modifier
.fillMaxHeight()
.padding(start = 20.dp),
fontSize = 18.sp,
)
}
}
6.显示到Activity中
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items
class LoadActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_load)
val composeView = findViewById<ComposeView>(R.id.compose)
composeView.setContent {
val viewModel: GithubViewModel = viewModel()
val lazyPagingItems = viewModel.repositorPager.collectAsLazyPagingItems()
SwipeRefreshList(lazyPagingItems){
items(items = lazyPagingItems) { item ->
item?.let {
RepositorCard(item)
}
}
}
}}
}
7.UI组件RepositorCard
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Card
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
@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.avatarUrl,
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.fullName, style = MaterialTheme.typography.subtitle1)
}
}
}
}