如何在ViewModel中通过SearchView过滤LiveData<List<Note>>并通知观察者?
我有Note实体、包含SQL逻辑的NoteDAO、继承RoomDatabase的NoteDatabase类,还有关联NoteDAO和NoteDatabase的NoteRepository。NoteViewModel通过NoteRepository与数据库交互。
在Fragment的onCreateView中,我通过NoteViewModel获取所有笔记,观察数据后用adapter.submitList更新RecyclerView的条目。
但集成SearchView时遇到问题:能正常获取查询文本,但尝试过滤NoteViewModel中类型为LiveData<List<Note>>的allNotes变量时,无法按预期通知观察者更新列表。
试过把allNotes改成MutableLiveData,想在filterNotes方法里用setValue或postValue通知观察者,但不仅过滤没生效,连未过滤的原始笔记列表都拿不到了。
现有代码
FirstFragment代码
@Override public View onCreateView( ....... ) { ........ RecyclerView recyclerView = binding.recyclerView; final NoteListAdapter adapter = new NoteListAdapter(new NoteListAdapter.WordDiff()); recyclerView.setAdapter(adapter); recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); mNoteViewModel = new ViewModelProvider(requireActivity()).get(NoteViewModel.class); mNoteViewModel.getAllNotes().observe(getViewLifecycleOwner(), new Observer<List<Note>>() { @Override public void onChanged(List<Note> notes) { Log.d("MainPage", "onChanged: " + notes); adapter.submitList(notes); } }); ........ }
NoteViewModel代码
public class NoteViewModel extends AndroidViewModel { private final NoteRepository mRepository; private final LiveData<List<Note>> mAllNotes; public NoteViewModel(@NonNull Application application) { super(application); mRepository = new NoteRepository(application); mAllNotes = mRepository.getAllNotes(); } LiveData<List<Note>> getAllNotes() { return mAllNotes; } public void insert (Note note) { mRepository.insert(note); } }
NoteRepository代码
public class NoteRepository { private final NoteDAO mNoteDAO; @NotNull private final LiveData<List<Note>> mAllNotes; NoteRepository(Application application) { NoteRoomDatabase db = NoteRoomDatabase.getDatabase(application); mNoteDAO = db.noteDAO(); mAllNotes = mNoteDAO.getAllNotes(); } public LiveData<List<Note>> getAllNotes() { return mAllNotes; } public void insert(Note note) { NoteRoomDatabase.databaseWriteExecutor.execute(() -> { mNoteDAO.insert(note); }); } }
NoteDAO代码
@Dao public interface NoteDAO { @Insert void insert(Note note); @Query("SELECT * FROM notes ORDER BY id DESC") LiveData<List<Note>> getAllNotes(); @Query("SELECT * FROM notes where title = :title ORDER BY id DESC") LiveData<List<Note>> getNotesByTitle(String title); @Query("SELECT * FROM notes where id = :id ORDER BY id DESC") LiveData<Note> getNoteById(long id); @Query("SELECT * FROM notes where color = :color ORDER BY id DESC") LiveData<List<Note>> getNotesByColor(String color); }
解决方法
1. 优化DAO的搜索查询
把精确匹配的getNotesByTitle改成模糊匹配,更符合搜索场景:
@Dao public interface NoteDAO { // ... 其他方法不变 @Query("SELECT * FROM notes WHERE title LIKE '%' || :searchTerm || '%' ORDER BY id DESC") LiveData<List<Note>> getNotesBySearchTerm(String searchTerm); }
2. 给Repository添加搜索方法
public class NoteRepository { // ... 其他代码不变 public LiveData<List<Note>> getNotesBySearchTerm(String searchTerm) { return mNoteDAO.getNotesBySearchTerm(searchTerm); } }
3. ViewModel用MediatorLiveData切换数据源
不要直接修改mAllNotes的类型,用MediatorLiveData来合并全量数据和搜索结果,既保留Room的自动更新,又支持动态过滤:
public class NoteViewModel extends AndroidViewModel { private final NoteRepository mRepository; // 存放搜索关键词 private final MutableLiveData<String> mSearchQuery = new MutableLiveData<>(); // 对外暴露的过滤后数据 private final MediatorLiveData<List<Note>> mFilteredNotes = new MediatorLiveData<>(); public NoteViewModel(@NonNull Application application) { super(application); mRepository = new NoteRepository(application); // 先加载全量笔记数据 LiveData<List<Note>> allNotes = mRepository.getAllNotes(); mFilteredNotes.addSource(allNotes, mFilteredNotes::setValue); // 监听搜索关键词变化,切换数据源 mFilteredNotes.addSource(mSearchQuery, searchTerm -> { // 先移除之前的数据源,避免重复回调 mFilteredNotes.removeSource(allNotes); LiveData<List<Note>> targetData; if (TextUtils.isEmpty(searchTerm)) { // 搜索框为空时,显示全量数据 targetData = allNotes; } else { // 有搜索词时,加载搜索结果 targetData = mRepository.getNotesBySearchTerm(searchTerm); } // 添加新的数据源 mFilteredNotes.addSource(targetData, mFilteredNotes::setValue); }); } // 对外提供观察过滤后数据的入口 public LiveData<List<Note>> getFilteredNotes() { return mFilteredNotes; } // 设置搜索关键词 public void setSearchQuery(String searchTerm) { mSearchQuery.setValue(searchTerm); } // ... 插入方法不变 public void insert(Note note) { mRepository.insert(note); } }
4. Fragment中修改观察对象并绑定SearchView
@Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // ... 其他初始化代码不变 mNoteViewModel = new ViewModelProvider(requireActivity()).get(NoteViewModel.class); // 改为观察过滤后的笔记数据 mNoteViewModel.getFilteredNotes().observe(getViewLifecycleOwner(), notes -> { Log.d("MainPage", "onChanged: " + notes); adapter.submitList(notes); }); // 绑定SearchView的输入监听 SearchView searchView = binding.searchView; searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { mNoteViewModel.setSearchQuery(query); return true; } @Override public boolean onQueryTextChange(String newText) { mNoteViewModel.setSearchQuery(newText); return true; } }); return binding.getRoot(); }
为什么之前的方法无效?
Room返回的LiveData是自带数据库监听的不可变数据源,如果把它赋值给MutableLiveData后手动调用setValue,会破坏Room原本的数据源监听逻辑,导致无法获取原始数据更新。
用MediatorLiveData可以灵活切换不同的LiveData数据源,既保留Room自动感知数据库变化的特性,又能根据搜索关键词动态切换查询结果,完美解决过滤通知的问题。
内容的提问来源于stack exchange,提问作者ahmetkca

