diff --git a/docs/dart/eliza-chat-screenshot.png b/docs/dart/eliza-chat-screenshot.png new file mode 100644 index 00000000..3de14377 Binary files /dev/null and b/docs/dart/eliza-chat-screenshot.png differ diff --git a/docs/dart/getting-started.md b/docs/dart/getting-started.md index 71778f21..86201965 100644 --- a/docs/dart/getting-started.md +++ b/docs/dart/getting-started.md @@ -2,3 +2,423 @@ title: Getting started sidebar_position: 1 --- + +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)][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](https://en.wikipedia.org/wiki/ELIZA), +a very simple natural language processor built in the 1960s to represent a +psychotherapist. **The ELIZA service is +[implemented using Connect-Go][go-demo], is +[already up and running](https://connectrpc.com/demo) in production, and +supports both the [gRPC-Web][grpc-web] and [Connect](../protocol.md) +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][buf-cli] installed, and include it in the `$PATH`. +- [Flutter][flutter] installed and setup for atleast one platform. + +## Create a new Flutter app + +Create a Flutter app called `eliza` by running: + +```bash +flutter create eliza +cd eliza +``` + +Next add a dependency on the `connectrpc` package by running the following: + +```bash +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. + +```bash +$ mkdir -p proto && touch proto/eliza.proto +``` + +Open up the above file and add the following service definition: + +```proto +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][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`][buf.yaml] by running `buf config init` at the root of your repository. Then, edit `buf.yaml` +to use our `proto` directory: + +```yaml title=buf.yaml +version: v2 +// highlight-next-line +modules: +// highlight-next-line + - path: proto +lint: + use: + - DEFAULT +breaking: + use: + - FILE +``` + +Next, tell Buf how to generate code by putting this into +[`buf.gen.yaml`][buf.gen.yaml]: + +```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: + +```bash +$ 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][dart-protobuf] 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 + + +```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}); + + @override + 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; + + @override + State createState() => _ChatPageState(); +} + +class _ChatPageState extends State { + 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); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + 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! 🎉 + +import ElizaChatScreenshot from './eliza-chat-screenshot.png'; + +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][http2] client) and the protocol itself (in this case the [Connect protocol](../protocol.md)). + +If we wanted to use JSON instead of Protobuf we'd only need to make a simple line change: + +```dart +final transport = Transport( + baseUrl: "https://demo.connectrpc.com", + //highlight-next-line + 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-clients.md) + +### Using gRPC + +If you'd like to use gRPC as the transport protocol in the above example, +simply change the following 2 lines: + +```dart +import 'package:eliza/gen/eliza.pb.dart'; +import 'package:flutter/material.dart'; +import 'package:connectrpc/http2.dart'; +import 'package:connectrpc/protobuf.dart'; +//highlight-next-line +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(), + //highlight-next-line + 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](testing.md) 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](../protocol.md), +[gRPC][grpc], and [gRPC-Web][grpc-web] protocols. +Instructions for switching between them +can be found [here](./using-clients.md). + +We recommend using Connect-Dart over [gRPC-Dart][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](./using-clients.md#using-generated-clients) 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](./testing.md) 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](./interceptors.md). +- **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][envoy-grpc-bridge] +for converting requests made using the Connect and gRPC-Web protocols to gRPC, +enabling you to use Connect-Dart without the [http2][http2] dependency. + +[buf]: https://buf.build/docs/ +[buf-cli]: https://buf.build/docs/installation +[buf.gen.yaml]: https://buf.build/docs/configuration/v2/buf-gen-yaml +[buf.yaml]: https://buf.build/docs/configuration/v2/buf-yaml +[dart-protobuf]: https://pub.dev/packages/protobuf +[envoy-grpc-bridge]: https://www.envoyproxy.io/docs/envoy/latest/ +[flutter]: https://docs.flutter.dev/get-started/install +[go-demo]: https://github.com/connectrpc/examples-go +[grpc]: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md +[grpc-dart]: https://pub.dev/packages/grpc +[grpc-web]: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md +[http2]: https://pub.dev/packages/http2 +[protobuf]: https://developers.google.com/protocol-buffers diff --git a/docusaurus.config.js b/docusaurus.config.js index 7d3b739f..9dd7b918 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -173,7 +173,7 @@ const config = { prism: { theme: lightCodeTheme, darkTheme: darkCodeTheme, - additionalLanguages: ["kotlin", "protobuf", "swift"], + additionalLanguages: ["kotlin", "protobuf", "swift", "dart"], }, }), }; diff --git a/src/components/home/Guides.tsx b/src/components/home/Guides.tsx index 013be527..55607995 100644 --- a/src/components/home/Guides.tsx +++ b/src/components/home/Guides.tsx @@ -100,6 +100,13 @@ export default function Guides() { title="Android guide" description="Kotlin clients available" /> + diff --git a/static/img/logos/dart.svg b/static/img/logos/dart.svg new file mode 100644 index 00000000..a5ea786e --- /dev/null +++ b/static/img/logos/dart.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + +