# Flutter Networking

> When to activate: Flutter networking, dio, http, retrofit, interceptors, error handling, API client, REST, JSON parsing, caching

- Skill: `mattakushi432/flutter-networking` (Agent Skill)
- Install (CLI): `npx skillmds@latest add mattakushi432/flutter-networking`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mattakushi432/flutter-networking/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: Mattakushi432 (https://skillmd.com/u/mattakushi432)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mattakushi432/flutter-networking

---

# Flutter Networking Patterns

## Dio Setup with Interceptors

```dart
final dioProvider = Provider<Dio>((ref) {
  final dio = Dio(BaseOptions(
    baseUrl: 'https://api.example.com/v1',
    connectTimeout: const Duration(seconds: 10),
    receiveTimeout: const Duration(seconds: 30),
    headers: {'Accept': 'application/json'},
  ));

  dio.interceptors.addAll([
    AuthInterceptor(ref),
    LogInterceptor(requestBody: true, responseBody: true),
    RetryInterceptor(dio: dio, retries: 3),
  ]);

  return dio;
});
```

## Auth Interceptor

```dart
class AuthInterceptor extends Interceptor {
  AuthInterceptor(this.ref);
  final Ref ref;

  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    final token = ref.read(authTokenProvider);
    if (token != null) {
      options.headers['Authorization'] = 'Bearer $token';
    }
    handler.next(options);
  }

  @override
  void onError(DioException e, ErrorInterceptorHandler handler) async {
    if (e.response?.statusCode == 401) {
      try {
        final newToken = await ref.read(authServiceProvider).refreshToken();
        ref.read(authTokenProvider.notifier).update(newToken);
        // Retry request with new token
        e.requestOptions.headers['Authorization'] = 'Bearer $newToken';
        final response = await ref.read(dioProvider).fetch(e.requestOptions);
        return handler.resolve(response);
      } catch (_) {
        ref.read(authServiceProvider).logout();
      }
    }
    handler.next(e);
  }
}
```

## Repository Pattern

```dart
abstract class UserRepository {
  Future<User> fetchUser(String id);
  Future<List<User>> fetchUsers({int page = 1, int limit = 20});
  Future<User> createUser(CreateUserDto dto);
}

class DioUserRepository implements UserRepository {
  DioUserRepository(this._dio);
  final Dio _dio;

  @override
  Future<User> fetchUser(String id) async {
    final response = await _dio.get('/users/$id');
    return User.fromJson(response.data as Map<String, dynamic>);
  }

  @override
  Future<List<User>> fetchUsers({int page = 1, int limit = 20}) async {
    final response = await _dio.get('/users', queryParameters: {
      'page': page,
      'limit': limit,
    });
    return (response.data['data'] as List)
        .map((j) => User.fromJson(j as Map<String, dynamic>))
        .toList();
  }

  @override
  Future<User> createUser(CreateUserDto dto) async {
    final response = await _dio.post('/users', data: dto.toJson());
    return User.fromJson(response.data as Map<String, dynamic>);
  }
}
```

## Error Handling

```dart
sealed class ApiError {
  const ApiError();
}
class NetworkError extends ApiError { const NetworkError(); }
class ServerError extends ApiError { final int code; const ServerError(this.code); }
class AuthError extends ApiError { const AuthError(); }
class UnknownError extends ApiError { final Object error; const UnknownError(this.error); }

ApiError mapDioError(DioException e) => switch (e.type) {
  DioExceptionType.connectionTimeout ||
  DioExceptionType.receiveTimeout => const NetworkError(),
  DioExceptionType.badResponse => switch (e.response?.statusCode) {
    401 => const AuthError(),
    >= 500 => ServerError(e.response!.statusCode!),
    _ => UnknownError(e),
  },
  _ => UnknownError(e),
};

// Usage in repository
Future<Result<User>> fetchUserSafe(String id) async {
  try {
    final user = await fetchUser(id);
    return Success(user);
  } on DioException catch (e) {
    return Failure(mapDioError(e).toString());
  }
}
```

## JSON Serialization

```dart
// Manual (no codegen)
class User {
  const User({required this.id, required this.name, this.avatar});
  final String id;
  final String name;
  final String? avatar;

  factory User.fromJson(Map<String, dynamic> json) => User(
    id: json['id'] as String,
    name: json['name'] as String,
    avatar: json['avatar'] as String?,
  );

  Map<String, dynamic> toJson() => {
    'id': id,
    'name': name,
    if (avatar != null) 'avatar': avatar,
  };
}

// With json_serializable (pubspec: json_serializable, json_annotation, build_runner)
@JsonSerializable()
class User {
  const User({required this.id, required this.name});
  final String id;
  @JsonKey(name: 'display_name')
  final String name;

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}
```

## Caching with dio_cache_interceptor

```dart
final cacheStore = MemCacheStore(maxSize: 10485760, maxEntrySize: 1048576);

dio.interceptors.add(DioCacheInterceptor(
  options: CacheOptions(
    store: cacheStore,
    policy: CachePolicy.refreshForceCache,
    maxStale: const Duration(minutes: 5),
  ),
));
```

## Multipart / File Upload

```dart
Future<void> uploadAvatar(String filePath) async {
  final formData = FormData.fromMap({
    'avatar': await MultipartFile.fromFile(filePath, filename: 'avatar.jpg'),
    'userId': currentUserId,
  });
  await _dio.post('/users/avatar', data: formData,
    onSendProgress: (sent, total) => print('${(sent / total * 100).toInt()}%'));
}
```

## Cancellation

```dart
final cancelToken = CancelToken();

Future<void> fetchWithCancel() async {
  try {
    await _dio.get('/slow-endpoint', cancelToken: cancelToken);
  } on DioException catch (e) {
    if (CancelToken.isCancel(e)) print('Request cancelled');
  }
}

// Cancel from outside (e.g., dispose)
cancelToken.cancel('Widget disposed');
```

