I am following the approach described in this post (the corresponding repository can be found here) to use the Paging library in order to partially load data from my Firestore database. Instead of making use of a different activity (as is done in the original post), I put the code in a Fragment.
In my DataSource, the loadInitial
function gets called in which we subscribe to the results.
Once the results are available, calling callback.onResult
with as argument the newly retrieved data does not reach the adapter. After stepping in and through the callback.onResult
function, the following exception occurs in the onNext
method:
java.lang.IllegalStateException: callback.onResult already called, cannot call again.
However, if I press the back button, login again, the invocation of callback.Onresult
does reach the adapter.
Initially, this made me believe that I was doing something wrong that was related to the lifecycle of the activity/fragment, however changing the place at which the adapter is initialized with the activity context did not change the result.
This post also mentions something about loading a fragment twice in order to get things working, which might explain the observed result when pressing the back button and returning.
EDIT: Added more relevant (updated) code
EDIT 2: Added extra Dagger annotations and refactored code in WorkManager
to make use of RxJava2
EDIT 3: I completely stripped all of the Dagger 2 usages and now the code works, so the problem is Dagger related
EDIT 4: Problem solved, see my post below
The relevant code is as follows:
Fragment
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()
}
DataSource
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
}
Adapter
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
}
}