Skip to content

Code Generation

The rpc_dart_generator package eliminates boilerplate. You define an annotated abstract interface; build_runner generates the caller and responder implementations.


Setup

# pubspec.yaml
dev_dependencies:
  rpc_dart_generator: ^0.1.6
  build_runner: any

The source file must declare a part directive pointing to the generated file:

part 'my_contract.g.dart';

Run generation:

fvm dart run build_runner build
# or watch mode
fvm dart run build_runner watch

Annotations

@RpcService

Marks an abstract class as an RPC service contract:

@RpcService(
  name: 'Calculator',
  transferMode: RpcDataTransferMode.zeroCopy, // default
)
abstract class ICalculatorContract { ... }
Parameter Type Description
name String Service identifier used for routing
transferMode RpcDataTransferMode Default transfer mode for all methods

@RpcMethod

Annotates each method. There are shorthand constructors for every pattern:

@RpcMethod.unary(name: 'add')
Future<AddResponse> add(AddRequest request, {RpcContext? context});

@RpcMethod.serverStream(name: 'countUp')
Stream<CountResponse> countUp(CountRequest request, {RpcContext? context});

@RpcMethod.clientStream(name: 'upload')
Future<UploadResponse> upload(Stream<Chunk> chunks, {RpcContext? context});

@RpcMethod.bidirectionalStream(name: 'chat')
Stream<Message> chat(Stream<Message> incoming, {RpcContext? context});

Or use the base constructor with an explicit kind:

@RpcMethod(
  name: 'countUp',
  kind: RpcMethodKind.serverStream,
  transferMode: RpcDataTransferMode.codec, // override service default
  description: 'Streams integers from 1 to n',
)
Stream<CountResponse> countUp(CountRequest request, {RpcContext? context});
Parameter Type Description
name String Method identifier used for routing
kind RpcMethodKind unary, serverStream, clientStream, bidirectionalStream
transferMode RpcDataTransferMode? Overrides service-level default
description String? Passed to the generated addXxxMethod call

Transfer Modes

Mode Requires codec Use case
RpcDataTransferMode.zeroCopy No InMemory transport, same process
RpcDataTransferMode.codec Yes (IRpcSerializable + fromJson) Any network transport

In zero-copy mode the generator emits no codec references — objects are passed by reference through the transport.

In codec mode the generator emits a XxxContractCodecs class with const codec instances and wires them into every addXxxMethod / callXxx call automatically.


What Gets Generated

For ICalculatorContract the generator produces three classes in calculator_contract.g.dart:

XxxContractNames

Compile-time constants for service and method names. Use them instead of raw strings to avoid typos:

class CalculatorContractNames {
  static const service = 'Calculator';
  static const sum     = 'sum';
  static const numbers = 'numbers';

  // For running multiple instances of the same service:
  static String instance(String suffix) => '${service}_$suffix';
}

XxxContractCodecs (codec mode only)

const codec instances, one per request/response type:

class CalculatorSerializeContractCodecs {
  static const codecSumRequest  = RpcCodec<SumRequest>.withDecoder(SumRequest.fromJson);
  static const codecSumResponse = RpcCodec<SumResponse>.withDecoder(SumResponse.fromJson);
}

XxxContractResponder

An abstract class extending RpcResponderContract. Implements setup() — you only implement the methods:

// Generated (abstract)
abstract class CalculatorContractResponder extends RpcResponderContract
    implements ICalculatorContract {
  CalculatorContractResponder({
    String? serviceNameOverride,
    RpcDataTransferMode dataTransferMode = RpcDataTransferMode.zeroCopy,
  });

  @override
  void setup() {
    addUnaryMethod<SumRequest, SumResponse>(
      methodName: CalculatorContractNames.sum,
      handler: sum,
    );
    // ...
  }
}

// Your code
class CalculatorResponder extends CalculatorContractResponder {
  @override
  Future<SumResponse> sum(SumRequest request, {RpcContext? context}) async {
    final total = request.values.fold<double>(0, (a, b) => a + b);
    return SumResponse(result: total);
  }
}

XxxContractCaller

A concrete class extending RpcCallerContract. Ready to use:

// Generated (concrete, use directly)
class CalculatorContractCaller extends RpcCallerContract
    implements ICalculatorContract {
  CalculatorContractCaller(
    RpcCallerEndpoint endpoint, {
    String? serviceNameOverride,
    RpcDataTransferMode dataTransferMode = RpcDataTransferMode.zeroCopy,
  });
}

// Usage
final caller = CalculatorContractCaller(
  RpcCallerEndpoint(transport: callerTransport),
);
final result = await caller.sum(SumRequest(values: [1, 2, 3]));

Multiple Service Instances

If you need to run two instances of the same service (e.g. a primary and a beta), use serviceNameOverride and XxxContractNames.instance(suffix):

server.registerServiceContract(CalculatorResponder());
server.registerServiceContract(
  CalculatorResponder(
    serviceNameOverride: CalculatorContractNames.instance('beta'),
  ),
);

final callerBeta = CalculatorContractCaller(
  endpoint,
  serviceNameOverride: CalculatorContractNames.instance('beta'),
);

Models for Codec Mode

When using RpcDataTransferMode.codec, models must implement IRpcSerializable and provide a fromJson factory. The generator uses fromJson to construct codec instances at compile time (const constructors required).

class SumRequest implements IRpcSerializable {
  SumRequest({required this.values});
  final List<double> values;

  factory SumRequest.fromJson(Map<String, dynamic> json) {
    final raw = json['values'] as List<dynamic>? ?? const [];
    return SumRequest(values: raw.map((e) => (e as num).toDouble()).toList());
  }

  @override
  Map<String, dynamic> toJson() => {'values': values};
}

You can also use json_serializable — the generator only requires that fromJson and toJson exist.


Full Example

// contract.dart
import 'package:rpc_dart/rpc_dart.dart';
part 'contract.g.dart';

@RpcService(name: 'Calculator', transferMode: RpcDataTransferMode.zeroCopy)
abstract class ICalculatorContract {
  @RpcMethod.unary(name: 'sum')
  Future<SumResponse> sum(SumRequest request, {RpcContext? context});

  @RpcMethod.serverStream(name: 'numbers')
  Stream<SumResponse> numbers(SumRequest request, {RpcContext? context});
}

class SumRequest  { SumRequest({required this.values}); final List<double> values; }
class SumResponse { SumResponse({required this.result}); final double result; }
// contract.dart
import 'package:rpc_dart/rpc_dart.dart';
part 'contract.g.dart';

@RpcService(name: 'Calculator', transferMode: RpcDataTransferMode.codec)
abstract class ICalculatorContract {
  @RpcMethod.unary(name: 'sum')
  Future<SumResponse> sum(SumRequest request, {RpcContext? context});
}

class SumRequest implements IRpcSerializable {
  SumRequest({required this.values});
  final List<double> values;
  factory SumRequest.fromJson(Map<String, dynamic> json) =>
      SumRequest(values: (json['values'] as List).map((e) => (e as num).toDouble()).toList());
  @override
  Map<String, dynamic> toJson() => {'values': values};
}

class SumResponse implements IRpcSerializable {
  SumResponse({required this.result});
  final double result;
  factory SumResponse.fromJson(Map<String, dynamic> json) =>
      SumResponse(result: (json['result'] as num).toDouble());
  @override
  Map<String, dynamic> toJson() => {'result': result};
}