使用FirebaseRecyclerAdapter读取复杂嵌套Firebase实时数据库的POJO构建问题

问题描述
userId下的首个数据节点存储链表类型数据,每个索引包含data和info两部分。目前的困惑点在于索引data下的唯一动态键-MkqHbtLzqzI...,每个索引data下都有多个这类动态生成的不同键,其中Matrix是List<Float>类型。
我们需要使用FirebaseRecyclerAdapter,该组件的setQuery方法要求指定映射的类类型,但不清楚如何为上述结构定义对应的POJO类(Kotlin中为data class)。
需要加载到RecyclerView的对象从2这个索引开始。截图中6154...是userId,其下有一个名为data的节点,该节点存储了linkedList<Long>格式的条目列表,需要将这些条目展示在recyclerView中。
尝试编写了如下data classes,但存在的问题是这些类既无法获取到索引值(截图中的2),也无法获取到动态唯一键(-MkqH...)。
现有POJO定义
UserData(用于firebaseRecyclerViewAdapter的类)
@Keep @IgnoreExtraProperties data class UserData( var data: UserPrefsData? = null, var info: UserPrefsInfo? = null ) { @Exclude fun toMap(): Map<String, Any?> { return mapOf( "data" to data, "info" to info ) } }
UserPrefsData
@Keep @IgnoreExtraProperties data class UserPrefsData( var color: String = BRUSH_BLACK, var stroke: Float = 8f, var data: String = "", var shape: Shape? = null, var fill: Boolean = false, var matrix : ArrayList<Float>? = null ) { @Exclude fun toMap(): Map<String, Any?> { return mapOf( "color" to color, "stroke" to stroke, "data" to data, "shape" to shape, "fill" to fill, "matrix" to matrix ) } }
UserPrefsInfo
@Keep @IgnoreExtraProperties data class PageInfo( var color: String = BRUSH_WHITE, var backGround: BackGround = BackGround.COLOR, var orientation: Int = ORIENTATION_PORTRAIT, var previous: Int = -1, var next: Int = -1 ) { @Exclude fun toMap(): Map<String, Any?> { return mapOf( "color" to color, "backGround" to backGround, "orientation" to orientation, "previous" to previous, "next" to next ) } }
Activity代码
private fun initFirebaseAdapter(): FirebaseAdapter? { val userDataRef = databaseReference.child("data") val options = FirebaseRecyclerOptions.Builder<UserData>() .setLifecycleOwner(this) .setQuery(userDataRef, UserData::class.java) .build() return FirebaseAdapter( options, isAdmin, rvItemEventListener ) }
private fun setRecyclerViewAdapter( recyclerView: RecyclerView?, recyclerViewAdapter: RecyclerView.Adapter<out RecyclerView.ViewHolder>? ) { recyclerView?.adapter = recyclerViewAdapter }
rvFirebaseAdapter = initFirebaseAdapter() setRecyclerViewAdapter(binding.idVRv, rvFirebaseAdapter)
FirebaseAdapter代码
class FirebaseAdapter(var options: FirebaseRecyclerOptions<UserData>, val showDeleteOption: Boolean, private val callbackListener: Callbacks.RecyclerViewItemCallback): FirebaseRecyclerAdapter<UserData, FirebaseAdapter.UserViewHolder>(options) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder { val itemView = LayoutInflater.from(parent.context).inflate(R.layout.user_item, parent, false) val binding = UserItemBinding.bind(itemView) return UserViewHolder(binding, itemView) } override fun onBindViewHolder(holder: UserViewHolder, position: Int) { Timber.d(" :$LOG_APP_NAME: FirebaseAdapter: :onBindViewHolder: position: $position item: ${getItem(position)} _getting data here - it is working_") if (position != RecyclerView.NO_POSITION) { bindData(holder, position) } } private fun bindData(holder: UserViewHolder, position: Int) { Timber.d(" :$LOG_APP_NAME: FirebaseAdapter: :bindData: position: $position" _This is also working_) holder.binding.idVTvPageNumber.text = (position + 1).toString() } override fun getItemCount(): Int { Timber.d(" :$LOG_APP_NAME: FirebaseAdapter: :superGetItemCount: ${super.getItemCount()}") return super.getItemCount() } inner class UserViewHolder(val binding: UserItemBinding, itemView: View): RecyclerView.ViewHolder(itemView) { init { if (showDeleteOption) { binding.idVIvDelete.setOnClickListener { Timber.d(" :$LOG_APP_NAME: FirebaseAdapter: UserViewHolder: :bindingAdapterPosition: $bindingAdapterPosition absoluteAdapterPosition: $absoluteAdapterPosition" _This is working_) onDeleteItem(adapterPosition, binding.idVIvDelete) } } else { binding.idVIvDelete.visibility = View.GONE } } } private fun onDeleteItem(adapterPosition: Int, idVIvDelete: View) { if (adapterPosition != RecyclerView.NO_POSITION) { Timber.d(" :$LOG_APP_NAME: FirebaseAdapter: :onDeleteItem: position: $adapterPosition" _This is also working_) val item = getItem(adapterPosition) getRef(adapterPosition).removeValue() callbackListener.onDeleteItem(adapterPosition, item, idVIvDelete, itemCount) notifyItemRemoved(adapterPosition) } } override fun onBindViewHolder( holder: UserViewHolder, position: Int, model: UserData ) { Timber.d(" :$LOG_APP_NAME: FirebaseAdapter: :onBindViewHolder: firebase: position: $position UserData: $model" _This is never being called_) if (position != RecyclerView.NO_POSITION) { bindData(holder, position) } } override fun onChildChanged( type: ChangeEventType, snapshot: DataSnapshot, newIndex: Int, oldIndex: Int ) { super.onChildChanged(type, snapshot, newIndex, oldIndex) Timber.d(" :$LOG_APP_NAME: FirebaseAdapter: :onChildChanged: snapshot: $snapshot" _This is working_) } override fun onDataChanged() { super.onDataChanged() Timber.d(" :$LOG_APP_NAME: FirebaseAdapter: :onDataChanged: ") } override fun onError(error: DatabaseError) { super.onError(error) Timber.d(" :$LOG_APP_NAME: FirebaseAdapter: :onError: $error") } override fun getItem(position: Int): UserData { Timber.d(" :$LOG_APP_NAME: FirebaseAdapter: :getItem: position: $position") return super.getItem(position) } override fun getRef(position: Int): DatabaseReference { Timber.d(" :$LOG_APP_NAME: FirebaseAdapter: :getRef: position: $position") return super.getRef(position) } }
2021年10月6日更新
默认的onBindViewHolder会调用FirebaseRecyclerAdapter的onBindViewHolder,之前的实现没有加super调用,因此删除onBindViewHolder(holder: UserViewHolder, position: Int)方法后,onBindViewHolder(holder: UserViewHolder, position: Int, model: UserData)可以正常被触发,但仍然不知道如何获取上述索引值和动态唯一键。
解决方案
1. 核心逻辑说明
索引值和动态键不需要在POJO类中定义,Firebase SDK本身提供了对应的获取方式:
- 外层的数字索引(截图中的
2):在适配器中调用getRef(position).key即可直接拿到该节点的键值 - data下的动态唯一键:将
UserData类中data字段的类型改为Map<String, UserPrefsData>,Map的key就是-MkqH...这类动态生成的唯一键,value是对应的数据实体
2. 调整POJO定义
@Keep @IgnoreExtraProperties data class UserData( // 改为Map存储动态键和对应的数据实体 var data: Map<String, UserPrefsData>? = null, // 修正命名,原有类名为PageInfo不是UserPrefsInfo var info: PageInfo? = null ) { @Exclude fun toMap(): Map<String, Any?> { return mapOf( "data" to data, "info" to info ) } }
其余UserPrefsData和PageInfo类无需调整。
3. 调整查询范围(从索引2开始加载)
private fun initFirebaseAdapter(): FirebaseAdapter? { val userDataRef = databaseReference.child("data") // 按key排序,从值为2的节点开始查询 val query = userDataRef.orderByKey().startAt("2") val options = FirebaseRecyclerOptions.Builder<UserData>() .setLifecycleOwner(this) .setQuery(query, UserData::class.java) .build() return FirebaseAdapter( options, isAdmin, rvItemEventListener ) }
4. 适配器中取值示例
override fun onBindViewHolder(holder: UserViewHolder, position: Int, model: UserData) { // 拿到外层数字索引值 val indexKey = getRef(position).key // 遍历拿到data下所有动态键和对应的数据 model.data?.forEach { (dynamicKey, prefsData) -> // dynamicKey就是-MkqH...这类动态唯一键 val matrix = prefsData.matrix // 后续绑定视图的业务逻辑 } // 拿info字段的方式和原有逻辑一致 val pageInfo = model.info }
内容的提问来源于stack exchange,提问作者Sagar Patel

