Hướng Dẫn Parse JSON Trong Flutter Bằng Model + Factory Constructor
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:
- ✅ Lập Trình Ứng Dụng Di Động Flutter - Xây dựng ứng dụng Flutter từ cơ bản đến nâng cao
📝 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.
