基于多个过滤器过滤数据集的最佳方法
Best way to filter a dataset based on multiple filters
我有一个包含用户 GradeEntity
对象的数据集,这些对象包含对其关联的 SubjectEntity
实例的引用。我现在需要允许用户根据两个选项过滤该数据集:
- 主题
- 日期范围
这是我在过滤过程中要遵循的逻辑。
假设初始数据集包含两个成绩实体:
- 年级(题目:数学,日期:7 月 11 日)
- 年级(题目:数学,日期:7 月 14 日)
现在假设用户根据“数学”对成绩进行排序,但还没有 select 日期范围。显示了所有相应的成绩,但现在用户想要在过滤后的数据集上应用日期范围。 He/She select 范围为 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())
}
}
您可以在 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
,以获取通过 Subject
或 Date
筛选的结果。如果您还有任何疑问,请随时在评论中提问:)
我认为只公开一个 LiveData 并将未过滤结果的完整列表保密会更干净。那么 Fragment/Activity 只需要观察一个 LiveData 一次,并且可以单独修改应用哪些过滤器。当过滤器发生变化时,让 ViewModel 通过从头开始重新过滤所有值的列表来自动更新 LiveData 中的值。
如果您的支持数据也能自动更新,那就太好了。为此,您应该添加一个 repo/DAO 函数,该函数 returns 一个流。基本上是 dao.getAllGrades()
的副本, 不是 挂起函数,returns 是 Flow<List<GradeEntity>>
。我们称之为 getAllGradesFlow()
.
要更改过滤器,我们可以公开设置当前过滤器的属性,并将它们设置为触发对支持数据的重新过滤,以便自动刷新 LiveData。为了减少 functions/properties 的数量,我们可以使用 ClosedRange<Date>
作为日期过滤器的 属性 类型。也可以使用 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 以清除过滤器。
我有一个包含用户 GradeEntity
对象的数据集,这些对象包含对其关联的 SubjectEntity
实例的引用。我现在需要允许用户根据两个选项过滤该数据集:
- 主题
- 日期范围
这是我在过滤过程中要遵循的逻辑。 假设初始数据集包含两个成绩实体:
- 年级(题目:数学,日期:7 月 11 日)
- 年级(题目:数学,日期:7 月 14 日)
现在假设用户根据“数学”对成绩进行排序,但还没有 select 日期范围。显示了所有相应的成绩,但现在用户想要在过滤后的数据集上应用日期范围。 He/She select 范围为 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())
}
}
您可以在 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
,以获取通过 Subject
或 Date
筛选的结果。如果您还有任何疑问,请随时在评论中提问:)
我认为只公开一个 LiveData 并将未过滤结果的完整列表保密会更干净。那么 Fragment/Activity 只需要观察一个 LiveData 一次,并且可以单独修改应用哪些过滤器。当过滤器发生变化时,让 ViewModel 通过从头开始重新过滤所有值的列表来自动更新 LiveData 中的值。
如果您的支持数据也能自动更新,那就太好了。为此,您应该添加一个 repo/DAO 函数,该函数 returns 一个流。基本上是 dao.getAllGrades()
的副本, 不是 挂起函数,returns 是 Flow<List<GradeEntity>>
。我们称之为 getAllGradesFlow()
.
要更改过滤器,我们可以公开设置当前过滤器的属性,并将它们设置为触发对支持数据的重新过滤,以便自动刷新 LiveData。为了减少 functions/properties 的数量,我们可以使用 ClosedRange<Date>
作为日期过滤器的 属性 类型。也可以使用 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 以清除过滤器。