Flutter SignalR: Comunicação em Tempo Real
Flutter SignalR é a combinação que vamos usar para implementar comunicação em tempo real entre aplicativos Web e Mobile. Ao final, você terá um chat em que mensagens enviadas no navegador aparecem no app e vice-versa.
O servidor será uma aplicação ASP.NET Core 3.1 com Razor Pages, e o app será um projeto Flutter que usa o pacote signalr_client. Além disso, um cliente Web em JavaScript vai servir para testar tudo pelo navegador.
Pré-requisitos
Para seguir este tutorial, você precisa do Flutter instalado e já ter dado alguns passos com ele. Se você ainda não começou, siga nosso artigo para dar os primeiros passos com Flutter.
No lado do servidor, precisaremos do .NET Core instalado. Se você ainda não tem ele ou o Visual Studio Code, instale ambos antes de prosseguir.
Vamos começar com Flutter SignalR?
ASP.NET Core SignalR
O SignalR é uma biblioteca de código aberto que acompanha o ASP.NET Core e simplifica a comunicação em tempo real entre aplicações. Em outras palavras, o código do servidor consegue enviar conteúdo para os clientes no instante em que algo acontece.
O SignalR funciona no modelo cliente e servidor. Nosso servidor distribui mensagens para um indivíduo, para um grupo ou para todos, enquanto diversos clientes consomem essas mensagens.
Transportes e protocolos
Segundo a documentação oficial do ASP.NET Core SignalR, a biblioteca suporta três transportes. São eles WebSockets, Server-Sent Events e Long Polling, nessa ordem de preferência.
O SignalR escolhe automaticamente o melhor transporte disponível entre servidor e cliente. Na prática, o WebSocket costuma vencer, porque oferece a melhor performance.
Se quiser entender o protocolo por baixo dessa camada, vale ler nosso artigo sobre WebSockets no ASP.NET. Ele mostra o que o SignalR abstrai para você.
Para serializar as mensagens, o SignalR oferece um protocolo de texto baseado em JSON, que é o padrão. Também existe um protocolo binário baseado em MessagePack, que gera mensagens menores.
Hubs, grupos e usuários
A peça central de qualquer solução Flutter SignalR é o Hub. Ele funciona como um canal de alto nível em que cliente e servidor chamam métodos um do outro, e o SignalR cuida de entregar as chamadas.
A partir do Hub, você envia mensagens para todos os conectados ou para grupos de distribuição. Basta enviar uma mensagem para um grupo, e o SignalR se encarrega de entregar a cada membro.
Além dos grupos, podemos enviar uma mensagem para um indivíduo específico, direcionando-a para seu nome de usuário.
Os clientes também invocam as funções dos Hubs diretamente. Para isso, usamos bibliotecas cliente, como o pacote Flutter SignalR que veremos aqui.
Cada tipo de cliente (Desktop, Web, Mobile) precisa de uma biblioteca diferente para conversar com o Hub. A Microsoft fornece várias delas prontas, como as de JavaScript, .NET e Java, e a comunidade cobre outras plataformas, como o Dart.
Flutter SignalR: criando o servidor
Nosso primeiro passo no Flutter SignalR é criar o servidor de mensagens. Ele vai receber e enviar as informações para nossos apps.
Criando o projeto
Vamos criar o projeto com o auxílio do dotnet CLI, executando o comando abaixo no terminal.
dotnet new webapp -o SignalRChat
Esse comando cria uma aplicação Web com ASP.NET Core e Razor Pages. Para este cenário, o tipo do projeto não importa muito, então Razor Pages, MVC ou outro modelo funcionariam da mesma forma.
Abra o projeto no Visual Studio Code e estaremos prontos para começar.
Caso tenha dificuldades na criação do projeto, ou se este for seu primeiro contato com ASP.NET Core, faça antes nossa carreira Backend .NET Developer.
Adicionando o SignalR
Como comentamos, o SignalR age em conjunto com o ASP.NET Core e se divide em duas partes: cliente e servidor. Neste projeto, vamos usar as duas, mas você poderia deixar apenas o servidor aqui e o cliente em outra aplicação.
A parte do servidor já vem no framework compartilhado do ASP.NET Core 3.1, então não precisamos instalar nenhum pacote. Por outro lado, os scripts JavaScript usados pelo cliente Web não acompanham o projeto.
Você pode instalar esses scripts como faremos aqui e depois copiá-los para outra aplicação. Também pode usar uma CDN, se preferir.
O cliente Flutter SignalR, por sua vez, vem de um pacote criado por terceiros, que instalaremos mais adiante neste artigo.
Para obter os scripts do cliente Web, vamos usar o LibMan, executando os passos abaixo.
# Instala a ferramenta LibMan e baixa os scripts do cliente SignalR
dotnet tool install -g Microsoft.Web.LibraryManager.Cli
libman install @microsoft/signalr@latest -p unpkg -d wwwroot/js/signalr --files dist/browser/signalr.js --files dist/browser/signalr.min.js
Dessa forma, o LibMan baixa os arquivos e já os coloca nas pastas corretas da aplicação Web. Ao término, você deve ver um resultado parecido com este.
wwwroot/js/signalr/dist/browser/signalr.js written to disk
wwwroot/js/signalr/dist/browser/signalr.min.js written to disk
Installed library @microsoft/signalr@latest to wwwroot/js/signalr
Criando um Hub
Com os scripts no lugar, vamos criar o Hub do nosso Flutter SignalR. Para isso, crie uma pasta chamada Hubs e, dentro dela, um arquivo ChatHub.cs com o conteúdo abaixo.
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;
namespace SignalRChat.Hubs
{
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
// Repassa a mensagem para todos os clientes conectados
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
}
Um Hub nada mais é do que uma classe que herda da classe base Hub. Ela contém métodos como o SendMessage, que os clientes poderão invocar depois.
Para disseminar a mensagem, usamos a propriedade Clients. Ela permite enviar para um indivíduo, para um grupo ou para todos, com opções como Clients.Caller, Clients.Others e Clients.Group.
Neste caso, usamos Clients.All para repassar o usuário e a mensagem recebidos a todos que estão conectados ao servidor. O nome ReceiveMessage é o evento que os clientes vão escutar.
Configurando o SignalR
O próximo passo do Flutter SignalR é configurar a aplicação, e isso exige apenas três alterações no arquivo Startup.cs. Primeiro, adicionamos o namespace do Hub no topo do arquivo.
using SignalRChat.Hubs;
Em seguida, registramos o serviço do SignalR no método ConfigureServices, logo após o services.AddRazorPages.
services.AddSignalR();
Por fim, mapeamos a rota do Hub no método Configure, adicionando a linha abaixo logo após o endpoints.MapRazorPages.
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapHub<ChatHub>("/chatHub"); // Linha adicionada
});
Ao configurar um endpoint com o MapHub, informamos seu caminho, que se junta à URL padrão da aplicação, neste caso https://localhost:5001. Assim, qualquer conexão com o Hub usa a URL https://localhost:5001/chatHub.
Configurando o cliente Web
Com o SignalR configurado, vamos criar um cliente Web simples apenas para testá-lo. Não vamos nos prender ao HTML e ao CSS, porque o foco aqui é a comunicação com o Flutter.
Substitua o conteúdo do arquivo Pages/Index.cshtml pelo código abaixo.
@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}
<div class="container">
<div class="row"> </div>
<div class="row">
<div class="col-2">User</div>
<div class="col-4"><input type="text" id="userInput" /></div>
</div>
<div class="row">
<div class="col-2">Message</div>
<div class="col-4"><input type="text" id="messageInput" /></div>
</div>
<div class="row"> </div>
<div class="row">
<div class="col-6">
<input type="button" id="sendButton" value="Send Message" />
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<hr />
</div>
</div>
<div class="row">
<div class="col-6">
<ul id="messagesList"></ul>
</div>
</div>
<script src="~/js/signalr/dist/browser/signalr.js"></script>
<script src="~/js/chat.js"></script>
Nessa página, apenas três elementos importam: um campo para o usuário, um campo para a mensagem e um botão que envia a mensagem ao servidor.
No final da página, temos duas referências JavaScript. A primeira aponta para o arquivo que o LibMan baixou, e a segunda para o chat.js, que ainda vamos criar.
Caso queira separar o Frontend do servidor, basta copiar o signalr.js e o chat.js para o seu outro projeto.
Como nosso foco é o Flutter, não vou detalhar a implementação em JavaScript. Então, crie o arquivo wwwroot/js/chat.js com o código abaixo.
"use strict";
var connection = new signalR.HubConnectionBuilder().withUrl("/chatHub").build();
// Desabilita o botão até a conexão ser estabelecida
document.getElementById("sendButton").disabled = true;
connection.on("ReceiveMessage", function (user, message) {
var msg = message.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
var encodedMsg = user + " says " + msg;
var li = document.createElement("li");
li.textContent = encodedMsg;
document.getElementById("messagesList").appendChild(li);
});
connection.start().then(function () {
document.getElementById("sendButton").disabled = false;
}).catch(function (err) {
return console.error(err.toString());
});
document.getElementById("sendButton").addEventListener("click", function (event) {
var user = document.getElementById("userInput").value;
var message = document.getElementById("messageInput").value;
connection.invoke("SendMessage", user, message).catch(function (err) {
return console.error(err.toString());
});
event.preventDefault();
});
Executando a aplicação
Isso é tudo de que precisamos no servidor do Flutter SignalR, com um chat Web funcionando. Para testar, execute o comando abaixo.
dotnet watch run
Se tudo deu certo, ao abrir o navegador em https://localhost:5001, você verá a aplicação rodando e conseguirá enviar mensagens. Abra várias abas e repare que elas se comunicam entre si.
Caso ocorra algum erro de certificado, execute os comandos abaixo para confiar no certificado de desenvolvimento do ASP.NET Core.
dotnet dev-certs https --clean
dotnet dev-certs https --trust
Flutter SignalR: criando o app
Agora vamos concentrar os esforços em conectar o Flutter ao servidor. O objetivo é receber e enviar mensagens em tempo real pelo app.
Criando o app
O primeiro passo é criar um app em branco. Então, abra o Visual Studio Code, pressione CTRL (ou CMD) + SHIFT + P e selecione a opção Flutter: New Project.
Se quiser seguir o mesmo nome, nosso app se chama flusigr, e assim seu código fica totalmente compatível com o nosso. Também não se esqueça de abrir seu emulador preferido ou conectar seu telefone.
Instalando o pacote Flutter SignalR
Para se conectar ao servidor, precisamos de um cliente Flutter SignalR, e esse pacote se chama signalr_client. Ele suporta os mesmos três transportes do servidor: WebSocket, Server-Sent Events e Long Polling.
Abra o arquivo pubspec.yaml e adicione a referência abaixo.
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.2
signalr_client: ^0.1.6 # Referência do SignalR
Por enquanto, isso é tudo. Rode a aplicação para garantir que está tudo funcionando; como resultado, você verá o app padrão do Flutter, com um contador na tela.
Vale um aviso: a versão 0.1.6 foi a última publicada desse pacote e não acompanha o Dart 3. Em projetos novos, a comunidade passou a usar o pacote signalr_netcore, que segue a mesma API e acrescenta reconexão automática.
Aceitando certificados HTTPS inválidos
O primeiro problema que vamos enfrentar é que nosso servidor roda sobre HTTPS, mas sem um certificado confiável para o emulador. Isso gera um erro de certificado que precisamos corrigir antes de continuar.
Vamos sobrescrever as requisições HTTP do app para aceitar certificados inválidos. Esse código não faz parte do SignalR, e talvez você nem passe por esse problema.
Altere o método main do arquivo main.dart como no exemplo abaixo.
import 'dart:io';
// Esta classe permite acesso ao localhost com certificados HTTPS inválidos
class MyHttpOverrides extends HttpOverrides {
@override
HttpClient createHttpClient(SecurityContext context) {
HttpClient client = super.createHttpClient(context);
client.badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
return client;
}
}
void main() {
HttpOverrides.global = new MyHttpOverrides();
runApp(MyApp());
}
Atenção: use esse recurso apenas em desenvolvimento. Em produção, o servidor precisa de um certificado válido, e o app nunca deve aceitar certificados inválidos.
Com isso, o servidor fica disponível em https://10.0.2.2:5001/chatHub. O endereço 10.0.2.2 é o apelido que o emulador Android usa para o localhost da sua máquina, e /chatHub é o caminho do Hub.
Se você usa o simulador do iOS, o próprio localhost funciona, porque o simulador compartilha a rede do Mac.
Importante: deixe o servidor rodando com o comando dotnet watch run em outra instância do Visual Studio Code ou em algum terminal.
Flutter SignalR: conectando ao servidor
O primeiro passo para conectar o Flutter SignalR é importar a biblioteca que instalamos.
import 'package:signalr_client/signalr_client.dart';
Com essa importação, usamos o HubConnectionBuilder para criar a conexão e, depois, ouvir e enviar mensagens. Ele exige a URL que vimos há pouco, então vamos criar a instância logo no começo da classe _MyHomePageState.
class _MyHomePageState extends State<MyHomePage> {
final hubConnection = HubConnectionBuilder().withUrl("https://10.0.2.2:5001/chatHub").build();
// ...
Note que o método withUrl recebe o endereço do servidor e o build devolve a conexão pronta para uso.
Precisamos ainda iniciar essa conexão, e o melhor lugar para isso é a inicialização do componente, no método initState. Porém, o método que inicia a conexão é assíncrono, e o initState não, então criamos um método separado chamado startConnection.
class _MyHomePageState extends State<MyHomePage> {
final hubConnection = HubConnectionBuilder().withUrl("https://10.0.2.2:5001/chatHub").build();
@override
void initState() {
super.initState();
startConnection();
}
void startConnection() async {
await hubConnection.start(); // Inicia a conexão com o servidor
}
// ...
Flutter SignalR: ouvindo as mensagens
Nosso desafio agora é ouvir as mensagens que o servidor envia. Para isso, usamos a mesma conexão que acabamos de criar.
Toda mensagem chega como uma lista de objetos. Se o servidor enviar um JSON, o signalr_client usa o dart:convert para decodificá-lo, e você pode convertê-lo no seu próprio tipo com um método fromJson.
No nosso caso, vamos trabalhar com a lista mesmo: o primeiro item é o usuário e o segundo é a mensagem. Então, criamos o método onReceiveMessage, que recebe essa lista.
void onReceiveMessage(List<Object> result) {
print(result);
}
Agora usamos o método on da conexão para escutar o evento ReceiveMessage do servidor. Sempre que uma nova mensagem chegar, o pacote chama o método acima.
@override
void initState() {
super.initState();
hubConnection.onclose((_) {
print("Connection lost");
});
hubConnection.on("ReceiveMessage", onReceiveMessage);
startConnection();
}
Aproveitei para registrar a função onclose, que exibe "Connection lost" toda vez que o app se desconecta do servidor.
Nesse momento, rode a aplicação e envie mensagens pelo navegador. Assim, você verá cada mensagem impressa no console do Flutter.
Flutter SignalR: exibindo as mensagens
Para exibir as mensagens na tela, primeiro vamos armazená-las em uma lista de strings. Por isso, criamos essa variável também no começo da classe.
class _MyHomePageState extends State<MyHomePage> {
final hubConnection = HubConnectionBuilder().withUrl("https://10.0.2.2:5001/chatHub").build();
final List<String> messages = new List<String>();
// ...
Em seguida, refatoramos o onReceiveMessage para popular a lista. Como o estado muda a cada mensagem, chamamos o setState para que o Flutter atualize a tela.
void onReceiveMessage(List<Object> result) {
setState(() {
messages.add("${result[0]} says: ${result[1]}");
});
}
Usamos result[0] para o nome do usuário e result[1] para a mensagem. Se quiser conferir o formato exato, basta imprimir o resultado, como fizemos antes.
Se o app crescer e várias telas precisarem dessas mensagens, o setState deixa de ser suficiente. Nesse caso, vale ler nosso artigo sobre setState, BLoC e MobX no Flutter.
Agora usamos um ListView.builder para exibir as mensagens de forma dinâmica.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: ListView.builder(
itemCount: messages.length,
itemBuilder: (ctx, i) {
return Text(messages[i]);
},
),
),
);
}
Muito bem, neste momento você já vê no app, em tempo real, as mensagens enviadas pelo cliente Web com Flutter SignalR!
Flutter SignalR: enviando mensagens
Para enviar uma mensagem, precisamos de um campo de texto e, para obter seu valor com facilidade, de um TextEditingController.
class _MyHomePageState extends State<MyHomePage> {
final controller = new TextEditingController(); // Linha adicionada
final hubConnection = HubConnectionBuilder().withUrl("https://10.0.2.2:5001/chatHub").build();
final List<String> messages = new List<String>();
// ...
Para enviar a mensagem ao servidor pelo Flutter SignalR, usamos novamente a conexão, agora com o método invoke. Ele chama um método definido no ChatHub.cs, lá no servidor.
void sendMessage() async {
await hubConnection.invoke("SendMessage",
args: <Object>["Flutter", controller.text]).catchError((err) {
print(err);
});
}
Aqui chamamos o SendMessage passando um array de objetos como argumentos. O primeiro é uma string fixa, Flutter, e o segundo é o texto do campo.
Repare que a ordem e a quantidade de argumentos precisam bater com a assinatura do método no Hub. Caso contrário, o servidor rejeita a chamada e o catchError imprime o erro.
Falta apenas um campo de texto e um botão para chamar o sendMessage.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: ListView.builder(
itemCount: messages.length,
itemBuilder: (ctx, i) {
return Text(messages[i]);
},
),
),
bottomSheet: Container(
color: Theme.of(context).accentColor,
padding: EdgeInsets.all(20),
height: 90,
child: TextFormField(
controller: controller,
decoration: InputDecoration(labelText: "Message..."),
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.send),
onPressed: () {
sendMessage();
},
),
);
}
Código final do main.dart
O arquivo main.dart completo fica no formato abaixo.
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:signalr_client/signalr_client.dart';
// Esta classe permite acesso ao localhost com certificados HTTPS inválidos
class MyHttpOverrides extends HttpOverrides {
@override
HttpClient createHttpClient(SecurityContext context) {
HttpClient client = super.createHttpClient(context);
client.badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
return client;
}
}
void main() {
HttpOverrides.global = new MyHttpOverrides();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter SignalR',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter SignalR'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final controller = new TextEditingController();
final hubConnection = HubConnectionBuilder().withUrl("https://10.0.2.2:5001/chatHub").build();
final List<String> messages = new List<String>();
@override
void initState() {
super.initState();
hubConnection.onclose((_) {
print("Connection lost");
});
hubConnection.on("ReceiveMessage", onReceiveMessage);
startConnection();
}
void onReceiveMessage(List<Object> result) {
setState(() {
messages.add("${result[0]} says: ${result[1]}");
});
}
void startConnection() async {
await hubConnection.start();
}
void sendMessage() async {
await hubConnection.invoke("SendMessage", args: <Object>["Flutter", controller.text]).catchError((err) {
print(err);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: ListView.builder(
itemCount: messages.length,
itemBuilder: (ctx, i) {
return Text(messages[i]);
},
),
),
bottomSheet: Container(
color: Theme.of(context).accentColor,
padding: EdgeInsets.all(20),
height: 90,
child: TextFormField(
controller: controller,
decoration: InputDecoration(labelText: "Message..."),
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.send),
onPressed: () {
sendMessage();
},
),
);
}
}
Agora é só executar a aplicação, e você terá um chat funcionando entre a aplicação Web e o app Flutter.
Boas práticas com Flutter SignalR
O exemplo deste artigo é propositalmente simples. Antes de levar o Flutter SignalR para um app real, porém, vale considerar alguns cuidados.
Primeiro, encerre a conexão quando a tela sair de cena. Para isso, sobrescreva o método dispose do State e chame hubConnection.stop(), assim como o controller.dispose().
Em seguida, trate a perda de conexão. O onclose que registramos é o ponto ideal para avisar o usuário ou tentar reconectar depois de alguns segundos.
Além disso, nunca deixe o badCertificateCallback liberado em produção. Esse atalho existe apenas para facilitar o desenvolvimento local.
Por fim, prefira mensagens tipadas em JSON quando o volume de dados crescer. Dessa forma, o app converte cada mensagem em um objeto Dart com fromJson, e o código fica mais seguro.
Se quiser comparar com uma versão mais enxuta do servidor, confira nosso artigo sobre SignalR com Minimal APIs.
Conclusão
Flutter SignalR trabalham de forma conjunta e fácil. Neste artigo, vimos o básico de como criar um Hub e enviar e receber mensagens por ele em aplicações Web e Mobile.
O código-fonte do servidor e do app desenvolvidos neste artigo está disponível no GitHub.
FAQ
Preciso instalar algum pacote NuGet para o servidor SignalR no ASP.NET Core 3.1?
Não. O servidor SignalR já faz parte do framework compartilhado do ASP.NET Core 3.1, basta chamar AddSignalR e MapHub.
Por que o app usa 10.0.2.2 em vez de localhost?
Porque, dentro do emulador Android, localhost aponta para o próprio emulador. O endereço 10.0.2.2 aponta para a máquina que roda o emulador.
Posso aceitar certificados inválidos em produção?
Não. Esse ajuste serve apenas para desenvolvimento local, e em produção o servidor precisa de um certificado válido.
O pacote signalr_client funciona com Dart 3?
Não. A versão 0.1.6 é anterior ao Dart 3, e projetos novos costumam usar o signalr_netcore, que mantém uma API semelhante.
Qual transporte o SignalR usa por padrão?
O SignalR negocia automaticamente e prefere WebSockets. Se não estiver disponível, recorre a Server-Sent Events e, depois, a Long Polling.