Hướng dẫn Riverpod trong Flutter (Flutter Riverpod Tutorial)

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.

SIÊU SALE SHOPEE 12.12 https://shope.ee/1VOIDFMXxP TIKI https://bitly.global/CJK6J1

Mời bạn xem danh sách video hướng dẫn quản lý trạng thái trong flutter:

SIÊU SALE SHOPEE 12.12 https://shope.ee/1VOIDFMXxP TIKI https://bitly.global/CJK6J1

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.

SIÊU SALE SHOPEE 12.12 https://shope.ee/1VOIDFMXxP TIKI https://bitly.global/CJK6J1

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)

SIÊU SALE SHOPEE 12.12 https://shope.ee/1VOIDFMXxP TIKI https://bitly.global/CJK6J1

Hãy tưởng tượng bạn có MySecondClass phụ thuộc vào MyFirstClass.

main.dart

SIÊU SALE SHOPEE 12.12 https://shope.ee/1VOIDFMXxP TIKI https://bitly.global/CJK6J1
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 😟

main.dart
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.

SIÊU SALE SHOPEE 12.12 https://shope.ee/1VOIDFMXxP TIKI https://bitly.global/CJK6J1

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:

SIÊU SALE SHOPEE 12.12 https://shope.ee/1VOIDFMXxP TIKI https://bitly.global/CJK6J1

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.

SIÊU SALE SHOPEE 12.12 https://shope.ee/1VOIDFMXxP TIKI https://bitly.global/CJK6J1

Ư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

SIÊU SALE SHOPEE 12.12 https://shope.ee/1VOIDFMXxP TIKI https://bitly.global/CJK6J1

Bước 1. Thêm thư viện riverpod

Đầu tiên ta cần thêm package riverpod vào file pubspec.yaml trong dự án Flutter của bạn. Trong bài này, ta sẽ sử dụng flutter_riverpod, ngoài ra còn có hooks_riverpodriverpod.

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.

SIÊU SALE SHOPEE 12.12 https://shope.ee/1VOIDFMXxP TIKI https://bitly.global/CJK6J1

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,…

SIÊU SALE SHOPEE 12.12 https://shope.ee/1VOIDFMXxP TIKI https://bitly.global/CJK6J1

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:

SIÊU SALE SHOPEE 12.12 https://shope.ee/1VOIDFMXxP TIKI https://bitly.global/CJK6J1
function_analogy.dart
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ẻ.

SIÊU SALE SHOPEE 12.12 https://shope.ee/1VOIDFMXxP TIKI https://bitly.global/CJK6J1
main.dart
void main() {
  runApp(
    ProviderScope(
      child: MyApp(),
    ),
  );
}

Bước 4. Theo dõi một provider

Làm cách nào để chúng ta lấy được string từ greetingProvider để có thể hiển thị trong Text? Thực tế có hai cách để làm điều đó.

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.

SIÊU SALE SHOPEE 12.12 https://shope.ee/1VOIDFMXxP TIKI https://bitly.global/CJK6J1

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.

SIÊU SALE SHOPEE 12.12 https://shope.ee/1VOIDFMXxP TIKI https://bitly.global/CJK6J1

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:

SIÊU SALE SHOPEE 12.12 https://shope.ee/1VOIDFMXxP TIKI https://bitly.global/CJK6J1
main.dart
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

SIÊU SALE SHOPEE 12.12 https://shope.ee/1VOIDFMXxP TIKI https://bitly.global/CJK6J1
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

Riverpod’s providers objects không dựa vào types để tìm kiếm các provider, nên có nhiều provider cùng type mà không gặp vấn đề gì
example.dart
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.

SIÊU SALE SHOPEE 12.12 https://shope.ee/1VOIDFMXxP TIKI https://bitly.global/CJK6J1

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.

SIÊU SALE SHOPEE 12.12 https://shope.ee/1VOIDFMXxP TIKI https://bitly.global/CJK6J1

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

Nếu bạn muốn chuyển một URL do người dùng xác định đến responseProvider? Hãy dùng family, thay đổi responseProvider thành như sau…

main.dart

SIÊU SALE SHOPEE 12.12 https://shope.ee/1VOIDFMXxP TIKI https://bitly.global/CJK6J1
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

SIÊU SALE SHOPEE 12.12 https://shope.ee/1VOIDFMXxP TIKI https://bitly.global/CJK6J1
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.
main.dart
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.

SIÊU SALE SHOPEE 12.12 https://shope.ee/1VOIDFMXxP TIKI https://bitly.global/CJK6J1

Bài viết được biên tập từ https://resocoder.com/2020/11/27/flutter-riverpod-tutorial-the-better-provider/

SIÊU SALE SHOPEE 12.12 https://shope.ee/1VOIDFMXxP TIKI https://bitly.global/CJK6J1

Leave a Comment