Сортировка и фильтрация записей с помощью базы данных Room и Kotlin Flow

Основные сведения

Использование библиотеки Room для локального хранилища  — довольно стандартная практика на устройствах Android в 2021 году. Одна из замечательных особенностей базы данных Room  —  поддержка сопрограмм и Rxjava, базы данных Room с потоком Kotlin. Рассмотрим простой способ создания компактных и отзывчивых (реактивных) приложений Android.

Есть 4 рекомендации по созданию эффективного и удобного в работе кода.

  1. Сортировка и фильтрация должны выполняться по базе данных Room. Модели представления (view-models) или репозитории не должны иметь с ней никакой логической связи.
  2. Пользователь может применять фильтр и сортировку по отдельности или совместно. Необходимо предусмотреть все сценарии.
  3. Реализация должна быть компактной и простой.
  4. Нужна постоянная синхронизация пользовательского интерфейса (UI) с базой данных. Например, при добавлении или удалении записи она должна отражаться в UI без каких-либо дополнительных действий в интерфейсе.

Room Entity

Создадим приложение, в котором можно хранить перечень подписок (Subscription) с такими сведениями, как название, сумма оплаты, категория (которую назовем меткой  —  Label). Итак, есть два класса Entity  —  Subscription и Label

Пример:

@Entity
data class Subscription(
@PrimaryKey(autoGenerate = true) @ColumnInfo(name= "id") var id : Int = 0,
@ColumnInfo(name= "name") var name : String,
@ColumnInfo(name= "amount") var amount : Int?,
@Embedded var lable: Lable? = null
)

@Entity
data class Lable(
@PrimaryKey(autoGenerate = true) @ColumnInfo(name= "lableId") var lableId : Int = 0,
@ColumnInfo(name= "lableName") var lableName : String,

Room Quires

В объекте доступа к данным (Data Access Object  —  DAO) пишем запросы, связанные с базой данных Room, которые понятны с помощью SQL. Нам нужно реализовать запросы, связанные с фильтрацией и сортировкой. Сортировка должна выполняться по возрастанию и убыванию.

Фильтр

Сначала создадим простой запрос с фильтром, используя предоставляемые Room операторы WHERE и IN:

@Query("SELECT * FROM Subscription WHERE lableId IN (:lableID)")
fun getLabledSubscriptions(lableID : Int): Flow<List<Subscription>>

Сохранение типа возвращаемого значения в виде потока позволяет приложению Android быстро реагировать на изменения в базе данных Room.

Сортировка

Далее рассмотрим простую сортировку в двух вариантах. Таким образом у нас будет две функции  —  одна для названия, а другая для суммы. Каждая функция принимает в качестве входных данных Int.

  • Если входное значение 1, сортировка будет по возрастанию.
  • Если входное значение 2, сортировка будет по убыванию.

Будем использовать комбинацию ORDER BY с CASE и WHEN.

Запросы сортировки:

@Query("SELECT * FROM Subscription ORDER BY " +
"CASE WHEN :isAsc = 1 THEN name END ASC, " +
"CASE WHEN :isAsc = 2 THEN name END DESC ")
fun getAllSortedByName(isAsc : Int?): Flow<List<Subscription>>

@Query("SELECT * FROM Subscription ORDER BY " +
"CASE WHEN :isAsc = 1 THEN amount END ASC, " +
"CASE WHEN :isAsc = 2 THEN amount END DESC ")
fun getAllSortedByAmount(isAsc : Int?): Flow<List<Subscription>>

Комбинация фильтра и сортировки

Мы можем объединить два вышеуказанных запроса и создать еще два, которые работают в сочетании с фильтром и сортировкой как по названию, так и по сумме. 

Пример запросов фильтрации с сортировкой:

@Query("SELECT * FROM Subscription " +
"WHERE lableId IN (:lableID) "+
" ORDER BY " +
"CASE WHEN :isAsc = 1 THEN name END ASC, " +
"CASE WHEN :isAsc = 2 THEN name END DESC ")
fun getLableSortedByName(lableID : Int, isAsc : Int?): Flow<List<Subscription>>

@Query("SELECT * FROM Subscription " +
"WHERE lableId IN (:lableID) "+
"ORDER BY " +
"CASE WHEN :isAsc = 1 THEN amount END ASC, " +
"CASE WHEN :isAsc = 2 THEN amount END DESC ")
fun getLablSortedByAmount(lableID : Int, isAsc : Int?): Flow<List<Subscription>>

В следующей более сложной части рассмотрим поток и модель представления.


View-model + Flow

Примечание: прежде чем приступить к дальнейшему чтению, ознакомьтесь с базовыми понятиями этих компонентов.

Классификации

Во-первых, есть два класса перечисления:

  • тип сортировки (по возрастанию, по убыванию или ничего);
  • поле сортировки (название, сумма или нет ничего).

Пример:

enum class SortType {
    ASCENDING, DESCENDNG, NONE
}

enum class SortField {
    NAME, AMOUNT, NONE
}

MutableStateFlow и LiveData

Мы должны создать MutableStateFlow типа Kotlin triple. Triple в Kotlin  —  это не что иное, как тройной массив значений. Здесь типом значений будет SortField, SortType и Int (метка id для фильтра). Пример:

var lbaleID : Int? = null
val sortFlow = MutableStateFlow(Triple(SortField.NONE,SortType.NONE,lbaleID))

Можно обойтись и без MutableStateFlow, используя для подключения к UI функцию расширения asLiveData для преобразования MutableStateFlow в LiveData. Часть этого кодирования будет очевидна после завершения логики в модели представления (view model).

flatMapLatest

flatMapLatest возвращает поток, который переключается на новый поток, создаваемый функцией преобразования каждый раз, когда исходный поток выдает значение. При этом предыдущий поток, созданный блоком преобразования, отменяется.

В данном случае исходным потоком является  —  sortFlow, а функцией преобразования будет выполнение запросов к базе данных Room. Поэтому, когда применяем flatMapLatest к Sortflow, внутри лямбды все время будем получать последние значения трех полей. Здесь мы соответствующим образом выполняем запросы и возвращаем список. Возвращаемым типом будет Flow с типом списка Subscription.

Переходя к лямбда-логике, используя первый параметр определяем, по какому полю необходимо определить тип фильтра. Второй параметр определяет тип сортировки, а третий предписывает необходимость фильтрации по какому-либо id

Пример кода, логика Filtermaplatest для сортировки и фильтрации списка:

@ExperimentalCoroutinesApi
val subscriptionsListFlow = sortFlow
.flatMapLatest {
when(it.first){
SortField.NAME -> {
it.third?.let { thirdd->
subscriptionsDatabase
.subscriptionsDao()
.getLableSortedByName(thirdd,it.second.getSortNumber())
}?:run {
subscriptionsDatabase
.subscriptionsDao()
.getAllSortedByName(it.second.getSortNumber())
}
}

SortField.AMOUNT -> {
it.third?.let { thirdd->
subscriptionsDatabase
.subscriptionsDao()
.getAllSortedByAmount(thirdd,it.second.getSortNumber())
}?:run{
subscriptionsDatabase
.subscriptionsDao()
.getAllSortedByAmount(it.second.getSortNumber())
}
}

SortField.NONE -> {
it.third?.let { thirdd->
subscriptionsDatabase
.subscriptionsDao()
.getLabled(thirdd)
}?:run {
subscriptionsDatabase
.subscriptionsDao()
.getAll()
}
}
}
}

Имея дело с потоками для получения данных из Room и обновления условий, получаем практически полную реактивность. Теперь можно наблюдать данные из системы представления, но я предпочитаю конвертировать их в live data (реальные данные).

@ExperimentalCoroutinesApi
val subscriptionsList = subscriptionsListFlow.asLiveData()

Наблюдение из системы представления

Самое сложное в основном сделано. Но те, кто работает с набором инструментов Jetpack, могут использовать observeAsState, чтобы еще более повысить реактивность. Традиционный пользовательский интерфейс предоставляет реальные данные из компонентов Android.

val data  = subscriptionsListViewmodel
.subscriptionsList.observeAsState(emptyList<Subscription>())

Lambda для обновления сортировки

Чтобы обновить сортировку или фильтр, по выбору пользователя можно создать простую лямбду в модели представления и обновить sortFlow, как показано ниже:

fun update( sortFiled : SortFiled, sortType : SortType, lableID : Int){
sortFlow.value = Triple(sortFiled,sortType,lbaleID)
}

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Siva Ganesh Kantamani: Sorting and Filtering Records Using Room DataBase and Kotlin Flow

Предыдущая статьяКак реализуется пользовательское взаимодействие на страницах JavaScript?
Следующая статьяJava Hibernate