Подробный обзор виджета StreamBuilder от Flutter

Так что же такое виджет StreamBuilder?

Это средство реагирования на асинхронную обработку данных. Виджет StreamBuilder - это StatefulWidget, поэтому он может вести «текущую сводку» и / или записывать и отмечать «последний элемент данных» из потока данных. В большинстве случаев StreamBuilder принимает во внимание последнее «событие данных» (последнее встреченное событие элемента данных из потока) для определения следующего виджета, который будет построен.

Мы говорим здесь о трансляциях

StreamBuilder - один из двух асинхронных виджетов, предоставляемых Flutter для работы с асинхронными операциями. Другой виджет - FutureBuilder. На самом деле, я бы рекомендовал сначала прочитать статью Decode FutureBuilder, а затем вернуться к этой статье. Это потому, что тот же подход, примененный здесь в StreamBuilder, можно найти в том, как работает виджет FutureBuilder, но с использованием менее сложного класса Future по сравнению с классом Stream.

Откровенно говоря, Streams немного более увлекательны. Что касается этой статьи, мы будем ссылаться на потоки, поскольку они относятся только к виджету StreamBuilder. В Streams есть еще много чего, но это уже отдельная статья.

Нет встроенного кода?

Как всегда, я предпочитаю использовать скриншоты, а не суть, чтобы показать код в своих статьях. Я считаю, что с ними легче работать и легче читать. Однако вы можете щелкнуть / коснуться их, чтобы увидеть код как суть или в Github. По иронии судьбы, эту статью о мобильной разработке лучше читать на компьютере, чем на телефоне. Кроме того, мы программируем в основном на своих компьютерах; не на наших телефонах. Теперь.

Давай начнем.

Учиться на примере

Мы будем использовать пример из Flutter Cookbook, Работа с WebSockets, чтобы продемонстрировать StreamBuilder в действии. In включает отправку текстового сообщения на веб-сервер, который просто возвращает это текстовое сообщение - с использованием объекта класса типа Stream. Ниже приведен снимок экрана, отображающий сердце примера Поваренной книги. Это функция build (), которая содержит виджет TextFormField для ввода текстового сообщения и виджет StreamBuilder для получения и последующего отображения этого текстового сообщения. эхом отозвался веб-сервером. Оба виджета содержатся в виджете Столбец. Для этого примера это очень простой интерфейс.

Основание ручья

StreamBuilder - это StatefulWidget. Это StatefulWidget, но не раньше, чем расширяется другой класс, который фактически расширяет StatefulWidget, StreamBuilderBase ‹T, AsyncSnapshot ‹T››. На снимке экрана ниже изображена иерархия классов:

Класс StreamBuilderBase ‹T, S› расширяет StatefulWidget и является абстрактным классом. В нем есть несколько абстрактных функций, которые необходимо реализовать при расширении класса. Одна из них - функция build ().

Это любопытно, поскольку с большинством StatefulWidgets именно связанный с ними объект State определяет функцию build (), а не сам StatefuleWidget. Однако в случае класса StreamBuilderBase связанный с ним объект State ссылается на собственную функцию build () этого StatefulWidget.

T для данных

Примечание. Здесь используются общие типы данных. Из потока могут поступать данные любого типа. Это может быть любой класс, любой из встроенных типов Дарта s или даже специальный тип, называемый динамический. Как вы видите на снимке экрана ниже, виджет StreamBuilder использует нотацию универсального типа данных (заглавная буква T) для указания типа данных в Stream. В большинстве случаев вы указываете тип данных в угловых скобках при определении StreamBuilder.

Обратите внимание, что в примере Cookbook тип данных подразумевается, а не указывается в угловых скобках. Однако его можно было так же легко уточнить. В этом случае задействован сервер WebSocket, который может возвращать данные любого типа. К счастью, язык Dart позволяет это сделать с помощью специального типа dynamic. Итак, небольшое изменение примера с Поваренной книгой будет работать точно так же.

The Heart Of The StreamBuilder

Как и виджет FutureBuilder, виджет StreamBuilder включает функцию, называемую _subscribe (), и является основой его работы. Сегодня, чтобы понять, как работает StreamBuilder, мы начнем с него и выйдем из этой функции, объясняя, как задействованные части сочетаются друг с другом на каждом этапе пути.

Слушайте поток

В частности, прислушивайтесь к следующему событию данных в потоке. Строка, показанная на скриншоте выше, является сердцем асинхронной операции StreamBuilder. Функция listen () добавляет к потоку обработчик данных. Это первый параметр в функции listen (), и это подписка на поток, которая остается привязанной к этому потоку, пока не будет отменена подписка. Теперь каждый раз, когда происходит событие данных (т. Е. Из потока поступает новый элемент данных), вызывается обработчик подписанных данных. Обратите внимание, что это потоки с одной подпиской и, в отличие от потоков вещания, на него может быть подписан только один слушатель.

При более внимательном рассмотрении этого конкретного обработчика данных мы видим сам StatefulWidget, widget (то есть StreamBuilder ‹T›), на который имеется ссылка и предоставляется вновь поступивший элемент данных типа данных T, который в этот пример из Поваренной книги, который, как мы знаем, относится к особому типу, динамический. Все заключено в функцию setState ().

Теперь, поскольку он заключен в функцию setState (), мы также знаем, что функция build () объекта State вскоре будет запущена с предоставлением переменной экземпляра _summary в собственную функцию build () StatefulWidget. Давайте рассмотрим эту переменную экземпляра или свойство под названием _summary.

Сделайте шаг назад и посмотрите StreamBuilder

Для этого давайте вернемся немного назад и еще раз посмотрим, что составляет виджет StreamBuilder. Мы знаем, что виджет StreamBuilder расширяет класс StreamBuilderBase ‹T, AsyncSnapshot ‹T››.

Класс StreamBuilderBase ‹T, S› расширяет класс StatefulWidget и создает частный библиотечный объект состояния с именем _StreamBuilderBaseState. Сокращенные снимки экрана этих двух классов показаны ниже.

У каждого есть переменные экземпляра, представляющие особый интерес. StatefulWidget определяет свойство Stream с именем stream универсального типа данных T. Хотя связанный с ним объект State определяет свойство с именем _summary универсального типа данных, С.

Вернувшись назад по иерархии классов, мы можем увидеть тип данных свойства «сводка взаимодействия», _summary, в случае класса StreamBuilder - это класс типа AsyncSnapshot ‹T›.

Сделать снимок

Итак, при каждом поступлении элемента данных из потока он собирается как сводка этого события данных в неизменяемый объект класса типа AsyncSnapshot. Этому объекту также предоставляется соответствующее состояние соединения в форме перечислимого типа, называемого ConnectionState. Итак, как описано в документации, этот объект AsyncSnapshot является неизменным представлением самого последнего взаимодействия с асинхронным вычислением, будь то Stream или Future. Следовательно, он также используется с виджетом FutureBuilder.

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

Было бы разумно, чтобы свойство data относилось к типу данных Generic, T. Это тот же тип, что и сам StreamBuilder: StreamBuilder ‹T›. Свойство должно удерживать элемент данных, когда он прибывает из потока. Обратите внимание, что свойство error относится к типу Object - базовому объекту всех объектов в языке программирования Dart. Это позволяет разработчику, если и когда возникает ошибка, назначить «любой» объект, который он считает подходящим - будь то класс Exception или другой. Должен любить варианты.

Прохождение

Давайте посмотрим, как создается снимок. Помните, что обработчик данных в форме анонимной функции был определен в функции listen () объекта Stream. Как вы знаете, объект Stream упоминается в StatefulWidget как переменная экземпляра с именем stream. Так что же происходит, когда, наконец, происходит событие данных и элемент данных прибывает из потока? Это случилось:

Мы видим, что переменная экземпляра _summary, (общего типа, S) в этом примере Cookbook относится к типу класса, AsyncSnapshot ‹T›, принимается (вместе с с полученным элементом данных) функцией StatefulWidget, afterData (), а затем снова возвращается. Так что же это за функция afterData ()?

Прослеживая иерархию классов, мы обнаруживаем, что функция afterData () определена в абстрактном классе StreamBuilderBase ‹T, S›, но реализована в его подклассе StreamBuilder ‹T› следующим образом:

Именно там мы видим, что «снимок» создается с учетом последних данных. Обратите внимание, что первый параметр, current, по существу игнорируется. Однако «состояние соединения» active также передается в неизменяемый объект и затем возвращается вызывающей стороне, присваивая его этой переменной экземпляра, _summary.

Еще раз взглянув на функцию _subscribe (), в этом примере Cookbook переменная экземпляра _summary будет иметь тип AsyncSnapshot ‹dynamic›. Ему был назначен неизменяемый объект из функции afterData (), и все это заключено в функцию setState () объекта State. Таким образом, вскоре после этого вызывается функция build () объекта State.

При этом новый неизменяемый объект типа AsyncSnapshot ‹dynamic› упоминается в переменной экземпляра _summary и передается в функцию build (), находящуюся в соответствующий StatefulWidget. Посмотрим, что дальше.

Заполните сборку

Абстрактный класс StreamBuilderBase ‹T, S› определяет функцию build (), но она реализуется ее подклассом StreamBuilder ‹T›. Все это показано на двух скриншотах ниже.

Конструктор StreamBuilder

У класса StreamBuilder есть переменная экземпляра, называемая builder. Он представляет функцию, и эта функция вызывается, когда объект State StreamBuilder вызывает свою функцию build (), которая, в свою очередь, вызывает функцию build () StreamBuilder (см. Снимок экрана выше). Эта функция, в свою очередь, вызывает функцию, представленную свойством, builder. Это означает, что эта переменная экземпляра или свойство, называемое builder, на самом деле является «обязательным» именованным параметром, найденным в конструкторе const StreamBuilder.

Вы можете догадаться, что typedef, AyncWidgetBuilder ‹T›, для переменной экземпляра builder включает в себя Объект BuildContext и объект AsyncSnapshot в качестве параметров. Это позволяет разработчикам затем определять и передавать, в большинстве случаев, анонимную функцию с такими параметрами в именованный параметр builder в конструкторе StreamBuilder.

В нашем примере Cookbook мы видим, что анонимная функция действительно определена и передана конструктору StreamBuilder. Он возвращает виджет Padding с виджетом Text, отображающий элемент данных, полученный из потока.

Повернись и сделай это снова

Давайте вернемся и продолжим процесс снова. На этот раз мы начнем с примера Cookbook, в котором определен StreamBuilder. Мы видели это на скриншоте выше. Он принимает объект потока WebSocket и анонимную функцию, но как насчет «начальных данных»?

Первоначальный снимок

Как видно на снимке экрана выше, существует именованный параметр initialData. Он не является обязательным и, если он предоставляется, должен иметь тот же тип данных, что и поток данных - и если он предоставлен, в результате будет получен исходный объект «моментальный снимок» с этим значением в качестве элемента данных, а не с нулевым значением в противном случае.

Вы видите, что класс StreamBuilder ‹T› также включает функцию, называемую initial (), и возвращает объект AsyncSnapshot, принимающий значение «начальных данных», если оно есть (в противном случае - null), а также состояние соединения. из 'none.' См. выше.

Первая сборка

Как вы знаете, StreamBuilder является подклассом виджета StatefulWidget, поэтому при первом построении связанный с ним объект State будет выполнять «одноразовый» вызов функции initState (). Обратите внимание на функции в функции initState (). Вы видели это раньше.

Теперь вы видите, что функция initial (), включающая именованный параметр initialData, фактически вызывается и создает исходный объект AsyncSnapshot в переменной экземпляра, _summary .

Здесь также сначала вызывается функция _subscribe (), реализующая «обработчик данных» для указанного объекта Stream. Видите, как все складывается? StreamBuilder теперь готов ответить на любое событие асинхронных данных, которое может исходить из потока.

Данные, при ошибке или готово

Как видно выше, в функции listen () объекта Stream используются два дополнительных «именованных параметра». Оба определяют «функции обратного вызова», которые вызываются в случае параметра «onError», если в потоке есть ошибка, или, в случае параметра «onDone», когда поток закрывается и отправляет сообщение «готово». ' мероприятие.

Опять же, если выполняется какая-либо из этих трех функций обратного вызова, создается новый неизменяемый объект типа AsyncSnapshot ‹T›, который назначается свойству _summary. Обратите внимание: каждая из них вызывает разные функции, реализованные в классе StreamBuilder ‹T›: afterData (), afterError () и afterDone ().

Вы можете видеть, что каждый созданный объект AsyncSnapshot уникален в зависимости от обстоятельств. Первый из них мы уже рассмотрели. Он вызывает статическую функцию withData () и предоставляет элемент данных и состояние подключения «активно». Затем, конечно же, скоро появится функция setState (). вызывается, вызывая повторный запуск build (), передавая элемент данных функции "builder" StreamBuilder.

Если в потоке произошла ошибка, создается объект AsyncSnapshot с объектом класса типа Object. Опять же, допущение использования «любого класса» для описания ошибки. В большинстве случаев это класс Exception. Затем, конечно, вскоре вызывается функция setState () и т. Д. И т. Д.

Только когда поток закрывается и отправляет событие «done», текущий объект AsynSnapshot не игнорируется. Как видите, функция inState () используется для создания нового объекта AsynSnapshot, сохраняющего любой элемент данных, содержащийся в текущем объекте, но теперь с состоянием подключения «готово». Затем, конечно же, Скоро будет вызвана функция setState ()… вы поняли.

Сначала ждем

Знаете, еще до того, как из потока поступит даже первый элемент данных, происходит одна вещь. Итак, перейдем к обзору. Опять же, в примере Cookbook был создан объект потока и передан конструктору StreamBuilder. Также в именованный параметр StreamBuilder builder передается анонимная функция, которая запускается при каждой сборке виджета, в которой она сама возвращает виджет. Хорошо, пока все хорошо.

Поскольку это его «первая сборка», связанный объект State StreamBuilder запускает свою функцию initState (). Там создается «начальный» объект AsyncSnapshot, принимающий любой элемент исходных данных, переданный в конструктор StreamBuilder. В этом случае начальный элемент данных не был передан, поэтому свойство AsyncSnapshot, data, имеет значение null. Кроме того, в этой «первой сборке» функция _subscribe () вызывается, определяя «обработчик данных», «обработчик ошибок» и «обработчик выполненных операций» для указанного объекта Stream. Таким образом, подготовка приложения к ответу на любые и все «события», которые теперь могут происходить из Stream. У нас все хорошо?

Так что после всего этого! После того, как мы в некотором смысле «подключились» к потоку. Теперь мы готовы ко всему из Stream. После этого угадайте, что нам делать?

Мы вызываем функцию afterConnected () и создаем еще один совершенно новый объект AsyncShapshot, присваивая его переменной «summary» с состоянием подключения… «ожидание».

Мы снова видим функцию inState (). Он вызывается в функции afterConnected (), которая присваивает начальное значение данных (если есть) новому объекту AsyncSnapshot, но теперь с состоянием соединения «ожидание».

Итак, что строили вначале?

Так что же построили в первую очередь? Что ж, давай посмотрим… эээ… ничего.

Когда выполнение этого приложения поступает в функцию 'builder' в первый раз, оно обнаруживает, что логическое выражение snapshot.hasData имеет значение false, и поэтому в виджет Text передается пустая строка. .

Опять же, состояние подключения снимка установлено на ConnectionState.waiting. В этом конкретном примере из потока ничего не поступает, пока пользователь не введет текст. Затем этот текст «возвращается» обратно пользователю веб-сервером с использованием объекта Stream. Наконец, он передается в текстовый виджет, который отображает его на экране.

Так что с первой сборкой особо не на что смотреть. Ниже приведен снимок (как бы) класса AsyncSnapshot. Как видите, свойство hasData на самом деле является получателем. Тот, который просто проверяет наличие null в переменной экземпляра data.

Давай запустим!

Теперь мы запустим приложение, но в этой статье быстро рассмотрим процесс.

В каком состоянии мы находимся?

Опять же, когда вы впервые запустите пример, там будет не на что смотреть. Однако ниже я вставил оператор Switch Case в функцию «builder» и стратегически разместил точку останова, где состояние соединения в настоящее время установлено на «ожидание». Как вы видите, в начале состояние подключения именно такое.

После выпадения из оператора Switch Case логическое выражение snapshot.hasData будет ложным, и отобразится «пустая строка». Таким образом, все будет так, пока мы не введем фразу «Hello World!» И первый элемент данных не вернется с веб-сервера и не вернется через поток.

Именно в «обработчике данных» фраза «Hello World!» Возвращается с веб-сервера обратно через поток. Здесь создается новый объект AsyncSnapshot, который в конечном итоге поступает в качестве параметра snapshot в функцию «builder», указанную ниже.

Мы знаем, что по прибытии в функцию «builder» состояние соединения объекта AsyncSnapshot будет «активным», а выражение snapshot.hasData будет истинным. Следовательно, на экране появляется фраза «Hello World!». Легкий.

TL; DR

Этого должно быть достаточно. Вы так не думаете? Остальная часть этой статьи - просто подливка.

Нулевой поток?

Вы обратили внимание на оператор if, окружающий весь код, составляющий функцию, _subscribe ()? Конечно, это подразумевает, что переменная экземпляра или свойство с именем stream, найденное в абстрактном классе StreamBuilderBase, может иметь значение NULL по той или иной причине.

Почему нулевой поток?

Взглянув на конструктор StreamBuilder, мы легко увидим, что действительно параметр Stream является необязательным. То есть это именованный параметр, и по самой природе именованных параметров это необязательный параметр без аннотации «@required». Итак, почему это необязательно ?? Обратите внимание, цель не в этом. Это не должно быть необязательным. Однако он позволяет время от времени иметь значение NULL. В большинстве случаев на протяжении всего срока службы самого приложения.

Нет потока Нет данных

Вернувшись к функции initState (), мы знаем, что создается объект AsyncSnapshot с начальными данными, если они есть, и состоянием подключения none. Итак, когда build () сначала запускается с нулевым значением для потока, здесь особо не на что смотреть - особенно в этом примере Cookbook.

Нулевой поток или нет

Обратите внимание: на скриншоте выше я буквально изменил параметр Stream на null без фатальных последствий. Сработала «горячая перезагрузка», и пример приложения был восстановлен без ошибок. Это означает, что время от времени из «параметра потока» может поступать нулевое значение. Это позволяет ему быть нулевым; быть «другим» потоком на протяжении всего времени существования самого приложения. Как разработчик, вы можете определить, имеет ли поток значение NULL или нет. Значение connectionState.none указывает, что Stream имеет значение NULL. Значение connectionState.waiting указывает, что Stream не является нулевым. С опцией «начальные данные» разработчик может предоставить элемент данных до тех пор, пока поток действительно не перейдет в «оперативный режим» и больше не будет иметь значение null.

Изменить поток

Кстати, при постоянной горячей перезагрузке, когда я изменил параметр Stream на null, сработала точка останова в функции объекта State, didUpdateWidget (). Таким образом, демонстрируя, что если и когда Stream действительно изменится так, как ему разрешено, StreamBuilder узнает об этом. И если Stream изменится, должно произойти несколько вещей.

После подтверждения изменения в первом операторе if выше, второй оператор if проверяет, вызывалась ли уже функция _subscribe (), создавая StreamSubscription. Объект, хранящийся в переменной экземпляра, _subscription.

Знайте, что этот объект StreamSubscription, впервые созданный функцией listen (), является активным объектом, предоставляющим обработчики событий «onData», «onError» и «onDone». Он также предоставляет множество других обработчиков событий, хотя здесь не упоминается, поскольку они выходят за рамки этой статьи. Его можно использовать, чтобы остановить прослушивание или временно приостановить события. Поэтому важно закрыть (то есть отменить) такие подписки, когда поток закрыт и больше не используется. Фактически, это может привести к утечкам памяти, если такие подписки не отменить. Именно здесь на помощь приходит функция _unsubscribe ().

Как вы видите выше, объект Stream Subscription вызывает свою функцию cancel (). Это отменяет подписку, поэтому она больше не получает события. Сама ссылка затем аннулируется (присваивается null), чтобы, казалось бы, сразу же стимулировать сборку мусора. Хотя лично я уверен, работает ли это ... пока.

Отказаться от подписки при удалении

Обратите внимание: это так важно, когда все сделано и приложение завершено, подписка действительно отменена - и поэтому функция _unsubscribe () также вызывается в другой подходящей функции: объект State удалить функцию (). Когда вы создаете свои собственные потоки, не забудьте отменить их!

Состояние изменений

После отмены подписки мы еще не закончили. Создается совершенно новый объект AsyncSnapshot, чтобы передать текущее состояние дел.

Именно в этот момент приложение считается «отключенным» от потока. Следовательно, это затем отражается в состоянии соединения, присвоенном новому объекту AsyncSnapshot как «none». Элемент данных, если таковой имеется, из предыдущего объекта «моментального снимка» переносится в новый.

Ошибка потока

Я взял этот небольшой фрагмент кода, чтобы внести ошибку в пример Cookbook. Я просто вставил ее в файл Dart как высокоуровневую функцию. Все, что он делает, это возвращает целые числа от 1 до 3 в потоке, пока цикл for не достигнет 4. Затем он намеренно генерирует исключение.

Затем я создал новое свойство или переменную экземпляра в StatefulWidget из примера Cookbook, назвав его stream. Затем он инициализируется вызовом этой новой функции, в результате чего создается объект Stream. Угадай, к чему все это идет.

Как вы видите выше, я просто перешел к именованному параметру StreamBuilder, stream, и вставил этот новый объект Stream. Затем я снова запущу пример Cookbook. Кстати, вы обратили внимание на тип возврата в этой новой функции Встроенный тип, int. Виджет SteamBuilder (без угловых скобок) может определять тип данных потока, позволяя ему теперь принимать список целых чисел вместо последовательности текста. Кроме того, он включает в себя веб-сервер, который отображает любой тип данных, отправленный ему.

Шаг за шагом к ошибке

Я разместил несколько точек останова в функции _subscribe () StreamBuilder, что позволяет нам пройти через то, что займет всего микросекунды при повторном запуске этого примера Cookbook. Ниже вы можете увидеть, как в обработчик данных объекта Stream поступает буквально «поток» чисел.

Мы в ошибке

Когда новая функция достигает целого числа 4, мы намеренно выдаем ошибку, чтобы посмотреть, что произойдет. Происходит другое событие. Вместо этого вызывается функция обратного вызова onError, а переменная экземпляра _summary получает объект AsyncSnapshot другого типа. Его состояние соединения по-прежнему установлено как «активное», но теперь ему предоставляется объект типа класса Exception.

Следовательно, поток закрывается. Поскольку в StreamBuilder есть код для случая, когда поток закрывает переменную экземпляра, _summary немедленно снова заменяется еще одним объектом AsyncSnapshot. Тот, который по-прежнему сохраняет объект «error», но теперь с состоянием подключения «done».

Опять же, все это происходит в коде, заключенном в функцию setState () объекта State, и поэтому вы знаете, что вновь созданный объект «моментальный снимок», в свою очередь, будет передан в определенную функцию «строитель».

Так случилось, я ввел новый оператор if в эту функцию «builder». Тот, который проверяет логическое выражение, snapshot.hasError. Если это правда, эта ошибка отображается на экране. См. Выше.

Проверить на наличие ошибок

Всегда полезно проверять наличие ошибок в вашей функции «строитель». Как мы проверяем ошибки? В большинстве случаев вы используете getter, hasError в классе AsyncSnapshot, как мы только что сделали в примере приложения. Он проверяет само существование объекта «ошибка».

Итак, вот и мы. Краткое изложение того, что такое StreamBuilder и как он работает. Все сводится к функции listen (), назначенной указанному объекту Stream. С помощью серии функций setState (), включенных в функцию listen (), включающий StatefulWidget, который является StreamBuilder, затем отвечает соответственно на каждое асинхронное событие, которое может поступать из 'data source »к объекту Stream - будь то элемент данных, ошибка или событие« готово ».

Ваше здоровье.

* исходный код по состоянию на 03 апреля 2019 г.

→ Другие рассказы Грега Перри