Cách tổ chức thư mục dự án Flutter để dễ mở rộng và bảo trì

  • author-image

    Nguyễn Đình Cường

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

Dự án Flutter mới tạo chỉ có một thư mục lib với file main.dart. Sau vài tháng, nếu mọi thứ đều dồn vào đó, bạn sẽ mất nhiều thời gian chỉ để tìm file. Bài viết gợi ý cách tổ chức thư mục giúp dự án gọn gàng khi lớn dần.

Hai cách chia phổ biến

Chia theo loại (layer-first): gom file theo vai trò kỹ thuật như models, screens, widgets, services. Dễ hình dung lúc đầu, nhưng khi ứng dụng lớn, một tính năng nằm rải rác ở nhiều thư mục.

Chia theo tính năng (feature-first): mỗi tính năng như đăng nhập, giỏ hàng, hồ sơ có thư mục riêng chứa mọi thứ nó cần. Khi sửa một tính năng, bạn làm việc gần như trong một chỗ. Đây là cách được nhiều nhóm ưa chuộng cho dự án vừa và lớn.

Gợi ý cấu trúc feature-first

lib/
  main.dart
  app.dart                  # MaterialApp, theme, router
  core/
    constants/              # hằng số dùng chung
    network/                # cấu hình http/dio, xử lý lỗi
    theme/                  # màu, kiểu chữ
    utils/                  # hàm tiện ích
    widgets/                # widget dùng chung nhiều nơi
  features/
    auth/
      data/                 # api, repository, model từ JSON
      domain/               # thực thể, quy tắc nghiệp vụ
      presentation/         # màn hình, widget, state
    cart/
      data/
      domain/
      presentation/
    profile/
      data/
      domain/
      presentation/

Bạn không cần ba tầng data, domain, presentation cho mọi tính năng. Với tính năng đơn giản, chỉ cần một thư mục với vài file. Hãy tăng độ phức tạp khi thật sự cần.

Vai trò từng tầng

  • presentation: chứa màn hình, widget và logic trạng thái giao diện (Provider, Riverpod, Bloc...).
  • domain: các lớp thuần Dart mô tả khái niệm nghiệp vụ và giao diện repository, không phụ thuộc Flutter hay thư viện mạng.
  • data: triển khai repository, gọi API, đọc cơ sở dữ liệu, chuyển đổi JSON thành model.

Chiều phụ thuộc nên đi một hướng: presentation biết domain, data cũng biết domain, còn domain không biết ai cả. Nhờ vậy bạn đổi nguồn dữ liệu hay đổi thư viện giao diện mà ít ảnh hưởng đến phần còn lại.

Quy ước đặt tên

  1. Tên file dùng chữ thường, phân cách bằng dấu gạch dưới (snake_case): login_page.dart.
  2. Tên lớp dùng PascalCase: LoginPage.
  3. Hậu tố thể hiện vai trò: _page, _widget, _repository, _controller, _model.
  4. Mỗi file một lớp chính để dễ tìm kiếm.

Dùng file tổng hợp (barrel) có chừng mực

Bạn có thể tạo file xuất lại nhiều file trong một thư mục để import gọn:

// features/auth/auth.dart
export 'presentation/login_page.dart';
export 'domain/user.dart';

Dùng vừa phải, vì lạm dụng sẽ che giấu phụ thuộc thật và làm biên dịch chậm hơn.

Tách thành gói riêng khi lớn

Với dự án rất lớn hoặc nhiều ứng dụng dùng chung mã, bạn có thể tách phần dùng chung thành các gói cục bộ (local packages) trong cùng kho mã, ví dụ gói giao diện chung, gói mạng, gói từng tính năng. Cách này buộc ranh giới rõ ràng nhưng làm cấu hình phức tạp hơn, nên chỉ làm khi thật sự cần.

Những thứ không nên bỏ quên

  • Cấu hình môi trường: tách cấu hình cho phát triển, thử nghiệm và phát hành (flavors), không nhét địa chỉ máy chủ vào code lung tung.
  • Thư mục test: phản chiếu cấu trúc lib để dễ tìm bài kiểm thử tương ứng.
  • Tài nguyên: thư mục assets chia nhỏ (images, icons, fonts) và khai báo trong pubspec.yaml.
  • Phân tích mã: bật bộ quy tắc lint để cả nhóm viết theo cùng quy ước.

Đừng thiết kế quá sớm

Một sai lầm hay gặp là dựng cấu trúc khổng lồ cho ứng dụng chỉ có ba màn hình. Hãy bắt đầu đơn giản, và tái cấu trúc khi thấy dấu hiệu: file quá dài, khó tìm, hai người sửa đè lên nhau, hay một thay đổi nhỏ phải đụng nhiều thư mục.

Kết luận

Không có cấu trúc đúng tuyệt đối. Quan trọng là nhất quán, phù hợp quy mô và cả nhóm hiểu. Chia theo tính năng, giữ phụ thuộc một chiều và đặt tên rõ ràng sẽ giúp dự án dễ sống lâu.

Ví dụ tính năng giỏ hàng

Để cụ thể hơn, hãy hình dung tính năng giỏ hàng. Thư mục presentation chứa CartPage và các widget con như CartItemTile. Domain chứa lớp CartItem và giao diện CartRepository. Data chứa CartRepositoryImpl cùng lớp gọi API hoặc cơ sở dữ liệu cục bộ. Khi cần đổi nguồn dữ liệu từ bộ nhớ máy sang máy chủ, bạn chỉ thay phần triển khai ở data mà không đụng đến giao diện. Mỗi người trong nhóm cũng có thể nhận một tính năng riêng mà ít xung đột mã.

Nếu muốn xem cách tổ chức một dự án Flutter hoàn chỉnh, bạn có thể tham khảo các khóa học tại codebem.com.

author_photo
Nguyễn Đình Cường

0 comment