- Sử dụng
httppackage cho REST cơ bản. - Sử dụng
Diovới interceptor, retry, timeout. - Parse JSON: manual +
json_serializablecodegen. - Pattern Repository: tách HTTP khỏi widget.
- Error handling: network, parse, business.
- Timeout & retry strategy.
retrofitpackage: type-safe API client.- Upload file (multipart).
- WebSocket cơ bản với
web_socket_channel.
1. http package — basic
flutter pub add http
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<List<User>> fetchUsers() async {
final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/users'));
if (response.statusCode == 200) {
final json = jsonDecode(response.body) as List;
return json.map((j) => User.fromJson(j)).toList();
} else {
throw Exception('Failed: ${response.statusCode}');
}
}
2. Response handling
final response = await http.get(uri);
response.statusCode; // int: 200, 404, 500...
response.body; // String
response.bodyBytes; // Uint8List — cho binary
response.headers; // Map<String, String>
response.contentLength;
3. JSON decode
import 'dart:convert';
final raw = response.body; // '{"id": 1, "name": "Việt"}'
// jsonDecode trả dynamic — phải cast
final obj = jsonDecode(raw) as Map<String, dynamic>;
print(obj['name']); // 'Việt'
// List response
final list = jsonDecode(raw) as List;
final users = list.map((j) => User.fromJson(j as Map<String, dynamic>)).toList();
4. Model class manual
class User {
final int id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
factory User.fromJson(Map<String, dynamic> json) => User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
);
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'email': email,
};
}
5. JSON codegen — json_serializable
flutter pub add json_annotation
flutter pub add --dev build_runner json_serializable
// user.dart
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart'; // codegen output
@JsonSerializable()
class User {
final int id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
# Generate
dart run build_runner build
# Hoặc watch mode (auto regenerate khi save)
dart run build_runner watch
6. freezed + json_serializable — pattern modern
freezed sinh thêm copyWith, ==, hashCode, toString — pattern chuẩn cho immutable model.
flutter pub add freezed_annotation json_annotation
flutter pub add --dev build_runner freezed json_serializable
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
part 'user.g.dart';
@freezed
class User with _$User {
const factory User({
required int id,
required String name,
required String email,
@Default(false) bool premium,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
// Auto có:
// - copyWith
// - == + hashCode
// - toString
// - fromJson / toJson
7. POST / PUT / DELETE
// POST với body JSON
final response = await http.post(
Uri.parse('$baseUrl/users'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'name': 'Việt', 'email': 'v@x.com'}),
);
// PUT — update
await http.put(Uri.parse('$baseUrl/users/1'), headers: ..., body: ...);
// DELETE
await http.delete(Uri.parse('$baseUrl/users/1'));
8. Dio — mạnh hơn http
flutter pub add dio
import 'package:dio/dio.dart';
final dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
headers: {'Accept': 'application/json'},
));
// GET
final r = await dio.get('/users');
print(r.data); // auto parse JSON, không cần jsonDecode
// POST
await dio.post('/users', data: {'name': 'Việt'});
// Query param
await dio.get('/users', queryParameters: {'page': 1, 'limit': 10});
9. Dio interceptor
// Auth interceptor — auto add Authorization header
class AuthInterceptor extends Interceptor {
final String Function() getToken;
AuthInterceptor(this.getToken);
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
final token = getToken();
options.headers['Authorization'] = 'Bearer $token';
handler.next(options);
}
}
// Log interceptor — built-in
dio.interceptors.add(LogInterceptor(
requestBody: true,
responseBody: true,
));
dio.interceptors.add(AuthInterceptor(() => tokenService.token));
Retry interceptor — exponential backoff
class RetryInterceptor extends Interceptor {
final Dio dio;
RetryInterceptor(this.dio);
@override
Future<void> onError(DioException err, ErrorInterceptorHandler handler) async {
final options = err.requestOptions;
final retryCount = (options.extra['retryCount'] as int?) ?? 0;
if (retryCount < 3 && (err.response?.statusCode ?? 0) >= 500) {
options.extra['retryCount'] = retryCount + 1;
await Future.delayed(Duration(seconds: 1 << retryCount)); // 1s, 2s, 4s
try {
final r = await dio.fetch(options);
return handler.resolve(r);
} catch (e) {
return handler.next(err);
}
}
handler.next(err);
}
}
Hoặc dùng package dio_smart_retry sẵn.
10. CancelToken — cancel khi widget unmount
class _UserPageState extends State<UserPage> {
final _cancelToken = CancelToken();
Future<void> _load() async {
try {
final r = await dio.get('/users', cancelToken: _cancelToken);
if (!mounted) return;
setState(() => _users = r.data);
} on DioException catch (e) {
if (CancelToken.isCancel(e)) return;
// handle other errors
}
}
@override
void dispose() {
_cancelToken.cancel('Widget disposed');
super.dispose();
}
}
11. Repository pattern
// Interface — Bloc/Cubit depend trên này
abstract class UserRepository {
Future<User> fetchUser(int id);
Future<List<User>> fetchAll();
}
// Impl HTTP
class UserRepositoryImpl implements UserRepository {
final Dio _dio;
UserRepositoryImpl(this._dio);
@override
Future<User> fetchUser(int id) async {
try {
final r = await _dio.get('/users/$id');
return User.fromJson(r.data);
} on DioException catch (e) {
throw ApiException(e.response?.statusCode ?? 0, e.message ?? '');
}
}
@override
Future<List<User>> fetchAll() async {
final r = await _dio.get('/users');
return (r.data as List).map((j) => User.fromJson(j)).toList();
}
}
// Inject vào Bloc (Ch 7)
class UserBloc extends Cubit<UserState> {
final UserRepository _repo;
UserBloc(this._repo) : super(UserInitial());
Future<void> load(int id) async {
emit(UserLoading());
try {
final u = await _repo.fetchUser(id);
emit(UserLoaded(u));
} on ApiException catch (e) {
emit(UserError(e.message));
}
}
}
- Test: mock
UserRepositoryinterface, test Bloc với mock data — không cần HTTP thật. - Swap source: đổi HTTP → GraphQL → local DB không cần đổi Bloc/UI.
- Cache layer: wrap repo với caching impl, transparent với caller.
- Tách concern: Bloc không biết JSON shape, không biết Dio.
12. Custom ApiException
class ApiException implements Exception {
final int statusCode;
final String message;
final String? endpoint;
const ApiException(this.statusCode, this.message, [this.endpoint]);
@override
String toString() => 'ApiException($statusCode) $message [$endpoint]';
}
// Trong catch DioException — convert thành ApiException
try {
final r = await dio.get('/users/$id');
return User.fromJson(r.data);
} on DioException catch (e) {
throw ApiException(
e.response?.statusCode ?? 0,
e.response?.data?['message'] ?? e.message,
'/users/$id',
);
}
13. Timeout pattern
// Dio — option
Dio(BaseOptions(
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
));
// http package — qua .timeout
try {
final r = await http.get(uri).timeout(const Duration(seconds: 10));
} on TimeoutException {
// handle
}
14. retrofit — annotation-driven client
flutter pub add retrofit dio
flutter pub add --dev retrofit_generator build_runner
import 'package:retrofit/retrofit.dart';
import 'package:dio/dio.dart';
part 'api_client.g.dart';
@RestApi(baseUrl: 'https://api.example.com')
abstract class ApiClient {
factory ApiClient(Dio dio, {String baseUrl}) = _ApiClient;
@GET('/users')
Future<List<User>> getUsers();
@GET('/users/{id}')
Future<User> getUser(@Path() int id);
@POST('/users')
Future<User> createUser(@Body() User user);
}
// Codegen sinh impl ApiClient với Dio bên dưới
final api = ApiClient(Dio());
final users = await api.getUsers();
15. Upload file — Multipart
final formData = FormData.fromMap({
'avatar': await MultipartFile.fromFile(localPath, filename: 'avatar.jpg'),
'name': 'Việt',
});
final r = await dio.post(
'/upload',
data: formData,
onSendProgress: (sent, total) {
print('${(sent / total * 100).toStringAsFixed(1)}%');
},
);
16. WebSocket — web_socket_channel
flutter pub add web_socket_channel
import 'package:web_socket_channel/web_socket_channel.dart';
final channel = WebSocketChannel.connect(
Uri.parse('wss://echo.websocket.events'),
);
// Listen incoming
channel.stream.listen(
(message) => print('Received: $message'),
onError: (e) => print('Error: $e'),
onDone: () => print('Closed'),
);
// Send message
channel.sink.add('Hello!');
// Close
channel.sink.close();
17. Pitfall: jsonDecode lớn block UI
JSON 10MB parse mất hàng trăm ms — main isolate block, UI jank.
// ❌ Sai — block main
final data = jsonDecode(largeJson);
// ✅ Đúng — parse trong isolate khác
final data = await compute(jsonDecode, largeJson);
// Hoặc convert sang model trong isolate
List<User> parseUsers(String raw) {
final list = jsonDecode(raw) as List;
return list.map((j) => User.fromJson(j)).toList();
}
final users = await compute(parseUsers, response.body);
18. Bài tập
List user từ JSONPlaceholder
App list user từ https://jsonplaceholder.typicode.com/users. Pull-to-refresh.
Error state với retry button.
💡 Gợi ý đáp án
Dùng RefreshIndicator wrap ListView. State: loading, success, error.
Detail page
Tap user → detail page fetch /users/:id. Loading + error state đầy đủ.
💡 Gợi ý đáp án
Navigate qua go_router với path param. Hoặc Navigator 1.0 với arguments.
Migrate http → Dio + interceptor
Migrate bài 1 từ http sang dio. Add LogInterceptor để log request/response.
💡 Gợi ý đáp án
Tham khảo section 8-9. dio.interceptors.add(LogInterceptor(requestBody: true, responseBody: true)).
Repository pattern + test mock
Implement repository: UserRepository interface, UserRepositoryImpl(Dio) concrete.
UserBloc inject. Test UserBloc với mock repo.
💡 Gợi ý đáp án
Mock với mocktail: class MockRepo extends Mock implements UserRepository {}. Chương 14 đi sâu.
JWT auth flow
Add JWT auth flow: login → save token (SharedPreferences, Ch 11) → interceptor add header. Handle 401 → refresh token retry.
💡 Gợi ý đáp án
AuthInterceptor onRequest add Bearer. RetryInterceptor on 401 → call refresh → retry original request.
19. Quiz
http.get return type?
Xem đáp án
Đáp án: Future<http.Response>. Response có statusCode, body, headers.
jsonDecode string JSON 10MB — chuyện gì?
Xem đáp án
Đáp án: Block main isolate, UI jank. Fix: compute(jsonDecode, body) để parse ở isolate khác.
Dio interceptor onError — handler.next vs handler.reject?
Xem đáp án
Đáp án: .next tiếp tục chain (lan tỏa error). .reject stop chain, throw error sớm. .resolve swap thành response thành công.
freezed model auto sinh method nào?
Xem đáp án
Đáp án: copyWith, ==, hashCode, toString, optional toJson/fromJson (qua json_serializable). Pattern matching khi dùng sealed/union.
CancelToken use case?
Xem đáp án
Đáp án: Cancel request khi widget unmount, tránh setState after dispose. Hoặc khi user search nhanh, cancel request cũ.
Retry với 401 (unauthorized) — strategy?
Xem đáp án
Đáp án: Refresh access token bằng refresh token. Retry original request với token mới. Refresh fail → logout user.
Multipart upload Dio — class chính?
Xem đáp án
Đáp án: FormData + MultipartFile.fromFile. Pass vào data: param.
http follow redirect (3xx) tự không?
Xem đáp án
Đáp án: Có, mặc định. Disable: tạo Request object trực tiếp với followRedirects = false.
20. Tổng kết
- ✅
httpđủ cho project nhỏ.diocho project lớn cần interceptor. - ✅ JSON: manual fromJson/toJson hoặc
json_serializablecodegen. - ✅
freezed+json_serializable= pattern modern cho immutable model. - ✅ Dio interceptor: auth, log, retry.
- ✅ CancelToken cho cancel + cleanup.
- ✅ Repository pattern: tách HTTP/JSON khỏi Bloc.
- ✅ Custom
ApiExceptionchuẩn cho error handling. - ✅ Timeout + retry exponential backoff.
- ✅
retrofitcodegen API client từ annotation. - ✅ Multipart upload qua FormData + MultipartFile.
- ✅ WebSocket với
web_socket_channel. - ✅ JSON lớn →
compute()ra isolate khác.
21. Kết nối
- Ch 7-8: Bloc/Provider gọi repository.
- Ch 11: cache response local (offline-first).
- Ch 14: test repo + Bloc với mock.