RecyclerView结合LiveData时Item点击响应异常问题求助
问题:RecyclerView Item点击需多次触发才能响应
开发Android应用时,使用RecyclerView展示LiveData提供的用户位置数据,但遇到RecyclerView Item点击需要3-4次才能触发响应的异常,希望找到解决方法使Item点击即时生效。
HomeFragment代码
package com.kttelematicsolutions.routeplay.fragments import android.content.Intent import android.os.Build import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.annotation.RequiresApi import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.gms.location.FusedLocationProviderClient import com.google.android.gms.location.LocationServices import com.kttelematicsolutions.routeplay.LocationService import com.kttelematicsolutions.routeplay.MainViewModel import com.kttelematicsolutions.routeplay.R import com.kttelematicsolutions.routeplay.adapters.LocationAdapter import com.kttelematicsolutions.routeplay.databinding.FragmentHomeBinding import com.kttelematicsolutions.routeplay.realmClasses.LocationData import com.kttelematicsolutions.routeplay.realmClasses.User class HomeFragment : Fragment(), LocationAdapter.OnItemClickListener { private lateinit var binding: FragmentHomeBinding private val viewModel: MainViewModel by viewModels() private lateinit var userLocationListAdapter: LocationAdapter private lateinit var fusedLocationClient: FusedLocationProviderClient private val navController by lazy { findNavController() } private var userId = "" override fun onStart() { super.onStart() viewModel.currentUser.observe(viewLifecycleOwner) { currentUser -> if (currentUser != null) { val serviceIntent = Intent(requireContext(), LocationService::class.java) serviceIntent.putExtra("userId", currentUser.username) requireContext().startService(serviceIntent) } } } override fun onStop() { super.onStop() requireContext().stopService(Intent(requireContext(), LocationService::class.java)) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) fusedLocationClient = LocationServices.getFusedLocationProviderClient(requireActivity()) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { binding = FragmentHomeBinding.inflate(inflater, container, false) return binding.root } @RequiresApi(Build.VERSION_CODES.O) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val locationRecycler = view.findViewById<RecyclerView>(R.id.location_recycler) val layoutManager = LinearLayoutManager(requireContext()) locationRecycler.layoutManager = layoutManager viewModel.currentUser() binding.usernameLayout.setOnClickListener { showUserListPopup() } viewModel.currentUser.observe(viewLifecycleOwner) { viewModel.currentUser() if (it != null) { bindViewData(it) it.username?.let { it1 -> userId = it1 viewModel.getLocationOfUser(it1) } } } userLocationListAdapter = LocationAdapter(emptyList() , this) locationRecycler.adapter = userLocationListAdapter viewModel.usersLocationLiveData.observe(viewLifecycleOwner) { locationData -> userLocationListAdapter.updateData(locationData) } } private fun bindViewData(currentUser: User) { binding.usernameText.text = currentUser.username binding.title2.text = getString(R.string.location_log, currentUser.username) } private fun showUserListPopup() { val userListDialog = UserListDialogFragment() userListDialog.show(childFragmentManager, "UserListDialog") } override fun onItemClick(location : LocationData) { binding.progress.visibility = View.VISIBLE val bundle = Bundle() bundle.putString("id", location.id) bundle.putString("userId", location.userId) bundle.putDouble("latitude", location.latitude) bundle.putDouble("longitude", location.longitude) bundle.putString("timestamp", location.timestamp) Log.d("TAG", "onItemClick: works") navController.navigate(R.id.action_homeFragment_to_mapsFragment, bundle) Log.d("TAG", "onItemClick: works2") } }
LocationAdapter代码
package com.kttelematicsolutions.routeplay.adapters import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.ImageButton import android.widget.RelativeLayout import android.widget.TextView import androidx.cardview.widget.CardView import androidx.recyclerview.widget.RecyclerView import com.kttelematicsolutions.routeplay.realmClasses.LocationData import com.kttelematicsolutions.routeplay.R class LocationAdapter( private var locations: List<LocationData>, private var onItemClickListener: OnItemClickListener ) : RecyclerView.Adapter<LocationAdapter.LocationViewHolder>() { interface OnItemClickListener { fun onItemClick(location: LocationData) } fun updateData(updatedLocationData: List<LocationData>) { locations = updatedLocationData notifyDataSetChanged() } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LocationViewHolder { val view = LayoutInflater.from(parent.context) .inflate(R.layout.location_log_item, parent, false) return LocationViewHolder(view) } override fun onBindViewHolder(holder: LocationViewHolder, position: Int) { val location = locations[position] holder.bind(location) } override fun getItemCount(): Int { return locations.size } inner class LocationViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { private val locationNameTextView: TextView = itemView.findViewById(R.id.location_name) private val cityNameTextView: TextView = itemView.findViewById(R.id.city_name) private val timeStampTextView: TextView = itemView.findViewById(R.id.time_stamp) private val viewButton: Button = itemView.findViewById(R.id.viewButton) private val locationLayout: RelativeLayout = itemView.findViewById(R.id.overlay) fun bind(location: LocationData) { locationNameTextView.text = location.latitude.toString() cityNameTextView.text = location.longitude.toString() timeStampTextView.text = location.timestamp viewButton.setOnClickListener { onItemClickListener.onItemClick(location) } locationLayout.setOnClickListener { onItemClickListener.onItemClick(location) } } } }
MainViewModel代码
package com.kttelematicsolutions.routeplay import android.location.Location import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.kttelematicsolutions.routeplay.realmClasses.LocationData import com.kttelematicsolutions.routeplay.realmClasses.Otp import com.kttelematicsolutions.routeplay.realmClasses.RealmApplication import com.kttelematicsolutions.routeplay.realmClasses.User import io.realm.kotlin.UpdatePolicy import io.realm.kotlin.delete import io.realm.kotlin.ext.query import io.realm.kotlin.query.RealmResults import kotlinx.coroutines.launch class MainViewModel : ViewModel() { private val realm = RealmApplication.realm private val _usersLiveData = MutableLiveData<List<User>>() val usersLiveData: LiveData<List<User>> = _usersLiveData private val _currentUser = MutableLiveData<User>() val currentUser : LiveData<User> = _currentUser private val _usersLocationLiveData = MutableLiveData<List<LocationData>>() val usersLocationLiveData: LiveData<List<LocationData>> = _usersLocationLiveData fun addUser( email : String , username : String , password : String , otp : String ) { viewModelScope.launch { realm.write { val user = User().apply { this.username = username this.email = email this.password = password } copyToRealm(user , updatePolicy = UpdatePolicy.ALL) val generateOtp = Otp().apply { this.code = otp this.id = user.email.toString() } copyToRealm(generateOtp , updatePolicy = UpdatePolicy.ALL) } } } fun verifyOtp( enteredOtp : String , email : String , callback : (success : Boolean) -> Unit ) { viewModelScope.launch { try { val otpObject = realm.query<Otp>("id == $0" , email).find().first() if (otpObject.code == enteredOtp) { realm.write { this.delete<Otp>() // Delete the used OTP } callback(true) realm.query<User>("email == $0", email).find().first().also { realm.writeBlocking { findLatest(it)?.isLoggedIn = true } } } else { callback(false) } } catch (exception : Exception) { Log.e("OtpViewModel" , "Error verifying user: ${exception.message}") } } } fun loginUser(email : String, password : String, otp : String, callback : (success : Boolean) -> Unit){ viewModelScope.launch { try { val userObject = realm.query<User>("email == $0 AND password == $1", email, password).find().first() if(userObject.isLoggedIn){ val generateOtp = Otp().apply { this.code = otp this.id = email } realm.write { copyToRealm(generateOtp , updatePolicy = UpdatePolicy.ALL) } realm.query<User>("email == $0", email).find().first().also { realm.writeBlocking { findLatest(it)?.isLoggedIn = true findLatest(it)?.currentUser = true } } callback(true) }else{ callback(false) } }catch (exception : Exception){ callback(false) Log.e("LoginViewModel" , "User Login Failed: ${exception.message}") } } } fun checkUserAlreadyExists(email: String, username: String, callback : (success : Boolean) -> Unit){ viewModelScope.launch { try { realm.query<User>("email == $0 OR username == $1", email, username).find().first() callback(true) }catch (exception : Exception){ callback(false) Log.e("SignupViewModel" , "User not exists: ${exception.message}") } } } fun setCurrentUser(selectedUser: String) { viewModelScope.launch { try { realm.query<User>("email == $0", selectedUser).find().first().also { realm.writeBlocking { findLatest(it)?.currentUser = true } Log.d("SignupViewModel" , "User updated: Done") } val otherUsers = realm.query<User>("email != $0", selectedUser) for (user in otherUsers.find()) { realm.write{ findLatest(user)?.currentUser = false } } Log.d("SignupViewModel" , "Other User update: Done") }catch (exception : Exception){ Log.e("SignupViewModel" , "User not exists: ${exception.message}") } } } fun getAllUsers() { viewModelScope.launch { val userResults: RealmResults<User> = realm.query<User>().find() val users = userResults.toList() _usersLiveData.postValue(users) } } fun currentUser(){ viewModelScope.launch { try { val currentUserResult = realm.query<User>("currentUser==$0", true).find().first() _currentUser.postValue(currentUserResult) }catch (exception : Exception){ Log.e("HomeViewModel" , "User not found: ${exception.message}") } } } fun saveLocationDataToRealm(userId: String, locationResult: Location, timestamp: String) { viewModelScope.launch { realm.write { val locationData = LocationData().apply { this.userId = userId this.latitude = locationResult.latitude this.longitude = locationResult.longitude this.timestamp = timestamp } copyToRealm(locationData, UpdatePolicy.ALL) Log.e("HomeViewModel" , "User location: $locationData") } } } fun getLocationOfUser(userId: String){ viewModelScope.launch{ try { val currentUserLocationResult = realm.query<LocationData>("userId==$0", userId).find().toList() _usersLocationLiveData.postValue(currentUserLocationResult) }catch (exception: Exception){ Log.e("HomeViewModel" , "User location not found: ${exception.message}") } } } fun getPreviousLocations(userId: String, currentLocationId: String): LiveData<List<LocationData>> { val previousLocationsLiveData = MutableLiveData<List<LocationData>>() viewModelScope.launch { try { val previousLocationsResult = realm.query<LocationData>("userId == $0 AND id <= $1", userId, currentLocationId).find() previousLocationsLiveData.postValue(previousLocationsResult) } catch (exception: Exception) { Log.e("MainViewModel", "Error retrieving previous locations: ${exception.message}") } } return previousLocationsLiveData } }
解决方法
1. 修复LiveData循环触发问题
在HomeFragment的onViewCreated方法中,viewModel.currentUser.observe回调内重复调用viewModel.currentUser()会导致LiveData循环发送数据,RecyclerView持续刷新打断点击响应,移除该重复调用:
viewModel.currentUser.observe(viewLifecycleOwner) { // 移除 viewModel.currentUser() 这行代码 if (it != null) { bindViewData(it) it.username?.let { it1 -> userId = it1 viewModel.getLocationOfUser(it1) } } }
2. 优化Adapter数据更新逻辑
当前使用notifyDataSetChanged()会触发RecyclerView全量刷新,导致Item状态重置。改用AsyncListDiffer实现局部高效刷新:
class LocationAdapter( private var onItemClickListener: OnItemClickListener ) : RecyclerView.Adapter<LocationAdapter.LocationViewHolder>() { interface OnItemClickListener { fun onItemClick(location: LocationData) } private val diffCallback = object : DiffUtil.ItemCallback<LocationData>() { override fun areItemsTheSame(oldItem: LocationData, newItem: LocationData): Boolean { return oldItem.id == newItem.id // 依赖LocationData的唯一id判断 } override fun areContentsTheSame(oldItem: LocationData, newItem: LocationData): Boolean { return oldItem.latitude == newItem.latitude && oldItem.longitude == newItem.longitude && oldItem.timestamp == newItem.timestamp && oldItem.userId == newItem.userId } } private val differ = AsyncListDiffer(this, diffCallback) fun updateData(updatedLocationData: List<LocationData>) { differ.submitList(updatedLocationData) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LocationViewHolder { val view = LayoutInflater.from(parent.context) .inflate(R.layout.location_log_item, parent, false) return LocationViewHolder(view) } override fun onBindViewHolder(holder: LocationViewHolder, position: Int) { val location = differ.currentList[position] holder.bind(location) } override fun getItemCount(): Int { return differ.currentList.size } inner class LocationViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { private val locationNameTextView: TextView = itemView.findViewById(R.id.location_name) private val cityNameTextView: TextView = itemView.findViewById(R.id.city_name) private val timeStampTextView: TextView = itemView.findViewById(R.id.time_stamp) private val viewButton: Button = itemView.findViewById(R.id.viewButton) private val locationLayout: RelativeLayout = itemView.findViewById(R.id.overlay) fun bind(location: LocationData) { locationNameTextView.text = location.latitude.toString() cityNameTextView.text = location.longitude.toString() timeStampTextView.text = location.timestamp viewButton.setOnClickListener { onItemClickListener.onItemClick(location) } locationLayout.setOnClickListener { onItemClickListener.onItemClick(location) } } } }
同时修改Fragment中Adapter的初始化代码:
userLocationListAdapter = LocationAdapter(this) // 移除初始空列表参数 locationRecycler.adapter = userLocationListAdapter
3. 避免重复添加LiveData观察者
将onStart中viewModel.currentUser.observe的逻辑移至onViewCreated,避免每次onStart都添加新观察者,减少不必要的回调:
@RequiresApi(Build.VERSION_CODES.O
相关产品推荐
相关产品推荐

