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

Поток и StreamController

Прежде всего, давайте сначала поговорим о потоках, поскольку я уже говорил вам, что они закладывают основу для реактивного программирования. Проще говоря, вы можете рассматривать Stream как поток данных, который поступает асинхронно, то есть вы точно не знаете, когда поступят определенные данные. Он представлен как Stream<T>, где T — любой тип данных.

В отличие от Future, когда вы вызываете метод, возвращающий Stream, выполнение не прерывается, когда результат выдается (эквивалент Stream для return). Вам нужно будет вручную закрыть поток, чтобы завершить его работу.

Говоря о потоках, вы также должны быть знакомы с StreamController. StreamController — это, по сути, менеджер потока. Мы используем его, чтобы определить, какой тип потока нас интересует, и для некоторых других целей.

Чтобы легче понять это, представьте себе трубу с двумя концами. Один для подачи воды в трубу, а другой для выпуска воды из трубы. Если подумать, вода свободно течет внутри трубы. Точно так же StreamController имеет два конца — один для ввода данных, а другой — для выхода. Данные поступают в StreamController через Sink . Данные также могут быть изменены перед отправкой.

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

  • Поток с одной подпиской. Этот тип потока допускает только одного слушателя в течение всего времени существования этого потока. Вы не можете повторно прослушать поток даже после его закрытия.
  • Широковещательный поток. Этот тип потока допускает любое количество слушателей.

Реактивное программирование

Проще говоря, реактивное программирование — это программирование с использованием асинхронных потоков данных.

Если репозиторий на уровне данных возвращает данные в виде Stream (который будут прослушивать модели ViewModel), вы можете рассматривать это как реактивное программирование.

Но как это все-таки поможет нам? Если это то, о чем вы думали, позвольте мне продемонстрировать два изображения.

Картина стоит тысячи слов — Генрик Ибсен

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

  • Сначала инициализируется представление.
  • Затем ViewModel (или блок) запрашивает необходимые данные. Связь происходит с репозиторием.
  • Репозиторий связывается с соответствующими источниками данных для получения необходимых данных.
  • Источник данных выполняет некоторые операции выборки данных и возвращает данные обратно в репозиторий.
  • Репозиторий возвращает данные обратно в ViewModel.
  • И, наконец, данные потребляются пользовательским интерфейсом.

Хотя в таком подходе к выполнению сетевых запросов нет ничего плохого, в больших проектах он может оказаться излишним. Если мы добавим некоторые данные в нашу базу данных, нам сразу же придется вызывать некоторые методы, такие как loadData() или getUpdatedData().

loadData() — это однократная доставка данных, что означает, что вам не следует вызывать такие методы, как loadData(), снова и снова. Приложение должно содержать логику для периодического запроса обновлений.

Как лучше сделать то же самое? И ответом на это будет реактивный репозиторий. Чтобы проиллюстрировать это, посмотрите это изображение.

Эта диаграмма имеет меньше шагов, чем то, что мы видели ранее. Но как это работает? Давайте посмотрим на это.

  • Во-первых, представление инициализируется и запрашивает у ViewModel начальные данные и наблюдает за ними. Наблюдает — это еще один термин для обозначения прослушивания или подписки.
  • Затем ViewModel запрашивает репозиторий для данных и наблюдает за репозиторием.
  • А репозиторий запрашивает данные из источника данных и наблюдает за источником данных.

Теперь, как данные, которые будут поступать из источника данных, будут в конечном итоге использоваться пользовательским интерфейсом, если данные не возвращаются обратно?

Вот здесь-то и проявляются подходы Streams и Reactive. 🤩

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

Опять же, репозиторий наблюдает (или слушает) ViewModel, ViewModel будет немедленно уведомлен, когда репозиторий выдает некоторый поток данных.

И, наконец, поскольку ViewModel наблюдает пользовательский интерфейс, пользовательский интерфейс получает правильные данные для отображения на экране.

При таком подходе нам не нужно снова и снова вызывать такие методы, как loadData(), по мере поступления новых данных. Мы добавляем в поток только входящие данные, после чего соответствующие слушатели автоматически получают уведомления об изменениях. Это то, что мы называем реактивным подходом.

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

Демо

Мы создадим очень простое приложение, которое изначально будет отображать список имен в ListView. И мы добавим еще несколько имен, но следуем реактивному подходу.

Вот так выглядит наше финальное приложение.

Сначала создайте новый проект Flutter и избавьтесь от стандартного приложения-счетчика. Также добавьте пакеты Equatable и RxDart в ваш pubspec.yaml файл.

flutter create stream_demo

Я назвал проект stream_demo . Вы можете назвать свой проект как угодно. Затем внутри вашей библиотеки создайте три папки и назовите их данные, домен и презентация. Файл main.dart также должен находиться в каталоге lib.

lib
 |__ data
 |__ domain
 |__ presentation
 |__ main.dart

Сначала мы начнем с Domain Layer. Внутри папки домена создайте файл models.dart. И вставьте следующий код.

Далее мы будем работать над уровнем данных. Создайте два файла repository.dart и remote_data_source.dart внутри папки данных.

Теперь добавьте следующий код в файл remote_data_source.dart.

Приведенный выше код просто возвращает List<User>, который будет возвращен репозиторием. Итак, добавьте следующий код в файл repository.dart.

Здесь UserRepository зависит от RemoteDataSource. В теле конструктора мы вызываем getUsers() и список пользователей добавляется в файл StreamController.

Кроме того, мы используем BehaviorSubject вместо StreamController. BehaviorSubject происходит из пакета RxDart. Пакет представляет собой просто расширение Stream, предоставляемое Dart. Используя BehaviorSubject, любые новые слушатели, когда начинают прослушивать поток, сразу же получают последний переданный поток данных.

Мы можем предположить, что BehaviorSubject имеет какой-то механизм кэширования, который немедленно кэширует последний отправленный Stream.

Также обратите внимание на то, как реализован метод addUser(). Мы просто добавляем нового пользователя в newList и добавляем List в наш StreamController. Таким образом, всякий раз, когда добавляется новый пользователь, слушатели, подписанные на этот поток (в блоке), немедленно получают уведомление, и наш пользовательский интерфейс немедленно обновляется.

У меня был один вопрос в прошлом, когда я видел, что вместо Stream<User> возвращается Stream<List<User>>. Основными причинами этого являются:

Когда приходит новый пользователь, мы не можем сказать, должен ли он быть вверху нашего списка, внизу или где-то посередине.

И один из основных принципов шаблона BLoC заключается в том, чтобы передавать те данные в пользовательском интерфейсе, которые будут представлены в пользовательском интерфейсе, и избегать какой-либо дополнительной бизнес-логики в пользовательском интерфейсе.

Теперь мы можем работать над Презентацией Слоем. Он будет состоять из блока, виджетов и страниц. Итак, создайте три папки внутри папки презентации и назовите их блок, виджеты и страницы соответственно.

Начнем с блока. Добавьте три файла — user_bloc.dart, user_event.dart и user_state.dart.

Событие UserRequested запускается сразу после запуска приложения. Событие UserAdded запускается, когда мы хотим добавить пользователя в наш существующий список. И событие UserInputChanged существует для обработки события, когда мы вводим данные в текстовое поле.

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

Заключение

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

Если у вас есть что сказать мне, пожалуйста, сделайте это. Я буду с нетерпением ждать всех отзывов, которые я получу. Вы также можете следить за мной в Твиттере по адресу @b_plab98, где я пишу о Flutter и Android.

Мои социальные сети:

До следующего раза, удачного кодирования!!! 👨‍💻

— Биплаб Датта