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

Hướng dẫn Provider: State management dễ hiểu nhất

Được viết bởi Admin|0 lượt xem
Hướng dẫn Provider: State management dễ hiểu nhất

Tóm tắt nhanh

Hướng dẫn chi tiết cách sử dụng Provider cho state management trong Flutter với ví dụ thực tế và dễ hiểu.

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

Provider là giải pháp state management được Google khuyến nghị cho Flutter. Bài viết này sẽ hướng dẫn bạn sử dụng Provider từ cơ bản đến nâng cao.

Provider là gì?

Provider là một wrapper around InheritedWidget, giúp quản lý state dễ dàng hơn.

Lợi ích:

  • ✅ Đơn giản, dễ học
  • ✅ Official recommendation
  • ✅ Giảm boilerplate
  • ✅ Dễ test

Setup

1. Dependencies

[object Object]

2. Install

[object Object]

Basic Provider

1. Tạo Provider

[object Object]

2. Provide Provider

[object Object]

3. Sử dụng Provider

[object Object]

Multiple Providers

1. MultiProvider

[object Object]

Advanced Usage

1. Provider.of với listen

[object Object]

2. Consumer

[object Object]

3. Selector

[object Object]

Real-world Example

1. Todo App với Provider

[object Object]

Best Practices

1. Dispose Resources

[object Object]

2. Separate Providers

[object Object]

Provider thực chất hoạt động như thế nào?

Provider không phải phép thuật. Nó là một lớp mỏng bọc quanh InheritedWidget — cơ chế có sẵn của Flutter để truyền dữ liệu xuống cây widget mà không phải truyền qua từng constructor.

Hiểu hai điểm sau là đủ để tự debug hầu hết lỗi Provider:

  1. Provider đặt dữ liệu vào một vị trí trong cây widget. Widget con tìm ngược lên trên để lấy. Nếu provider nằm ở nhánh khác của cây, nó không tìm thấy — và bạn nhận lỗi ProviderNotFoundException.
  2. notifyListeners() đánh dấu các widget đang lắng nghe là cần vẽ lại. Widget không lắng nghe sẽ không bị ảnh hưởng. Vì vậy câu hỏi "làm sao giảm rebuild" thực chất là "làm sao để ít widget lắng nghe nhất".

Một hệ quả rất thực tế: nếu bạn thấy giao diện không cập nhật, hãy kiểm tra theo thứ tự — provider có nằm phía trên widget đó không, widget đó có đang lắng nghe (watch, Consumer, Selector) không, và hàm có thật sự gọi notifyListeners() không.

Bốn cách đọc dữ liệu và khi nào dùng cách nào

Đây là phần quyết định code của bạn sạch hay rối, và cũng là phần bị hỏi nhiều khi phỏng vấn.

[object Object]

Quy tắc thực dụng:

  • Dùng read cho mọi lời gọi hàm — nút bấm, sự kiện, callback. Trong build thì gần như không bao giờ nên dùng read cho dữ liệu.
  • Dùng select khi widget chỉ quan tâm một trường của provider. Ví dụ thanh giỏ hàng chỉ cần số lượng sản phẩm, không cần danh sách sản phẩm. Thiếu select, mọi thay đổi trong giỏ đều làm thanh đó vẽ lại.
  • Truyền phần tĩnh vào tham số child của Consumer. Đây là cách rẻ nhất để chặn rebuild cho các widget nặng như biểu tượng, ảnh minh họa.

Đặt provider ở đâu: phạm vi mới là quyết định quan trọng

Sai lầm phổ biến là đặt tất cả provider ở gốc ứng dụng. Hệ quả: mọi màn hình đều có thể lấy dữ liệu của mọi tính năng, dẫn tới phụ thuộc chéo và rất khó test.

Quy tắc nên theo:

  • Toàn cục: những thứ thật sự dùng khắp nơi và sống theo phiên — trạng thái đăng nhập, chủ đề, cài đặt ngôn ngữ.
  • Theo nhánh màn hình: giỏ hàng, bộ lọc danh sách, trạng thái đang chỉnh sửa của một quy trình. Đặt provider ngay trong nhánh đó.
[object Object]

Một lợi ích kèm theo: provider đặt ở nhánh màn hình sẽ tự được giải phóng khi màn hình bị xoá, nên bạn không phải nhớ dọn dẹp thủ công.

Phụ thuộc giữa các provider

Khi provider B cần dữ liệu từ provider A (rất thường gặp: hồ sơ người dùng cần trạng thái đăng nhập), đừng để B tự đi tìm A trong build của widget. Hãy khai báo quan hệ ngay lúc tạo.

[object Object]

Cách này giữ luồng dữ liệu một chiều và rõ ràng: khi AuthProvider thay đổi, ProfileProvider tự cập nhật theo. Đây cũng là điểm mà người phỏng vấn hay hỏi để kiểm tra bạn có hiểu vòng đời state hay chỉ biết viết Consumer.

Test provider mà không cần giao diện

Một trong những lý do lớn nhất để tách state ra ChangeNotifier là khả năng test. Toàn bộ logic dưới đây chạy trong môi trường kiểm thử thuần, không cần dựng widget.

[object Object]

Khi provider phụ thuộc lớp gọi API, hãy truyền lớp đó qua constructor để test có thể thay bằng bản giả. Nội dung nền tảng về kiểm thử ở bài giới thiệu unit test, widget test, integration test.

Lỗi thường gặp khi dùng Provider

  1. ProviderNotFoundException. Thường vì provider nằm ở nhánh khác của cây, hoặc vì bạn đọc provider bên trong Timer, Stream, hay callback chạy sau khi widget bị xoá. Cách chắc chắn: dùng read ở nơi có context hợp lệ và kiểm tra mounted sau các hàm bất đồng bộ.
  2. Dùng context sau await. Cần kiểm tra widget còn tồn tại trước khi tiếp tục.
  3. Gọi notifyListeners() trong build. Gây vòng lặp vẽ lại vô hạn. Mọi thay đổi trạng thái phải xảy ra ngoài build.
  4. Sửa dữ liệu trong danh sách mà không tạo bản mới. Với danh sách phức tạp, hãy tạo danh sách mới bằng List.of rồi mới thông báo, để widget so sánh thấy khác biệt rõ ràng.
  5. Không giải phóng tài nguyên. ChangeNotifier có dispose — hãy dùng nó để huỷ Timer, StreamSubscription, TextEditingController.
  6. Dùng một provider khổng lồ cho cả ứng dụng. Provider lớn làm mọi thứ phụ thuộc vào nhau, test khó và rebuild lan rộng. Tách theo tính năng, đúng như phần Best Practices ở trên.

Khi nào Provider không còn phù hợp?

Provider xuất sắc cho phần lớn ứng dụng, đặc biệt khi bạn mới bắt đầu. Nhưng khi luồng trạng thái trở nên phức tạp — nhiều bước, nhiều nhánh, cần ghi lại chuỗi sự kiện để tái hiện lỗi, hoặc cần chia sẻ logic giữa các nền tảng — mô hình hướng sự kiện của Bloc thường dễ kiểm soát hơn.

Dấu hiệu nên chuyển:

  • Bạn phải thêm cờ trạng thái liên tục để biết đang tải, đang lỗi, đang gửi, đã gửi.
  • Bug chỉ tái hiện theo một thứ tự thao tác nhất định và rất khó tái hiện.
  • Nhiều widget cùng lắng nghe một provider, và bạn không còn chắc phần nào vẽ lại vì lý do gì.

Bảng so sánh đầy đủ giữa các thư viện ở bài so sánh Provider, Riverpod, Bloc, GetX, và ví dụ từng bước với Bloc ở bài hướng dẫn Bloc/Cubit. Nếu muốn luyện các khái niệm này bằng một dự án thật, hãy làm theo bài xây dựng chức năng quản lý task.

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

Nên dùng context.watch hay Consumer?

watch gọn hơn khi bạn cần dữ liệu ở ngay trong build của một widget nhỏ. Consumer phù hợp khi chỉ một vùng nhỏ cần vẽ lại, hoặc khi bạn muốn truyền child tĩnh để chặn rebuild. Cả hai đều đúng — chọn theo phạm vi ảnh hưởng.

Provider có phải là thư viện duy nhất được Google khuyến nghị không?

Không. Provider là lựa chọn được nhắc tới nhiều và có tài liệu tốt, nhưng Flutter không ràng buộc bạn vào một thư viện quản lý trạng thái nào. Điều được đánh giá khi làm việc nhóm là bạn chọn có lý do và giữ nhất quán trong toàn dự án.

ChangeNotifierProvider.value dùng khi nào?

Khi provider đã được tạo ở nơi khác và bạn chỉ muốn cung cấp nó xuống cây — ví dụ provider được truyền vào màn hình, hoặc tạo từ một widget cha. Lưu ý: với .value, provider không tự dispose object đó, vòng đời do nơi tạo quản lý.

Làm sao tránh rebuild cả trang khi chỉ số lượng thay đổi?

Dùng select để chỉ lắng nghe trường cần thiết, tách phần hiển thị số lượng ra widget riêng, và truyền phần tĩnh qua child. Ba việc này giải quyết hầu hết trường hợp rebuild lan rộng.

Xử lý trạng thái bất đồng bộ cho đúng ngay từ đầu

Phần lớn lỗi trải nghiệm trong ứng dụng thật đến từ việc chỉ có hai trạng thái "có dữ liệu" và "không có dữ liệu", trong khi thực tế cần tối thiểu bốn. Cách làm gọn nhất là mô hình hoá trạng thái thành một kiểu duy nhất.

[object Object]

Với Dart hỗ trợ sealed class, trình biên dịch buộc bạn xử lý đủ các nhánh khi dùng switch, nên không thể quên trường hợp lỗi:

[object Object]

Và ở giao diện, mọi trạng thái đều có giao diện tương ứng:

[object Object]

Lợi ích rõ nhất không phải là code ngắn hơn, mà là không thể quên nhánh. Mỗi lần thêm trạng thái mới, trình biên dịch chỉ ra tất cả chỗ cần cập nhật.

Provider cho form và dữ liệu nhập liệu

Form là nơi hai loại trạng thái cùng tồn tại: giá trị đang nhập (thuộc giao diện) và kết quả kiểm tra, kết quả gửi (thuộc nghiệp vụ). Trộn cả hai vào một ChangeNotifier thường dẫn tới việc gọi notifyListeners() cho từng ký tự, làm giao diện nặng dần.

Cách phân chia thực dụng:

[object Object]

Còn giá trị từng ô nhập vẫn để TextEditingController quản lý. Lý do: TextEditingController đã xử lý con trỏ, vùng chọn, hành vi bàn phím — đưa những thứ đó vào provider là tự làm khó mình. Provider chỉ nên nhận dữ liệu khi người dùng bấm gửi, hoặc khi cần kiểm tra chéo nhiều trường.

Một chi tiết nhỏ nhưng quan trọng: chặn gửi trùng bằng if (_isSubmitting) return;. Người dùng bấm nút hai lần khi mạng chậm là chuyện xảy ra thường xuyên, và hậu quả là tạo hai bản ghi.

Tách lớp: Provider, Repository và lớp gọi API

Provider nên biết về nghiệp vụ, không nên biết về HTTP. Cách tách này giúp test dễ hơn nhiều và là câu hỏi kiến trúc thường gặp khi phỏng vấn.

[object Object]

Khi đó provider nhận CourseRepository qua constructor, và trong test bạn truyền một bản giả trả dữ liệu cố định. Đây chính là điểm khác biệt giữa một provider test được và một provider phụ thuộc mạng.

Provider và router: đọc trạng thái đăng nhập trong redirect

Khi ứng dụng dùng router khai báo, redirect cần biết trạng thái đăng nhập. Hai cách làm, và chỉ một cách nên dùng.

Không nên: tìm provider bằng context trong redirect. Router được tạo một lần ở gốc ứng dụng, nơi context có thể chưa nằm dưới Provider. Kết quả là lỗi ProviderNotFoundException chỉ xuất hiện ở một số luồng.

Nên dùng: tạo một ChangeNotifier nhỏ chỉ chứa trạng thái đăng nhập, tạo nó bên ngoài cây widget, rồi truyền cùng lúc cho cả router và Provider.

[object Object]

Cấu trúc này giữ một nguồn sự thật duy nhất cho trạng thái đăng nhập: cả router và giao diện đều đọc từ cùng một object.

Widget test với Provider

Test provider ở mức logic đã trình bày ở phần trên. Bổ sung thêm test ở mức widget để kiểm tra giao diện có phản ứng đúng với trạng thái:

[object Object]

Hai trạng thái lỗi và rỗng là thứ đáng test nhất vì chúng ít được mở bằng tay trong lúc phát triển, và đó cũng là lúc người dùng thật gặp lỗi.

Tối ưu rebuild: vài kỹ thuật cụ thể

Ngoài select và Consumer với child, ba điều dưới đây giúp giảm công vẽ đáng kể trong ứng dụng có nhiều provider:

  1. Đừng tạo object mới trong build để so sánh. Nếu bạn truyền [a, b] hoặc {'x': 1} mới vào select, phép so sánh luôn thấy khác và widget luôn vẽ lại. Hãy so sánh trên trường cụ thể hoặc giá trị nguyên thuỷ.
  2. Tách provider theo tần suất thay đổi. Một provider gộp cả dữ liệu ít đổi (danh mục) và dữ liệu đổi liên tục (số lượng trong giỏ) sẽ làm mọi widget lắng nghe nó vẽ lại thường xuyên. Tách ra hai provider là cách rẻ nhất.
  3. Đặt Consumer ở đúng chỗ. Đặt Consumer bao cả màn hình để lấy một trường nhỏ là lỗi phổ biến; hãy để Consumer bao đúng widget cần giá trị đó.

Một phép kiểm tra nhanh: trong môi trường phát triển, bật in theo dõi dựng lại và cuộn một danh sách. Nếu số lần dựng lại của widget tĩnh tăng theo thao tác cuộn, phạm vi lắng nghe của bạn đang quá rộng.

Chuyển dần sang Riverpod hoặc Bloc

Chuyển cả dự án cùng lúc là cách chắc chắn nhất để tạo ra một đợt lỗi lớn. Nếu bạn thấy dự án đang chạm giới hạn của Provider, hãy chuyển theo từng tính năng:

  1. Chọn một tính năng có luồng trạng thái phức tạp nhất — thường là xác thực, thanh toán, hoặc luồng đặt hàng nhiều bước.
  2. Viết lại phần trạng thái của tính năng đó bằng công cụ mới, giữ nguyên giao diện nếu có thể.
  3. Chạy song song trong một thời gian. Hai công cụ cùng tồn tại là bình thường; điều không nên là trộn cả hai trong cùng một tính năng.
  4. Đặt quy ước cho nhóm: tính năng mới dùng công cụ nào, tính năng cũ chuyển khi nào.

Về lựa chọn: Riverpod phù hợp khi bạn muốn giảm phụ thuộc vào BuildContext, cần suy luận rõ về phụ thuộc giữa các nguồn dữ liệu, và muốn test thuận tiện hơn. Bloc phù hợp khi luồng trạng thái có nhiều bước, cần ghi lại sự kiện, hoặc nhóm đã quen mô hình hướng sự kiện — xem bài hướng dẫn Bloc/Cubit và bài so sánh các thư viện.

Câu hỏi thường gặp (tiếp)

Có nên dùng Provider cho trạng thái toàn cục như cài đặt người dùng?

Có, đây đúng là trường hợp phù hợp: ít thay đổi, dùng ở nhiều nơi, sống theo phiên làm việc. Nếu muốn lưu lâu dài, hãy kết hợp với lớp lưu trữ cục bộ và đọc lại khi khởi động.

Provider có gây rò rỉ bộ nhớ không?

Bản thân ChangeNotifierProvider tự gọi dispose cho object khi nó bị xoá khỏi cây. Rò rỉ xảy ra khi object của bạn giữ tài nguyên mà không giải phóng trong dispose — Timer, StreamSubscription, AnimationController. Hãy kiểm tra chính lớp provider của bạn.

Làm sao biết một thay đổi trạng thái thực sự làm bao nhiêu widget vẽ lại?

Cách nhanh nhất là tách một widget tĩnh bọc trong const và tạm thời đếm số lần build bằng biến đếm hoặc debugPrint. Nếu số lần tăng khi trạng thái không liên quan thay đổi, phạm vi lắng nghe đang rộng hơn cần thiết.

Provider có phù hợp cho dự án lớn không?

Có, nếu bạn chia provider theo tính năng và giữ quy tắc rõ ràng về phạm vi đặt provider. Vấn đề không nằm ở thư viện mà ở việc gom mọi trạng thái vào một provider khổng lồ — lúc đó mọi lựa chọn đều trở nên khó bảo trì.

Provider có dùng song song với Bloc trong cùng dự án không?

Có, và điều đó hợp lý trong giai đoạn chuyển đổi. Ví dụ bạn có thể giữ provider cho những phần nhẹ như chủ đề, ngôn ngữ, cài đặt, đồng thời dùng Bloc cho luồng nghiệp vụ phức tạp. Điều cần tránh là dùng cả hai cho cùng một phần trạng thái, vì khi đó không ai biết trạng thái thật đang nằm ở đâu và bug sẽ rất khó lần.

Nên đặt tên provider và file như thế nào cho dễ tìm?

Đặt theo tính năng chứ không theo loại dữ liệu: CheckoutProvider tốt hơn OrderListProvider chung chung, và file nên nằm trong thư mục của chính tính năng đó. Quy ước này quan trọng khi dự án lớn dần: bạn tìm theo tính năng nhanh hơn nhiều so với tìm theo kiểu dữ liệu, và nó giúp giới hạn rõ phần nào được phép truy cập phần nào.

Provider có tự lưu trạng thái khi người dùng đóng app không?

Không. ChangeNotifier chỉ sống trong bộ nhớ. Muốn giữ lại sau khi đóng app, bạn phải ghi xuống lớp lưu trữ (bộ nhớ cục bộ, cơ sở dữ liệu) và đọc lại khi khởi động. Cách làm gọn là để chính provider chịu trách nhiệm đọc/ghi qua Repository trong load() và sau mỗi thay đổi quan trọng — nhờ vậy tầng giao diện không cần biết dữ liệu được lưu ở đâu.

Checklist trạng thái trước khi release

Phần lớn lỗi trạng thái chỉ lộ ra khi người dùng làm những việc bạn không thử: xoay màn hình, mở hai màn hình cùng lúc, mất mạng giữa thao tác. Rà nhanh danh sách sau:

  • Mỗi tính năng có provider riêng, không dồn vào một provider toàn cục.
  • Provider nhận phụ thuộc qua constructor, không tự tạo lớp gọi API bên trong.
  • Có đủ bốn trạng thái: đang tải, có dữ liệu, rỗng, lỗi kèm nút thử lại.
  • Không gọi notifyListeners() trong build, và có kiểm tra vòng đời với tác vụ bất đồng bộ.
  • Mọi tài nguyên (Timer, subscription, controller) được giải phóng trong dispose.
  • Nút gửi có chặn bấm trùng.
  • Widget tĩnh không bị vẽ lại khi trạng thái không liên quan thay đổi.
  • Logic nghiệp vụ có test ở mức provider, không cần dựng giao diện.
  • Trạng thái đăng nhập có một nguồn sự thật duy nhất cho cả router và giao diện.

Kết luận

Provider là:

  • ✅ Đơn giản và dễ học
  • ✅ Official recommendation
  • ✅ Phù hợp cho hầu hết apps
  • ✅ Dễ test và maintain

Tóm tắt:

  1. Setup: Thêm provider package
  2. Create Provider: Extend ChangeNotifier
  3. Provide: Wrap app với Provider
  4. Use: Consumer hoặc Provider.of

Provider là lựa chọn tốt để bắt đầu với state management! 🚀


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.