networking

SKILL.md

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:

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:

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:

final class OrdersRepository implements IOrdersRepository {
  OrdersRepository({required this.remote});
  final IOrdersRemoteDataSource remote;

  
  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:

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):

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):

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):

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.
Weekly Installs
2
GitHub Stars
3
First Seen
Feb 27, 2026
Installed on
opencode2
gemini-cli2
antigravity2
claude-code2
github-copilot2
codex2