flutter-networking
Comprehensive Flutter networking guidance including HTTP CRUD operations, WebSocket connections, authentication, error handling, and performance optimization. Use when Claude needs to implement HTTP requests GET POST PUT DELETE, WebSocket real-time communication, authenticated requests with headers and tokens, background parsing with isolates, REST API integration with proper error handling, or any network-related functionality in Flutter applications.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install madteacher-mad-agents-skills-flutter-networking
Repository
Skill path: flutter-networking
Comprehensive Flutter networking guidance including HTTP CRUD operations, WebSocket connections, authentication, error handling, and performance optimization. Use when Claude needs to implement HTTP requests GET POST PUT DELETE, WebSocket real-time communication, authenticated requests with headers and tokens, background parsing with isolates, REST API integration with proper error handling, or any network-related functionality in Flutter applications.
Open repositoryBest for
Primary workflow: Ship Full Stack.
Technical facets: Full Stack, Backend, Integration.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: MADTeacher.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install flutter-networking into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/MADTeacher/mad-agents-skills before adding flutter-networking to shared team environments
- Use flutter-networking for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: flutter-networking
description: Comprehensive Flutter networking guidance including HTTP CRUD operations, WebSocket connections, authentication, error handling, and performance optimization. Use when Claude needs to implement HTTP requests GET POST PUT DELETE, WebSocket real-time communication, authenticated requests with headers and tokens, background parsing with isolates, REST API integration with proper error handling, or any network-related functionality in Flutter applications.
metadata:
author: Stanislav [MADTeacher] Chernyshev
version: "1.0"
---
# Flutter Networking
## Quick Start
Add HTTP dependency to `pubspec.yaml`:
```yaml
dependencies:
http: ^1.6.0
```
Basic GET request:
```dart
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<Album> fetchAlbum() async {
final response = await http.get(
Uri.parse('https://api.example.com/albums/1'),
);
if (response.statusCode == 200) {
return Album.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to load album');
}
}
```
Use in UI with `FutureBuilder`:
```dart
FutureBuilder<Album>(
future: futureAlbum,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data!.title);
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
return const CircularProgressIndicator();
},
)
```
## HTTP Methods
### GET - Fetch Data
Use for retrieving data. See [http-basics.md](references/http-basics.md) for complete examples.
### POST - Create Data
Use for creating new resources. Requires `Content-Type: application/json` header.
```dart
final response = await http.post(
Uri.parse('https://api.example.com/albums'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(<String, String>{'title': title}),
);
```
See [http-basics.md](references/http-basics.md) for POST examples.
### PUT - Update Data
Use for updating existing resources.
```dart
final response = await http.put(
Uri.parse('https://api.example.com/albums/1'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(<String, String>{'title': title}),
);
```
### DELETE - Remove Data
Use for deleting resources.
```dart
final response = await http.delete(
Uri.parse('https://api.example.com/albums/1'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
);
```
## WebSocket
Add WebSocket dependency:
```yaml
dependencies:
web_socket_channel: ^3.0.3
```
Basic WebSocket connection:
```dart
import 'package:web_socket_channel/web_socket_channel.dart';
final _channel = WebSocketChannel.connect(
Uri.parse('wss://echo.websocket.events'),
);
// Listen for messages
StreamBuilder(
stream: _channel.stream,
builder: (context, snapshot) {
return Text(snapshot.hasData ? '${snapshot.data}' : '');
},
)
// Send message
_channel.sink.add('Hello');
// Close connection
_channel.sink.close();
```
See [websockets.md](references/websockets.md) for complete WebSocket implementation.
## Authentication
Add authorization headers to requests:
```dart
import 'dart:io';
final response = await http.get(
Uri.parse('https://api.example.com/data'),
headers: {HttpHeaders.authorizationHeader: 'Bearer $token'},
);
```
Common authentication patterns:
- **Bearer Token**: `Authorization: Bearer <token>`
- **Basic Auth**: `Authorization: Basic <base64_credentials>`
- **API Key**: `X-API-Key: <key>`
See [authentication.md](references/authentication.md) for detailed authentication strategies.
## Error Handling
Handle HTTP errors appropriately:
```dart
if (response.statusCode >= 200 && response.statusCode < 300) {
return Data.fromJson(jsonDecode(response.body));
} else if (response.statusCode == 401) {
throw UnauthorizedException();
} else if (response.statusCode == 404) {
throw NotFoundException();
} else {
throw ServerException();
}
```
See [error-handling.md](references/error-handling.md) for comprehensive error handling strategies.
## Performance
### Background Parsing with Isolates
For large JSON responses, use `compute()` to parse in background isolate:
```dart
import 'package:flutter/foundation.dart';
Future<List<Photo>> fetchPhotos(http.Client client) async {
final response = await client.get(
Uri.parse('https://api.example.com/photos'),
);
return compute(parsePhotos, response.body);
}
List<Photo> parsePhotos(String responseBody) {
final parsed = (jsonDecode(responseBody) as List)
.cast<Map<String, dynamic>>();
return parsed.map<Photo>((json) => Photo.fromJson(json)).toList();
}
```
See [performance.md](references/performance.md) for optimization techniques.
## Integration with Architecture
When using MVVM architecture (see [flutter-architecture](../flutter-architecture/)):
1. **Service Layer**: Create HTTP service for API endpoints
2. **Repository Layer**: Aggregate data from services, handle caching
3. **ViewModel Layer**: Transform repository data for UI
Example service:
```dart
class AlbumService {
final http.Client _client;
AlbumService(this._client);
Future<Album> fetchAlbum(int id) async {
final response = await _client.get(
Uri.parse('https://api.example.com/albums/$id'),
);
if (response.statusCode == 200) {
return Album.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to load album');
}
}
}
```
## Common Patterns
### Repository Pattern
Single source of truth for data type:
```dart
class AlbumRepository {
final AlbumService _service;
final LocalStorage _cache;
Future<Album> getAlbum(int id) async {
try {
return await _cache.getAlbum(id) ??
await _service.fetchAlbum(id);
} catch (e) {
throw AlbumFetchException();
}
}
}
```
### Retry Logic
Implement exponential backoff for failed requests:
```dart
Future<T> fetchWithRetry<T>(
Future<T> Function() fetch, {
int maxRetries = 3,
}) async {
for (int i = 0; i < maxRetries; i++) {
try {
return await fetch();
} catch (e) {
if (i == maxRetries - 1) rethrow;
await Future.delayed(Duration(seconds: 2 << i));
}
}
throw StateError('Unreachable');
}
```
## Best Practices
### DO
- Use type-safe model classes with `fromJson` factories
- Handle all HTTP status codes appropriately
- Parse JSON in background isolates for large responses
- Implement retry logic for transient failures
- Cache responses when appropriate
- Use proper timeouts
- Secure tokens and credentials
### DON'T
- Parse JSON on main thread for large responses
- Ignore error states in UI
- Store tokens in source code or public repositories
- Make requests without timeout configuration
- Block UI thread with network operations
- Throw generic exceptions without context
## Resources
### references/
- [http-basics.md](references/http-basics.md) - Complete HTTP CRUD operations examples
- [websockets.md](references/websockets.md) - WebSocket implementation patterns
- [authentication.md](references/authentication.md) - Authentication strategies and token management
- [error-handling.md](references/error-handling.md) - Comprehensive error handling patterns
- [performance.md](references/performance.md) - Optimization techniques and best practices
### assets/examples/
- `fetch_example.dart` - Complete GET request with FutureBuilder
- `post_example.dart` - POST request implementation
- `websocket_example.dart` - WebSocket client with stream handling
- `auth_example.dart` - Authenticated request example
- `background_parsing.dart` - compute() for JSON parsing
### assets/code-templates/
- `http_service.dart` - Reusable HTTP service template
- `repository_template.dart` - Repository pattern template
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### references/http-basics.md
```markdown
# HTTP Basics
## Overview
Complete guide for HTTP operations in Flutter using the `http` package.
## Dependencies
Add to `pubspec.yaml`:
```yaml
dependencies:
http: ^1.6.0
```
## GET Requests
### Basic GET Request
```dart
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<Album> fetchAlbum(int id) async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/albums/$id'),
);
if (response.statusCode == 200) {
return Album.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to load album');
}
}
```
### GET with Query Parameters
```dart
final response = await http.get(
Uri.parse('https://api.example.com/albums')
.replace(queryParameters: {
'userId': '1',
'_limit': '10',
}),
);
```
### GET with Custom Headers
```dart
final response = await http.get(
Uri.parse('https://api.example.com/data'),
headers: {
'Accept': 'application/json',
'User-Agent': 'MyApp/1.0',
},
);
```
## POST Requests
### Create Resource
```dart
Future<Album> createAlbum(String title) async {
final response = await http.post(
Uri.parse('https://jsonplaceholder.typicode.com/albums'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(<String, String>{'title': title}),
);
if (response.statusCode == 201) {
return Album.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to create album');
}
}
```
### POST with Complex Object
```dart
Future<User> createUser(User user) async {
final response = await http.post(
Uri.parse('https://api.example.com/users'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(user.toJson()),
);
if (response.statusCode == 201) {
return User.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to create user');
}
}
```
## PUT Requests
### Update Entire Resource
```dart
Future<Album> updateAlbum(int id, String title) async {
final response = await http.put(
Uri.parse('https://jsonplaceholder.typicode.com/albums/$id'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(<String, String>{'title': title, 'id': id.toString()}),
);
if (response.statusCode == 200) {
return Album.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to update album');
}
}
```
## DELETE Requests
### Delete Resource
```dart
Future<Album> deleteAlbum(String id) async {
final http.Response response = await http.delete(
Uri.parse('https://jsonplaceholder.typicode.com/albums/$id'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
);
if (response.statusCode == 200) {
return Album.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to delete album');
}
}
```
## Model Classes
### JSON Parsing Pattern
```dart
class Album {
final int userId;
final int id;
final String title;
const Album({
required this.userId,
required this.id,
required this.title,
});
factory Album.fromJson(Map<String, dynamic> json) {
return switch (json) {
{'userId': int userId, 'id': int id, 'title': String title} => Album(
userId: userId,
id: id,
title: title,
),
_ => throw const FormatException('Failed to load album.'),
};
}
Map<String, dynamic> toJson() => {
'userId': userId,
'id': id,
'title': title,
};
}
```
### Nested JSON Parsing
```dart
class User {
final int id;
final String name;
final Address address;
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
address: Address.fromJson(json['address'] as Map<String, dynamic>),
);
}
}
class Address {
final String street;
final String city;
factory Address.fromJson(Map<String, dynamic> json) {
return Address(
street: json['street'] as String,
city: json['city'] as String,
);
}
}
```
## List Parsing
```dart
Future<List<Album>> fetchAlbums() async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/albums'),
);
if (response.statusCode == 200) {
final List<dynamic> data = jsonDecode(response.body);
return data.map((json) => Album.fromJson(json)).toList();
} else {
throw Exception('Failed to load albums');
}
}
```
## Using with FutureBuilder
```dart
class _MyAppState extends State<MyApp> {
late Future<Album> futureAlbum;
@override
void initState() {
super.initState();
futureAlbum = fetchAlbum();
}
@override
Widget build(BuildContext context) {
return FutureBuilder<Album>(
future: futureAlbum,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else if (snapshot.hasData) {
return Text(snapshot.data!.title);
} else {
return const Text('No data');
}
},
);
}
}
```
## Timeout Configuration
```dart
Future<Album> fetchAlbum() async {
try {
final response = await http.get(
Uri.parse('https://api.example.com/albums/1'),
).timeout(
const Duration(seconds: 10),
onTimeout: () {
throw TimeoutException('Request timeout');
},
);
return Album.fromJson(jsonDecode(response.body));
} on TimeoutException catch (e) {
throw Exception('Request timed out: $e');
}
}
```
## Common Response Headers
```dart
final contentType = response.headers['content-type'];
final contentLength = int.parse(response.headers['content-length'] ?? '0');
final cacheControl = response.headers['cache-control'];
```
## Best Practices
1. **Always handle status codes** - Check response.statusCode before processing
2. **Use type-safe models** - Create classes with fromJson/toJson methods
3. **Set timeouts** - Prevent indefinite hanging requests
4. **Handle exceptions** - Catch and properly handle network errors
5. **Use appropriate HTTP methods** - GET for reading, POST for creating, PUT for updating, DELETE for removing
```
### references/websockets.md
```markdown
# WebSockets
## Overview
Complete guide for implementing WebSocket connections in Flutter using the `web_socket_channel` package.
## Dependencies
Add to `pubspec.yaml`:
```yaml
dependencies:
web_socket_channel: ^3.0.3
```
## Basic Connection
### Connecting to WebSocket
```dart
import 'package:web_socket_channel/web_socket_channel.dart';
final _channel = WebSocketChannel.connect(
Uri.parse('wss://echo.websocket.events'),
);
```
### Sending Messages
```dart
void _sendMessage(String message) {
if (message.isNotEmpty) {
_channel.sink.add(message);
}
}
```
### Receiving Messages with StreamBuilder
```dart
StreamBuilder(
stream: _channel.stream,
builder: (context, snapshot) {
return Text(snapshot.hasData ? '${snapshot.data}' : '');
},
)
```
### Closing Connection
```dart
@override
void dispose() {
_channel.sink.close();
super.dispose();
}
```
## Complete Example
```dart
import 'package:flutter/material.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
class WebSocketDemo extends StatefulWidget {
const WebSocketDemo({super.key});
@override
State<WebSocketDemo> createState() => _WebSocketDemoState();
}
class _WebSocketDemoState extends State<WebSocketDemo> {
final TextEditingController _controller = TextEditingController();
final _channel = WebSocketChannel.connect(
Uri.parse('wss://echo.websocket.events'),
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('WebSocket Demo')),
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Form(
child: TextFormField(
controller: _controller,
decoration: const InputDecoration(labelText: 'Send a message'),
),
),
const SizedBox(height: 24),
StreamBuilder(
stream: _channel.stream,
builder: (context, snapshot) {
return Text(snapshot.hasData ? '${snapshot.data}' : '');
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _sendMessage(_controller.text),
tooltip: 'Send message',
child: const Icon(Icons.send),
),
);
}
void _sendMessage(String message) {
if (message.isNotEmpty) {
_channel.sink.add(message);
}
}
@override
void dispose() {
_channel.sink.close();
_controller.dispose();
super.dispose();
}
}
```
## Connection States
### Handling Connection States
```dart
enum ConnectionState { connecting, connected, disconnected, error }
class _WebSocketDemoState extends State<WebSocketDemo> {
ConnectionState _connectionState = ConnectionState.connecting;
String _errorMessage = '';
@override
void initState() {
super.initState();
_connect();
}
void _connect() {
try {
final channel = WebSocketChannel.connect(Uri.parse('wss://example.com/ws'));
channel.stream.listen(
(data) {
setState(() {
_connectionState = ConnectionState.connected;
});
},
onError: (error) {
setState(() {
_connectionState = ConnectionState.error;
_errorMessage = error.toString();
});
},
onDone: () {
setState(() {
_connectionState = ConnectionState.disconnected;
});
},
);
_channel = channel;
} catch (e) {
setState(() {
_connectionState = ConnectionState.error;
_errorMessage = e.toString();
});
}
}
}
```
## Reconnection Logic
### Automatic Reconnection
```dart
class _WebSocketDemoState extends State<WebSocketDemo> {
WebSocketChannel? _channel;
Timer? _reconnectTimer;
int _reconnectAttempts = 0;
static const int _maxReconnectAttempts = 5;
void _connect() {
try {
_channel = WebSocketChannel.connect(Uri.parse('wss://example.com/ws'));
_channel!.stream.listen(
(data) {
_reconnectAttempts = 0;
},
onError: (error) {
_scheduleReconnect();
},
onDone: () {
_scheduleReconnect();
},
);
} catch (e) {
_scheduleReconnect();
}
}
void _scheduleReconnect() {
if (_reconnectAttempts >= _maxReconnectAttempts) {
return;
}
_reconnectAttempts++;
final delay = Duration(seconds: _reconnectAttempts * 2);
_reconnectTimer?.cancel();
_reconnectTimer = Timer(delay, () {
_connect();
});
}
@override
void dispose() {
_reconnectTimer?.cancel();
_channel?.sink.close();
super.dispose();
}
}
```
## JSON Messages
### Sending JSON Data
```dart
import 'dart:convert';
void _sendJsonMessage(Map<String, dynamic> data) {
_channel.sink.add(jsonEncode(data));
}
// Usage
_sendJsonMessage({
'type': 'chat',
'message': 'Hello, world!',
'userId': 123,
});
```
### Receiving JSON Data
```dart
StreamBuilder(
stream: _channel.stream,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const CircularProgressIndicator();
}
try {
final data = jsonDecode(snapshot.data as String) as Map<String, dynamic>;
return Text('Message: ${data['message']}');
} catch (e) {
return Text('Error parsing JSON: $e');
}
},
)
```
### Type-Safe Message Handling
```dart
abstract class WebSocketMessage {
factory WebSocketMessage.fromJson(Map<String, dynamic> json) {
final type = json['type'] as String;
switch (type) {
case 'chat':
return ChatMessage.fromJson(json);
case 'notification':
return NotificationMessage.fromJson(json);
default:
throw FormatException('Unknown message type: $type');
}
}
}
class ChatMessage extends WebSocketMessage {
final String message;
final int userId;
ChatMessage({required this.message, required this.userId});
factory ChatMessage.fromJson(Map<String, dynamic> json) {
return ChatMessage(
message: json['message'] as String,
userId: json['userId'] as int,
);
}
}
StreamBuilder(
stream: _channel.stream,
builder: (context, snapshot) {
if (!snapshot.hasData) return const SizedBox();
try {
final message = WebSocketMessage.fromJson(
jsonDecode(snapshot.data as String) as Map<String, dynamic>,
);
if (message is ChatMessage) {
return ChatBubble(message: message.message);
}
} catch (e) {
return Text('Error: $e');
}
return const SizedBox();
},
)
```
## Authentication
### Connecting with Auth Token
```dart
void _connectWithToken(String token) {
final uri = Uri.parse('wss://example.com/ws').replace(
queryParameters: {'token': token},
);
_channel = WebSocketChannel.connect(uri);
}
```
### Sending Auth Header
```dart
import 'package:web_socket_channel/io.dart';
void _connectWithHeaders(String token) {
final channel = IOWebSocketChannel.connect(
'wss://example.com/ws',
headers: {
'Authorization': 'Bearer $token',
'X-Client-ID': 'my-app',
},
);
_channel = channel;
}
```
## Multiple Channels
### Managing Multiple Connections
```dart
class WebSocketManager {
final Map<String, WebSocketChannel> _channels = {};
void connect(String channelId, String url) {
if (_channels.containsKey(channelId)) {
_channels[channelId]?.sink.close();
}
_channels[channelId] = WebSocketChannel.connect(Uri.parse(url));
}
void send(String channelId, String message) {
_channels[channelId]?.sink.add(message);
}
Stream<dynamic> getStream(String channelId) {
return _channels[channelId]!.stream;
}
void close(String channelId) {
_channels[channelId]?.sink.close();
_channels.remove(channelId);
}
void closeAll() {
_channels.forEach((key, channel) => channel.sink.close());
_channels.clear();
}
}
```
## Error Handling
### Handling WebSocket Errors
```dart
channel.stream.listen(
(data) {
print('Received: $data');
},
onError: (error) {
print('WebSocket error: $error');
},
onDone: () {
print('WebSocket connection closed');
},
cancelOnError: false,
);
```
## Best Practices
1. **Always close connections** - Dispose WebSocket in dispose() method
2. **Handle connection states** - Track connecting, connected, disconnected states
3. **Implement reconnection** - Add automatic reconnection with exponential backoff
4. **Validate incoming data** - Parse and validate JSON messages
5. **Use type-safe models** - Create message classes for different message types
6. **Secure connections** - Use wss:// (WebSocket Secure) instead of ws://
7. **Handle network changes** - Reconnect on network state changes
8. **Limit message size** - Validate and limit incoming message sizes
```
### references/authentication.md
```markdown
# Authentication
## Overview
Complete guide for implementing authentication in Flutter HTTP requests.
## Authentication Methods
### Bearer Token (JWT)
Most common for REST APIs using JWT (JSON Web Tokens):
```dart
import 'dart:io';
Future<Album> fetchAlbum(String token) async {
final response = await http.get(
Uri.parse('https://api.example.com/albums/1'),
headers: {HttpHeaders.authorizationHeader: 'Bearer $token'},
);
if (response.statusCode == 200) {
return Album.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to load album');
}
}
```
### Basic Authentication
```dart
import 'dart:convert';
String basicAuthHeader(String username, String password) {
final credentials = '$username:$password';
return 'Basic ${base64Encode(utf8.encode(credentials))}';
}
Future<Album> fetchAlbum(String username, String password) async {
final response = await http.get(
Uri.parse('https://api.example.com/albums/1'),
headers: {
HttpHeaders.authorizationHeader: basicAuthHeader(username, password),
},
);
if (response.statusCode == 200) {
return Album.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to load album');
}
}
```
### API Key
```dart
Future<Album> fetchAlbum(String apiKey) async {
final response = await http.get(
Uri.parse('https://api.example.com/albums/1'),
headers: {'X-API-Key': apiKey},
);
if (response.statusCode == 200) {
return Album.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to load album');
}
}
```
## Token Storage
### Using shared_preferences
Add to `pubspec.yaml`:
```yaml
dependencies:
shared_preferences: ^2.5.4
```
Store and retrieve tokens:
```dart
import 'package:shared_preferences/shared_preferences.dart';
class TokenStorage {
static const String _tokenKey = 'auth_token';
Future<void> saveToken(String token) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_tokenKey, token);
}
Future<String?> getToken() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(_tokenKey);
}
Future<void> clearToken() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_tokenKey);
}
}
```
### Using flutter_secure_storage
For more secure storage:
```yaml
dependencies:
flutter_secure_storage: ^10.0.0
```
```dart
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class SecureTokenStorage {
final _storage = const FlutterSecureStorage();
static const String _tokenKey = 'auth_token';
Future<void> saveToken(String token) async {
await _storage.write(key: _tokenKey, value: token);
}
Future<String?> getToken() async {
return await _storage.read(key: _tokenKey);
}
Future<void> clearToken() async {
await _storage.delete(key: _tokenKey);
}
}
```
## Authenticated HTTP Client
### Wrapper Class
```dart
import 'dart:io';
import 'package:http/http.dart' as http;
class AuthenticatedHttpClient {
final TokenStorage _tokenStorage;
AuthenticatedHttpClient(this._tokenStorage);
Future<http.Response> get(String url) async {
final token = await _tokenStorage.getToken();
if (token == null) {
throw UnauthorizedException();
}
return await http.get(
Uri.parse(url),
headers: {HttpHeaders.authorizationHeader: 'Bearer $token'},
);
}
Future<http.Response> post(
String url, {
Map<String, String>? headers,
Object? body,
}) async {
final token = await _tokenStorage.getToken();
if (token == null) {
throw UnauthorizedException();
}
return await http.post(
Uri.parse(url),
headers: {
HttpHeaders.authorizationHeader: 'Bearer $token',
...?headers,
},
body: body,
);
}
Future<http.Response> put(
String url, {
Map<String, String>? headers,
Object? body,
}) async {
final token = await _tokenStorage.getToken();
if (token == null) {
throw UnauthorizedException();
}
return await http.put(
Uri.parse(url),
headers: {
HttpHeaders.authorizationHeader: 'Bearer $token',
...?headers,
},
body: body,
);
}
Future<http.Response> delete(String url) async {
final token = await _tokenStorage.getToken();
if (token == null) {
throw UnauthorizedException();
}
return await http.delete(
Uri.parse(url),
headers: {HttpHeaders.authorizationHeader: 'Bearer $token'},
);
}
}
class UnauthorizedException implements Exception {
final String message;
UnauthorizedException([this.message = 'Unauthorized']);
@override
String toString() => message;
}
```
## Token Refresh
### Refresh Token Pattern
```dart
class AuthManager {
final TokenStorage _tokenStorage;
final http.Client _client;
String? _accessToken;
String? _refreshToken;
AuthManager(this._tokenStorage, this._client);
Future<String> getAccessToken() async {
if (_accessToken != null && !_isTokenExpired(_accessToken)) {
return _accessToken!;
}
return await _refreshAccessToken();
}
bool _isTokenExpired(String token) {
// Decode JWT and check expiration
return false;
}
Future<String> _refreshAccessToken() async {
final response = await _client.post(
Uri.parse('https://api.example.com/auth/refresh'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'refreshToken': _refreshToken}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as Map<String, dynamic>;
_accessToken = data['accessToken'] as String;
_refreshToken = data['refreshToken'] as String;
await _tokenStorage.saveToken(_accessToken!);
return _accessToken!;
} else {
throw Exception('Failed to refresh token');
}
}
Future<void> logout() async {
await _tokenStorage.clearToken();
_accessToken = null;
_refreshToken = null;
}
}
```
### Interceptor Pattern
```dart
class AuthenticatedClient extends http.BaseClient {
final http.Client _inner;
final AuthManager _authManager;
AuthenticatedClient(this._inner, this._authManager);
@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
final token = await _authManager.getAccessToken();
request.headers['Authorization'] = 'Bearer $token';
final response = await _inner.send(request);
if (response.statusCode == 401) {
await _authManager.refreshAccessToken();
final newToken = await _authManager.getAccessToken();
request.headers['Authorization'] = 'Bearer $newToken';
return await _inner.send(request);
}
return response;
}
}
```
## Login Flow
### Complete Login Example
```dart
import 'dart:convert';
class AuthService {
final http.Client _client;
final TokenStorage _tokenStorage;
AuthService(this._client, this._tokenStorage);
Future<User> login(String email, String password) async {
final response = await _client.post(
Uri.parse('https://api.example.com/auth/login'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'email': email,
'password': password,
}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as Map<String, dynamic>;
final token = data['token'] as String;
final user = User.fromJson(data['user'] as Map<String, dynamic>);
await _tokenStorage.saveToken(token);
return user;
} else if (response.statusCode == 401) {
throw InvalidCredentialsException();
} else {
throw Exception('Login failed');
}
}
Future<void> logout() async {
await _tokenStorage.clearToken();
}
}
class User {
final int id;
final String email;
final String name;
User({required this.id, required this.email, required this.name});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
email: json['email'] as String,
name: json['name'] as String,
);
}
}
class InvalidCredentialsException implements Exception {
@override
String toString() => 'Invalid email or password';
}
```
## OAuth2
### OAuth2 Flow
```dart
class OAuth2Service {
final http.Client _client;
final TokenStorage _tokenStorage;
OAuth2Service(this._client, this._tokenStorage);
Future<String> authenticate(
String clientId,
String clientSecret,
String code,
String redirectUri,
) async {
final response = await _client.post(
Uri.parse('https://oauth.example.com/token'),
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: {
'grant_type': 'authorization_code',
'client_id': clientId,
'client_secret': clientSecret,
'code': code,
'redirect_uri': redirectUri,
},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as Map<String, dynamic>;
final token = data['access_token'] as String;
await _tokenStorage.saveToken(token);
return token;
} else {
throw Exception('OAuth authentication failed');
}
}
}
```
## Best Practices
1. **Store tokens securely** - Use flutter_secure_storage for sensitive tokens
2. **Refresh tokens** - Implement token refresh before expiration
3. **Handle 401 errors** - Automatically retry with refreshed token
4. **Clear tokens on logout** - Remove stored tokens when user logs out
5. **Use HTTPS** - Never send tokens over unencrypted connections
6. **Token rotation** - Rotate refresh tokens for better security
7. **Scope tokens** - Use minimal required scopes
8. **Error handling** - Provide clear error messages for authentication failures
```
### references/error-handling.md
```markdown
# Error Handling
## Overview
Complete guide for handling errors in Flutter networking operations.
## HTTP Status Code Handling
### Standard Status Codes
```dart
Future<Album> fetchAlbum(int id) async {
final response = await http.get(
Uri.parse('https://api.example.com/albums/$id'),
);
switch (response.statusCode) {
case 200:
case 201:
case 204:
return Album.fromJson(jsonDecode(response.body));
case 400:
throw BadRequestException('Invalid request');
case 401:
throw UnauthorizedException('Not authenticated');
case 403:
throw ForbiddenException('Access denied');
case 404:
throw NotFoundException('Album not found');
case 429:
throw TooManyRequestsException('Rate limit exceeded');
case 500:
case 502:
case 503:
throw ServerException('Server error');
default:
throw HttpException(
'HTTP ${response.statusCode}: ${response.reasonPhrase}',
);
}
}
```
## Custom Exception Classes
### Base Exception Classes
```dart
abstract class ApiException implements Exception {
final String message;
final int? statusCode;
ApiException(this.message, [this.statusCode]);
@override
String toString() => message;
}
class BadRequestException extends ApiException {
BadRequestException(String message) : super(message, 400);
}
class UnauthorizedException extends ApiException {
UnauthorizedException(String message) : super(message, 401);
}
class ForbiddenException extends ApiException {
ForbiddenException(String message) : super(message, 403);
}
class NotFoundException extends ApiException {
NotFoundException(String message) : super(message, 404);
}
class TooManyRequestsException extends ApiException {
TooManyRequestsException(String message) : super(message, 429);
}
class ServerException extends ApiException {
ServerException(String message) : super(message, 500);
}
class NetworkException extends ApiException {
NetworkException(String message) : super(message);
}
class TimeoutException extends ApiException {
TimeoutException(String message) : super(message);
}
```
## Parsing Errors
### JSON Parsing with Error Handling
```dart
class Album {
final int userId;
final int id;
final String title;
Album({required this.userId, required this.id, required this.title});
factory Album.fromJson(Map<String, dynamic> json) {
try {
return Album(
userId: json['userId'] as int,
id: json['id'] as int,
title: json['title'] as String,
);
} on TypeError catch (e) {
throw JsonParseException('Failed to parse album: $e');
} catch (e) {
throw JsonParseException('Unknown parsing error: $e');
}
}
}
class JsonParseException extends ApiException {
JsonParseException(String message) : super(message);
}
```
### Safe JSON Parsing
```dart
class SafeParser {
static String? parseString(Map<String, dynamic> json, String key) {
try {
return json[key] as String?;
} catch (_) {
return null;
}
}
static int? parseInt(Map<String, dynamic> json, String key) {
try {
return json[key] as int?;
} catch (_) {
return null;
}
}
static double? parseDouble(Map<String, dynamic> json, String key) {
try {
return json[key] as double?;
} catch (_) {
return null;
}
}
}
// Usage
factory User.fromJson(Map<String, dynamic> json) {
return User(
name: SafeParser.parseString(json, 'name') ?? 'Unknown',
age: SafeParser.parseInt(json, 'age') ?? 0,
score: SafeParser.parseDouble(json, 'score') ?? 0.0,
);
}
```
## Network Error Handling
### Timeout Handling
```dart
import 'dart:async';
Future<T> withTimeout<T>(Future<T> future, Duration duration) async {
try {
return await future.timeout(
duration,
onTimeout: () {
throw TimeoutException('Request timed out after ${duration.inSeconds}s');
},
);
} on TimeoutException catch (e) {
throw TimeoutException(e.message ?? 'Request timed out');
}
}
// Usage
final response = await withTimeout(
http.get(Uri.parse('https://api.example.com/data')),
const Duration(seconds: 10),
);
```
### Connection Errors
```dart
Future<T> withConnectionHandling<T>(Future<T> future) async {
try {
return await future;
} on SocketException catch (e) {
throw NetworkException('No internet connection: ${e.message}');
} on HttpException catch (e) {
throw NetworkException('HTTP error: ${e.message}');
} on FormatException catch (e) {
throw NetworkException('Invalid response format: ${e.message}');
} catch (e) {
throw NetworkException('Unknown network error: $e');
}
}
```
## Error Display in UI
### Error Widget
```dart
class ErrorDisplay extends StatelessWidget {
final Object error;
final VoidCallback? onRetry;
const ErrorDisplay({
super.key,
required this.error,
this.onRetry,
});
String get _errorMessage {
if (error is NetworkException) {
return 'Network error. Please check your connection.';
} else if (error is UnauthorizedException) {
return 'You are not authorized. Please log in.';
} else if (error is NotFoundException) {
return 'The requested resource was not found.';
} else if (error is ServerException) {
return 'Server error. Please try again later.';
} else if (error is TimeoutException) {
return 'Request timed out. Please try again.';
} else {
return 'An error occurred: $error';
}
}
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 48, color: Colors.red),
const SizedBox(height: 16),
Text(
_errorMessage,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge,
),
if (onRetry != null) ...[
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: onRetry,
icon: const Icon(Icons.refresh),
label: const Text('Retry'),
),
],
],
),
),
);
}
}
```
### FutureBuilder with Error Handling
```dart
FutureBuilder<Album>(
future: futureAlbum,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return ErrorDisplay(
error: snapshot.error!,
onRetry: () {
setState(() {
futureAlbum = fetchAlbum();
});
},
);
}
if (snapshot.hasData) {
return AlbumCard(album: snapshot.data!);
}
return const SizedBox();
},
)
```
## Retry Logic
### Exponential Backoff
```dart
Future<T> fetchWithRetry<T>(
Future<T> Function() fetch, {
int maxRetries = 3,
Duration initialDelay = const Duration(seconds: 1),
}) async {
for (int i = 0; i < maxRetries; i++) {
try {
return await fetch();
} catch (e) {
if (i == maxRetries - 1) rethrow;
if (!_isRetryableError(e)) rethrow;
final delay = initialDelay * (i + 1);
await Future.delayed(delay);
}
}
throw StateError('Unreachable');
}
bool _isRetryableError(Object error) {
return error is NetworkException ||
error is TimeoutException ||
error is ServerException ||
error is TooManyRequestsException;
}
```
### Retry on Specific Status Codes
```dart
Future<Album> fetchAlbumWithRetry(int id) async {
return await fetchWithRetry(
() => fetchAlbum(id),
maxRetries: 3,
);
}
```
## Error Logging
### Error Logger
```dart
class ErrorLogger {
static void logError(Object error, StackTrace? stackTrace) {
debugPrint('Error: $error');
if (stackTrace != null) {
debugPrint('StackTrace: $stackTrace');
}
// Send to error tracking service
// AnalyticsService.logError(error, stackTrace);
}
}
// Usage
try {
await fetchAlbum();
} catch (e, stackTrace) {
ErrorLogger.logError(e, stackTrace);
}
```
## Best Practices
1. **Handle all status codes** - Don't assume 200 is the only success code
2. **Use specific exception types** - Create custom exceptions for different error types
3. **Parse JSON safely** - Handle type errors gracefully
4. **Implement retry logic** - Retry transient failures with exponential backoff
5. **Set timeouts** - Prevent indefinite hanging requests
6. **Display user-friendly messages** - Show clear error messages in the UI
7. **Log errors** - Track errors for debugging and monitoring
8. **Provide retry options** - Allow users to retry failed operations
```
### references/performance.md
```markdown
# Performance
## Overview
Complete guide for optimizing Flutter networking performance.
## Background Parsing with Isolates
### Using compute() for JSON Parsing
For large JSON responses, parse in a background isolate to prevent UI jank:
```yaml
dependencies:
http: ^1.6.0
```
```dart
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
Future<List<Photo>> fetchPhotos(http.Client client) async {
final response = await client.get(
Uri.parse('https://jsonplaceholder.typicode.com/photos'),
);
if (response.statusCode == 200) {
return compute(parsePhotos, response.body);
} else {
throw Exception('Failed to load photos');
}
}
List<Photo> parsePhotos(String responseBody) {
final parsed = (jsonDecode(responseBody) as List)
.cast<Map<String, dynamic>>();
return parsed.map<Photo>((json) => Photo.fromJson(json)).toList();
}
```
### When to Use compute()
Use `compute()` when:
- JSON response is larger than 10KB
- Parsing takes more than 10ms
- UI jank is noticeable during parsing
Don't use for:
- Small responses (<1KB)
- Simple objects with few fields
- When response speed is critical (isolate overhead)
## Caching
### In-Memory Cache
```dart
class CacheService<T> {
final Map<String, CacheEntry<T>> _cache = {};
final Duration defaultTtl;
CacheService({this.defaultTtl = const Duration(minutes: 5)});
Future<T?> get(String key) async {
final entry = _cache[key];
if (entry == null) return null;
if (DateTime.now().isAfter(entry.expiry)) {
_cache.remove(key);
return null;
}
return entry.value;
}
Future<void> set(String key, T value, {Duration? ttl}) async {
_cache[key] = CacheEntry(
value: value,
expiry: DateTime.now().add(ttl ?? defaultTtl),
);
}
Future<void> clear() async {
_cache.clear();
}
}
class CacheEntry<T> {
final T value;
final DateTime expiry;
CacheEntry({required this.value, required this.expiry});
}
```
### HTTP Cache Headers
Respect server cache directives:
```dart
Future<Album> fetchAlbum(int id) async {
final response = await http.get(
Uri.parse('https://api.example.com/albums/$id'),
);
final cacheControl = response.headers['cache-control'];
if (cacheControl != null) {
// Parse and respect cache headers
}
return Album.fromJson(jsonDecode(response.body));
}
```
## Request Batching
### Batching Multiple Requests
```dart
Future<List<Album>> fetchAlbumsBatch(List<int> ids) async {
final futures = ids.map((id) => fetchAlbum(id));
return await Future.wait(futures);
}
// Usage
final albums = await fetchAlbumsBatch([1, 2, 3, 4, 5]);
```
### Request Deduplication
```dart
class RequestDeduplicator<T> {
final Map<String, Future<T>> _pendingRequests = {};
Future<T> fetch(String key, Future<T> Function() fetchFn) async {
if (_pendingRequests.containsKey(key)) {
return await _pendingRequests[key]!;
}
final future = fetchFn();
_pendingRequests[key] = future;
try {
return await future;
} finally {
_pendingRequests.remove(key);
}
}
}
// Usage
final deduplicator = RequestDeduplicator<Album>();
final album1 = await deduplicator.fetch(
'album-1',
() => fetchAlbum(1),
);
final album2 = await deduplicator.fetch(
'album-1', // Same key - won't make duplicate request
() => fetchAlbum(1),
);
```
## Pagination
### Offset-Based Pagination
```dart
Future<List<Album>> fetchAlbumsPage({
int offset = 0,
int limit = 20,
}) async {
final response = await http.get(
Uri.parse('https://api.example.com/albums')
.replace(queryParameters: {
'offset': offset.toString(),
'limit': limit.toString(),
}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as List;
return data.map((json) => Album.fromJson(json)).toList();
} else {
throw Exception('Failed to load albums');
}
}
```
### Cursor-Based Pagination
```dart
Future<PageResult<Album>> fetchAlbumsPage({
String? cursor,
int limit = 20,
}) async {
final uri = Uri.parse('https://api.example.com/albums')
.replace(queryParameters: {
'limit': limit.toString(),
if (cursor != null) 'cursor': cursor,
});
final response = await http.get(uri);
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as Map<String, dynamic>;
return PageResult(
items: (data['items'] as List)
.map((json) => Album.fromJson(json))
.toList(),
nextCursor: data['nextCursor'] as String?,
);
} else {
throw Exception('Failed to load albums');
}
}
class PageResult<T> {
final List<T> items;
final String? nextCursor;
PageResult({required this.items, required this.nextCursor});
}
```
## Compression
### Enable Compression
```dart
Future<Album> fetchAlbum(int id) async {
final response = await http.get(
Uri.parse('https://api.example.com/albums/$id'),
headers: {
'Accept-Encoding': 'gzip, deflate, br',
},
);
return Album.fromJson(jsonDecode(response.body));
}
```
## Connection Pooling
### Reuse HTTP Client
Create a single HTTP client instance for the app:
```dart
class HttpClient {
static final HttpClient _instance = HttpClient._internal();
factory HttpClient() => _instance;
final http.Client _client = http.Client();
HttpClient._internal();
http.Client get client => _client;
void dispose() {
_client.close();
}
}
// Usage
final httpClient = HttpClient();
final response = await httpClient.client.get(url);
```
## Optimistic UI
### Optimistic Updates
```dart
class AlbumRepository {
final CacheService<Album> _cache;
Future<Album> updateAlbum(int id, String title) async {
// Optimistic update
final cachedAlbum = await _cache.get('album-$id');
final updatedAlbum = Album(
userId: cachedAlbum!.userId,
id: cachedAlbum.id,
title: title,
);
await _cache.set('album-$id', updatedAlbum);
try {
final networkAlbum = await _updateAlbumOnServer(id, title);
await _cache.set('album-$id', networkAlbum);
return networkAlbum;
} catch (e) {
// Revert on error
await _cache.set('album-$id', cachedAlbum);
rethrow;
}
}
Future<Album> _updateAlbumOnServer(int id, String title) async {
final response = await http.put(
Uri.parse('https://api.example.com/albums/$id'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'title': title}),
);
if (response.statusCode == 200) {
return Album.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to update album');
}
}
}
```
## Lazy Loading
### Lazy Load Images
```dart
class NetworkImageLoader {
static const int _maxCacheSize = 100;
static final Map<String, Uint8List> _imageCache = {};
static Future<Uint8List> loadImage(String url) async {
if (_imageCache.containsKey(url)) {
return _imageCache[url]!;
}
final response = await http.get(Uri.parse(url));
final bytes = response.bodyBytes;
if (_imageCache.length >= _maxCacheSize) {
_imageCache.clear();
}
_imageCache[url] = bytes;
return bytes;
}
}
```
## Performance Monitoring
### Request Timing
```dart
class RequestTimer {
final Stopwatch _stopwatch = Stopwatch();
T timeRequest<T>(String name, T Function() request) {
_stopwatch.reset();
_stopwatch.start();
try {
return request();
} finally {
_stopwatch.stop();
debugPrint('$name took ${_stopwatch.elapsedMilliseconds}ms');
}
}
}
// Usage
final timer = RequestTimer();
final album = timer.timeRequest('fetchAlbum', () => fetchAlbum(1));
```
## Best Practices
1. **Use compute() for large JSON** - Parse large responses in isolates
2. **Implement caching** - Reduce network requests with in-memory cache
3. **Batch requests** - Combine multiple requests when possible
4. **Deduplicate requests** - Avoid duplicate in-flight requests
5. **Use pagination** - Load data in chunks for large datasets
6. **Enable compression** - Use gzip for request/response compression
7. **Reuse HTTP client** - Create single client instance for app
8. **Optimistic UI** - Update UI immediately, rollback on error
9. **Lazy load resources** - Load images and resources on demand
10. **Monitor performance** - Track request times for optimization
```