Skip to main content

Hướng Dẫn Call API Bằng Dio Có Kèm Interceptor và Retry Logic

· 9 min read

Trong bài viết này, chúng ta sẽ học cách sử dụng Dio để gọi API trong Flutter với các tính năng nâng cao như interceptors và retry logic. Đây là những kỹ thuật quan trọng để xây dựng ứng dụng Flutter chuyên nghiệp.


1️⃣ Cài Đặt và Cấu Hình Cơ Bản

1.1 Cài Đặt Dio

Thêm vào pubspec.yaml:

dependencies:
dio: ^5.4.0
dio_retry: ^2.0.0 # Cho retry logic

1.2 Tạo Dio Instance

import 'package:dio/dio.dart';

class ApiClient {
late Dio _dio;

ApiClient() {
_dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: Duration(seconds: 5),
receiveTimeout: Duration(seconds: 3),
sendTimeout: Duration(seconds: 3),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
));
}

Dio get dio => _dio;
}

2️⃣ Interceptors - Xử Lý Requests và Responses

2.1 Log Interceptor

Ghi log tất cả requests và responses:

import 'package:dio/dio.dart';

void setupLogInterceptor(Dio dio) {
dio.interceptors.add(LogInterceptor(
request: true,
requestHeader: true,
requestBody: true,
responseHeader: true,
responseBody: true,
error: true,
logPrint: (obj) {
// Sử dụng logger của bạn (ví dụ: logger package)
print(obj);
},
));
}

2.2 Authentication Interceptor

Tự động thêm token vào mọi request:

class AuthInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
// Lấy token từ storage
final token = TokenStorage.getToken();

if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}

handler.next(options);
}
}

// Sử dụng
dio.interceptors.add(AuthInterceptor());

2.3 Refresh Token Interceptor

Tự động refresh token khi hết hạn:

class RefreshTokenInterceptor extends Interceptor {
final Dio refreshDio;

RefreshTokenInterceptor(this.refreshDio);

@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
// Nếu lỗi 401 (Unauthorized)
if (err.response?.statusCode == 401) {
try {
// Refresh token
final newToken = await _refreshToken();

if (newToken != null) {
// Lưu token mới
TokenStorage.saveToken(newToken);

// Retry request với token mới
final opts = err.requestOptions;
opts.headers['Authorization'] = 'Bearer $newToken';

final response = await refreshDio.request(
opts.path,
options: Options(
method: opts.method,
headers: opts.headers,
),
data: opts.data,
queryParameters: opts.queryParameters,
);

return handler.resolve(response);
}
} catch (e) {
// Nếu refresh token thất bại, đăng xuất
await AuthService.logout();
return handler.reject(err);
}
}

return handler.next(err);
}

Future<String?> _refreshToken() async {
try {
final refreshToken = TokenStorage.getRefreshToken();
final response = await refreshDio.post(
'/auth/refresh',
data: {'refresh_token': refreshToken},
);

return response.data['access_token'];
} catch (e) {
return null;
}
}
}

// Sử dụng
final refreshDio = Dio();
dio.interceptors.add(RefreshTokenInterceptor(refreshDio));

2.4 Error Handling Interceptor

Xử lý lỗi tập trung:

class ErrorInterceptor extends Interceptor {
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
String errorMessage = 'Đã xảy ra lỗi';

switch (err.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
errorMessage = 'Kết nối timeout. Vui lòng thử lại.';
break;
case DioExceptionType.badResponse:
final statusCode = err.response?.statusCode;
switch (statusCode) {
case 400:
errorMessage = 'Yêu cầu không hợp lệ';
break;
case 401:
errorMessage = 'Chưa đăng nhập hoặc token đã hết hạn';
break;
case 403:
errorMessage = 'Không có quyền truy cập';
break;
case 404:
errorMessage = 'Không tìm thấy tài nguyên';
break;
case 500:
errorMessage = 'Lỗi server. Vui lòng thử lại sau.';
break;
default:
errorMessage = 'Lỗi server: $statusCode';
}
break;
case DioExceptionType.cancel:
errorMessage = 'Request đã bị hủy';
break;
case DioExceptionType.unknown:
errorMessage = 'Không có kết nối internet';
break;
default:
errorMessage = err.message ?? 'Đã xảy ra lỗi';
}

// Hiển thị thông báo lỗi (sử dụng snackbar, dialog, etc.)
ErrorHandler.showError(errorMessage);

handler.next(err);
}
}

// Sử dụng
dio.interceptors.add(ErrorInterceptor());

2.5 Request/Response Transformer Interceptor

Transform dữ liệu trước khi gửi/nhận:

class DataTransformerInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
// Transform request data
if (options.data is Map) {
// Thêm timestamp, device info, etc.
options.data = {
...options.data as Map,
'timestamp': DateTime.now().toIso8601String(),
'device_id': DeviceInfo.getDeviceId(),
};
}

handler.next(options);
}

@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
// Transform response data
if (response.data is Map) {
final data = response.data as Map;

// Unwrap nested data nếu cần
if (data.containsKey('data')) {
response.data = data['data'];
}
}

handler.next(response);
}
}

// Sử dụng
dio.interceptors.add(DataTransformerInterceptor());

3️⃣ Retry Logic - Tự Động Thử Lại Khi Thất Bại

3.1 Sử dụng dio_retry Package

import 'package:dio/dio.dart';
import 'package:dio_retry/dio_retry.dart';

void setupRetryInterceptor(Dio dio) {
dio.interceptors.add(
RetryInterceptor(
dio: dio,
options: RetryOptions(
retries: 3, // Số lần retry
retryInterval: Duration(seconds: 2), // Khoảng thời gian giữa các lần retry
exponentialBackoff: true, // Tăng dần thời gian chờ
retryableExtraStatuses: [401, 403], // Retry với các status code này
),
),
);
}

3.2 Custom Retry Logic

Tự xây dựng retry logic:

class RetryInterceptor extends Interceptor {
final int maxRetries;
final Duration retryDelay;

RetryInterceptor({
this.maxRetries = 3,
this.retryDelay = const Duration(seconds: 2),
});

@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
if (_shouldRetry(err)) {
final retryCount = err.requestOptions.extra['retryCount'] ?? 0;

if (retryCount < maxRetries) {
// Tăng retry count
err.requestOptions.extra['retryCount'] = retryCount + 1;

// Đợi trước khi retry
await Future.delayed(retryDelay * (retryCount + 1));

try {
// Retry request
final response = await dio.request(
err.requestOptions.path,
options: Options(
method: err.requestOptions.method,
headers: err.requestOptions.headers,
),
data: err.requestOptions.data,
queryParameters: err.requestOptions.queryParameters,
extra: err.requestOptions.extra,
);

return handler.resolve(response);
} catch (e) {
// Nếu vẫn lỗi, tiếp tục retry hoặc reject
if (retryCount + 1 < maxRetries) {
return onError(err, handler);
}
}
}
}

handler.next(err);
}

bool _shouldRetry(DioException err) {
// Chỉ retry với một số loại lỗi nhất định
return err.type == DioExceptionType.connectionTimeout ||
err.type == DioExceptionType.receiveTimeout ||
err.type == DioExceptionType.sendTimeout ||
(err.response?.statusCode != null &&
err.response!.statusCode! >= 500);
}
}

// Sử dụng
dio.interceptors.add(RetryInterceptor(maxRetries: 3));

4️⃣ Ví Dụ Hoàn Chỉnh: API Service Với Interceptors và Retry

import 'package:dio/dio.dart';
import 'package:dio_retry/dio_retry.dart';

class ApiService {
late Dio _dio;
static final ApiService _instance = ApiService._internal();

factory ApiService() => _instance;

ApiService._internal() {
_dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: Duration(seconds: 5),
receiveTimeout: Duration(seconds: 3),
sendTimeout: Duration(seconds: 3),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
));

_setupInterceptors();
}

void _setupInterceptors() {
// 1. Log Interceptor
_dio.interceptors.add(LogInterceptor(
requestBody: true,
responseBody: true,
error: true,
));

// 2. Auth Interceptor
_dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
final token = _getToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
return handler.next(options);
},
));

// 3. Retry Interceptor
_dio.interceptors.add(
RetryInterceptor(
dio: _dio,
options: RetryOptions(
retries: 3,
retryInterval: Duration(seconds: 2),
exponentialBackoff: true,
),
),
);

// 4. Error Interceptor
_dio.interceptors.add(InterceptorsWrapper(
onError: (error, handler) {
_handleError(error);
return handler.next(error);
},
));
}

String? _getToken() {
// Lấy token từ secure storage
// return SecureStorage.getToken();
return null;
}

void _handleError(DioException error) {
// Xử lý lỗi tập trung
// Có thể hiển thị snackbar, log, etc.
print('API Error: ${error.message}');
}

// GET request
Future<Response<T>> get<T>(
String path, {
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
try {
return await _dio.get<T>(
path,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
} on DioException catch (e) {
throw _mapException(e);
}
}

// POST request
Future<Response<T>> post<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
try {
return await _dio.post<T>(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
} on DioException catch (e) {
throw _mapException(e);
}
}

// PUT request
Future<Response<T>> put<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
try {
return await _dio.put<T>(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
} on DioException catch (e) {
throw _mapException(e);
}
}

// DELETE request
Future<Response<T>> delete<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
try {
return await _dio.delete<T>(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
} on DioException catch (e) {
throw _mapException(e);
}
}

Exception _mapException(DioException error) {
switch (error.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return TimeoutException('Request timeout');
case DioExceptionType.badResponse:
return ServerException(
'Server error: ${error.response?.statusCode}',
error.response?.statusCode,
);
case DioExceptionType.cancel:
return CancelException('Request cancelled');
default:
return NetworkException('Network error: ${error.message}');
}
}
}

// Custom exceptions
class TimeoutException implements Exception {
final String message;
TimeoutException(this.message);
}

class ServerException implements Exception {
final String message;
final int? statusCode;
ServerException(this.message, this.statusCode);
}

class CancelException implements Exception {
final String message;
CancelException(this.message);
}

class NetworkException implements Exception {
final String message;
NetworkException(this.message);
}

5️⃣ Sử Dụng API Service

// Trong repository hoặc service
class UserRepository {
final ApiService _apiService = ApiService();

Future<User> getUser(int id) async {
try {
final response = await _apiService.get<Map<String, dynamic>>('/users/$id');
return User.fromJson(response.data!);
} catch (e) {
throw Exception('Failed to get user: $e');
}
}

Future<List<User>> getUsers() async {
try {
final response = await _apiService.get<List<dynamic>>('/users');
return (response.data as List)
.map((json) => User.fromJson(json))
.toList();
} catch (e) {
throw Exception('Failed to get users: $e');
}
}

Future<User> createUser(User user) async {
try {
final response = await _apiService.post<Map<String, dynamic>>(
'/users',
data: user.toJson(),
);
return User.fromJson(response.data!);
} catch (e) {
throw Exception('Failed to create user: $e');
}
}
}

6️⃣ Best Practices

6.1 Thứ Tự Interceptors

Thứ tự interceptors quan trọng:

// 1. Log interceptor (đầu tiên để log tất cả)
dio.interceptors.add(LogInterceptor());

// 2. Auth interceptor (thêm token)
dio.interceptors.add(AuthInterceptor());

// 3. Retry interceptor (retry khi lỗi)
dio.interceptors.add(RetryInterceptor());

// 4. Error interceptor (xử lý lỗi cuối cùng)
dio.interceptors.add(ErrorInterceptor());

6.2 Sử Dụng Singleton Pattern

Tạo một instance Dio duy nhất cho toàn bộ app:

class ApiService {
static final ApiService _instance = ApiService._internal();
factory ApiService() => _instance;
ApiService._internal();
}

6.3 Cancel Token Cho Long-Running Requests

final cancelToken = CancelToken();

// Gọi API
_apiService.get('/data', cancelToken: cancelToken);

// Hủy khi không cần thiết (ví dụ: khi widget dispose)
@override
void dispose() {
cancelToken.cancel('Widget disposed');
super.dispose();
}

7️⃣ Kết Luận

Với Dio, interceptors và retry logic, bạn có thể:

Tự động thêm authentication vào mọi request
Xử lý lỗi tập trung và nhất quán
Tự động retry khi request thất bại
Log requests/responses để debug
Transform data trước khi gửi/nhận

💡 Lời khuyên: Sử dụng interceptors để tách biệt concerns. Mỗi interceptor nên có một nhiệm vụ cụ thể. Điều này giúp code dễ đọc, dễ test và dễ bảo trì.


🎓 Học Sâu Hơn Về Flutter

Muốn master Flutter, API Integration, và các best practices? Tham gia các khóa học tại Hướng Nghiệp Dữ Liệu:

📚 Khóa Học Liên Quan:


📝 Bài viết này được biên soạn bởi đội ngũ Hướng Nghiệp Dữ Liệu. Để cập nhật thêm về Flutter, Dio và các best practices trong phát triển ứng dụng di động, hãy theo dõi blog của chúng tôi.

Hướng Dẫn Cloud Firestore CRUD Đầy Đủ

· 10 min read

Cloud Firestore là một NoSQL database mạnh mẽ của Firebase. Bài viết này sẽ hướng dẫn bạn cách thực hiện CRUD operations (Create, Read, Update, Delete) với Firestore trong Flutter một cách đầy đủ.


1️⃣ Cài Đặt và Setup

1.1 Thêm Dependencies

Thêm vào pubspec.yaml:

dependencies:
firebase_core: ^2.24.2
cloud_firestore: ^4.13.6

Sau đó chạy:

flutter pub get

1.2 Khởi Tạo Firestore

import 'package:firebase_core/firebase_core.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'firebase_options.dart';

void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(MyApp());
}

1.3 Cấu Hình Firestore Rules

Trong Firebase Console > Firestore Database > Rules:

rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Cho phép đọc/ghi nếu đã đăng nhập
match /{document=**} {
allow read, write: if request.auth != null;
}

// Hoặc public read, authenticated write
match /posts/{postId} {
allow read: if true;
allow write: if request.auth != null;
}
}
}

2️⃣ Cấu Trúc Dữ Liệu Firestore

2.1 Collections và Documents

Firestore có cấu trúc:

  • Collection: Tương tự như table trong SQL
  • Document: Tương tự như row trong SQL
  • Field: Tương tự như column trong SQL
users (collection)
├── user1 (document)
│ ├── name: "Nguyễn Văn A"
│ ├── email: "a@example.com"
│ └── age: 25
└── user2 (document)
├── name: "Trần Thị B"
├── email: "b@example.com"
└── age: 30

2.2 Model Class

class User {
final String? id;
final String name;
final String email;
final int age;
final DateTime createdAt;

User({
this.id,
required this.name,
required this.email,
required this.age,
DateTime? createdAt,
}) : createdAt = createdAt ?? DateTime.now();

// Convert to Map for Firestore
Map<String, dynamic> toMap() {
return {
'name': name,
'email': email,
'age': age,
'createdAt': Timestamp.fromDate(createdAt),
};
}

// Create from Firestore document
factory User.fromMap(String id, Map<String, dynamic> map) {
return User(
id: id,
name: map['name'] as String,
email: map['email'] as String,
age: map['age'] as int,
createdAt: (map['createdAt'] as Timestamp).toDate(),
);
}

// Create from Firestore document snapshot
factory User.fromDocument(DocumentSnapshot doc) {
return User.fromMap(doc.id, doc.data() as Map<String, dynamic>);
}
}

3️⃣ CREATE - Tạo Dữ Liệu

3.1 Thêm Document với Auto ID

import 'package:cloud_firestore/cloud_firestore.dart';

class FirestoreService {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;

// Thêm document với auto-generated ID
Future<String> addUser(User user) async {
try {
final docRef = await _firestore
.collection('users')
.add(user.toMap());

return docRef.id;
} catch (e) {
throw Exception('Error adding user: $e');
}
}
}

3.2 Thêm Document với Custom ID

// Thêm document với ID tùy chỉnh
Future<void> addUserWithId(String userId, User user) async {
try {
await _firestore
.collection('users')
.doc(userId)
.set(user.toMap());
} catch (e) {
throw Exception('Error adding user: $e');
}
}

3.3 Thêm Document với Merge

// Merge: Nếu document đã tồn tại, chỉ update các field được chỉ định
Future<void> addUserWithMerge(String userId, Map<String, dynamic> data) async {
try {
await _firestore
.collection('users')
.doc(userId)
.set(data, SetOptions(merge: true));
} catch (e) {
throw Exception('Error adding user: $e');
}
}

3.4 Thêm Nested Document

// Thêm document vào subcollection
Future<void> addPostToUser(String userId, Post post) async {
try {
await _firestore
.collection('users')
.doc(userId)
.collection('posts')
.add(post.toMap());
} catch (e) {
throw Exception('Error adding post: $e');
}
}

4️⃣ READ - Đọc Dữ Liệu

4.1 Đọc Single Document

// Đọc một document
Future<User?> getUser(String userId) async {
try {
final doc = await _firestore
.collection('users')
.doc(userId)
.get();

if (doc.exists) {
return User.fromDocument(doc);
}
return null;
} catch (e) {
throw Exception('Error getting user: $e');
}
}

4.2 Đọc Tất Cả Documents

// Đọc tất cả documents trong collection
Future<List<User>> getAllUsers() async {
try {
final snapshot = await _firestore
.collection('users')
.get();

return snapshot.docs
.map((doc) => User.fromDocument(doc))
.toList();
} catch (e) {
throw Exception('Error getting users: $e');
}
}

4.3 Query với Điều Kiện

// Query với where
Future<List<User>> getUsersByAge(int minAge) async {
try {
final snapshot = await _firestore
.collection('users')
.where('age', isGreaterThanOrEqualTo: minAge)
.get();

return snapshot.docs
.map((doc) => User.fromDocument(doc))
.toList();
} catch (e) {
throw Exception('Error getting users: $e');
}
}

// Multiple conditions
Future<List<User>> getUsersByAgeAndName(int age, String name) async {
try {
final snapshot = await _firestore
.collection('users')
.where('age', isEqualTo: age)
.where('name', isEqualTo: name)
.get();

return snapshot.docs
.map((doc) => User.fromDocument(doc))
.toList();
} catch (e) {
throw Exception('Error getting users: $e');
}
}

4.4 Sorting và Limiting

// Sort và limit
Future<List<User>> getUsersSortedByAge({int limit = 10}) async {
try {
final snapshot = await _firestore
.collection('users')
.orderBy('age', descending: false)
.limit(limit)
.get();

return snapshot.docs
.map((doc) => User.fromDocument(doc))
.toList();
} catch (e) {
throw Exception('Error getting users: $e');
}
}

// Pagination
Future<List<User>> getUsersPaginated({
required int limit,
DocumentSnapshot? startAfter,
}) async {
try {
Query query = _firestore
.collection('users')
.orderBy('createdAt', descending: true)
.limit(limit);

if (startAfter != null) {
query = query.startAfterDocument(startAfter);
}

final snapshot = await query.get();

return snapshot.docs
.map((doc) => User.fromDocument(doc))
.toList();
} catch (e) {
throw Exception('Error getting users: $e');
}
}

4.5 Real-time Updates

// Stream để lắng nghe thay đổi real-time
Stream<List<User>> getUsersStream() {
return _firestore
.collection('users')
.snapshots()
.map((snapshot) => snapshot.docs
.map((doc) => User.fromDocument(doc))
.toList());
}

// Stream cho single document
Stream<User?> getUserStream(String userId) {
return _firestore
.collection('users')
.doc(userId)
.snapshots()
.map((doc) => doc.exists ? User.fromDocument(doc) : null);
}

5️⃣ UPDATE - Cập Nhật Dữ Liệu

5.1 Update Toàn Bộ Document

// Update toàn bộ document
Future<void> updateUser(String userId, User user) async {
try {
await _firestore
.collection('users')
.doc(userId)
.set(user.toMap());
} catch (e) {
throw Exception('Error updating user: $e');
}
}

5.2 Update Một Số Fields

// Update chỉ một số fields
Future<void> updateUserFields(
String userId,
Map<String, dynamic> fields,
) async {
try {
await _firestore
.collection('users')
.doc(userId)
.update(fields);
} catch (e) {
throw Exception('Error updating user: $e');
}
}

// Ví dụ sử dụng
await updateUserFields('user123', {
'name': 'Tên mới',
'age': 26,
});

5.3 Increment/Decrement

// Tăng/giảm giá trị số
Future<void> incrementUserAge(String userId) async {
try {
await _firestore
.collection('users')
.doc(userId)
.update({
'age': FieldValue.increment(1),
});
} catch (e) {
throw Exception('Error incrementing age: $e');
}
}

5.4 Array Operations

// Thêm vào array
Future<void> addToArray(String userId, String item) async {
try {
await _firestore
.collection('users')
.doc(userId)
.update({
'tags': FieldValue.arrayUnion([item]),
});
} catch (e) {
throw Exception('Error adding to array: $e');
}
}

// Xóa khỏi array
Future<void> removeFromArray(String userId, String item) async {
try {
await _firestore
.collection('users')
.doc(userId)
.update({
'tags': FieldValue.arrayRemove([item]),
});
} catch (e) {
throw Exception('Error removing from array: $e');
}
}

6️⃣ DELETE - Xóa Dữ Liệu

6.1 Xóa Document

// Xóa document
Future<void> deleteUser(String userId) async {
try {
await _firestore
.collection('users')
.doc(userId)
.delete();
} catch (e) {
throw Exception('Error deleting user: $e');
}
}

6.2 Xóa Field

// Xóa một field
Future<void> deleteUserField(String userId, String fieldName) async {
try {
await _firestore
.collection('users')
.doc(userId)
.update({
fieldName: FieldValue.delete(),
});
} catch (e) {
throw Exception('Error deleting field: $e');
}
}

6.3 Xóa Subcollection

// Xóa tất cả documents trong subcollection
Future<void> deleteUserPosts(String userId) async {
try {
final snapshot = await _firestore
.collection('users')
.doc(userId)
.collection('posts')
.get();

final batch = _firestore.batch();
for (var doc in snapshot.docs) {
batch.delete(doc.reference);
}
await batch.commit();
} catch (e) {
throw Exception('Error deleting posts: $e');
}
}

7️⃣ Transactions và Batch Writes

7.1 Transactions

// Transaction: Đảm bảo atomic operations
Future<void> transferPoints(
String fromUserId,
String toUserId,
int points,
) async {
try {
await _firestore.runTransaction((transaction) async {
// Đọc documents
final fromDoc = await transaction.get(
_firestore.collection('users').doc(fromUserId),
);
final toDoc = await transaction.get(
_firestore.collection('users').doc(toUserId),
);

if (!fromDoc.exists || !toDoc.exists) {
throw Exception('User not found');
}

final fromPoints = fromDoc.data()!['points'] as int;
if (fromPoints < points) {
throw Exception('Insufficient points');
}

// Update
transaction.update(fromDoc.reference, {
'points': FieldValue.increment(-points),
});
transaction.update(toDoc.reference, {
'points': FieldValue.increment(points),
});
});
} catch (e) {
throw Exception('Error transferring points: $e');
}
}

7.2 Batch Writes

// Batch write: Thực hiện nhiều operations cùng lúc
Future<void> batchUpdateUsers(List<String> userIds, Map<String, dynamic> data) async {
try {
final batch = _firestore.batch();

for (var userId in userIds) {
final docRef = _firestore.collection('users').doc(userId);
batch.update(docRef, data);
}

await batch.commit();
} catch (e) {
throw Exception('Error batch updating users: $e');
}
}

8️⃣ Ví Dụ Hoàn Chỉnh: Firestore Service

import 'package:cloud_firestore/cloud_firestore.dart';

class FirestoreService {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;

// CREATE
Future<String> createUser(User user) async {
try {
final docRef = await _firestore
.collection('users')
.add(user.toMap());
return docRef.id;
} catch (e) {
throw Exception('Error creating user: $e');
}
}

// READ - Single
Future<User?> getUser(String userId) async {
try {
final doc = await _firestore
.collection('users')
.doc(userId)
.get();
return doc.exists ? User.fromDocument(doc) : null;
} catch (e) {
throw Exception('Error getting user: $e');
}
}

// READ - All
Future<List<User>> getAllUsers() async {
try {
final snapshot = await _firestore
.collection('users')
.get();
return snapshot.docs
.map((doc) => User.fromDocument(doc))
.toList();
} catch (e) {
throw Exception('Error getting users: $e');
}
}

// READ - Query
Future<List<User>> getUsersByAge(int minAge) async {
try {
final snapshot = await _firestore
.collection('users')
.where('age', isGreaterThanOrEqualTo: minAge)
.orderBy('age')
.get();
return snapshot.docs
.map((doc) => User.fromDocument(doc))
.toList();
} catch (e) {
throw Exception('Error getting users: $e');
}
}

// READ - Stream
Stream<List<User>> getUsersStream() {
return _firestore
.collection('users')
.snapshots()
.map((snapshot) => snapshot.docs
.map((doc) => User.fromDocument(doc))
.toList());
}

// UPDATE
Future<void> updateUser(String userId, Map<String, dynamic> data) async {
try {
await _firestore
.collection('users')
.doc(userId)
.update(data);
} catch (e) {
throw Exception('Error updating user: $e');
}
}

// DELETE
Future<void> deleteUser(String userId) async {
try {
await _firestore
.collection('users')
.doc(userId)
.delete();
} catch (e) {
throw Exception('Error deleting user: $e');
}
}
}

9️⃣ Sử Dụng Với UI

9.1 StreamBuilder

class UsersListWidget extends StatelessWidget {
final FirestoreService _service = FirestoreService();

@override
Widget build(BuildContext context) {
return StreamBuilder<List<User>>(
stream: _service.getUsersStream(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}

if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}

if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Center(child: Text('No users found'));
}

final users = snapshot.data!;
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
title: Text(user.name),
subtitle: Text(user.email),
trailing: Text('Age: ${user.age}'),
);
},
);
},
);
}
}

🔟 Best Practices

10.1 Indexes

  • ✅ Tạo composite indexes cho queries phức tạp
  • ✅ Firestore sẽ tự động suggest indexes

10.2 Security Rules

  • ✅ Luôn validate data trong security rules
  • ✅ Chỉ cho phép authenticated users
  • ✅ Validate data structure

10.3 Performance

  • ✅ Sử dụng pagination cho large collections
  • ✅ Limit số lượng documents trong queries
  • ✅ Sử dụng indexes cho queries

10.4 Error Handling

  • ✅ Luôn handle errors
  • ✅ Hiển thị thông báo cho người dùng
  • ✅ Log errors để debug

1️⃣1️⃣ Kết Luận

Cloud Firestore giúp bạn:

NoSQL database: Linh hoạt, dễ scale
Real-time updates: Stream data tự động
Offline support: Hoạt động offline
Security: Security rules mạnh mẽ
Scalable: Tự động scale

💡 Lời khuyên: Sử dụng StreamBuilder cho real-time data. Luôn validate data và sử dụng security rules. Tạo indexes cho queries phức tạp.


🎓 Học Sâu Hơn Về Flutter

Muốn master Flutter, Firestore, và các best practices? Tham gia các khóa học tại Hướng Nghiệp Dữ Liệu:

📚 Khóa Học Liên Quan:


📝 Bài viết này được biên soạn bởi đội ngũ Hướng Nghiệp Dữ Liệu. Để cập nhật thêm về Flutter, Firestore và các best practices trong phát triển ứng dụng di động, hãy theo dõi blog của chúng tôi.

Hướng Dẫn Firebase Authentication Cho Người Mới

· 11 min read

Firebase Authentication là một dịch vụ xác thực người dùng mạnh mẽ và dễ sử dụng. Bài viết này sẽ hướng dẫn bạn cách tích hợp Firebase Authentication vào Flutter từ đầu đến cuối.


1️⃣ Cài Đặt và Setup

1.1 Tạo Firebase Project

  1. Truy cập Firebase Console
  2. Click "Add project"
  3. Điền tên project và làm theo hướng dẫn
  4. Bật Authentication trong Firebase Console

1.2 Cài Đặt FlutterFire CLI

dart pub global activate flutterfire_cli

1.3 Cấu Hình Firebase cho Flutter

flutterfire configure

Lệnh này sẽ:

  • Tự động tạo file firebase_options.dart
  • Cấu hình cho cả Android và iOS

1.4 Thêm Dependencies

Thêm vào pubspec.yaml:

dependencies:
firebase_core: ^2.24.2
firebase_auth: ^4.15.3
google_sign_in: ^6.1.6 # Cho Google Sign In

Sau đó chạy:

flutter pub get

1.5 Khởi Tạo Firebase

import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';

void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(MyApp());
}

2️⃣ Email/Password Authentication

2.1 Đăng Ký (Sign Up)

import 'package:firebase_auth/firebase_auth.dart';

class AuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;

// Đăng ký với email và password
Future<UserCredential?> signUpWithEmail({
required String email,
required String password,
}) async {
try {
final userCredential = await _auth.createUserWithEmailAndPassword(
email: email,
password: password,
);

// Gửi email xác nhận (optional)
await userCredential.user?.sendEmailVerification();

return userCredential;
} on FirebaseAuthException catch (e) {
print('Sign up error: ${e.message}');
throw _handleAuthException(e);
} catch (e) {
print('Unexpected error: $e');
throw Exception('Đã xảy ra lỗi không xác định');
}
}

String _handleAuthException(FirebaseAuthException e) {
switch (e.code) {
case 'weak-password':
return 'Mật khẩu quá yếu';
case 'email-already-in-use':
return 'Email đã được sử dụng';
case 'invalid-email':
return 'Email không hợp lệ';
default:
return e.message ?? 'Đã xảy ra lỗi';
}
}
}

2.2 Đăng Nhập (Sign In)

// Đăng nhập với email và password
Future<UserCredential?> signInWithEmail({
required String email,
required String password,
}) async {
try {
final userCredential = await _auth.signInWithEmailAndPassword(
email: email,
password: password,
);

return userCredential;
} on FirebaseAuthException catch (e) {
print('Sign in error: ${e.message}');
throw _handleSignInException(e);
} catch (e) {
print('Unexpected error: $e');
throw Exception('Đã xảy ra lỗi không xác định');
}
}

String _handleSignInException(FirebaseAuthException e) {
switch (e.code) {
case 'user-not-found':
return 'Không tìm thấy tài khoản với email này';
case 'wrong-password':
return 'Mật khẩu không đúng';
case 'invalid-email':
return 'Email không hợp lệ';
case 'user-disabled':
return 'Tài khoản đã bị vô hiệu hóa';
default:
return e.message ?? 'Đã xảy ra lỗi';
}
}

2.3 Đăng Xuất (Sign Out)

Future<void> signOut() async {
try {
await _auth.signOut();
} catch (e) {
print('Sign out error: $e');
throw Exception('Không thể đăng xuất');
}
}

2.4 Reset Password

Future<void> resetPassword(String email) async {
try {
await _auth.sendPasswordResetEmail(email: email);
} on FirebaseAuthException catch (e) {
throw _handleAuthException(e);
} catch (e) {
throw Exception('Không thể gửi email reset password');
}
}

3️⃣ Google Sign In

3.1 Cấu Hình Google Sign In

Android

Thêm vào android/app/build.gradle:

dependencies {
implementation 'com.google.android.gms:play-services-auth:20.7.0'
}

Lấy SHA-1 key:

keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android

Thêm SHA-1 vào Firebase Console > Project Settings > Your apps

iOS

Thêm vào ios/Runner/Info.plist:

<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>YOUR_REVERSED_CLIENT_ID</string>
</array>
</dict>
</array>

3.2 Implement Google Sign In

import 'package:google_sign_in/google_sign_in.dart';

class AuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
final GoogleSignIn _googleSignIn = GoogleSignIn();

// Đăng nhập với Google
Future<UserCredential?> signInWithGoogle() async {
try {
// Trigger the authentication flow
final GoogleSignInAccount? googleUser = await _googleSignIn.signIn();

if (googleUser == null) {
// User canceled the sign-in
return null;
}

// Obtain the auth details from the request
final GoogleSignInAuthentication googleAuth =
await googleUser.authentication;

// Create a new credential
final credential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);

// Sign in to Firebase with the Google credential
return await _auth.signInWithCredential(credential);
} on FirebaseAuthException catch (e) {
print('Google sign in error: ${e.message}');
throw Exception('Đăng nhập Google thất bại: ${e.message}');
} catch (e) {
print('Unexpected error: $e');
throw Exception('Đã xảy ra lỗi không xác định');
}
}

// Đăng xuất Google
Future<void> signOutGoogle() async {
await _googleSignIn.signOut();
await _auth.signOut();
}
}

4️⃣ Phone Authentication

4.1 Cấu Hình Phone Auth

  1. Bật Phone Authentication trong Firebase Console
  2. Thêm test phone numbers (cho development)

4.2 Implement Phone Auth

class AuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
String? _verificationId;

// Gửi OTP
Future<void> sendOTP(String phoneNumber) async {
try {
await _auth.verifyPhoneNumber(
phoneNumber: phoneNumber,
verificationCompleted: (PhoneAuthCredential credential) async {
// Auto verification (Android only)
await _auth.signInWithCredential(credential);
},
verificationFailed: (FirebaseAuthException e) {
throw Exception('Xác thực thất bại: ${e.message}');
},
codeSent: (String verificationId, int? resendToken) {
_verificationId = verificationId;
},
codeAutoRetrievalTimeout: (String verificationId) {
_verificationId = verificationId;
},
timeout: Duration(seconds: 60),
);
} catch (e) {
throw Exception('Không thể gửi OTP: $e');
}
}

// Xác thực OTP
Future<UserCredential?> verifyOTP(String smsCode) async {
try {
if (_verificationId == null) {
throw Exception('Verification ID không tồn tại');
}

final credential = PhoneAuthProvider.credential(
verificationId: _verificationId!,
smsCode: smsCode,
);

return await _auth.signInWithCredential(credential);
} on FirebaseAuthException catch (e) {
throw Exception('Mã OTP không đúng: ${e.message}');
} catch (e) {
throw Exception('Xác thực thất bại: $e');
}
}
}

5️⃣ Quản Lý User Session

5.1 Auth State Stream

class AuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;

// Stream để theo dõi trạng thái đăng nhập
Stream<User?> get authStateChanges => _auth.authStateChanges();

// User hiện tại
User? get currentUser => _auth.currentUser;

// Kiểm tra đăng nhập
bool get isSignedIn => _auth.currentUser != null;

// Lấy thông tin user
User? get user => _auth.currentUser;
}

5.2 Auth Wrapper Widget

import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';

class AuthWrapper extends StatelessWidget {
final Widget signedInWidget;
final Widget signedOutWidget;

const AuthWrapper({
Key? key,
required this.signedInWidget,
required this.signedOutWidget,
}) : super(key: key);

@override
Widget build(BuildContext context) {
return StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}

if (snapshot.hasData) {
return signedInWidget;
} else {
return signedOutWidget;
}
},
);
}
}

// Sử dụng
void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: AuthWrapper(
signedInWidget: HomeScreen(),
signedOutWidget: LoginScreen(),
),
);
}
}

6️⃣ User Profile Management

6.1 Cập Nhật Thông Tin User

class AuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;

// Cập nhật display name
Future<void> updateDisplayName(String name) async {
try {
await _auth.currentUser?.updateDisplayName(name);
await _auth.currentUser?.reload();
} catch (e) {
throw Exception('Không thể cập nhật tên: $e');
}
}

// Cập nhật email
Future<void> updateEmail(String newEmail) async {
try {
await _auth.currentUser?.verifyBeforeUpdateEmail(newEmail);
} catch (e) {
throw Exception('Không thể cập nhật email: $e');
}
}

// Cập nhật password
Future<void> updatePassword(String newPassword) async {
try {
await _auth.currentUser?.updatePassword(newPassword);
} catch (e) {
throw Exception('Không thể cập nhật mật khẩu: $e');
}
}

// Cập nhật photo URL
Future<void> updatePhotoURL(String photoURL) async {
try {
await _auth.currentUser?.updatePhotoURL(photoURL);
await _auth.currentUser?.reload();
} catch (e) {
throw Exception('Không thể cập nhật ảnh đại diện: $e');
}
}

// Xóa tài khoản
Future<void> deleteAccount() async {
try {
await _auth.currentUser?.delete();
} catch (e) {
throw Exception('Không thể xóa tài khoản: $e');
}
}
}

6.2 Email Verification

// Gửi email xác nhận
Future<void> sendEmailVerification() async {
try {
await _auth.currentUser?.sendEmailVerification();
} catch (e) {
throw Exception('Không thể gửi email xác nhận: $e');
}
}

// Kiểm tra email đã xác nhận chưa
bool get isEmailVerified => _auth.currentUser?.emailVerified ?? false;

// Reload user để cập nhật trạng thái
Future<void> reloadUser() async {
await _auth.currentUser?.reload();
}

7️⃣ Ví Dụ Hoàn Chỉnh: Auth Service

import 'package:firebase_auth/firebase_auth.dart';
import 'package:google_sign_in/google_sign_in.dart';

class AuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
final GoogleSignIn _googleSignIn = GoogleSignIn();
String? _verificationId;

// Stream
Stream<User?> get authStateChanges => _auth.authStateChanges();
User? get currentUser => _auth.currentUser;
bool get isSignedIn => _auth.currentUser != null;

// Email/Password
Future<UserCredential?> signUpWithEmail({
required String email,
required String password,
}) async {
try {
final userCredential = await _auth.createUserWithEmailAndPassword(
email: email,
password: password,
);
await userCredential.user?.sendEmailVerification();
return userCredential;
} on FirebaseAuthException catch (e) {
throw _handleAuthException(e);
}
}

Future<UserCredential?> signInWithEmail({
required String email,
required String password,
}) async {
try {
return await _auth.signInWithEmailAndPassword(
email: email,
password: password,
);
} on FirebaseAuthException catch (e) {
throw _handleAuthException(e);
}
}

// Google Sign In
Future<UserCredential?> signInWithGoogle() async {
try {
final GoogleSignInAccount? googleUser = await _googleSignIn.signIn();
if (googleUser == null) return null;

final GoogleSignInAuthentication googleAuth =
await googleUser.authentication;

final credential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);

return await _auth.signInWithCredential(credential);
} catch (e) {
throw Exception('Google sign in failed: $e');
}
}

// Phone Auth
Future<void> sendOTP(String phoneNumber) async {
await _auth.verifyPhoneNumber(
phoneNumber: phoneNumber,
verificationCompleted: (credential) async {
await _auth.signInWithCredential(credential);
},
verificationFailed: (e) {
throw Exception('Verification failed: ${e.message}');
},
codeSent: (verificationId, resendToken) {
_verificationId = verificationId;
},
codeAutoRetrievalTimeout: (verificationId) {
_verificationId = verificationId;
},
timeout: Duration(seconds: 60),
);
}

Future<UserCredential?> verifyOTP(String smsCode) async {
if (_verificationId == null) {
throw Exception('Verification ID not found');
}

final credential = PhoneAuthProvider.credential(
verificationId: _verificationId!,
smsCode: smsCode,
);

return await _auth.signInWithCredential(credential);
}

// Other methods
Future<void> signOut() async {
await _googleSignIn.signOut();
await _auth.signOut();
}

Future<void> resetPassword(String email) async {
await _auth.sendPasswordResetEmail(email: email);
}

Future<void> sendEmailVerification() async {
await _auth.currentUser?.sendEmailVerification();
}

bool get isEmailVerified => _auth.currentUser?.emailVerified ?? false;

String _handleAuthException(FirebaseAuthException e) {
switch (e.code) {
case 'weak-password':
return 'Mật khẩu quá yếu';
case 'email-already-in-use':
return 'Email đã được sử dụng';
case 'user-not-found':
return 'Không tìm thấy tài khoản';
case 'wrong-password':
return 'Mật khẩu không đúng';
case 'invalid-email':
return 'Email không hợp lệ';
default:
return e.message ?? 'Đã xảy ra lỗi';
}
}
}

8️⃣ UI Example: Login Screen

class LoginScreen extends StatefulWidget {
@override
_LoginScreenState createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _authService = AuthService();
bool _isLoading = false;

Future<void> _signIn() async {
setState(() => _isLoading = true);

try {
await _authService.signInWithEmail(
email: _emailController.text.trim(),
password: _passwordController.text,
);
// Navigation sẽ được xử lý bởi AuthWrapper
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toString())),
);
} finally {
setState(() => _isLoading = false);
}
}

Future<void> _signInWithGoogle() async {
setState(() => _isLoading = true);

try {
await _authService.signInWithGoogle();
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toString())),
);
} finally {
setState(() => _isLoading = false);
}
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: _emailController,
decoration: InputDecoration(labelText: 'Email'),
keyboardType: TextInputType.emailAddress,
),
SizedBox(height: 16),
TextField(
controller: _passwordController,
decoration: InputDecoration(labelText: 'Mật khẩu'),
obscureText: true,
),
SizedBox(height: 24),
ElevatedButton(
onPressed: _isLoading ? null : _signIn,
child: _isLoading
? CircularProgressIndicator()
: Text('Đăng nhập'),
),
SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _isLoading ? null : _signInWithGoogle,
icon: Icon(Icons.g_mobiledata),
label: Text('Đăng nhập với Google'),
),
],
),
),
);
}
}

9️⃣ Best Practices

9.1 Security

  • ✅ Luôn validate input
  • ✅ Sử dụng strong password requirements
  • ✅ Enable email verification
  • ✅ Implement rate limiting
  • ✅ Store sensitive data securely

9.2 Error Handling

  • ✅ Hiển thị thông báo lỗi thân thiện
  • ✅ Log errors để debug
  • ✅ Handle network errors

9.3 User Experience

  • ✅ Hiển thị loading state
  • ✅ Auto-fill email khi có thể
  • ✅ Remember me functionality
  • ✅ Biometric authentication (optional)

🔟 Kết Luận

Firebase Authentication giúp bạn:

Dễ tích hợp: Setup nhanh chóng
Nhiều phương thức: Email, Google, Phone, etc.
Bảo mật cao: Được Google quản lý
Miễn phí: Free tier rộng rãi
Scalable: Tự động scale

💡 Lời khuyên: Luôn enable email verification và implement proper error handling. Sử dụng AuthWrapper để quản lý authentication state trong toàn bộ app.


🎓 Học Sâu Hơn Về Flutter

Muốn master Flutter, Firebase, và các best practices? Tham gia các khóa học tại Hướng Nghiệp Dữ Liệu:

📚 Khóa Học Liên Quan:


📝 Bài viết này được biên soạn bởi đội ngũ Hướng Nghiệp Dữ Liệu. Để cập nhật thêm về Flutter, Firebase Authentication và các best practices trong phát triển ứng dụng di động, hãy theo dõi blog của chúng tôi.

Hướng Dẫn Parse JSON Trong Flutter Bằng Model + Factory Constructor

· 11 min read

Parse JSON là một kỹ năng cơ bản nhưng quan trọng trong Flutter. Bài viết này sẽ hướng dẫn bạn cách parse JSON chuyên nghiệp bằng cách sử dụng model class với factory constructor.


1️⃣ Tại Sao Sử Dụng Model Class?

1.1 Vấn Đề Khi Parse JSON Thủ Công

// ❌ Không tốt: Parse trực tiếp
void fetchUser() async {
final response = await http.get(Uri.parse('https://api.example.com/user'));
final json = jsonDecode(response.body);

// Truy cập trực tiếp - dễ lỗi, không type-safe
String name = json['name'];
int age = json['age'];
// Nếu API thay đổi, code sẽ lỗi runtime
}

Vấn đề:

  • ❌ Không type-safe
  • ❌ Dễ lỗi khi API thay đổi
  • ❌ Khó maintain
  • ❌ Không có autocomplete

1.2 Giải Pháp: Model Class

// ✅ Tốt: Sử dụng model class
class User {
final String name;
final int age;

User({required this.name, required this.age});

factory User.fromJson(Map<String, dynamic> json) {
return User(
name: json['name'] as String,
age: json['age'] as int,
);
}

Map<String, dynamic> toJson() {
return {
'name': name,
'age': age,
};
}
}

// Sử dụng
void fetchUser() async {
final response = await http.get(Uri.parse('https://api.example.com/user'));
final json = jsonDecode(response.body);
final user = User.fromJson(json);

// Type-safe, có autocomplete
print(user.name);
print(user.age);
}

Lợi ích:

  • ✅ Type-safe
  • ✅ Dễ maintain
  • ✅ Có autocomplete
  • ✅ Dễ test
  • ✅ Có thể validate data

2️⃣ Model Class Cơ Bản

2.1 Model Đơn Giản

class User {
final int id;
final String name;
final String email;
final bool isActive;

User({
required this.id,
required this.name,
required this.email,
required this.isActive,
});

// Factory constructor để parse từ JSON
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
isActive: json['is_active'] as bool? ?? true, // Default value
);
}

// Method để convert sang JSON
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
'is_active': isActive,
};
}

// Copy method để tạo instance mới với một số thay đổi
User copyWith({
int? id,
String? name,
String? email,
bool? isActive,
}) {
return User(
id: id ?? this.id,
name: name ?? this.name,
email: email ?? this.email,
isActive: isActive ?? this.isActive,
);
}

@override
String toString() {
return 'User(id: $id, name: $name, email: $email, isActive: $isActive)';
}
}

2.2 Sử Dụng Model

// Parse từ JSON string
String jsonString = '''
{
"id": 1,
"name": "Nguyễn Văn A",
"email": "nguyenvana@example.com",
"is_active": true
}
''';

final json = jsonDecode(jsonString) as Map<String, dynamic>;
final user = User.fromJson(json);

print(user.name); // Nguyễn Văn A
print(user.email); // nguyenvana@example.com

// Convert sang JSON
final userJson = user.toJson();
print(jsonEncode(userJson));

3️⃣ Xử Lý Các Trường Hợp Đặc Biệt

3.1 Nullable Fields

class User {
final int id;
final String name;
final String? phone; // Nullable
final String? avatar; // Nullable

User({
required this.id,
required this.name,
this.phone,
this.avatar,
});

factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
phone: json['phone'] as String?,
avatar: json['avatar'] as String?,
);
}

Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'phone': phone,
'avatar': avatar,
};
}
}

3.2 Default Values

factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
phone: json['phone'] as String? ?? '', // Default empty string
isActive: json['is_active'] as bool? ?? true, // Default true
createdAt: json['created_at'] != null
? DateTime.parse(json['created_at'] as String)
: DateTime.now(), // Default now
);
}

3.3 Type Conversion

class Product {
final int id;
final String name;
final double price;
final DateTime createdAt;

Product({
required this.id,
required this.name,
required this.price,
required this.createdAt,
});

factory Product.fromJson(Map<String, dynamic> json) {
return Product(
id: json['id'] as int,
name: json['name'] as String,
// Convert string to double
price: (json['price'] as num).toDouble(),
// Convert string to DateTime
createdAt: DateTime.parse(json['created_at'] as String),
);
}

Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'price': price,
'created_at': createdAt.toIso8601String(),
};
}
}

4️⃣ Nested Objects

4.1 Object Trong Object

// Address model
class Address {
final String street;
final String city;
final String country;

Address({
required this.street,
required this.city,
required this.country,
});

factory Address.fromJson(Map<String, dynamic> json) {
return Address(
street: json['street'] as String,
city: json['city'] as String,
country: json['country'] as String,
);
}

Map<String, dynamic> toJson() {
return {
'street': street,
'city': city,
'country': country,
};
}
}

// User model với Address
class User {
final int id;
final String name;
final Address address; // Nested object

User({
required this.id,
required this.name,
required this.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>),
);
}

Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'address': address.toJson(),
};
}
}

// JSON example
String jsonString = '''
{
"id": 1,
"name": "Nguyễn Văn A",
"address": {
"street": "123 Đường ABC",
"city": "Hà Nội",
"country": "Việt Nam"
}
}
''';

final user = User.fromJson(jsonDecode(jsonString) as Map<String, dynamic>);
print(user.address.city); // Hà Nội

4.2 Nullable Nested Object

class User {
final int id;
final String name;
final Address? address; // Nullable nested object

User({
required this.id,
required this.name,
this.address,
});

factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
address: json['address'] != null
? Address.fromJson(json['address'] as Map<String, dynamic>)
: null,
);
}

Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'address': address?.toJson(),
};
}
}

5️⃣ Lists và Arrays

5.1 List of Primitives

class User {
final int id;
final String name;
final List<String> tags; // List of strings

User({
required this.id,
required this.name,
required this.tags,
});

factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
tags: (json['tags'] as List<dynamic>)
.map((tag) => tag as String)
.toList(),
);
}

Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'tags': tags,
};
}
}

5.2 List of Objects

class User {
final int id;
final String name;
final List<Address> addresses; // List of Address objects

User({
required this.id,
required this.name,
required this.addresses,
});

factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
addresses: (json['addresses'] as List<dynamic>)
.map((address) => Address.fromJson(address as Map<String, dynamic>))
.toList(),
);
}

Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'addresses': addresses.map((address) => address.toJson()).toList(),
};
}
}

5.3 Nullable List

factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
addresses: json['addresses'] != null
? (json['addresses'] as List<dynamic>)
.map((address) => Address.fromJson(address as Map<String, dynamic>))
.toList()
: [], // Default empty list
);
}

6️⃣ Enum Parsing

6.1 Parse Enum Từ String

enum UserStatus {
active,
inactive,
suspended,
}

class User {
final int id;
final String name;
final UserStatus status;

User({
required this.id,
required this.name,
required this.status,
});

factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
status: UserStatus.values.firstWhere(
(e) => e.name == json['status'],
orElse: () => UserStatus.inactive, // Default
),
);
}

Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'status': status.name,
};
}
}

6.2 Enum Với Custom Values

enum UserStatus {
active('active'),
inactive('inactive'),
suspended('suspended');

final String value;
const UserStatus(this.value);

static UserStatus fromString(String value) {
return UserStatus.values.firstWhere(
(e) => e.value == value,
orElse: () => UserStatus.inactive,
);
}
}

class User {
final int id;
final String name;
final UserStatus status;

User({
required this.id,
required this.name,
required this.status,
});

factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
status: UserStatus.fromString(json['status'] as String),
);
}

Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'status': status.value,
};
}
}

7️⃣ Ví Dụ Hoàn Chỉnh: Complex Model

class User {
final int id;
final String name;
final String email;
final String? phone;
final Address? address;
final List<String> tags;
final List<Order> orders;
final UserStatus status;
final DateTime createdAt;
final DateTime? updatedAt;

User({
required this.id,
required this.name,
required this.email,
this.phone,
this.address,
required this.tags,
required this.orders,
required this.status,
required this.createdAt,
this.updatedAt,
});

factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
phone: json['phone'] as String?,
address: json['address'] != null
? Address.fromJson(json['address'] as Map<String, dynamic>)
: null,
tags: json['tags'] != null
? (json['tags'] as List<dynamic>).map((e) => e as String).toList()
: [],
orders: json['orders'] != null
? (json['orders'] as List<dynamic>)
.map((e) => Order.fromJson(e as Map<String, dynamic>))
.toList()
: [],
status: UserStatus.fromString(json['status'] as String? ?? 'inactive'),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: json['updated_at'] != null
? DateTime.parse(json['updated_at'] as String)
: null,
);
}

Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
'phone': phone,
'address': address?.toJson(),
'tags': tags,
'orders': orders.map((e) => e.toJson()).toList(),
'status': status.value,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt?.toIso8601String(),
};
}

User copyWith({
int? id,
String? name,
String? email,
String? phone,
Address? address,
List<String>? tags,
List<Order>? orders,
UserStatus? status,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return User(
id: id ?? this.id,
name: name ?? this.name,
email: email ?? this.email,
phone: phone ?? this.phone,
address: address ?? this.address,
tags: tags ?? this.tags,
orders: orders ?? this.orders,
status: status ?? this.status,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}

@override
String toString() {
return 'User(id: $id, name: $name, email: $email)';
}
}

class Address {
final String street;
final String city;
final String country;

Address({
required this.street,
required this.city,
required this.country,
});

factory Address.fromJson(Map<String, dynamic> json) {
return Address(
street: json['street'] as String,
city: json['city'] as String,
country: json['country'] as String,
);
}

Map<String, dynamic> toJson() {
return {
'street': street,
'city': city,
'country': country,
};
}
}

class Order {
final int id;
final double total;
final DateTime orderDate;

Order({
required this.id,
required this.total,
required this.orderDate,
});

factory Order.fromJson(Map<String, dynamic> json) {
return Order(
id: json['id'] as int,
total: (json['total'] as num).toDouble(),
orderDate: DateTime.parse(json['order_date'] as String),
);
}

Map<String, dynamic> toJson() {
return {
'id': id,
'total': total,
'order_date': orderDate.toIso8601String(),
};
}
}

enum UserStatus {
active('active'),
inactive('inactive'),
suspended('suspended');

final String value;
const UserStatus(this.value);

static UserStatus fromString(String value) {
return UserStatus.values.firstWhere(
(e) => e.value == value,
orElse: () => UserStatus.inactive,
);
}
}

8️⃣ Sử Dụng Với API

import 'package:dio/dio.dart';
import 'dart:convert';

class UserRepository {
final Dio dio = Dio();

Future<User> getUser(int id) async {
try {
final response = await dio.get('/users/$id');
return User.fromJson(response.data as Map<String, dynamic>);
} catch (e) {
throw Exception('Failed to get user: $e');
}
}

Future<List<User>> getUsers() async {
try {
final response = await dio.get('/users');
return (response.data as List<dynamic>)
.map((json) => User.fromJson(json as Map<String, dynamic>))
.toList();
} catch (e) {
throw Exception('Failed to get users: $e');
}
}

Future<User> createUser(User user) async {
try {
final response = await dio.post(
'/users',
data: user.toJson(),
);
return User.fromJson(response.data as Map<String, dynamic>);
} catch (e) {
throw Exception('Failed to create user: $e');
}
}
}

9️⃣ Best Practices

9.1 Error Handling

factory User.fromJson(Map<String, dynamic> json) {
try {
return User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
);
} catch (e) {
throw FormatException('Failed to parse User: $e');
}
}

9.2 Validation

factory User.fromJson(Map<String, dynamic> json) {
// Validate required fields
if (json['id'] == null || json['name'] == null) {
throw FormatException('Missing required fields');
}

// Validate email format
final email = json['email'] as String;
if (!email.contains('@')) {
throw FormatException('Invalid email format');
}

return User(
id: json['id'] as int,
name: json['name'] as String,
email: email,
);
}

9.3 Sử Dụng json_serializable (Optional)

Để tự động generate code, bạn có thể sử dụng json_serializable:

dependencies:
json_annotation: ^4.8.1

dev_dependencies:
build_runner: ^2.4.7
json_serializable: ^6.7.1
import 'package:json_annotation/json_annotation.dart';

part 'user.g.dart';

@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);
}

🔟 Kết Luận

Parse JSON với model class và factory constructor giúp bạn:

Type-safe: Tránh lỗi runtime
Dễ maintain: Code rõ ràng, dễ đọc
Có autocomplete: IDE hỗ trợ tốt
Dễ test: Có thể test parsing logic
Validation: Có thể validate data

💡 Lời khuyên: Luôn sử dụng model class cho JSON parsing. Đừng parse trực tiếp từ Map<String, dynamic> trong business logic.


🎓 Học Sâu Hơn Về Flutter

Muốn master Flutter, JSON Parsing, và các best practices? Tham gia các khóa học tại Hướng Nghiệp Dữ Liệu:

📚 Khóa Học Liên Quan:


📝 Bài viết này được biên soạn bởi đội ngũ Hướng Nghiệp Dữ Liệu. Để cập nhật thêm về Flutter, JSON parsing và các best practices trong phát triển ứng dụng di động, hãy theo dõi blog của chúng tôi.

Hướng Dẫn Sử Dụng Riverpod/Provider Cho Quản Lý State Khi Gọi API

· 10 min read

Quản lý state khi gọi API là một thách thức trong Flutter. Bài viết này sẽ hướng dẫn bạn cách sử dụng Riverpod hoặc Provider để quản lý state một cách hiệu quả khi gọi API.


1️⃣ Tại Sao Cần State Management Cho API?

1.1 Vấn Đề Khi Không Dùng State Management

// ❌ Không tốt: State management thủ công
class UserScreen extends StatefulWidget {
@override
_UserScreenState createState() => _UserScreenState();
}

class _UserScreenState extends State<UserScreen> {
User? user;
bool isLoading = false;
String? error;

@override
void initState() {
super.initState();
_loadUser();
}

Future<void> _loadUser() async {
setState(() => isLoading = true);
try {
final response = await api.getUser(1);
setState(() {
user = response;
isLoading = false;
});
} catch (e) {
setState(() {
error = e.toString();
isLoading = false;
});
}
}

@override
Widget build(BuildContext context) {
// Phải xử lý loading, error, data trong mọi widget
if (isLoading) return CircularProgressIndicator();
if (error != null) return Text('Error: $error');
if (user == null) return Text('No data');
return UserWidget(user: user!);
}
}

Vấn đề:

  • ❌ Code lặp lại nhiều
  • ❌ Khó test
  • ❌ Khó share state giữa các widget
  • ❌ Không có caching

1.2 Giải Pháp: State Management

Với Riverpod/Provider:

  • ✅ Code sạch hơn
  • ✅ Dễ test
  • ✅ Share state dễ dàng
  • ✅ Có caching
  • ✅ Auto dispose

2️⃣ Sử Dụng Riverpod

2.1 Cài Đặt

dependencies:
flutter_riverpod: ^2.4.9
riverpod_annotation: ^2.3.3

dev_dependencies:
build_runner: ^2.4.7
riverpod_generator: ^2.3.9

2.2 Tạo API Provider

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:dio/dio.dart';

// API Service
final apiServiceProvider = Provider<Dio>((ref) {
return Dio(BaseOptions(
baseUrl: 'https://api.example.com',
));
});

// User Repository
final userRepositoryProvider = Provider<UserRepository>((ref) {
final dio = ref.watch(apiServiceProvider);
return UserRepository(dio);
});

2.3 Tạo Async State Provider

import 'package:flutter_riverpod/flutter_riverpod.dart';

// User Provider với async state
final userProvider = FutureProvider.family<User, int>((ref, userId) async {
final repository = ref.watch(userRepositoryProvider);
return await repository.getUser(userId);
});

// Users List Provider
final usersProvider = FutureProvider<List<User>>((ref) async {
final repository = ref.watch(userRepositoryProvider);
return await repository.getUsers();
});

2.4 Sử Dụng Trong Widget

import 'package:flutter_riverpod/flutter_riverpod.dart';

class UserScreen extends ConsumerWidget {
final int userId;

const UserScreen({Key? key, required this.userId}) : super(key: key);

@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider(userId));

return Scaffold(
appBar: AppBar(title: Text('User')),
body: userAsync.when(
data: (user) => UserDetailsWidget(user: user),
loading: () => Center(child: CircularProgressIndicator()),
error: (error, stack) => ErrorWidget(
error: error,
onRetry: () => ref.refresh(userProvider(userId)),
),
),
);
}
}

3️⃣ StateNotifier Cho Complex State

3.1 Tạo State Class

// User State
class UserState {
final User? user;
final bool isLoading;
final String? error;

UserState({
this.user,
this.isLoading = false,
this.error,
});

UserState copyWith({
User? user,
bool? isLoading,
String? error,
}) {
return UserState(
user: user ?? this.user,
isLoading: isLoading ?? this.isLoading,
error: error,
);
}
}

3.2 Tạo StateNotifier

import 'package:flutter_riverpod/flutter_riverpod.dart';

class UserNotifier extends StateNotifier<UserState> {
final UserRepository repository;

UserNotifier(this.repository) : super(UserState());

Future<void> loadUser(int userId) async {
state = state.copyWith(isLoading: true, error: null);

try {
final user = await repository.getUser(userId);
state = state.copyWith(user: user, isLoading: false);
} catch (e) {
state = state.copyWith(
error: e.toString(),
isLoading: false,
);
}
}

Future<void> updateUser(User user) async {
state = state.copyWith(isLoading: true, error: null);

try {
final updatedUser = await repository.updateUser(user);
state = state.copyWith(user: updatedUser, isLoading: false);
} catch (e) {
state = state.copyWith(
error: e.toString(),
isLoading: false,
);
}
}

void clearError() {
state = state.copyWith(error: null);
}
}

// Provider
final userNotifierProvider = StateNotifierProvider<UserNotifier, UserState>((ref) {
final repository = ref.watch(userRepositoryProvider);
return UserNotifier(repository);
});

3.3 Sử Dụng StateNotifier

class UserScreen extends ConsumerWidget {
final int userId;

const UserScreen({Key? key, required this.userId}) : super(key: key);

@override
Widget build(BuildContext context, WidgetRef ref) {
final userState = ref.watch(userNotifierProvider);

// Load user khi widget được build
ref.listen(userNotifierProvider.notifier, (previous, next) {
next.loadUser(userId);
});

if (userState.isLoading) {
return Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}

if (userState.error != null) {
return Scaffold(
body: ErrorWidget(
error: userState.error!,
onRetry: () => ref.read(userNotifierProvider.notifier).loadUser(userId),
),
);
}

if (userState.user == null) {
return Scaffold(
body: Center(child: Text('No data')),
);
}

return Scaffold(
appBar: AppBar(title: Text('User')),
body: UserDetailsWidget(user: userState.user!),
);
}
}

4️⃣ AsyncNotifier (Riverpod 2.0+)

4.1 Tạo AsyncNotifier

import 'package:flutter_riverpod/flutter_riverpod.dart';

class UserAsyncNotifier extends AsyncNotifier<User> {
@override
Future<User> build() async {
// Load initial data
final userId = ref.watch(selectedUserIdProvider);
final repository = ref.watch(userRepositoryProvider);
return await repository.getUser(userId);
}

Future<void> refresh() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final userId = ref.read(selectedUserIdProvider);
final repository = ref.read(userRepositoryProvider);
return await repository.getUser(userId);
});
}

Future<void> updateUser(User user) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final repository = ref.read(userRepositoryProvider);
return await repository.updateUser(user);
});
}
}

// Provider
final userAsyncNotifierProvider =
AsyncNotifierProvider<UserAsyncNotifier, User>(() {
return UserAsyncNotifier();
});

4.2 Sử Dụng AsyncNotifier

class UserScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userAsyncNotifierProvider);

return Scaffold(
appBar: AppBar(
title: Text('User'),
actions: [
IconButton(
icon: Icon(Icons.refresh),
onPressed: () => ref.read(userAsyncNotifierProvider.notifier).refresh(),
),
],
),
body: userAsync.when(
data: (user) => UserDetailsWidget(user: user),
loading: () => Center(child: CircularProgressIndicator()),
error: (error, stack) => ErrorWidget(
error: error,
onRetry: () => ref.read(userAsyncNotifierProvider.notifier).refresh(),
),
),
);
}
}

5️⃣ Sử Dụng Provider (Alternative)

5.1 Cài Đặt Provider

dependencies:
provider: ^6.1.1

5.2 Tạo ChangeNotifier

import 'package:flutter/foundation.dart';

class UserProvider extends ChangeNotifier {
final UserRepository repository;

User? _user;
bool _isLoading = false;
String? _error;

User? get user => _user;
bool get isLoading => _isLoading;
String? get error => _error;

UserProvider(this.repository);

Future<void> loadUser(int userId) async {
_isLoading = true;
_error = null;
notifyListeners();

try {
_user = await repository.getUser(userId);
_error = null;
} catch (e) {
_error = e.toString();
_user = null;
} finally {
_isLoading = false;
notifyListeners();
}
}

Future<void> refresh() async {
if (_user != null) {
await loadUser(_user!.id);
}
}
}

5.3 Sử Dụng Provider

import 'package:provider/provider.dart';

class UserScreen extends StatelessWidget {
final int userId;

const UserScreen({Key? key, required this.userId}) : super(key: key);

@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => UserProvider(UserRepository())..loadUser(userId),
child: Consumer<UserProvider>(
builder: (context, provider, child) {
if (provider.isLoading) {
return Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}

if (provider.error != null) {
return Scaffold(
body: ErrorWidget(
error: provider.error!,
onRetry: () => provider.loadUser(userId),
),
);
}

if (provider.user == null) {
return Scaffold(
body: Center(child: Text('No data')),
);
}

return Scaffold(
appBar: AppBar(title: Text('User')),
body: UserDetailsWidget(user: provider.user!),
);
},
),
);
}
}

6️⃣ Caching và Auto Refresh

6.1 Caching với Riverpod

// Provider với cache
final cachedUserProvider = FutureProvider.family<User, int>((ref, userId) async {
final repository = ref.watch(userRepositoryProvider);

// Cache sẽ tự động được quản lý bởi Riverpod
return await repository.getUser(userId);
});

// Auto refresh khi dependency thay đổi
final userWithAutoRefreshProvider = FutureProvider.family<User, int>((ref, userId) async {
// Watch một provider khác để trigger refresh
ref.watch(refreshTriggerProvider);

final repository = ref.watch(userRepositoryProvider);
return await repository.getUser(userId);
});

6.2 Manual Refresh

class UserScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider(1));

return Scaffold(
appBar: AppBar(
title: Text('User'),
actions: [
IconButton(
icon: Icon(Icons.refresh),
onPressed: () {
// Invalidate và reload
ref.invalidate(userProvider(1));
},
),
],
),
body: userAsync.when(
data: (user) => UserDetailsWidget(user: user),
loading: () => Center(child: CircularProgressIndicator()),
error: (error, stack) => ErrorWidget(error: error),
),
);
}
}

7️⃣ Error Handling với Riverpod

7.1 Custom Error Handling

final userProvider = FutureProvider.family<User, int>((ref, userId) async {
try {
final repository = ref.watch(userRepositoryProvider);
return await repository.getUser(userId);
} on AppException catch (e) {
// Handle specific exceptions
throw e;
} catch (e) {
throw UnknownException('Failed to load user');
}
});

// Sử dụng
class UserScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider(1));

// Listen to errors
ref.listen(userProvider(1), (previous, next) {
next.whenOrNull(
error: (error, stack) {
if (error is AppException) {
ErrorHandler.handleError(error);
}
},
);
});

return userAsync.when(
data: (user) => UserDetailsWidget(user: user),
loading: () => CircularProgressIndicator(),
error: (error, stack) => ErrorWidget(error: error),
);
}
}

8️⃣ Ví Dụ Hoàn Chỉnh

8.1 Repository

class UserRepository {
final Dio dio;

UserRepository(this.dio);

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

Future<List<User>> getUsers() async {
final response = await dio.get('/users');
return (response.data as List<dynamic>)
.map((json) => User.fromJson(json as Map<String, dynamic>))
.toList();
}

Future<User> createUser(User user) async {
final response = await dio.post('/users', data: user.toJson());
return User.fromJson(response.data as Map<String, dynamic>);
}
}

8.2 Providers

// API Service
final apiServiceProvider = Provider<Dio>((ref) {
return Dio(BaseOptions(baseUrl: 'https://api.example.com'));
});

// Repository
final userRepositoryProvider = Provider<UserRepository>((ref) {
final dio = ref.watch(apiServiceProvider);
return UserRepository(dio);
});

// User Provider
final userProvider = FutureProvider.family<User, int>((ref, userId) async {
final repository = ref.watch(userRepositoryProvider);
return await repository.getUser(userId);
});

// Users List Provider
final usersProvider = FutureProvider<List<User>>((ref) async {
final repository = ref.watch(userRepositoryProvider);
return await repository.getUsers();
});

8.3 UI

// User List Screen
class UserListScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final usersAsync = ref.watch(usersProvider);

return Scaffold(
appBar: AppBar(
title: Text('Users'),
actions: [
IconButton(
icon: Icon(Icons.refresh),
onPressed: () => ref.invalidate(usersProvider),
),
],
),
body: usersAsync.when(
data: (users) => ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(users[index].name),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => UserScreen(userId: users[index].id),
),
);
},
);
},
),
loading: () => Center(child: CircularProgressIndicator()),
error: (error, stack) => ErrorWidget(
error: error,
onRetry: () => ref.invalidate(usersProvider),
),
),
);
}
}

// User Detail Screen
class UserScreen extends ConsumerWidget {
final int userId;

const UserScreen({Key? key, required this.userId}) : super(key: key);

@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider(userId));

return Scaffold(
appBar: AppBar(title: Text('User')),
body: userAsync.when(
data: (user) => UserDetailsWidget(user: user),
loading: () => Center(child: CircularProgressIndicator()),
error: (error, stack) => ErrorWidget(
error: error,
onRetry: () => ref.invalidate(userProvider(userId)),
),
),
);
}
}

9️⃣ Best Practices

9.1 Tổ Chức Code

lib/
providers/
api_providers.dart
user_providers.dart
repositories/
user_repository.dart
models/
user.dart
screens/
user_screen.dart

9.2 Separation of Concerns

  • ✅ Repository: Xử lý API calls
  • ✅ Provider: Quản lý state
  • ✅ Widget: Hiển thị UI

9.3 Testing

// Test provider
void main() {
test('userProvider loads user correctly', () async {
final container = ProviderContainer();
final user = await container.read(userProvider(1).future);
expect(user.id, 1);
});
}

🔟 Kết Luận

Sử dụng Riverpod/Provider cho API state management giúp:

Code sạch hơn: Tách biệt logic và UI
Dễ test: Test providers độc lập
Caching tự động: Riverpod tự động cache
Auto dispose: Tự động cleanup
Type-safe: Compile-time safety

💡 Lời khuyên: Sử dụng Riverpod cho dự án mới. Nó mạnh mẽ hơn Provider và có nhiều tính năng tốt hơn. Provider vẫn tốt nếu bạn đã có codebase sử dụng nó.


🎓 Học Sâu Hơn Về Flutter

Muốn master Flutter, State Management, và các best practices? Tham gia các khóa học tại Hướng Nghiệp Dữ Liệu:

📚 Khóa Học Liên Quan:


📝 Bài viết này được biên soạn bởi đội ngũ Hướng Nghiệp Dữ Liệu. Để cập nhật thêm về Flutter, state management và các best practices trong phát triển ứng dụng di động, hãy theo dõi blog của chúng tôi.

Hướng Dẫn Upload Hình Lên Server Với Multipart (Dio)

· 11 min read

Upload hình ảnh là một tính năng phổ biến trong ứng dụng di động. Bài viết này sẽ hướng dẫn bạn cách upload hình ảnh lên server trong Flutter sử dụng Dio với Multipart, bao gồm chọn ảnh, compress, và theo dõi tiến trình upload.


1️⃣ Cài Đặt Dependencies

1.1 Thêm Packages

Thêm vào pubspec.yaml:

dependencies:
dio: ^5.4.0
image_picker: ^1.0.7
path_provider: ^2.1.1
image: ^4.1.3 # Cho compress image

Sau đó chạy:

flutter pub get

1.2 Cấu Hình Permissions

Android (android/app/src/main/AndroidManifest.xml)

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

iOS (ios/Runner/Info.plist)

<key>NSPhotoLibraryUsageDescription</key>
<string>App cần truy cập thư viện ảnh để chọn ảnh</string>
<key>NSCameraUsageDescription</key>
<string>App cần truy cập camera để chụp ảnh</string>

2.1 Sử Dụng Image Picker

import 'package:image_picker/image_picker.dart';
import 'dart:io';

class ImagePickerService {
final ImagePicker _picker = ImagePicker();

// Chọn ảnh từ gallery
Future<File?> pickImageFromGallery() async {
try {
final XFile? image = await _picker.pickImage(
source: ImageSource.gallery,
imageQuality: 85, // Chất lượng ảnh (0-100)
maxWidth: 1920, // Giới hạn kích thước
maxHeight: 1080,
);

if (image != null) {
return File(image.path);
}
return null;
} catch (e) {
print('Error picking image: $e');
return null;
}
}

// Chụp ảnh từ camera
Future<File?> pickImageFromCamera() async {
try {
final XFile? image = await _picker.pickImage(
source: ImageSource.camera,
imageQuality: 85,
maxWidth: 1920,
maxHeight: 1080,
);

if (image != null) {
return File(image.path);
}
return null;
} catch (e) {
print('Error taking picture: $e');
return null;
}
}

// Chọn nhiều ảnh
Future<List<File>> pickMultipleImages() async {
try {
final List<XFile> images = await _picker.pickMultiImage(
imageQuality: 85,
maxWidth: 1920,
maxHeight: 1080,
);

return images.map((xFile) => File(xFile.path)).toList();
} catch (e) {
print('Error picking multiple images: $e');
return [];
}
}
}

3️⃣ Compress Ảnh Trước Khi Upload

3.1 Compress Image Service

import 'dart:io';
import 'package:image/image.dart' as img;
import 'package:path_provider/path_provider.dart';

class ImageCompressor {
// Compress image và trả về file mới
static Future<File?> compressImage(File imageFile, {int quality = 85}) async {
try {
// Đọc file ảnh
final bytes = await imageFile.readAsBytes();
final image = img.decodeImage(bytes);

if (image == null) return null;

// Resize nếu cần (giữ tỷ lệ)
img.Image resizedImage;
if (image.width > 1920 || image.height > 1080) {
resizedImage = img.copyResize(
image,
width: image.width > image.height ? 1920 : null,
height: image.height > image.width ? 1080 : null,
maintainAspect: true,
);
} else {
resizedImage = image;
}

// Encode lại với chất lượng đã chỉ định
final compressedBytes = img.encodeJpg(resizedImage, quality: quality);

// Lưu vào temporary directory
final tempDir = await getTemporaryDirectory();
final compressedFile = File(
'${tempDir.path}/compressed_${DateTime.now().millisecondsSinceEpoch}.jpg',
);

await compressedFile.writeAsBytes(compressedBytes);

return compressedFile;
} catch (e) {
print('Error compressing image: $e');
return null;
}
}

// Get file size in MB
static Future<double> getFileSizeInMB(File file) async {
final bytes = await file.length();
return bytes / (1024 * 1024);
}
}

3.2 Sử Dụng Compressor

Future<File?> prepareImageForUpload(File originalImage) async {
// Kiểm tra kích thước file
final originalSize = await ImageCompressor.getFileSizeInMB(originalImage);
print('Original size: ${originalSize.toStringAsFixed(2)} MB');

// Nếu file > 2MB, compress
if (originalSize > 2.0) {
final compressedImage = await ImageCompressor.compressImage(
originalImage,
quality: 85,
);

if (compressedImage != null) {
final compressedSize = await ImageCompressor.getFileSizeInMB(compressedImage);
print('Compressed size: ${compressedSize.toStringAsFixed(2)} MB');
return compressedImage;
}
}

return originalImage;
}

4️⃣ Upload Ảnh Với Dio Multipart

4.1 Upload Service Cơ Bản

import 'package:dio/dio.dart';
import 'dart:io';

class ImageUploadService {
final Dio dio;

ImageUploadService(this.dio);

// Upload single image
Future<Map<String, dynamic>> uploadImage(
File imageFile, {
String? fileName,
Map<String, dynamic>? additionalData,
}) async {
try {
// Tạo FormData
final formData = FormData.fromMap({
'image': await MultipartFile.fromFile(
imageFile.path,
filename: fileName ?? 'image_${DateTime.now().millisecondsSinceEpoch}.jpg',
),
// Thêm các field khác nếu cần
if (additionalData != null) ...additionalData,
});

// Upload
final response = await dio.post(
'/upload/image',
data: formData,
);

return response.data as Map<String, dynamic>;
} catch (e) {
print('Error uploading image: $e');
rethrow;
}
}
}

4.2 Upload Với Progress Tracking

class ImageUploadService {
final Dio dio;

ImageUploadService(this.dio);

// Upload với progress callback
Future<Map<String, dynamic>> uploadImageWithProgress(
File imageFile, {
String? fileName,
Function(int sent, int total)? onProgress,
Map<String, dynamic>? additionalData,
}) async {
try {
final formData = FormData.fromMap({
'image': await MultipartFile.fromFile(
imageFile.path,
filename: fileName ?? 'image_${DateTime.now().millisecondsSinceEpoch}.jpg',
),
if (additionalData != null) ...additionalData,
});

final response = await dio.post(
'/upload/image',
data: formData,
onSendProgress: (sent, total) {
if (onProgress != null) {
onProgress(sent, total);
}
final progress = (sent / total * 100).toStringAsFixed(0);
print('Upload progress: $progress%');
},
);

return response.data as Map<String, dynamic>;
} catch (e) {
print('Error uploading image: $e');
rethrow;
}
}
}

4.3 Upload Multiple Images

Future<Map<String, dynamic>> uploadMultipleImages(
List<File> imageFiles, {
Function(int sent, int total)? onProgress,
Map<String, dynamic>? additionalData,
}) async {
try {
// Tạo list MultipartFile
final multipartFiles = await Future.wait(
imageFiles.asMap().entries.map((entry) async {
final index = entry.key;
final file = entry.value;
return await MultipartFile.fromFile(
file.path,
filename: 'image_$index.jpg',
);
}),
);

final formData = FormData.fromMap({
'images': multipartFiles, // Server sẽ nhận array
if (additionalData != null) ...additionalData,
});

final response = await dio.post(
'/upload/images',
data: formData,
onSendProgress: (sent, total) {
if (onProgress != null) {
onProgress(sent, total);
}
},
);

return response.data as Map<String, dynamic>;
} catch (e) {
print('Error uploading images: $e');
rethrow;
}
}

5️⃣ Ví Dụ Hoàn Chỉnh: Upload Service

import 'package:dio/dio.dart';
import 'dart:io';
import 'package:image_picker/image_picker.dart';

class ImageUploadService {
final Dio dio;
final ImagePicker _picker = ImagePicker();

ImageUploadService(this.dio);

// Chọn và upload ảnh từ gallery
Future<UploadResult> pickAndUploadFromGallery({
Function(int sent, int total)? onProgress,
Map<String, dynamic>? additionalData,
bool compress = true,
}) async {
try {
// Chọn ảnh
final XFile? image = await _picker.pickImage(
source: ImageSource.gallery,
imageQuality: 85,
maxWidth: 1920,
maxHeight: 1080,
);

if (image == null) {
return UploadResult(
success: false,
error: 'Không có ảnh được chọn',
);
}

final imageFile = File(image.path);

// Compress nếu cần
File? fileToUpload = imageFile;
if (compress) {
fileToUpload = await ImageCompressor.compressImage(imageFile);
if (fileToUpload == null) {
fileToUpload = imageFile; // Fallback to original
}
}

// Upload
final result = await uploadImage(
fileToUpload!,
onProgress: onProgress,
additionalData: additionalData,
);

return UploadResult(
success: true,
data: result,
);
} catch (e) {
return UploadResult(
success: false,
error: e.toString(),
);
}
}

// Upload image file
Future<Map<String, dynamic>> uploadImage(
File imageFile, {
String? fileName,
Function(int sent, int total)? onProgress,
Map<String, dynamic>? additionalData,
}) async {
try {
final formData = FormData.fromMap({
'image': await MultipartFile.fromFile(
imageFile.path,
filename: fileName ?? 'image_${DateTime.now().millisecondsSinceEpoch}.jpg',
),
if (additionalData != null) ...additionalData,
});

final response = await dio.post(
'/upload/image',
data: formData,
onSendProgress: onProgress,
);

return response.data as Map<String, dynamic>;
} on DioException catch (e) {
throw UploadException(
message: e.message ?? 'Upload failed',
statusCode: e.response?.statusCode,
);
}
}

// Upload multiple images
Future<Map<String, dynamic>> uploadMultipleImages(
List<File> imageFiles, {
Function(int sent, int total)? onProgress,
Map<String, dynamic>? additionalData,
}) async {
try {
final multipartFiles = await Future.wait(
imageFiles.asMap().entries.map((entry) async {
final index = entry.key;
final file = entry.value;
return await MultipartFile.fromFile(
file.path,
filename: 'image_$index.jpg',
);
}),
);

final formData = FormData.fromMap({
'images': multipartFiles,
if (additionalData != null) ...additionalData,
});

final response = await dio.post(
'/upload/images',
data: formData,
onSendProgress: onProgress,
);

return response.data as Map<String, dynamic>;
} on DioException catch (e) {
throw UploadException(
message: e.message ?? 'Upload failed',
statusCode: e.response?.statusCode,
);
}
}
}

// Result class
class UploadResult {
final bool success;
final Map<String, dynamic>? data;
final String? error;

UploadResult({
required this.success,
this.data,
this.error,
});
}

// Custom exception
class UploadException implements Exception {
final String message;
final int? statusCode;

UploadException({required this.message, this.statusCode});

@override
String toString() => message;
}

6️⃣ UI Component: Upload Widget

6.1 Image Upload Widget

import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'dart:io';

class ImageUploadWidget extends StatefulWidget {
final ImageUploadService uploadService;
final Function(String imageUrl)? onUploadSuccess;
final Function(String error)? onUploadError;

const ImageUploadWidget({
Key? key,
required this.uploadService,
this.onUploadSuccess,
this.onUploadError,
}) : super(key: key);

@override
_ImageUploadWidgetState createState() => _ImageUploadWidgetState();
}

class _ImageUploadWidgetState extends State<ImageUploadWidget> {
File? _selectedImage;
bool _isUploading = false;
double _uploadProgress = 0.0;

Future<void> _pickImage() async {
final result = await widget.uploadService.pickAndUploadFromGallery(
onProgress: (sent, total) {
setState(() {
_uploadProgress = sent / total;
});
},
compress: true,
);

if (result.success) {
setState(() {
_selectedImage = null; // Clear selected image
_isUploading = false;
_uploadProgress = 0.0;
});

final imageUrl = result.data?['url'] as String?;
if (imageUrl != null && widget.onUploadSuccess != null) {
widget.onUploadSuccess!(imageUrl);
}

ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Upload thành công!')),
);
} else {
setState(() {
_isUploading = false;
_uploadProgress = 0.0;
});

if (widget.onUploadError != null) {
widget.onUploadError!(result.error ?? 'Upload thất bại');
}

ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(result.error ?? 'Upload thất bại'),
backgroundColor: Colors.red,
),
);
}
}

Future<void> _selectImage() async {
final picker = ImagePicker();
final image = await picker.pickImage(
source: ImageSource.gallery,
imageQuality: 85,
);

if (image != null) {
setState(() {
_selectedImage = File(image.path);
});
}
}

Future<void> _uploadImage() async {
if (_selectedImage == null) return;

setState(() {
_isUploading = true;
_uploadProgress = 0.0;
});

try {
final result = await widget.uploadService.uploadImage(
_selectedImage!,
onProgress: (sent, total) {
setState(() {
_uploadProgress = sent / total;
});
},
);

setState(() {
_isUploading = false;
_uploadProgress = 0.0;
_selectedImage = null;
});

final imageUrl = result['url'] as String?;
if (imageUrl != null && widget.onUploadSuccess != null) {
widget.onUploadSuccess!(imageUrl);
}

ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Upload thành công!')),
);
} catch (e) {
setState(() {
_isUploading = false;
_uploadProgress = 0.0;
});

if (widget.onUploadError != null) {
widget.onUploadError!(e.toString());
}

ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Upload thất bại: ${e.toString()}'),
backgroundColor: Colors.red,
),
);
}
}

@override
Widget build(BuildContext context) {
return Column(
children: [
// Preview image
if (_selectedImage != null)
Container(
height: 200,
width: double.infinity,
margin: EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(8),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(
_selectedImage!,
fit: BoxFit.cover,
),
),
),

// Progress indicator
if (_isUploading)
Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
LinearProgressIndicator(value: _uploadProgress),
SizedBox(height: 8),
Text(
'${(_uploadProgress * 100).toStringAsFixed(0)}%',
style: TextStyle(fontSize: 12),
),
],
),
),

// Buttons
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton.icon(
onPressed: _isUploading ? null : _selectImage,
icon: Icon(Icons.image),
label: Text('Chọn ảnh'),
),
SizedBox(width: 16),
if (_selectedImage != null)
ElevatedButton.icon(
onPressed: _isUploading ? null : _uploadImage,
icon: Icon(Icons.cloud_upload),
label: Text('Upload'),
),
],
),
],
);
}
}

7️⃣ Sử Dụng Với State Management (Riverpod)

7.1 Upload Provider

import 'package:flutter_riverpod/flutter_riverpod.dart';

final imageUploadServiceProvider = Provider<ImageUploadService>((ref) {
final dio = ref.watch(dioProvider);
return ImageUploadService(dio);
});

final uploadStateProvider = StateNotifierProvider<UploadNotifier, UploadState>((ref) {
final service = ref.watch(imageUploadServiceProvider);
return UploadNotifier(service);
});

class UploadState {
final File? selectedImage;
final bool isUploading;
final double progress;
final String? imageUrl;
final String? error;

UploadState({
this.selectedImage,
this.isUploading = false,
this.progress = 0.0,
this.imageUrl,
this.error,
});

UploadState copyWith({
File? selectedImage,
bool? isUploading,
double? progress,
String? imageUrl,
String? error,
}) {
return UploadState(
selectedImage: selectedImage ?? this.selectedImage,
isUploading: isUploading ?? this.isUploading,
progress: progress ?? this.progress,
imageUrl: imageUrl ?? this.imageUrl,
error: error,
);
}
}

class UploadNotifier extends StateNotifier<UploadState> {
final ImageUploadService service;

UploadNotifier(this.service) : super(UploadState());

Future<void> selectImage() async {
final picker = ImagePicker();
final image = await picker.pickImage(source: ImageSource.gallery);

if (image != null) {
state = state.copyWith(selectedImage: File(image.path));
}
}

Future<void> uploadImage() async {
if (state.selectedImage == null) return;

state = state.copyWith(isUploading: true, progress: 0.0, error: null);

try {
final result = await service.uploadImage(
state.selectedImage!,
onProgress: (sent, total) {
state = state.copyWith(progress: sent / total);
},
);

state = state.copyWith(
isUploading: false,
imageUrl: result['url'] as String?,
selectedImage: null,
);
} catch (e) {
state = state.copyWith(
isUploading: false,
error: e.toString(),
);
}
}
}

8️⃣ Best Practices

8.1 Compress Images

  • ✅ Luôn compress ảnh trước khi upload
  • ✅ Giới hạn kích thước (ví dụ: max 2MB)
  • ✅ Giữ chất lượng hợp lý (85-90%)

8.2 Error Handling

  • ✅ Xử lý lỗi network
  • ✅ Xử lý lỗi server
  • ✅ Hiển thị thông báo rõ ràng cho người dùng

8.3 Progress Tracking

  • ✅ Hiển thị progress bar
  • ✅ Cho phép cancel upload
  • ✅ Thông báo khi upload hoàn thành

8.4 Security

  • ✅ Validate file type
  • ✅ Validate file size
  • ✅ Sanitize file name

9️⃣ Kết Luận

Upload hình ảnh với Dio Multipart giúp bạn:

Upload dễ dàng: Sử dụng FormData và MultipartFile
Theo dõi tiến trình: Progress callback
Upload nhiều ảnh: Hỗ trợ multiple files
Compress ảnh: Giảm kích thước trước khi upload
Error handling: Xử lý lỗi tốt

💡 Lời khuyên: Luôn compress ảnh trước khi upload để tiết kiệm băng thông và thời gian. Sử dụng progress indicator để cải thiện trải nghiệm người dùng.


🎓 Học Sâu Hơn Về Flutter

Muốn master Flutter, File Upload, và các best practices? Tham gia các khóa học tại Hướng Nghiệp Dữ Liệu:

📚 Khóa Học Liên Quan:


📝 Bài viết này được biên soạn bởi đội ngũ Hướng Nghiệp Dữ Liệu. Để cập nhật thêm về Flutter, file upload và các best practices trong phát triển ứng dụng di động, hãy theo dõi blog của chúng tôi.

Làm Sao Để Học Tốt Tin Học Cấp 3: Hướng Dẫn Toàn Diện Cho Học Sinh

· 15 min read

Tin học cấp 3 là môn học quan trọng, không chỉ giúp bạn phát triển tư duy logic mà còn mở ra nhiều cơ hội nghề nghiệp trong tương lai. Bài viết này sẽ hướng dẫn bạn các phương pháp học tin học hiệu quả từ cơ bản đến nâng cao.


1️⃣ Xây Dựng Nền Tảng Vững Chắc

1.1 Hiểu Rõ Mục Tiêu Học Tập

Trước khi bắt đầu, hãy xác định mục tiêu của bạn:

Mục tiêu ngắn hạn: Đạt điểm cao trong các bài kiểm tra, thi học kỳ
Mục tiêu trung hạn: Thành thạo một ngôn ngữ lập trình (Python, C++, Java)
Mục tiêu dài hạn: Định hướng nghề nghiệp trong lĩnh vực IT

1.2 Nắm Vững Kiến Thức Cơ Bản

Các Khái Niệm Quan Trọng

1. Thuật Toán (Algorithm)

  • Hiểu cách giải quyết vấn đề từng bước
  • Luyện tập vẽ sơ đồ khối (flowchart)
  • Phân tích độ phức tạp của thuật toán

2. Cấu Trúc Dữ Liệu

  • Mảng (Array)
  • Danh sách liên kết (Linked List)
  • Ngăn xếp (Stack) và Hàng đợi (Queue)
  • Cây (Tree) và Đồ thị (Graph)

3. Lập Trình Cơ Bản

  • Biến và kiểu dữ liệu
  • Cấu trúc điều khiển (if/else, for, while)
  • Hàm và thủ tục
  • Lập trình hướng đối tượng (OOP)

1.3 Chọn Ngôn Ngữ Lập Trình Phù Hợp

Python - Lựa Chọn Tốt Cho Người Mới Bắt Đầu

# Ví dụ: Chương trình tính tổng các số từ 1 đến n
def tinh_tong(n):
tong = 0
for i in range(1, n + 1):
tong += i
return tong

# Sử dụng
n = int(input("Nhập số n: "))
ket_qua = tinh_tong(n)
print(f"Tổng từ 1 đến {n} là: {ket_qua}")

Ưu điểm của Python:

  • Cú pháp đơn giản, dễ đọc
  • Nhiều thư viện hỗ trợ
  • Ứng dụng rộng rãi trong thực tế
  • Cộng đồng lớn, tài liệu phong phú

C++ - Cho Học Sinh Muốn Học Sâu

// Ví dụ: Chương trình tính giai thừa
#include <iostream>
using namespace std;

long long giai_thua(int n) {
if (n == 0 || n == 1) {
return 1;
}
return n * giai_thua(n - 1);
}

int main() {
int n;
cout << "Nhập số n: ";
cin >> n;
cout << "Giai thừa của " << n << " là: " << giai_thua(n);
return 0;
}

Ưu điểm của C++:

  • Hiệu suất cao
  • Kiểm soát bộ nhớ tốt
  • Phù hợp cho thi học sinh giỏi
  • Nền tảng cho nhiều ngôn ngữ khác

2️⃣ Phương Pháp Học Hiệu Quả

2.1 Học Lý Thuyết Kết Hợp Thực Hành

Quy Trình Học Hiệu Quả

  1. Đọc và hiểu lý thuyết

    • Đọc kỹ giáo trình, sách tham khảo
    • Ghi chú các khái niệm quan trọng
    • Đặt câu hỏi khi không hiểu
  2. Thực hành ngay lập tức

    • Viết code ngay sau khi học lý thuyết
    • Làm các bài tập từ dễ đến khó
    • Tự tạo bài tập cho mình
  3. Ôn tập định kỳ

    • Xem lại code đã viết
    • Giải lại các bài tập cũ
    • Tối ưu hóa code

2.2 Luyện Tập Thường Xuyên

Lịch Học Đề Xuất

Mỗi ngày:

  • 30 phút: Đọc lý thuyết mới
  • 1 giờ: Thực hành viết code
  • 30 phút: Làm bài tập

Mỗi tuần:

  • 2-3 giờ: Làm dự án nhỏ
  • 1 giờ: Ôn tập kiến thức cũ
  • 1 giờ: Tham gia cộng đồng, hỏi đáp

Các Nguồn Bài Tập

Trang web luyện tập:

  • LeetCode (leetcode.com)
  • HackerRank (hackerrank.com)
  • Codeforces (codeforces.com)
  • VNOI (vnoi.info) - Cho học sinh Việt Nam

Sách bài tập:

  • "Cấu trúc dữ liệu và giải thuật" - Nguyễn Đức Nghĩa
  • "Lập trình C++" - Phạm Văn Ất
  • "Python Crash Course" - Eric Matthes

2.3 Học Từ Lỗi Sai

Cách Xử Lý Lỗi Hiệu Quả

1. Đọc kỹ thông báo lỗi

# Ví dụ lỗi thường gặp
def tinh_tong(a, b):
return a + b

# Lỗi: thiếu tham số
ket_qua = tinh_tong(5) # TypeError: tinh_tong() missing 1 required positional argument

2. Debug từng bước

  • In giá trị biến để kiểm tra
  • Sử dụng debugger
  • Chia nhỏ code để test

3. Ghi chép lỗi thường gặp

  • Tạo file ghi chép các lỗi đã gặp
  • Ghi lại cách giải quyết
  • Tham khảo khi gặp lỗi tương tự

2.4 Học Nhóm và Thảo Luận

Lợi Ích Của Học Nhóm

Học hỏi lẫn nhau: Mỗi người có cách tiếp cận khác nhau
Giải quyết vấn đề nhanh hơn: Nhiều người cùng suy nghĩ
Động lực học tập: Có bạn bè cùng mục tiêu
Chia sẻ tài liệu: Tiết kiệm thời gian tìm kiếm

Cách Tổ Chức Học Nhóm Hiệu Quả

  1. Chọn nhóm 3-5 người: Không quá đông, không quá ít
  2. Đặt mục tiêu rõ ràng: Cùng nhau đạt điểm cao, hoàn thành dự án
  3. Phân công công việc: Mỗi người nghiên cứu một phần
  4. Gặp mặt định kỳ: 1-2 lần/tuần để thảo luận
  5. Sử dụng công cụ online: Discord, Zoom để học từ xa

3️⃣ Kỹ Năng Lập Trình Quan Trọng

3.1 Tư Duy Thuật Toán

Các Bước Giải Quyết Bài Toán

Bước 1: Phân tích đề bài

  • Đọc kỹ yêu cầu
  • Xác định input và output
  • Tìm các trường hợp đặc biệt

Bước 2: Thiết kế thuật toán

  • Vẽ sơ đồ khối
  • Viết pseudocode
  • Kiểm tra tính đúng đắn

Bước 3: Viết code

  • Chuyển thuật toán thành code
  • Chú thích code rõ ràng
  • Đặt tên biến có ý nghĩa

Bước 4: Test và tối ưu

  • Test với nhiều test case
  • Kiểm tra edge cases
  • Tối ưu hóa nếu cần

Ví Dụ: Giải Bài Toán Tìm Số Nguyên Tố

def la_so_nguyen_to(n):
"""
Kiểm tra xem n có phải số nguyên tố không
"""
if n < 2:
return False

# Chỉ cần kiểm tra đến căn bậc 2 của n
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False

return True

# Sử dụng
so_can_kiem_tra = int(input("Nhập số cần kiểm tra: "))
if la_so_nguyen_to(so_can_kiem_tra):
print(f"{so_can_kiem_tra} là số nguyên tố")
else:
print(f"{so_can_kiem_tra} không phải số nguyên tố")

3.2 Kỹ Năng Debug

Các Kỹ Thuật Debug

1. Print Debugging

def tinh_trung_binh(danh_sach):
print(f"Danh sách: {danh_sach}") # Debug: xem giá trị
tong = sum(danh_sach)
print(f"Tổng: {tong}") # Debug: kiểm tra tổng
so_luong = len(danh_sach)
print(f"Số lượng: {so_luong}") # Debug: kiểm tra số lượng
return tong / so_luong if so_luong > 0 else 0

2. Sử dụng Debugger

  • Breakpoint: Dừng tại điểm cụ thể
  • Step over: Chạy từng dòng
  • Watch: Theo dõi giá trị biến

3. Unit Testing

def test_tinh_trung_binh():
assert tinh_trung_binh([1, 2, 3, 4, 5]) == 3.0
assert tinh_trung_binh([10, 20, 30]) == 20.0
assert tinh_trung_binh([]) == 0
print("Tất cả test đều pass!")

test_tinh_trung_binh()

3.3 Code Style và Best Practices

Quy Tắc Viết Code Sạch

1. Đặt tên biến có ý nghĩa

# ❌ Không tốt
x = 10
y = 20
z = x + y

# ✅ Tốt
so_hang_1 = 10
so_hang_2 = 20
tong = so_hang_1 + so_hang_2

2. Viết comment rõ ràng

def tim_max(danh_sach):
"""
Tìm số lớn nhất trong danh sách

Args:
danh_sach: List các số

Returns:
Số lớn nhất trong danh sách, None nếu danh sách rỗng
"""
if not danh_sach:
return None
return max(danh_sach)

3. Chia nhỏ hàm

# ❌ Không tốt: Hàm quá dài, làm nhiều việc
def xu_ly_du_lieu(data):
# 100 dòng code làm nhiều việc
pass

# ✅ Tốt: Chia thành nhiều hàm nhỏ
def doc_du_lieu(file):
pass

def xu_ly_du_lieu(data):
pass

def luu_ket_qua(result):
pass

4️⃣ Chuẩn Bị Cho Thi Cử

4.1 Ôn Tập Hiệu Quả

Kế Hoạch Ôn Tập 3 Tháng

Tháng 1: Ôn tập toàn bộ lý thuyết

  • Xem lại tất cả các chương đã học
  • Làm lại bài tập trong sách giáo khoa
  • Ghi chép các công thức, thuật toán quan trọng

Tháng 2: Luyện đề

  • Làm đề thi các năm trước
  • Làm bài tập nâng cao
  • Tham gia các kỳ thi thử

Tháng 3: Tổng hợp và bổ sung

  • Xem lại các lỗi thường mắc
  • Làm đề thi mẫu
  • Chuẩn bị tâm lý thi cử

4.2 Kỹ Năng Làm Bài Thi

Chiến Lược Làm Bài

1. Đọc toàn bộ đề trước

  • Xác định độ khó của từng câu
  • Phân bổ thời gian hợp lý
  • Làm câu dễ trước, câu khó sau

2. Quản lý thời gian

  • Dành 10 phút đầu để đọc đề
  • Dành 5 phút cuối để kiểm tra lại
  • Phân bổ thời gian cho từng câu

3. Kiểm tra kỹ code

  • Test với nhiều test case
  • Kiểm tra edge cases
  • Đảm bảo không có lỗi syntax

Các Lỗi Thường Mắc Khi Thi

Không đọc kỹ đề: Hiểu sai yêu cầu
Không test code: Code chạy sai nhưng không biết
Quên edge cases: Không xử lý trường hợp đặc biệt
Quản lý thời gian kém: Dành quá nhiều thời gian cho một câu

4.3 Tài Liệu Ôn Tập

Sách Tham Khảo

Lý thuyết:

  • "Tin học đại cương" - Bộ GD&ĐT
  • "Cấu trúc dữ liệu và giải thuật" - Nguyễn Đức Nghĩa
  • "Lập trình Python" - Nguyễn Văn Hòa

Bài tập:

  • "Bài tập Tin học" - Nhiều tác giả
  • Đề thi các năm trước
  • Tài liệu từ các trường chuyên

Nguồn Online

  • YouTube: Các kênh dạy lập trình
  • Coursera, edX: Khóa học miễn phí
  • GitHub: Code mẫu, dự án tham khảo
  • Stack Overflow: Hỏi đáp khi gặp vấn đề

5️⃣ Xây Dựng Dự Án Thực Tế

5.1 Bắt Đầu Với Dự Án Đơn Giản

Dự Án Đề Xuất Cho Người Mới

1. Máy tính đơn giản

def may_tinh():
print("=== MÁY TÍNH ĐƠN GIẢN ===")
a = float(input("Nhập số thứ nhất: "))
phep_toan = input("Nhập phép toán (+, -, *, /): ")
b = float(input("Nhập số thứ hai: "))

if phep_toan == '+':
ket_qua = a + b
elif phep_toan == '-':
ket_qua = a - b
elif phep_toan == '*':
ket_qua = a * b
elif phep_toan == '/':
if b != 0:
ket_qua = a / b
else:
print("Lỗi: Không thể chia cho 0!")
return
else:
print("Phép toán không hợp lệ!")
return

print(f"Kết quả: {a} {phep_toan} {b} = {ket_qua}")

may_tinh()

2. Quản lý danh sách công việc (To-Do List)

danh_sach_cong_viec = []

def them_cong_viec():
cong_viec = input("Nhập công việc mới: ")
danh_sach_cong_viec.append(cong_viec)
print(f"Đã thêm: {cong_viec}")

def hien_thi_cong_viec():
if not danh_sach_cong_viec:
print("Danh sách trống!")
else:
print("\n=== DANH SÁCH CÔNG VIỆC ===")
for i, cv in enumerate(danh_sach_cong_viec, 1):
print(f"{i}. {cv}")

def xoa_cong_viec():
hien_thi_cong_viec()
if danh_sach_cong_viec:
stt = int(input("Nhập số thứ tự công việc cần xóa: "))
if 1 <= stt <= len(danh_sach_cong_viec):
cv_da_xoa = danh_sach_cong_viec.pop(stt - 1)
print(f"Đã xóa: {cv_da_xoa}")
else:
print("Số thứ tự không hợp lệ!")

# Menu chính
while True:
print("\n=== QUẢN LÝ CÔNG VIỆC ===")
print("1. Thêm công việc")
print("2. Hiển thị danh sách")
print("3. Xóa công việc")
print("4. Thoát")

lua_chon = input("Chọn chức năng (1-4): ")

if lua_chon == '1':
them_cong_viec()
elif lua_chon == '2':
hien_thi_cong_viec()
elif lua_chon == '3':
xoa_cong_viec()
elif lua_chon == '4':
print("Tạm biệt!")
break
else:
print("Lựa chọn không hợp lệ!")

3. Game đoán số

import random

def game_doan_so():
so_bi_mat = random.randint(1, 100)
so_lan_doan = 0
so_lan_toi_da = 7

print("=== GAME ĐOÁN SỐ ===")
print("Tôi đã nghĩ một số từ 1 đến 100. Bạn có thể đoán trong 7 lần!")

while so_lan_doan < so_lan_toi_da:
so_doan = int(input(f"\nLần {so_lan_doan + 1}/{so_lan_toi_da}: Nhập số bạn đoán: "))
so_lan_doan += 1

if so_doan == so_bi_mat:
print(f"🎉 Chúc mừng! Bạn đã đoán đúng sau {so_lan_doan} lần!")
return
elif so_doan < so_bi_mat:
print("Số của bạn nhỏ hơn số bí mật!")
else:
print("Số của bạn lớn hơn số bí mật!")

print(f"😢 Hết lượt rồi! Số bí mật là: {so_bi_mat}")

game_doan_so()

5.2 Nâng Cấp Dự Án

Từ Dự Án Đơn Giản Đến Phức Tạp

Bước 1: Thêm tính năng

  • Lưu dữ liệu vào file
  • Giao diện đẹp hơn
  • Xử lý lỗi tốt hơn

Bước 2: Tối ưu code

  • Refactor code
  • Sử dụng hàm, class
  • Áp dụng design patterns

Bước 3: Mở rộng

  • Thêm tính năng mới
  • Kết nối database
  • Tạo giao diện web

6️⃣ Định Hướng Nghề Nghiệp

6.1 Các Ngành Nghề Liên Quan Đến Tin Học

Lập Trình Viên (Developer)

Frontend Developer:

  • HTML, CSS, JavaScript
  • React, Vue.js
  • Thiết kế giao diện người dùng

Backend Developer:

  • Python, Java, Node.js
  • Database (SQL, NoSQL)
  • API Development

Full-Stack Developer:

  • Kết hợp Frontend và Backend
  • Có thể làm cả hai

Khoa Học Dữ Liệu (Data Science)

  • Phân tích dữ liệu
  • Machine Learning
  • Trí tuệ nhân tạo (AI)
  • Python, R, SQL

An Ninh Mạng (Cybersecurity)

  • Bảo mật hệ thống
  • Ethical Hacking
  • Phân tích mã độc
  • Network Security

6.2 Lộ Trình Học Tập

Cho Học Sinh Cấp 3

Năm 1 (Lớp 10):

  • Nắm vững lập trình cơ bản
  • Làm quen với một ngôn ngữ (Python hoặc C++)
  • Hoàn thành các dự án nhỏ

Năm 2 (Lớp 11):

  • Học cấu trúc dữ liệu và giải thuật
  • Tham gia các cuộc thi lập trình
  • Xây dựng portfolio cá nhân

Năm 3 (Lớp 12):

  • Chọn chuyên ngành yêu thích
  • Học các công nghệ chuyên sâu
  • Chuẩn bị cho đại học/đi làm

6.3 Tài Nguyên Học Tập

Khóa Học Online

Miễn phí:

  • Codecademy
  • freeCodeCamp
  • Khan Academy
  • Coursera (có chứng chỉ miễn phí)

Trả phí (chất lượng cao):

  • Udemy
  • Pluralsight
  • LinkedIn Learning

Cộng Đồng và Diễn Đàn

  • Stack Overflow: Hỏi đáp lập trình
  • GitHub: Chia sẻ code, dự án
  • Reddit: r/learnprogramming, r/Python
  • Discord: Các server lập trình

7️⃣ Tips và Tricks

7.1 Tối Ưu Hóa Quá Trình Học

Pomodoro Technique

  • Học 25 phút, nghỉ 5 phút
  • Sau 4 chu kỳ, nghỉ 15-30 phút
  • Giúp tập trung tốt hơn

Active Recall

  • Không chỉ đọc, mà phải tự giải thích lại
  • Làm bài tập không xem đáp án trước
  • Tự tạo câu hỏi và trả lời

Spaced Repetition

  • Ôn tập lại kiến thức sau 1 ngày, 3 ngày, 1 tuần
  • Sử dụng flashcard
  • Ứng dụng: Anki, Quizlet

7.2 Công Cụ Hỗ Trợ Học Tập

Code Editor

VS Code: Miễn phí, nhiều extension
PyCharm: Tốt cho Python
IntelliJ IDEA: Tốt cho Java

Version Control

  • Git: Quản lý phiên bản code
  • GitHub: Lưu trữ code online
  • Học Git ngay từ đầu sẽ rất hữu ích

Tài Liệu và Ghi Chú

  • Notion: Ghi chép, quản lý dự án
  • Obsidian: Ghi chép dạng markdown
  • OneNote: Ghi chép của Microsoft

7.3 Tránh Các Sai Lầm Thường Gặp

Học quá nhiều lý thuyết, ít thực hành

  • Giải pháp: Thực hành ngay sau khi học lý thuyết

Copy code mà không hiểu

  • Giải pháp: Luôn đọc và hiểu code trước khi copy

Bỏ qua kiến thức cơ bản

  • Giải pháp: Xây dựng nền tảng vững chắc

Học quá nhiều ngôn ngữ cùng lúc

  • Giải pháp: Tập trung vào 1-2 ngôn ngữ trước

Không làm dự án thực tế

  • Giải pháp: Bắt đầu với dự án nhỏ, tăng dần độ khó

8️⃣ Kết Luận

Học tốt tin học cấp 3 không chỉ giúp bạn đạt điểm cao mà còn mở ra nhiều cơ hội trong tương lai. Hãy nhớ:

Xây dựng nền tảng vững chắc: Nắm vững kiến thức cơ bản
Thực hành thường xuyên: Code mỗi ngày
Học từ lỗi sai: Mỗi lỗi là một bài học
Xây dựng dự án: Áp dụng kiến thức vào thực tế
Tham gia cộng đồng: Học hỏi từ người khác
Kiên trì: Lập trình cần thời gian, đừng nản lòng

💡 Lời khuyên cuối: Đừng so sánh bản thân với người khác. Mỗi người có tốc độ học khác nhau. Quan trọng là bạn tiến bộ mỗi ngày!


🎓 Tài Nguyên Học Tập Từ Hướng Nghiệp Dữ Liệu

Muốn học lập trình chuyên sâu và có định hướng nghề nghiệp rõ ràng? Tham gia các khóa học tại Hướng Nghiệp Dữ Liệu:

📚 Khóa Học Liên Quan:


📝 Bài viết này được biên soạn bởi đội ngũ Hướng Nghiệp Dữ Liệu. Để cập nhật thêm về phương pháp học tin học, lập trình và định hướng nghề nghiệp, hãy theo dõi blog của chúng tôi.

Xử Lý Lỗi API (401, 403, 500) và Hiển Thị Thông Báo Cho Người Dùng

· 11 min read

Xử lý lỗi API một cách chuyên nghiệp là yếu tố quan trọng để tạo trải nghiệm người dùng tốt. Bài viết này sẽ hướng dẫn bạn cách xử lý các lỗi API phổ biến và hiển thị thông báo thân thiện cho người dùng.


1️⃣ Các Loại Lỗi API Phổ Biến

1.1 HTTP Status Codes

Status CodeÝ NghĩaNguyên Nhân
400Bad RequestDữ liệu gửi lên không hợp lệ
401UnauthorizedChưa đăng nhập hoặc token hết hạn
403ForbiddenKhông có quyền truy cập
404Not FoundTài nguyên không tồn tại
500Internal Server ErrorLỗi server
502Bad GatewayGateway lỗi
503Service UnavailableService không khả dụng

1.2 Network Errors

  • Connection timeout
  • No internet connection
  • DNS resolution failed
  • SSL certificate error

2️⃣ Tạo Custom Exception Classes

2.1 Base Exception

abstract class AppException implements Exception {
final String message;
final int? statusCode;

AppException(this.message, [this.statusCode]);

@override
String toString() => message;
}

2.2 Specific Exceptions

// Bad Request (400)
class BadRequestException extends AppException {
BadRequestException([String? message])
: super(message ?? 'Yêu cầu không hợp lệ', 400);
}

// Unauthorized (401)
class UnauthorizedException extends AppException {
UnauthorizedException([String? message])
: super(message ?? 'Chưa đăng nhập hoặc phiên đăng nhập đã hết hạn', 401);
}

// Forbidden (403)
class ForbiddenException extends AppException {
ForbiddenException([String? message])
: super(message ?? 'Bạn không có quyền truy cập tài nguyên này', 403);
}

// Not Found (404)
class NotFoundException extends AppException {
NotFoundException([String? message])
: super(message ?? 'Không tìm thấy tài nguyên', 404);
}

// Server Error (500)
class ServerException extends AppException {
ServerException([String? message])
: super(message ?? 'Lỗi server. Vui lòng thử lại sau.', 500);
}

// Network Error
class NetworkException extends AppException {
NetworkException([String? message])
: super(message ?? 'Không có kết nối internet. Vui lòng kiểm tra lại.', null);
}

// Timeout
class TimeoutException extends AppException {
TimeoutException([String? message])
: super(message ?? 'Kết nối timeout. Vui lòng thử lại.', null);
}

// Unknown Error
class UnknownException extends AppException {
UnknownException([String? message])
: super(message ?? 'Đã xảy ra lỗi không xác định', null);
}

3️⃣ Xử Lý Lỗi Trong Dio Interceptor

3.1 Error Interceptor

import 'package:dio/dio.dart';

class ErrorInterceptor extends Interceptor {
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
AppException exception;

switch (err.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
exception = TimeoutException('Kết nối timeout. Vui lòng thử lại.');
break;

case DioExceptionType.badResponse:
final statusCode = err.response?.statusCode;
final message = _extractErrorMessage(err.response?.data);

switch (statusCode) {
case 400:
exception = BadRequestException(message);
break;
case 401:
exception = UnauthorizedException(message);
break;
case 403:
exception = ForbiddenException(message);
break;
case 404:
exception = NotFoundException(message);
break;
case 500:
case 502:
case 503:
exception = ServerException(message);
break;
default:
exception = ServerException('Lỗi server: $statusCode');
}
break;

case DioExceptionType.cancel:
exception = UnknownException('Request đã bị hủy');
break;

case DioExceptionType.unknown:
if (err.message?.contains('SocketException') ?? false) {
exception = NetworkException();
} else {
exception = UnknownException(err.message);
}
break;

default:
exception = UnknownException(err.message);
}

handler.reject(
DioException(
requestOptions: err.requestOptions,
error: exception,
type: err.type,
response: err.response,
),
);
}

String? _extractErrorMessage(dynamic data) {
if (data is Map<String, dynamic>) {
return data['message'] as String? ??
data['error'] as String?;
} else if (data is String) {
return data;
}
return null;
}
}

3.2 Sử Dụng Interceptor

final dio = Dio();

dio.interceptors.add(ErrorInterceptor());

// Khi gọi API, lỗi sẽ được map thành custom exception
try {
final response = await dio.get('/api/data');
} on DioException catch (e) {
if (e.error is AppException) {
final exception = e.error as AppException;
// Xử lý exception
_handleError(exception);
}
}

4️⃣ Error Handler Service

4.1 Centralized Error Handler

class ErrorHandler {
static void handleError(AppException exception) {
// Log error
_logError(exception);

// Show user-friendly message
_showErrorMessage(exception);
}

static void _logError(AppException exception) {
// Log to crashlytics, sentry, etc.
print('Error: ${exception.message} (${exception.statusCode})');
}

static void _showErrorMessage(AppException exception) {
// Get current context
final context = NavigationService.navigatorKey.currentContext;
if (context == null) return;

// Show snackbar
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(exception.message),
backgroundColor: _getErrorColor(exception),
duration: Duration(seconds: 3),
action: SnackBarAction(
label: 'Đóng',
textColor: Colors.white,
onPressed: () {},
),
),
);
}

static Color _getErrorColor(AppException exception) {
if (exception is UnauthorizedException) {
return Colors.orange;
} else if (exception is ForbiddenException) {
return Colors.red;
} else if (exception is ServerException) {
return Colors.red.shade700;
} else if (exception is NetworkException) {
return Colors.blue;
} else {
return Colors.grey;
}
}

// Get user-friendly message
static String getUserMessage(AppException exception) {
return exception.message;
}

// Check if error is retryable
static bool isRetryable(AppException exception) {
return exception is NetworkException ||
exception is TimeoutException ||
exception is ServerException;
}
}

5️⃣ Hiển Thị Thông Báo Lỗi Trong UI

5.1 Snackbar

void showErrorSnackbar(BuildContext context, AppException exception) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(Icons.error_outline, color: Colors.white),
SizedBox(width: 12),
Expanded(
child: Text(
exception.message,
style: TextStyle(color: Colors.white),
),
),
],
),
backgroundColor: Colors.red,
duration: Duration(seconds: 4),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
action: SnackBarAction(
label: 'Đóng',
textColor: Colors.white,
onPressed: () {},
),
),
);
}

5.2 Dialog

Future<void> showErrorDialog(
BuildContext context,
AppException exception, {
VoidCallback? onRetry,
}) async {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: [
Icon(Icons.error_outline, color: Colors.red),
SizedBox(width: 8),
Text('Lỗi'),
],
),
content: Text(exception.message),
actions: [
if (onRetry != null && ErrorHandler.isRetryable(exception))
TextButton(
onPressed: () {
Navigator.of(context).pop();
onRetry();
},
child: Text('Thử lại'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Đóng'),
),
],
),
);
}

5.3 Custom Error Widget

class ErrorWidget extends StatelessWidget {
final AppException exception;
final VoidCallback? onRetry;

const ErrorWidget({
Key? key,
required this.exception,
this.onRetry,
}) : super(key: key);

@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
_getErrorIcon(),
size: 64,
color: Colors.red,
),
SizedBox(height: 16),
Text(
_getErrorTitle(),
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text(
exception.message,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
if (onRetry != null && ErrorHandler.isRetryable(exception)) ...[
SizedBox(height: 24),
ElevatedButton.icon(
onPressed: onRetry,
icon: Icon(Icons.refresh),
label: Text('Thử lại'),
),
],
],
),
),
);
}

IconData _getErrorIcon() {
if (exception is NetworkException) {
return Icons.wifi_off;
} else if (exception is UnauthorizedException) {
return Icons.lock_outline;
} else if (exception is ServerException) {
return Icons.error_outline;
} else {
return Icons.warning_amber_rounded;
}
}

String _getErrorTitle() {
if (exception is NetworkException) {
return 'Không có kết nối';
} else if (exception is UnauthorizedException) {
return 'Phiên đăng nhập hết hạn';
} else if (exception is ServerException) {
return 'Lỗi server';
} else {
return 'Đã xảy ra lỗi';
}
}
}

6️⃣ Xử Lý Lỗi Trong Repository/Service

6.1 Repository với Error Handling

class UserRepository {
final Dio dio;

UserRepository(this.dio);

Future<User> getUser(int id) async {
try {
final response = await dio.get('/users/$id');
return User.fromJson(response.data as Map<String, dynamic>);
} on DioException catch (e) {
if (e.error is AppException) {
throw e.error as AppException;
}
throw UnknownException('Failed to get user');
}
}

Future<List<User>> getUsers() async {
try {
final response = await dio.get('/users');
return (response.data as List<dynamic>)
.map((json) => User.fromJson(json as Map<String, dynamic>))
.toList();
} on DioException catch (e) {
if (e.error is AppException) {
throw e.error as AppException;
}
throw UnknownException('Failed to get users');
}
}
}

6.2 Service với Error Handling

class ApiService {
final Dio dio;
final ErrorHandler errorHandler;

ApiService(this.dio, this.errorHandler);

Future<T> handleRequest<T>(
Future<Response> Function() request,
T Function(dynamic data) parser,
) async {
try {
final response = await request();
return parser(response.data);
} on DioException catch (e) {
if (e.error is AppException) {
final exception = e.error as AppException;
errorHandler.handleError(exception);
throw exception;
}
final exception = UnknownException('Request failed');
errorHandler.handleError(exception);
throw exception;
} catch (e) {
if (e is AppException) {
errorHandler.handleError(e);
}
rethrow;
}
}
}

7️⃣ Xử Lý Lỗi Trong Widget/UI

7.1 Sử Dụng FutureBuilder

class UserListWidget extends StatelessWidget {
final UserRepository userRepository;

const UserListWidget({Key? key, required this.userRepository}) : super(key: key);

@override
Widget build(BuildContext context) {
return FutureBuilder<List<User>>(
future: userRepository.getUsers(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}

if (snapshot.hasError) {
final error = snapshot.error;
if (error is AppException) {
return ErrorWidget(
exception: error,
onRetry: () {
// Retry logic
},
);
}
return ErrorWidget(
exception: UnknownException('Đã xảy ra lỗi'),
onRetry: () {
// Retry logic
},
);
}

if (snapshot.hasData) {
final users = snapshot.data!;
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
return UserListItem(user: users[index]);
},
);
}

return SizedBox.shrink();
},
);
}
}

7.2 Sử Dụng try-catch trong Event Handler

class UserScreen extends StatefulWidget {
@override
_UserScreenState createState() => _UserScreenState();
}

class _UserScreenState extends State<UserScreen> {
final UserRepository userRepository = UserRepository(ApiService.dio);
User? user;
bool isLoading = false;
AppException? error;

@override
void initState() {
super.initState();
_loadUser();
}

Future<void> _loadUser() async {
setState(() {
isLoading = true;
error = null;
});

try {
final loadedUser = await userRepository.getUser(1);
setState(() {
user = loadedUser;
isLoading = false;
});
} on AppException catch (e) {
setState(() {
error = e;
isLoading = false;
});

// Show error message
ErrorHandler.handleError(e);
} catch (e) {
final exception = UnknownException('Đã xảy ra lỗi không xác định');
setState(() {
error = exception;
isLoading = false;
});
ErrorHandler.handleError(exception);
}
}

@override
Widget build(BuildContext context) {
if (isLoading) {
return Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}

if (error != null) {
return Scaffold(
body: ErrorWidget(
exception: error!,
onRetry: _loadUser,
),
);
}

if (user == null) {
return Scaffold(
body: Center(child: Text('Không có dữ liệu')),
);
}

return Scaffold(
appBar: AppBar(title: Text('User')),
body: UserDetailsWidget(user: user!),
);
}
}

8️⃣ Xử Lý 401 - Refresh Token

8.1 Refresh Token Interceptor

class RefreshTokenInterceptor extends Interceptor {
final Dio refreshDio;
final TokenService tokenService;

RefreshTokenInterceptor(this.refreshDio, this.tokenService);

@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
if (err.response?.statusCode == 401) {
try {
// Refresh token
final newToken = await tokenService.refreshToken();

if (newToken != null) {
// Retry original request
final opts = err.requestOptions;
opts.headers['Authorization'] = 'Bearer $newToken';

final response = await refreshDio.request(
opts.path,
options: Options(
method: opts.method,
headers: opts.headers,
),
data: opts.data,
queryParameters: opts.queryParameters,
);

return handler.resolve(response);
} else {
// Refresh failed, logout
await tokenService.logout();
throw UnauthorizedException('Phiên đăng nhập đã hết hạn');
}
} catch (e) {
if (e is AppException) {
return handler.reject(
DioException(
requestOptions: err.requestOptions,
error: e,
),
);
}
}
}

return handler.next(err);
}
}

9️⃣ Best Practices

9.1 Centralized Error Handling

  • ✅ Tạo một ErrorHandler service duy nhất
  • ✅ Map tất cả lỗi thành custom exceptions
  • ✅ Log errors để debug
  • ✅ Hiển thị thông báo thân thiện với người dùng

9.2 User-Friendly Messages

  • ✅ Tránh technical jargon
  • ✅ Giải thích rõ ràng vấn đề
  • ✅ Đưa ra hướng giải quyết nếu có thể
  • ✅ Cung cấp nút "Thử lại" khi phù hợp

9.3 Error Logging

class ErrorLogger {
static void logError(AppException exception, {StackTrace? stackTrace}) {
// Log to console
print('Error: ${exception.message}');
if (stackTrace != null) {
print('Stack trace: $stackTrace');
}

// Log to crashlytics/sentry
// FirebaseCrashlytics.instance.recordError(exception, stackTrace);
}
}

🔟 Kết Luận

Xử lý lỗi API chuyên nghiệp giúp:

Trải nghiệm người dùng tốt hơn: Thông báo rõ ràng, dễ hiểu
Dễ debug: Log errors đầy đủ
Code sạch hơn: Xử lý lỗi tập trung
Maintainable: Dễ thêm/xóa/sửa error handling

💡 Lời khuyên: Luôn xử lý lỗi một cách graceful. Đừng để app crash vì lỗi API. Hãy hiển thị thông báo thân thiện và cho phép người dùng thử lại.


🎓 Học Sâu Hơn Về Flutter

Muốn master Flutter, Error Handling, và các best practices? Tham gia các khóa học tại Hướng Nghiệp Dữ Liệu:

📚 Khóa Học Liên Quan:


📝 Bài viết này được biên soạn bởi đội ngũ Hướng Nghiệp Dữ Liệu. Để cập nhật thêm về Flutter, error handling và các best practices trong phát triển ứng dụng di động, hãy theo dõi blog của chúng tôi.