【问题标题】:Android Jetpack Pagination using Fragment: Adapter not receiving callback使用 Fragment 的 Android Jetpack 分页:适配器未收到回调
【发布时间】: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 查询。因此,您需要做的是镜像what Room does,尽管有可能观察对 FULL 集合所做的更改但仅显示集合的一个 SUBSET 对于 Firestore 来说很棘手。

标签: android kotlin android-recyclerview pagination dagger-2


【解决方案1】:

问题的原因是在DataFactory 中进行了依赖注入。 原代码如下:

@Singleton
class AnimalDataFactory @Inject constructor(): DataSource.Factory<String, Animal>() {


    private var datasourceLiveData = MutableLiveData<AnimalDataSource>()


    override fun create(): AnimalDataSource {
        // CORRECT: DataSource MUST be initialized here, otherwise items will not show up on initial activity/fragment launch
        val dataSource = AnimalDataSource()
        datasourceLiveData.postValue(dataSource)
        return dataSource
    }
}

当我把它改成

@Singleton
class AnimalDataFactory @Inject constructor(var dataSource: AnimalDataSourc): DataSource.Factory<String, Animal>() {


    private var datasourceLiveData = MutableLiveData<AnimalDataSource>()


    override fun create(): AnimalDataSource {
        // WRONG
        datasourceLiveData.postValue(dataSource)
        return dataSource
    }
}

我不太清楚为什么这可以解决问题,但它确实有效。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-08-23
    • 1970-01-01
    • 1970-01-01
    • 2021-07-21
    • 1970-01-01
    • 1970-01-01
    • 2015-11-28
    相关资源
    最近更新 更多