Use and encapsulation of the Flutter state management provider

Flutter provides the InheritedWidget class to help us handle state management between parent and child components. Providers wrap around inheritedWidgets to make them easy for developers to use and take. But when you first look at the provider’s documentation, it’s a bit confusing:

name description
Provider The most basic form of provider. It takes a value and exposes it, whatever the value is.
ListenableProvider A specific provider for Listenable object. ListenableProvider will listen to the object and ask widgets which depend on it to rebuild whenever the listener is called.
ChangeNotifierProvider A specification of ListenableProvider for ChangeNotifier. It will automatically call ChangeNotifier.dispose when needed.
ValueListenableProvider Listen to a ValueListenable and only expose ValueListenable.value.
StreamProvider Listen to a Stream and expose the latest value emitted.
FutureProvider Takes a Future and updates dependents when the future completes.

Aren’t providers easy to use? I just want to manage my state in a simple way, but I’m given so many choices. Which one should I choose? The choice of difficult disease anxious to pull hair.

use

Create a new Futter project and change the default counter layout as follows:

Click the FlatButton to change the counter state of the application, incrementing the counter by 1. The first two lines of text show the latest value of the counter state. The FlatButton and the two texts are widgets in different parts.

  1. Rely on provider in the pubspec.yaml file:
Dependencies: Flutter: SDK: flutter provider: ^4.1.2Copy the code
  1. Import:import 'package:provider/provider.dart';

Provider

Provider is the most basic Provider widget type in the Provider package. It can provide a value to all of the included widgets, but does not update the widget when the value changes.

Add MyModel class, as the value to be supplied by the Provider, declare the counter value here, and put the method to change the value here. When the button is clicked, call incrementCounter() on MyModel object, delay 2 seconds and change counter:

class MyModel {
  
  MyModel({this.counter=0});

  int counter = 0;

  Future<void> incrementCounter() async {
    await Future.delayed(Duration(seconds: 2));
    counter++;
    print(counter); }}Copy the code

Wrap the Provider widget at the top of the Widget tree and provide the MyModel object to the Widget tree through the Provider. We then use two ways to get the provider-provided value, in Column:

  1. Use provider.of (context) to get a reference to the MyModel object.
  2. Then use the Consumer widget to get a reference to the MyModel object;
class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Provider(
      create: (_) => MyModel(),
      child: Scaffold(
        appBar: AppBar(
          title: Text('provider'), ), body: Column( children: <Widget>[ Builder( builder: MyModel _model = provider. Of <MyModel>(context);return Container(
                    margin: const EdgeInsets.only(top: 20),
                    width: MediaQuery.of(context).size.width,
                    padding: const EdgeInsets.all(20),
                    alignment: Alignment.center,
                    color: Colors.lightBlueAccent,
                    child: Text('${_model.counter}')); },), Consumer<MyModel>(// get provider builder: (context, model, child) {return Container(
                  margin: const EdgeInsets.only(top: 20),
                  width: MediaQuery.of(context).size.width,
                  padding: const EdgeInsets.all(20),
                  alignment: Alignment.center,
                  color: Colors.lightGreen,
                  child: Text(
                    '${model.counter}',),); },), Consumer<MyModel>(// get provider builder: (context, model, child) {returnFlatButton( color: Colors.tealAccent, onPressed:model.incrementCounter, child: Icon(Icons.add)); },),],),),); }}Copy the code

Click the FlatButton, and the Model calls incrementCounter(), incrementing the count by 1. However, the UI is not rebuilt because the Provider widget does not listen for changes to the values it provides.

Print out the change in the calculated value

ChangeNotifierProvider

Unlike the most basic Provider widget, the ChangeNotifierProvider listens for changes in the model objects it provides. When a value changes, it recreates all the consumers below and the places where provider.of (context) is used to listen and get the supplied value.

Change the Provider to ChangeNotifierProvider in the code. MyModel is mixed with ChangeNotifier (inheritance is the same). The notifyListeners() are then called after the counter change, so the ChangeNotifierProvider is notified and the Consumer and listening places rebuild their widgets.

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => MyModel(),
      child: Scaffold(
        appBar: AppBar(
          title: Text('provider'),
        ),
        body: Column(
          children: <Widget>[
            Builder(
              builder: (context) {
                MyModel _model = Provider.of<MyModel>(context);
                return Container(
                    margin: const EdgeInsets.only(top: 20),
                    width: MediaQuery.of(context).size.width,
                    padding: const EdgeInsets.all(20),
                    alignment: Alignment.center,
                    color: Colors.lightBlueAccent,
                    child: Text('${_model.counter}'));
              },
            ),
            Consumer<MyModel>(
              builder: (context, model, child) {
                return Container(
                  margin: const EdgeInsets.only(top: 20),
                  width: MediaQuery.of(context).size.width,
                  padding: const EdgeInsets.all(20),
                  alignment: Alignment.center,
                  color: Colors.lightGreen,
                  child: Text(
                    '${model.counter}',),); }, ), Consumer<MyModel>( builder: (context, model, child) {returnFlatButton( color: Colors.tealAccent, onPressed: model.incrementCounter, child: Icon(Icons.add)); },),],),),); } } class MyModel with ChangeNotifier{ // <--- MyModel MyModel({this.counter = 0}); int counter = 0; Future<void> incrementCounter() async { await Future.delayed(Duration(seconds: 2)); counter++;print(counter); notifyListeners(); }}Copy the code

Each click changes the value of the counter. What if the value of the first row is left unchanged? Text: MyModel _model = Provider. Of

(context,listen: false);

FutureProvider

FutureProvider is basically just a wrapper around a normal FutureBuilder. We need to give it some initial data to display in the UI, and we need to set the Future for it to supply. When the Future is complete, the FutureProvider notifies the Consumer to rebuild its widget.

In the code below, we use a MyModel with counter 0 to provide some initial data to the UI, and we add a Future function that returns a MyModel with counter 1 after 3 seconds. Like the base Provider, FutureProvider does not listen for any changes within the model itself. In the code below, counter is incremented by a button click event, but the UI is unaffected.

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FutureProvider(
      initialData: MyModel(counter: 0),
      create: (context) => someAsyncFunctionToGetMyModel(),
      child: Scaffold(
        appBar: AppBar(
          title: Text('provider'),
        ),
        body: Column(
          children: <Widget>[
            Builder(
              builder: (context) {
                MyModel _model = Provider.of<MyModel>(context, listen: false);
                return Container(
                    margin: const EdgeInsets.only(top: 20),
                    width: MediaQuery.of(context).size.width,
                    padding: const EdgeInsets.all(20),
                    alignment: Alignment.center,
                    color: Colors.lightBlueAccent,
                    child: Text('${_model.counter}'));
              },
            ),
            Consumer<MyModel>(
              builder: (context, model, child) {
                return Container(
                  margin: const EdgeInsets.only(top: 20),
                  width: MediaQuery.of(context).size.width,
                  padding: const EdgeInsets.all(20),
                  alignment: Alignment.center,
                  color: Colors.lightGreen,
                  child: Text(
                    '${model.counter}',),); }, ), Consumer<MyModel>( builder: (context, model, child) {returnFlatButton( color: Colors.tealAccent, onPressed: model.incrementCounter, child: Icon(Icons.add)); },),],),),); } Future<MyModel> someAsyncFunctionToGetMyModel() async { // <--- asyncfunction
    await Future.delayed(Duration(seconds: 3));
    return MyModel(counter: 1);
  }
}

class MyModel with ChangeNotifier {
  //                                               <--- MyModel
  MyModel({this.counter = 0});

  int counter = 0;

  Future<void> incrementCounter() async {
    await Future.delayed(Duration(seconds: 2));
    counter++;
    print(counter); notifyListeners(); }}Copy the code

The FutureProvider notifies the Consumer to rebuild the Future after it has been set. However, after the Future is complete, clicking the button will not update the UI.

FutureProvider works for pages that have not been refreshed or changed, just like FutureBuilder.

StreamProvider

StreamProvider is basically a wrapper around StreamBuilder, like FutureProvider above. The difference is that StreamProvider provides streams, while FutureProvider requires a Future.

The StreamProvider also does not listen for changes to the Model itself. It only listens for new events in the stream:

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StreamProvider(
      initialData: MyModel(counter: 0),
      create: (context) => getStreamOfMyModel(),
      child: Scaffold(
        appBar: AppBar(
          title: Text('provider'),
        ),
        body: Column(
          children: <Widget>[
            Builder(
              builder: (context) {
                MyModel _model = Provider.of<MyModel>(context, listen: false);
                return Container(
                    margin: const EdgeInsets.only(top: 20),
                    width: MediaQuery.of(context).size.width,
                    padding: const EdgeInsets.all(20),
                    alignment: Alignment.center,
                    color: Colors.lightBlueAccent,
                    child: Text('${_model.counter}'));
              },
            ),
            Consumer<MyModel>(
              builder: (context, model, child) {
                return Container(
                  margin: const EdgeInsets.only(top: 20),
                  width: MediaQuery.of(context).size.width,
                  padding: const EdgeInsets.all(20),
                  alignment: Alignment.center,
                  color: Colors.lightGreen,
                  child: Text(
                    '${model.counter}',),); }, ), Consumer<MyModel>( builder: (context, model, child) {returnFlatButton( color: Colors.tealAccent, onPressed: model.incrementCounter, child: Icon(Icons.add)); },),],),),); } Stream<MyModel>getStreamOfMyModel() {
    return Stream<MyModel>.periodic(
        Duration(seconds: 1), (x) => MyModel(counter: x)).take(10);
  }
}

class MyModel with ChangeNotifier {
  //                                               <--- MyModel
  MyModel({this.counter = 0});

  int counter = 0;

  Future<void> incrementCounter() async {
    await Future.delayed(Duration(seconds: 2));
    counter++;
    print(counter); notifyListeners(); }}Copy the code

The StreamProvider is set up with a stream that updates every second, and the count on the UI changes every second. But clicking a button doesn’t refresh the UI either. So you can also think of it as a StreamBuilder.

ValueListenableProvider

ValueListenableProvider is similar to ValueChange encapsulation. It acts the same as ChangeNotifierProvider. When a value changes, the ValueListenableProvider notifies the Consumer to rebuild. The Provider provides MyModel to the Consumer, and then adds ValueNotifier in MyModel to ValueListenableProvider:

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Provider<MyModel>(
      create: (context) => MyModel(),
      child: Consumer<MyModel>(
        builder: (context, myModel, child) {
          return ValueListenableProvider<int>.value(
            value: myModel.counter,
            child: Scaffold(
              appBar: AppBar(
                title: Text('provider'),
              ),
              body: Column(
                children: <Widget>[
                  Builder(
                    builder: (context) {
                      var count = Provider.of<int>(context);
                      return Container(
                          margin: const EdgeInsets.only(top: 20),
                          width: MediaQuery.of(context).size.width,
                          padding: const EdgeInsets.all(20),
                          alignment: Alignment.center,
                          color: Colors.lightBlueAccent,
                          child: Text('Currently: $count'));
                    },
                  ),
                  Consumer<int>(
                    builder: (context, value, child) {
                      return Container(
                        margin: const EdgeInsets.only(top: 20),
                        width: MediaQuery.of(context).size.width,
                        padding: const EdgeInsets.all(20),
                        alignment: Alignment.center,
                        color: Colors.lightGreen,
                        child: Text(
                          '$value',),); }, ), Consumer<MyModel>( builder: (context, model, child) {returnFlatButton( color: Colors.tealAccent, onPressed: model.incrementCounter, child: Icon(Icons.add)); },),],),),); })); } } class MyModel { ValueNotifier<int> counter = ValueNotifier(0); Future<void> incrementCounter() async { await Future.delayed(Duration(seconds: 2));print(counter.value++); counter.value = counter.value; }}Copy the code

ListenableProvider

ListenableProvider is the same as ChangeNotifierProvider, except that if Model is a complex Model, ChangeNotifierProvider will automatically call its _Disposer method whenever you need it, So use the ChangeNotifierProvider.

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListenableProvider<MyModel>(
      create: (context) => MyModel(),
      child: Scaffold(
        appBar: AppBar(
          title: Text('provider'),
        ),
        body: Column(
          children: <Widget>[
            Builder(
              builder: (context) {
                MyModel modol = Provider.of<MyModel>(context);
                return Container(
                    margin: const EdgeInsets.only(top: 20),
                    width: MediaQuery.of(context).size.width,
                    padding: const EdgeInsets.all(20),
                    alignment: Alignment.center,
                    color: Colors.lightBlueAccent,
                    child: Text('Currently: ${modol.counter}'));
              },
            ),
            Consumer<MyModel>(
              builder: (context, model, child) {
                return Container(
                  margin: const EdgeInsets.only(top: 20),
                  width: MediaQuery.of(context).size.width,
                  padding: const EdgeInsets.all(20),
                  alignment: Alignment.center,
                  color: Colors.lightGreen,
                  child: Text(
                    '${model.counter}',),); }, ), Consumer<MyModel>( builder: (context, model, child) {returnFlatButton( color: Colors.tealAccent, onPressed: model.incrementCounter, child: Icon(Icons.add)); },),],),),); } } class MyModel with ChangeNotifier { int counter = 0; Future<void> incrementCounter() async { await Future.delayed(Duration(seconds: 2)); counter++; notifyListeners();print(counter); }}Copy the code

MultiProvider

The examples above all use only one Model object. If you need to provide a second type of Model object, you can nest a Provider. However, the indentation of the nesting puzzle is not readable. It’s pretty neat to use MultiProvider,

Let’s change the counter above, usually the home page will have a banner and a list. We use the counter above to simulate the banner and the counter below to simulate the list:

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider<BannerModel>(create: (context) => BannerModel()),
        ChangeNotifierProvider<ListModel>(create: (context) => ListModel()),
      ],
      child: Scaffold(
        appBar: AppBar(
          title: Text('provider'),
        ),
        body: Column(
          children: <Widget>[
            Builder(
              builder: (context) {
                BannerModel modol = Provider.of<BannerModel>(context);
                return Container(
                    margin: const EdgeInsets.only(top: 20),
                    width: MediaQuery.of(context).size.width,
                    padding: const EdgeInsets.all(20),
                    alignment: Alignment.center,
                    color: Colors.lightBlueAccent,
                    child: Text(${modol.counter}));
              },
            ),
            Consumer<ListModel>(
              builder: (context, model, child) {
                return Container(
                  margin: const EdgeInsets.only(top: 20),
                  width: MediaQuery.of(context).size.width,
                  padding: const EdgeInsets.all(20),
                  alignment: Alignment.center,
                  color: Colors.lightGreen,
                  child: Text(
                    ${model. Counter},),); }, ), Consumer<BannerModel>( builder: (context, model, child) {return FlatButton(
                    color: Colors.tealAccent,
                    onPressed: model.getBanner,
                    child: Text("For the banner"));
              },
            ),
            Consumer<ListModel>(
              builder: (context, model, child) {
                return FlatButton(
                    color: Colors.tealAccent,
                    onPressed: model.getList,
                    child: Text("Get the list")); },),],),),); } } class BannerModel with ChangeNotifier { int counter = 0; Future<void> getBanner() async { await Future.delayed(Duration(seconds: 2)); counter++; notifyListeners();print(counter);
  }
}

class ListModel with ChangeNotifier {
  int counter = 0;

  Future<void> getList() async {
    await Future.delayed(Duration(seconds: 2));
    counter++;
    notifyListeners();
    print(counter); }}Copy the code

Press the Banner button to get the value for the banner individually and update the Consumer for the banner. Same thing with lists.

ProxyProvider

If you want to provide two models, but one Model depends on the other, you can use ProxyProvider in this case. A ProxyProvider takes A value from one Provider and injects it into another Provider,

To change the above, such as the upload image function, you need to submit the image to the image server, and then send the link to the background server:

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider<PicModel>(create: (context) => PicModel()),
        ProxyProvider<PicModel, SubmitModel>(
          update: (context, myModel, anotherModel) => SubmitModel(myModel),
        ),
      ],
      child: Scaffold(
        appBar: AppBar(
          title: Text('provider'),
        ),
        body: Column(
          children: <Widget>[
            Builder(
              builder: (context) {
                PicModel modol = Provider.of<PicModel>(context);
                return Container(
                    margin: const EdgeInsets.only(top: 20),
                    width: MediaQuery.of(context).size.width,
                    padding: const EdgeInsets.all(20),
                    alignment: Alignment.center,
                    color: Colors.lightBlueAccent,
                    child: Text('Submit image: ${modol.counter}'));
              },
            ),

            Consumer<PicModel>(
              builder: (context, model, child) {
                return FlatButton(
                    color: Colors.tealAccent,
                    onPressed: model.upLoadPic,
                    child: Text("Submit picture"));
              },
            ),
            Consumer<SubmitModel>(
              builder: (context, model, child) {
                return FlatButton(
                    color: Colors.tealAccent,
                    onPressed: model.subMit,
                    child: Text("Submit")); },),],),),); } } class PicModel with ChangeNotifier { int counter = 0; Future<void> upLoadPic() async { await Future.delayed(Duration(seconds: 2)); counter++; notifyListeners();print(counter); } } class SubmitModel { PicModel _model; SubmitModel(this._model); Future<void> subMit() async { await _model.upLoadPic(); }}Copy the code

Encapsulate providers based on MVVM mode

I believe you have understood the process of provider, as shown below:

The use of Provider has been demonstrated above. In development, we need Model to act as ViewModel to handle business logic, but it is troublesome to write boilerplate code every time, so we need to package it and make it easy to use.

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<LoginViewModel>(
      create: (BuildContext context) {
        return LoginViewModel(loginServive: LoginServive());
      },
      child: Scaffold(
        appBar: AppBar(
          title: Text('provider'),
        ),
        body: Column(
          children: <Widget>[
            Consumer<LoginViewModel>(
              builder: (context, model, child) {
                return Text(model.info);
              },
            ),
            Consumer<LoginViewModel>(
              builder: (context, model, child) {
                return FlatButton(
                    color: Colors.tealAccent,
                    onPressed: () => model.login("pwd"),
                    child: Text("Login")); },),],),),); } } /// viewModel class LoginViewModel extends ChangeNotifier { LoginServive _loginServive; String info ='Please log in';

  LoginViewModel({@required LoginServive loginServive})
      : _loginServive = loginServive;

  Future<String> login(String pwd) async {
    info = await _loginServive.login(pwd);
    notifyListeners();
  }
}

/// api
class LoginServive {
  static const String Login_path = 'xxxxxx';

  Future<String> login(String pwd) async {
    return new Future.delayed(const Duration(seconds: 1), () => "Login successful"); }}Copy the code

This page writing method, basically every page, let’s start the encapsulation step by step.

  1. In general, loading a page will display a loading state, and then loading successful display data, failure display page, so enumerate a page state:
enum ViewState { Loading, Success,Failure }
Copy the code
  1. The ViewModel updates the UI whenever the page state property changes, often calling notifyListeners to move this step into BaseModel:
class BaseModel extends ChangeNotifier {
  ViewState _state = ViewState.Loading;

  ViewState get state => _state;

  void setState(ViewState viewState) { _state = viewState; notifyListeners(); }}Copy the code
  1. We know that the UI needs the ChangeNotifierProvider to provide the Model and update the UI with Consumer. So we built it into BaseView as well:
class BaseWidget<T extends ChangeNotifier> extends StatefulWidget {
  final Widget Function(BuildContext context, T value, Widget child) builder;
  final T model;
  final Widget child;

  BaseWidget({Key key, this.model, this.builder, this.child}) : super(key: key);

  @override
  State<StatefulWidget> createState() => _BaseWidgetState();

}

class _BaseWidgetState<T extends ChangeNotifier> extends State<BaseWidget<T>> {

  T model;

  @override
  void initState() {
    model = widget.model;
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    returnChangeNotifierProvider<T>.value( value: model, child: Consumer<T>( builder: widget.builder, child: widget.child, ), ); }}Copy the code
  1. Sometimes our page data is only partially updated. The Consumer child attribute is the UI that does not need to be rebuilt when the model changes, so we put the UI that needs to be updated in the Builder and the UI that does not need to be updated in the Child:
Consumer<LoginViewModel>( // Pass the login header as a prebuilt-static child child: LoginHeader(controller: _controller), builder: (context, model, child) => Scaffold( ... Body: Column (children: [// unupdated part of child,...] )Copy the code
  1. Most of the time, we are already in a page and need to fetch data, so we move this operation into the base class as well:
class BaseWidget<T extends ChangeNotifier> extends StatefulWidget { final Function(T) onModelReady; . BaseWidget({ ... this.onModelReady, }); . }... @override voidinitState() {
  model = widget.model;

  if(widget.onModelReady ! = null) { widget.onModelReady(model); } super.initState(); }Copy the code

Now we complete the login page with the wrapped base class:

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BaseWidget<LoginViewModel>(
      model: LoginViewModel(loginServive: LoginServive()),
      builder: (context, model, child) => Scaffold(
        appBar: AppBar(
          title: Text('provider'),
        ),
        body: Column(
          children: <Widget>[
            model.state == ViewState.Loading
                ? Center(
                    child: CircularProgressIndicator(),
                  )
                : Text(model.info),
            FlatButton(
                color: Colors.tealAccent,
                onPressed: () => model.login("pwd"),
                child: Text("Login"() [() [() [() [() } } /// viewModel class LoginViewModel extends BaseModel { LoginServive _loginServive; String info ='Please log in';

  LoginViewModel({@required LoginServive loginServive})
      : _loginServive = loginServive;

  Future<String> login(String pwd) async {
    setState(ViewState.Loading);
    info = await _loginServive.login(pwd);
    setState(ViewState.Success);
  }
}

/// api
class LoginServive {
  static const String Login_path = 'xxxxxx';

  Future<String> login(String pwd) async {
    return new Future.delayed(const Duration(seconds: 1), () => "Login successful");
  }
}

enum ViewState { Loading, Success, Failure, None }

class BaseModel extends ChangeNotifier {
  ViewState _state = ViewState.None;

  ViewState get state => _state;

  void setState(ViewState viewState) {
    _state = viewState;
    notifyListeners();
  }
}

class BaseWidget<T extends ChangeNotifier> extends StatefulWidget {
  final Widget Function(BuildContext context, T model, Widget child) builder;
  final T model;
  final Widget child;
  final Function(T) onModelReady;

  BaseWidget({
    Key key,
    this.builder,
    this.model,
    this.child,
    this.onModelReady,
  }) : super(key: key);

  _BaseWidgetState<T> createState() => _BaseWidgetState<T>();
}

class _BaseWidgetState<T extends ChangeNotifier> extends State<BaseWidget<T>> {
  T model;

  @override
  void initState() {
    model = widget.model;

    if(widget.onModelReady ! = null) { widget.onModelReady(model); } super.initState(); } @override Widget build(BuildContext context) {returnChangeNotifierProvider<T>( create: (BuildContext context) => model, child: Consumer<T>( builder: widget.builder, child: widget.child, ), ); }}Copy the code