最佳实践:使用 Room 和 LiveData 的运行时过滤器

Best practice: Runtime filters with Room and LiveData

我正在处理一个屏幕,该屏幕使用回收器显示 Room 包装的 DB 的内容。适配器从隐藏对 Room DAO 对象的查询调用的 ViewModel 获取 LiveData。因此,LiveData 对象实际上是一个 ComputableLiveData 对象,它知道 Room DB 的更改。

现在我想在屏幕上添加过滤器选项。我将在这个 Room-LiveData-ViewModel 设置中在哪里/如何实现它?

适配器或 ViewModel 应该 "postfilter" LiveData 中的结果吗?我是否应该为每次过滤器更改从房间重新查询数据?我可以为此重用底层的(可计算的)LiveData 吗?如果不是,我真的应该为每个过滤器更改创建新的 LiveData 吗?

这里讨论了一个类似的问题:Reload RecyclerView after data change with Room, ViewModel and LiveData

所以,我最终这样做了:

  • 片段将过滤器状态转发给 ViewModel。副作用:过滤器状态可能被多个(即由于配置更改而导致的后续)片段实例使用。也许你想要那个,也许不是。我愿意。
  • ViewModel 包含一个 MediatorLiveData 实例。它只有一个来源:Room DB LiveData 对象。源只是将更改转发给调解器。如果过滤器被片段改变,则源被重新查询交换。

回答我的详细问题:

  • 无后置过滤
  • 是,重新查询过滤器更改
  • 我不重用 ComputableLiveData(不确定是否可行)

关于评论中的讨论:

  • 我不应用分页

关于 Room 的最后说明:我错了吗?还是我需要为每个要应用的过滤器组合编写单独的 DAO 方法?好的,我可以通过字符串插入 select 语句的可选部分,但那样我就失去了 Room 的好处。某种使语句可组合的语句构建器会很好。

编辑:请注意下面 Ridcully 的评论。他提到了 SupportSQLiteQueryBuilder 和 @RawQuery 来解决我猜的最后一部分。不过我还没有检查出来。

感谢CommonsWare and pskink的帮助!

我正在处理类似的问题。最初我有 RxJava 但现在我正在将它转换为 LiveData。

这就是我在 ViewModel 中的做法:

// Inside ViewModel
MutableLiveData<FilterState> modelFilter = new MutableLiveData<>();
LiveData<PagedList<Model>> modelLiveData;

此模型实时数据在视图模型构造函数中按以下方式构造:

        // In ViewModel constructor
        modelLiveData = Transformations.switchMap(modelFilter,
                    new android.arch.core.util.Function<FilterState, LiveData<PagedList<Model>>>() {
                        @Override
                        public LiveData<PagedList<Model>> apply(FilterState filterState) {
                            return modelRepository.getModelLiveData(getQueryFromFilter(filterState));
                        }
                    });

当视图模型收到另一个要应用的过滤器时,它会:

// In ViewModel. This method receives the filtering data and sets the modelFilter 
// mutablelivedata with this new filter. This will be "transformed" in new modelLiveData value.
public void filterModel(FilterState filterState) {

    modelFilter.postValue(filterState);
}

然后,这个新的过滤器将 "transformed" 在一个新的 livedata 值中,该值将被发送给观察者(一个片段)。

fragment通过视图模型中的调用获取实时数据观察:

// In ViewModel
public LiveData<PagedList<Model>> getModelLiveData() {

    return modelLiveData;

}

在我的片段中我有:

@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);

    ViewModel viewModel = ViewModelProviders.of(this.getActivity()).get(ViewModel.class);

    viewModel.getModelLiveData().observe(this.getViewLifecycleOwner(), new Observer<PagedList<Model>>() {
        @Override
        public void onChanged(@Nullable PagedList<Model> model) {
            modelListViewAdapter.submitList(model);
        }
    });

}

希望对您有所帮助。

基于 Francisco 的回答(非常感谢!),下面是我如何基于 EditText 输入实现类似的动态数据库过滤,但在 Kotlin 中。

这是 Dao 查询示例,我根据传入的过滤器字符串执行 select:

// Dao query with filter
@Query("SELECT * from myitem WHERE name LIKE :filter ORDER BY _id")
fun getItemsFiltered(filter: String): LiveData<List<MyItem>>

我有一个存储库,但在本例中它只是一个简单的传递。如果您没有存储库,您可以直接从 ViewModel 调用 dao 方法。

// Repository
fun getItemsFiltered(filter: String): LiveData<List<MyItem>> {
    return dao.getItemsFiltered(filter)
}

然后在 ViewModel 中我使用了 Francisco 也使用过的 Transformations 方法。然而,我的过滤器只是一个包裹在 MutableLiveData 中的简单字符串。 setFilter 方法 posts 新的过滤器值,这反过来会导致 allItemsFiltered 被转换。

// ViewModel
var allItemsFiltered: LiveData<List<MyItem>>
var filter = MutableLiveData<String>("%")

init {
    allItemsFiltered = Transformations.switchMap(filter) { filter ->
        repository.getItemsFiltered(filter)
    }
}

// set the filter for allItemsFiltered
fun setFilter(newFilter: String) {
    // optional: add wildcards to the filter
    val f = when {
        newFilter.isEmpty() -> "%"
        else -> "%$newFilter%"
    }
    filter.postValue(f) // apply the filter
}

请注意,初始过滤器值默认设置为 return 所有项目的通配符 ("%")。如果您不设置此项,则在您调用 setFilter 之前不会观察到任何项目。

这是片段中的代码,我在其中观察 allItemsFiltered 并应用过滤。请注意,当我的搜索 EditText 更改时以及视图状态恢复时,我会更新过滤器。后者将设置您的初始过滤器,并在屏幕旋转时恢复现有的过滤器值(如果您的应用支持)。

// Fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    // observe the filtered items
    viewModel.allItemsFiltered.observe(viewLifecycleOwner, Observer { items ->
        // update the displayed items when the filtered results change
        items.let { adapter.setItems(it) }
    })

    // update the filter as search EditText input is changed
    search_et.addTextChangedListener {text: Editable? ->
        if (text != null) viewModel.setFilter(text.toString())
    }
}

override fun onViewStateRestored(savedInstanceState: Bundle?) {
    super.onViewStateRestored(savedInstanceState)

    // update the filter to current search text (this also restores the filter after screen rotation)
    val filter = search_et.text?.toString() ?: ""
    viewModel.setFilter(filter)

}

希望对您有所帮助!!

免责声明:这是我的第一个 post,所以如果我错过了什么,请告诉我。我不确定如何 link Francisco 的回答,否则我会那样做。它确实帮助我实现了目标。

您可以使用 CASE WHEN THEN 对数据库进行排序 看这段代码

创建常量class 用于排序 id

object Constant{
  const val NAME_ASC = 1    
  const val NAME_DESC = 2   
  const val ADDED_ASC = 3  
  const val ADDED_DESC = 4 
}

接口道

@Query(
    "SELECT * FROM table WHERE name=:name ORDER BY" +
            " CASE WHEN :sortBy = $NAME_ASC THEN title END ASC , " +
            " CASE WHEN :sortBy = $NAME_DESC THEN title END DESC , " +
            " CASE WHEN :sortBy = $ADDED_ASC  THEN added END ASC , " +
            " CASE WHEN :sortBy = $ADDED_DESC THEN added END DESC , " +
)
fun getItems(name: String, sortBy: Int): MutableLiveData<Item>

你的仓库Class

fun getItems(name: String, sortBy: Int) : MutableLiveData<Items>{
    return myDao.getItems(name,sortBy)
  }