Contracts
Define the interface between services with method names and signatures.
RPC Dart is built around several core concepts that work together to provide a powerful and flexible Remote Procedure Call framework. Understanding these concepts is essential to effectively using the library.
Contracts
Define the interface between services with method names and signatures.
Endpoints
Handle communication between client and server sides.
Transports
Manage the underlying communication mechanism (InMemory, HTTP, WebSocket, etc.).
Context
Carry metadata and control information across RPC calls.
Contracts in RPC Dart define the service interface - what methods are available and how they should be called. They serve as the single source of truth for both client and server implementations.
abstract interface class ICalculatorContract { static const name = 'Calculator'; static const methodAdd = 'add'; static const methodSubtract = 'subtract'; static const methodDivide = 'divide';}
Endpoints are the communication points that handle RPC calls. There are two types:
The client-side endpoint that initiates RPC calls:
final caller = RpcCallerEndpoint(transport: transport);final calculator = CalculatorCaller(caller);
// Make RPC callfinal result = await calculator.add(AddRequest(5, 3));
The server-side endpoint that handles incoming RPC calls:
final responder = RpcResponderEndpoint(transport: transport);responder.registerServiceContract(CalculatorResponder());responder.start();
Transports define how messages are sent between endpoints. RPC Dart is transport-agnostic, meaning you can switch transports without changing your business logic.
Transport | Use Case | Performance | Complexity |
---|---|---|---|
InMemory | Testing, single-process | Highest (zero-copy) | Lowest |
HTTP | Web services, microservices | Medium | Medium |
WebSocket | Real-time, bidirectional | Medium | Medium |
Isolate | Multi-core, isolation | High | Higher |
// Development/Testingfinal (clientTransport, serverTransport) = RpcInMemoryTransport.pair();
// Production HTTPfinal httpTransport = RpcHttpTransport( url: 'https://api.example.com', timeout: Duration(seconds: 30),);
// Real-time WebSocketfinal wsTransport = RpcWebSocketTransport( url: 'wss://api.example.com/rpc',);
The same service code works with any transport!
RPC Context carries additional information alongside the actual RPC call data:
// Server side - accessing contextFuture<AddResponse> _add(AddRequest request, {RpcContext? context}) async { final correlationId = context?.correlationId; final userAgent = context?.metadata['user-agent'];
if (context?.isCancelled ?? false) { throw RpcException('Request was cancelled'); }
return AddResponse(request.a + request.b);}
// Client side - passing metadatafinal context = RpcContext() ..metadata['user-agent'] = 'MyApp/1.0' ..deadline = DateTime.now().add(Duration(seconds: 5));
final result = await calculator.add( AddRequest(5, 3), context: context,);
RPC Dart supports all standard RPC communication patterns:
Simple request-response pattern:
Future<AddResponse> add(AddRequest request) { return callUnary<AddRequest, AddResponse>( methodName: 'add', request: request, );}
Server sends multiple responses to a single request:
Stream<NumberResponse> getNumbers(NumberRequest request) { return callServerStream<NumberRequest, NumberResponse>( methodName: 'getNumbers', request: request, );}
Client sends multiple requests, server responds once:
Future<SumResponse> sumNumbers(Stream<NumberRequest> requests) { return callClientStream<NumberRequest, SumResponse>( methodName: 'sumNumbers', requests: requests, );}
Both client and server can send multiple messages:
Stream<EchoResponse> echo(Stream<EchoRequest> requests) { return callBidirectionalStream<EchoRequest, EchoResponse>( methodName: 'echo', requests: requests, );}
One of RPC Dart’s unique features is zero-copy object transfer with InMemory transport:
class LargeData { final List<int> data = List.generate(1000000, (i) => i);}
// With InMemory transport, this object is passed by reference// No serialization/deserialization overhead!final result = await service.processLargeData(LargeData());
RPC Dart provides structured error handling:
// Server throws structured errorsif (request.b == 0) { throw RpcException( code: 'DIVISION_BY_ZERO', message: 'Cannot divide by zero', details: {'operand': 'b', 'value': 0}, );}
// Client catches and handlestry { final result = await calculator.divide(DivideRequest(10, 0));} on RpcException catch (e) { print('RPC Error: ${e.code} - ${e.message}'); print('Details: ${e.details}');}
RPC Dart supports middleware for cross-cutting concerns:
// Logging middlewareclass LoggingMiddleware extends RpcMiddleware { @override Future<T> call<T>(RpcCall call, RpcNext next) async { print('Calling ${call.method}'); final result = await next(call); print('Completed ${call.method}'); return result; }}
// Register middlewareresponder.addMiddleware(LoggingMiddleware());
Now that you understand the core concepts, explore: