Hướng dẫn Riverpod trong Flutter
(Flutter Riverpod Tutorial)
Trong Flutter có rất nhiều cách quản lý state như Provider, Bloc, GetX, Redux,… Mỗi cách đều có những ưu nhược điểm riêng. Trong số đó, có 3 phương pháp quản lý state phổ biến hơn cả là Provider (được team Flutter của Google đề xuất sử dụng), Boc (được rất nhiều lập trình viên chuyên nghiệp khuyên dùng) và gần đây nhất là GetX được đông đảo người mới tiếp cận Flutter lựa chọn nhờ sự đơn giản, dễ sử dụng.
Mời bạn xem danh sách video hướng dẫn quản lý trạng thái trong flutter:
Provider có một số hạn chết nhất định và Riverpod chính là bản nâng cấp của Provider để khắc phục những hạn chế đó.
Có thể bạn không để ý, “Riverpod” chính là các chữ cái của “Provider” được sắp xếp lại.
Riverpod trong Flutter có gì khác Provider?
Nhược điểm của Provider
Theo thiết kế, Provider là một cải tiến của InheritedWidget và nó phụ thuộc vào các widget của Flutter. Bạn sẽ sử dụng các widget mặc định của Flutter xây dựng lên ứng dụng của mình.
Việc kết hợp các Provider rất dài dòng, việc trộn lẫn giữa UI code và dependency injection khiến code rất khó đọc. (Để hiểu dependency injection mời bạn xem video sau https://www.youtube.com/watch?v=SVqHgRbwBLs)
Hãy tưởng tượng bạn có MySecondClass
phụ thuộc vào MyFirstClass
.
main.dart
class MySecondClass { final MyFirstClass myFirstClass; MySecondClass(this.myFirstClass); }
Khi đó, để tạo provider MySecondClass
bạn sẽ phải viết code lồng nhau rất phức tạp. Tưởng tượng bạn có khoảng 10 class phụ thuộc nhau thì code sẽ như một mớ bùi nhùi 😟
class MyWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Provider( create: (context) => MyFirstClass(), child: ProxyProvider<MyFirstClass, MySecondClass>( update: (context, firstClass, previous) => MySecondClass(firstClass), child: MyVisibleWidget(), ), ); } }
Thứ hai, Provider chỉ dựa vào “type” để tìm các đối tượng mà bạn cần. Nếu bạn có hai đối tượng cùng “type”, bạn chỉ có thể nhận được đối tượng gần nhất.
main.dart
class MyWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Provider( create: (context) => 'A String far away.', child: Provider( create: (context) => 'A String that is close.', builder: (context, child) { // Displays 'A String that is close.' // There's no way to obtain 'A String far away.' return Text(Provider.of<String>(context)); }, ), ); } }
Cuối cùng, nếu bạn cố gắng truy cập vào một “type” không được cung cấp, bạn sẽ chỉ gặp lỗi khi chạy. Bên trong bất kỳ widget nào, bạn có thể truy cập tới các Provider của mình theo type
với cú pháp sau:
Provider.of<MyType>(context)
Nhưng nếu bạn không cẩn thận, có thể gặp lỗi với ProviderNotFoundException
khi chạy chương trình, dù lúc biên dịch chương trình hoàn toàn không báo lỗi. Điều này không phải là lý tưởng vì chúng ta phải luôn cố gắng bắt càng nhiều lỗi càng tốt tại thời điểm compile-time.
Ưu điểm của Riverpod
- Compile safe: Không còn gặp lỗi
ProviderNotFoundException
hoặc quên xử lý các trạng thái loading. Sử dụng Riverpod, nếu code được biên dịch thành công, nó sẽ hoạt động theo đúng ý bạn. - Provider, without its limitations: Riverpod có hỗ trợ multiple provider có cùng type; kết hợp các providers không đồng bộ; thêm providers từ mọi nơi, …
- Không phụ thuộc vào Flutter: Create/share/tests providers, with no dependency on Flutter. Điều này bao gồm việc có thể listen providers mà không cần một BuildContext.
2. Cách sử dụng Riverpod trong Flutter
Bước 1. Thêm thư viện riverpod
pubspec.yaml
dependencies: flutter: sdk: flutter flutter_riverpod: ^0.12.1
Bước 2. Khai báo biến toàn cục
Riverpod’s Providers không được đặt trong widget tree. Thay vào đó, chúng là các biến toàn cục nằm ở bất kỳ file nào mà bạn muốn.
final greetingProvider = Provider((ref) => 'Hello Riverpod!');
Provider đơn giản nhất này có thể cung cấp một giá trị read-only. Có nhiều loại Providers khác nữa để làm việc với Futures, Streams, ChangeNotifier, StateNotifier,…
Tham số ref có kiểu ProviderReference. Bạn sẽ thấy ở phần sau, nó chủ yếu được sử dụng để giải quyết sự phụ thuộc giữa các Provider.
Mặc dù Provider object có thể truy cập toàn cục, nhưng điều này không có nghĩa là provided object (trong trường hợp này là chuỗi “Hello Riverpod!”) là global. Giống như với một hàm toàn cục, bạn có thể gọi nó từ bất cứ đâu nhưng giá trị trả về cũng có thể trở thành phạm vi cục bộ. Hãy xem xét đoạn code sau:
String globalFunction() { return 'some value'; } class MyClass { void _classMethod() { final valueLocalToThisMethod = globalFunction(); } }
Bước 3. ProviderScope
Sau khi cài đặt xong Riverpod, chúng ta cần bao widget gốc của mình bằng ProviderScope
.
Gói Riverpod chỉ đi kèm với một InheritedWidget duy nhất cần được đặt phía trên toàn bộ widget trê được gọi là ProviderScope. Nó chịu trách nhiệm giữ một thứ gọi là ProviderContainer, thứ này có trách nhiệm lưu trữ trạng thái của các đối tượng Provider riêng lẻ.
void main() { runApp( ProviderScope( child: MyApp(), ), ); }
Bước 4. Theo dõi một provider
main.dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Riverpod Tutorial',
home: Scaffold(
appBar: AppBar(
title: Text('Riverpod Tutorial'),
),
body: Center(
child: Text('greeting goes here'),
),
),
);
}
}
Cách đầu tiên là thay đổi superclass tiện thành ConsumerWidget
của package flutter_riverpod
. Điều này thêm một tham số ScopedReader
vào phương thức build của class đó. Widget sẽ được rebuild nếu có bất kỳ sự thay đổi nào xảy ra.
main.dart
class MyApp extends ConsumerWidget {
@override
Widget build(BuildContext context, ScopedReader watch) {
// Gets the string from the provider and causes
// the widget to rebuild when the value changes.
final greeting = watch(greetingProvider);
return MaterialApp(
title: 'Riverpod Tutorial',
home: Scaffold(
appBar: AppBar(
title: Text('Riverpod Tutorial'),
),
body: Center(
child: Text(greeting),
),
),
);
}
}
Cách khác để nhận được giá trị từ provider là dùng Consumer, cách này sẽ hữu ích nếu bạn muốn nhanh chóng tối ưu hóa việc xây dựng lại widget con của mình, không muốn các widget khác cũng phải rebuild lại theo. Trong trường hợp này ta chỉ cần xây dựng lại Text widget bản bị ảnh hưởng trên toàn bộ cây widget.
main.dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Riverpod Tutorial',
home: Scaffold(
appBar: AppBar(
title: Text('Riverpod Tutorial'),
),
body: Center(
child: Consumer(
builder: (context, ref, child) {
final greeting = ref.watch(greetingProvider);
return Text(greeting);
},
),
),
),
);
}
}
Đọc một provider
Đôi khi, không thể gọi “watch” vì bạn không ở trong phương thức build. Hoặc bạn chỉ muốn lấy giá trị từ provider ra chứ không muốn widget sẽ rebuild. Ví dụ: bạn có thể muốn thực hiện một hành động khi một nút được nhấn. Đó là khi bạn có thể gọi context.read(). Dưới đây là một loại provider khác – ChangeNotifierProvider:
class IncrementNotifier extends ChangeNotifier {
int _value = 0;
int get value => _value;
void increment() {
_value += 1;
notifyListeners();
}
}
final incrementProvider = ChangeNotifierProvider((ref) => IncrementNotifier());
Ta sẽ lấy ví dụ với Counter App. Text widget sẽ theo “watch” provider và tự động được rebuilt nếu xảy ra thay đổi và FloatingActionButton sẽ chỉ đọc provider để gọi phương thức increment() trên đó.
main.dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Riverpod Tutorial',
home: Scaffold(
body: Center(
child: Consumer(
builder: (context, watch, child) {
final incrementNotifier = watch(incrementProvider);
return Text(incrementNotifier.value.toString());
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
context.read(incrementProvider).increment();
},
child: Icon(Icons.add),
),
),
);
}
}
3. Những điểm tuyệt vời của Riverpod
Sử dụng nhiều provider cùng type
final firstStringProvider = Provider((ref) => 'First');
final secondStringProvider = Provider((ref) => 'Second');
// Somewhere inside a ConsumerWidget
final first = watch(firstStringProvider);
final second = watch(secondStringProvider);
Dependency between providers
Bất kỳ ứng dụng nào trong thực tế đều có sự phụ thuộc giữa các lớp. Ví dụ: bạn có thể có ChangeNotifier phụ thuộc vào Repository,mà nó lại phụ thuộc vào HttpClient. Xử lý các phụ thuộc như vậy với Riverpod rất đơn giản và dễ đọc.
Với ví dụ đơn giản sau, chỉ có một FutureProvider phụ thuộc trực tiếp vào một FakeHttpClient. Việc getting một provider khác bên trong function của provider được thực hiện bằng cách gọi read trên tham số ProviderReference – ref luôn được chuyển vào. Nếu bạn phụ thuộc vào provider có giá trị có thể thay đổi, bạn cũng có thể gọi watch.
main.dart
class FakeHttpClient {
Future<String> get(String url) async {
await Future.delayed(const Duration(seconds: 1));
return 'Response from $url';
}
}
final fakeHttpClientProvider = Provider((ref) => FakeHttpClient());
final responseProvider = FutureProvider<String>((ref) async {
final httpClient = ref.read(fakeHttpClientProvider);
return httpClient.get('https://resocoder.com');
});
Sử dụng các giá trị từ FutureProvider từ UI thay cho FutureBuilders rất tuyệt vời. Riverpod giúp việc xây dựng các widgets dựa trên Future một cách dễ dàng.
main.dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Riverpod Tutorial',
home: Scaffold(
body: Center(
child: Consumer(
builder: (context, watch, child) {
final responseAsyncValue = watch(responseProvider);
return responseAsyncValue.map(
data: (_) => Text(_.value),
loading: (_) => CircularProgressIndicator(),
error: (_) => Text(
_.error.toString(),
style: TextStyle(color: Colors.red),
),
);
},
),
),
),
);
}
}
Passing arguments to providers
main.dart
final responseProvider =
FutureProvider.family<String, String>((ref, url) async {
final httpClient = ref.read(fakeHttpClientProvider);
return httpClient.get(url);
});
Bạn có thể thử thay đổi chuỗi URL được mã hóa cứng và bạn sẽ thấy rằng hàm này sẽ FutureProvider
chạy lại hàm tạo của nó mỗi khi bạn thay đổi chuỗi được truyền vào.
main.dart
final responseAsyncValue = watch(responseProvider('https://resocoder.com'));
Automatically disposing of state
Bộ nhớ đệm cho provider’s state là rất tuyệt nhưng đôi khi bạn muốn hủy trạng thái của một provider khi nó không còn được sử dụng nữa vì nhiều lý do như:
- Khi sử dụng Firebase, bạn muốn đóng connect để tránh các phí phát sinh
- Để thiết lập lại trạng thái khi người dùng rời khỏi màn hình và vào lại.
final responseProvider =
FutureProvider.autoDispose.family<String, String>((ref, url) async {
final httpClient = ref.read(fakeHttpClientProvider);
return httpClient.get(url);
});
autoDispose sẽ loại bỏ provider’s state ngay khi provider không được sử dụng. Trong ví dụ trên, điều này xảy ra ta thay đổi đối số được truyền vào provider family. Tuy nhiên, autoDispose hữu ích ngay cả khi bạn không sử dụng family modifier. Trong trường hợp đó, việc xóa bỏ được bắt đầu khi ConsumerWidget một provider bị disposed.
Bài viết được biên tập từ https://resocoder.com/2020/11/27/flutter-riverpod-tutorial-the-better-provider/