{"id":28239129,"url":"https://github.com/nogipx/rpc_dart","last_synced_at":"2026-02-28T02:02:28.733Z","repository":{"id":297786608,"uuid":"994700112","full_name":"nogipx/rpc_dart","owner":"nogipx","description":"gRPC-inspired library built on pure Dart, Backend-for-Domain (BFD)","archived":false,"fork":false,"pushed_at":"2025-06-07T12:50:17.000Z","size":4487,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-06-07T13:39:02.060Z","etag":null,"topics":["backend-for-domain","dart","grpc","rpc","rpc-framework"],"latest_commit_sha":null,"homepage":"https://pub.dev/packages/rpc_dart","language":"Dart","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"lgpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nogipx.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-06-02T10:51:50.000Z","updated_at":"2025-06-07T12:50:20.000Z","dependencies_parsed_at":"2025-06-07T13:39:05.155Z","dependency_job_id":"1b7d4c0d-cae1-4b31-a7f1-4110e34bb32b","html_url":"https://github.com/nogipx/rpc_dart","commit_stats":null,"previous_names":["nogipx/rpc_dart"],"tags_count":13,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nogipx%2Frpc_dart","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nogipx%2Frpc_dart/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nogipx%2Frpc_dart/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nogipx%2Frpc_dart/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nogipx","download_url":"https://codeload.github.com/nogipx/rpc_dart/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nogipx%2Frpc_dart/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":258777349,"owners_count":22756080,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["backend-for-domain","dart","grpc","rpc","rpc-framework"],"created_at":"2025-05-19T02:11:43.637Z","updated_at":"2026-02-28T02:02:23.289Z","avatar_url":"https://github.com/nogipx.png","language":"Dart","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Pub Version](https://img.shields.io/pub/v/rpc_dart.svg)](https://pub.dev/packages/rpc_dart)\n[![CI](https://github.com/nogipx/rpc_dart/workflows/CI/badge.svg)](https://github.com/nogipx/rpc_dart/actions/workflows/ci.yml)\n[![Coverage Status](https://coveralls.io/repos/github/nogipx/rpc_dart/badge.svg?branch=main)](https://coveralls.io/github/nogipx/rpc_dart?branch=main)\n\n# RPC Dart\n\n\u003e **RPC библиотека на чистом Dart для типобезопасного взаимодействия между компонентами**\n\n## Основные концепции\n\n**RPC Dart** построен на следующих ключевых концепциях:\n\n- **Контракты (Contracts)** — определяют API сервиса через интерфейсы и методы\n- **Responder** — серверная часть, обрабатывает входящие запросы\n- **Caller** — клиентская часть, отправляет запросы  \n- **Endpoint** — точка подключения, управляет транспортом\n- **Transport** — уровень передачи данных (InMemory, Isolate, HTTP)\n- **Codec** — сериализация/десериализация сообщений\n\n## Ключевые возможности\n\n- **Полная поддержка RPC паттернов** — unary calls, server streams, client streams, bidirectional streams\n- **Встроенный InMemory транспорт** — для разработки и тестирования\n- **Типобезопасность** — все запросы/ответы строго типизированы\n- **Автоматическая трассировка** — trace ID генерируется автоматически, передается через RpcContext\n- **Без внешних зависимостей** — только чистый Dart\n- **Встроенные примитивы** — готовые обертки для String, Int, Double, Bool, List\n- **Простое тестирование** — с InMemory транспортом и моками\n\n## CORD\n\nRPC Dart предлагает **CORD (Contract-Oriented Remote Domains)** — архитектурный подход для структурирования бизнес-логики через изолированные домены с типобезопасными RPC контрактами.\n\n**📚 [Подробнее](docs/cord.md)**\n\n## Quick Start\n\n### [Готовые примеры использования](example/)\n\n### 1. Определите контракт и модели\n\n```dart\n// Request/Response объекты реализуют IRpcSerializable\nclass CalculationRequest implements IRpcSerializable {\n  final double a, b;\n  final String operation; // 'add', 'subtract', 'multiply', 'divide'\n  \n  CalculationRequest({required this.a, required this.b, required this.operation});\n  \n  @override\n  Map\u003cString, dynamic\u003e toJson() =\u003e {'a': a, 'b': b, 'operation': operation};\n  \n  static CalculationRequest fromJson(Map\u003cString, dynamic\u003e json) =\u003e CalculationRequest(\n    a: json['a'] is int ? (json['a'] as int).toDouble() : json['a'],\n    b: json['b'] is int ? (json['b'] as int).toDouble() : json['b'],\n    operation: json['operation'],\n  );\n  \n  static RpcCodec\u003cCalculationRequest\u003e get codec =\u003e RpcCodec(CalculationRequest.fromJson);\n}\n\nclass CalculationResponse implements IRpcSerializable {\n  final double? result;\n  final bool success;\n  final String? errorMessage;\n  \n  CalculationResponse({this.result, this.success = true, this.errorMessage});\n  \n  @override\n  Map\u003cString, dynamic\u003e toJson() =\u003e {\n    'result': result, 'success': success, 'errorMessage': errorMessage,\n  };\n  \n  static CalculationResponse fromJson(Map\u003cString, dynamic\u003e json) =\u003e CalculationResponse(\n    result: json['result'], success: json['success'] ?? true, errorMessage: json['errorMessage'],\n  );\n      \n  static RpcCodec\u003cCalculationResponse\u003e get codec =\u003e RpcCodec(CalculationResponse.fromJson);\n}\n```\n\n### 2. Создайте сервер (Responder)\n\n```dart\nclass CalculatorResponder extends RpcResponderContract {\n  CalculatorResponder() : super('CalculatorService');\n  \n  @override\n  void setup() {\n    addUnaryMethod\u003cCalculationRequest, CalculationResponse\u003e(\n      methodName: 'calculate',\n      handler: calculate,\n      requestCodec: CalculationRequest.codec,\n      responseCodec: CalculationResponse.codec,\n    );\n    \n    addBidirectionalMethod\u003cCalculationRequest, CalculationResponse\u003e(\n      methodName: 'streamCalculate',\n      handler: streamCalculate,\n      requestCodec: CalculationRequest.codec,\n      responseCodec: CalculationResponse.codec,\n    );\n  }\n  \n  Future\u003cCalculationResponse\u003e calculate(CalculationRequest request, {RpcContext? context}) async {\n    try {\n      double result;\n      switch (request.operation) {\n        case 'add': result = request.a + request.b; break;\n        case 'subtract': result = request.a - request.b; break;\n        case 'multiply': result = request.a * request.b; break;\n        case 'divide':\n          if (request.b == 0) throw Exception('Division by zero');\n          result = request.a / request.b; break;\n        default: throw Exception('Unknown operation: ${request.operation}');\n      }\n      return CalculationResponse(result: result);\n    } catch (e) {\n      return CalculationResponse(success: false, errorMessage: e.toString());\n    }\n  }\n  \n  Stream\u003cCalculationResponse\u003e streamCalculate(Stream\u003cCalculationRequest\u003e requests, {RpcContext? context}) async* {\n    await for (final request in requests) {\n      yield await calculate(request, context: context);\n    }\n  }\n}\n```\n\n### 3. Создайте клиент (Caller)\n\n```dart\nclass CalculatorCaller extends RpcCallerContract {\n  CalculatorCaller(RpcCallerEndpoint endpoint) : super('CalculatorService', endpoint);\n  \n  Future\u003cCalculationResponse\u003e calculate(CalculationRequest request) {\n    return endpoint.unaryRequest\u003cCalculationRequest, CalculationResponse\u003e(\n      serviceName: serviceName,\n      methodName: 'calculate',\n      requestCodec: CalculationRequest.codec,\n      responseCodec: CalculationResponse.codec,\n      request: request,\n    );\n  }\n  \n  Stream\u003cCalculationResponse\u003e streamCalculate(Stream\u003cCalculationRequest\u003e requests) {\n    return endpoint.bidirectionalStream\u003cCalculationRequest, CalculationResponse\u003e(\n      serviceName: serviceName,\n      methodName: 'streamCalculate',\n      requestCodec: CalculationRequest.codec,\n      responseCodec: CalculationResponse.codec,\n      requests: requests,\n    );\n  }\n  \n  // Удобный метод для сложения\n  Future\u003cdouble\u003e add(double a, double b) async {\n    final response = await calculate(CalculationRequest(a: a, b: b, operation: 'add'));\n    if (!response.success) throw Exception(response.errorMessage);\n    return response.result!;\n  }\n}\n```\n\n### 4. Запустите сервер и клиент\n\n```dart\nvoid main() async {\n  // Создаем InMemory транспорт\n  final (clientTransport, serverTransport) = RpcInMemoryTransport.pair();\n  \n  // Настраиваем сервер\n  final serverEndpoint = RpcResponderEndpoint(transport: serverTransport);\n  serverEndpoint.registerServiceContract(CalculatorResponder());\n  serverEndpoint.start(); // Важно: явно запускаем эндпоинт!\n  \n  // Настраиваем клиент\n  final clientEndpoint = RpcCallerEndpoint(transport: clientTransport);\n  final calculator = CalculatorCaller(clientEndpoint);\n  \n  // Делаем RPC вызовы с автоматической генерацией trace ID\n  final result = await calculator.add(10, 20);\n  print('10 + 20 = $result'); // 10 + 20 = 30.0\n  \n  // Или с пользовательским контекстом\n  final context = RpcContextUtils.withTracing(traceId: 'user_operation_123');\n  final resultWithContext = await calculator.calculate(\n    CalculationRequest(a: 5, b: 3, operation: 'multiply'),\n    context: context,\n  );\n  \n  // Работаем со стримом вычислений\n  final requests = Stream.fromIterable([\n    CalculationRequest(a: 5, b: 3, operation: 'add'),\n    CalculationRequest(a: 10, b: 2, operation: 'multiply'),\n    CalculationRequest(a: 15, b: 3, operation: 'divide'),\n  ]);\n  \n  await for (final response in calculator.streamCalculate(requests)) {\n    if (response.success) {\n      print('Result: ${response.result}');\n    } else {\n      print('Error: ${response.errorMessage}');\n    }\n  }\n  \n  // Закрываем ресурсы\n  await serverEndpoint.close();\n  await clientEndpoint.close();\n}\n```\n\n## Транспорты\n\n### InMemory Transport (включен в основную библиотеку)\nИдеально для разработки, тестирования и монолитных приложений:\n\n```dart\nfinal (clientTransport, serverTransport) = RpcInMemoryTransport.pair();\n// Использование: разработка, unit-тесты, простые приложения\n```\n\n### Дополнительные транспорты\n\nRPC Dart поддерживает создание кастомных транспортов через интерфейс `RpcTransport`:\n\n```dart\n// Пример кастомного транспорта\nclass CustomHttpTransport implements RpcTransport {\n  @override\n  Future\u003cvoid\u003e send(RpcMessage message) async {\n    // Реализация отправки через HTTP\n  }\n  \n  @override\n  Stream\u003cRpcMessage\u003e get messageStream =\u003e _messageController.stream;\n}\n\nfinal endpoint = RpcCallerEndpoint(transport: CustomHttpTransport());\n```\n\n**Возможные варианты расширения:**\n- **Isolate Transport** — для CPU-интенсивных задач и изоляции сбоев\n- **HTTP Transport** — для микросервисов и распределенных систем\n- **WebSocket Transport** — для real-time приложений\n\n**Ключевое преимущество:** код домена остается неизменным при смене транспорта!\n\n## Типы RPC взаимодействий\n\n| Тип | Описание | Пример использования |\n|-----|----------|---------------------|\n| **Unary Call** | Запрос → Ответ | CRUD операции, валидация |\n| **Server Stream** | Запрос → Поток ответов | Live обновления, прогресс |\n| **Client Stream** | Поток запросов → Ответ | Batch upload, агрегация |\n| **Bidirectional Stream** | Поток ↔ Поток | Чаты, real-time коллаборация |\n\n## Встроенные примитивы\n\nRPC Dart предоставляет готовые обертки для примитивных типов:\n\n```dart\n// Встроенные примитивы с кодеками\nfinal name = RpcString('John');       // Строки\nfinal age = RpcInt(25);              // Целые числа  \nfinal height = RpcDouble(175.5);      // Дробные числа\nfinal isActive = RpcBool(true);       // Булевы значения\nfinal tags = RpcList\u003cRpcString\u003e([...]);  // Списки\n\n// Удобные расширения\nfinal message = 'Hello'.rpc;     // RpcString\nfinal count = 42.rpc;            // RpcInt\nfinal price = 19.99.rpc;         // RpcDouble\nfinal enabled = true.rpc;        // RpcBool\n\n// Числовые примитивы поддерживают арифметические операторы\nfinal sum = RpcInt(10) + RpcInt(20);      // RpcInt(30)\nfinal product = RpcDouble(3.14) * RpcDouble(2.0);  // RpcDouble(6.28)\n\n// Доступ к значению через свойство .value\nfinal greeting = RpcString('Hello ') + RpcString('World'); \nprint(greeting.value); // \"Hello World\"\n```\n\n## StreamDistributor\n\n**StreamDistributor** — это мощный менеджер для управления серверными стримами, который превращает обычный `StreamController` в брокер сообщений с расширенными возможностями:\n\n### Основные возможности\n\n- **Широковещательная публикация** — отправка сообщений всем подключенным клиентам\n- **Фильтрованная публикация** — отправка по условию только определенным клиентам  \n- **Управление жизненным циклом** — автоматическое создание/удаление стримов\n- **Автоматическая очистка** — удаление неактивных стримов по таймеру\n- **Метрики и мониторинг** — отслеживание активности и производительности\n\n### Пример использования\n\n```dart\n// Создаем дистрибьютор для уведомлений\nfinal distributor = StreamDistributor\u003cNotificationEvent\u003e(\n  config: StreamDistributorConfig(\n    enableAutoCleanup: true,\n    inactivityThreshold: Duration(minutes: 5),\n  ),\n);\n\n// Создаем клиентские стримы для разных пользователей\nfinal userStream1 = distributor.createClientStreamWithId('user_123');\nfinal userStream2 = distributor.createClientStreamWithId('user_456');\n\n// Слушаем уведомления\nuserStream1.listen((notification) {\n  print('User 123 получил: ${notification.message}');\n});\n\n// Отправляем всем клиентам\ndistributor.publish(NotificationEvent(\n  message: 'Системное уведомление для всех',\n  priority: Priority.normal,\n));\n\n// Отправляем только клиентам с определенными ID\ndistributor.publishFiltered(\n  NotificationEvent(message: 'VIP уведомление'),\n  (client) =\u003e ['user_123', 'premium_user_789'].contains(client.clientId),\n);\n\n// Получаем метрики\nfinal metrics = distributor.metrics;\nprint('Активных клиентов: ${metrics.currentStreams}');\nprint('Отправлено сообщений: ${metrics.totalMessages}');\n```\n\n**Применение:** Идеально для реализации real-time уведомлений, чатов, live обновлений и других pub/sub сценариев в серверных стримах.\n\n## Тестирование\n\n```dart\n// Unit тест с моком (используйте любую мок-библиотеку)\nclass MockUserService extends Mock implements UserCaller {}\n\ntest('should handle user not found', () async {\n  final mockUserService = MockUserService();\n  when(() =\u003e mockUserService.getUser(any()))\n      .thenThrow(RpcException(code: RpcStatus.NOT_FOUND));\n  \n  final bloc = UserBloc(mockUserService);\n  \n  expect(\n    () =\u003e bloc.add(LoadUserEvent(id: '123')),\n    emitsError(isA\u003cUserNotFoundException\u003e()),\n  );\n});\n\n// Integration тест с InMemory транспортом\ntest('full integration test', () async {\n  final (clientTransport, serverTransport) = RpcInMemoryTransport.pair();\n  \n  final server = RpcResponderEndpoint(transport: serverTransport);\n  server.registerServiceContract(UserResponder(MockUserRepository()));\n  server.start();\n  \n  final client = UserCaller(RpcCallerEndpoint(transport: clientTransport));\n  \n  final response = await client.getUser(GetUserRequest(id: '123'));\n  expect(response.user.id, equals('123'));\n});\n```\n\n## FAQ\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eПодходит ли для production?\u003c/strong\u003e\u003c/summary\u003e\n\nРекомендуем тщательно протестировать библиотеку в вашей конкретной среде перед production деплоем.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eКак тестировать RPC код?\u003c/strong\u003e\u003c/summary\u003e\n\n```dart\n// Unit тесты с моками\nclass MockUserService extends Mock implements UserCaller {}\n\ntest('должен обработать ошибку \"пользователь не найден\"', () async {\n  final mockService = MockUserService();\n  when(() =\u003e mockService.getUser(any()))\n      .thenThrow(RpcException(code: RpcStatus.NOT_FOUND));\n  \n  final bloc = UserBloc(mockService);\n  expect(() =\u003e bloc.loadUser('123'), throwsA(isA\u003cUserNotFoundException\u003e()));\n});\n\n// Integration тесты с InMemory транспортом\ntest('полный интеграционный тест', () async {\n  final (client, server) = RpcInMemoryTransport.pair();\n  final endpoint = RpcResponderEndpoint(transport: server);\n  endpoint.registerServiceContract(TestService());\n  endpoint.start();\n  \n  final caller = TestCaller(RpcCallerEndpoint(transport: client));\n  final result = await caller.getData();\n  expect(result.value, equals('expected'));\n});\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eКакую производительность ожидать?\u003c/strong\u003e\u003c/summary\u003e\n\nПроизводительность зависит от многих факторов: среды выполнения, размера данных, типа транспорта. Для получения точных цифр запустите бенчмарки в вашей среде:\n\n```bash\ndart run benchmark/main.dart\n```\n\n**Общие наблюдения:**\n- InMemory транспорт имеет минимальные накладные расходы\n- CBOR сериализация обычно быстрее JSON\n- HTTP транспорт добавляет сетевую задержку\n- Для больших данных рассмотрите streaming или chunking\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eКак обрабатывать ошибки?\u003c/strong\u003e\u003c/summary\u003e\n\nRPC Dart использует gRPC-статусы для унифицированной обработки ошибок:\n\n```dart\ntry {\n  final result = await userService.getUser(request);\n} on RpcException catch (e) {\n  showError('RPC ошибка: ${e.message}');\n} on RpcDeadlineExceededException catch (e) {\n  showError('Таймаут запроса: ${e.timeout}');\n} on RpcCancelledException catch (e) {\n  showError('Операция отменена: ${e.message}');\n} catch (e) {\n  // Обработка неожиданных ошибок\n  logError('Unexpected error', error: e);\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eКак масштабировать RPC архитектуру?\u003c/strong\u003e\u003c/summary\u003e\n\n**CORD принципы масштабирования:**\n\n1. **Разделяйте домены** — каждый домен должен иметь четкую ответственность\n2. **Используйте контракты** — для типобезопасного взаимодействия\n3. **Минимизируйте связи** — домены общаются только через RPC\n4. **Централизуйте логику** — бизнес-логика в Responder'ах\n5. **Кэшируйте результаты** — в Caller'ах для UI оптимизации\n\n```dart\n// ❌ Плохо - прямые зависимости\nclass OrderBloc {\n  final UserRepository userRepo;\n  final PaymentRepository paymentRepo;\n  final NotificationRepository notificationRepo;\n}\n\n// ✅ Хорошо - через RPC контракты\nclass OrderBloc {\n  final UserCaller userService;\n  final PaymentCaller paymentService;\n  final NotificationCaller notificationService;\n}\n```\n\n\u003c/details\u003e\n\n---\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eBenchmark\u003c/strong\u003e\u003c/summary\u003e\n\u003ca href=\"https://bencher.dev/perf/rpc-dart?lower_value=false\u0026upper_value=false\u0026lower_boundary=false\u0026upper_boundary=false\u0026x_axis=version\u0026branches=336e550c-b07f-4a22-9a82-8ec26eb358de\u0026testbeds=c793799b-60f0-408a-832f-0afad9807be8\u0026benchmarks=e99c2168-aa9a-410f-b5d3-53be26e8aba2%2Ca139d61d-6327-4c37-9309-acb50a135912%2C46ba5e12-ef65-4255-bdc5-4fff6b1ac8c4%2Ccb8a8f66-581b-445a-8c3b-dbe7d0d70847\u0026measures=cac9b185-2af3-4ffe-a992-d3e3139d3e95\u0026start_time=1744741020215\u0026end_time=1749579420215\u0026tab=plots\u0026plots_search=a271874c-7479-4152-b94e-54db025f6abf\u0026key=true\u0026reports_per_page=4\u0026branches_per_page=8\u0026testbeds_per_page=8\u0026benchmarks_per_page=8\u0026plots_per_page=8\u0026reports_page=1\u0026branches_page=1\u0026testbeds_page=1\u0026benchmarks_page=1\u0026plots_page=1\u0026utm_medium=share\u0026utm_source=bencher\u0026utm_content=img\u0026utm_campaign=perf%2Bimg\u0026utm_term=rpc-dart\"\u003e\u003cimg src=\"https://api.bencher.dev/v0/projects/rpc-dart/perf/img?branches=336e550c-b07f-4a22-9a82-8ec26eb358de\u0026heads=\u0026testbeds=c793799b-60f0-408a-832f-0afad9807be8\u0026benchmarks=e99c2168-aa9a-410f-b5d3-53be26e8aba2%2Ca139d61d-6327-4c37-9309-acb50a135912%2C46ba5e12-ef65-4255-bdc5-4fff6b1ac8c4%2Ccb8a8f66-581b-445a-8c3b-dbe7d0d70847\u0026measures=cac9b185-2af3-4ffe-a992-d3e3139d3e95\u0026start_time=1744741020215\u0026end_time=1749579420215\u0026title=rpc_dart_throughput\" title=\"rpc_dart_throughput\" alt=\"rpc_dart_throughput for rpc_dart - Bencher\" /\u003e\u003c/a\u003e\n\u003c/details\u003e\n\n**Полезные ссылки:**\n- [CORD Architecture](docs/contract_oriented_remote_domains.md)\n- [RPC Dart на pub.dev](https://pub.dev/packages/rpc_dart)\n- [Исходный код на GitHub](https://github.com/nogipx/rpc_dart)\n- [Примеры кода](example/)\n- [Issues и поддержка](https://github.com/nogipx/rpc_dart/issues)\n\n*Создавайте масштабируемые Flutter приложения с RPC Dart!*\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnogipx%2Frpc_dart","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnogipx%2Frpc_dart","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnogipx%2Frpc_dart/lists"}