Skip to main content

One post tagged with "Model"

View All Tags

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.