Flow или не Flow? Подписка на сообщения в Kotlin.

В этой небольшой статье я хочу обсудить различные паттерны выполнения подписки на реакцию в Kotlin через обратные вызовы и через Flow.

Предположим, что у вас есть некий объект или структура, и вы хотите наблюдать за изменениями, происходящими в этой структуре.

Классический способ для этого следующий:

fun MyStructure.onChange(block: MyStructure.(Key) -> Unit)
Войти в полноэкранный режим Выйти из полноэкранного режима

а затем используйте его следующим образом:

myStructure.onChange{ key ->
  println(get(key))
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Я намеренно сделал это, используя функцию приемника Kotlin, потому что она делает классическую компоновку еще более аккуратной. Вы передаете не только место изменения, но и ссылку на измененный объект, поэтому вы можете использовать предопределенную функцию блока.

Теперь давайте посмотрим, как это выглядит с использованием Kotlin coroutines Flow API:

val MyStructure.changes: Flow<Key>

myStructure.changes.onEach{ key ->
  println(myStructure.get(key)
}.launchIn(scope)
Вход в полноэкранный режим Выход из полноэкранного режима

Кажется, что Flow намного сложнее в использовании. Кроме того, для его использования требуется CoroutineScope. А поскольку вы не должны использовать для этого GlobalScope, вам нужно подумать о жизненном цикле scope.

Я не хочу говорить здесь о проблемах производительности. Если у вас так много подписчиков, это проблема производительности, вам нужно решать эту проблему в другом месте, но накладные расходы Flow, вероятно, также больше.

С другой стороны, вы можете легко выполнять преобразования с событиями из Flow. Например, так:

myStructure.changes
  .filter{ key -> key.startsWith("my")}
  .map{ key -> myStructure.get(key)}
  .onEach{ value -> println(value)}
  .launchIn(scope)
Вход в полноэкранный режим Выход из полноэкранного режима

Но есть проблема, о которой люди часто забывают.

Если вы делаете подписку, вы, вероятно, захотите и отписаться от нее. Иначе это приводит к утечке памяти (в смысле VM) — некоторые объекты избегают сборки мусора, потому что кто-то держит их хэндл подписки.

Поэтому вы пишете код, подобный этому:

fun MyStructure.onChange(block: MyStructure.(Key) -> Unit)
fun MyStructure.removeChangeListener(block: MyStructure.(Key) -> Unit)
Войти в полноэкранный режим Выйти из полноэкранного режима

На самом деле, пожалуйста, не пишите такой код. Потому что в этом случае вы полагаетесь на равенство функций для удаления правильного слушателя, а равенство функций ненадежно. Более того, вы не сможете создать функцию через лямбду, как я сделал выше. Вам нужно создать ее отдельно и где-то хранить.

Я обычно делаю это следующим образом:

fun MyStructure.onChange(owner: Any, block: MyStructure.(Key) -> Unit)
fun MyStructure.removeChangeListener(owner: Any)
Войти в полноэкранный режим Выйти из полноэкранного режима

В этом случае я использую объект owner для проверки равенства владельца обратного вызова. Если один владелец держит несколько обратных вызовов, все они будут удалены, и это имеет свое применение. Единственная проблема теперь — не забыть удалить слушателя, когда он больше не используется (а это значит, что нужно подумать о времени жизни, как в случае с Flow).

Flow не требует специальных методов отписки. Обычно, если вы прекращаете потреблять поток, он больше не используется. Также вы можете отписаться от него вручную, отменив задание подписки:

val job = myStructure.changes.onEach{ key ->
  println(myStructure.get(key)
}.launchIn(scope)

job.cancel()
Войти в полноэкранный режим Выйти из полноэкранного режима

Благодаря структурированному параллелизму, вам обычно не нужно думать об отмене задания вручную. Когда внешняя область закрывается, все дочерние задания отменяются автоматически. Таким образом, управление временем жизни происходит автоматически.

Тем не менее, вам нужно управлять временем жизни через область видимости coroutine (что довольно удобно).


Оба способа действительны, и я не могу сказать, что один из них должен всегда использоваться вместо другого. Думаю, для сложных систем я бы предпочел способ Flow, потому что нет риска забыть отписаться. Но в некоторых случаях вы не захотите возиться с диапазонами.


Фотография обложки Криса Стенгера на Unsplash

Оцените статью
devanswers.ru
Добавить комментарий