Реактивный источник данных
Проблема и решение
Почти во всех приложениях обязательно есть работа с данными, которые мы получаем от сервера, из интернета, от базы данных и т.д.
Какие-то данные будут отображаться на одном экране приложения, какие-то на двух, а какие-то на трех и более.
Например, приложение любой социальной сети: один пост может быть репостнут разными людьми и группами. У поста есть лайки, комментарии, репосты и просмотры.
Чтобы на всех экранах отображать данные о постах в актуальном состоянии мы можем закидывать сервер запросами, однако нет гарантии, что мы нигде не ошибемся и где-нибудь не забудем добавить обновление. Из-за этого отображение поста на разных экранах будет отличаться. В любом случае, поддерживать такой проект будет очень тяжело.
Чтобы избежать всех этих проблем нам нужно использовать такой источник данных, который бы позволил обновлять данные автоматически, а не вручную.
То, что нам нужно называется Реактивный источник данных - он выдает подписки, т.е. что-то, на что мы можем подписаться на UI
. Благодаря этому, при любых обновлениях данных в источнике на экране они также обновятся.
Как это представлено в проекте:
- источник данных, который использует паттерн Observer, например Flow или LiveData:
- база данных, которая выдает
Flow
- репозиторий, который держит
StateFlow
илиLiveData
в оперативной памяти Socket
, который выдаетFlow
- база данных, которая выдает
- подписываемся на
Flow
илиLiveData
у источника данных
Пример реализации
Common code
В этом примере источником данных нам будет служить хранилище устройства, а средствами библиотеки multiplatform-settings мы будем получать не просто значение по ключу, а Flow
и подписываться на него.
Инструкция по подключению multiplatform-settings-coroutines
.
Начнем с класса KeyValueStorage
, к которому будем обращаться через объект репозитория.
KeyValueStorage.kt
:
private const val MESSAGE_KEY = "message_key"
class KeyValueStorage(private val settings: ObservableSettings) {
val messageFlow: Flow<String?> = settings.getStringOrNullFlow(MESSAGE_KEY)
private var messageValue: String? by settings.nullableString(MESSAGE_KEY)
fun changeMessageValue(message: String?) {
this.messageValue = message
}
fun resetData() {
settings.clear()
}
}
Нам понадобятся:
MESSAGE_KEY
- константа-ключ, чтобы не хардкодить егоmessageFlow
-Flow
значения по ключуMESSAGE_KEY
, на который мы будем подписыватьсяmessageValue
- приватная переменная, с помощью которой мы сможем задать значение переменной по ключуMESSAGE_KEY
changeMessageValue(message: String?)
- функция-сеттер дляmessageValue
resetData()
- функция для очистки значений всех сохраненных переменных
На UI
мы преобразуем messageFlow
в StateFlow
, чтобы заработал его функционал с доступом к последнему значению по переменной value
.
messageValue
мы используем из-за того, что в библиотеке multiplatform-settings
нет возможности получить MutableStateFlow
, а нам нужна возможность изменять значение, чтобы увидеть реактивность нашего репозитория. Преобразовать Flow
в MutableStateFlow
довольно проблематично, поэтому оставим пока так.
В репозитории мы будем работать не напрямую с keyValueStorage
, а используя вспомогательные функции setMessage(message: String?)
и getMessage()
.
Repository.kt
:
class Repository(observableSettings: ObservableSettings) {
private val keyValueStorage = KeyValueStorage()
fun getMessage(): Flow<String?> {
return keyValueStorage.messageFlow
}
fun setMessage(message: String?) {
keyValueStorage.changeMessageValue(message)
}
}
И наконец, вьюмодель:
ViewModel
:
class FirstViewModel(private val repository: Repository) : ViewModel() {
val message: StateFlow<String?> =
repository.getMessage().stateIn(viewModelScope, SharingStarted.Lazily, null)
fun setMessage(message: String?) {
repository.setMessage(message)
}
}
Теперь, со стороны UI
нам достаточно будет просто подписаться на обновления message
, и получать актуальные значения при его изменении. Изменять message
можно будет используя функцию setMessage(message: String?)
.
Этот подход используется на реальных проектах, мы будем применять его в восьмом блоке.