使用Fragment的Android Jetpack分页:适配器未收到回调

时间:2019-03-08 11:31:02

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

我正在遵循this中所述的方法(可以在here中找到相应的存储库) 使用分页库以从Firestore数据库中部分加载数据。 我没有使用其他活动(如原始文章中所述),而是将代码放在Fragment中。

在我的数据源中,调用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
    }
}

1 个答案:

答案 0 :(得分:0)

问题的原因是在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
    }
}

我不太确定为什么能解决这个问题,但是可以。