RecyclerView 与 Paging Library 和 PositionalDataSource 保持为空
RecyclerView remains empty with Paging Library and PositionalDataSource
我正在尝试在我的项目中配置 Android 分页库,以将分页的消息列表加载到 RecyclerView 中。因为我的 API 使用偏移量和最大值,所以我使用的是 PositionalDataSource。
这是我的 DataSource 实现,其中 DataStore 使用 RetroFit 加载消息,我可以在控制台中看到消息正在正确加载,并转换为 MessageListItem 的实例:
class MessageDataSource: PositionalDataSource<MessageListItem>() {
override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<MessageListItem>) {
DataStore.shared.loadMessages(params.startPosition, params.loadSize) { result, error ->
if(result != null) {
callback.onResult(result.items)
} else {
callback.onError(MessageDataSourceException(error))
}
}
}
override fun loadInitial(
params: LoadInitialParams,
callback: LoadInitialCallback<MessageListItem>
) {
DataStore.shared.loadMessages(params.requestedStartPosition, params.requestedLoadSize) { response, error ->
if(response != null) {
callback.onResult(response.items, response.offset, response.total)
} else {
callback.onError(MessageDataSourceException(error))
}
}
}
}
class MessageDataSourceException(rootCause: Throwable? = null): Exception(rootCause)
这是我的 DataSourceFactory 实现:
class MessageDataSourceFactory: DataSource.Factory<Int, MessageListItem>() {
val messageLiveDataSource = MutableLiveData<MessageDataSource>()
private lateinit var messageDataSource: MessageDataSource
override fun create(): DataSource<Int, MessageListItem> {
messageDataSource = MessageDataSource()
messageLiveDataSource.postValue(messageDataSource)
return messageDataSource
}
}
这是我的 MessageListAdapter 实现:
object MessageListItemDiff: DiffUtil.ItemCallback<MessageListItem>() {
override fun areItemsTheSame(oldItem: MessageListItem, newItem: MessageListItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: MessageListItem, newItem: MessageListItem): Boolean {
return oldItem == newItem
}
}
class MessageListAdapter(private val clickListener: View.OnClickListener):
PagedListAdapter<MessageListItem, MessageListAdapter.MessageHolder>(MessageListItemDiff) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageHolder {
val inflatedView = LayoutInflater.from(parent.context).inflate(R.layout.item_message, parent, false)
return MessageHolder(inflatedView, clickListener)
}
override fun onBindViewHolder(holder: MessageHolder, position: Int) {
holder.bind(getItem(position)!!)
}
class MessageHolder(itemView: View, private val clickListener: View.OnClickListener) : RecyclerView.ViewHolder(itemView) {
val unreadIndicator = itemView.findViewById<ImageView>(R.id.unreadIndicator)
val title = itemView.findViewById<TextView>(R.id.title)
val dateSent = itemView.findViewById<TextView>(R.id.dateSent)
val cardView = itemView.findViewById<CardView>(R.id.card_view)
fun bind(message: MessageListItem) {
cardView.tag = message
cardView.setOnClickListener(clickListener)
title.text = message.title
dateSent.text = TimeAgo.using(message.dateSent.time)
if(message.isRead) {
unreadIndicator.setImageResource(0)
} else {
unreadIndicator.setImageResource(R.drawable.ic_unread)
}
}
}
}
最后是我的 ViewModel:
class MessageListViewModel: ViewModel() {
val messagePagedList: LiveData<PagedList<MessageListItem>>
val liveDataSource: LiveData<MessageDataSource>
init {
val messageDataSourceFactory = MessageDataSourceFactory()
liveDataSource = messageDataSourceFactory.messageLiveDataSource
val pagedListConfig = PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setPageSize(30)
.setPrefetchDistance(90)
.build()
messagePagedList = LivePagedListBuilder(messageDataSourceFactory, pagedListConfig).build()
}
}
这里是片段中的 onViewCreated 实现,它应该显示名为 messageList 的回收器视图:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
messageList.layoutManager = LinearLayoutManager(context!!)
messageList.setHasFixedSize(true)
messageListViewModel = ViewModelProvider(this).get(MessageListViewModel::class.java)
messageListAdapter = MessageListAdapter(this)
messageListViewModel.messagePagedList.observe(this, Observer { messages ->
messageListAdapter.submitList(messages)
})
messageList.adapter = messageListAdapter
}
问题是我可以看到数据正在从服务器加载,但它从未到达回收器视图。如果我在观察者行 (messageListAdapter.submitList(messages)
) 上添加一个断点,我会收到一次带有空消息列表的调用,仅此而已。
我不得不承认我真的对所有这些 类 以及它们应该做什么感到困惑,这是我在 Android 中的第一个分页实现,我不得不调整我的代码在这里和那里找到,因为我不想使用 Room 数据库、RxJava 或 PageKeyedDataSource,而大多数示例都这样做。
知道会发生什么吗?
改变这个:
messageListViewModel.messagePagedList.observe(this, Observer { messages ->
messageListAdapter.submitList(messages)
})
有了这个:
messageListViewModel.messagePagedList.observe(viewLifeCycleOwner, PagedList(messageListAdapter::submitList))
来源:https://developer.android.com/topic/libraries/architecture/paging#ex-observe-livedata
据我所知,为了使一切正常工作,PagedList
实例必须在 LiveData
调度后立即预加载初始数据。为此,需要在 loadInitial()
方法 return 时加载数据,这意味着您需要执行网络调用 同步 并调用 callback.onResult()
从方法 return 之前的 loadInitial()
方法调用,而不是使用回调。在那里同步执行网络调用是安全的,因为 LivePagedListBuilder
会负责从后台线程调用 PagedList.Builder()
。
此外,错误处理实现在这一点上(在版本 2.1.1 中)几乎没有记录和不完整,因此在许多情况下调用最近添加的 callback.onError()
方法将失败。例如,在 2.1.1 版本中根本没有实现错误处理 TiledPagedList
,这是用于 PositionalDataSource
的 PagedList
类型.
最后,如果您 return loadInitial()
中的列表的确切大小(就像您在此处所做的那样),那么在 loadRange()
中您需要确保始终 return 正是请求的项目数。如果 API 请求 30 个项目而你只 return 20,你的应用程序可能会崩溃。我发现的一种解决方法是,您可以使用 null
值填充结果列表,使其始终具有请求的大小,但随后您需要启用占位符。或者,不要 return loadInitial() 中的确切大小,列表将动态增长。
这个 API 很复杂,使用起来也很棘手,所以不要责备自己。 Google 目前正在开发一个用 Kotlin 编写的新版本 3.0,它有望解决旧版本的所有问题。
我正在尝试在我的项目中配置 Android 分页库,以将分页的消息列表加载到 RecyclerView 中。因为我的 API 使用偏移量和最大值,所以我使用的是 PositionalDataSource。
这是我的 DataSource 实现,其中 DataStore 使用 RetroFit 加载消息,我可以在控制台中看到消息正在正确加载,并转换为 MessageListItem 的实例:
class MessageDataSource: PositionalDataSource<MessageListItem>() {
override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<MessageListItem>) {
DataStore.shared.loadMessages(params.startPosition, params.loadSize) { result, error ->
if(result != null) {
callback.onResult(result.items)
} else {
callback.onError(MessageDataSourceException(error))
}
}
}
override fun loadInitial(
params: LoadInitialParams,
callback: LoadInitialCallback<MessageListItem>
) {
DataStore.shared.loadMessages(params.requestedStartPosition, params.requestedLoadSize) { response, error ->
if(response != null) {
callback.onResult(response.items, response.offset, response.total)
} else {
callback.onError(MessageDataSourceException(error))
}
}
}
}
class MessageDataSourceException(rootCause: Throwable? = null): Exception(rootCause)
这是我的 DataSourceFactory 实现:
class MessageDataSourceFactory: DataSource.Factory<Int, MessageListItem>() {
val messageLiveDataSource = MutableLiveData<MessageDataSource>()
private lateinit var messageDataSource: MessageDataSource
override fun create(): DataSource<Int, MessageListItem> {
messageDataSource = MessageDataSource()
messageLiveDataSource.postValue(messageDataSource)
return messageDataSource
}
}
这是我的 MessageListAdapter 实现:
object MessageListItemDiff: DiffUtil.ItemCallback<MessageListItem>() {
override fun areItemsTheSame(oldItem: MessageListItem, newItem: MessageListItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: MessageListItem, newItem: MessageListItem): Boolean {
return oldItem == newItem
}
}
class MessageListAdapter(private val clickListener: View.OnClickListener):
PagedListAdapter<MessageListItem, MessageListAdapter.MessageHolder>(MessageListItemDiff) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageHolder {
val inflatedView = LayoutInflater.from(parent.context).inflate(R.layout.item_message, parent, false)
return MessageHolder(inflatedView, clickListener)
}
override fun onBindViewHolder(holder: MessageHolder, position: Int) {
holder.bind(getItem(position)!!)
}
class MessageHolder(itemView: View, private val clickListener: View.OnClickListener) : RecyclerView.ViewHolder(itemView) {
val unreadIndicator = itemView.findViewById<ImageView>(R.id.unreadIndicator)
val title = itemView.findViewById<TextView>(R.id.title)
val dateSent = itemView.findViewById<TextView>(R.id.dateSent)
val cardView = itemView.findViewById<CardView>(R.id.card_view)
fun bind(message: MessageListItem) {
cardView.tag = message
cardView.setOnClickListener(clickListener)
title.text = message.title
dateSent.text = TimeAgo.using(message.dateSent.time)
if(message.isRead) {
unreadIndicator.setImageResource(0)
} else {
unreadIndicator.setImageResource(R.drawable.ic_unread)
}
}
}
}
最后是我的 ViewModel:
class MessageListViewModel: ViewModel() {
val messagePagedList: LiveData<PagedList<MessageListItem>>
val liveDataSource: LiveData<MessageDataSource>
init {
val messageDataSourceFactory = MessageDataSourceFactory()
liveDataSource = messageDataSourceFactory.messageLiveDataSource
val pagedListConfig = PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setPageSize(30)
.setPrefetchDistance(90)
.build()
messagePagedList = LivePagedListBuilder(messageDataSourceFactory, pagedListConfig).build()
}
}
这里是片段中的 onViewCreated 实现,它应该显示名为 messageList 的回收器视图:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
messageList.layoutManager = LinearLayoutManager(context!!)
messageList.setHasFixedSize(true)
messageListViewModel = ViewModelProvider(this).get(MessageListViewModel::class.java)
messageListAdapter = MessageListAdapter(this)
messageListViewModel.messagePagedList.observe(this, Observer { messages ->
messageListAdapter.submitList(messages)
})
messageList.adapter = messageListAdapter
}
问题是我可以看到数据正在从服务器加载,但它从未到达回收器视图。如果我在观察者行 (messageListAdapter.submitList(messages)
) 上添加一个断点,我会收到一次带有空消息列表的调用,仅此而已。
我不得不承认我真的对所有这些 类 以及它们应该做什么感到困惑,这是我在 Android 中的第一个分页实现,我不得不调整我的代码在这里和那里找到,因为我不想使用 Room 数据库、RxJava 或 PageKeyedDataSource,而大多数示例都这样做。
知道会发生什么吗?
改变这个:
messageListViewModel.messagePagedList.observe(this, Observer { messages ->
messageListAdapter.submitList(messages)
})
有了这个:
messageListViewModel.messagePagedList.observe(viewLifeCycleOwner, PagedList(messageListAdapter::submitList))
来源:https://developer.android.com/topic/libraries/architecture/paging#ex-observe-livedata
据我所知,为了使一切正常工作,PagedList
实例必须在 LiveData
调度后立即预加载初始数据。为此,需要在 loadInitial()
方法 return 时加载数据,这意味着您需要执行网络调用 同步 并调用 callback.onResult()
从方法 return 之前的 loadInitial()
方法调用,而不是使用回调。在那里同步执行网络调用是安全的,因为 LivePagedListBuilder
会负责从后台线程调用 PagedList.Builder()
。
此外,错误处理实现在这一点上(在版本 2.1.1 中)几乎没有记录和不完整,因此在许多情况下调用最近添加的 callback.onError()
方法将失败。例如,在 2.1.1 版本中根本没有实现错误处理 TiledPagedList
,这是用于 PositionalDataSource
的 PagedList
类型.
最后,如果您 return loadInitial()
中的列表的确切大小(就像您在此处所做的那样),那么在 loadRange()
中您需要确保始终 return 正是请求的项目数。如果 API 请求 30 个项目而你只 return 20,你的应用程序可能会崩溃。我发现的一种解决方法是,您可以使用 null
值填充结果列表,使其始终具有请求的大小,但随后您需要启用占位符。或者,不要 return loadInitial() 中的确切大小,列表将动态增长。
这个 API 很复杂,使用起来也很棘手,所以不要责备自己。 Google 目前正在开发一个用 Kotlin 编写的新版本 3.0,它有望解决旧版本的所有问题。