Getting started
Connect-Dart is a small library (<100KB!) that provides support for using generated, type-safe, and idiomatic Dart APIs to communicate with your app's servers using Protocol Buffers (Protobuf). It works with the Connect, gRPC, and gRPC-Web protocols.
Imagine a world where you can jump right into building products and focus on the user experience without needing to handwrite REST/JSON endpoints or models — instead using generated APIs that utilize the latest Dart features and are guaranteed to match the server's modeling.
In this guide, we'll use Connect-Dart to create a chat app for ELIZA, a very simple natural language processor built in the 1960s to represent a psychotherapist. The ELIZA service is implemented using Connect-Go, is already up and running in production, and supports both the gRPC-Web and Connect protocols - both of which can be used with Connect-Dart for this tutorial. The APIs we'll be using are defined in a Protobuf schema that we'll use to generate a Connect-Dart client.
This tutorial should take ~10 minutes from start to finish.
Prerequisites
- The Buf CLI installed, and include it in the
$PATH
. - Flutter installed and setup for at least one platform other than web. For web related setup please see HTTP stack, after the getting started guide.
Note: Some platforms might need additional configuration to make HTTP requests.
Create a new Flutter app
Create a Flutter app called eliza
by running:
flutter create eliza
cd eliza
Next add a dependency on the connectrpc
package by running the following:
flutter pub add connectrpc
Define a service
First, we need to add a Protobuf file that includes our service definition. For this tutorial, we are going to construct a unary endpoint for a service that is a stripped-down implementation of ELIZA, the famous natural language processing program.
$ mkdir -p proto && touch proto/eliza.proto
Open up the above file and add the following service definition:
syntax = "proto3";
package connectrpc.eliza.v1;
message SayRequest {
string sentence = 1;
}
message SayResponse {
string sentence = 1;
}
service ElizaService {
rpc Say(SayRequest) returns (SayResponse) {}
}
Open the newly created eliza.proto
file in the editor.
This file declares a connectrpc.eliza.v1
Protobuf package,
a service called ElizaService
, and a single method
called Say
. Under the hood, these components will be used to form the path
of the API's HTTP URL.
The file also contains two models, SayRequest
and SayResponse
, which
are the input and output for the Say
RPC method.
Generate code
We're going to generate our code using Buf, a modern replacement for Google's protobuf compiler. We installed Buf earlier, but we also need a few configuration files to get going.
First, scaffold a basic buf.yaml
by running buf config init
at the root of your repository. Then, edit buf.yaml
to use our proto
directory:
version: v2
modules:
- path: proto
lint:
use:
- DEFAULT
breaking:
use:
- FILE
Next, tell Buf how to generate code by putting this into
buf.gen.yaml
:
version: v2
plugins:
- remote: buf.build/connectrpc/dart
out: lib/gen
- remote: buf.build/protocolbuffers/dart
out: lib/gen
include_wkt: true
include_imports: true
With those configuration files in place, we can now generate code:
$ buf generate
In your lib/gen
directory, you should now see some generated Dart files:
lib/gen
├── eliza.connect.client.dart
├── eliza.connect.spec.dart
├── eliza.pb.dart
├── eliza.pbenum.dart
├── eliza.pbjson.dart
└── eliza.pbserver.dart
The .connect.client.dart
file contains a production client that conforms to ElizaService
.
The .pb*.dart
files were generated by Google's
Dart plugin and contains the corresponding Dart
classes for the SayRequest
and SayResponse
messages we defined in our Protobuf file.
At this point, your app should build successfully.
Integrate into the app
Replace main.dart
with:
Click to expand main.dart
main.dart
import 'package:eliza/gen/eliza.pb.dart';
import 'package:flutter/material.dart';
import 'package:connectrpc/http2.dart';
import 'package:connectrpc/connect.dart';
import 'package:connectrpc/protobuf.dart';
import 'package:connectrpc/protocol/connect.dart' as protocol;
import './gen/eliza.connect.client.dart';
final transport = protocol.Transport(
baseUrl: "https://demo.connectrpc.com",
codec: const ProtoCodec(), // Or JsonCodec()
httpClient: createHttpClient(),
);
void main() {
runApp(const ElizaApp());
}
class ElizaApp extends StatelessWidget {
const ElizaApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Eliza',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: ChatPage(transport: transport),
);
}
}
class ChatPage extends StatefulWidget {
const ChatPage({super.key, required this.transport});
final Transport transport;
State<ChatPage> createState() => _ChatPageState();
}
class _ChatPageState extends State<ChatPage> {
final messages = List<({String sentence, bool byUser})>.empty(growable: true);
final currentSentence = TextEditingController();
void addMessage(String sentence, bool byUser) {
setState(() => messages.add((sentence: sentence, byUser: byUser)));
}
void send(String sentence) async {
addMessage(sentence, true);
final response = await ElizaServiceClient(widget.transport).say(
SayRequest(sentence: sentence),
);
addMessage(response.sentence, false);
}
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Expanded(
child: ListView(
children: [
for (final message in messages)
Column(
key: ObjectKey(message),
children: [
if (message.byUser) ...[
const Row(
children: [
Spacer(),
Text(
"You",
style: TextStyle(
color: Colors.grey,
fontWeight: FontWeight.w600,
),
)
],
),
Row(
children: [
const Spacer(),
Text(
message.sentence,
textAlign: TextAlign.left,
),
],
)
] else ...[
const Row(
children: [
Text(
"Eliza",
style: TextStyle(
color: Colors.blue,
fontWeight: FontWeight.w600,
),
),
Spacer(),
],
),
Row(
children: [
Text(
message.sentence,
textAlign: TextAlign.left,
),
const Spacer(),
],
)
]
],
)
],
),
),
Row(
children: [
Flexible(
child: TextField(
controller: currentSentence,
decoration: const InputDecoration(
hintText: 'Write your message...',
border: UnderlineInputBorder(),
),
),
),
TextButton(
onPressed: () {
final sentence = currentSentence.text;
if (sentence.isEmpty) {
return;
}
send(sentence);
currentSentence.clear();
},
child: const Text(
'Send',
style: TextStyle(color: Colors.blue),
),
)
],
)
],
),
),
),
);
}
}
Build and run the app, and you should be able to chat with Eliza! 🎉
Breaking it down
Let's dive into what some of the code above is doing, particularly regarding how it is interacting with the Connect library.
Creating a Transport
At the very top, it creates an instance of Transport
. This is responsible for configuring various options like serialization (i.e., JSON or Protobuf), http client (in this case an http2 client) and the protocol itself (in this case the Connect protocol).
If we wanted to use JSON instead of Protobuf we'd only need to make a simple line change:
final transport = Transport(
baseUrl: "https://demo.connectrpc.com",
codec: const JsonCodec(),
httpClient: createHttpClient(),
);
The Http client can be changed to use dart:io
(it only supports H/1), a fetch
based implementation for flutter web, or by creating a new implementation for the HttpClient
type. For more customization options,
see the documentation on using clients
Using gRPC
If you'd like to use gRPC as the transport protocol in the above example, simply change the following 2 lines:
import 'package:eliza/gen/eliza.pb.dart';
import 'package:flutter/material.dart';
import 'package:connectrpc/http2.dart';
import 'package:connectrpc/protobuf.dart';
import 'package:connectrpc/protocol/grpc.dart' as protocol;
import './gen/eliza.connect.client.dart';
final transport = protocol.Transport(
baseUrl: "https://demo.connectrpc.com",
codec: const JsonCodec(), // Or JsonCodec()
httpClient: createHttpClient(),
statusParser: const StatusParser()
);
Using the generated code
Take a look at the ChatPage
widget above. It is initialized with a Transport
interface. Accepting an interface allows for injecting mocks into widgets for testing. We won't get into mocks and testing here, but
you can check out the testing docs for details and examples.
Whenever the send(...)
method is invoked, we create an ElizaServiceClient
from a transport and pass the request to the say(...)
method on the generated client and await a response from the server.
All of this is done using type-safe generated APIs from the Protobuf
file we wrote earlier.
Creating clients is free, as they are extension types on the Transport
. This takes away the need to create and pass different clients to different pages/widgets. Instead, all of widgets only need the Transport
type and can maintain a local value of the extension type without any cost.
Using gRPC or gRPC-Web
Connect-Dart supports the Connect, gRPC, and gRPC-Web protocols. Instructions for switching between them can be found here.
We recommend using Connect-Dart over gRPC-Dart even if you're using the gRPC protocol for a few reasons:
- Idiomatic, typed APIs. No more hand-writing REST/JSON endpoints and models. Connect-Dart generates idiomatic APIs that utilize the latest Dart features such as extensions and eliminates the need to worry about serialization.
- First-class testing support. Connect-Dart comes with a mockable
Transport
that enables easy testability with minimal handwritten boilerplate. - Easy-to-use tooling. Connect-Dart integrates with the Buf CLI, enabling remote code generation without having to install and configure local dependencies.
- Flexibility. Connect-Dart supports swapping http clients with built in
support for
dart:io
,fetch
andhttp2
. It also supports unified interceptors. - Binary size. The Connect-Dart library is very small (<100KB) and tree shakable to only include the protocol code in use.
If your backend services are already using gRPC today, Envoy provides support for converting requests made using the Connect and gRPC-Web protocols to gRPC, enabling you to use Connect-Dart without the http2 dependency.