Room预打包数据库Fragment中SearchView搜索功能失效问题排查
我正在开发一款药品词典APP,用于存储药品名称及详细信息。APP包含1个主Activity和3个Fragment,主XML中的Fragment Host承载其他Fragment。点击首页Fragment的按钮可进入MedicineListFragment,该Fragment的RecyclerView展示assets文件夹中预打包Room数据库的药品名称。我希望实现搜索功能,用户输入查询词后能显示对应结果。已在ActionBar中实现可点击的SearchView,但点击输入后无任何反应。本项目参考Android官方Room and Flow教程,仅需在含RecyclerView的MedicineListFragment中实现搜索功能。尝试多种方法均未成功,怀疑问题出在ViewModel或MedicineNameAdapter的初始化环节。
主Activity代码
class MainActivity : AppCompatActivity( ) { lateinit var binding: ActivityMainBinding private lateinit var navController: NavController private lateinit var toolbar : Toolbar private lateinit var navigationView: NavigationView private lateinit var appBarConfiguration: AppBarConfiguration private lateinit var drawerLayout: DrawerLayout private lateinit var listener : NavController.OnDestinationChangedListener override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) toolbar = findViewById(R.id.my_Toolbar) setSupportActionBar(toolbar) navigationView = findViewById(R.id.navigation_v) val bNavigationView = findViewById<BottomNavigationView>(R.id.bnv) val novController = findNavController(R.id.fragmentContainerView) bNavigationView.setupWithNavController(novController) drawerLayout = findViewById(R.id.drawer_layout) appBarConfiguration = AppBarConfiguration(setOf(R.id.settingFragment,R.id.aboutFragment),drawerLayout) setupActionBarWithNavController(novController,drawerLayout) navigationView.setupWithNavController(novController) val navHomeFragment = supportFragmentManager.findFragmentById(R.id.fragmentContainerView) as NavHostFragment var navController = navHomeFragment.navController setupActionBarWithNavController(novController) } override fun onSupportNavigateUp(): Boolean { val navController = findNavController(R.id.fragmentContainerView) return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp() } }
MedicineListFragment代码
class MedicinwListFragment : Fragment(R.layout.fragment_medicinw_list),MenuProvider,SearchView.OnQueryTextListener { private var _binding: FragmentMedicinwListBinding? = null private val binding get() = _binding!! private lateinit var medicineNameAdapter: MedicineNameAdapter private val viewModel: MedicineTableVeiwmodel by activityViewModels { MedicineTableVeiwmodel.MedicineNameViewModelFactory( (activity?.application as MedicineTableApplication).database.medicineDao()) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, ): View { // Inflate the layout for this fragment _binding = FragmentMedicinwListBinding.inflate(inflater, container, false) val view = binding.root val menuHost : MenuHost = requireActivity() menuHost.addMenuProvider(this,viewLifecycleOwner,Lifecycle.State.RESUMED) medicineNameAdapter = MedicineNameAdapter { } setHasOptionsMenu(true) return view } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val recyclerview = binding.recyclerView recyclerview.layoutManager = LinearLayoutManager(requireContext()) val medicineNameAdapter = MedicineNameAdapter { val action = MedicinwListFragmentDirections .actionMedicinwListFragmentToMedinformationf( medicineName = it.medicineName ) view.findNavController().navigate(action) } recyclerview.adapter = medicineNameAdapter lifecycle.coroutineScope.launch { viewModel.fulMedicine().collect() { medicineNameAdapter.submitList(it) } } } override fun onDestroyView() { super.onDestroyView() _binding = null } override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { menuInflater.inflate(R.menu.medicine_list_f_menu,menu) val mSearch = menu.findItem(R.id.menu_search) val msearchView = mSearch.actionView as SearchView msearchView.isSubmitButtonEnabled = true msearchView.setOnQueryTextListener(this) } override fun onMenuItemSelected(menuItem: MenuItem): Boolean { return true } override fun onQueryTextSubmit(query: String?): Boolean { if (query != null){ searchDatabase(query) } return true } override fun onQueryTextChange(newText: String?): Boolean { if (newText != null){ searchDatabase(newText) } return true } fun searchDatabase(query: String?){ val searchQuery = "%$query%" viewModel.searcheMedicine(searchQuery).observe(this, { list -> medicineNameAdapter.submitList(list) }) } }
Entity代码
@Entity(tableName = "medicine") data class Medicine( @PrimaryKey (autoGenerate = true) var id: Int , @NotNull @ColumnInfo(name = "medicine_Name") var medicineName: String , @NotNull @ColumnInfo(name = "medicine_Info") var medicineInfo: String , )
Dao代码
@Dao interface MedicineDao { @Query("SELECT * FROM Medicine ORDER BY medicine_Name ASC") fun getAll() :kotlinx.coroutines.flow.Flow <List<Medicine>> @Query("SELECT * FROM medicine WHERE medicine_Name = :medicineName ORDER BY medicine_Info ASC") fun getByMedicinname( medicineName: String) : kotlinx.coroutines.flow.Flow <List<Medicine>> @Query("SELECT * FROM medicine WHERE medicine_Name LIKE :searchQuery") fun searchMedicine(searchQuery : String?) : LiveData <List<Medicine>> }
ViewModel代码
class MedicineTableVeiwmodel (var medicineDao: MedicineDao): ViewModel() { fun fulMedicine(): Flow <List<Medicine>> = medicineDao.getAll() fun medicineForMedicinname(name: String) :Flow <List<Medicine>> = medicineDao.getByMedicinname(name) fun searcheMedicine(searchQuery:String) : LiveData<List<Medicine>> = medicineDao.searchMedicine(searchQuery) class MedicineNameViewModelFactory(var medicineDao: MedicineDao): ViewModelProvider.Factory{ override fun <T : ViewModel> create(modelClass: Class<T>): T { if (modelClass.isAssignableFrom(MedicineTableVeiwmodel::class.java)){ @Suppress("UNCHECKED_CAST") return MedicineTableVeiwmodel(medicineDao) as T } throw IllegalArgumentException("UNKNOWN viewModel class") } } }
搜索菜单代码
<menu xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <item android:id="@+id/menu_search" android:title="@string/search" android:icon="@drawable/ic_baseline_search_24" app:showAsAction="ifRoom" android:iconTint="@color/white" app:actionViewClass="androidx.appcompat.widget.SearchView" tools:targetApi="o" /> </menu>
问题排查与修复方案
1. Adapter实例重复初始化问题
在MedicineListFragment的onCreateView中初始化了全局的medicineNameAdapter,但在onViewCreated里又重新创建了一个局部的medicineNameAdapter并设置给RecyclerView。后续搜索时调用的是全局的Adapter实例,而RecyclerView绑定的是局部实例,导致搜索结果无法更新到界面。
修复:
删除onViewCreated中的局部medicineNameAdapter初始化,直接使用全局实例:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val recyclerview = binding.recyclerView recyclerview.layoutManager = LinearLayoutManager(requireContext()) // 使用全局Adapter实例 medicineNameAdapter = MedicineNameAdapter { val action = MedicinwListFragmentDirections .actionMedicinwListFragmentToMedinformationf( medicineName = it.medicineName ) view.findNavController().navigate(action) } recyclerview.adapter = medicineNameAdapter lifecycle.coroutineScope.launch { viewModel.fulMedicine().collect() { medicineNameAdapter.submitList(it) } } }
2. LiveData与Flow混用问题
项目中主要使用Flow处理数据,但搜索接口返回的是LiveData,且每次调用searchDatabase都会重复观察LiveData,可能导致多次订阅或内存泄漏。统一使用Flow保持风格一致。
修复:
- 修改Dao中的
searchMedicine方法返回Flow:
@Query("SELECT * FROM medicine WHERE medicine_Name LIKE :searchQuery") fun searchMedicine(searchQuery : String) : Flow<List<Medicine>>
- 修改ViewModel中的对应方法:
fun searcheMedicine(searchQuery:String) : Flow<List<Medicine>> = medicineDao.searchMedicine(searchQuery)
- 修改
searchDatabase方法,使用Flow收集数据:
fun searchDatabase(query: String){ val searchQuery = "%$query%" lifecycle.coroutineScope.launch { viewModel.searcheMedicine(searchQuery).collect { list -> medicineNameAdapter.submitList(list) } } }
3. 搜索查询空值处理
原searchDatabase方法中query可能为空,需添加空值判断,输入为空时显示全部数据:
override fun onQueryTextSubmit(query: String?): Boolean { searchDatabase(query ?: "") return true } override fun onQueryTextChange(newText: String?): Boolean { searchDatabase(newText ?: "") return true } fun searchDatabase(query: String){ lifecycle.coroutineScope.launch { val flow = if(query.isBlank()){ viewModel.fulMedicine() } else { val searchQuery = "%$query%" viewModel.searcheMedicine(searchQuery) } flow.collect { list -> medicineNameAdapter.submitList(list) } } }
4. MenuProvider与setHasOptionsMenu重复设置
在onCreateView中同时调用了menuHost.addMenuProvider和setHasOptionsMenu(true),会导致菜单冲突,保留MenuProvider方式即可:
override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, ): View { _binding = FragmentMedicinwListBinding.inflate(inflater, container, false) val view = binding.root val menuHost : MenuHost = requireActivity() menuHost.addMenuProvider(this,viewLifecycleOwner,Lifecycle.State.RESUMED) medicineNameAdapter = MedicineNameAdapter { } // 删除setHasOptionsMenu(true) return view }
内容的提问来源于stack exchange,提问作者Tamim

