Changing the way, for the good !!!

Which is the easiest way to manage my app state? Hmm…

History of State Management in Flutter…..

In its early stages, Flutter introduced….

  1. setState()
setState(() {    _counter++;    //YOUR LOGIC.......   });

2. In year 2018, Business Logic Component (BLoC) was announced at Google I/O ’18. The BLoC pattern uses Reactive Programming to handle the flow of data within an app.

BLoC….
BLoC….

BLoC has two simple components: Sinks and Streams, both of which are provided by a StreamController.

Sinks In, Streams Out…..!!!

Programmers were still getting used to it until……..


3. In this year, Provider was introduced

Provider is a state management package built by the community, not by Google. 


Why Provider ?…

This is one of the hot questions out there to all the Flutter developers..

Lets see first, the things needed for BLoC…

You should be aware about the Streams, Sinks, StreamControllers…What is their correct use, how to expose and dispose them….

You need to have some implementation of BlocProvider (Dont confuse this with Provider)….

Sample BLoC class:

class YourBloc {
var yourVar;
final yourVarController = StreamController<yourType>();
Stream<yourType> get yourVarStream => counterController.stream;
StreamSink<yourType> get yourVarSink => counterController.sink;

yourMethod() {
// some logic staff;
yourVar = yourNewValue;
yourVarSink.add(yourVar);
}
dispose() {
yourVarController.close();
}
}

These concepts are important, no doubt,……but if you are new to Flutter and want to quickly get over State Management, you may hesitate to learn these things at first…..

Note : This is only based on my personal opinion and does not aim to demotivate BLoC lovers…:)

Sample Provider class: (Details about this class are below)

class AppData with ChangeNotifier {
String wonder;
  AppData({
this.wonder = firstWonderForLoading,
});
  void updateWonderToShow(String _country) {
wonder = _country;
notifyListeners();
}
}

Advantages of Provider….

In my opinion,

  1. There is no additional boiler plates needed to get a Provider working
  2. Availability of the parameter initialData, you have control over the data you expect….
  3. You can choose to dispose the widget or let the Provider take care of that…:)
  4. You can decide either to listen to data changes or not…(See below)
  5. Builder method of Provider, is only called once during the entire lifecycle of the instance of the Provider….
  6. You can decide whether a Provider can be a complex or a simple 🙂
  7. No doubt, a Provider is easy to read…..

Why is Provider Different?

Hmm, I would describe this as :

Provider helps us in getting the updated model…….along with the power of making changes in that model with the help of :

  • ListenableProvider
  • ChangeNotifierProvider
  • ValueListenableProvider
  • StreamProvider
  • FutureProvider

If you are here with me till now, lets deep dive into Providers…..


Begin….

Provider is a state management package built by the community and accepted by Google….

Installing provider,

Put this dependency in your pubspec.yaml.

provider: ^3.0.0+1 // as of now
App with Provider….
App with Provider….

Part One of App…

For listening to data continuously, we had StreamBuilder in Flutter…

StreamBuilder(
stream: //YOUR STREAM,
builder: (BuildContext context, AsyncSnapshot snapshot){
return //YOUR CHILD;
})

Now with Provider, we have another widget, StreamProvider

StreamProvider.value(
value: // YOUR STREAM,
child: // YOUR CHILD,
),

This listens to the value and exposes it to all its descendants……


Top half of the screenshot displays the Wonder of the World….

How to do ?

We create a StreamProvider like…

StreamProvider<LocationModelNormal>.value(
initialData: LocationModelNormal.initialData(), value:locationStreamInstance.specificLocation(_wonderToShow.wonder),
child: LocationStreamProviderWidget(),
)

LocationModelNormal is the model class, which we are providing to the Stream Provider….

LocationStreamProviderWidget is the child widget.

locationStreamInstance.specificLocation is the stream to the StreamProvider……

///For documents.....
Stream<LocationModelNormal> specificLocation(String docId) {
final _listModel = userDataStream(
documentId: docId,
).map((list) => LocationModelNormal.fromMap(list.data));

return _listModel;
}

This renders the Wonder which is specified by the StreamProvider…!!!

Part Two of the App…

In the bottom portion of the app, we have given user the power to view the preferred wonder ………

Provider - and its Types
Provider — and its Types

Now, we want to render the StreamProvider with the preferred wonder…

How to do ? 

  1. Wrap your parent widget of the app with ChangeNotifierProvider
return Scaffold(
appBar: AppBar(
title: Text('$data'),
),
body: ChangeNotifierProvider<AppData>(
builder: (context) => AppData(),
child: WondersBody(),
),
);

ChangeNotifierProvider is listening to any changes in the AppData model class…

How does this model look like ??

class AppData with ChangeNotifier {
String wonder;

AppData({
this.wonder = firstWonderForLoading,
});

void updateWonderToShow(String _country) {
wonder = _country;
notifyListeners();
}
}

Normal class but with a mixinChangeNotifier, which adds listening capability…

2. As user selects the wonder, simply call the updateWonderToShow function and pass the respective user selected wonder…


Accessing data via Provider…

There are 2 ways to do so :

  1. Using Consumer Widget…

Wrap the child which you want to show with a Consumer Widget…For instance, 

Consumer<String>(
builder: (context, value, child) => Text(value),
),

You can pass the model also as 

Provider<WonderOptions>.value(
value: WonderOptions(),
child: Flexible(
child: Consumer<WonderOptions>(
builder: (context,model, child) => WonderNames(),
),
),
),

Consumer widget will rebuild as the data changes…

2. Using Provider.of<T>(…)

Detailed description,

T of<T>(BuildContext context, {bool listen = true})

What this means :

Obtains the nearest [Provider<T>] up its widget tree and returns its value.
If [listen] is true (default), later value changes will trigger a new [State.build] to widgets, and [State.didChangeDependencies] for [StatefulWidget].

You can use the following to access the data

final _wonderToShow = Provider.of<AppData>(context, listen: true);

But, by default the listen parameter is set to true, hence……


In our case, we can access the data as

final _wonderToShow = Provider.of<AppData>(context);

How to pass the data now, simple 🙂

Flexible(
flex: 2,
child: StreamProvider<LocationModelNormal>.value(
initialData: LocationModelNormal.initialData(),
value:
locationStreamInstance.specificLocation(_wonderToShow.wonder),
child: LocationStreamProviderWidget(),
),
),

As you can see, _wonderToShow.wonder gives us the current wonder and the respective details are shown accordingly…


Till now, we saw ChangeNotifierProvider and StreamProvider….

Use of Provider…
Use of Provider…

Lets see a use case of Provider itself…

In this app, particularly the wonder selection (see above image)….data here mostly won’t change, for instance

  1. There are only 7 buttons….
  2. Each button has a specific name….

In terms of Firestore think of when a user logs in….

The userid, email, etc won’t change right!!!…You can model this type of data and pass down via Provider…

Coming back to our app, we can provide this type of data via Provider to a widget……Lets see how….

Provider<WonderOptions>.value(
value: WonderOptions(),
child: Flexible(
child: WonderNames(),
),
),

Here, you can see we have wrapped our widget (WonderNames) with Provider which provides the model/data of WonderOptions…

For accessing the data inside our widget, we will use the above mentioned approach…

Provider.of<WonderOptions>(context)

How does WonderOptions class look like?

const int wonderConstOptions = 7;

const List<String> wonderNamesList = [
firstWonder,
secondWonder,
thirdWonder,
fourthWonder,
fifthWonder,
sixthWonder,
seventhWonder,
];
class WonderOptions {
_CurrentWonderOptions _currentWonderOptions =
_CurrentWonderOptions(wonderConstOptions);

_CurrentWonderNames _currentWonderNames = _CurrentWonderNames(wonderNamesList);

int get wonderCount => _currentWonderOptions.wonderCount;

set wonderCount(int newValue) {
if (newValue == _currentWonderOptions.wonderCount) return;
_currentWonderOptions = _CurrentWonderOptions(newValue);
}

List<String> get wonderNames => _currentWonderNames.wonderNames;

set wonderNames(List<String> newValue) {
if (newValue == _currentWonderNames.wonderNames) return;
_currentWonderNames = _CurrentWonderNames(newValue);
}

WonderOptions() {
_fetchWonders();
_generateWondersList();
}

void _fetchWonders() {
_currentWonderOptions = _CurrentWonderOptions(wonderCount);
}

void _generateWondersList() {
_currentWonderNames = _CurrentWonderNames(wonderNames);
}
}

class _CurrentWonderOptions {
final int wonderCount;

const _CurrentWonderOptions(
this.wonderCount,
);
}

class _CurrentWonderNames {
final List<String> wonderNames;

const _CurrentWonderNames(
this.wonderNames,
);
}

Showing the selected Wonder….

Currently the user selects the wonder to see and the app displays accordingly..

Now, we want to show the name of current wonder selected as below:

Use of ValueListenableProvider…
Use of ValueListenableProvider…

Lets use the ValueListenableProvider….

As the documentation says, 

Listen to a ValueListenable and only expose ValueListenable.value.

Any change in this value will stop listening to the previous value and listen the new one.

What this means, 

ValueListenableProvider<String>.value(
value: _currentWonder,
child: Consumer<String>(
builder: (context, value, _) => Text(value),
),
),
),

The value parameter inside the ValueListenableProvider expects a variable of type ValueNotifier…

So, the variable _currentWonder is :

final _wonderToShow = Provider.of<AppData>(context);

final _currentWonder = ValueNotifier(_wonderToShow.wonder);

If the user selects a different wonder, this value is updated and our ValueListenableProvider notifies to the child widget…

We are using the Consumer widget (from provider package) to display the wonder selected by the user….

Summary…

Finally, we want to show the summary of the wonder, 

for instance, wonder name and the total number of wonders….

Use of MultiProvider…
Use of MultiProvider…

Now, our total number of wonders are inside one Provider and the current wonder being displayed inside another Provider…..

Hmm, time to use MultiProvider….

As per the documentation,

When injecting many values in big applications, Provider can rapidly become pretty nested, hence we have MultiProvider….

MultiProvider(  providers: [    Provider<Foo>.value(value: foo),    Provider<Bar>.value(value: bar),  ],  child: someWidget,)

How to use in our app,….

MultiProvider(
providers: [
Provider<WonderOptions>.value(value: WonderOptions()),
StreamProvider<LocationModelNormal>.value(
initialData: LocationModelNormal.initialData(),
value: locationStreamInstance
.specificLocation(_wonderToShow.wonder),
),
],
child: SummaryWidget(),
),

Here, we are using 2 providers namely :

  1. Provider of WonderOptions
  2. StreamProvider of LocationModeNormal

These can be accessed by the child widget, in our case, SummaryWidget

Lets see, 

final _totalWonders = Provider.of<WonderOptions>(context);

This gets us the WonderOptions….

final _wonderDetail = Provider.of<LocationModelNormal>(context);

This gets us the LocationModeNormal…

Access the desired parameters from these models to show in the UI…..:)

Leave a Reply

Your email address will not be published. Required fields are marked *