[Flutter] State management ②: From provider to Riverpod. Take the counter app as an example.

This article can be read in about 28 minutes.

Introduction

In the previous article, I described state management from statefulWidget + setState to Provider. This time, we will look at everything from Provider to Riverpod.

Provider challenges

  1. Context-dependent: Provider is highly dependent on context BuildContext, which makes the code difficult to test and reuse. This dependency is especially problematic when trying to access the state from outside the widget tree or from non-UI code.
  2. When using immutability Provider , you can have mutable state, which can lead to bugs. Ideally, the state should be immutable, but Provideryou cannot force this alone.
  3. Initialization complexity: If there are other states that a particular state depends on, Providerit is relatively complex to handle this.

Let’s look at an example of a counter app.

Dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => CounterProvider(),
      child: MaterialApp(
        home: MyHomePage(),
      ),
    );
  }
}

class CounterProvider with ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counter = Provider.of<CounterProvider>(context);

    return Scaffold(
      appBar: AppBar(
        title: Text('Provider Counter Example'),
      ),
      body: Center(
        child: Text('Counter: ${counter.count}'),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => counter.increment(),
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

context dependent

MyHomePageBuildContextis CounterProviderbeing accessed using . This context dependency causes problems, especially when you want to access the state but its position in the widget tree is unclear.

Immutability problem = object overwriting

CounterProvider_countstate that can be changed within . Flutter is a reactive framework, so it typically notifyListeners()calls to notify the widget tree of changes. However, if for some reason notifyListeners()it is not called, the UI will be out of sync with the actual state.

Initialization complexity

Although not clearly shown in this simple counter app, in more complex scenarios where multiple counters depend on each other, Providermanaging these dependencies can become cumbersome. For example, if one counter is to be initialized dependent on the value of another counter, this dependency needs to be precisely coded, but Providermanaging it alone may not be intuitive.

How to write Riverpod

Riverpodis a library designed to try to solve these problems. ProviderDeveloped by the authors of and Providerimproved upon the ideas of.

A similar counter app implemented using Riverpod would look like this:

Add ProviderScope to route

ProviderScope If you enclose a Widget in the upper tree with , you will be able to call the Provider in the Widget in the lower tree. In the code below, by MyApp() enclosing ProviderScope , Provider can be called by Widgets after MyApp.

Dart
void main() {
  runApp(
    ProviderScope(child: MyApp()),
  );
}

Define Provider as a global variable

Provider as a global variable

  • counterProvideris a global variable that can be accessed from anywhere in the app. This CounterNotifierallows references to manage state from anywhere
  • StateNotifierProvider When defining, <  subclass name of managed,   type of data to be managed> is required StateNotifierProvider after .StateNotifierStateNotifier
  • counterProvideris StateNotifierProvider<CounterNotifier, int>an object of type, and its entity is CounterNotifieran instance of the class. CounterNotifierinherits StateNotifier<int>from and manages the state of integer types.

callback function

  • StateNotifierProviderThe callback function passed to the constructor CounterNotifiercreates and returns an instance of . This function is executed the first time the provider is called and initializes the state.

ProviderRef

  • The callback function argument is an object used to refer to state or services from other providers .refProviderRef

StateNotifier

  • CounterNotifieris a class that inherits from StateNotifier, which manages the state of the counter (in this case, the integer value). super(0)In the constructor , 0it means that the initial value of the counter is set to .

Update status

  • incrementThe method increments the value of the counter. staterepresents the current counter value, and state++the state is updated by. This is immutable state management, where new state objects are created to update state and old state objects are destroyed . StateNotifiernotifies subscribed widgets when the state is updated and triggers a redraw of the UI.
Dart
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
  return CounterNotifier();
});

class CounterNotifier extends StateNotifier<int> {
  CounterNotifier() : super(0);

  void increment() {
    state++;
  }
}

Example of using ref

For example, CounterNotifierif gets an initial value from a configuration provider on startup, refyou can access that value using:

In this example, CounterNotifierthe constructor of receives an initial value, which settingsProvideris obtained from . ref.readmethod settingsProviderto access the current value and CounterNotifieruse that value to initialize the . refis the key object for accessing other providers within a provider’s callback.

Dart

Dart
final settingsProvider = Provider<Settings>((ref) => Settings());

final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
  // Get settings from settingsProvider
  final settings = ref.read(settingsProvider);
  // Initialize CounterNotifier by getting initial counter value from configuration
  return CounterNotifier(settings.initialCounterValue);
});

class CounterNotifier extends StateNotifier<int> {
  CounterNotifier(int initialCounter) : super(initialCounter);

  void increment() {
    state++;
  }
}

Get data from Provider

ConsumerWidget

  • ConsumerWidgetis a special widget provided by Riverpod that you Providercan inherit from to configure the widget to read data from and rebuild based on changes to that data.

WidgetRef

  • buildWidgetRefThe arguments passed to the method Providerare used to read data from. is used in place of and provides access to data and additional functionality regarding the widget lifecycle.WidgetRefBuildContextProvider

watch() and read()

  • ref.watch()ref.read()The argument specified to the method is a globally defined provider object. In this case, ref.watch(counterProvider)by calling , counterProviderwe can access the state managed by , in this case the value of a counter, and watch that value change.
  • ref.watch():When the UI needs to monitor data changes
  • ref.read(): When the UI does not need to monitor data changes. For example, when triggering an action (e.g. processing when a button is tapped).

Calling increment()

  • FloatingActionButtononPressedBy calling Sode ref.read(counterProvider.notifier).increment(), the value of the counter increases.
  • StateNotifier A subclass of  can be obtained by specifying  an instance WidgetRef of the class watch() or read() as an argument .StateNotifierProvider.notifier
  • StateNotifier provides two different objects, and  it is not necessary to directly manage and access the state (  Is necessary.state.notifier.notifier
Dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';


class MyApp extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);

    return Scaffold(
      appBar: AppBar(
        title: Text('Riverpod Counter Example'),
      ),
      body: Center(
        child: Text('Counter: $count'),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.read(counterProvider.notifier).increment(),
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

What changed and what stayed the same from Provider to Riverpod?

ConsumerWidget role: Same

  • In both cases, ConsumerWidgetis responsible for updating the UI based on data changes. ConsumerWidgetallows the widget to be rebuilt when the data it depends on changes.

Introduction of WidgetRef

  • BuildContextProvider requires when accessing state . It locates and references the state based on the widget’s position in the Flutter widget tree. That is, get the state using , like or Provider.of<T>(context).context.watch<T>()BuildContext
  • With Riverpod WidgetRef‘s introduction, developers can use widgets to read state and monitor state changes using widgets BuildContextwithout worrying about their location. At this time , Riverpod internally handles the location of the widget that needs to change its state (a role traditionally played by BuildContext).ref.read(provider)ref.watch(provider)

Benefits of Riverpod

Context independent (due to the introduction of WidgefRef)

  • Riverpod BuildContextallows you to access state without relying on. ProviderScopeProvide state throughout your app using , and WidgetRefaccess state using . This allows you to easily view the status from anywhere and improves the ease of testing.

Enhanced immutability = discard objects and create new ones instead of overwriting them

  • StateNotifierBy using , you can guarantee state immutability. stateis kept private and statecan only be modified through properties. This keeps the UI and state consistent and reduces the risk of bugs.

Clear dependency management

  • With Riverpod, dependencies between providers can be clearly defined. To reference a provider, use ref.watchor ref.read, which allows state dependencies to be managed clearly and concisely.

Difference and usage of CosumerWidget and ConsumerStatefulWidget

ConsumerWidget

  • ConsumerWidgetis immutable and has no internal state. Any state change causes the entire widget to be rebuilt. In a class that inherits, the method receives as an argument.ConsumerWidgetbuildWidgetRef
  • In cases where state management is not required (displaying static content, simple display of external data, etc.), it ConsumerWidgetis simpler and easier to maintain.
  • StatelessWidgetWidgetUse when you Note that Consumerif you use it without wrapping it with , Widgetthe entire file will be rebuilt.
Dart
class MyConsumerWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final counter = ref.watch(counterProvider);
    return Text('$counter');
  }
}

ConsumerStatefulWidget

  • ConsumerStatefulWidgetRiverpod compatible version of ConsumerStateTohatosono . The main difference is that objects are included as properties. This allows you to access Riverpod’s features at any time within the app .StatefulWidgetStateConsumerStaterefConsumerStateref
  • ConsumerWidgetis passed as a method argument , but no such argument is passed in . Since is a property of you can use it directly within the class .buildrefConsumerStatefulWidgetrefConsumerStateConsumerStateref
  • ref.watchIf there are multiple , and the state is updated frequently , ConsumerWidgetit will be rebuilt each time, which may be inefficient in terms of performance ( Consumerif you use it without wrapping it, the state change part will be rebuilt) (This can be prevented, as it is subject to In such scenarios, ConsumerStatefulWidgetit is better to use .
Dart
class MyConsumerStatefulWidget extends ConsumerStatefulWidget {
  @override
  _MyConsumerStatefulWidgetState createState() => _MyConsumerStatefulWidgetState();
}

class _MyConsumerStatefulWidgetState extends ConsumerState<MyConsumerStatefulWidget> {
  @override
  Widget build(BuildContext context) {
    final counter = ref.watch(counterProvider);
    return Text('$counter');
  }
}

6 types of providers

Provider nameType of data to managethe purpose
ProviderAnyProvide data that cannot be modified externally and is accessible throughout the app
StateNotifierProviderSubclass of StateNotifierProvide state management that encapsulates state changes and business logic
FutureProviderany FutureHandle the results of asynchronous operations and provide data that will be available at some point in the future
StreamProviderany StreamSubscribe to data from streams and provide data that changes over time
StateProviderAnyProvides simple state reading and writing and easy state management within widgets
ChangeNotifierProviderSubclasses of ChangeNotifierUse ChangeNotifier to listen for state changes and notify multiple listeners about state changes

Provider

  • Purpose: Provide data that cannot be changed externally. Used when you want the provider’s users to only read the value without manipulating it.
  • Use case: Provide configuration information that is shared across applications.
Dart
final configProvider = Provider((ref) => AppConfig(apiBaseUrl: '<https://api.example.com>'));
final config = ref.watch(configProvider);

StateProvider

  • Purpose: Provide simple state reading and writing and easy state management within widgets. Used when you want the provider’s users to manipulate and reference the value.
  • Example use: Manage UI toggle state.
Dart
final toggleProvider = StateProvider<bool>((ref) => false);
ref.read(toggleProvider.notifier).state = !ref.read(toggleProvider);

StateNotifierProvider

  • Purpose: Provide state management that encapsulates state changes and business logic.
  • Usage example: State management of a counter app.
Dart
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) => CounterNotifier());
ref.read(counterProvider.notifier).increment();

FutureProvider

  • Purpose: Handle the results of asynchronous operations and provide data that will be available at some point in the future.
  • Example usage: Fetching data from a network request.
Dart
final fooProvicer = FutureProvider<String>((ref) async {
  await Future.delayed(const Duration(seconds: 3));
  return "foo";
});

class FooWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) => Consumer(
    builder: (context, ref, child) => ref.watch(fooProvicer).when(
      data: (foo) => Text(foo),
      error: (err, st) => const Text("Error"),
      loading: () => const Text("Loading"),
    ),
  );
}

StreamProvider

  • Purpose: Subscribe to data from streams and provide data that changes over time. Stream version of FutureProvider
  • Use case: Provide real-time stock price information.
Dart
final fooProvicer = FutureProvider<String>((ref) async {
  await Future.delayed(const Duration(seconds: 3));
  return "foo";
});

class FooWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) => Consumer(
    builder: (context, ref, child) => ref.watch(fooProvicer).when(
      data: (foo) => Text(foo),
      error: (err, st) => const Text("Error"),
      loading: () => const Text("Loading"),
    ),
  );
}

ChangeNotifierProvider

  • Purpose: ChangeNotifierListen for state changes using and notify multiple listeners of state changes.
Dart
final formStateProvider = ChangeNotifierProvider<FormStateNotifier>((ref) => FormStateNotifier());

コメント

Copied title and URL