PageKeyedDataSource是比较常见的一种DataSource,适用于数据源以“页”的方式进行请求的情况。例如,请求参数为page=2&pagesize=10,则表示数据源以10条数据为一页,当前返回第二页的5条数据。
接下来首先来按照MVVM架构跑通一个笑话大全数据接口,这里使用的是最新笑话接口,key是我自己注册的。
接口:http://v.juhe.cn/joke/content/text.php?key=您申请的KEY&page=1&pagesize=10
接口返回数据示例:
{
"error_code": 0,
"reason": "Success",
"result": {
"data": [
{
"content": "女生分手的原因有两个,\r\n一个是:闺蜜看不上。另一个是:闺蜜看上了。",
"hashId": "607ce18b4bed0d7b0012b66ed201fb08",
"unixtime": 1418815439,
"updatetime": "2014-12-17 19:23:59"
},
{
"content": "老师讲完课后,问道\r\n“同学们,你们还有什么问题要问吗?”\r\n这时,班上一男同学举手,\r\n“老师,这节什么课?”",
"hashId": "20670bc096a2448b5d78c66746c930b6",
"unixtime": 1418814837,
"updatetime": "2014-12-17 19:13:57"
},
......
]
}
}
首先添加相关依赖,这里使用Retrofit作为网络请求库。其余的按照需要依次添加即可。由于该接口为http接口,需要在AndroidManifest添加
android:networkSecurityConfig="@xml/network_config"
network_config.xml
<?xml version ="1.0" encoding ="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system"/>
</trust-anchors>
</base-config>
</network-security-config>
以及别忘了添加网络权限。
Model:
//按照"最新笑话"定义
data class JokeResponse(val reason:String, val result:Result,@SerializedName("error_code") val errorCode :Int) {
data class Result(val data:List<Joke>)
data class Joke(val content:String, val hashId:String,val unixtime:Long,val updatetime:String)
}
web请求相关:
interface JokeService {
@GET("content/text.php?key=${
MyApplication.KEY}")
fun getJokes(@Query("page") page:Int,@Query("pagesize") pagesize:Int):
Call<JokeResponse>//text.php?page=1&pagesize=10&key=
}
object ServiceCreator {
private const val BASE_URL = "http://v.juhe.cn/joke/"
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
fun <T> create(serviceClass:Class<T>):T = retrofit.create(serviceClass)
inline fun <reified T> create():T = create(T::class.java)
}
//统一的网络数据源访问入口,对所有网络请求的API进行封装,单例类
object RetrofitNetwork {
//获取最新笑话列表
private val jokeService = ServiceCreator.create(JokeService::class.java)
suspend fun searchJokes(page:Int,pagesize:Int) = jokeService.getJokes(page,pagesize).await()
private suspend fun <T> Call<T>.await():T{
Log.d("suspendCoroutine",request().toString())
return suspendCoroutine {
continuation->
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>){
val body = response.body()
Log.d("RetrofitNetwork",body.toString())
if (body!=null) continuation.resume(body)
else continuation.resumeWithException(RuntimeException("response body is null"))
}
override fun onFailure(call: Call<T>, t: Throwable){
Log.d("onFailure",continuation.toString())
continuation.resumeWithException(t)
}
})
}
}
}
//仓库层的统一封装入口,单例类
object Repository {
//查看最新笑话
fun searchJokes(page:Int,pagesize:Int) = fire(Dispatchers.IO){
val jokeInfoResponse = RetrofitNetwork.searchJokes(page,pagesize)
Log.d("jokeInfoResponse",jokeInfoResponse.toString())
if (jokeInfoResponse.errorCode == 0){
val joke= jokeInfoResponse.result.data
Result.success(joke)
}else{
Result.failure(RuntimeException("joke response status is ${
jokeInfoResponse.reason}"))
}
}
//使用suspend关键字,以表示所有传入的Lambda表达式中的代码也是有挂起函数上下文的
private fun <T> fire(context: CoroutineContext, block: suspend ()->Result<T>) = liveData<Result<T>>(context) {
val result = try {
block()
}catch (e:Exception){
Result.failure<JokeResponse>(e)
}
emit(result as Result<T>)
}
}
viewModel:
class JokeViewModel : ViewModel() {
private val searchJokeLiveData = MutableLiveData<ArrayList<Int>>()
val jokeLiveData = Transformations.switchMap(searchJokeLiveData){
searchInfo->
Repository.searchJokes(searchInfo[0],searchInfo[1])
}
fun searchJokes(page:Int,pagesize:Int){
val searchJoke = arrayListOf<Int>(page,pagesize)
searchJokeLiveData.value = searchJoke
}
}
Activity:
这里只是做一个简单的接口测试,暂时没有写页面。
class PageKeyedDataSourceTestMainActivity : AppCompatActivity() {
val viewModel by lazy {
ViewModelProvider(this).get(JokeViewModel::class.java) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_positional_data_source_test_main)
viewModel.searchJokes(1,10)
viewModel.jokeLiveData.observe(this, Observer {
result->
val joke=result.getOrNull()
})
}
}
运行一下,查看Log,接口跑通即可。
接下来改为使用Paging组件分页请求网络数据,下面是一个关系图。
红色部分的DataSource是根据不同种类分别有具体的实现,其余部分基本通用。
①首先添加一个PageKeyedDataSource类,代码如下:
class JokePageKeyedDataSource: PageKeyedDataSource<Int, JokeResponse.Joke>() {
companion object{
const val FIRST_PAGE=1
const val PAGE_SIZE=10
}
override fun loadInitial(
params: LoadInitialParams<Int>,
callback: LoadInitialCallback<Int, JokeResponse.Joke>
) {
//页面首次加载数据时调用,在该方法内调用API接口加载第一页数据
val job = Job()
val scope = CoroutineScope(job)
scope.launch {
Repository.searchJokes(FIRST_PAGE, PAGE_SIZE,callback)
}
//所有kotlinx.coroutines中的挂起函数都是可取消的,都不需要检查协程是否已取消,
// 然后停止任务执行,或是抛出 CancellationException 异常。
// job.cancel()
}
override fun loadAfter(
params: LoadParams<Int>,
callback: LoadCallback<Int, JokeResponse.Joke>
) {
//加载下一页数据
//接口实测最多20页,之后都只显示第20页的内容,因此20页以后不加载
val nextKey:Int? = if (params.key>=20) {
null
}else{
params.key+1
}
val job = Job()
val scope = CoroutineScope(job)
scope.launch {
Repository.searchJokes(params.key, PAGE_SIZE,callback,nextKey)
}
// job.cancel()
}
override fun loadBefore(
params: LoadParams<Int>,
callback: LoadCallback<Int, JokeResponse.Joke>
) {
//加载前一页数据
//目前用不上,什么都不做
}
}
里面我们实现了两个方法,分别是loadInitial()和loadAfter()。
callback.onResult(joke,null, JokePageKeyedDataSource.FIRST_PAGE +1)//第一个参数会交给PagedList
第一个参数是加载得到的数据,第二个参数是上一页key,由于不存在上一页,所以设置为null,第三个参数为下一页key,如果不存在,也设置为null。
②在Repository单例类中重载两个searchJokes()方法,如下:
suspend fun searchJokes(
page:Int, pagesize:Int,
callback: PageKeyedDataSource.LoadInitialCallback<Int, JokeResponse.Joke>) {
val jokeInfoResponse = RetrofitNetwork.searchJokes(page,pagesize)
Log.d("jokeInfoResponse",jokeInfoResponse.toString())
if (jokeInfoResponse.errorCode == 0){
val joke= jokeInfoResponse.result.data
callback.onResult(joke,null, JokePageKeyedDataSource.FIRST_PAGE +1)//第一个参数会交给PagedList
}else{
}
}
suspend fun searchJokes(
page:Int, pagesize:Int,
callback: PageKeyedDataSource.LoadCallback<Int, JokeResponse.Joke>,previousOrNexPageKey: Int?) {
val jokeInfoResponse = RetrofitNetwork.searchJokes(page,pagesize)
Log.d("jokeInfoResponse",jokeInfoResponse.toString())
if (jokeInfoResponse.errorCode == 0){
val joke= jokeInfoResponse.result.data
callback.onResult(joke,previousOrNexPageKey)//第一个参数会交给PagedList
}else{
}
}
这样PageKeyedDataSource和API Service就写完了。接下来我们需要建立一个DataSourceFactory,它负责创建JokePageKeyedDataSource,并使用LiveData包装JokePageKeyedDataSource,将其暴露给JokeViewModel。
③创建JokeDataSourceFactory。
class JokeDataSourceFactory:DataSource.Factory<Int,JokeResponse.Joke> (){
private val liveDataSource = MutableLiveData<JokePageKeyedDataSource>()
override fun create(): DataSource<Int, JokeResponse.Joke> {
val dataSource = JokePageKeyedDataSource()
liveDataSource.postValue(dataSource)
return dataSource
}
}
④修改JokeViewModel,添加如下代码:
//通过LivePagedListBuilder创建和配置PageList,并使用LiveData包装PageList,将其暴露给Activity
var jokePagedList:LiveData<PagedList<JokeResponse.Joke>>
init {
val config = PagedList.Config.Builder()
.setEnablePlaceholders(true)//设置控件占位
.setPageSize(JokePageKeyedDataSource.PAGE_SIZE)//设置每页大小,通常与DataSource中请求参数值一致
.setPrefetchDistance(3)//设置当前距离底部还有多少条数据时开始加载下一页数据
.setInitialLoadSizeHint(JokePageKeyedDataSource.PAGE_SIZE*4)//设置首次加载数据数量,默认3倍
.setMaxSize(65536*JokePageKeyedDataSource.PAGE_SIZE)//设置PagedList所能承受的最大数量,超过会异常
.build()
jokePagedList =
LivePagedListBuilder<Int,JokeResponse.Joke>(JokeDataSourceFactory(),config).build()
}
现在只剩下页面显示部分了。
⑤编写页面布局文件。
activity_positional_data_source_test_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/jokeRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
joke_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp">
<TextView
android:id="@+id/contentText"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:paddingTop="15dp"
android:id="@+id/updateTimeText"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
以上两个简单的布局文件没什么需要强调的,接下来进入正题。
⑥编写展示列表数据的JokePagedListAdapter。
class JokePagedListAdapter(val context: Context):PagedListAdapter<JokeResponse.Joke,JokePagedListAdapter.JokeViewHolder> (DIFF_CALLBACK){
companion object{
//DiffUtil用于计算两个数据列表之间的差异,只会更新需要更新的数据源,不需要刷新整个数据源
//比notifyDataSetChanged()对整个数据源刷新效率高,且可以轻松地为列表加入动画效果
private val DIFF_CALLBACK=object :DiffUtil.ItemCallback<JokeResponse.Joke>(){
//检测两个对象是否代表同一个Item
override fun areItemsTheSame(
oldItem: JokeResponse.Joke,
newItem: JokeResponse.Joke
): Boolean {
return oldItem.hashId == newItem.hashId
}
//检测两个Item是否存在不一样的数据
override fun areContentsTheSame(
oldItem: JokeResponse.Joke,
newItem: JokeResponse.Joke
): Boolean {
return oldItem == newItem
}
}
}
inner class JokeViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
val contentText: TextView = itemView.findViewById(R.id.contentText)
val updateTimeText: TextView = itemView.findViewById(R.id.updateTimeText)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): JokeViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.joke_item,parent,false)
return JokeViewHolder(view)
}
override fun onBindViewHolder(holder: JokeViewHolder, position: Int) {
val joke = getItem(position)//若当前有数据则知己与UI控件绑定,反之getItem通知PagedList去获取下一页数据
if (joke != null) {
holder.contentText.text = joke.content
holder.updateTimeText.text = joke.updatetime
}else{
holder.contentText.text = ""
holder.updateTimeText.text = ""
}
}
}
JokePagedListAdapter需要继承自PagedListAdapter,注意下在onBindViewHolder()方法中调用getItem()方法的注释。PagedList在收到通知后会让DataSource执行具体的数据获取工作。
⑦修改PageKeyedDataSourceTestMainActivity。
class PageKeyedDataSourceTestMainActivity : AppCompatActivity() {
val viewModel by lazy {
ViewModelProvider(this).get(JokeViewModel::class.java) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_positional_data_source_test_main)
// viewModel.searchJokes(1,10)
// viewModel.jokeLiveData.observe(this, Observer { result->
// val joke=result.getOrNull()
//
// })
jokeRecyclerView.layoutManager = LinearLayoutManager(this)
jokeRecyclerView.setHasFixedSize(true)
jokeRecyclerView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
val jokePagedListAdapter = JokePagedListAdapter(this)
viewModel.jokePagedList.observe(this,Observer {
jokes->
jokePagedListAdapter.submitList(jokes)
})
jokeRecyclerView.adapter = jokePagedListAdapter
}
}
注释掉原来的代码,现在我们将jokeRecyclerView与JokePagedListAdapter进行绑定,当数据发生变化时,该变化会通过LiveData传递过来,然后再通过jokePagedListAdapter.submitList()方法刷新数据。