【发布时间】:2023-03-24 13:43:01
【问题描述】:
我遵循this帖子中描述的方法(可以找到相应的存储库here) 使用分页库从我的 Firestore 数据库中部分加载数据。 我没有使用不同的活动(如原始帖子中所做的那样),而是将代码放在片段中。
在我的数据源中,loadInitial 函数被调用,我们在其中订阅结果。
一旦结果可用,调用callback.onResult 作为参数新检索的数据不会到达适配器。进入并通过callback.onResult函数后,onNext方法出现如下异常:
java.lang.IllegalStateException: callback.onResult already called, cannot call again.
但是,如果我按下后退按钮,再次登录,callback.Onresult 的调用确实到达适配器。
最初,这让我相信我做错了与活动/片段的生命周期相关的事情,但是使用活动上下文更改适配器的初始化位置并没有改变结果。
This 帖子还提到了关于加载片段两次以使事情正常进行的一些事情,这可能解释了按下后退按钮并返回时观察到的结果。
编辑:添加更多相关(更新)代码
编辑 2:在 WorkManager 中添加了额外的 Dagger 注释和重构代码以使用 RxJava2
编辑 3:我完全剥离了 Dagger 2 的所有用法,现在代码可以正常工作,所以问题与 Dagger 相关
编辑 4:问题已解决,请参阅下面的帖子
相关代码如下:
片段
class TradeRequestFragment : Fragment() {
private val auth: FirebaseAuth by lazy { FirebaseAuth.getInstance() }
private lateinit var rootView: View
@SuppressLint("RestrictedApi")
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
rootView = inflater.inflate(R.layout.fragment_trade_requests, container, false)
return rootView
}
private fun initAdapter() {
// Setup the RecyclerView for the User's trade requests.
// 1. get a reference to recyclerView
val tradeRequestRecyclerView = rootView.findViewById<RecyclerView>(R.id.trade_requests_recycler_view)
// 2. set LayoutManager
tradeRequestRecyclerView.layoutManager = LinearLayoutManager(activity?.applicationContext)
val tradeRequestAdapter = TradeRequestAdapter(activity?.applicationContext)
// Dagger 2 injection
val component = DaggerTradeRequestFragment_TradeRequestComponent.builder().tradeRequestModule(TradeRequestModule()).build()
val tradeRequestViewModel = TradeRequestViewModel(component.getTradeRequestDataProvider())
// 3. Set Adapter
tradeRequestRecyclerView.adapter = tradeRequestAdapter
// 4. Set the item animator
tradeRequestRecyclerView.addItemDecoration(DividerItemDecoration(activity?.applicationContext, LinearLayoutManager.VERTICAL))
tradeRequestViewModel.getTradeRequests()?.observe(viewLifecycleOwner, Observer(tradeRequestAdapter::submitList))
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
initAdapter()
}
@Module
inner class TradeRequestModule {
@Provides
@Singleton
fun provideDataSource(): TradeRequestsDataSource {
return TradeRequestsDataSource(auth.currentUser?.uid
?: "")
}
@Provides
@Singleton
fun provideUserId(): String {
return auth.currentUser?.uid ?: ""
}
}
@Singleton
@Component(modules = [TradeRequestModule::class])
interface TradeRequestComponent {
fun getTradeRequestDataProvider(): TradeRequestDataProvider
}
}
TradeRequestManager
class TradeRequestManager private constructor(userID: String) {
companion object : Utils.Singleton<TradeRequestManager, String>(::TradeRequestManager)
private val TRADE_REQUEST_ROUTE = "tradeRequests"
private val userIDKey = "userId"
private val userTradeRequestsAdapterInvalidation = PublishSubject.create<Any>()
private val database = FirebaseFirestore.getInstance()
private val databaseRef: Query? by lazy {
try {
database.collection(TRADE_REQUEST_ROUTE).whereEqualTo(userIDKey, userID)
} catch (e: Exception) {
Log.e(this.TAG(), "Could not retrieve the user's trade requests", e.cause)
null
}
}
private val tradeRequestBuilder: Moshi by lazy {
Moshi.Builder()
.add(ZonedDateTime::class.java, ZonedDateTimeAdapter())
.add(CurrencyUnit::class.java, CurrencyUnitAdapter())
.add(Money::class.java, JodaMoneyAdapter())
.add(KotlinJsonAdapterFactory())
.build()
}
private val tradeRequestAdapter: JsonAdapter<TradeRequest> by lazy { tradeRequestBuilder.adapter(TradeRequest::class.java) }
// TODO see [here][https://leaks.wanari.com/2018/07/30/android-jetpack-paging-firebase]
init {
databaseRef?.addSnapshotListener(object : EventListener<QuerySnapshot> {
override fun onEvent(snapshot: QuerySnapshot?, e: FirebaseFirestoreException?) {
if (e != null) {
Log.e(this.TAG(), "listener:error", e)
return
}
if (snapshot == null) {
return
}
for (dc in snapshot.documentChanges) {
when (dc.type) {
DocumentChange.Type.ADDED -> userTradeRequestsAdapterInvalidation.onNext(true)
DocumentChange.Type.MODIFIED -> userTradeRequestsAdapterInvalidation.onNext(true)
DocumentChange.Type.REMOVED -> userTradeRequestsAdapterInvalidation.onNext(true)
}
}
}
})
}
fun getUserTradeRequestsChangeSubject(): PublishSubject<Any>? {
return userTradeRequestsAdapterInvalidation
}
// https://stackoverflow.com/questions/45420829/group-data-with-rxjava-2-add-element-to-each-group-and-output-one-list
fun getTradeRequests(count: Int): Single<List<TradeRequest?>> {
if (databaseRef == null) {
return Observable.empty<List<TradeRequest?>>().singleOrError()
}
// By default, we order by 'creationDate' descending
// If the field by which we order does not exists, no results are returned
return RxFirestore.observeQueryRef(databaseRef!!.orderBy("creationDate", Query.Direction.DESCENDING).limit(count.toLong()))
.firstElement()
.toSingle()
.flattenAsObservable { list -> list.documents }
.flatMap { doc -> Observable.just(doc.data as? Map<String, String>) }
.map { json -> tryOrNull { tradeRequestAdapter.fromJsonValue(json) } }
.filter { tradeRequest -> tradeRequest != null }
.toList()
}
fun getTradeRequestsAfter(key: String, value: String, count: Int, order: Query.Direction): Single<Pair<List<TradeRequest?>, String>> {
if (databaseRef == null) {
return Observable.empty<Pair<List<TradeRequest?>, String>>().singleOrError()
}
val result = RxFirestore.observeQueryRef(databaseRef!!.whereGreaterThanOrEqualTo(key, value).limit(count.toLong()).orderBy(key, order))
.firstElement()
.toSingle()
.flattenAsObservable { list -> list.documents }
.flatMap { doc -> Observable.just(doc.data as? Map<String, String>) }
.map { json -> tryOrNull { tradeRequestAdapter.fromJsonValue(json) } }
.filter { tradeRequest -> tradeRequest != null }
.toList()
val tradeRequests = result.blockingGet()
// FIXME determine next filter value
var newFilterValue = ""
if (tradeRequests.size == count) {
// Either the number of elements is capped or exactly "count" elements matched
newFilterValue = ""
}
// END FIXME
return Observable.just(Pair(tradeRequests, newFilterValue)).singleOrError()
}
fun getTradeRequestsBefore(key: String, value: String, count: Int, order: Query.Direction): Single<Pair<List<TradeRequest?>, String>> {
if (databaseRef == null) {
return Observable.empty<Pair<List<TradeRequest?>, String>>().singleOrError()
}
val result = RxFirestore.observeQueryRef(databaseRef!!.whereLessThan(key, value).limit(count.toLong()).orderBy(key, order))
.firstElement()
.toSingle()
.flattenAsObservable { list -> list.documents }
.flatMap { doc -> Observable.just(doc.data as? Map<String, String>) }
.map { json -> tryOrNull { tradeRequestAdapter.fromJsonValue(json) } }
.filter { tradeRequest -> tradeRequest != null }
.toList()
val tradeRequests = result.blockingGet()
// FIXME determine next filter value
var newFilterValue = ""
if (tradeRequests.size == count) {
// Either the number of elements is capped or exactly "count" elements matched
newFilterValue = ""
}
// END FIXME
return Observable.just(Pair(tradeRequests, newFilterValue)).singleOrError()
}
数据源
class TradeRequestsDataSource @Inject constructor(var userId: String, var filter: Data) : ItemKeyedDataSource<String, TradeRequest>() {
private var filterKey: String
private var filterValue: String
private var filterOrder: Query.Direction
private var userTradeRequestsManager: TradeRequestManager = TradeRequestManager.getInstance(userId)
init {
userTradeRequestsManager.getUserTradeRequestsChangeSubject()?.observeOn(Schedulers.io())?.subscribeOn(Schedulers.computation())?.subscribe {
invalidate()
}
filterKey = filter.getString("filterKey") ?: ""
filterValue = filter.getString("filterValue") ?: ""
filterOrder = try {
Query.Direction.valueOf(filter.getString("filterOrder") ?: "")
} catch (e: Exception) {
Query.Direction.DESCENDING
}
}
override fun loadInitial(params: LoadInitialParams<String>, callback: LoadInitialCallback<TradeRequest>) {
Log.i(this.TAG(), "Loading the initial items in the RecyclerView")
userTradeRequestsManager.getTradeRequests(params.requestedLoadSize).singleElement().subscribe({ tradeRequests ->
Log.i(this.TAG(), "We received the callback")
callback.onResult(tradeRequests)
}, {})
}
override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<TradeRequest>) {
userTradeRequestsManager.getTradeRequestsAfter(params.key, this.filterValue, params.requestedLoadSize, this.filterOrder).singleElement().subscribe({
this.filterValue = it.second
callback.onResult(it.first)
}, {})
}
override fun loadBefore(params: LoadParams<String>, callback: LoadCallback<TradeRequest>) {
userTradeRequestsManager.getTradeRequestsBefore(params.key, this.filterValue, params.requestedLoadSize, this.filterOrder).singleElement().subscribe({
this.filterValue = it.second
callback.onResult(it.first)
}, {})
}
override fun getKey(item: TradeRequest): String {
return filterKey
}
适配器
class TradeRequestAdapter(val context: Context?) : PagedListAdapter<TradeRequest, TradeRequestAdapter.TradeRequestViewHolder>(
object : DiffUtil.ItemCallback<TradeRequest>() {
override fun areItemsTheSame(oldItem: TradeRequest, newItem: TradeRequest): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: TradeRequest, newItem: TradeRequest): Boolean {
return oldItem.amount == newItem.amount &&
oldItem.baseCurrency == newItem.baseCurrency &&
oldItem.counterCurrency == newItem.counterCurrency &&
oldItem.creationDate == newItem.creationDate &&
oldItem.userId == newItem.userId
}
}) {
private lateinit var mInflater: LayoutInflater
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TradeRequestViewHolder {
mInflater = LayoutInflater.from(context)
val view = mInflater.inflate(R.layout.trade_request_item, parent, false)
return TradeRequestViewHolder(view)
}
override fun onBindViewHolder(holder: TradeRequestViewHolder, position: Int) {
val tradeRequest = getItem(position)
holder.tradeRequestBaseCurrency.text = tradeRequest?.baseCurrency.toString()
holder.tradeRequestCounterCurrency.text = tradeRequest?.counterCurrency.toString()
holder.tradeRequestAmount.text = tradeRequest?.amount.toString()
}
// Placeholder class for displaying a single TradeRequest
class TradeRequestViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var tradeRequestBaseCurrency: TextView = itemView.trade_request_item_from_currency
var tradeRequestCounterCurrency: TextView = itemView.trade_request_item_to_currency
var tradeRequestAmount: TextView = itemView.trade_request_item_amount
}
}
【问题讨论】:
-
可能
TradeRequestManager公开Flowable/Observable而不是Single,因此您正在创建永不消亡的订阅。 -
你说得对,getTradeRequests 函数显示了我对
TradeRequestManager.getTradeRequests的实现,它确实返回了一个 Observable。我对 RXJava 很陌生,但我发现了toSingle方法,它可以从Observable转换为Single世界。但是我不能将它应用于PublishSubject -
好的,但总体而言,错误的来源是
TradeRequestManager被滥用,但我们看不到它的代码。 -
我添加了
TradeRequestManager代码并更改了TradeRequestsDataSource中的订阅,以使它们使用singleElement()方法观察Single。假设这是正确完成的,我仍然观察到相同的行为 -
我不明白为什么你的
TradeRequestsDataSource不是一个PositionalDataSource,它使用startAt+limit包装支持limit/offset的FireStore 查询。因此,您需要做的是镜像whatRoomdoes,尽管有可能观察对 FULL 集合所做的更改但仅显示集合的一个 SUBSET 对于 Firestore 来说很棘手。
标签: android kotlin android-recyclerview pagination dagger-2