Gọi API trong Flutter với package http: từ GET đến xử lý lỗi

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

Hầu hết ứng dụng thực tế đều cần lấy dữ liệu từ máy chủ: danh sách sản phẩm, tin tức, thông tin người dùng. Trong Flutter, cách đơn giản nhất để làm việc này là package http. Bài viết hướng dẫn gọi API, chuyển JSON thành đối tượng Dart và xử lý lỗi đúng cách.

Cài đặt package http

Thêm package vào dự án bằng lệnh sau, hoặc khai báo trong file pubspec.yaml:

flutter pub add http

Với Android, ứng dụng cần quyền truy cập mạng. Khi chạy ở chế độ debug quyền này đã có sẵn, nhưng khi phát hành bạn cần chắc chắn file AndroidManifest.xml có dòng khai báo android.permission.INTERNET. Template của Flutter thường đã thêm giúp bạn ở bản debug và profile, nên hãy kiểm tra lại cho bản release.

Tạo model từ JSON

Giả sử API trả về danh sách bài viết với các trường id, title và body. Ta tạo một lớp Dart để biểu diễn dữ liệu này:

class Post {
  final int id;
  final String title;
  final String body;

  const Post({required this.id, required this.title, required this.body});

  factory Post.fromJson(Map<String, dynamic> json) {
    return Post(
      id: json['id'] as int,
      title: json['title'] as String,
      body: json['body'] as String,
    );
  }
}

Dùng model có kiểu rõ ràng thay vì thao tác trực tiếp với Map giúp bạn bắt lỗi sớm và được trình soạn thảo gợi ý code.

Viết hàm gọi API

import 'dart:convert';
import 'package:http/http.dart' as http;

Future<List<Post>> fetchPosts() async {
  final uri = Uri.parse('https://jsonplaceholder.typicode.com/posts');
  final response = await http.get(uri).timeout(const Duration(seconds: 10));

  if (response.statusCode == 200) {
    final List<dynamic> data = jsonDecode(response.body);
    return data.map((e) => Post.fromJson(e as Map<String, dynamic>)).toList();
  } else {
    throw Exception('Lỗi máy chủ: ${response.statusCode}');
  }
}

Một vài điểm đáng chú ý:

  • Dùng Uri.parse thay vì chuỗi thô, với tham số truy vấn có thể dùng Uri.https để tự mã hóa.
  • Luôn đặt timeout để ứng dụng không chờ mãi khi mạng yếu.
  • Kiểm tra statusCode trước khi giải mã JSON.

Gửi dữ liệu với POST

Khi gửi JSON lên máy chủ, hãy đặt header Content-Type và mã hóa nội dung:

Future<void> createPost(String title, String body) async {
  final response = await http.post(
    Uri.parse('https://jsonplaceholder.typicode.com/posts'),
    headers: {'Content-Type': 'application/json'},
    body: jsonEncode({'title': title, 'body': body}),
  );
  if (response.statusCode != 201) {
    throw Exception('Không tạo được bài viết');
  }
}

Hiển thị dữ liệu bằng FutureBuilder

FutureBuilder giúp bạn xử lý ba trạng thái: đang tải, có lỗi, có dữ liệu. Lưu ý quan trọng là hãy tạo Future một lần (ví dụ trong initState), đừng gọi hàm fetch trực tiếp trong build vì mỗi lần vẽ lại sẽ gọi API thêm lần nữa.

late Future<List<Post>> _future;

@override
void initState() {
  super.initState();
  _future = fetchPosts();
}

@override
Widget build(BuildContext context) {
  return FutureBuilder<List<Post>>(
    future: _future,
    builder: (context, snapshot) {
      if (snapshot.connectionState != ConnectionState.done) {
        return const Center(child: CircularProgressIndicator());
      }
      if (snapshot.hasError) {
        return Center(child: Text('Có lỗi: ${snapshot.error}'));
      }
      final posts = snapshot.data!;
      return ListView.builder(
        itemCount: posts.length,
        itemBuilder: (_, i) => ListTile(title: Text(posts[i].title)),
      );
    },
  );
}

Những điều nên làm khi ứng dụng lớn dần

  1. Tách lớp gọi API ra một repository riêng, không gọi http trực tiếp trong widget.
  2. Xử lý lỗi mạng, lỗi quá hạn và lỗi định dạng dữ liệu thành các thông báo thân thiện.
  3. Không lưu khóa bí mật cứng trong code ứng dụng, vì mã di động có thể bị dịch ngược.
  4. Với nhu cầu phức tạp hơn như interceptor, tự động làm mới token, hãy cân nhắc package dio.
  5. Dùng thêm một thư viện sinh code như json_serializable khi model có nhiều trường.

Kết luận

Quy trình gọi API trong Flutter gồm vài bước nhất quán: gửi yêu cầu, kiểm tra mã phản hồi, giải mã JSON thành model rồi hiển thị theo từng trạng thái. Khi đã quen mẫu này, bạn có thể áp dụng cho hầu hết các ứng dụng cần dữ liệu từ máy chủ.

Tách tầng repository và phân loại lỗi

Khi ứng dụng lớn dần, gọi http trực tiếp trong widget sẽ khó kiểm thử. Hãy bọc lời gọi trong một lớp repository và phân loại lỗi để giao diện hiển thị thông báo phù hợp:

class ApiException implements Exception {
  final String message;
  const ApiException(this.message);
  @override
  String toString() => message;
}

class PostRepository {
  final http.Client _client;
  PostRepository([http.Client? client]) : _client = client ?? http.Client();

  Future<List<Post>> getPosts() async {
    try {
      final res = await _client
          .get(Uri.parse('https://jsonplaceholder.typicode.com/posts'))
          .timeout(const Duration(seconds: 10));
      if (res.statusCode != 200) {
        throw ApiException('Máy chủ trả về mã ${res.statusCode}');
      }
      final list = jsonDecode(res.body) as List<dynamic>;
      return list.map((e) => Post.fromJson(e as Map<String, dynamic>)).toList();
    } on TimeoutException {
      throw const ApiException('Kết nối quá chậm, vui lòng thử lại');
    } on SocketException {
      throw const ApiException('Không có kết nối mạng');
    }
  }
}

Nhận Client qua constructor cho phép bạn truyền một client giả trong bài kiểm thử, nhờ vậy kiểm thử không phải gọi mạng thật. Gói http còn cung cấp MockClient trong thư viện testing để làm việc này gọn gàng.

Nếu bạn muốn xây một ứng dụng hoàn chỉnh kết nối backend từ đầu đến cuối, hãy xem các khóa học tại codebem.com.

author_photo
Nguyễn Đình Cường

0 comment