Dio & HTTP Client trong Flutter

1. Tổng quan về Dio và HTTP Client

Trong quá trình phát triển ứng dụng Flutter, việc giao tiếp với máy chủ (Server) là một phần quan trọng để ứng dụng có thể lấy dữ liệu, gửi thông tin, đăng nhập, đăng ký tài khoản, tải hình ảnh, cập nhật sản phẩm hoặc thực hiện các nghiệp vụ khác. Để thực hiện những hoạt động này, ứng dụng cần sử dụng giao thức HTTP để trao đổi dữ liệu với Backend API.

HTTP Client là thành phần chịu trách nhiệm gửi các HTTP Request từ ứng dụng đến máy chủ và nhận HTTP Response từ máy chủ trả về. Trong Flutter, HTTP Client giúp ứng dụng giao tiếp với các hệ thống Backend được xây dựng bằng những công nghệ như Node.js, Express, NestJS, ASP.NET Core, Spring Boot hoặc Django.

Dio là một thư viện HTTP Client phổ biến trong Flutter và Dart, cung cấp nhiều tính năng hỗ trợ quá trình giao tiếp với API. Ngoài khả năng gửi và nhận dữ liệu, Dio còn hỗ trợ cấu hình thời gian chờ, xử lý lỗi, Interceptor, gửi dữ liệu dạng FormData, tải lên tệp, tải xuống tệp và quản lý các thông tin trong HTTP Request.

Dio không phải là ngôn ngữ lập trình, framework giao diện hay một công cụ xây dựng Backend. Đây là thư viện được sử dụng trong tầng giao tiếp mạng của ứng dụng Flutter.

Ví dụ, khi người dùng mở trang danh sách sản phẩm trong một ứng dụng thương mại điện tử, Flutter cần gửi yêu cầu đến Backend để lấy dữ liệu sản phẩm. Dio có thể đảm nhiệm việc gửi yêu cầu đó, nhận dữ liệu JSON từ máy chủ và chuyển dữ liệu về cho các tầng xử lý tiếp theo trong ứng dụng.

2. Khái niệm HTTP Client

2.1. HTTP Client

HTTP Client là thành phần thực hiện giao tiếp HTTP giữa một ứng dụng và máy chủ. Trong kiến trúc Client–Server, ứng dụng Flutter đóng vai trò Client, còn Backend API đóng vai trò cung cấp dữ liệu và xử lý nghiệp vụ.

Khi người dùng thực hiện một thao tác cần dữ liệu từ máy chủ, ứng dụng sẽ tạo một HTTP Request. Request được gửi đến địa chỉ của API thông qua mạng Internet hoặc mạng nội bộ. Máy chủ tiếp nhận yêu cầu, kiểm tra thông tin, thực hiện nghiệp vụ và trả về HTTP Response.

Một HTTP Client thường hỗ trợ các nhiệm vụ sau:

  • Xác định địa chỉ API cần gửi yêu cầu.

  • Lựa chọn phương thức HTTP phù hợp.

  • Gửi dữ liệu và các tham số cần thiết.

  • Đính kèm Header như Authorization, Content-Type và Accept.

  • Nhận dữ liệu phản hồi từ máy chủ.

  • Đọc mã trạng thái HTTP.

  • Xử lý lỗi kết nối, lỗi máy chủ hoặc lỗi trong quá trình truyền dữ liệu.

  • Hỗ trợ các thao tác truyền tải dữ liệu như tải lên và tải xuống tệp.

Trong Dart, thư viện chuẩn dart:io có cung cấp HttpClient, còn Flutter có thể sử dụng package http hoặc package dio để gửi các yêu cầu HTTP. Mỗi giải pháp có cách sử dụng và tập hợp tính năng riêng.

2.2. HTTP Request

HTTP Request là yêu cầu được Client gửi đến Server nhằm thực hiện một thao tác cụ thể.

Một HTTP Request thường bao gồm các thành phần chính:

  • URL: Địa chỉ tài nguyên hoặc API cần truy cập.

  • Method: Phương thức HTTP xác định loại thao tác cần thực hiện.

  • Headers: Chứa thông tin bổ sung về yêu cầu, định dạng dữ liệu hoặc thông tin xác thực.

  • Query Parameters: Các tham số được truyền trên URL để tìm kiếm, lọc hoặc phân trang dữ liệu.

  • Request Body: Phần nội dung gửi lên máy chủ, thường được sử dụng khi tạo hoặc cập nhật dữ liệu.

Ví dụ, ứng dụng cần lấy danh sách sản phẩm từ API:

GET /api/products?page=1&limit=10
Authorization: Bearer your_access_token
Accept: application/json

Trong ví dụ này, GET là phương thức lấy dữ liệu, /api/products là đường dẫn API, còn page=1 và limit=10 là các tham số phân trang.

Đối với thao tác đăng ký tài khoản, ứng dụng có thể gửi một Request sử dụng phương thức POST cùng với dữ liệu JSON chứa email, mật khẩu và những thông tin cần thiết khác.

2.3. HTTP Response

HTTP Response là phản hồi do máy chủ gửi về sau khi tiếp nhận và xử lý Request.

Một Response thường bao gồm:

  • Status Code: Mã trạng thái cho biết kết quả xử lý yêu cầu ở cấp độ HTTP.

  • Headers: Thông tin mô tả dữ liệu phản hồi, bộ nhớ đệm hoặc các thuộc tính khác.

  • Response Body: Nội dung dữ liệu mà máy chủ trả về, có thể là JSON, văn bản, dữ liệu nhị phân hoặc nội dung khác.

Ví dụ, API trả về danh sách sản phẩm dưới dạng JSON:

{
  "success": true,
  "message": "Lấy danh sách sản phẩm thành công",
  "data": [
    {
      "id": 1,
      "name": "Áo thun",
      "price": 250000
    },
    {
      "id": 2,
      "name": "Quần jeans",
      "price": 450000
    }
  ]
}

Flutter có thể đọc dữ liệu JSON này, chuyển đổi thành các đối tượng Dart và hiển thị danh sách sản phẩm trên giao diện.

Cần lưu ý rằng việc nhận được Response không đồng nghĩa với việc nghiệp vụ luôn thành công. Ví dụ, máy chủ có thể trả về mã 400, 401 hoặc 500 để thông báo yêu cầu không hợp lệ, chưa được xác thực hoặc xảy ra lỗi phía máy chủ.

3. Các phương thức HTTP phổ biến

HTTP Client hỗ trợ nhiều phương thức HTTP. Việc lựa chọn phương thức phù hợp giúp API thể hiện rõ mục đích của từng thao tác.

3.1. GET

GET được sử dụng để truy xuất dữ liệu từ máy chủ.

Ví dụ:

  • Lấy danh sách sản phẩm.

  • Lấy thông tin chi tiết của một sản phẩm.

  • Lấy thông tin hồ sơ người dùng.

  • Lấy danh sách đơn hàng.

  • Lấy dữ liệu thống kê.

final response = await dio.get('/products');

print(response.data);

Thông thường, GET không được sử dụng để yêu cầu máy chủ thay đổi trạng thái nghiệp vụ. Những tham số tìm kiếm và phân trang thường được truyền thông qua Query Parameters.

3.2. POST

POST thường được sử dụng để gửi dữ liệu nhằm tạo tài nguyên mới hoặc thực hiện một thao tác nghiệp vụ.

Ví dụ:

  • Đăng nhập.

  • Đăng ký tài khoản.

  • Tạo đơn hàng.

  • Gửi đánh giá sản phẩm.

  • Gửi biểu mẫu liên hệ.

final response = await dio.post(
  '/login',
  data: {
    'email': '[email protected]',
    'password': 'your_password',
  },
);

Dữ liệu trong data sẽ được Dio đưa vào Request Body. Với dữ liệu JSON thông thường, Dio có thể tự xử lý việc tuần tự hóa dữ liệu và thiết lập kiểu nội dung phù hợp.

3.3. PUT

PUT thường được sử dụng để thay thế hoặc cập nhật toàn bộ biểu diễn của một tài nguyên theo thiết kế API.

final response = await dio.put(
  '/users/1',
  data: {
    'name': 'Nguyen Van A',
    'email': '[email protected]',
  },
);

Ý nghĩa chính xác của thao tác còn phụ thuộc vào cách Backend thiết kế API.

3.4. PATCH

PATCH thường được sử dụng để cập nhật một phần dữ liệu của tài nguyên.

Ví dụ, chỉ thay đổi tên hiển thị của người dùng mà không cần gửi lại toàn bộ thông tin hồ sơ.

final response = await dio.patch(
  '/users/1',
  data: {
    'name': 'Tran Van B',
  },
);

3.5. DELETE

DELETE được sử dụng để yêu cầu xóa một tài nguyên.

final response = await dio.delete('/products/1');

Tùy theo thiết kế Backend, thao tác xóa có thể là xóa vật lý, xóa mềm hoặc chuyển tài nguyên sang trạng thái không còn hoạt động.

4. Tổng quan về thư viện Dio

Dio là một package HTTP Client dành cho Dart, thường được sử dụng trong các ứng dụng Flutter có nhu cầu giao tiếp với REST API hoặc những dịch vụ hỗ trợ HTTP.

Dio cung cấp giao diện lập trình giúp lập trình viên thực hiện các yêu cầu mạng mà không cần tự xây dựng toàn bộ quy trình tạo Request, truyền dữ liệu và đọc Response.

Một số chức năng quan trọng của Dio bao gồm:

  • Gửi Request bằng các phương thức HTTP phổ biến.

  • Cấu hình baseUrl để quản lý địa chỉ API chung.

  • Thiết lập connectTimeout, receiveTimeout và sendTimeout.

  • Truyền Query Parameters và Request Body.

  • Thiết lập Header mặc định.

  • Quản lý mã trạng thái và lỗi HTTP.

  • Sử dụng Interceptor để xử lý Request, Response và lỗi tập trung.

  • Gửi dữ liệu dạng FormData.

  • Tải lên và tải xuống tệp.

  • Theo dõi tiến trình truyền dữ liệu.

  • Hủy Request thông qua CancelToken.

  • Tích hợp cơ chế chuyển đổi dữ liệu JSON.

  • Cấu hình chứng chỉ và những tùy chọn liên quan đến kết nối khi có nhu cầu phù hợp.

Nhờ những tính năng này, Dio có thể được sử dụng cho cả ứng dụng nhỏ lẫn ứng dụng có kiến trúc nhiều tầng như ứng dụng thương mại điện tử, ứng dụng quản lý thành viên phòng gym hoặc hệ thống quản trị nội bộ.

Tuy nhiên, Dio không tự tạo ra API, không thay thế Backend và không tự quản lý toàn bộ nghiệp vụ đăng nhập hoặc phân quyền. Những chức năng này cần được phối hợp giữa Flutter, Backend và cơ chế xác thực tương ứng.

5. Cài đặt và khởi tạo Dio trong Flutter

5.1. Cài đặt package

Để sử dụng Dio, cần thêm package vào dự án Flutter.

Mở Terminal tại thư mục dự án và chạy lệnh:

flutter pub add dio

Sau khi cài đặt, có thể import thư viện vào tệp Dart:

import 'package:dio/dio.dart';

Nên sử dụng lệnh quản lý package của Flutter để lấy phiên bản tương thích và cập nhật khai báo phụ thuộc trong pubspec.yaml.

5.2. Khởi tạo Dio cơ bản

Để gửi Request, trước tiên cần tạo một đối tượng Dio.

import 'package:dio/dio.dart';

final dio = Dio();

Future<void> getProducts() async {
  try {
    final response = await dio.get(
      'https://api.example.com/products',
    );

    print(response.data);
  } on DioException catch (e) {
    print('Lỗi Dio: ${e.message}');
  }
}

Trong ví dụ trên:

  • Dio() tạo một HTTP Client.

  • dio.get() gửi Request theo phương thức GET.

  • await chờ Future hoàn thành trước khi tiếp tục thực thi hàm.

  • response.data chứa dữ liệu phản hồi sau khi Dio xử lý Response.

  • DioException được sử dụng để xử lý những lỗi mà Dio báo cáo.

Địa chỉ api.example.com chỉ là địa chỉ minh họa, cần được thay bằng API thực tế của dự án.

5.3. Cấu hình Base URL

Trong một dự án, nhiều API thường sử dụng chung một địa chỉ gốc. Ví dụ:

  • /products

  • /users

  • /orders

  • /categories

Thay vì viết lại toàn bộ URL trong từng Request, có thể cấu hình baseUrl ngay khi khởi tạo Dio.

import 'package:dio/dio.dart';

class ApiClient {
  static final Dio dio = Dio(
    BaseOptions(
      baseUrl: 'https://api.example.com',
      connectTimeout: const Duration(seconds: 10),
      receiveTimeout: const Duration(seconds: 15),
      sendTimeout: const Duration(seconds: 15),
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
      },
    ),
  );
}

Sau khi cấu hình, có thể gọi API bằng đường dẫn tương đối:

final response = await ApiClient.dio.get('/products');

Dio sẽ kết hợp baseUrl với đường dẫn Request để tạo địa chỉ API hoàn chỉnh.

Trong dự án thực tế, nên tập trung cấu hình Dio tại một vị trí thay vì tạo nhiều đối tượng Client không cần thiết. Cách này giúp quản lý Header, Timeout, Interceptor và những quy tắc giao tiếp API dễ dàng hơn.

6. Cơ chế hoạt động của Dio trong Flutter

Quá trình giao tiếp API thường diễn ra theo trình tự sau:

 

Cơ chế này có thể được giải thích qua từng bước:

Bước 1: Người dùng thực hiện thao tác. Người dùng mở trang sản phẩm, đăng nhập hoặc yêu cầu tải dữ liệu. Giao diện Flutter kích hoạt một hàm xử lý tương ứng.

Bước 2: Service hoặc Repository tiếp nhận yêu cầu. Tầng này xác định API cần gọi, chuẩn bị tham số và phối hợp với các thành phần liên quan.

Bước 3: Dio tạo HTTP Request. Dio sử dụng URL, phương thức HTTP, Header, tham số và nội dung dữ liệu đã được cung cấp. Nếu có Interceptor, Request có thể được bổ sung thông tin xác thực hoặc ghi log trước khi gửi đi.

Bước 4: Backend tiếp nhận và xử lý. Máy chủ kiểm tra Request, xác thực người dùng, thực hiện nghiệp vụ và truy vấn cơ sở dữ liệu nếu cần. Sau đó, Backend tạo HTTP Response.

Bước 5: Dio nhận Response. Dio đọc phản hồi từ máy chủ và cung cấp dữ liệu, mã trạng thái cùng các thông tin liên quan. Những phản hồi không đáp ứng cấu hình xử lý thành công có thể được chuyển thành DioException.

Bước 6: Ứng dụng xử lý kết quả. Repository hoặc Service có thể chuyển đổi JSON thành Model, sau đó cung cấp dữ liệu cho hệ thống quản lý trạng thái. Giao diện được cập nhật để hiển thị dữ liệu mới hoặc thông báo lỗi.

Điểm quan trọng là Dio chỉ chịu trách nhiệm cho phần giao tiếp HTTP. Việc truy vấn cơ sở dữ liệu, xử lý nghiệp vụ và quyết định quyền truy cập thuộc về Backend; việc hiển thị dữ liệu thuộc về Flutter UI.

7. Các thành phần quan trọng trong Dio

7.1. Dio Instance

Dio Instance là đối tượng Client được tạo từ lớp Dio. Đối tượng này được sử dụng để gửi Request và quản lý các cấu hình giao tiếp mạng.

final dio = Dio();

Trong dự án nhỏ, có thể sử dụng một đối tượng Dio trực tiếp. Trong dự án lớn, nên đóng gói đối tượng này trong một lớp ApiClient hoặc một thành phần tương đương để các Service và Repository có thể dùng chung cấu hình.

Việc tái sử dụng Dio Instance giúp tránh tình trạng mỗi API sử dụng một cấu hình khác nhau, đồng thời hỗ trợ quản lý Interceptor và Header tập trung.

7.2. BaseOptions

BaseOptions là nơi khai báo những cấu hình mặc định được áp dụng cho các Request sử dụng Dio Instance tương ứng.

Các thuộc tính thường gặp gồm:

  • baseUrl: Địa chỉ gốc của API.

  • connectTimeout: Giới hạn thời gian thiết lập kết nối.

  • sendTimeout: Giới hạn thời gian gửi dữ liệu.

  • receiveTimeout: Giới hạn thời gian chờ nhận dữ liệu.

  • headers: Các Header mặc định.

  • queryParameters: Những tham số truy vấn mặc định khi phù hợp.

  • responseType: Kiểu dữ liệu mong muốn khi nhận Response.

  • contentType: Kiểu nội dung được sử dụng cho Request.

Ví dụ:

final dio = Dio(
  BaseOptions(
    baseUrl: 'https://api.example.com',
    connectTimeout: const Duration(seconds: 10),
    receiveTimeout: const Duration(seconds: 15),
    headers: {
      'Accept': 'application/json',
    },
  ),
);

Cần hiểu rằng Timeout không phải lúc nào cũng có nghĩa là toàn bộ thao tác mạng sẽ kết thúc trong một khoảng thời gian cố định. Mỗi loại Timeout áp dụng cho một giai đoạn khác nhau, và chi tiết hành vi có thể phụ thuộc vào nền tảng cũng như phiên bản Dio.

7.3. Options

Options cho phép tùy chỉnh cấu hình cho một Request cụ thể.

Ví dụ, một API cần Header riêng hoặc cần chỉ định cách xử lý mã trạng thái khác với cấu hình mặc định.

final response = await dio.get(
  '/products',
  options: Options(
    headers: {
      'Accept-Language': 'vi',
    },
  ),
);

BaseOptions phù hợp với các thiết lập dùng chung, trong khi Options phù hợp với những thiết lập cần áp dụng cho từng Request.

7.4. Response

Response là đối tượng chứa kết quả trả về từ Request.

Một số thuộc tính thường được sử dụng:

  • data: Nội dung phản hồi.

  • statusCode: Mã trạng thái HTTP.

  • statusMessage: Thông báo trạng thái nếu có.

  • headers: Header của Response.

  • requestOptions: Thông tin về Request đã tạo ra Response.

Ví dụ:

final response = await dio.get('/products');

print(response.statusCode);
print(response.data);
print(response.headers);

Nếu API trả JSON và Dio giải mã thành dữ liệu Dart, response.data có thể là Map, List, chuỗi hoặc một kiểu dữ liệu khác tùy theo nội dung và cấu hình phản hồi.

7.5. DioException

DioException là kiểu ngoại lệ được Dio sử dụng để biểu diễn nhiều loại lỗi liên quan đến quá trình gửi Request hoặc xử lý Response.

Một số trường hợp thường gặp:

  • Không thể kết nối đến máy chủ.

  • Hết thời gian chờ.

  • Request bị hủy.

  • Máy chủ trả về mã trạng thái được cấu hình là lỗi.

  • Lỗi phát sinh trong quá trình truyền hoặc xử lý dữ liệu.

Ví dụ:

try {
  final response = await dio.get('/products');
  print(response.data);
} on DioException catch (e) {
  print('Loại lỗi: ${e.type}');
  print('Thông báo: ${e.message}');
  print('Mã HTTP: ${e.response?.statusCode}');
}

Không nên chỉ hiển thị nguyên văn e.message cho người dùng cuối. Thông báo kỹ thuật có thể khó hiểu hoặc tiết lộ chi tiết không cần thiết. Nên chuyển đổi lỗi thành thông báo phù hợp với ngữ cảnh của ứng dụng.

8. Interceptor trong Dio

8.1. Khái niệm Interceptor

Interceptor là cơ chế cho phép can thiệp vào quá trình gửi Request, nhận Response và xử lý lỗi mà không cần lặp lại cùng một đoạn mã trong từng API.

Trong một ứng dụng Flutter, nhiều Request có thể cần cùng một hành vi, chẳng hạn:

  • Gắn Access Token vào Header.

  • Ghi log phương thức, URL và thời gian gọi API.

  • Chuẩn hóa một số thông tin của Response.

  • Xử lý lỗi xác thực.

  • Điều phối quy trình làm mới Token.

  • Theo dõi lỗi trong quá trình phát triển.

Nếu không sử dụng Interceptor, lập trình viên có thể phải viết những logic này trong nhiều hàm khác nhau. Khi yêu cầu thay đổi, việc sửa từng nơi sẽ tốn thời gian và dễ gây thiếu nhất quán.

Interceptor giúp tập trung những quy tắc giao tiếp chung vào một vị trí.

8.2. Các loại Interceptor

Dio cung cấp những phương thức chính sau trong InterceptorsWrapper:

  • onRequest: Được gọi trong quá trình xử lý Request trước khi Request được gửi đi.

  • onResponse: Được gọi khi nhận được Response để xử lý phản hồi.

  • onError: Được gọi khi xảy ra lỗi theo cơ chế xử lý của Dio.

Ví dụ:

final dio = Dio(
  BaseOptions(
    baseUrl: 'https://api.example.com',
  ),
);

dio.interceptors.add(
  InterceptorsWrapper(
    onRequest: (options, handler) {
      print('Request: ${options.method} ${options.uri}');
      handler.next(options);
    },
    onResponse: (response, handler) {
      print('Status: ${response.statusCode}');
      handler.next(response);
    },
    onError: (error, handler) {
      print('Error: ${error.message}');
      handler.next(error);
    },
  ),
);

Các lệnh handler.next(...) cho phép quá trình xử lý tiếp tục với đối tượng tương ứng. Nếu Interceptor không gọi tiếp, không trả về một kết quả thay thế và cũng không kết thúc xử lý theo cách phù hợp, Request có thể bị treo.

Ngoài ra, Interceptor có thể sửa đổi Request, thay thế Response hoặc xử lý lỗi theo yêu cầu của ứng dụng. Vì vậy, khi xây dựng Interceptor, cần kiểm soát rõ luồng tiếp tục, kết thúc hoặc chuyển đổi kết quả.

8.3. Gắn Access Token bằng Interceptor

Trong ứng dụng có chức năng đăng nhập, Backend thường cấp Access Token để xác thực những Request cần quyền truy cập.

Thay vì gắn Token thủ công vào từng lời gọi API, có thể sử dụng Interceptor để thêm Header Authorization.

dio.interceptors.add(
  InterceptorsWrapper(
    onRequest: (options, handler) {
      final token = accessToken;

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

      handler.next(options);
    },
  ),
);

Đây là đoạn mã minh họa, trong đó accessToken là giá trị được lấy từ một thành phần quản lý phiên đăng nhập của ứng dụng.

Không nên lưu Access Token nhạy cảm trong một biến cố định rồi coi đó là giải pháp quản lý phiên hoàn chỉnh. Trong ứng dụng thực tế, Token cần được lấy từ nơi lưu trữ phù hợp, có cơ chế cập nhật và xóa khi đăng xuất hoặc phiên hết hiệu lực.

8.4. Xử lý Token hết hạn

Khi Access Token hết hạn, Backend có thể trả về mã 401 Unauthorized. Ứng dụng có thể sử dụng Interceptor để phối hợp với quy trình Refresh Token, lấy Access Token mới và gửi lại Request nếu phù hợp.

Một quy trình cơ bản gồm:

  1. Gửi Request với Access Token hiện tại.

  2. Nhận phản hồi cho biết Token không còn hợp lệ.

  3. Kiểm tra lỗi có thuộc trường hợp cần làm mới Token hay không.

  4. Gửi yêu cầu Refresh Token đến API tương ứng.

  5. Cập nhật Access Token mới vào nơi lưu trữ.

  6. Gửi lại Request ban đầu nếu việc làm mới thành công.

  7. Nếu không thể làm mới Token, xóa phiên đăng nhập và yêu cầu người dùng đăng nhập lại.

Cần thiết kế cẩn thận để tránh vòng lặp vô hạn khi Refresh Token cũng bị lỗi. Đồng thời, nếu nhiều Request cùng nhận lỗi 401, ứng dụng nên kiểm soát việc làm mới Token để tránh gửi quá nhiều yêu cầu Refresh đồng thời.

Không phải mọi lỗi 401 đều có nghĩa là Access Token hết hạn. Một số trường hợp có thể liên quan đến thông tin xác thực không hợp lệ hoặc chính sách bảo mật của Backend. Vì vậy, cần căn cứ vào hợp đồng API của hệ thống để quyết định cách xử lý.

9. Truyền dữ liệu bằng Query Parameters và Request Body

9.1. Query Parameters

Query Parameters được sử dụng để truyền những tham số thông qua URL. Các tham số này thường phục vụ chức năng tìm kiếm, lọc và phân trang.

Ví dụ, lấy trang thứ hai với 20 sản phẩm mỗi trang:

final response = await dio.get(
  '/products',
  queryParameters: {
    'page': 2,
    'limit': 20,
  },
);

Dio sẽ xây dựng URL chứa các tham số truy vấn tương ứng.

Cách truyền này giúp tránh việc phải tự nối chuỗi URL và tự mã hóa các ký tự đặc biệt trong giá trị tham số.

9.2. Request Body

Request Body chứa dữ liệu được gửi đến máy chủ, thường gặp ở các thao tác tạo mới hoặc cập nhật tài nguyên.

final response = await dio.post(
  '/products',
  data: {
    'name': 'Áo thun',
    'price': 250000,
    'quantity': 50,
  },
);

Trong trường hợp này, data chứa các trường dữ liệu cần gửi. Với API nhận JSON, Backend cần được cấu hình để đọc nội dung JSON tương ứng.

Lập trình viên nên tuân theo cấu trúc dữ liệu mà Backend quy định, bao gồm tên trường, kiểu dữ liệu, trường bắt buộc và quy tắc kiểm tra dữ liệu.

10. Tải lên tệp bằng FormData

10.1. Khái niệm FormData

FormData được sử dụng khi cần gửi dữ liệu dạng multipart/form-data, thường áp dụng cho những Request chứa cả trường văn bản và tệp.

Ví dụ trong ứng dụng thương mại điện tử, người quản trị cần tạo sản phẩm mới với tên, giá, mô tả và hình ảnh. Các trường văn bản cùng hình ảnh có thể được gửi trong một Request nếu Backend hỗ trợ định dạng này.

Dio cung cấp FormData và MultipartFile để hỗ trợ thao tác đó.

10.2. Ví dụ tải ảnh lên máy chủ

import 'package:dio/dio.dart';

Future<void> uploadImage(
  Dio dio,
  String imagePath,
) async {
  try {
    final formData = FormData.fromMap({
      'image': await MultipartFile.fromFile(
        imagePath,
        filename: 'product.jpg',
      ),
    });

    final response = await dio.post(
      '/upload',
      data: formData,
      onSendProgress: (sent, total) {
        if (total > 0) {
          final progress = sent / total;
          print('Tiến trình tải lên: ${(progress * 100).toStringAsFixed(0)}%');
        }
      },
    );

    print(response.data);
  } on DioException catch (e) {
    print('Tải ảnh thất bại: ${e.message}');
  }
}

Đoạn mã trên minh họa việc gửi tệp từ một đường dẫn có sẵn trên hệ thống tệp. Khi sử dụng trong Flutter, cần bảo đảm imagePath hợp lệ và phù hợp với nền tảng đang chạy. Trên ứng dụng web, cách truy cập tệp khác với Android hoặc iOS.

Backend cũng cần được cấu hình để tiếp nhận multipart/form-data, kiểm tra định dạng, giới hạn dung lượng và xử lý tệp an toàn.

Không nên mặc định tin tưởng tên tệp hoặc phần mở rộng do Client gửi lên. Những kiểm tra quan trọng như loại tệp, kích thước và quyền truy cập cần được thực hiện ở phía máy chủ.

11. Tải xuống tệp và theo dõi tiến trình

Dio hỗ trợ tải dữ liệu về một đường dẫn thông qua phương thức download(). Chức năng này có thể được sử dụng để tải hóa đơn, tài liệu, hình ảnh hoặc các tệp khác do máy chủ cung cấp.

Ví dụ:

Future<void> downloadFile(Dio dio) async {
  try {
    final response = await dio.download(
      'https://api.example.com/files/report.pdf',
      '/path/to/report.pdf',
      onReceiveProgress: (received, total) {
        if (total > 0) {
          final progress = received / total;
          print('Tiến trình tải xuống: ${(progress * 100).toStringAsFixed(0)}%');
        }
      },
    );

    print('Tải xuống hoàn tất: ${response.statusCode}');
  } on DioException catch (e) {
    print('Lỗi tải xuống: ${e.message}');
  }
}

Đường dẫn lưu tệp trong ví dụ chỉ mang tính minh họa. Ứng dụng thực tế cần xác định thư mục lưu trữ hợp lệ theo nền tảng, quyền truy cập và yêu cầu của hệ điều hành.

Tiến trình tải xuống có thể được sử dụng để cập nhật thanh tiến trình hoặc thông báo cho người dùng về trạng thái tải tệp.

Khi tổng dung lượng chưa xác định, giá trị total có thể không hữu ích cho việc tính phần trăm. Trong trường hợp đó, giao diện nên hiển thị trạng thái tải thay vì tự tính một tỷ lệ không chính xác.

12. Xử lý lỗi trong Dio

12.1. Phân loại lỗi

Xử lý lỗi là một phần quan trọng khi làm việc với HTTP Client, bởi quá trình giao tiếp mạng không phải lúc nào cũng thành công.

Một số nhóm lỗi thường gặp:

  • Lỗi kết nối: Thiết bị không thể kết nối đến máy chủ.

  • Lỗi Timeout: Quá trình kết nối, gửi hoặc nhận dữ liệu vượt quá giới hạn thời gian.

  • Lỗi HTTP: Server trả về mã trạng thái cho biết Request không được xử lý thành công theo cấu hình.

  • Lỗi hủy Request: Yêu cầu bị hủy bởi ứng dụng hoặc người dùng.

  • Lỗi dữ liệu: Nội dung phản hồi không đúng cấu trúc mà ứng dụng mong đợi.

  • Lỗi nghiệp vụ: Backend trả về thông báo về điều kiện nghiệp vụ không được đáp ứng.

Cần phân biệt lỗi HTTP với lỗi nghiệp vụ. Ví dụ, Backend có thể trả mã 200 OK nhưng nội dung JSON lại thể hiện một điều kiện nghiệp vụ thất bại theo quy ước riêng. Ngược lại, một mã 500 cho biết máy chủ gặp lỗi khi xử lý yêu cầu nhưng không nhất thiết tiết lộ nguyên nhân cụ thể.

12.2. Ví dụ xử lý lỗi theo mã trạng thái

Future<void> loadProducts(Dio dio) async {
  try {
    final response = await dio.get('/products');

    print('Dữ liệu: ${response.data}');
  } on DioException catch (e) {
    switch (e.response?.statusCode) {
      case 400:
        print('Dữ liệu yêu cầu không hợp lệ');
        break;
      case 401:
        print('Cần xác thực hoặc đăng nhập lại');
        break;
      case 403:
        print('Không có quyền thực hiện thao tác');
        break;
      case 404:
        print('Không tìm thấy tài nguyên');
        break;
      case 500:
        print('Máy chủ gặp lỗi');
        break;
      default:
        print('Lỗi giao tiếp: ${e.message}');
    }
  }
}

Đây là ví dụ cơ bản, chưa bao gồm đầy đủ các loại lỗi. Trong thực tế, có thể kiểm tra thêm e.type, trạng thái kết nối, lỗi chứng chỉ và những trường hợp đặc thù theo nền tảng.

Cũng cần lưu ý rằng validateStatus trong cấu hình Dio ảnh hưởng đến việc mã trạng thái nào được xem là thành công. Vì vậy, không nên chỉ dựa vào một danh sách mã cố định mà bỏ qua cấu hình của HTTP Client.

12.3. Nguyên tắc xử lý lỗi

Một giải pháp tốt nên tách biệt thông tin dành cho lập trình viên và thông báo dành cho người dùng.

Ví dụ, lỗi kết nối có thể được hiển thị thành thông báo yêu cầu người dùng kiểm tra mạng. Lỗi xác thực có thể dẫn đến quy trình đăng nhập lại. Lỗi máy chủ có thể hiển thị thông báo thử lại sau.

Không nên tự động gửi lại tất cả Request khi xảy ra lỗi. Việc gửi lại các thao tác tạo đơn hàng hoặc thanh toán có thể gây ra tác dụng phụ nếu máy chủ đã xử lý Request trước khi kết nối bị gián đoạn.

Cơ chế Retry cần xác định loại lỗi có thể thử lại, số lần thử, khoảng thời gian chờ và tính an toàn của thao tác. Đối với những nghiệp vụ nhạy cảm, Backend có thể cần hỗ trợ khóa chống xử lý trùng lặp như Idempotency Key.

13. Hủy Request bằng CancelToken

Trong ứng dụng Flutter, đôi khi Request không còn cần thiết trước khi hoàn thành. Ví dụ, người dùng rời khỏi trang tìm kiếm hoặc thay đổi từ khóa liên tục khiến những Request cũ không còn hữu ích.

Dio cung cấp CancelToken để hỗ trợ hủy Request.

final cancelToken = CancelToken();

Future<void> searchProducts(Dio dio, String keyword) async {
  try {
    final response = await dio.get(
      '/products/search',
      queryParameters: {
        'keyword': keyword,
      },
      cancelToken: cancelToken,
    );

    print(response.data);
  } on DioException catch (e) {
    if (CancelToken.isCancel(e)) {
      print('Request đã bị hủy');
    } else {
      print('Lỗi: ${e.message}');
    }
  }
}

Khi cần hủy Request, có thể gọi:

cancelToken.cancel('Không còn cần kết quả tìm kiếm');

Trong ứng dụng thực tế, CancelToken nên được quản lý theo vòng đời của thao tác hoặc màn hình. Cần tránh sử dụng chung một Token cho nhiều Request độc lập nếu việc hủy một thao tác có thể vô tình hủy cả những thao tác khác.

Việc hủy Request cũng không đảm bảo rằng Backend đã ngừng xử lý nghiệp vụ. Request có thể đã đến máy chủ trước khi Client hủy kết nối. Do đó, không nên sử dụng hủy Request như một cơ chế thay thế cho việc kiểm soát giao dịch ở phía Backend.

14. Tổ chức Dio theo kiến trúc Service và Repository

14.1. Vai trò của từng tầng

Trong ứng dụng Flutter, có thể tách phần giao tiếp API khỏi giao diện bằng cách sử dụng các tầng như API Client, Service, Repository và Model.

API Client chịu trách nhiệm cấu hình Dio, bao gồm Base URL, Timeout, Header và Interceptor.

Service đóng gói những thao tác giao tiếp với API, chẳng hạn lấy danh sách sản phẩm hoặc tạo đơn hàng.

Repository cung cấp giao diện truy cập dữ liệu cho những tầng khác của ứng dụng. Repository có thể phối hợp dữ liệu từ API, bộ nhớ đệm hoặc nguồn dữ liệu cục bộ tùy theo kiến trúc.

Model biểu diễn dữ liệu dưới dạng các đối tượng Dart để thuận tiện cho việc xử lý và kiểm tra kiểu dữ liệu.

UI và State Management tiếp nhận dữ liệu từ Repository hoặc Service thông qua cơ chế quản lý trạng thái, sau đó cập nhật giao diện.

Việc tách tầng giúp giao diện không phải trực tiếp xây dựng URL, gắn Token hoặc tự xử lý từng chi tiết HTTP. Đồng thời, cách tổ chức này giúp dễ kiểm thử và thay đổi cách giao tiếp API khi cần.

14.2. Cấu trúc thư mục tham khảo

Một dự án Flutter có thể tổ chức phần giao tiếp API như sau:

lib/
├── core/
│   └── network/
│       └── api_client.dart
├── features/
│   └── products/
│       ├── data/
│       │   ├── models/
│       │   │   └── product.dart
│       │   ├── services/
│       │   │   └── product_service.dart
│       │   └── repositories/
│       │       └── product_repository.dart
│       └── presentation/
│           ├── screens/
│           │   └── product_screen.dart
│           └── providers/
│               └── product_provider.dart
└── main.dart

Đây là cấu trúc tham khảo, không phải quy tắc bắt buộc của Flutter. Dự án nhỏ có thể dùng cấu trúc đơn giản hơn, còn dự án lớn có thể tổ chức theo Clean Architecture hoặc Feature-first Architecture.

14.3. Xây dựng ApiClient

Tạo tệp lib/core/network/api_client.dart:

import 'package:dio/dio.dart';

class ApiClient {
  ApiClient()
      : dio = Dio(
          BaseOptions(
            baseUrl: 'https://api.example.com',
            connectTimeout: const Duration(seconds: 10),
            sendTimeout: const Duration(seconds: 15),
            receiveTimeout: const Duration(seconds: 15),
            headers: {
              'Accept': 'application/json',
            },
          ),
        ) {
    dio.interceptors.add(
      InterceptorsWrapper(
        onRequest: (options, handler) {
          print('API Request: ${options.method} ${options.uri}');
          handler.next(options);
        },
        onResponse: (response, handler) {
          print('API Status: ${response.statusCode}');
          handler.next(response);
        },
        onError: (error, handler) {
          print('API Error: ${error.message}');
          handler.next(error);
        },
      ),
    );
  }

  final Dio dio;
}

Lớp ApiClient đóng vai trò quản lý một Dio Instance với cấu hình thống nhất. Khi cần thay đổi Base URL hoặc bổ sung Interceptor, có thể thực hiện tập trung tại đây.

Trong môi trường phát triển, log Request và lỗi có thể hỗ trợ việc gỡ lỗi. Tuy nhiên, khi phát hành ứng dụng, cần tránh ghi lại mật khẩu, Access Token hoặc những dữ liệu nhạy cảm.

14.4. Xây dựng Model

Tạo tệp lib/features/products/data/models/product.dart:

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

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

  factory Product.fromJson(Map<String, dynamic> json) {
    return Product(
      id: (json['id'] as num).toInt(),
      name: json['name'] as String,
      price: (json['price'] as num).toDouble(),
    );
  }
}

Model Product biểu diễn một sản phẩm với ba thuộc tính: mã sản phẩm, tên và giá.

Phương thức fromJson() chuyển dữ liệu dạng Map sang đối tượng Dart có kiểu dữ liệu rõ ràng. Việc sử dụng num cho trường giá cho phép tiếp nhận cả số nguyên và số thập phân trước khi chuyển thành double.

Trong ứng dụng thực tế, cần điều chỉnh Model theo cấu trúc JSON thực tế của Backend, bao gồm trường có thể bị thiếu, giá trị null, định dạng ngày tháng và những kiểu dữ liệu đặc thù.

14.5. Xây dựng ProductService

Tạo tệp lib/features/products/data/services/product_service.dart:

import 'package:dio/dio.dart';
import '../models/product.dart';

class ProductService {
  ProductService(this.dio);

  final Dio dio;

  Future<List<Product>> getProducts() async {
    final response = await dio.get('/products');

    final data = response.data as List<dynamic>;

    return data
        .map(
          (item) => Product.fromJson(
            item as Map<String, dynamic>,
          ),
        )
        .toList();
  }
}

ProductService chịu trách nhiệm gọi API lấy danh sách sản phẩm và chuyển đổi dữ liệu JSON thành danh sách Product.

Việc truyền Dio thông qua constructor giúp Service không phải tự tạo một HTTP Client mới. Đây cũng là một ví dụ về Dependency Injection, bởi đối tượng phụ thuộc được cung cấp từ bên ngoài.

Đoạn mã trên giả định API trả về một mảng JSON trực tiếp. Nếu Backend trả dữ liệu theo cấu trúc như {"success": true, "data": [...]}, cần lấy danh sách từ trường data thay vì ép kiểu toàn bộ response.data thành List.

14.6. Sử dụng Service trong ứng dụng

Có thể khởi tạo và gọi Service như sau:

final apiClient = ApiClient();
final productService = ProductService(apiClient.dio);

Future<void> loadProducts() async {
  try {
    final products = await productService.getProducts();

    for (final product in products) {
      print('${product.name}: ${product.price}');
    }
  } on DioException catch (e) {
    print('Không thể lấy dữ liệu: ${e.message}');
  } catch (e) {
    print('Lỗi xử lý dữ liệu: $e');
  }
}

Ví dụ này minh họa cách sử dụng Service độc lập với giao diện. Trong ứng dụng thực tế, có thể kết hợp với Provider, Riverpod, BLoC hoặc một giải pháp quản lý trạng thái khác để đưa dữ liệu lên UI.

15. Kết hợp Dio với quản lý trạng thái Flutter

Dio xử lý giao tiếp mạng nhưng không tự quản lý toàn bộ trạng thái giao diện. Khi gọi API, ứng dụng thường cần thể hiện ít nhất ba trạng thái:

  • Loading: Request đang được xử lý.

  • Success: Dữ liệu đã được lấy thành công.

  • Error: Request thất bại hoặc dữ liệu không thể xử lý.

Ví dụ, khi người dùng mở trang sản phẩm, giao diện có thể hiển thị vòng tròn tải trong lúc gọi API. Sau khi nhận được dữ liệu, danh sách sản phẩm xuất hiện. Nếu xảy ra lỗi, giao diện có thể hiển thị thông báo cùng nút thử lại.

Có thể quản lý những trạng thái này bằng setState() trong ứng dụng nhỏ hoặc sử dụng Provider, Riverpod và BLoC trong các ứng dụng có yêu cầu phức tạp hơn.

Điều quan trọng là không nên gọi API liên tục chỉ vì Widget được dựng lại. Nếu lời gọi API được đặt trực tiếp trong build() mà không có cơ chế kiểm soát, ứng dụng có thể gửi nhiều Request không cần thiết.

Thay vào đó, nên gọi API trong những điểm phù hợp với vòng đời Widget hoặc trong các phương thức của lớp quản lý trạng thái. Cần có cơ chế kiểm soát việc tải lại, xử lý lỗi và hủy thao tác khi cần.

16. So sánh Dio và package HTTP

Trong Flutter, dio và http đều có thể được sử dụng để gửi Request và nhận Response từ máy chủ. Hai package có cách tổ chức API và mức độ hỗ trợ tính năng khác nhau.

Package http cung cấp giao diện tương đối đơn giản cho các thao tác HTTP thông dụng. Với ứng dụng nhỏ, chỉ cần gọi một vài API và không có nhiều yêu cầu tùy chỉnh, package này có thể đáp ứng nhu cầu.

Dio cung cấp thêm những tiện ích như Interceptor, cấu hình theo từng Client, theo dõi tiến trình truyền dữ liệu, hủy Request bằng CancelToken và hỗ trợ thuận tiện cho những tình huống tải lên hoặc tải xuống tệp.

Việc lựa chọn không nhất thiết phụ thuộc vào việc package nào phổ biến hơn. Lập trình viên cần xem xét độ phức tạp của ứng dụng, yêu cầu bảo trì, cách xử lý xác thực, nhu cầu tải tệp và khả năng tích hợp với kiến trúc hiện tại.

Một điểm cần phân biệt là HTTP Client là khái niệm chung về thành phần giao tiếp HTTP, trong khi Dio và package http là những thư viện cụ thể có thể đảm nhiệm vai trò đó trong Dart.

17. Bảo mật khi sử dụng Dio

Dio không tự bảo đảm an toàn cho toàn bộ dữ liệu truyền giữa ứng dụng và máy chủ. Bảo mật phụ thuộc vào cấu hình kết nối, cách quản lý thông tin xác thực và việc triển khai Backend.

Một số nguyên tắc quan trọng cần tuân thủ:

Sử dụng HTTPS. Trong môi trường thực tế, nên sử dụng HTTPS để mã hóa dữ liệu truyền qua mạng và bảo vệ thông tin khỏi nguy cơ bị đọc trộm trên đường truyền.

Quản lý Token đúng cách. Access Token và Refresh Token cần được lưu trữ phù hợp với nền tảng. Không nên đưa Token vào mã nguồn công khai hoặc ghi chúng vào log.

Không tin tưởng dữ liệu từ Client. Dữ liệu gửi từ Flutter cần được Backend xác thực và kiểm tra. Việc ẩn một nút chức năng trên giao diện không có nghĩa là người dùng không thể gửi Request trực tiếp đến API.

Kiểm soát thông tin trong log. Log nên tập trung vào những thông tin phục vụ gỡ lỗi và giám sát, tránh ghi lại mật khẩu, Token, thông tin cá nhân hoặc dữ liệu nhạy cảm.

Không tắt kiểm tra chứng chỉ trong môi trường thực tế. Việc bỏ qua xác thực chứng chỉ TLS có thể khiến kết nối dễ bị tấn công. Nếu cần cấu hình chứng chỉ đặc biệt, phải thực hiện theo một phương án bảo mật phù hợp.

Kiểm soát quyền truy cập ở Backend. Backend cần kiểm tra quyền trên từng tài nguyên và từng thao tác. Client chỉ có vai trò gửi yêu cầu và hiển thị kết quả, không phải thành phần quyết định cuối cùng về quyền truy cập.

Quản lý lỗi hợp lý. Thông báo lỗi dành cho người dùng nên dễ hiểu, trong khi chi tiết kỹ thuật được lưu ở hệ thống giám sát phù hợp và không làm lộ thông tin nội bộ.

18. Kiểm thử và bảo trì mã nguồn sử dụng Dio

Khi dự án có nhiều API, việc kiểm thử tầng giao tiếp mạng giúp giảm rủi ro khi thay đổi cấu trúc dữ liệu hoặc xử lý lỗi.

Một số nội dung nên được kiểm thử:

  • Request sử dụng đúng phương thức HTTP.

  • URL và Query Parameters được tạo đúng.

  • Header xác thực được gửi đúng khi cần.

  • Request Body khớp với cấu trúc mà Backend yêu cầu.

  • JSON được chuyển đổi thành Model chính xác.

  • Lỗi HTTP được chuyển đổi thành trạng thái phù hợp.

  • Timeout và việc hủy Request được xử lý đúng.

  • Interceptor không gây vòng lặp hoặc chặn Request ngoài ý muốn.

  • Dữ liệu nhạy cảm không xuất hiện trong log.

Để kiểm thử dễ dàng, nên thiết kế Service nhận Dio thông qua constructor thay vì tự khởi tạo Client bên trong mỗi phương thức. Cách tổ chức này cho phép thay thế thành phần giao tiếp mạng bằng một cấu hình giả lập trong quá trình kiểm thử.

Ví dụ, khi kiểm tra ProductService, có thể mô phỏng API trả về danh sách sản phẩm, trả lỗi 404 hoặc trả dữ liệu không hợp lệ. Từ đó xác minh rằng Service xử lý đúng kết quả trong từng trường hợp.

Ngoài kiểm thử đơn vị, cần kiểm thử tích hợp với Backend thực tế để xác nhận tính tương thích giữa cấu trúc Request, Response và hợp đồng API của hệ thống.

19. Những lỗi thường gặp khi sử dụng Dio

19.1. Sai Base URL hoặc đường dẫn API

Khi baseUrl và đường dẫn được kết hợp không đúng, Request có thể gửi đến địa chỉ không tồn tại hoặc sai môi trường.

Cần kiểm tra địa chỉ gốc, đường dẫn, cấu hình môi trường phát triển và địa chỉ máy chủ khi chạy trên thiết bị thật.

19.2. Không xử lý dữ liệu JSON đúng cấu trúc

Không phải API nào cũng trả về List hoặc Map trực tiếp. Nếu ứng dụng ép kiểu sai, lỗi có thể xảy ra trong quá trình chuyển đổi dữ liệu.

Cần kiểm tra cấu trúc Response thực tế trước khi viết Model và Service.

19.3. Quên sử dụng await

Các phương thức gửi Request của Dio thường trả về Future. Nếu không chờ kết quả đúng cách, dữ liệu có thể chưa sẵn sàng tại thời điểm sử dụng hoặc lỗi không được xử lý tại vị trí mong muốn.

Cần sử dụng async và await phù hợp, đồng thời tránh chặn luồng giao diện bằng những thao tác đồng bộ kéo dài.

19.4. Gọi API nhiều lần ngoài ý muốn

Việc đặt lời gọi API trực tiếp trong build() hoặc kích hoạt Request mỗi lần trạng thái thay đổi không kiểm soát có thể dẫn đến các yêu cầu trùng lặp.

Nên tách logic lấy dữ liệu ra khỏi phần dựng giao diện và sử dụng cơ chế quản lý trạng thái phù hợp.

19.5. Xử lý lỗi không đầy đủ

Nếu chỉ bắt lỗi Dio mà bỏ qua lỗi chuyển đổi JSON hoặc lỗi nghiệp vụ, ứng dụng vẫn có thể gặp lỗi chưa được xử lý.

Nên phân biệt lỗi mạng, lỗi HTTP, lỗi dữ liệu và lỗi nghiệp vụ để có cách phản hồi tương ứng.

19.6. Không quản lý vòng đời Request

Một Request có thể hoàn thành sau khi người dùng đã rời khỏi màn hình. Nếu ứng dụng tiếp tục cập nhật trạng thái của Widget đã bị loại bỏ, có thể phát sinh lỗi hoặc xử lý dữ liệu không còn cần thiết.

Nên kiểm soát vòng đời thao tác, sử dụng cơ chế hủy Request khi phù hợp và tuân thủ quy tắc cập nhật trạng thái của Flutter.

20. Kết luận

Dio và HTTP Client là những kiến thức quan trọng trong quá trình phát triển ứng dụng Flutter có giao tiếp với Backend API. HTTP Client cung cấp khả năng gửi Request và nhận Response, trong khi Dio là một thư viện cụ thể giúp thực hiện những thao tác đó với nhiều tính năng hỗ trợ cho ứng dụng thực tế.

Việc hiểu rõ HTTP Request, HTTP Response, các phương thức HTTP, BaseOptions, Options, Interceptor, DioException, CancelToken và cơ chế truyền dữ liệu giúp lập trình viên xây dựng tầng giao tiếp mạng có tổ chức và dễ bảo trì hơn.

Trong một dự án Flutter, Dio nên được đặt ở tầng giao tiếp mạng, kết hợp với Service hoặc Repository để tách biệt việc gọi API khỏi giao diện. Dữ liệu nhận được cần được chuyển đổi thành Model, quản lý thông qua giải pháp State Management và hiển thị theo những trạng thái phù hợp.

Bên cạnh khả năng gửi và nhận dữ liệu, lập trình viên cần chú trọng đến xử lý lỗi, quản lý Timeout, bảo mật Token, kiểm tra dữ liệu và kiểm thử các tình huống mạng không ổn định. Những yếu tố này quyết định mức độ tin cậy của ứng dụng khi hoạt động trong môi trường thực tế.

Bài viết khác

REST API & HTTP trong Flutter

REST API & HTTP trong Flutter 1. REST API và HTTP 1.1. Khái niệm REST API REST API là một phương thức thiết kế giao tiếp giữa các hệ thống phần mềm thông qua giao thức HTTP. REST là viết tắt của Representational State Transfer, còn API là viết tắt của Application Programming Interface. REST […]

Flutter Widget Fundamentals – Tối ưu hiệu năng và kiểm thử Widget

Tối ưu hiệu năng và kiểm thử Widget trong Flutter 1. Rebuild Optimization – Tối ưu quá trình xây dựng lại giao diện 1.1. Khái niệm Rebuild Optimization Rebuild Optimization là quá trình tối ưu việc xây dựng lại giao diện trong Flutter nhằm giảm những lần thực thi build() không cần thiết, hạn chế […]

Equivalence Partitioning(Phân vùng tương đương)

1.Equivalence Partitioning (Phân vùng tương đương) là gì? Equivalence Partitioning (Phân vùng tương đương) là một trong những kỹ thuật thiết kế kiểm thử hộp đen (Black-box Testing Techniques) cơ bản và quan trọng nhất trong kỹ nghệ phần mềm. Kỹ thuật này được áp dụng nhằm giảm thiểu số lượng ca kiểm thử (Test […]

Flutter Widget Fundamentals – Các cơ chế UI nâng cao trong Flutter

Các cơ chế UI nâng cao trong Flutter 1. Gesture System và Hit Testing 1.1. Tổng quan về Gesture System Gesture System là hệ thống xử lý cử chỉ tương tác trong Flutter, cho phép ứng dụng nhận biết và phản hồi những hành động của người dùng như chạm, nhấn giữ, kéo thả, vuốt, […]

Flutter Widget Fundamentals – Vòng đời và quản lý trạng thái của Widget

Vòng đời và quản lý trạng thái của Widget trong Flutter 1. StatefulWidget Lifecycle chuyên sâu 1.1. Khái niệm vòng đời của StatefulWidget Trong Flutter, vòng đời của StatefulWidget là chuỗi các giai đoạn mà một Widget trải qua từ khi được tạo, khởi tạo trạng thái, hiển thị giao diện, nhận các thay đổi […]

UAT(Kiểm thử chấp nhận người dùng)

UAT là gì? Trong toàn bộ Vòng đời phát triển phần mềm (SDLC), sau khi trải qua hàng loạt các cấp độ kiểm thử kỹ thuật nội bộ như Unit Testing, Integration Testing, System Testing, sản phẩm phần mềm vẫn chưa thể chính thức phát hành. Bước đệm cuối cùng và mang tính chất quyết […]

Leave a Reply

Your email address will not be published. Required fields are marked *