【问题标题】:Best way to filter a dataset based on multiple filters基于多个过滤器过滤数据集的最佳方法
【发布时间】:2021-09-24 16:48:38
【问题描述】:

我有一个数据集,其中包含用户的 GradeEntity 对象,其中包含对其关联的 SubjectEntity 实例的引用。我现在需要允许用户根据两个选项过滤该数据集:

  1. 主题标题
  2. 日期范围

这就是我在过滤过程中要遵循的逻辑。 假设初始数据集包含两个年级实体:

  1. 年级(标题:数学,日期:7 月 11 日)
  2. 年级(标题:数学,日期:7 月 14 日)

现在让我们假设用户根据“数学”对成绩进行排序,但尚未选择日期范围。显示了所有相应的成绩,但现在用户想要在过滤的数据集上应用日期范围。他/她选择从 7 月 1 日到 7 月 12 日的范围。所以,现在用户只能看到一年级实体。现在假设他们想要将日期范围过滤器从 7 月 1 日更改为 7 月 15 日。我希望他们现在能够看到两个成绩。这意味着我不能像在当前 ViewModel 代码中那样重新过滤过滤后的数据集。当用户首先根据日期过滤,然后是主题,然后选择另一个主题时,同样的问题。这种过滤过程的最佳方法是什么?

现在我正在使用共享的MutableLiveData<List<GradeEntity>> 变量,但这不适用于我上面提到的所有情况。

ViewModel.kt:

class GradesViewModel(private val database: AppDatabase) : ViewModel() {
    private val _grades: MutableLiveData<List<GradeEntity>> = MutableLiveData(emptyList())
    val grades: LiveData<List<GradeEntity>> = _grades
    private val _filteredGrades: MutableLiveData<List<GradeEntity>> = MutableLiveData(emptyList())
    val filteredGrades: LiveData<List<GradeEntity>> = _filteredGrades

    init {
        getGradesFromDB()
    }

    private fun getGradesFromDB() {
        viewModelScope.launch(Dispatchers.IO) {
            val grades = database.getGradeDao().getAllGrades()
            if (grades.isNotEmpty() && grades != _grades.value) _grades.postValue(grades)
        }
    }

    fun getSubjectTitlesRelatedToGrades(): MutableSet<String> {
        val subjectTitles: MutableSet<String> = mutableSetOf("All Subjects")
        for (gradeEntity in grades.value!!) {
            subjectTitles.add(gradeEntity.subject.title)
        }
        return subjectTitles
    }

    fun filterGradesBasedOnSubject(subjectTitle: String) {
        viewModelScope.launch {
            filterGradesBasedOnSubjectAsync(subjectTitle)
        }
    }

    fun filterGradesBasedOnDate(startDate: Date, endDate: Date) {
        viewModelScope.launch {
            filterGradesBasedOnDateAsync(startDate, endDate)
        }
    }

    private suspend fun filterGradesBasedOnSubjectAsync(subjectTitle: String) {
        val filteredData = viewModelScope.async(Dispatchers.IO) {
            if (filteredGrades.value!!.isEmpty()) {
                grades.value?.stream()
                    ?.filter { gradeEntity -> gradeEntity.subject.title == subjectTitle }?.toList()
                    ?: emptyList()
            } else filteredGrades.value?.stream()
                ?.filter { gradeEntity -> gradeEntity.subject.title == subjectTitle }?.toList()
                ?: emptyList()
        }
        _filteredGrades.postValue(filteredData.await())
    }

    private suspend fun filterGradesBasedOnDateAsync(startDate: Date, endDate: Date) {
        val filteredData = viewModelScope.async(Dispatchers.IO) {
            val formatter = SimpleDateFormat("MMMM dd yyyy", Locale.getDefault())
            return@async if (filteredGrades.value!!.isEmpty()) {
                grades.value?.stream()
                    ?.filter { gradeEntity ->
                        formatter.parse(gradeEntity.formattedDateTime).after(startDate) &&
                                formatter.parse(gradeEntity.formattedDateTime).before(endDate)
                    }?.toList()
                    ?: emptyList()
            } else {
                filteredGrades.value?.stream()
                    ?.filter { gradeEntity ->
                        formatter.parse(gradeEntity.formattedDateTime).after(startDate) &&
                                formatter.parse(gradeEntity.formattedDateTime).before(endDate)
                    }?.toList()
                    ?: emptyList()
            }
        }
        _filteredGrades.postValue(filteredData.await())
    }
}

【问题讨论】:

    标签: android kotlin mvvm kotlin-coroutines


    【解决方案1】:

    我认为只公开一个 LiveData 并将未过滤结果的完整列表保密会更干净。那么 Fragment/Activity 只需要观察一个 LiveData 一次,并且可以单独修改应用哪些过滤器。当过滤器发生变化时,通过从头开始重新过滤所有值的列表,让 ViewModel 自动更新 LiveData 中的值。

    让您的支持数据也自动更新也很好。为此,您应该添加一个返回 Flow 的 repo/DAO 函数。基本上是dao.getAllGrades() 的一个副本,它不是一个挂起函数并返回一个Flow&lt;List&lt;GradeEntity&gt;&gt;。我们称之为getAllGradesFlow()

    要更改过滤器,我们可以公开设置当前过滤器的属性,并设置它们以触发对支持数据的重新过滤,以便自动刷新 LiveData。为了减少函数/属性的数量,我们可以使用ClosedRange&lt;Date&gt; 作为日期过滤器的属性类型。也可以使用 in 运算符轻松检查日期是否在两个日期之间。在调用方使用 .. 运算符很容易指定。

    我还想提一下,你不必指定Dispatchers.IO 来调用DAO 中的挂起函数。按照惯例,挂起函数不会阻塞,因此无需指定任何调度程序来调用它们。

    class GradesViewModel(private val database: AppDatabase) : ViewModel() {
        private var allGrades: List<GradeEntity> = emptyList()
        private val _grades: MutableLiveData<List<GradeEntity>> = MutableLiveData(emptyList())
        val grades: LiveData<List<GradeEntity>> = _grades
        var datesFilter: ClosedRange<Date>? by Delegates.observable(null) { _, _, _ -> onFilterChange() }
        var subjectTitleFilter: String? by Delegates.observable(null) { _, _, _ -> onFilterChange() }
    
        init {
            getGradesFromDB()
        }
    
        private fun getGradesFromDB() {
            database.getGradeDao().getAllGradesFlow()
                .onEach { 
                    allGrades = it 
                    publishFilteredGrades()
                }.launchIn(viewModelScope)
        }
    
        private fun onFilterChange() = viewModelScope.launch {
            publishFilteredGrades()
        }
    
        // Not related to your question but I simplified this.
        fun getSubjectTitlesRelatedToGrades(): Set<String> {
            return setOf("All Subjects") + allGrades.map { it.subject.title }
        }
    
        // Specifying a suspend function with dispatcher here because filtering 
        // a long list might be CPU heavy.
        private suspend fun publishFilteredGrades() = withContext(Dispatchers.Default) {
            var filteredGrades = allGrades
            subjectTitleFilter?.let { subjectTitle ->
                filteredGrades = filteredGrades.filter { it.subject.title == subjectTitle }
            }
            datesFilter?.let { dates ->
                val formatter = SimpleDateFormat("MMMM dd yyyy", Locale.getDefault())
                filteredGrades = filteredGrades.filter { 
                    formatter.parse(it.formattedDateTime) in dates 
                }
            }
            _grades.postValue(filteredGrades)
        }
    }
    

    从片段中,您只需要观察单个 LiveData 即可更新您的列表视图。任意顺序更改这两个过滤器属性,设置为null即可清除过滤器。

    【讨论】:

    • 这是一个令人惊讶的好回应,感谢您抽出宝贵时间写下所有这些。实际上,我不得不研究 Flows、Delegates.observable 和 ClosedRange,我已经学到了很多东西!再次感谢您,这非常有效,我想我现在会开始更频繁地使用 Flows。附言不应该将allGrades 字段声明为var,因为我们在getGradesFromDB 内重新分配它?
    • 是的,应该是var
    【解决方案2】:

    您可以在MutableLiveData 上设置SwitchMap 运算符。每当调用 MutableLiveData 的值发生变化时,SwitchMap 就会再次触发代码。

    例如,假设您只想根据日期或主题进行过滤,您可以使用以下内容:

    private val _isDateSelected: MutableLiveData<Boolean> = MutableLiveData(false)
    
    val currentResult = _isDateSelected.switchMap { dateIsSelected ->
    
       if(dateIsSelected) {
    
       // here the code that filters by date and returns a LiveData appears
      } else {
    
      // here the code that filters by subject and returns a LiveData appears 
      }
    }
    
    fun setSubjectSelectedFilter() {
      _isDateSelected = false
    }
    
    fun setDateSelectedFilter() {
      _isDateSelected = true
    }
    

    然后,您可以在您的视图中观察此 currentResult LiveData,以通过 SubjectDate 获得过滤结果。如果您还有任何疑问,请随时在 cmets 中提问 :)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-06-23
      • 2021-01-12
      • 2023-04-08
      • 1970-01-01
      • 1970-01-01
      • 2012-11-11
      • 1970-01-01
      • 2019-03-21
      相关资源
      最近更新 更多