Мы - Дима (@fonfon) и Настя, Android-разработчики в компании СберЗдоровье. В этой статье мы хотим рассказать о том, как мы перевели весь наш проект с LiveData на Flow, с какими трудностями столкнулись и что полезного узнали. Эта статья будет полезна тем, кто работает с LiveData, уже пробовал / хочет попробовать Flow для хранения состояний во ViewModel, а также командам, которые планируют миграцию всего проекта на новый инструмент.  Вместе с Kotlin Coroutines JetBrains предоставил нам такие средства для общения между корутинами, как Channels и Flow. Изначально мы начали использовать корутины в других частях проекта, в частности, для сетевого слоя. В желании унифицировать инструментарий и подключаемые библиотеки мы решили перейти на Flow вместо LiveData для взаимодействия наших ViewModel с View-слоем. Мы старались сделать переход поэтапным, чтобы снизить риски ошибок. Начали с внедрения Flow во вьюмодели для некоторых новых экранов в приложении, и постепенно, спустя несколько релизов, стали переписывать старый код. Сейчас в нашем проекте совсем не осталось LiveData. В этой статье я расскажу подробно о плюсах и минусах каждого инструмента, а также о шагах, которые мы предприняли при миграции. Возможно, на примере опыта нашей команды вы сможете избежать некоторых ошибок и принять решение для своего проекта.  Чтобы разобрать различия между инструментами, мы выделили несколько наиболее показательных тестовых случаев. Ниже рассмотрим каждый из подходов на их примере:  Наличие подписки в момент отправки данных - есть/нет; То, как отправляются данные - моментально/с задержками/есть ли реакция на различные потоки; Зависимость от данных - отправляется ли новый пакет данных/такой-же или те-же самые данные; Возможность повторно прочитать текущее значение; Отправка единичных событий; Привязка к жизненному циклу - механизм подписки к жизненному циклу. Google предлагает нам 3 основных инструмента для хранения состояний во ViewModel: LiveData, MutableLiveData, MediatorLiveData. Чаще всего для переключения состояний мы пользуемся MutableLiveData, поэтому рассмотрим ее ближе.  MutableLiveData позволяет отправлять данные через свойство value и метод postValue, при этом value позволяет мгновенно отправлять новые данные, но только в основном потоке, а postValue позволяет это делать из разных потоков, но с некоторой задержкой на синхронизацию. В основных тестах использовалась именно MutableLiveData и с ней мы получаем результаты, от которых и будем отталкиваться: При наличии подписки приходят все эвенты, во время подписки приходит последнее значение; При отправке данных нужно следить за потоками и вызываемыми методами отправки, при этом гарантирована стабильная отправка данных; LiveData никак не проверяет новые данные и при отправке одного и того-же объекта он придет 2 раза; В случае MutableLiveData, текущее значение мы можем получить через value; Для отправки единичных эвентов требуется Дополнительный класс - LiveDataEvent; Подписка происходит через функцию observe,  c привязкой к жизненному циклу или через observeForever.  LiveDataEvent Channel наследует 2 интерфейса SendChannel и ReceiveChannel: SendChannel реализует 2 метода отправки данных: Функция send  объявлена с модификатором suspend , поэтому выполнение корутины может быть приостановлено если при попытке отправки буфер канала заполнен или он просто отсутствует. В данном случае приостановка будет до тех пор, пока другая корутина не начнет читать значения из канала. Функция trySend объявлена без модификатора suspend  и выполняет отправку элемента в канал моментально без блокировки. Интерфейс ReceiveChannel также имеет две основные функции для чтения данных из канала: Функция receive предназначена для чтения значения и последующего его удаления из канала. Имеет модификатор suspend и корутина приостанавливает свое выполнение при чтении из пустого канала до тех пор, пока не появится новое значение. При попытке чтения из закрытого канала будет сгенерировано исключение либо с типом ClosedReceiveChannelException, либо с другим классом, если таковой был передан в функцию close. Функция tryReceive, по аналогии с функцией trySend для производителя, является не блокирующей. Для интерфейса Channel также существует функция фабрика: В зависимости от входных параметров можно получить немного различающиеся объекты, реализующие интерфейс. Так при capacity > 0 при вызове метода send корутина не будет блокироваться, пока не заполнится буффер. Более того корутина не будет блокироваться при любом размере, если указать стратегию DROP_OLDEST или DROP_LATEST. Также стоит указать то, так как основной работой, для которой был придуман Channel - передача данных, то он никак не хранит последнее значение.  Channel - это первый инструмент, который предложила нам компания JetBrains для общения между корутинами. По некоторой неопытности мы использовали Channel и делали это неправильно. Фактически Channel не реализует подписку на получение данных, и получение данных сильно зависит от параметров канала; При наличии получателя данных все отправляемые данные успешно приходят; Channel не сравнивает данные, поэтому приходят все эвенты; Нельзя получить значение повторно; Все эвенты приходят единожды; Для широковещательной рассылки требуется использовать отдельную реализацию -BroadcastChannel со своими особенностями. Channel представляет собой интерфейс, который реализуют 3 класса, каждый из которых, в зависимости от входных параметров реализует немного разное поведение; Channel реализует только P2P отправку данных и если требуется еще одна подписка, то нужно использовать BroadcastChannel со своими особенностями реализации; Channel использует внутри себя синхронизацию потоков, что приводит к более медленной передаче данных; Для того чтобы обработать данные от Channel и отправить их далее, потребуется создать дополнительную подписку, на что будет требоваться больше памяти. Более продвинутым инструментом для общения между корутинами является Flow, который появился в версии 1.4. Фактически Flow закрывает все минусы, которые были в Channel. Чаще всего для переключения состояний мы пользуемся MutableStateFlow и MutableSharedFlow, поэтому рассмотрим их ближе. Методы отправки данных: Получение данных происходит через collect функцию, единую для всех Flow. Для создания MutableSharedFlow присутствует Функция-фабрика: В отличие от Channel можно увидеть, что есть буфер для повторения части событий и буфер для отправки данных. Также можно выделить важное значение - общий буффер, который строится так: В зависимости от размера общего буффера и стратегии, по аналогии с Channel, можно получить SharedFlow, с немного различным поведением. Так при bufferCapacity0 > 0 при вызове метода send корутина не будет блокироваться, пока не заполнится буффер. Более того корутина не будет блокироваться при любом размере, если указать стратегию DROP_OLDEST или DROP_LATEST. <Дублируется из Channel> В зависимости от параметров, передаваемых в фабрику мы получаем несколько отличающееся поведение; При наличии подписки все отправляемые эвенты успешно доходят, при отсутствии подписки - все зависит от размера общего буффера; SharedFlow не сравнивает данные, поэтому приходят все эвенты; Можно повторно получить данные из replayCache при его размере > 0; Все эвенты могут приходить как единожды, так и с повторением; Flow поддерживает широковещательную рассылку. StateFlow сделан по образу и подобию SharedFlow. Вот почему StateFlow ничего более, как доработанная специализация такой реализации SharedFlow: Если вглядеться подробнее в интерфейс MutableStateFlow, то можно увидеть интерфейс StateFlow со значением и функцией compareAndSet. Основной функцией обновления данных в StateFlow является compareAndSet, которая сверяет текущее значение с ожидаемым и обновляет его на значение из поля update. При этом если value, expect и update равны между собой функция вернет true, без обновления значения. В этой мелочи кроется маленький дьявол. Если во время обновления данных происходило какое-либо преобразование, отдававшее новый результат и устанавливалось то же значение, то преобразования данных не происходило и не обновлялось фактическое состояние. В какой-то момент мы даже поймали на этом ошибку. При наличии подписки все состояния обновляются, только если не эмитятся те же самые данные, причем во время подписки мы получим последнее значение; При достаточно большом объеме данных операция проверки может длиться достаточно долго, что в определенных случаях приводит к результатам, сравнимым с posValue у LiveData; Обновление состояния зависит от предыдущего состояния; Повторно получить данные можно через value; Для отправки единичных эвентов требуется LiveDataEvent. На примере тестовых случаев мы разобрались, какой инструмент для чего подходит лучше, и можем дать следующие рекомендации:  Для данных, полученных из репозитория, лучше использовать: val flow = MutableStateFlow(STATUS_DATA) В данном случае использование StateFlow приведет к более редким обновлениям данных и более удобному их получению. Для отправки состояний из viewModel лучше использовать:private val flow = MutableSharedFlow<T>(   replay = 1,   extraBufferCapacity = 0,   onBufferOverflow = BufferOverflow.DROP_OLDEST)Так как не происходит проверка данных, то мы будем получать все состояния, а так-же последнее из них будет повторно воспроизводиться при смене конфигурации. Для отправки единичных эвентов из viewModel лучше использовать:private val flow = MutableSharedFlow<T>(   replay = 0,   extraBufferCapacity = 1,   onBufferOverflow = BufferOverflow.DROP_OLDEST)В данном случае будет сохраняться последний отправленный эвент, до первого считывания его подписчиком. Ознакомившись с разными инструментами и подходами мы сделали следующие выводы:  С LiveData проще работать, но Flow дает больше гибкости; При работе с Flow нужно учитывать особенности работы корутин; Требуется выбирать, где использовать StateFlow, а где - SharedFlow; Для SharedFlow нужно правильно подбирать параметры; Возможно использование Channel, но только в ограниченных случаях.  Мы начали миграцию с написания нового кода с применением Flow. Первой ViewModel на новом подходе стала новая главная в приложении, с нее мы начали писать новые вьюмодели сразу на Flow. Рефакторинг старого кода производился медленно и постепенно, в рамках техдолга.  В процессе рефакторинга мы затронули в том числе и старые презентеры, которые были написаны еще с применением реактивного подхода. Эти работы тоже учитываются в общих сроках миграции. Этап с рефакторингом был самым трудоемким, например, для одного из самых старых разделов приложения время работ составило ~30-40 часов.  Мы закончили переход удалением последнего вхождения LiveData в старом коде, таким образом общие сроки составили чуть меньше 1 года.  На момент старта миграции в проекте было ~55 классов ViewModel, сейчас - более 100.