在使用 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 作为警告

在本地为 Django 使用 SQLite,在服务器上使用 Postgres