【问题标题】:Best practice: Runtime filters with Room and LiveData最佳实践:使用 Room 和 LiveData 的运行时过滤器
【发布时间】:2018-02-13 15:05:10
【问题描述】:

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

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

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

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

【问题讨论】:

  • 您的数据集有多大?如果您没有将整个数据集保存在内存中,根据定义,您必须返回数据库以了解过滤器状态的任何变化。
  • 你在使用paging支持库吗?
  • 我正在寻找一个与我的数据集大小无关的答案。 ;) 但感谢您提出后过滤器可能不是最佳做法的建议。
  • @pskink 已经一年了,有没有更新/更好的使用 Room 过滤的解决方案? (在问我自己的问题之前检查)。我使用 LiveData>,所以甚至不能使用 MediatorLiveData。我在每次过滤器更改时请求并重新附加观察者。只是感觉不对:(
  • @AdiB 你不需要MediatorLiveData - 只需在每次更改搜索条件时使用Transformations.switchMap

标签: android mvvm android-room android-livedata


【解决方案1】:

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

这就是我在 ViewModel 中的做法:

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

这个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);
}

然后,这个新过滤器将被“转换”为一个新的 livedata 值,该值将发送给观察者(一个片段)。

片段通过视图模型中的调用获取livedata来观察:

// 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);
        }
    });

}

希望对你有帮助。

【讨论】:

    【解决方案2】:

    所以,我最终这样做了:

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

    回答我的详细问题:

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

    关于cmets中的讨论:

    • 我不应用分页

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

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

    感谢CommonsWarepskink 的帮助!

    【讨论】:

    • 你好@Oderik,我也在尝试过滤我的回收器视图,它消耗了viewmodel的lovedata条目的结果。您能否解释一下您找到了什么解决方案,因为我无法清楚地理解您的答案。
    • 当我们在片段生命周期中初始化一次视图模型时,如何将过滤器状态转发给视图模型,我设法在 onCreateView 中这样做。以及如何使用 MediatorLiveData 实例重新查询过滤器状态的变化。除此之外,我正在使用类似语句 "@Query("SELECT * FROM network WHERE name LIKE :networkName || '%'") public abstract LiveData> getNetworksByName(String networkName);"以这种方式,它只是过滤名称是否“以”给定文本开头,但如何查询名称是否“包含”给定单词。
    • 我认为这超出了这个问题的范围。也许你应该发布自己的。
    • 有SupportSQLiteQueryBuilder,可以和DAO中的@RawQuery一起使用。
    【解决方案3】:

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

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

    // 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 方法发布新的过滤器值,这反过来会导致 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
    }
    

    请注意,初始过滤器值设置为通配符(“%”)以默认返回所有项目。如果您不设置此项,则在您调用 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)
    
    }
    

    希望有帮助!!

    免责声明:这是我的第一篇文章,如果我遗漏了什么,请告诉我。我不确定如何链接到弗朗西斯科的答案,否则我会这样做。它绝对帮助我实现了我的实现。

    【讨论】:

    • 你对这段代码进行了渗透测试吗?它似乎容易受到 SQL 注入攻击。
    • 嗨@pkuszewski,不,我没有。因此,如果您有更多关于潜在漏洞以及如何缓解的信息,请分享。谢谢!
    【解决方案4】:

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

    创建一个用于排序 id 的 Constant 类

    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>
    

    你的存储库类

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

    【讨论】:

    • 谢谢,我不知道这个选项。它有助于减少所需的 Dao 方法的数量。关于这个线程的主要问题:就像所有其他答案一样,这个问题建议重新查询。
    猜你喜欢
    • 1970-01-01
    • 2015-11-16
    • 2019-04-26
    • 2019-11-30
    • 1970-01-01
    • 1970-01-01
    • 2014-05-23
    • 2013-02-04
    • 1970-01-01
    相关资源
    最近更新 更多