Kotlin Room MVVM架构下如何筛选展示当前队伍的LiveData球员列表
问题描述
- 在第一个Fragment中创建了对应
team_table表的队伍名称 - 跳转至球员列表Fragment时,需要页面默认仅展示当前所选队伍对应的球员列表
- 当前异常表现:仅当更新球员名称后返回球员列表页时筛选逻辑可正常生效,其余场景下页面会展示所有队伍的全部球员
原有实现代码
UserViewModel
class UserViewModel(application: Application): AndroidViewModel(application) { val readAllDataTeam: LiveData<List<Team>>? val readAllDataUser: LiveData<List<User>>? val readAllDataEval: LiveData<List<Eval>>? val readAllDataUserWithEval: LiveData<List<UserWithEval>>? val readAllDataTeamWithUser: LiveData<List<TeamWithUser>>? private val repositoryTeam: UserRepository private val repositoryUser: UserRepository private val repositoryEval: UserRepository private val repositoryUserWithEval: UserRepository private val repositoryTeamWithUser: UserRepository init { val teamDao = UserDatabase.getDatabase(application)?.userDao() repositoryTeam = UserRepository(teamDao!!) readAllDataTeam = repositoryTeam.readAllDataTeam val userDao = UserDatabase.getDatabase(application)?.userDao() repositoryUser = UserRepository(userDao!!) readAllDataUser = repositoryUser.readAllDataUser val evalDao = UserDatabase.getDatabase(application)?.userDao() repositoryEval = UserRepository(evalDao!!) readAllDataEval = repositoryEval.readAllDataEval val userEvalDao = UserDatabase.getDatabase(application)?.userDao() repositoryUserWithEval = UserRepository(userEvalDao!!) readAllDataUserWithEval= repositoryUserWithEval.readAllDataUserEval val teamUserDao = UserDatabase.getDatabase(application)?.userDao() repositoryTeamWithUser = UserRepository(teamUserDao!!) readAllDataTeamWithUser = repositoryTeamWithUser.readAllDataTeamWithUser } //Team fun addTeam(team: Team) { viewModelScope.launch(Dispatchers.IO) { repositoryTeam.addTeam(team) } } fun updateTeam(team: Team) { viewModelScope.launch(Dispatchers.IO) { repositoryTeam.updateTeam(team) } } fun deleteTeam(team: Team) { viewModelScope.launch(Dispatchers.IO) { repositoryTeam.deleteTeam(team) } } fun deleteAllTeams() { viewModelScope.launch(Dispatchers.IO) { repositoryTeam.deleteAllTeams() } } //Eval fun addEval(eval: Eval) { viewModelScope.launch(Dispatchers.IO) { repositoryEval.addEval(eval) } } fun updateEval(eval: Eval) { viewModelScope.launch(Dispatchers.IO) { repositoryEval.updateEval(eval) } } fun deleteEval(eval: Eval) { viewModelScope.launch(Dispatchers.IO) { repositoryEval.deleteEval(eval) } } fun deleteAllEvals() { viewModelScope.launch(Dispatchers.IO) { repositoryEval.deleteAllEvals() } } //User fun addUser(user: User) { viewModelScope.launch(Dispatchers.IO) { repositoryUser.addUser(user) } } fun updateUser(user: User) { viewModelScope.launch(Dispatchers.IO) { repositoryUser.updateUser(user) } } fun deleteUser(user: User) { viewModelScope.launch(Dispatchers.IO) { repositoryUser.deleteUser(user) } } fun deleteAllUsers() { viewModelScope.launch(Dispatchers.IO) { repositoryUser.deleteAllUsers() } } fun searchDatabase(searchQuery: String): LiveData<List<User>>? { return repositoryUser.searchDatabase(searchQuery) } fun searchDatabaseTeam(searchQuery: String): LiveData<List<Team>>? { return repositoryUser.searchDatabaseTeam(searchQuery) } fun getTeam(searchTeam: String): LiveData<List<User>>? { return repositoryUser.getTeam(searchTeam) } }
UserRepository
class UserRepository (private val userDao: UserDao) { val readAllDataUser: LiveData<List<User>>? = userDao.getUserData() val readAllDataEval: LiveData<List<Eval>>? = userDao.getEvalData() val readAllDataTeam: LiveData<List<Team>>? = userDao.getTeamData() val readAllDataUserEval: LiveData<List<UserWithEval>>? = userDao.getAllUserWithEval() val readAllDataTeamWithUser: LiveData<List<TeamWithUser>>? = userDao.getAllTeamWithUser() //User suspend fun addUser(user: User) { userDao.addUser(user) } suspend fun updateUser(user: User) { userDao.updateUser(user) } suspend fun deleteUser(user: User) { userDao.deleteUser(user) } suspend fun deleteAllUsers() { userDao.deleteAllUsers() } //Eval suspend fun addEval(eval: Eval) { userDao.addEval(eval) } suspend fun updateEval(eval: Eval) { userDao.updateEval(eval) } suspend fun deleteEval(eval: Eval) { userDao.deleteEval(eval) } suspend fun deleteAllEvals() { userDao.deleteAllEvals() } //Team suspend fun addTeam(team: Team) { userDao.addTeam(team) } suspend fun updateTeam(team: Team) { userDao.updateTeam(team) } suspend fun deleteTeam(team: Team) { userDao.deleteTeam(team) } suspend fun deleteAllTeams() { userDao.deleteAllTeams() } fun searchDatabase(searchQuery: String): LiveData<List<User>>? { return userDao.searchDatabase(searchQuery) } fun searchDatabaseTeam(searchQuery: String): LiveData<List<Team>>? { return userDao.searchDatabaseTeam(searchQuery) } fun getTeam(searchTeam: String): LiveData<List<User>>? { return userDao.getTeam(searchTeam) } }
UserDao
@Dao interface UserDao { @Query("SELECT * FROM user_table ORDER BY id ASC") fun getUserData(): LiveData<List<User>>? @Insert(onConflict = OnConflictStrategy.IGNORE) fun addUser(user: User) @Update fun insertEval(eval: Eval) @Update fun updateUser(user: User) @Delete fun deleteUser(user: User) @Query("DELETE FROM user_table") fun deleteAllUsers() @Query("SELECT * FROM user_table WHERE id=:id") fun getOneUser(id: Int): User @Transaction @Query("SELECT * FROM user_table JOIN eval_table on user_table.id = eval_table.idEval ") fun getAllUserWithEval(): LiveData<List<UserWithEval>> @Query("SELECT * FROM user_table WHERE firstName LIKE :searchQuery OR lastName LIKE :searchQuery OR nbTeam LIKE :searchQuery OR nameTeam LIKE :searchQuery") fun searchDatabase(searchQuery: String): LiveData<List<User>>? @Query("SELECT * FROM team_table WHERE nameTeam LIKE :searchQuery") fun searchDatabaseTeam(searchQuery: String): LiveData<List<Team>>? @Transaction @Query("SELECT * FROM user_table WHERE nameTeam LIKE :searchTeam") fun getTeam(searchTeam: String): LiveData<List<User>>? }
ListFragment
class ListFragment : Fragment(), SearchView.OnQueryTextListener { private var _binding: FragmentListBinding? = null private val binding get() = _binding!! private lateinit var mUserViewModel: UserViewModel private val args by navArgs<ListFragmentArgs>() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { _binding = FragmentListBinding.inflate(inflater, container,false) val view = binding.root val adapter = ListAdapter() val recyclerView = view.findViewById<RecyclerView>(R.id.recyclerview) recyclerView?.adapter = adapter recyclerView?.layoutManager = LinearLayoutManager(requireContext()) val searchTeam = args.currentTeam.nameTeam binding.tvNameTeam.text = searchTeam mUserViewModel = ViewModelProvider(this).get(UserViewModel::class.java) mUserViewModel.getTeam(searchTeam)?.observe(viewLifecycleOwner, Observer { userT -> adapter.setData(userT) }) binding.floatingActionButton.setOnClickListener{ findNavController().navigate(R.id.action_listFragment_to_addFragment) } setHasOptionsMenu(true) return view } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { inflater.inflate(R.menu.main_menu, menu) inflater.inflate(R.menu.delete_menu, menu) val search = menu?.findItem(R.id.menu_search) val searchView = search?.actionView as? SearchView searchView?.isSubmitButtonEnabled = true searchView?.setOnQueryTextListener(this) return } override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == R.id.menu_delete){ deleteAllUsers() } return super.onOptionsItemSelected(item) } override fun onDestroyView() { super.onDestroyView() _binding = null } private fun deleteAllUsers() { val builder = AlertDialog.Builder(requireContext()) builder.setPositiveButton("Yes"){ _, _ -> mUserViewModel.deleteAllUsers() Toast.makeText( requireContext(), "Suppression complète réussie", Toast.LENGTH_SHORT).show() } builder.setNegativeButton("No"){_, _ -> } builder.setTitle("Tout Supprimer?") builder.setMessage("Êtes-vous sûr de vouloir supprimer l'ensemble des données?") builder.create().show() } override fun onQueryTextSubmit(query: String?): Boolean { return true } override fun onQueryTextChange(query: String?): Boolean { if(query != null){ searchDatabase(query) } return true } private fun searchDatabase(query: String) { val searchQuery = "%$query%" val adapter = ListAdapter() val recyclerView = view?.findViewById<RecyclerView>(R.id.recyclerview) recyclerView?.adapter = adapter recyclerView?.layoutManager = LinearLayoutManager(requireContext()) mUserViewModel.searchDatabase(searchQuery)?.observe(this, { userT-> userT.let { adapter.setData(it) } }) } }
ListAdapter
class ListAdapter : RecyclerView.Adapter<ListAdapter.MyViewHolder>() { var userTList = emptyList<User>() class MyViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder { return MyViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.custom_row, parent, false)) } override fun getItemCount(): Int { return userTList.size } override fun onBindViewHolder(holder: MyViewHolder, position: Int) { val currentItemUT = userTList[position] holder.itemView.findViewById<TextView>(R.id.id_txt).text = currentItemUT.id.toString() holder.itemView.findViewById<TextView>(R.id.firstName_txt).text = currentItemUT.firstName holder.itemView.findViewById<TextView>(R.id.lastName_txt).text = currentItemUT.lastName holder.itemView.findViewById<TextView>(R.id.numeroTeam_txt).text = currentItemUT.nbTeam.toString() holder.itemView.findViewById<View>(R.id.rowLayout).setOnClickListener { val action = ListFragmentDirections.actionListFragmentToUpdateFragment(currentItemUT) holder.itemView.findNavController().navigate(action) } holder.itemView.findViewById<Button>(R.id.btn_start_eval).setOnClickListener { val action = ListFragmentDirections.actionListFragmentToEvalFragment(currentItemUT) holder.itemView.findNavController().navigate(action) } } fun setData(user: List<User>){ this.userTList = user notifyDataSetChanged() } }
问题根因
- 参数传递缺失:点击悬浮按钮跳转添加球员页时,没有把当前选中的队伍名称传递过去,新增球员时无法给
User实体的nameTeam字段赋值,导致新球员没有关联所属队伍 - 动态查询未托管:带参数的
getTeam()查询每次调用都会返回新的LiveData实例,ViewModel没有统一托管当前选中队伍对应的数据源,首次进入页面时新创建的LiveData没有触发初始数据加载,只有数据变更(比如更新球员)时Room才会推送数据,导致首次进入显示全量数据 - Adapter重复创建:搜索方法里每次触发搜索都会新建Adapter替换RecyclerView原有实例,导致原有观察队伍筛选的观察者绑定到旧Adapter上,数据更新无法同步到当前显示的列表
- LIKE查询语法问题:
getTeam方法的SQL用了LIKE匹配,但传入的参数没有加%通配符,只有完全匹配队伍名的球员才会被查出,容错性差
修复步骤
1. 修改ViewModel,用switchMap托管动态查询
把当前选中的队伍名作为可观察的MutableLiveData,用switchMap自动切换对应的球员列表数据源,保证整个页面生命周期内只观察同一个LiveData实例:
// 在UserViewModel里增加以下代码 // 当前选中的队伍名,默认空 private val _currentTeamName = MutableLiveData("") // 当前队伍对应的球员列表,switchMap会自动根据_currentTeamName的值切换查询结果 val currentTeamUsers: LiveData<List<User>> = _currentTeamName.switchMap { teamName -> if (teamName.isNullOrBlank()) { // 没选队伍时返回空列表 MutableLiveData(emptyList()) } else { // 选了队伍就查对应球员,加通配符做模糊匹配 repositoryUser.getTeam("%$teamName%") } } // 新增方法:设置当前选中的队伍 fun setCurrentTeam(teamName: String) { _currentTeamName.value = teamName }
2. 修正跳转逻辑,传递队伍参数到添加页
修改ListFragment里悬浮按钮的点击事件,把当前队伍名通过Safe Args传给添加Fragment:
binding.floatingActionButton.setOnClickListener{ val action = ListFragmentDirections.actionListFragmentToAddFragment(searchTeam) findNavController().navigate(action) }
在AddFragment里接收传递过来的队伍名参数,新增User对象时把nameTeam字段设置为这个参数值,保证新球员关联到当前队伍。
3. 调整ListFragment的观察逻辑,复用同一个Adapter
把Adapter初始化和观察者绑定放到合适的位置,不要在搜索方法里重复创建Adapter:
// 把adapter提为成员变量 private val adapter = ListAdapter() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { _binding = FragmentListBinding.inflate(inflater, container,false) val view = binding.root // 初始化RecyclerView,只设置一次adapter val recyclerView = view.findViewById<RecyclerView>(R.id.recyclerview) recyclerView?.adapter = adapter recyclerView?.layoutManager = LinearLayoutManager(requireContext()) val searchTeam = args.currentTeam.nameTeam binding.tvNameTeam.text = searchTeam mUserViewModel = ViewModelProvider(this).get(UserViewModel::class.java) // 设置当前选中队伍,自动触发查询 mUserViewModel.setCurrentTeam(searchTeam) // 只观察一次currentTeamUsers,不需要每次调用getTeam拿新的LiveData mUserViewModel.currentTeamUsers.observe(viewLifecycleOwner) { userList -> adapter.setData(userList) } binding.floatingActionButton.setOnClickListener{ val action = ListFragmentDirections.actionListFragmentToAddFragment(searchTeam) findNavController().navigate(action) } setHasOptionsMenu(true) return view } // 修改searchDatabase方法,不要新建adapter private fun searchDatabase(query: String) { val searchQuery = "%$query%" // 直接给现有adapter设置搜索结果,不要替换adapter mUserViewModel.searchDatabase(searchQuery)?.observe(viewLifecycleOwner) { userT-> adapter.setData(userT) } }
4. 修正搜索逻辑的范围
如果需要搜索时只在当前队伍内搜索,可以给Dao层的搜索方法增加队伍名参数,SQL里增加nameTeam = :currentTeam的过滤条件,避免搜索出其他队伍的球员
相关产品推荐
相关产品推荐

