Chúng ta đã cùng nhau tìm hiểu về state ở bài trước, và có lẽ đã quen thuộc với setState()
, công cụ cơ bản để cập nhật giao diện người dùng. Tuyệt vời! setState()
là điểm khởi đầu, nhưng khi ứng dụng của chúng ta lớn hơn, phức tạp hơn, chúng ta sẽ nhận ra rằng chỉ setState()
thôi là vẫn còn thiếu.
Chúng ta có thể gặp phải những vấn đề như:
- Prop drilling: Truyền dữ liệu qua nhiều lớp Widget không liên quan, khiến code trở nên lộn xộn và khó bảo trì.
- Khó khăn khi chia sẻ state: Làm thế nào để nhiều Widget khác nhau truy cập và cập nhật cùng một dữ liệu một cách hiệu quả?
- Hiệu suất: Việc cập nhật toàn bộ Widget tree khi chỉ một phần nhỏ dữ liệu thay đổi có thể ảnh hưởng đến hiệu suất.
- Kiểm thử: Rất khó để kiểm thử logic nghiệp vụ khi nó gắn liền với UI.
Đây là lúc chúng ta cần đến các giải pháp quản lý state nâng cao. Chúng không chỉ giúp chúng ta giải quyết những vấn đề trên mà còn định hình kiến trúc ứng dụng của chúng ta trở nên mạch lạc, dễ mở rộng và dễ kiểm thử hơn.
Hãy cùng tìm hiểu ba "ông lớn" trong thế giới quản lý trạng thái Flutter: Provider, BLoC và Riverpod.
1. Provider: Đơn giản, Mạnh mẽ và Được khuyên dùng
Provider là một package quản lý trạng thái cực kỳ phổ biến và được chính nhóm Flutter khuyến nghị cho hầu hết các trường hợp. Nó hoạt động dựa trên nguyên tắc InheritedWidget nhưng đơn giản và hiệu quả hơn rất nhiều.
Khi nào nên dùng?
- Chúng ta muốn một giải pháp dễ học, dễ sử dụng cho hầu hết các ứng dụng.
- Khi chúng ta cần truy cập dữ liệu hoặc dịch vụ từ một phần nào đó của Widget tree.
- Ứng dụng của chúng ta có độ phức tạp trung bình.
Lợi ích:
- Dễ học và sử dụng: Cú pháp trực quan, tài liệu phong phú.
- Hiệu quả: Chỉ xây dựng lại những Widget cần thiết.
- Linh hoạt: Có thể cung cấp nhiều loại dữ liệu khác nhau (objects, streams, futures).
- Giảm "prop drilling": Các Widget có thể "nghe" trực tiếp dữ liệu mà không cần truyền xuống từng cấp.
Nhược điểm:
- Phụ thuộc vào
BuildContext
: Hầu hết các hoạt động của Provider đều yêu cầuBuildContext
. Điều này có thể gây khó khăn khi bạn muốn truy cập provider từ các lớp không phải Widget (ví dụ: logic nghiệp vụ thuần túy) hoặc khi viết unit test cho các lớp này. - Phát hiện lỗi runtime: Một số lỗi, đặc biệt là liên quan đến việc Provider không được tìm thấy, chỉ xuất hiện khi ứng dụng chạy, làm cho việc debug đôi khi phức tạp hơn.
- Quản lý nhiều Providers có thể phức tạp: Khi ứng dụng có quá nhiều
ChangeNotifierProvider
,MultiProvider
, hoặc nhiều cấp độ Provider, cấu trúc code có thể trở nên lộn xộn hơn.
Ví dụ cơ bản về Counter với Provider:
Đầu tiên, hãy đảm bảo provider
đã có trong file pubspec.yaml
của bạn.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; void main() { runApp( ChangeNotifierProvider( create: (_) => CounterModel(), child: MyApp(), ), );
} class CounterModel extends ChangeNotifier { int value = 0; void increment() { value++; notifyListeners(); }
} class MyApp extends StatelessWidget { Widget build(BuildContext context) { final counter = context.watch<CounterModel>(); return MaterialApp( home: Scaffold( body: Center(child: Text('${counter.value}', style: TextStyle(fontSize: 40))), floatingActionButton: FloatingActionButton( onPressed: () => context.read<CounterModel>().increment(), child: Icon(Icons.add), ), ), ); }
}
2. BLoC: Kiến trúc rõ ràng, Dễ kiểm thử
BLoC (Business Logic Component) là một mẫu thiết kế và một package quản lý trạng thái tập trung vào việc tách biệt hoàn toàn logic nghiệp vụ (business logic) khỏi giao diện người dùng (UI). Trong BLoC, mọi thay đổi trạng thái đều được xử lý thông qua Events và States, mang lại tính dự đoán và khả năng kiểm thử cao.
Khi nào nên dùng?
- Chúng ta muốn một kiến trúc rõ ràng, dễ bảo trì cho các ứng dụng lớn, phức tạp.
- Ưu tiên khả năng kiểm thử (unit testing) logic nghiệp vụ.
- Cần kiểm soát chặt chẽ luồng dữ liệu và trạng thái với tính nhất quán cao.
Lợi ích:
- Tách biệt mối quan tâm (Separation of Concerns): Logic nghiệp vụ nằm riêng biệt, không bị trộn lẫn với UI.
- Dễ kiểm thử: Các BLoC là các lớp Dart thuần túy, dễ dàng được kiểm thử độc lập.
- Khả năng mở rộng cao: Dễ dàng thêm tính năng mới mà không ảnh hưởng đến các phần khác.
- Dự đoán được: Mọi thay đổi trạng thái đều thông qua các Events được đầu vào và
- States được đầu ra, giúp dễ dàng theo dõi và gỡ lỗi.
Nhược điểm:
- Độ phức tạp ban đầu: Đối với những dự án nhỏ hoặc người mới bắt đầu, kiến trúc BLoC có thể cảm thấy quá phức tạp và cần một đường cong học hỏi nhất định. Việc định nghĩa Events, States và ánh xạ từ Event sang State đòi hỏi nhiều boilerplate hơn.
- boilerplate: Mặc dù Cubit (phiên bản đơn giản hơn của BLoC) đã giảm bớt, nhưng BLoC truyền thống vẫn có một lượng boilerplate (code lặp đi lặp lại) đáng kể khi định nghĩa Events, States và các
mapEventToState
. - Quản lý dependency: Việc inject các dependency vào BLoC (ví dụ: Repository, API service) cần một cách tiếp cận rõ ràng, có thể dùng
get_it
hoặc kết hợp với Provider/Riverpod.
Ví dụ cơ bản về Counter với Bloc:
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; void main() { runApp( BlocProvider( create: (_) => CounterBloc(), child: MyApp(), ), );
} abstract class CounterEvent {} class Increment extends CounterEvent {} class CounterState { final int value; CounterState(this.value);
} class CounterBloc extends Bloc<CounterEvent, CounterState> { CounterBloc() : super(CounterState(0)) { on<Increment>((event, emit) => emit(CounterState(state.value + 1))); }
} class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Center( child: BlocBuilder<CounterBloc, CounterState>( builder: (context, state) => Text('${state.value}', style: TextStyle(fontSize: 40)), ), ), floatingActionButton: FloatingActionButton( onPressed: () => context.read<CounterBloc>().add(Increment()), child: Icon(Icons.add), ), ), ); }
}
3. Riverpod: An toàn, Linh hoạt, và Khắc phục nhược điểm của Provider
Riverpod là một framework quản lý trạng thái mới hơn, được tạo ra bởi cùng tác giả của Provider. Nó được thiết kế để khắc phục một số nhược điểm của Provider, đặc biệt là về mặt an toàn và khả năng kiểm thử.
Khi nào nên dùng?
- Chúng ta tìm kiếm một giải pháp quản lý trạng thái hiện đại, an toàn và mạnh mẽ.
- Chúng ta đã có kinh nghiệm với Provider và muốn một cái gì đó tốt hơn.
- Ưu tiên kiểm thử và muốn loại bỏ các vấn đề liên quan đến
BuildContext
trong quá trình kiểm thử.
Lợi ích:
- An toàn thời gian biên dịch: Riverpod phát hiện nhiều lỗi ngay trong quá trình biên dịch (compile-time) thay vì thời gian chạy (runtime), giúp giảm thiểu lỗi.
- Không phụ thuộc
BuildContext
: Các Provider trong Riverpod không cầnBuildContext
để hoạt động, giúp việc kiểm thử độc lập dễ dàng hơn nhiều. - Đơn giản hóa việc sử dụng nhiều Providers: Dễ dàng kết hợp và đọc giá trị từ nhiều Providers khác nhau.
- Tự động hủy bỏ: Riverpod tự động hủy bỏ các Provider khi không còn được sử dụng, giúp quản lý tài nguyên hiệu quả hơn.
Nhược điểm:
- Khái niệm mới lạ: Mặc dù được phát triển dựa trên Provider, Riverpod giới thiệu một số khái niệm và cú pháp mới (
ref
, các loạiProvider
khác nhau) có thể cần thời gian để làm quen. - Có thể "overkill" cho ứng dụng rất đơn giản: Đối với những ứng dụng cực kỳ nhỏ, đơn giản, việc sử dụng Riverpod có thể hơi phức tạp hơn so với
setState()
hoặc Provider thuần túy.
Ví dụ cơ bản về Counter với Riverpod:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; final counterProvider = StateProvider<int>((ref) => 0); void main() { runApp(ProviderScope(child: MyApp()));
} class MyApp extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final count = ref.watch(counterProvider); return MaterialApp( home: Scaffold( body: Center(child: Text('$count', style: TextStyle(fontSize: 40))), floatingActionButton: FloatingActionButton( onPressed: () => ref.read(counterProvider.notifier).state++, child: Icon(Icons.add), ), ), ); }
}
Kết luận
Không có giải pháp nào là "tốt nhất" cho mọi trường hợp. Quan trọng là chúng ta hiểu rõ ưu và nhược điểm của từng loại để đưa ra lựa chọn phù hợp với dự án và phong cách làm việc của mình:
- Nếu chúng ta mới bắt đầu hoặc dự án không quá phức tạp, hãy bắt đầu với Provider. Nó đơn giản, dễ hiểu và phù hợp với nhiều dự án.
- Nếu chúng ta muốn xây dựng một ứng dụng lớn với kiến trúc rõ ràng, dễ bảo trì và kiểm thử, hãy cân nhắc BLoC.
- Nếu chúng ta đã quen với Provider và muốn một giải pháp an toàn hơn, linh hoạt hơn, đặc biệt là trong việc kiểm thử và quản lý dependency, hãy khám phá Riverpod.
Trong bài tiếp theo chúng ta hãy cùng nhau tìm hiểu thêm về SharedPreferences – Lưu trữ dữ liệu cục bộ nhé
Cảm ơn các bạn đã theo dõi. Bài viết có gì sai sót thì mong các bạn góp ý dưới cmt giúp mình nhé. Nếu được hãy giúp mình một upvote và một bookmark nhé mình cảm ơn rất nhiều 🙏🙏🙏