Skip to main content

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

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! 🎉

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 and http2. 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.