В корутинах Flow — это класс, который может возвращать несколько объектов по очереди или сразу. Ключевое слово тут «несколько»: это главное его отличие от suspend function, которая возвращает один объект и завершается. Для примера, Flow гораздо удобнее, если вы подписываетесь на постоянные уведомления от вашего GPS или на получение сообщений в чате. Flow работает на основе корутин и представляет собой поток данных, которые можно обрабатывать асинхронно. С помощью Flow можно отправлять запросы на сервер или в базу данных без блокирования основного потока приложения. Все данные, которые возвращает Flow, должны быть, естественно, одного типа: если поток объявлен как Flow, то получать из него можно только объекты типа Int.
В работу Flow вовлечены три объекта:
- Producer — производит (создает, испускает) данные в виде потока. Данные передаются в отдельном потоке благодаря корутинам.
- Intermediary (Посредник) — класс или классы, которые могут модифицировать или изменять данные, произведенные Producer’ом. Обычно это какие-то вспомогательные классы или так называемые мапперы. Наличие посредников не обязательно, если данные не нужно модифицировать или переводить их из одного типа в другой.
- Consumer — получатель данных, произведённых Producer’ом.
входят в ТОП-30 с доходом
от 210 000 ₽/мес
Скачивайте и используйте уже сегодня:
Топ-30 самых востребованных и высокооплачиваемых профессий 2023
Поможет разобраться в актуальной ситуации на рынке труда
Подборка 50+ бесплатных нейросетей для упрощения работы и увеличения заработка
Только проверенные нейросети с доступом из России и свободным использованием
ТОП-100 площадок для поиска работы от GeekBrains
Список проверенных ресурсов реальных вакансий с доходом от 210 000 ₽
На простом примере посмотрим, как можно использовать Flow в приложении. Для построения приложения мы будем использовать упрощённый аналог чистой архитектуры:
- у нас будут данные, которые хранятся в условной базе данных: Data;
- будет источник данных, который будет получать данные из БД: DataSource;
- будет репозиторий, который работает с нашим источником данных: Repository;
- репозиторий будет использоваться в нашей ViewModel, и в итоге данные будут отображаться в Activity.
Для начала создадим простой класс для передачи наших данных. В нашем случае это data class, который содержит некое значение в виде String. Именно Data будет получать наша Activity:
internal data class Data(val data: String)
Теперь опишем источник наших данных. Пусть это будет умозрительная БД, на изменения данных в которой мы и хотим подписаться. Так как данные в ней будут изменяться постоянно, то Flow идеально подходит для наших целей. База данных:
internal object DataBase { fun fetchData() = Random.nextInt() }
У нашей базы данных есть метод, который возвращает нужную информацию в виде какого-то случайного числа. Таким образом мы имитируем постоянное изменение данных в БД.
Скачать файлПришло время DataSource и Flow. Класс DataSource принимает в конструктор два аргумента: базу данных и период обновления данных. Период равен одной секунде, указанной в миллисекундах, и содержит одну переменную типа Flow, которая содержит в себе данные из нашей БД, переведённые из Int в String. Чтобы создать простой поток, нужно воспользоваться flow builder. В нашем случае это простая функция Flow, в которой всё и происходит:
internal class DataSource( private val dataBase: DataBase = DataBase, private val refreshIntervalMs: Long = 1000 ) { val data: Flow<String> = flow { while (true) { val dataFromDataBase = dataBase.fetchData() emit(dataFromDataBase.toString()) delay(refreshIntervalMs) } } .flowOn(Dispatchers.Default) .catch { e -> println(e.message)//Error! } }
В бесконечном цикле мы обращаемся к БД, получаем случайное число и «испускаем» (функция emit) это число уже в виде String для всех, кто «подписан» на поток (вспоминаем Producer и Consumer). После этого мы делаем паузу на одну секунду в цикле с помощью функции delay. Функции flowOn и catch опциональны: код будет прекрасно работать и без них. Во flowOn можно явно указать, в каком потоке будет выполняться работа, а catch отловит ошибку, если такие появятся в процессе работы.
на курсы от GeekBrains до 08 декабря
Пришло время репозитория. В него мы передаём наш DataSource. У репозитория тоже всего одна переменная типа Flow. Обратите внимание, что DataSource возвращает тип данных String, а Репозиторий передает дальше уже Data. Репозиторий в данном случае является Посредником:
internal class Repository(dataSource: DataSource = DataSource()) { val userData: Flow<Data> = dataSource.data.map { data -> Data(data) } //.onEach { saveInCache(it) } }
Тут мы видим, как у переменной класса DataSource data (это и есть наш поток Flow) вызывается функция map, которая позволяет сохранить полученное значение String в класс Data. Функция onEach опциональна и показывает, что значение, возвращаемое нашим DataSource, можно сохранить для дальнейшего использования или сделать с ним ещё неограниченное количество операций.
Осталось описать последний класс нашей бизнес-логики — ViewModel. ViewModel содержит в себе LiveData, на которую подписана наша Активити. Всё, что нам нужно сделать, — передать в конструктор ViewModel наш Репозиторий и запустить процесс получения данных из Базы данных:
internal class MainViewModel( repository: Repository = Repository() ) : ViewModel() { val liveData: MutableLiveData<Data> = MutableLiveData() init { viewModelScope.launch { repository.userData.flowOn(Dispatchers.Main) .collect { data -> liveData.value = data } } } }
Делается это в момент создания ViewModel (блок инициализации init). Чтобы подписаться на Flow, нужно запустить процесс через корутины (помним, что поток выполняется асинхронно). Для этого у нас есть viewModelScope.launch, который мы запускаем в блоке инициализации (также его можно запускать и внутри suspend функции). Далее у userData вызываем функцию flowOn, где указываем, что все данные у нас будут отображаться уже в основном потоке приложения. Функция collect непосредственно запускает поток. Как только нам приходит очередная порция данных (раз в секунду), мы обновляем LiveData.
На самом деле всё это можно запустить в одну строку, так как у класса Flow есть для этого специальные функции:
val liveData: LiveData<Data> = repository.userData.asLiveData() /*val liveData: MutableLiveData<Data> = MutableLiveData() init { viewModelScope.launch { repository.userData.flowOn(Dispatchers.Main) .collect { data -> liveData.value = data } } }*/
Все классы бизнес-логики полностью:
internal class MainViewModel( repository: Repository = Repository() ) : ViewModel() { val liveData: LiveData<Data> = repository.userData.asLiveData() /*val liveData: MutableLiveData<Data> = MutableLiveData() init { viewModelScope.launch { repository.userData.flowOn(Dispatchers.Main) .collect { data -> liveData.value = data } } }*/ } internal class Repository(dataSource: DataSource = DataSource()) { val userData: Flow<Data> = dataSource.data.map { data -> Data(data) } //.onEach { saveInCache(it) } } internal class DataSource( private val dataBase: DataBase = DataBase, private val refreshIntervalMs: Long = 1000 ) { val data: Flow<String> = flow { while (true) { val dataFromDataBase = dataBase.fetchData() emit(dataFromDataBase.toString()) delay(refreshIntervalMs) } } /*.flowOn(Dispatchers.Default) .catch { e -> println(e.message)//Error! }*/ } internal object DataBase { fun fetchData() = Random.nextInt() } internal data class Data(val data: String)
Осталось отобразить весь нехитрый процесс на экране. Макет:
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/message" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
В Activity мы создаем ViewModel, подписываемся на изменение данных, и как только данные меняются, отображаем их на экране. Всё остальное происходит внутри ViewModel:
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.main_activity) val textView = findViewById<TextView>(R.id.message) ViewModelProvider(this).get(MainViewModel::class.java).liveData.observe( this, { dataFromDataBase -> textView.text = dataFromDataBase.data }) } }
Запускаем и наслаждаемся потоком данных:
Более развернутые и продвинутые работы с Flow можно посмотреть в этом репозитории.