Реактивный источник данных
Проблема и решение
Почти во всех приложениях обязательно есть работа с данными, которые мы получаем от сервера, из интернета, от базы данных и т.д.
Какие-то данные будут отображаться на одном экране приложения, какие-то на двух, а какие-то на трех и более.
Например, приложение любой социальной сети: один пост может быть репостнут разными людьми и группами. У поста есть лайки, комментарии, репосты и просмотры.
Чтобы на всех экранах отображать данные о постах в актуальном состоянии мы можем закидывать сервер запросами, однако нет гарантии, что мы нигде не ошибемся и где-нибудь не забудем добавить обновление. Из-за этого отображение поста на разных экранах будет отличаться. В любом случае, поддерживать такой проект будет очень тяжело.
Чтобы избежать всех этих проблем нам нужно использовать такой источник данных, который бы позволил обновлять данные автоматически, а не вручную.
То, что нам нужно называется Реактивный источник данных - он выдает подписки, т.е. что-то, на что мы можем подписаться на 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_KEYchangeMessageValue(message: String?)- функция-сеттер дляmessageValueresetData()- функция для очистки значений всех сохраненных переменных
На 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(observableSettings)
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?).
Не забывайте, что подписка на flow в зависимости от конфигурации отрабатывает с задержкой.
Этот подход используется на реальных проектах, мы будем применять его в восьмом блоке.