home / skills / yelmuratoff / agent_sync / networking
This skill helps you implement reliable API calls, robust parsing, and tests by enforcing layered data handling and explicit error mapping.
npx playbooks add skill yelmuratoff/agent_sync --skill networkingReview the files below or copy the command above to add this skill to your agents.
---
name: networking
description: When implementing API calls, background parsing, error mapping, and repository/client tests.
---
# Networking (Clients, Parsing, Testing)
## When to use
- Adding a new endpoint for a feature.
- Parsing large JSON responses.
- Introducing retries/timeouts or improving reliability.
- Writing unit tests around repositories and clients.
## Steps
### 1) Keep HTTP in the data layer
Typical flow:
- BLoC calls repository interface (domain)
- repository implementation calls datasource/client (data)
- datasource owns request/response details
Example datasource:
```dart
abstract interface class IOrdersRemoteDataSource {
Future<List<Map<String, Object?>>> fetchOrders();
}
```
### 2) Map failures into explicit exceptions
Define explicit exception types and convert low-level errors at the boundary:
```dart
final class NetworkException implements Exception {
const NetworkException(this.message, {this.cause});
final String message;
final Object? cause;
}
final class ParseException implements Exception {
const ParseException(this.message, {this.cause});
final String message;
final Object? cause;
}
```
Repository boundary example:
```dart
final class OrdersRepository implements IOrdersRepository {
OrdersRepository({required this.remote});
final IOrdersRemoteDataSource remote;
@override
Future<List<OrderDto>> getOrders() async {
try {
final maps = await remote.fetchOrders();
return maps.map(OrderDto.fromMap).toList();
} on FormatException catch (e) {
throw ParseException('Invalid orders payload', cause: e);
} catch (e) {
throw NetworkException('Failed to fetch orders', cause: e);
}
}
}
```
### 3) Parse large payloads off the UI thread
Use background parsing for large JSON:
```dart
import 'package:flutter/foundation.dart';
List<OrderDto> parseOrders(List<Map<String, Object?>> maps) =>
maps.map(OrderDto.fromMap).toList();
Future<List<OrderDto>> parseOrdersInBackground(List<Map<String, Object?>> maps) =>
compute(parseOrders, maps);
```
For heavier work, prefer `Isolate.run` (Dart 3) where available.
### 4) Unit test repositories with mocked datasources
Prefer mocking the datasource/client (not making real HTTP calls):
```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
class _RemoteMock extends Mock implements IOrdersRemoteDataSource {}
void main() {
test('getOrders returns parsed DTOs', () async {
final remote = _RemoteMock();
when(() => remote.fetchOrders()).thenAnswer(
(_) async => [
{'id': '1', 'createdAt': '2026-01-01T00:00:00.000Z', 'timeout': 1000},
],
);
final repo = OrdersRepository(remote: remote);
final result = await repo.getOrders();
expect(result.length, 1);
expect(result.first.id, '1');
});
}
```
### 5) Implement auth token refresh (three-component pattern)
When the API requires authentication with token refresh, use three collaborating components:
**TokenStorage** (backed by `flutter_secure_storage`, never `SharedPreferences`):
```dart
abstract interface class TokenStorage<T> {
Future<T?> load();
Future<void> save(T token);
Future<void> clear();
Stream<T?> getStream(); // broadcasts changes to all listeners
}
```
**AuthorizationClient** (validates expiry locally, calls refresh endpoint):
```dart
abstract interface class AuthorizationClient {
/// Returns valid access token, refreshing if expired.
/// Throws [RevokeTokenException] if refresh fails.
Future<String> getValidAccessToken();
}
```
**OAuthInterceptor** key constraints:
- Attaches `Authorization: Bearer <token>` before each request.
- On 401: acquires a sequential lock so only one refresh fires even when multiple requests receive 401 simultaneously. All concurrent requests queue and retry with the new token.
- On `RevokeTokenException`: calls `TokenStorage.clear()` and triggers logout.
Key constraint: the sequential lock prevents N concurrent 401 responses from triggering N separate refresh calls. Only the first refresh executes; others await its result.
Test cases required:
- Happy path: valid token attached, 200 returned.
- Expired token: refresh called once, original request retried with new token.
- Concurrent 401s: refresh called exactly once despite multiple 401s.
- Revoked token: `TokenStorage.clear()` called, logout triggered.
This skill packages guidance and utilities for implementing networked data layers: clients, parsing, error mapping, and repository/client tests. It focuses on keeping HTTP concerns in the data layer, converting low-level failures into explicit exceptions, offloading heavy parsing, and making auth token refresh robust and testable. The goal is predictable, testable networking code that scales in Flutter/Dart apps.
The pattern places HTTP calls inside datasource/client implementations that the repository calls, keeping domain code agnostic of transport details. Low-level errors (format issues, network failures) are mapped to explicit exception types at the repository boundary. Large JSON parsing runs off the UI thread using compute or Isolate.run. For authenticated APIs, a three-component pattern (TokenStorage, AuthorizationClient, OAuthInterceptor) centralizes token lifecycle and serializes refresh behavior.
Why map low-level errors into explicit exceptions?
Explicit exceptions (e.g., NetworkException, ParseException) provide a clear, testable contract at the repository boundary and avoid leaking transport-specific errors to higher layers.
When should I use compute vs Isolate.run?
Use compute for lightweight JSON mapping on older Dart versions; prefer Isolate.run for heavier CPU-bound parsing and when running on Dart 3+ for better control.