Fala Devs! Com toda certeza você já deve ter tido a necessidade de fazer uma requisição HTTP, eu mesmo já escrevi um artigo muito legal que utilizamos o DIO como cliente HTTP para fazermos requisições a nossa API (https://blog.flutterando.com.br/consumindo-api-utilizando-o-dio-9ec72aeceeaa).
Entretanto podemos passar por situações que precisamos fazer uma requisição em que o resultado venha de forma gradativa ao cliente, há algumas formas que podemos realizar esta tarefa, a primeira maneira é fazer que nosso cliente fique acessando o servidor continuamente em busca de atualizações, esta técnica é chamada de pooling.
Outra forma seria utilizar os WebSockets, eles são talvez a melhor opção quando queremos receber dados de forma gradativa e em tempo real.
Em seu funcionamento é criada uma sessão de comunicação interativa entre o cliente e o servidor, recebendo mensagens em tempo real, funcionando de forma semelhante ao Firestore.
Entretanto ela é uma técnica um pouco custosa e complexa, e as vezes não precisamos de lima solução tão robusta assim, então existe um meio termo?
E a resposta é SIM! O Server-Sent Events(popularmente conhecido como SSE), um método que funciona de forma semelhante ao Websocket, ele basicamente definira uma comunicação em uma API chamada de EventSource e esta comunicação se manterá aberta recebendo mensagens de forma gradativa do servidor.
E um bônus, neste artigo não implementaremos apenas o cliente, veremos também como criar o SSE do lado do servidor e para isto utilizaremos, é claro, o DART para o backend com o pacote SHELF. https://pub.dev/packages/shelfv
Servidor
Para isto vamos criar um projeto em branco em Dart. Execute
dart create server

Utilizaremos três pacotes para desenvolver nossa API em Dart:
Shelf(https://pub.dev/packages/shelf) — Pacote responsável por nos ajudar a criar nossos webservers
Shelf_router(https://pub.dev/packages/shelf_router) — Pacote que trata a criação de caminhas para os nossos endpoints.
Eventsource(https://pub.dev/packages/eventsource) — Pacote responsável por implementar o padrão do eventsource(SSE) em nossa api.

Com esses três pacotes nosso pubspec deverá ficar da seguinte maneira
A primeira tarefa que faremos é fazer uma implementação do eventsource em nosso projeto

Basta adicionarmos a implementação a seguir
| library shelf_eventsource; |
| import “dart:convert”; |
| import “package:eventsource/publisher.dart”; |
| import “package:eventsource/src/encoder.dart”; |
| import “package:shelf/shelf.dart”; |
| /// Create a shelf handler for the specified channel. |
| /// This handler can be passed to the [shelf.serve] method. |
| Handler eventSourceHandler( |
| EventSourcePublisher publisher, { |
| String channel = “”, |
| bool gzip = false, |
| }) { |
| // define the handler |
| Response shelfHandler(Request request) { |
| if (request.method != “GET”) { |
| return Response.notFound(null); |
| } |
| if (!request.canHijack) { |
| throw ArgumentError(“eventSourceHandler may only be used with a “ |
| “server that supports request hijacking.”); |
| } |
| // set content encoding to gzip if we allow it and the request supports it |
| bool useGzip = |
| gzip && (request.headers[“Accept-Encoding”] ?? “”).contains(“gzip”); |
| // hijack the raw underlying channel |
| request.hijack((untypedChannel) { |
| var socketChannel = (untypedChannel).cast<List<int>>(); |
| // create a regular UTF8 sink to write headers |
| var sink = utf8.encoder.startChunkedConversion(socketChannel.sink); |
| // write headers |
| sink.add(“HTTP/1.1 200 OK\r\n“ |
| “Content-Type: text/event-stream; charset=utf-8\r\n“ |
| “Cache-Control: no-cache, no-store, must-revalidate\r\n“ |
| “Connection: keep-alive\r\n“); |
| if (useGzip) sink.add(“Content-Encoding: gzip\r\n“); |
| sink.add(“\r\n“); |
| // create encoder for this connection |
| var encodedSink = EventSourceEncoder(compressed: useGzip) |
| .startChunkedConversion(socketChannel.sink); |
| // initialize the new subscription |
| publisher.newSubscription( |
| onEvent: encodedSink.add, |
| onClose: encodedSink.close, |
| channel: channel, |
| lastEventId: request.headers[“Last-Event-ID”]); |
| }); |
| } |
| return shelfHandler; |
| } |
Agora podemos implementar o entry point do nossa API que está no sse.dart
Primeiramente criamos o método que irá gerar as as mensagens retornadas para o cliente de forma assíncrona, para isto basta quando quisermos retornar alguma informação executar o publisher.add() passando um evento de resposta e na hora que finalizar o processamento executar o publisher.close()
Press enter or click to view image in full size

Feito isso podemos criar nosso endpoint (no caso o /events) e passar nosso EventSourcePublisher como parâmetro do método eventSourceHandler.
Press enter or click to view image in full size

| import “dart:async”; |
| import ‘dart:convert’; |
| import ‘package:eventsource/publisher.dart’; |
| import “package:shelf/shelf_io.dart” as io; |
| import ‘package:shelf_router/shelf_router.dart’; |
| import ‘eventsource.dart’; |
| main() { |
| var app = Router(); |
| app.get(“/events”, (r) { |
| final publisher = EventSourcePublisher(); |
| generateEvents(publisher); |
| var handler = eventSourceHandler(publisher); |
| handler(r); |
| }); |
| io.serve(app, “localhost”, 8080); |
| } |
| generateEvents(EventSourcePublisher publisher) { |
| int id = 0; |
| Timer.periodic(const Duration(seconds: 1), (timer) { |
| final data = json.encode({ |
| ‘id’: id, |
| ‘message’: ‘event $id‘, |
| ‘finished’: id == 10, |
| }); |
| publisher.add(Event(data: data)); |
| if (id == 10) { |
| timer.cancel(); |
| publisher.close(); |
| } |
| id++; |
| }); |
E sucesso! Nossa API está pronta, basta executar e ela estará disponível na porta que configuramos (8080), execute:
dart run

O resultado será o seguinte:
Press enter or click to view image in full size

Cliente
No nosso aplicativo Flutter só iremos precisar do pacote eventsource(https://pub.dev/packages/eventsource)
Adicione ele no seu pubspec

Este pacote nos oferece um objeto Eventsource que contém o método conect() que faz a conexão com a API e rebe as mensagens. Como próximo passo devemos criar uma Stream e emitir o resultado da mensagem que vem da API nessa Stream.
Press enter or click to view image in full size

Também devemos criar um getter para conseguir acessar a stream.
Press enter or click to view image in full size

A implementação da classe completa deve ficar assim:
| import ‘dart:async’; |
| import ‘dart:convert’; |
| import ‘package:eventsource/eventsource.dart’; |
| import ‘../enums/sse_enum.dart’; |
| import ‘../sse_adapter.dart’; |
| class EventsourceSseAdapterImpl implements SseAdapter { |
| final kTimeout = const Duration(seconds: 30); |
| late StreamController<Map> _streamController; |
| _url({ |
| required String baseUrl, |
| String? relativePath, |
| String? path, |
| String scheme = ‘http’, |
| Map<String, dynamic>? queryParameters, |
| }) { |
| final listQueryString = <String>[]; |
| var q = ”; |
| queryParameters?.forEach((key, value) { |
| listQueryString.add(‘$key=$value‘); |
| }); |
| if (listQueryString.isNotEmpty) q = ‘?${listQueryString.join(‘&’)}’; |
| if (scheme.isNotEmpty) { |
| baseUrl = baseUrl.replaceAll(‘http://’, ”).replaceAll(‘https://’, ”); |
| baseUrl = ‘$scheme://$baseUrl‘; |
| } |
| return ‘$baseUrl${relativePath ?? ”}${path ?? ”}$q‘; |
| } |
| _body(Map<String, dynamic>? data) { |
| if (data == null) return null; |
| return json.encode(data); |
| } |
| @override |
| Future<SseAdapter> connect({ |
| required String baseUrl, |
| String? relativePath, |
| required String path, |
| String scheme = ‘http’, |
| bool closeOnError = true, |
| Map<String, dynamic>? queryParameters, |
| Map<String, String>? headers, |
| Map<String, dynamic>? data, |
| SseMethod method = SseMethod.GET, |
| }) async { |
| try { |
| late final EventSource _eventSources; |
| final url = _url( |
| baseUrl: baseUrl, |
| relativePath: relativePath, |
| path: path, |
| queryParameters: queryParameters, |
| scheme: scheme, |
| ); |
| final body = _body(data); |
| _streamController = StreamController<Map>(); |
| _eventSources = await EventSource.connect( |
| url, |
| headers: headers, |
| method: method.value, |
| body: body, |
| ); |
| _eventSources.listen((Event event) { |
| try { |
| if (!_streamController.isClosed && event.data != null) { |
| var dataJson = json.decode(event.data!); |
| _streamController.add(dataJson); |
| } |
| } catch (e) { |
| rethrow; |
| } |
| }).onError((e) { |
| _streamController.addError(Exception(“Erro SSE”)); |
| if (closeOnError) close(); |
| }); |
| return this; |
| } catch (e) { |
| rethrow; |
| } |
| } |
| @override |
| Stream get stream => _streamController.stream.timeout(kTimeout); |
| @override |
| bool isClosed() => _streamController.isClosed; |
| @override |
| void close() { |
| _streamController.close(); |
| } |
| } |
Agora temos uma stream que emite um resultado de um Map<String, dynamic> agora em nossa datasource podemos escutar essa stream para ela fazer o mapper do JSON para um Model
Press enter or click to view image in full size

No repositório fazemos um mapper de model para entity.
Press enter or click to view image in full size

E por fim na nossa controller podemos escutar a Stream e mandar atualizar nossa store para fazer reatividade na nossa page.
Press enter or click to view image in full size

Desta forma teremos o seguinte resultado

Legal ne?? muito simples trabalhar com SSE em nossos aplicativos.
O projeto completo pode conferir abaixo




