Riverpod of flutter state management

Time:2022-1-8

Riverpod of flutter state management

I’ve seen it in some flutter topics in the last month or twoRiverpodThis keyword is a new way of flutter state management.

There are many ways of state management in flutter,ReduxBlocMobXProviderwait. For a single provider, I have also seen various combinations, such as changenotifier + provider / statenotifier + provider (+ freed). Each method has its own advantages. We can choose according to our own habits and the situation of the project. We won’t discuss it here. This article is just an introductionRiverpod, provide you with a new choice.

1. Introduction

Riverpod of flutter state management

RiverpodandProviderThey all come from the authorRemiRiverpodCan be consideredProviderTo realize the originally impossible function.Just like its name, the letters are the same as the provider, but different.

You can understandRiverpodyesProviderUpgraded version of, solvedProviderSome pain points:

  • ProvideryesInheritedWidgetSo it is necessary to read the stateBuildContext。 This leads to many limitations that many novices don’t understandInheritedWidgetandBuildContextIt is often difficult to get status across pagesProviderNotFoundExceptionandRiverpodNo longer rely on flutter, that is, it is not usedInheritedWidget, so it’s not necessaryBuildContext
  • Read object isCompile safe。 There are not so many runtime exceptions.
  • There can be multiple providers of the same type.
  • The provider can be private.
  • Automatically reclaim the provider when it is no longer in use.

Of course, at presentRiverpodThere are also some deficiencies (version 0.9.1):

  • After all, it was not born long ago, and it cannot be guaranteed to be completely stable.
  • There may be destructive changes to the API later. (for example, there are a lot of breaking in 0.7.0, which leads to errors in some of the examples I wrote earlier.)
  • It needs to be used with caution in the current production environment.

2. How to select

The author providesRiverpodHow to select the three methods is shown in the figure below:

Riverpod of flutter state management
Flutter is not introduced in this article_ Hooks related content, here I chooseflutter_riverpod。 Then add it topubspec.yamlMedium:

flutter_riverpod: ^0.9.1

Then executeflutter pub get

3. Foundation use

Provider

Use hereRiverpodofProviderIt takes three steps.

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// 1. Create a global provider to store "Hello world!"
final Provider<String> helloWorldProvider = Provider((_) => 'Hello World!');

void main() {
  runApp(
    // 2. Add providerscope. All flutter programs using riverpod must
    //Add it at the root of the widget tree to store each provider.
    ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Riverpod Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: ProviderExample(),
    );
  }
}

// 3. Use "consumerwidget" to obtain the corresponding provider in "build"
class ProviderExample extends ConsumerWidget {

  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final String value = watch(helloWorldProvider);

    return Scaffold(
      appBar: AppBar(title: Text('Provider Example')),
      body: Center(
        child: Text(value),
      ),
    );
  }
}

“Hello world!” is stored here UsingProvider, which provides an object that will never change. However, in most scenarios, the state is variable. The following is an example of a counter.

StateProvider

On the basis of “Hello world”, two modifications can be made.

  1. Define a global constantStateProvider

    final StateProvider<int> counterProvider = StateProvider((_) => 0);
  2. import 'package:flutter/material.dart';
    import 'package:flutter_riverpod/flutter_riverpod.dart';
    
    class StateProviderExample extends StatelessWidget {
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('StateProvider Example'),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Text(
                  'You have pushed the button this many times:',
                ),
                Consumer(
                  builder: (context, watch, _) {
                    ///Use consumer (encapsulation of consumerwidget) to control the refresh range.
                    int count = watch(counterProvider).state;
                    return Text(
                      '$count',
                      style: Theme.of(context).textTheme.headline4,
                    );
                  },
                ),
              ],
            ),
          ),
          floatingActionButton: FloatingActionButton(
            ///Use read to obtain the counter provider and operate state.
            onPressed: () => context.read(counterProvider).state++,
            tooltip: 'Increment',
            child: Icon(Icons.add),
          ),
        );
      }
    
    }

If your state is complex, you can useChangeNotifierProvider, if usedStateNotifier, you can useStateNotifierProvider。 actuallyStateProviderThe inside of the isStateController, orStateNotifier。 The source code is as follows;

class StateProvider<T>
    extends AlwaysAliveProviderBase<StateController<T>, StateController<T>> {
  
  StateProvider(
    Create<T, ProviderReference> create, {
    String name,
  }) : super((ref) => StateController(create(ref)), name);
 ...
}

class StateController<T> extends StateNotifier<T> {
  StateController(T state) : super(state);

  @override
  T get state => super.state;

  @override
  set state(T value) => super.state = value;
}

StateNotifierProviderUsage andStateProviderBasically the same. I won’t post it here. Those who are interested canClick here to view

ChangeNotifierProvider

There’s nothing to say in this part. Pay attentionChangeNotifierAndStateNotifierYou need to call it yourselfnotifyListenersNotification of changes.

final ChangeNotifierProvider<Counter> _counterProvider = ChangeNotifierProvider((_) => Counter());

class Counter extends ChangeNotifier {
  int _count = 0;

  int get count => _count;

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

class ChangeProviderNotifierExample extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('ChangeNotifierProvider Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Consumer(
              builder: (context, watch, _) {
                int count = watch(_counterProvider).count;
                return Text(
                  '$count',
                  style: Theme.of(context).textTheme.headline4,
                );
              },
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        ///Use read to get the counter provider.
        onPressed: () => context.read(_counterProvider).increment(),
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

FutureProvider

final FutureProvider<String> futureProvider = FutureProvider((_) async {
  ///Delay 3S
  await Future.delayed(const Duration(seconds: 3));
  return 'Riverpod';
});

class FutureProviderExample extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('FutureProvider Example'),
      ),
      body: Center(
        child: Consumer(
          builder: (context, watch, _) {
            AsyncValue<String> futureProviderValue = watch(futureProvider);
            ///Display according to corresponding status
            return futureProviderValue.when(
              loading: () => CircularProgressIndicator(),
              error: (error, stack) => Text('Oops, something unexpected happened'),
              data: (value) => Text(
                'Hello $value',
                style: Theme.of(context).textTheme.headline4,
              ),
            );
          },
        ),
      ),
    );
  }
}

The author also providesStreamProvider。 The usage is similar. If you are interested, you can see my sample code.

ProviderListener

If you want to monitor the status changes of the provider on the widget tree, you can useProviderListener。 Using the counter example above, when the counter is5Trigger listening when.

ProviderListener<StateController<int>>(
  provider: counterProvider,
  onChange: (_, counter) {
    if (counter.state == 5) {
      Print ('current counter is 5, trigger listening ');
    }
  },
  child: Consumer(
    builder: (context, watch, _) {
      int count = watch(counterProvider).state;
      return Text(
        '$count',
        style: Theme.of(context).textTheme.headline4,
      );
    },
  ),
),

ScopeProvider

Generally, when implementing a list item, we need to pass in the corresponding index, which is roughly as follows:

ListView.builder(
  itemCount: 50,
  itemBuilder: (context, index) {
    return ProductItem(index: index);
  },
)

If usedScopedProviderCombineProviderScope, you can simply get the index without receiving it from the constructor. It’s easy to use. You can use the code directly:

///Define scopedprovider
final ScopedProvider<int> currentProductIndex = ScopedProvider<int>(null);

class ScopeProviderExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('ScopedProvider'),
      ),
      body: ListView.builder(
        itemCount: 50,
        itemBuilder: (context, index) {
          return ProviderScope(
            overrides: [
              ///Modify value
              currentProductIndex.overrideWithValue(index),
            ],
            ///"Productitem" is instantiated with the 'const' keyword,
            ///But you can still get content dynamically internally.
            child: const ProductItem(),
          );
        },
      ),
    );
  }
}

class ProductItem extends ConsumerWidget {

  const ProductItem({Key key}): super(key: key);

  @override
  Widget build(BuildContext context, ScopedReader watch) {
      ///Get the corresponding index
    final index = watch(currentProductIndex);
    return ListTile(title: Text('item $index'));
  }
}

4. Modifier

family

familyThe function of is to add a parameter when obtaining the provider. Take the example directly, and you can see at a glance:

///With family, you can pass in city when you get the provider
final _weatherProvider = Provider.family<String, String>((ref, city) {
  return '$city (Sunny)';
});

class FamilyExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Family')),
      body: Center(
        child: Consumer(
          builder: (context, watch, _) {
            ///Here you can refer to "London"
            final String weather = watch(_weatherProvider('London'));
            return Text('$weather',);
          },
        ),
      ),
    );
  }
}

be careful:usefamilyThe parameters passed in are limited. such asbool intdoubleString, constant, or override==andhashCodeImmutable object.

autoDispose

In our previous example, the created provider is saved at the root of the widget tree. Therefore, even if the page is closed, the previous state will be obtained when entering the page again.

This is obviously inflexible, so it can be used hereautoDispose, it can automatically destroy the provider when we no longer use it. Then using it reasonably can avoid memory leakage.

For example, in the previous counter example, you only need to add oneautoDisposeSuch problems can be avoided.

final stateProvider = StateProvider.autoDispose((_) => 0);

If you need to customize the dispose event, you can useonDispose。 For example, your provider has a network request (using DIO):

final myProvider = FutureProvider.autoDispose((ref) async {
  
  final cancelToken = CancelToken();
  //When the provider is destroyed, the HTTP request is cancelled
  ref.onDispose(() => cancelToken.cancel());

  //HTTP request
  final response = await dio.get('path', cancelToken: cancelToken);
  //If the request completes successfully, it remains in this state.
  ref.maintainState = true;
  return response;
});

In the code aboveref.maintainState, this parameter defaults to false. If the user leaves the page and the request fails, the request will be executed again the next time. However, if the request completes successfully (maintainstate is true), the state will be retained and a new request will not be triggered the next time you re-enter the page.

useautoDisposeYou can limit whether the provider is global or local. In this way, it is more convenient to solve the problem of using provider across pages.

5. Advanced use

Combining providers

1. If the created provider needs the status of another provider, it needs to be usedProviderReferenceofreadmethod.

The following example is a provider given to a city and country. When creating a locationprovider, it obtains the status of the city and country.

final Provider<String> cityProvider = Provider((ref) => 'London');
final Provider<String> countryProvider = Provider((ref) => 'England');
final Provider<Location> locationProvider = Provider((ref) => Location(ref));

class Location {
  Location(this._ref);

  final ProviderReference _ref;

  String get label {
    ///Read get
    final city = _ref.read(cityProvider);
    final country = _ref.read(countryProvider);
    return '$city ($country)';
  }
}

useRiverpodYou can provide multiple providers of the same type, which is also compared withProviderAn advantage of.

2. If the obtained status value will change, we need to listen to it. have access toProviderReferenceofwatchmethod.

The following example is given to a city provider. When the city changes, the weather changes accordingly.

final StateProvider<String> cityProvider = StateProvider((ref) => 'London');
final StateProvider<String> weatherProvider = StateProvider((ref) {
  ///Watch monitor
  final String city = ref.watch(cityProvider).state;
  return '$city (Sunny)';
});

class CombiningProviderExample2 extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('CombiningProvider')),
      body: Center(
        child: Consumer(
          builder: (context, watch, _) {
            final String weather = watch(weatherProvider).state;
            return Text('$weather',);
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          String city = context.read(cityProvider).state;
                ///Modification status
          if (city == 'London') {
            context.read(cityProvider).state = "Xi'an";
          } else {
            context.read(cityProvider).state = 'London';
          }
        },
        tooltip: 'Refresh',
        child: Icon(Icons.refresh),
      ),
    );
  }
}

refresh

Force the provider to refresh immediately and return the created value again. This is suitable for the drop-down refresh of the list, or retry when the request data is wrong.

final FutureProvider<List<String>> productsProvider = FutureProvider((_) async {
  ///Delay 3S
  await Future.delayed(const Duration(seconds: 3));
  return List.generate(50, (index) => 'Item $index');
});

class RefreshProviderExample extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('RefreshProvider'),
      ),
      body: Center(
        child: Consumer(
          builder: (context, watch, _) {
            AsyncValue<List<String>> productsProviderValue = watch(productsProvider);
            return productsProviderValue.when(
              loading: () => CircularProgressIndicator(),
              error: (error, stack) => Text('Oops, something unexpected happened'),
              data: (list) => RefreshIndicator(
                onRefresh: () => context. Refresh (productsprovider), // / refresh
                child: ListView(
                  children: [
                    for (final item in list) ListTile(title: Text(item)),
                  ],
                ),
              ),
            );
          },
        ),
      ),
    );
  }
}

select

When a value in the state changes, the correspondingConsumerLowerbuilderWill execute and rebuild the widget. If usedselectYou can specify a value to refresh when it is changed, and accurately control the refresh range to avoid unnecessary rebuild.

However, at present (version 0.9.1),selectThis kind of local listening is only supportedhooks_riverpodWrappeduseProvider。 So you need to quote herehooks_riverpod

final ChangeNotifierProvider<Person> personProvider = ChangeNotifierProvider((_) => Person());

class Person extends ChangeNotifier {
  int _age = 0;
  int get age => _age;
  set age(int age) {
    _age = age;
    notifyListeners();
  }

  String _name = 'weilu';
  String get name => _name;
  set name(String name) {
    _name = name;
    notifyListeners();
  }
}

class SelectExample extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Select Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            HookBuilder(
              builder: (_) {
                String name = useProvider(personProvider.select((p) => p.name));
                ///If you use the following method, the text here will also refresh when the age changes.
//                String name = useProvider(personProvider).name;
                return Text(
                  'name:$name',
                );
              },
            ),
            HookBuilder(
              builder: (_) {
                int age = useProvider(personProvider.select((p) => p.age));
                return Text(
                  'age:$age',
                );
              },
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        //When the age changes, only the corresponding text will change.
        onPressed: () => context.read(personProvider).age = Random.secure().nextInt(255),
        tooltip: 'Refresh',
        child: Icon(Icons.refresh),
      ),
    );
  }
}

other

Careful you will find that in usereadIt is still used when obtaining the providercontext。 At first, it was not said that it was not usedInheritedWidget, so it’s not necessaryBuildContextAre you?

actuallyRiverpodThis is true in itself, but in the application of fluent, in order to facilitate efficient (time complexity O (1)) acquisition in widget treeProviderContainer(in)ProviderScopeIt is created implicitly in. It is used to store providers. It needs to be used at the rootInheritedWidgetTo facilitate the final acquisition of the provider.

readrefreshConsumerProviderListenerAnd other methods and the inside of the widget are actually calledProviderScope.containerOf(context, listen = xx);, the difference is the value of listen.

static ProviderContainer containerOf(
    BuildContext context, {
    bool listen = true,
  }) {
    UncontrolledProviderScope scope;
    if (listen) {
      scope = context //
          .dependOnInheritedWidgetOfExactType<UncontrolledProviderScope>();
    } else {
      scope = context
          .getElementForInheritedWidgetOfExactType<UncontrolledProviderScope>()
          .widget as UncontrolledProviderScope;
    }
    return scope.container;
  }

such asreadThe value of listen in is false, usegetElementForInheritedWidgetOfExactTypeMethod so that it will not be used when the data changesdidChangeDependencies, avoid unnecessary rebuild. In contrast,ConsumerProviderListenerThe value of listen in is true, which will realize the widget reconstruction we need.

We can also check the existing status through the fluent inspector. All statuses are summarized in theProviderScopeNext, this is alsoRiverpodAn advantage of. As shown in the figure below:

Riverpod of flutter state management


When this article is published, relevantRiverpodThere is little information and discussion. This article is also my understanding after practicing the official website documents. If there are errors, please point them out!

in my opinionRiverpodIt is a relatively easy and convenient way of state management. When it is stable, it should be loved by more people.

RiverpodI have uploaded the relevant sample code toGithub, you can have a look if you are interested. Back ifRiverpodWhen there are changes, I will update them in time. You can collect it, give me more praise and support, and give me some update power!

reference resources

Recommended Today

Proper memory alignment in go language

problem type Part1 struct { a bool b int32 c int8 d int64 e byte } Before we start, I want you to calculatePart1What is the total occupancy size? func main() { fmt.Printf(“bool size: %d\n”, unsafe.Sizeof(bool(true))) fmt.Printf(“int32 size: %d\n”, unsafe.Sizeof(int32(0))) fmt.Printf(“int8 size: %d\n”, unsafe.Sizeof(int8(0))) fmt.Printf(“int64 size: %d\n”, unsafe.Sizeof(int64(0))) fmt.Printf(“byte size: %d\n”, unsafe.Sizeof(byte(0))) fmt.Printf(“string size: %d\n”, […]