Hướng Nghiệp Dữ Liệu

Flutter: Xây dựng chức năng quản lý Tasks

Được viết bởi Admin|0 lượt xem
Flutter: Xây dựng chức năng quản lý Tasks

Tóm tắt nhanh

Hướng dẫn xây dựng chức năng quản lý task trong Flutter: mô hình dữ liệu, Provider, form nhập, lọc theo hạn, lưu trữ cục bộ và đồng bộ lên server.

Mục lục bài viết (32)

Quản lý tasks là một chức năng phổ biến trong các ứng dụng quản lý công việc, Todo List, dự án... Bài viết này hướng dẫn bạn xây dựng chức năng quản lý tasks hoàn chỉnh trong Flutter, sử dụng Provider để quản lý trạng thái và lưu trữ dữ liệu.

1. Mô hình dữ liệu Task

[object Object]

2. Provider quản lý danh sách tasks

[object Object]

3. Giao diện quản lý tasks

Hiển thị danh sách tasks

[object Object]

Thêm task mới

[object Object]

4. Lưu trữ tasks (local storage)

Bạn có thể sử dụng shared_preferences, hive hoặc sqflite để lưu trữ tasks. Ví dụ với shared_preferences:

[object Object]

5. Hình minh họa kiến trúc quản lý tasks

Task Manager Architecture

6. Best Practices

  • Sử dụng Provider hoặc Riverpod để quản lý state.
  • Tách biệt logic và UI.
  • Lưu trữ dữ liệu local hoặc cloud.
  • Sử dụng UUID cho id task.
  • Thêm xác nhận khi xóa task.

7. Tài liệu tham khảo

8. Sửa lại mô hình dữ liệu cho đúng ngay từ đầu

Đoạn code ở phần 1 chạy được nhưng có hai điểm sẽ gây bug khi dự án lớn dần: id sinh từ UniqueKey().toString() và object Task cho phép sửa trực tiếp từng trường.

Vấn đề của UniqueKey là nó chỉ duy nhất trong một phiên chạy. Sau khi lưu xuống máy và mở lại, bạn không còn cách nào biết bản ghi nào là bản ghi nào khi dữ liệu được đồng bộ lên server. Hãy dùng UUID hoặc để server cấp id.

Vấn đề thứ hai tinh vi hơn: object có thể sửa từng trường dẫn tới việc bạn vô tình đổi dữ liệu mà không gọi notifyListeners(), hoặc ngược lại gọi thông báo khi dữ liệu chưa thực sự đổi. Cách an toàn là để Task bất biến và tạo bản mới bằng copyWith.

[object Object]

Trường updatedAt không chỉ để hiển thị: khi đồng bộ, đây là thứ giúp bạn biết bản nào mới hơn. Thiếu nó, bạn sẽ gặp cảnh dữ liệu trên máy ghi đè lên dữ liệu mới hơn trên server.

9. Form thêm và sửa task có kiểm tra dữ liệu

Form là chỗ hay bị làm ẩu, dẫn tới task tiêu đề trống, hạn nằm trong quá khứ, hoặc người dùng mất dữ liệu đã nhập khi bấm nhầm.

[object Object]

Ba chi tiết đáng chú ý: kiểm tra mounted trước khi pop sau một hàm bất đồng bộ, giải phóng các TextEditingController trong dispose, và cắt khoảng trắng trước khi lưu. Cách xây dựng form chi tiết hơn, gồm cả kiểm tra theo từng trường, có ở bài form với TextFormField và validator.

10. Lọc, nhóm và sắp xếp theo hạn

Một danh sách phẳng sẽ nhanh chóng trở nên vô dụng khi có vài chục task. Người dùng thực tế cần bốn nhóm: Quá hạn, Hôm nay, Tuần này, Sau đó.

[object Object]

Hai lưu ý khi làm phần này:

  • So sánh theo ngày, không theo giờ. Nếu so trực tiếp với DateTime.now(), một task hạn hôm nay lúc 8h sẽ bị coi là quá hạn ngay lúc 8h01 dù người dùng vẫn đang trong ngày đó.
  • Cho phép truyền now vào hàm. Nhờ vậy bạn test được các trường hợp quá hạn mà không phải chờ tới ngày mai, và giao diện không bị lệch khi người dùng mở app qua nhiều ngày.

11. Xác nhận trước khi xoá và hoàn tác

Xoá là thao tác không thể lấy lại, nhưng bắt người dùng xác nhận bằng hộp thoại cho mọi trường hợp lại gây phiền. Cách cân bằng thường dùng: xoá có hiệu lực ngay, kèm một thanh thông báo có nút Hoàn tác trong vài giây.

[object Object]

Hàm restoreAt chỉ cần chèn lại đúng vị trí cũ. Điểm quan trọng về mặt trải nghiệm: người dùng được quyền sửa sai, nên không cần hộp thoại chặn dòng suy nghĩ của họ.

12. Lưu trữ cục bộ: dùng gì cho tính năng này

Với vài trăm task, ba lựa chọn đều đủ nhanh. Chọn theo nhu cầu truy vấn:

  • shared_preferences: đơn giản nhất, nhưng phải ghi–đọc cả danh sách mỗi lần thay đổi. Phù hợp dưới khoảng 100 task.
  • Hive: lưu theo khoá, ghi từng bản ghi, có sẵn cơ chế lắng nghe thay đổi. Đây là lựa chọn cân bằng nhất cho tính năng task.
  • sqflite hoặc drift: cần khi bạn muốn truy vấn phức tạp (lọc theo nhãn, thống kê theo tuần, tìm kiếm toàn văn). So sánh chi tiết ở bài so sánh Hive, SharedPreferences, SQLite.

Một điểm cần làm đúng: ghi dữ liệu theo từng thao tác, không phải ghi toàn bộ danh sách sau mỗi lần người dùng bấm. Cách này tránh việc mất dữ liệu khi ứng dụng bị đóng đột ngột và tránh giật giao diện vì phải mã hoá lại cả danh sách.

13. Đồng bộ lên server và xử lý khi mất mạng

Khi tính năng task cần dùng chung nhiều thiết bị, bạn bắt đầu phải đối mặt với ba vấn đề: thao tác khi không có mạng, thứ tự gửi, và xung đột.

Hướng làm phổ biến và đủ tốt cho phần lớn sản phẩm:

  1. Mọi thao tác ghi được đưa vào một hàng đợi cục bộ kèm trạng thái pending, và giao diện hiển thị ngay để người dùng không phải chờ.
  2. Khi có mạng, gửi lần lượt theo thời điểm tạo. Nếu thành công, đánh dấu synced; nếu lỗi tạm thời, thử lại với khoảng chờ tăng dần.
  3. Xung đột giải quyết theo quy tắc bản mới hơn theo updatedAt thắng, hoặc ưu tiên server — nhưng phải chọn rõ và ghi lại trong tài liệu dự án.

Cách gọi API, xử lý lỗi và trạng thái tải được trình bày ở bài tích hợp API trong Flutter.

14. Test cho TaskProvider

Logic task nằm trong provider, nên đây là chỗ rẻ nhất để viết test và cũng là chỗ hay phát sinh lỗi nhất. Không cần dựng giao diện, chỉ cần gọi hàm và kiểm tra kết quả.

[object Object]

Mẹo quan trọng: cho provider nhận lớp lưu trữ qua constructor thay vì tự tạo bên trong. Nhờ vậy test có thể dùng bản lưu trong bộ nhớ, chạy nhanh và không phụ thuộc thiết bị. Nội dung nền tảng ở bài giới thiệu unit test, widget test, integration test.

15. Những lỗi hay gặp khi làm tính năng quản lý task

  1. Dùng UniqueKey làm id. Chỉ duy nhất trong một phiên chạy — sẽ vỡ khi cần lưu trữ hoặc đồng bộ.
  2. Sửa trực tiếp object trong danh sách rồi quên gọi notifyListeners(). Người dùng bấm mà giao diện không đổi, hoặc đổi ở màn hình này mà không ở màn hình khác.
  3. Gọi notifyListeners() sau dispose. Thường xảy ra khi có tác vụ bất đồng bộ hoàn thành muộn; cần kiểm tra vòng đời trước khi thông báo.
  4. Dùng context sau await. Cần kiểm tra mounted trước khi điều hướng hoặc hiện thông báo.
  5. Không giải phóng controller. Mở/đóng form nhiều lần sẽ tích tụ TextEditingController và rò rỉ bộ nhớ.
  6. Đặt logic lọc và sắp xếp trong build. Việc này khiến máy tính lại toàn bộ danh sách ở mỗi lần vẽ. Hãy tính sẵn trong provider và phát lại danh sách đã xử lý.
  7. So sánh thời gian bằng giờ thay vì ngày. Nhóm "quá hạn" và "hôm nay" sẽ sai gần như mỗi ngày.

Một tính năng task nhìn nhỏ nhưng chứa gần đủ các chủ đề quan trọng của Flutter: quản lý trạng thái, vòng đời widget, xử lý bất đồng bộ, lưu trữ, đồng bộ và test. Vì vậy đây là dự án luyện tập có giá trị cao hơn nhiều so với làm lại các màn hình tĩnh.

16. Tìm kiếm và lọc có chống dội

Khi người dùng gõ vào ô tìm kiếm, mỗi ký tự đều có thể kích hoạt một lần lọc hoặc một lần gọi API. Gõ "flutter" là sáu lần gọi, và kết quả trả về không theo thứ tự. Cách xử lý là chống dội (debounce): chỉ thực hiện sau khi người dùng ngừng gõ một khoảng ngắn.

[object Object]

Hai điểm rất dễ bị bỏ sót:

  • Phải cancel() timer cũ mỗi lần người dùng gõ tiếp, và trong dispose. Nếu không, callback sẽ chạy sau khi màn hình đã bị xoá.
  • Tìm kiếm phía server cần chống kết quả về ngược thứ tự. Người dùng gõ "fl", rồi "flutter"; nếu kết quả của "fl" về sau, màn hình sẽ hiển thị kết quả sai. Cách xử lý: lưu lại từ khoá của request gần nhất và bỏ qua phản hồi nếu nó không khớp.

Với dữ liệu cục bộ vài nghìn task, tìm kiếm ngay trong bộ nhớ là đủ nhanh; chỉ cần debounce khi gọi API hoặc khi danh sách rất lớn.

17. Nhãn, ưu tiên và tác vụ lặp lại

Sau khi tính năng cơ bản chạy được, ba yêu cầu gần như luôn được hỏi thêm: gắn nhãn, đánh dấu ưu tiên, và tạo task lặp theo chu kỳ.

Về mặt dữ liệu, hai trường cần thêm cho mỗi task:

[object Object]

Một nguyên tắc thiết kế quan trọng: lưu quy tắc lặp, không lưu danh sách ngày đã sinh. Nếu bạn sinh sẵn 50 lần lặp vào cơ sở dữ liệu, người dùng sửa tiêu đề một lần sẽ phải sửa 50 bản ghi, và mọi thống kê sẽ sai. Hãy tính ngày kế tiếp khi cần, từ quy tắc.

Về ưu tiên, đừng dùng ma thuật số. Một hàm sắp xếp có thể kết hợp nhiều tiêu chí theo thứ tự rõ ràng — và đây là chỗ nên viết một hàm thuần để test dễ:

[object Object]

Hàm này không phụ thuộc giao diện, không phụ thuộc thời gian hệ thống, nên chỉ mất vài dòng để test đủ các trường hợp.

18. Nhắc nhở bằng thông báo cục bộ

Một tính năng task mà không nhắc thì người dùng vẫn phải tự nhớ — tức là chưa giải quyết được vấn đề. Cách làm phổ biến là dùng flutter_local_notifications kèm timezone để hẹn thông báo theo giờ địa phương.

[object Object]

Bốn điều cần lưu ý khi làm phần này, vì đây là chỗ tốn thời gian nhất:

  1. Phải xin quyền thông báo. Từ Android 13, quyền POST_NOTIFICATIONS là quyền lúc chạy, cần khai báo trong manifest và xin người dùng. iOS cũng cần xin quyền tương tự.
  2. Cần khai báo timezone và khởi tạo dữ liệu múi giờ trong main trước khi hẹn giờ. Thiếu bước này, thông báo thường lệch giờ hoặc không chạy.
  3. Thông báo hẹn chính xác có giới hạn. Trên Android 12 trở lên, hẹn chính xác cần quyền riêng và một số hãng siết chặt việc chạy nền. Dùng chế độ hẹn không chính xác (như ví dụ) thường là lựa chọn an toàn hơn cho ứng dụng thông thường.
  4. Huỷ và hẹn lại khi task thay đổi. Nếu người dùng đổi hạn, bạn phải huỷ thông báo cũ theo cùng id rồi hẹn lại — nếu không sẽ có hai thông báo cho cùng một việc.

19. Đồng bộ: hàng đợi, thử lại và chống gửi trùng

Khi tính năng task cần dùng trên nhiều thiết bị, đây là phần quyết định chất lượng sản phẩm — và cũng là phần dễ bị làm ẩu nhất.

Mô hình đủ tốt cho phần lớn ứng dụng gồm ba thành phần:

[object Object]

Chống gửi trùng là mục tiêu quan trọng nhất. Nếu mạng chập chờn, request có thể đã tới server nhưng phản hồi không về máy, và lần thử lại sẽ tạo bản ghi thứ hai. Cách chặn là gửi kèm một khoá duy nhất (id ở trên) và để server bỏ qua nếu khoá đó đã xử lý — gọi là idempotency key.

Thử lại có khoảng chờ tăng dần giúp tránh dồn tải khi server đang quá tải:

[object Object]

Xung đột phải có quy tắc rõ ràng ngay từ đầu. Với ứng dụng ghi chú và công việc cá nhân, quy tắc "bản có updatedAt mới hơn thắng" hoạt động tốt trong đa số tình huống. Với dữ liệu nhiều người cùng sửa (danh sách công việc của nhóm), cần ghi lại lịch sử thay đổi hoặc dùng phiên bản (version) để phát hiện xung đột và hỏi người dùng.

Ba trạng thái cần thể hiện trên giao diện: đang chờ gửi, đã đồng bộ, lỗi cần xử lý. Nếu người dùng không biết task của mình đã lên server hay chưa, họ sẽ mất niềm tin vào ứng dụng — và đó là vấn đề lớn hơn mọi lỗi kỹ thuật.

20. Widget test cho màn hình task

Ngoài test cho provider đã trình bày ở phần 14, màn hình task cũng đáng có vài test vì đây là nơi tập trung tương tác:

[object Object]

Điểm đáng chú ý: truyền provider bằng ChangeNotifierProvider.value với dữ liệu đã tạo sẵn giúp test kiểm soát được trạng thái ban đầu, không phụ thuộc vào việc màn hình tự tải dữ liệu.

21. Hiệu năng khi có hàng nghìn task

Danh sách task thường bắt đầu nhỏ rồi tăng dần, và đến một lúc nào đó người dùng sẽ thấy giật. Ba việc cần làm khi đó:

  1. Không tính lại danh sách đã lọc trong build. Hãy tính trong provider khi dữ liệu thay đổi, và phát ra danh sách đã xử lý. Nếu thật sự cần tính trong giao diện, dùng Selector/select để chỉ phần cần thiết vẽ lại.
  2. Chỉ dựng dòng đang nhìn thấy. ListView.builder là mặc định, và nếu chiều cao mỗi dòng cố định, thêm itemExtent để Flutter không phải đo từng dòng.
  3. Tải theo nhóm thay vì tải hết. Với vài nghìn task, hãy tải theo khoảng thời gian (hôm nay, tuần này, tháng này) hoặc phân trang, đúng cách đã trình bày ở bài ScrollController và tải thêm dữ liệu.

Với cơ sở dữ liệu cục bộ, hãy đánh chỉ mục cho các trường dùng để lọc và sắp xếp (trạng thái hoàn thành, hạn, updatedAt). Truy vấn có chỉ mục nhanh hơn nhiều lần khi dữ liệu lớn, và đây là khác biệt giữa sqflite/drift so với việc tự đọc toàn bộ rồi lọc trong Dart.

22. Trải nghiệm: bốn trạng thái ai cũng quên

Một tính năng task chỉ được coi là hoàn chỉnh khi bốn trạng thái dưới đây đều có giao diện rõ ràng:

  • Đang tải: hiện khung xương (skeleton) thay vì vòng xoay ở giữa màn hình nếu có thể đoán trước bố cục.
  • Rỗng: không chỉ ghi "Không có dữ liệu" mà nên hướng dẫn hành động tiếp theo, ví dụ nút "Thêm task đầu tiên" và một câu gợi ý cách dùng.
  • Lỗi: thông báo nguyên nhân ở mức người dùng hiểu được, kèm nút thử lại. Không hiển thị mã lỗi kỹ thuật thô.
  • Không có kết quả sau khi lọc: khác với rỗng hoàn toàn. Cần nói rõ "Không tìm thấy task nào khớp từ khoá" và cho nút xoá bộ lọc.

Phần này chiếm ít thời gian nhưng tạo khác biệt rõ rệt về cảm nhận chất lượng, và cũng là thứ hay bị bỏ qua khi làm dự án luyện tập.

23. Khả năng truy cập và thao tác một tay

Với ứng dụng công việc — thứ người dùng mở nhiều lần trong ngày, thường khi đang di chuyển — vài chi tiết nhỏ có giá trị lớn:

  • Vùng chạm đủ lớn. Checkbox nhỏ trong dòng task nên được bọc trong vùng chạm tối thiểu khoảng 44–48 điểm ảnh. Đặt onTap cho cả dòng thay vì chỉ icon.
  • Không phụ thuộc màu để truyền tải trạng thái. Task quá hạn không nên chỉ khác ở màu đỏ; hãy thêm nhãn chữ hoặc icon kèm nhãn ngữ nghĩa.
  • Thêm Semantics cho các thành phần do bạn tự vẽ. Nếu bạn dùng Stack để làm huy hiệu đếm số trên icon, trình đọc màn hình sẽ không biết đó là gì.
  • Hỗ trợ thao tác nhanh. Vuốt để hoàn thành hoặc xoá là thao tác quen thuộc; hãy nhớ cung cấp cách tương đương qua nhấn giữ hoặc menu, vì không phải ai cũng dùng được cử chỉ vuốt.
  • Phóng to chữ. Kiểm tra giao diện khi người dùng đặt cỡ chữ lớn nhất trong cài đặt hệ thống; đây là bài kiểm tra nhanh và thường phát hiện ra nhiều lỗi tràn khung.

Câu hỏi thường gặp

Task nên lưu cục bộ hay lưu trên server?

Nếu dữ liệu chỉ dùng trên một thiết bị, lưu cục bộ là đủ và đơn giản hơn nhiều. Chỉ cần server khi có yêu cầu đồng bộ nhiều thiết bị, chia sẻ cho người khác, hoặc cần sao lưu. Đừng thêm server chỉ vì cảm giác "cho chuyên nghiệp" — mỗi lần thêm server là thêm hàng loạt vấn đề về mạng, xung đột và phân quyền.

Khi nào nên dùng sqflite thay vì Hive?

Khi bạn cần truy vấn có điều kiện phức tạp, thống kê theo thời gian, tìm kiếm trên nhiều trường, hoặc dữ liệu có quan hệ (task thuộc dự án, dự án thuộc nhóm). Nếu chỉ cần lưu danh sách task và lọc đơn giản trong bộ nhớ, Hive nhẹ và dễ dùng hơn.

Thông báo nhắc hạn có hoạt động khi app bị đóng không?

Có, nếu bạn hẹn thông báo ở cấp hệ điều hành (như ví dụ zonedSchedule). Lưu ý: thông báo đã hẹn thường mất khi người dùng gỡ và cài lại app, nên cần cơ chế hẹn lại toàn bộ khi mở app lần đầu sau khi cài.

Làm sao để không mất dữ liệu khi app bị hệ điều hành thu hồi bộ nhớ?

Ghi dữ liệu ngay khi người dùng thao tác, thay vì chờ tới lúc rời màn hình. Kèm theo đó, lưu những gì đang nhập dở trong form (vào bộ nhớ cục bộ) và khôi phục lại khi mở lại màn hình — đây là chi tiết nhỏ nhưng được người dùng đánh giá cao.

Tổng kết tính năng quản lý task

Đi từ một danh sách đơn giản tới một tính năng dùng được thật, bạn sẽ đi qua gần như toàn bộ kiến thức nền của Flutter: mô hình dữ liệu bất biến, quản lý trạng thái, vòng đời widget, form và kiểm tra dữ liệu, thao tác theo ngày, xác nhận và hoàn tác, lưu trữ cục bộ, nhắc nhở cấp hệ điều hành, đồng bộ và xử lý lỗi mạng, cùng bộ test cho cả logic lẫn giao diện.

Hãy làm theo thứ tự: chạy được danh sách và form trước, thêm lưu trữ, rồi mới tới nhắc nhở và đồng bộ. Đừng bắt đầu từ phần đồng bộ — đó là phần khó nhất và cũng là phần dễ khiến bạn bỏ dở dự án giữa đường. Khi đã có bản chạy tốt trên máy, hãy đưa vào portfolio kèm README mô tả các quyết định kỹ thuật, vì đó chính là nội dung bạn sẽ kể trong buổi phỏng vấn — xem thêm bộ câu hỏi phỏng vấn Flutter thường gặp.


Học Flutter bài bản theo lộ trình

Nếu bạn muốn học có người sửa bài thay vì tự ghép tài liệu, Flutter Hướng Nghiệp đào tạo theo 3 cấp:

Lớp online trực tiếp qua Zoom/Google Meet, có bản ghi lại — không bắt buộc đến cơ sở.
Xem lịch khai giảng hoặc so sánh 5 cách học lập trình mobile nếu còn phân vân.