Guide to advanced features of Provider State Management

Deborah Oluwabunmi Joseph
6 min readFeb 27, 2024

--

Are you new to Flutter, and would you like to learn about the Provider state management? Check out my previous article on provider state management below👇

Are you interested in diving more into the Provider state management? If yes, then this article is for you. In this article, you will learn how to use the Provider package for Dependency Injection, FutureProvider, StreamProvider, ProxyProvider, ListenableProvider and the Selector. Let’s get started.

Inversion of Control and Dependency injection using Provider

This is the design pattern principle in which a software component is designed to receive its dependencies from an external source rather than creating them itself. It means giving the control of an object to another container or framework. Inversion of control in flutter could be achieved through a Service Locator or Dependency Injection.

Dependency injection

This is the principle used in creating a dependent object outside of a class and providing the object to a class through another. It aims to separate the concerns of constructing objects, and using them, leading to loosely coupled programs.
In Flutter application, we often have a class dependent on the function or methods of another, and the best way of writing a maintainable and testable code is to make the class independent of its dependencies. Dependencies could be network service, database service, location service, REST APIs, shared preferences, etc.
Furthermore, we can implement dependency injection by passing instances of dependencies into the class that needs them using the following approaches.

  • Constructor injection: we pass the dependencies of a class through its constructor.
class DataRepository {
final DataService _dataService;

DataRepository(this._dataService);

Future<String> fetchData() => _dataService.fetchData();
}
  • Method Injection: In this approach, we pass the dependencies of a class through a method.
class DataRepository {
DataService _dataService;

void setDataService(DataService dataService) {
_dataService = dataService;
}

Future<String> fetchData() => _dataService.fetchData();
}
  • Field Injection: Field injection is a less common form of dependency injection where we inject a dependency directly into a field of a class.
class DataRepository {
DataService dataService;

Future<String> fetchData() => dataService.fetchData();
}

Dependency Injection(DI) using Provider

Now that we understand Dependency Injection and how it could be achieved in Flutter, let’s discuss on Dependency Injection using Provider.

The provider package provides a mixture of dependency injection and state management built with widgets for widgets.
Firstly, in Provider, DI is achieved by injecting values and dependencies to the root of the Flutter application through the MultiProvider or Provider class. For instance,

    void main() {
runApp(
Provider<DataService>(
create: (_) => DataServiceImpl(),
child: MyApp(),
),
);
}
void main() {
runApp(
MultiProvider(
providers: [
Provider<Something>(create: (_) => Something()),
Provider<SomethingElse>(create: (_) => SomethingElse()),
Provider<AnotherThing>(create: (_) => AnotherThing()),
],
child: someWidget,
));}

Secondly, we can use the BuildContext, a reference to the location of a widget within the widget tree, to access dependencies that have been provided higher up in the widget tree.

For example, the Provider.of(context) method is used to access a dependency of type DataService that has been provided higher up in the widget tree.

class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final dataService = Provider.of<DataService>(context);
return Text('Data: ${dataService.fetchData()}');
}
}

FutureProvider

The FutureProvider is a wrapper around the FutureBuilder widget in Flutter. It is a Provider that allows you to listen to a future value (event) and it causes a rebuild of the widget when the future event is completed.

The FutureProvider takes some initial data (value/object), which is being shown in the UI while waiting for the future value. Then it rebuilds the widgets when the future value is available. This is done once and it stops listening to the provider for changes.

For continuous rebuild after each update is fetched, use StreamProvider which is a wrapper around StreamBuilder widget in flutter. Like FutureProvider it listens to a stream and exposes the value to its descendants.

FutureProvider(
builder: (_) async {
final json = await // TODO load json from something;
return MyConfig.fromJson(json);
}
)
StreamProvider<int>.value(  
initialData: 0, value: model.secondCountStream,
child: SecondWidget(),),

ProxyProvider

ProxyProvider is a provider that combines multiple values from other providers into a new object and sends the result to Provider. The new object will then be updated whenever one of the provider dependent on it changes.

Basically, it injects the changed value into another provider. ProxyProvider can be used when there is more than one provider used and the value of one provider depends on another provider at that time . The other provider updates its value whenever one of the providers it depends on also updates.

In the example below, the ProxyProvider is used to build translations which get its value from the Counter Provider.

Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => Counter()),
ProxyProvider<Counter, Translations>(
update: (_, counter, __) => Translations(counter.value),
),
],
child: Foo(),
);
}

class Translations {
const Translations(this._value);
final int _value;
String get title => 'You clicked $_value times';
}

The ProxyProvider comes under multiple variants such as ProxyProvider vs ProxyProvider2 vs ProxyProvider3, ..., The digit after the class name signifies the number of other providers it depends on.

ListenableProvider

ListenableProvider is the specific provider used for listenable objects. It listens to a Listenable, expose it to its descendants and rebuilds dependents whenever the listener emits an event.

It is interesting to note that the ChangeNotifierProvider is a subclass of ListenableProvider made for ChangeNotifier.

An example of a listenable objects in Flutter is the AnimatedWidget below.

class AnimatedLogo extends AnimatedWidget {
const AnimatedLogo({super.key, required Animation<double> animation})
: super(listenable: animation);

// Make the Tweens static because they don't change.
static final _opacityTween = Tween<double>(begin: 0.1, end: 1);
static final _sizeTween = Tween<double>(begin: 0, end: 300);

@override
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Center(
child: Opacity(
opacity: _opacityTween.evaluate(animation),
child: Container(
margin: const EdgeInsets.symmetric(vertical: 10),
height: _sizeTween.evaluate(animation),
width: _sizeTween.evaluate(animation),
child: const FlutterLogo(),
),
),
);
}
}

Check the link for more information and the documentation on ListenableProvider.

Selector

The selector is a class in the Provider package equivalent to the consumer class. It filters updates by selecting a limited amount of values and prevents rebuilds if they don’t change.

It allows the selection of the specific value in a Provider to listen to and rebuild the widget when the value changes. This is done by obtaining a value using Provider.of, and it passes that value to the selector. The selector compares the previous value to the new value and returns an object that contains only the information needed for the builder to complete. If the selected value is changed, then the builder method is triggered.


Container(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Selector<NumberProvider, int>(
selector: (_, provider) => provider.number1,
builder: (context, number1, child) {
print('Build num1');
return Container(
color: Colors.red,
padding: EdgeInsets.all(10),
child: Text('$number1'),
);
}),
Selector<NumberProvider, int>(
selector: (_, provider) => provider.number2,
builder: (context, number2, child) {
print('Build num2');
return Container(
color: Colors.green,
padding: EdgeInsets.all(10),
child: Text('$number2'),
);
}),
],
),
),

NOTE: The selected value must be immutable, or otherwise Selector may think nothing changed and not call builder again.

--

--