Получите бесплатно 4 курса для лёгкого старта работы в IT
Получить бесплатно
Главная БлогFlow: асинхронный «поток» в Kotlin
flow асинхронный поток в kotlin

Flow: асинхронный «поток» в Kotlin

Дата публикации: 22.11.2021
14 549
Время чтения: 14 минут
Дата обновления: 06.12.2023
Автор статьи:
Александр Аникин
В статье рассказывается:

В корутинах Flow — это класс, который может возвращать несколько объектов по очереди или сразу. Ключевое слово тут «несколько»: это главное его отличие от suspend function, которая возвращает один объект и завершается. Для примера, Flow гораздо удобнее, если вы подписываетесь на постоянные уведомления от вашего GPS или на получение сообщений в чате. Flow работает на основе корутин и представляет собой поток данных, которые можно обрабатывать асинхронно. С помощью Flow можно отправлять запросы на сервер или в базу данных без блокирования основного потока приложения. Все данные, которые возвращает Flow, должны быть, естественно, одного типа: если поток объявлен как Flow, то получать из него можно только объекты типа Int.

В работу Flow вовлечены три объекта:

  1. Producer — производит (создает, испускает) данные в виде потока. Данные передаются в отдельном потоке благодаря корутинам.
  2. Intermediary (Посредник) — класс или классы, которые могут модифицировать или изменять данные, произведенные Producer’ом. Обычно это какие-то вспомогательные классы или так называемые мапперы. Наличие посредников не обязательно, если данные не нужно модифицировать или переводить их из одного типа в другой.
  3. Consumer — получатель данных, произведённых Producer’ом.

Узнай, какие ИТ - профессии
входят в ТОП-30 с доходом
от 210 000 ₽/мес
Павел Симонов - исполнительный директор Geekbrains
Павел Симонов
Исполнительный директор Geekbrains
Команда GeekBrains совместно с международными специалистами по развитию карьеры подготовили материалы, которые помогут вам начать путь к профессии мечты.
Подборка содержит только самые востребованные и высокооплачиваемые специальности и направления в IT-сфере. 86% наших учеников с помощью данных материалов определились с карьерной целью на ближайшее будущее!

Скачивайте и используйте уже сегодня:

Павел Симонов - исполнительный директор Geekbrains
Павел Симонов
Исполнительный директор Geekbrains
pdf иконка

Топ-30 самых востребованных и высокооплачиваемых профессий 2023

Поможет разобраться в актуальной ситуации на рынке труда

doc иконка

Подборка 50+ бесплатных нейросетей для упрощения работы и увеличения заработка

Только проверенные нейросети с доступом из России и свободным использованием

pdf иконка

ТОП-100 площадок для поиска работы от GeekBrains

Список проверенных ресурсов реальных вакансий с доходом от 210 000 ₽

pdf 3,7mb
doc 1,7mb
Уже скачали 32196 pdf иконка

На простом примере посмотрим, как можно использовать 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 отловит ошибку, если такие появятся в процессе работы.

Дарим скидку от 60%
на курсы от GeekBrains до 08 декабря
Уже через 9 месяцев сможете устроиться на работу с доходом от 150 000 рублей
Забронировать скидку

Пришло время репозитория. В него мы передаём наш 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, можно сохранить для дальнейшего использования или сделать с ним ещё неограниченное количество операций.

Только до 9.12
Скачай подборку материалов, чтобы гарантированно найти работу в IT за 14 дней
Список документов:
ТОП-100 площадок для поиска работы от GeekBrains
20 профессий 2023 года, с доходом от 150 000 рублей
Чек-лист «Как успешно пройти собеседование»
Чтобы получить файл, укажите e-mail:
Введите e-mail, чтобы получить доступ к документам
Подтвердите, что вы не робот,
указав номер телефона:
Введите телефон, чтобы получить доступ к документам
Уже скачали 52300

Осталось описать последний класс нашей бизнес-логики — 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 можно посмотреть в этом репозитории.

Автор статьи:
Александр Аникин
Оцените статью:
5
Добавить комментарий

Сортировать:
По дате публикации
По рейтингу
Читайте также
prev
next
Бесплатные вебинары:
prev
next
Как работает дизайн-студия на примере одного кейса 

Как работает дизайн-студия на примере одного кейса 

Узнать подробнее
Инновационные подходы к обучению информационным технологиям

Инновационные подходы к обучению информационным технологиям

Узнать подробнее
Как стать Python-разработчиком

Как стать Python-разработчиком

Узнать подробнее
Что нужно знать разработчику

Что нужно знать разработчику

Узнать подробнее
Кто такой тестировщик и как им стать

Кто такой тестировщик и как им стать

Узнать подробнее
Чем занимается программист и как им стать

Чем занимается программист и как им стать

Узнать подробнее
Как искусственный интеллект помогает и мешает задачам кибербезопасности

Как искусственный интеллект помогает и мешает задачам кибербезопасности

Узнать подробнее
Бесплатный вебинар про внедрение искусственного интеллекта

Бесплатный вебинар про внедрение искусственного интеллекта

Узнать подробнее
Какие есть профессии в ИТ

Какие есть профессии в ИТ

Узнать подробнее
Смените профессию,
получите новые навыки,
запустите карьеру
Поможем подобрать обучение:
Забрать подарок

Получите подробную стратегию для новичков на 2023 год, как с нуля выйти на доход 200 000 ₽ за 7 месяцев

Подарки от Geekbrains из закрытой базы:
Осталось 17 мест

Поздравляем!
Вы выиграли 4 курса по IT-профессиям.
Дождитесь звонка нашего менеджера для уточнения деталей

Иван Степанин
Иван Степанин печатает ...