在使用 FirebaseRecyclerPagingAdapter 时,第二次单击 RecyclerView 中的项目时,片段显示为空
Posted
技术标签:
【中文标题】在使用 FirebaseRecyclerPagingAdapter 时,第二次单击 RecyclerView 中的项目时,片段显示为空【英文标题】:Fragment shows up empty on the second click of an item from a RecyclerView while using FirebaseRecyclerPagingAdapter 【发布时间】:2021-11-10 08:41:44 【问题描述】:在我的应用程序中,当我单击 RecyclerView 中的一个项目(问题)时,它使用 FirebaseRecyclerPagingAdapter 对来自 Firebase 实时数据库的数据进行分页,它会显示有关在另一个片段中单击的项目的详细信息(使用导航组件)。这在第一次单击时可以正常工作,但是当我返回上一个片段并再次单击 RecyclerView 上的同一项目时,不会显示该项目的详细信息。
因为我使用安全参数将项目 id (issueId) 传递给下一个片段,它用于查询 firebase 实时数据库并检索要显示的详细信息,所以我决定在 onViewCreated( ) 只是为了确保在第二次单击时传递了项目 ID,并且正在从数据库中检索详细信息(添加问题的用户的名称),但只是不显示。然后,我注意到一个奇怪的行为。
在第一次单击时,项目 id 会记录到控制台,详细信息也会记录到控制台,并且片段会显示详细信息。但是,在第二次单击时,项目 id 被记录到控制台(显示项目 id 应该被传递),但详细信息没有记录到控制台,也没有显示在片段中(因此片段显示上空)。现在奇怪的部分是,当我导航回上一个片段时,我会看到显示了两次详细信息的日志。
我注意到的另一个奇怪的事情是,RecyclerView 上的每个项目都有这种奇怪的行为,除了最后一个项目。最后一个项目在第二次点击时显示其详细信息,但我点击的任何其他项目都不会。
我还注意到,当我返回导航时,日志会显示我之前点击过两次的每个项目的详细信息,即使我点击的是不同的项目
我将适配器从 FirebaseRecyclerPagingAdapter 更改为 FirebaseRecyclerAdapter,一切正常。当我改回使用 FirebaseRecyclerPagingAdapter 时,存在同样的问题。
这是我的代码中的错误还是 FirebaseRecyclerPagingAdapter 本身的错误。可能是什么问题,我可以做些什么来解决它?
下面是 FirebaseRecyclerPagingAdapter:
package com.colley.android.adapter
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.colley.android.R
import com.colley.android.databinding.ItemIssueBinding
import com.colley.android.model.Issue
import com.colley.android.model.Profile
import com.firebase.ui.database.paging.DatabasePagingOptions
import com.firebase.ui.database.paging.FirebaseRecyclerPagingAdapter
import com.google.firebase.auth.FirebaseUser
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.ValueEventListener
import com.google.firebase.database.ktx.database
import com.google.firebase.database.ktx.getValue
import com.google.firebase.ktx.Firebase
class IssuesPagingAdapter(
options: DatabasePagingOptions<Issue>,
private val context: Context,
private val currentUser: FirebaseUser?,
private val clickListener: IssuePagingItemClickedListener
) : FirebaseRecyclerPagingAdapter<Issue, IssuePagingViewHolder>(options)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): IssuePagingViewHolder
val viewBinding = ItemIssueBinding
.inflate(LayoutInflater.from(parent.context), parent, false)
return IssuePagingViewHolder(viewBinding)
override fun onBindViewHolder(viewHolder: IssuePagingViewHolder, position: Int, model: Issue)
viewHolder.bind(currentUser, model, context, clickListener)
interface IssuePagingItemClickedListener
fun onItemClick(issueId: String, view: View)
fun onItemLongCLicked(issueId: String, view: View)
fun onUserClicked(userId: String, view: View)
class IssuePagingViewHolder (private val itemBinding : ItemIssueBinding) : RecyclerView.ViewHolder(itemBinding.root)
@SuppressLint("SetTextI18n")
fun bind(
currentUser: FirebaseUser?,
issue: Issue, context: Context,
clickListener: IssuesPagingAdapter.IssuePagingItemClickedListener) = with(itemBinding)
//set issue title, body, timeStamp, contributions and endorsements count
issueTitleTextView.text = issue.title
issueBodyTextView.text = issue.body
issueTimeStampTextView.text = issue.timeStamp
contributionsTextView.text = issue.contributionsCount.toString()
endorsementTextView.text = issue.endorsementsCount.toString()
//check if userId is not null
issue.userId?.let userId ->
//retrieve user profile
Firebase.database.reference.child("profiles").child(userId)
.addListenerForSingleValueEvent(
object : ValueEventListener
override fun onDataChange(snapshot: DataSnapshot)
val profile = snapshot.getValue<Profile>()
if (profile != null)
//set the name of user who raised this issue
userNameTextView.text = profile.name
//set the school of the user who raised this issue
userSchoolTextView.text = profile.school
override fun onCancelled(error: DatabaseError)
)
//retrieve user photo
Firebase.database.reference.child("photos").child(userId)
.addListenerForSingleValueEvent(
object : ValueEventListener
override fun onDataChange(snapshot: DataSnapshot)
val photo = snapshot.getValue<String>()
//set photo
if (photo != null)
Glide.with(root.context).load(photo)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE).into(userImageView)
else
Glide.with(root.context).load(R.drawable.ic_person).into(userImageView)
override fun onCancelled(error: DatabaseError)
)
root.setOnClickListener
if(issue.issueId != null)
clickListener.onItemClick(issue.issueId, it)
root.setOnLongClickListener
if(issue.issueId != null)
clickListener.onItemLongCLicked(issue.issueId, it)
true
userNameTextView.setOnClickListener
if(issue.userId != null)
clickListener.onUserClicked(issue.userId, it)
这是显示项目详细信息的片段:
package com.colley.android.view.fragment
import android.os.Bundle
import android.util.Log
import android.view.*
import android.view.View.GONE
import android.view.View.VISIBLE
import android.widget.Toast
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.colley.android.R
import com.colley.android.adapter.IssuesCommentsRecyclerAdapter
import com.colley.android.databinding.FragmentViewIssueBinding
import com.colley.android.model.Comment
import com.colley.android.model.Issue
import com.colley.android.model.Profile
import com.colley.android.view.dialog.IssueCommentBottomSheetDialogFragment
import com.firebase.ui.database.FirebaseRecyclerOptions
import com.firebase.ui.database.ObservableSnapshotArray
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseUser
import com.google.firebase.auth.ktx.auth
import com.google.firebase.database.*
import com.google.firebase.database.ktx.database
import com.google.firebase.database.ktx.getValue
import com.google.firebase.ktx.Firebase
class ViewIssueFragment :
Fragment(),
IssuesCommentsRecyclerAdapter.ItemClickedListener,
IssuesCommentsRecyclerAdapter.DataChangedListener
private val args: ViewIssueFragmentArgs by navArgs()
private var _binding: FragmentViewIssueBinding? = null
private val binding get() = _binding
private lateinit var dbRef: DatabaseReference
private lateinit var auth: FirebaseAuth
private lateinit var currentUser: FirebaseUser
private lateinit var recyclerView: RecyclerView
private lateinit var commentSheetDialog: IssueCommentBottomSheetDialogFragment
private var issue: Issue? = null
private var adapter: IssuesCommentsRecyclerAdapter? = null
private var manager: LinearLayoutManager? = null
private val uid: String
get() = currentUser.uid
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View?
_binding = FragmentViewIssueBinding.inflate(inflater, container, false)
recyclerView = binding?.issuesCommentsRecyclerView!!
return binding?.root
override fun onViewCreated(view: View, savedInstanceState: Bundle?)
super.onViewCreated(view, savedInstanceState)
//initialize Realtime Database
dbRef = Firebase.database.reference
//initialize authentication
auth = Firebase.auth
//initialize currentUser
currentUser = auth.currentUser!!
//log item id
Log.d("Log itemId", args.issueId)
//get a query reference to issue comments //order by time stamp
val commentsRef = dbRef.child("issues").child(args.issueId)
.child("comments").orderByChild("commentTimeStamp")
//the FirebaseRecyclerAdapter class and options come from the FirebaseUI library
//build an options to configure adapter. setQuery takes firebase query to listen to and a
//model class to which snapShots should be parsed
val options = FirebaseRecyclerOptions.Builder<Comment>()
.setQuery(commentsRef, Comment::class.java)
.build()
//initialize issue comments adapter
adapter = IssuesCommentsRecyclerAdapter(
options,
currentUser,
this,
this,
requireContext())
manager = LinearLayoutManager(requireContext())
//reversing layout and stacking fron end so that the most recent comments appear at the top
manager?.reverseLayout = true
manager?.stackFromEnd = true
recyclerView.layoutManager = manager
recyclerView.adapter = adapter
dbRef.child("issues").child(args.issueId).addValueEventListener(
object : ValueEventListener
override fun onDataChange(snapshot: DataSnapshot)
issue = snapshot.getValue<Issue>()
if(issue != null)
//listener for contrbutions count used to set count text
dbRef.child("issues").child(args.issueId)
.child("contributionsCount").addListenerForSingleValueEvent(
object : ValueEventListener
override fun onDataChange(snapshot: DataSnapshot)
val count = snapshot.getValue<Int>()
if(count != null)
binding?.contributionsTextView?.text = count.toString()
override fun onCancelled(error: DatabaseError)
)
//listener for endorsement counts used to set endorsement count text
dbRef.child("issues").child(args.issueId)
.child("endorsementsCount").addListenerForSingleValueEvent(
object : ValueEventListener
override fun onDataChange(snapshot: DataSnapshot)
val count = snapshot.getValue<Int>()
if(count != null)
binding?.endorsementTextView?.text = count.toString()
override fun onCancelled(error: DatabaseError)
)
//set issue title, body and time stamp, these don't need to change
binding?.issueTitleTextView?.text = issue?.title
binding?.issueBodyTextView?.text = issue?.body
binding?.issueTimeStampTextView?.text = issue?.timeStamp.toString()
//listener for user photo
dbRef.child("photos").child(issue?.userId.toString())
.addListenerForSingleValueEvent(
object : ValueEventListener
override fun onDataChange(snapshot: DataSnapshot)
val photo = snapshot.getValue<String>()
if(photo != null)
context?.let context -> binding?.userImageView?.let
imageView ->
Glide.with(context).load(photo).into(
imageView
)
else
context?.let context -> binding?.userImageView?.let
imageView ->
Glide.with(context).load(R.drawable.ic_profile).into(
imageView
)
override fun onCancelled(error: DatabaseError)
)
//listener for profile to set name and school
dbRef.child("profiles").child(issue?.userId.toString())
.addListenerForSingleValueEvent(
object : ValueEventListener
override fun onDataChange(snapshot: DataSnapshot)
val profile = snapshot.getValue<Profile>()
if (profile != null)
//log name details to console
profile.name?.let Log.d("Log Details", it)
binding?.userNameTextView?.text = profile.name
binding?.userSchoolTextView?.text = profile.school
override fun onCancelled(error: DatabaseError)
)
override fun onCancelled(error: DatabaseError)
)
binding?.commentLinearLayout?.setOnClickListener
commentSheetDialog = IssueCommentBottomSheetDialogFragment(
requireContext(),
requireView())
commentSheetDialog.arguments = bundleOf("issueIdKey" to args.issueId)
commentSheetDialog.show(parentFragmentManager, null)
binding?.endorseLinearLayout?.setOnClickListener
//update contributions count
dbRef.child("issues").child(args.issueId).child("endorsementsCount")
.runTransaction(
object : Transaction.Handler
override fun doTransaction(currentData: MutableData): Transaction.Result
//retrieve the current value of endorsement count at this location
var endorsementsCount = currentData.getValue<Int>()
if (endorsementsCount != null)
//increase the count by 1
endorsementsCount++
//reassign the value to reflect the new update
currentData.value = endorsementsCount
//set database issue value to the new update
return Transaction.success(currentData)
override fun onComplete(
error: DatabaseError?,
committed: Boolean,
currentData: DataSnapshot?
)
if (error == null && committed)
Toast.makeText(requireContext(), "Endorsed", Toast.LENGTH_SHORT)
.show()
)
//view profile when clicked
binding?.userImageView?.setOnClickListener
val action = issue?.userId?.let it1 ->
ViewIssueFragmentDirections.actionViewIssueFragmentToUserInfoFragment(it1)
if (action != null)
parentFragment?.findNavController()?.navigate(action)
//view user profile when clicked
binding?.userNameTextView?.setOnClickListener
val action = issue?.userId?.let it1 ->
ViewIssueFragmentDirections.actionViewIssueFragmentToUserInfoFragment(it1)
if (action != null)
parentFragment?.findNavController()?.navigate(action)
override fun onItemClick(comment: Comment, view: View)
//expand comment
override fun onItemLongCLicked(comment: Comment, view: View)
//create option to delete
//create option to respond
//view user profile
override fun onUserClicked(userId: String, view: View)
val action = ViewIssueFragmentDirections.actionViewIssueFragmentToUserInfoFragment(userId)
parentFragment?.findNavController()?.navigate(action)
override fun onStart()
super.onStart()
adapter?.startListening()
override fun onStop()
super.onStop()
adapter?.stopListening()
override fun onDestroy()
super.onDestroy()
_binding = null
override fun onDataAvailable(snapshotArray: ObservableSnapshotArray<Comment>)
//dismiss progress bar once snapshot is available
binding?.issuesCommentProgressBar?.visibility = GONE
//show that there are no comments if snapshot is empty else hide view
//show recycler view if snapshot is not empty else hide
if (snapshotArray.isEmpty())
binding?.noCommentsLayout?.visibility = VISIBLE
else
binding?.noCommentsLayout?.visibility = GONE
binding?.issuesCommentsRecyclerView?.visibility = VISIBLE
这是带有 recyclerView 的片段,显示了我如何初始化适配器:
package com.colley.android.view.fragment
import android.os.Bundle
import android.view.*
import android.view.View.GONE
import android.view.View.VISIBLE
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.paging.LoadState
import androidx.paging.PagingConfig
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.colley.android.R
import com.colley.android.adapter.IssuesPagingAdapter
import com.colley.android.databinding.FragmentIssuesBinding
import com.colley.android.model.Issue
import com.firebase.ui.database.paging.DatabasePagingOptions
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseUser
import com.google.firebase.auth.ktx.auth
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseReference
import com.google.firebase.database.ktx.database
import com.google.firebase.ktx.Firebase
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
class IssuesFragment :
Fragment(),
IssuesPagingAdapter.IssuePagingItemClickedListener
private var _binding: FragmentIssuesBinding? = null
private val binding get() = _binding!!
private lateinit var dbRef: DatabaseReference
private lateinit var auth: FirebaseAuth
private lateinit var currentUser: FirebaseUser
private var adapter: IssuesPagingAdapter? = null
private var manager: LinearLayoutManager? = null
private lateinit var recyclerView: RecyclerView
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
private val uid: String
get() = currentUser.uid
override fun onCreate(savedInstanceState: Bundle?)
super.onCreate(savedInstanceState)
//fragment can participate in populating the options menu
setHasOptionsMenu(true)
//initialize Realtime Database
dbRef = Firebase.database.reference
//initialize authentication
auth = Firebase.auth
//initialize currentUser
currentUser = auth.currentUser!!
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater)
super.onCreateOptionsMenu(menu, inflater)
menu.clear()
inflater.inflate(R.menu.isssues_menu, menu)
override fun onOptionsItemSelected(item: MenuItem): Boolean
return when (item.itemId)
R.id.search_issues_menu_item ->
Toast.makeText(context, "Searching issues", Toast.LENGTH_LONG).show()
true
else -> super.onOptionsItemSelected(item)
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View?
_binding = FragmentIssuesBinding.inflate(inflater, container, false)
recyclerView = binding.issueRecyclerView
swipeRefreshLayout = binding.swipeRefreshLayout
return binding.root
override fun onViewCreated(view: View, savedInstanceState: Bundle?)
super.onViewCreated(view, savedInstanceState)
//get a query reference to issues
val issuesQuery = dbRef.child("issues")
//configuration for how the FirebaseRecyclerPagingAdapter should load pages
val config = PagingConfig(
pageSize = 30,
prefetchDistance = 15,
enablePlaceholders = false
)
//Options to configure an FirebasePagingAdapter
val options = DatabasePagingOptions.Builder<Issue>()
.setLifecycleOwner(viewLifecycleOwner)
.setQuery(issuesQuery, config, Issue::class.java)
.setDiffCallback(object : DiffUtil.ItemCallback<DataSnapshot>()
override fun areItemsTheSame(
oldItem: DataSnapshot,
newItem: DataSnapshot
): Boolean
return oldItem.getValue(Issue::class.java)?.issueId == newItem.getValue(Issue::class.java)?.issueId
override fun areContentsTheSame(
oldItem: DataSnapshot,
newItem: DataSnapshot
): Boolean
return oldItem.getValue(Issue::class.java) == newItem.getValue(Issue::class.java)
)
.build()
//instantiate adapter
adapter = IssuesPagingAdapter(
options,
requireContext(),
currentUser,
this)
//Perform some action every time data changes or when there is an error.
viewLifecycleOwner.lifecycleScope.launch
adapter?.loadStateFlow?.collectLatest loadStates ->
when (loadStates.refresh)
is LoadState.Error ->
// The initial load failed. Call the retry() method
// in order to retry the load operation.
Toast.makeText(context, "Error fetching issues! Retrying..", Toast.LENGTH_SHORT).show()
//display no posts available at the moment
binding.noIssuesLayout.visibility = VISIBLE
adapter?.retry()
is LoadState.Loading ->
// The initial Load has begun
// ...
swipeRefreshLayout.isRefreshing = true
is LoadState.NotLoading ->
// The previous load (either initial or additional) completed
swipeRefreshLayout.isRefreshing = false
//remove display no posts available at the moment
binding.noIssuesLayout.visibility = GONE
when (loadStates.append)
is LoadState.Error ->
// The additional load failed. Call the retry() method
// in order to retry the load operation.
adapter?.retry()
is LoadState.Loading ->
// The adapter has started to load an additional page
// ...
swipeRefreshLayout.isRefreshing = true
is LoadState.NotLoading ->
if (loadStates.append.endOfPaginationReached)
// The adapter has finished loading all of the data set
swipeRefreshLayout.isRefreshing = false
//set recycler view layout manager
manager = LinearLayoutManager(requireContext())
recyclerView.layoutManager = manager
//initialize adapter
recyclerView.adapter = adapter
swipeRefreshLayout.setOnRefreshListener
adapter?.refresh()
override fun onDestroy()
super.onDestroy()
_binding = null
//navigate to new fragment with issue id
override fun onItemClick(issueId: String, view: View)
val action = HomeFragmentDirections.actionHomeFragmentToViewIssueFragment(issueId)
parentFragment?.findNavController()?.navigate(action)
override fun onItemLongCLicked(issueId: String, view: View)
override fun onUserClicked(userId: String, view: View)
val action = HomeFragmentDirections.actionHomeFragmentToUserInfoFragment(userId)
parentFragment?.findNavController()?.navigate(action)
Before click
After first click
After second click
【问题讨论】:
如果您遇到问题,最好在发布问题时创建MCVE。您为此问题发布了近 700(七百)行行代码。人们需要解析和尝试在线调试的内容很多。请编辑您的问题并隔离问题,这样可以增加获得帮助的机会。 我的错!我发现了这个错误。我应该使用 addListenerForSingleValueEvent 而不是 addValueEventListener 从显示单击项目详细信息的片段中的数据库中查询项目详细信息(问题),否则删除 onStop() 中的 addValueEventListener 以便侦听器不再附加到数据库时我导航回上一个片段。 【参考方案1】:使用addListenerForSingleValueEvent
而不是addValueEventListener
从显示点击项目详细信息的片段中的数据库中查询项目详细信息(问题)。否则,删除onStop()
中的addValueEventListener
,以便在导航回上一个片段时不再将侦听器附加到数据库。
【讨论】:
以上是关于在使用 FirebaseRecyclerPagingAdapter 时,第二次单击 RecyclerView 中的项目时,片段显示为空的主要内容,如果未能解决你的问题,请参考以下文章
为啥在使用 unicode 时我不能在 :before :after 内容之后使用空格
在哪里使用 callable 以及在哪里使用 Runnable Interface?
在 Observable RxSwift 中使用 'asPromise()' 可以在 PromiseKit Promise 中使用吗?
可以在 SELECT 查询中使用 IF() 但不能在 UPDATE 中使用
使用 React,在使用 react-transition-group 时,在 StrictMode 中不推荐使用 findDOMNode 作为警告