Chuyển tới nội dung chính

Kết Nối API RESTful trong Flutter: Hướng Dẫn Chi Tiết

· 8 phút để đọc

Kết nối API RESTful là một kỹ năng quan trọng trong phát triển ứng dụng di động, đặc biệt là khi bạn cần tương tác với các dịch vụ backend để lấy dữ liệu hoặc thực hiện các thao tác CRUD (Create, Read, Update, Delete). Trong bài viết này, chúng ta sẽ tìm hiểu cách kết nối API RESTful trong Flutter bằng thư viện http và dio. Flutter


1. Giới Thiệu về RESTful API​

REST (Representational State Transfer) là một kiến trúc phần mềm được sử dụng rộng rãi để thiết kế các hệ thống mạng, đặc biệt là các ứng dụng web. RESTful API là các API tuân thủ các nguyên tắc của REST.

Các phương thức HTTP phổ biến trong RESTful API:

  • GET: Lấy dữ liệu từ server.
  • POST: Gửi dữ liệu mới lên server.
  • PUT: Cập nhật dữ liệu đã có trên server.
  • DELETE: Xóa dữ liệu trên server.

2. Kết Nối API RESTful với Thư Viện http​

Thư viện http là một thư viện đơn giản và phổ biến trong Flutter để thực hiện các yêu cầu HTTP. Dưới đây là các bước để kết nối API RESTful với thư viện http.

Bước 1: Thêm Thư Viện http vào Project​

Thêm thư viện http vào file pubspec.yaml:

dependencies:
flutter:
sdk: flutter
http: ^0.13.3
Bước 2: Gọi API với Phương Thức GET
Dưới đây là ví dụ về cách gọi API với phương thức GET để lấy dữ liệu từ server:

dart
Copy
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

class GetDataExample extends StatefulWidget {
@override
_GetDataExampleState createState() => _GetDataExampleState();
}

class _GetDataExampleState extends State<GetDataExample> {
List<dynamic> data = [];

Future<void> fetchData() async {
final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts'));

if (response.statusCode == 200) {
setState(() {
data = json.decode(response.body);
});
} else {
throw Exception('Failed to load data');
}
}

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

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Fetch Data Example'),
),
body: ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(data[index]['title']),
subtitle: Text(data[index]['body']),
);
},
),
);
}
}
Bước 3: Gọi API với Phương Thức POST
Dưới đây là ví dụ về cách gọi API với phương thức POST để gửi dữ liệu lên server:

dart
Copy
Future<void> postData() async {
final response = await http.post(
Uri.parse('https://jsonplaceholder.typicode.com/posts'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(<String, String>{
'title': 'foo',
'body': 'bar',
'userId': '1',
}),
);

if (response.statusCode == 201) {
print('Data posted successfully');
} else {
throw Exception('Failed to post data');
}
}
3. Kết Nối API RESTful với Thư Viện dio
Thư viện dio là một thư viện mạnh mẽ hơn so với http, hỗ trợ nhiều tính năng nâng cao như interceptors, timeout, retry request, và hỗ trợ upload file. Dưới đây là các bước để kết nối API RESTful với thư viện dio.

Bước 1: Thêm Thư Viện dio vào Project
Thêm thư viện dio vào file pubspec.yaml:

yaml
Copy
dependencies:
flutter:
sdk: flutter
dio: ^4.0.0
Bước 2: Gọi API với Phương Thức GET
Dưới đây là ví dụ về cách gọi API với phương thức GET bằng dio:

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

Future<void> fetchDataWithDio() async {
final dio = Dio();
final response = await dio.get('https://jsonplaceholder.typicode.com/posts');

if (response.statusCode == 200) {
print(response.data);
} else {
throw Exception('Failed to load data');
}
}
Bước 3: Gọi API với Phương Thức POST
Dưới đây là ví dụ về cách gọi API với phương thức POST bằng dio:

dart
Copy
Future<void> postDataWithDio() async {
final dio = Dio();
final response = await dio.post(
'https://jsonplaceholder.typicode.com/posts',
data: {
'title': 'foo',
'body': 'bar',
'userId': '1',
},
);

if (response.statusCode == 201) {
print('Data posted successfully');
} else {
throw Exception('Failed to post data');
}
}
4. Xử Lý Lỗi và Hiển Thị Thông Báo
Khi kết nối API, việc xử lý lỗi là rất quan trọng để đảm bảo ứng dụng của bạn hoạt động ổn định. Dưới đây là cách xử lý lỗi và hiển thị thông báo cho người dùng:

dart
Copy
Future<void> fetchData() async {
try {
final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts'));

if (response.statusCode == 200) {
setState(() {
data = json.decode(response.body);
});
} else {
throw Exception('Failed to load data');
}
} catch (e) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Error'),
content: Text('Failed to load data: $e'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('OK'),
),
],
),
);
}
}
## Đi sâu: phân trang, upload file, và xử lý lỗi

### Phân trang — bắt buộc với mọi danh sách thật

```dart
class DanhSachPhanTrang<T> {
final List<T> duLieu;
final int trang;
final int tongTrang;
final bool conNua;

const DanhSachPhanTrang({
required this.duLieu,
required this.trang,
required this.tongTrang,
required this.conNua,
});
}

Future<DanhSachPhanTrang<SanPham>> laySanPham({
required int trang,
int kichThuoc = 20,
}) async {
final json = await api.get('/san-pham', query: {
'page': '$trang',
'limit': '$kichThuoc',
});

final ds = (json['du_lieu'] as List)
.map((e) => SanPham.fromJson(e as Map<String, dynamic>))
.toList();

final tongSo = json['tong'] as int;
return DanhSachPhanTrang(
duLieu: ds,
trang: trang,
tongTrang: (tongSo / kichThuoc).ceil(),
conNua: trang * kichThuoc < tongSo,
);
}

Upload file — hai cách và khi nào dùng​

import 'package:http_parser/http_parser.dart';

// CÁCH 1: multipart/form-data — phổ biến nhất
Future<String> uploadAnh(File file) async {
final request = http.MultipartRequest(
'POST',
Uri.parse('$baseUrl/upload'),
)..headers['Authorization'] = 'Bearer $token'
..files.add(await http.MultipartFile.fromPath(
'file',
file.path,
// Đặt content-type tường minh — nhiều server từ chối nếu thiếu
contentType: MediaType('image', 'jpeg'),
));

final streamed = await request.send();
final phanHoi = await http.Response.fromStream(streamed);

if (phanHoi.statusCode != 200 && phanHoi.statusCode != 201) {
throw Exception('Upload thất bại: ${phanHoi.statusCode}');
}
return jsonDecode(phanHoi.body)['url'] as String;
}
CáchKhi nào dùngGhi chú
multipart/form-dataẢnh, tài liệu, file nhị phânChuẩn, mọi backend đều hỗ trợ
base64 trong JSONFile rất nhỏ (< 100 KB)Tốn 33% dung lượng, chậm
Upload trực tiếp lên S3/CloudinaryFile lớnCần lấy signed URL từ backend trước

Xử lý lỗi có backoff — tránh bị chặn​

Future<T> thuLaiVoiBackoff<T>(
Future<T> Function() tacVu, {
int soLanToiDa = 3,
}) async {
Duration cho = const Duration(seconds: 1);

for (int lan = 0; lan < soLanToiDa; lan++) {
try {
return await tacVu();
} on ApiException catch (e) {
final nenThuLai = e.statusCode == 429 ||
(e.statusCode != null && e.statusCode! >= 500);

if (!nenThuLai || lan == soLanToiDa - 1) rethrow;

await Future.delayed(cho);
cho *= 2; // backoff theo cấp số nhân: 1s → 2s → 4s
}
}
throw ApiException('Đã hết số lần thử lại');
}

💡 Vì sao cần backoff: khi server quá tải (trả 429 hoặc 503), gửi lại ngay lập tức làm tình hình tệ hơn. Cấp số nhân cho server thời gian hồi phục — và cũng là điều các API lớn yêu cầu trong tài liệu của họ.

Hiển thị lỗi cho người dùng — đừng bao giờ hiện stack trace​

String thongDiepThanThien(Object loi) {
if (loi is ApiException) {
if (loi.statusCode == 401) return 'Phiên đăng nhập đã hết hạn';
if (loi.statusCode == 403) return 'Bạn không có quyền thực hiện thao tác này';
if (loi.statusCode == 404) return 'Không tìm thấy dữ liệu';
if (loi.statusCode == 429) return 'Vui lòng chờ một lát rồi thử lại';
if (loi.statusCode != null && loi.statusCode! >= 500) {
return 'Máy chủ đang gặp sự cố. Vui lòng thử lại sau.';
}
return loi.message;
}
return 'Đã xảy ra lỗi. Vui lòng thử lại.';
}

Ba nguyên tắc thông báo lỗi:

  1. Nói cho người dùng biết họ nên làm gì, không phải lỗi là gì. "Không kết nối được máy chủ" tốt hơn "SocketException".
  2. Không bao giờ hiển thị chi tiết kỹ thuật — lộ thông tin hệ thống và gây bối rối.
  3. Luôn log lỗi gốc để lập trình viên còn gỡ được.

5. Kết Luận​

Kết nối API RESTful là một kỹ năng quan trọng trong phát triển ứng dụng di động với Flutter. Bằng cách sử dụng các thư viện như http và dio, bạn có thể dễ dàng tương tác với các dịch vụ backend để lấy dữ liệu, gửi dữ liệu, và thực hiện các thao tác CRUD. Trong khóa học Flutter Level 3 của chúng tôi, bạn sẽ được học cách kết nối API RESTful một cách chuyên sâu, cùng với các kỹ thuật nâng cao như authentication, phân trang, và bảo mật API.