Topics In Demand
Notification
New

No notification found.

The Differences Between Provider Pattern and Bloc Pattern
The Differences Between Provider Pattern and Bloc Pattern

February 17, 2022

7247

0

There can be different approaches to programming to perform the same functionality. It depends on the programmer’s expertise and efficiency of the approach to get the functionality done in the best possible way. In Flutter, there are different programming architectures, or we can say state management techniques.

They are setState(), Bloc patternProvider pattern, and Scoped Model pattern. In this article, we are going to look at the Provider and Bloc patterns to see how we can create a login screen with all validations that are required in a generic login screen.

What is state management?

When a frontend application is built, we need to manage the values over the screen based on the event performed over the UI elements. For example, a text field has a value, which might change on a button click.

So, we need to manage that value on change detection. In other words, we need to manage the state of that text field. There are different types of state management techniques in Flutter.

We cannot compare one state management technique with the other. It depends upon the programmer’s knowledge that he can analyze to choose which pattern is best to complete his use case in the best possible way.

One of the state management approaches in Flutter is the Provider pattern.

We will try to see the difference when we create a login screen using Bloc pattern and Provider pattern.

Provider pattern

How do you use the Provider pattern?

There are three things to be mindful of when using the provider pattern:

  1. ChangeNotifier: ChangeNotifier can be understood as a class extended by other classes to provide notification if there is any change in the data of the class.

  2. ChangeNotifierProvider: ChangeNotifierProvider can be understood as a parent widget holding the reference of ChangeNotifier, which is responsible for rendering within the UI the changes that happened in ViewModel class data.

  3. Consumer: Consumer can be understood as a widget holding the reference of ViewModel Class that continually listens for any changes and rebuilds the child widget over which it has been wrapped.

We will see an example by implementing validation in a Login Screen.

Let’s start from the beginning:

We need to add provider package in our project’s .yaml file. At the time of writing, we are using provider version 4.0.5:

 

provider: ^4.0.5In main.dart file:void main() => runApp(MyApp());
 
 class MyApp extends StatelessWidget {
 
 @override
 Widget build(BuildContext context) {
 return MaterialApp(
 title: ‘Flutter Demo’,
 theme: ThemeData(
 
 primarySwatch: Colors.blue,
 ),
 home: LoginScreenWithProvider(),
 );
 }
 }

Now we have one Stateless widget class and one Provider (Or viewModel class) class extending ChangeNotifier, which will have all the logic written to see the changes on screen.

Provider pattern has two helper classes for validation

1. Validation.dart

class ValidationModel {
 final String value;
 final String error;
 
 ValidationModel(this.value, this.error);
 }
 
 class ValidatorType {
 
 static final RegExp email = RegExp(
 r’^(([^<>()[\]\\.,;:\s@\”]+(\.[^<>()[\]\\.,;:\s@\”]+)*)|(\”.+\”))@((\[[0–9]{1,3}\.[0–9]{1,3}\.[0–9]{1,3}\.[0–9]{1,3}\])|(([a-zA-Z\-0–9]+\.)+[a-zA-Z]{2,}))$’);
 static final RegExp password = RegExp(r’^(?=.*)(.){8,15}$’);
 }

When creating a mobile application, it is important to maintain the interaction between user and application. When we call the API, it takes some time to get the response. To fill that gap, we need to show some loading indicators so that users will be connected to the app.

To do that, we have an abstract class for showing and hiding the loading indicator.

2. Loader.dart

enum ViewState {Idle, Busy}abstract class LoaderState {ViewState _state = ViewState.Idle;ViewState get state => _state;void setState(ViewState viewState);}

Building a login screen using Provider Pattern

What are we trying to achieve here?

We are trying to write the code in which as soon as a user starts typing in the text field, the text field keeps showing the error message to the user as the email or password he is typing is a valid email or password.

Based on that, if email and password match, the regex will allow the UI button to be enabled, making it clickable.

login_screen.dartclass LoginScreenWithProvider extends StatelessWidget {
 
 @override
 Widget build(BuildContext context) {
 
 
 return ChangeNotifierProvider<LoginProvider>(
 
 create: (context) => LoginProvider(),
 child: Scaffold(
 appBar: AppBar(title: Text(‘Login’)),
 body: Padding(
 padding: const EdgeInsets.all(8.0),
 
 child: ListView(
 children: [
 
 // Email Field
 Consumer<LoginProvider>(builder: (context, provider, child) {
 return TextField(
 decoration: InputDecoration(
 labelText: “Email”,
 errorText: provider.email.error,
 ),
 onChanged: (String value) {
 print(‘provider.email.error ${provider.email.error});
 provider.changeEmail(value);
 },
 );
 }),
 
 
 ],
 ),
 )),
 );
 }
 }

Note: To do this, we will have to keep rebuilding the widget (in our case, it is a text field) but not the whole widget tree to comply with programming best practices.

You need to wrap the Scaffold widget inside the ChangeNotifierProvider widget so that we have the instance of Provider object to listen to the changes.

Most importantly, the widget you want to rebuild should be the child of the Consumer widget because it listens for changes in data should any event happen so that it can rebuild and update the widget on changes in data.

The best use of the Consumer widget is to use it as deep as possible within the widget tree.

The best practise here would be to rebuild the desired widget in the widget tree which needs the update. The best way to do that is to use the Consumer as deeply as possible.

Now we check for change notifier or provider class:

class LoginProvider with ChangeNotifier {
 
 ValidationModel _email = ValidationModel(null,null);
 
 //Getters
 ValidationModel get email => _email;
 
 bool get isValid {
 if (_email.value != null){
 return true;
 } else {
 return false;
 }
 }
 
 //Setters
 void changeEmail(String value){
 
 if (ValidatorType.email.hasMatch(value)){
 _email=ValidationModel(value,null);
 
 } else if (value.isEmpty){
 _email=ValidationModel(null,null);
 
 } else {
 _email=ValidationModel(null, “Enter a valid email”);
 
 }
 notifyListeners();
 
 }
 
 @override
 void dispose() {
 super.dispose();
 }
 
 }

As of now, for the sake of simplicity, we have picked the Email text field only to check how it works. On every keyboard hit, we call the changeEmail method to check if the email typed matches the regex or not. NotifyListeners() will keep notifying the Consumer widget to rebuild the text field widget, and errorText will be updated accordingly.

The password field also works in the same way:

ListView(
 children: [
 
 // Email Field
 Consumer<LoginProvider>(builder: (context, provider, child) {
 return TextField(
 decoration: InputDecoration(
 labelText: “Email”,
 errorText: provider.email.error,
 ),
 onChanged: (String value) {
 provider.changeEmail(value);
 },
 );
 }),
 
 // Password Field
 Consumer<LoginProvider>(builder: (context, provider, child) {
 return TextField(
 decoration: InputDecoration(
 labelText: “Password”,
 errorText: provider.password.error,
 ),
 onChanged: (String value) {
 provider.changePassword(value);
 },
 );
 }),
 
 //Update Indicator
 Consumer<LoginProvider>(builder: (context, provider, child) {
 return Container(
 height: 150,
 child: Center(
 child: provider.state == ViewState.Idle
 ? Container()
 : CircularProgressIndicator(),
 ),
 );
 }),
 
 //Submit button will be enabled on correct format of email and password
 Consumer<LoginProvider>(builder: (context, provider, child) {
 return RaisedButton(
 color: Colors.blue,
 disabledColor: Colors.grey,
 child: Text(
 ‘Submit’,
 style: TextStyle(color: Colors.white),
 ),
 onPressed: (!provider.isValid)
 ? null
 : () {
 provider
 .submitLogin()
 .then((LoginResponse response) {
 print(‘response: ${response.token} );
 
 Navigator.push(
 context,
 MaterialPageRoute(
 builder: (context) => DashboardScreen()));
 });
 },
 );
 })
 ],
 )

The login provider class will be:

login_provider.dartclass LoginProvider with ChangeNotifier implements LoaderState {
 
 LoginProvider(){
 setState(ViewState.Idle);
 }
 
 
 ValidationModel _email = ValidationModel(null,null);
 ValidationModel _password = ValidationModel(null,null);
 
 //Getters
 ValidationModel get email => _email;
 ValidationModel get password => _password;
 
 
 bool get isValid {
 if (_password.value != null && _email.value != null){
 return true;
 } else {
 return false;
 }
 }
 
 //Setters
 void changeEmail(String value){
 
 if (ValidatorType.email.hasMatch(value)){
 _email=ValidationModel(value,null);
 
 } else if (value.isEmpty){
 _email=ValidationModel(null,null);
 
 } else {
 _email=ValidationModel(null, “Enter a valid email”);
 
 }
 notifyListeners();
 
 }
 
 void changePassword(String value){
 
 if (ValidatorType.password.hasMatch(value)){
 _password=ValidationModel(value,null);
 } else if (value.isEmpty){
 _password=ValidationModel(null,null);
 } else {
 _password=ValidationModel(null, “Must have at least 8 characters”);
 }
 notifyListeners();
 }
 
 ViewState _state;
 
 @override
 void setState(ViewState viewState) {
 _state = viewState;
 notifyListeners();
 }
 
 @override
 ViewState get state => _state;
 
 Future<LoginResponse> submitLogin() async {
 
 setState(ViewState.Busy);
 final Mapable response = await apiClient.serverDataProvider.login(_email.value, _password.value,);
 setState(ViewState.Idle);
 if (response is LoginResponse) {
 print(‘response.token ${response.token});
 
 return response;
 }
 }
 
 @override
 void dispose() {
 
 super.dispose();
 }
 
 }

Upon typing of email and password, isValid is getting updated. If isValid is received as true by Consumer with RaisedButton child, then the button gets enabled else, it will remain disabled.

This is also the case with showing CircularIndicator, by updating setState method in LoginProvider class.

Bloc Pattern

What is a Bloc pattern?

The full form of Bloc is Business Logic Component. It means we have two classes: one contains all the UI components to render in the front end, and the other is the Bloc class which will have all the business logic and data preparation code so that it can be easily fed to the screen to show whatever is required.

Suppose we need to show the date with a proper format decided by the business. So the date will be formatted in Bloc class. In the front end, the class instance of the Bloc class will be available on the screen to render the date in the label, and no further modification is needed for the date.

We will try to achieve the same functionality to implement the Bloc Pattern to see how validation works in the Login Screen.

What are the prerequisites for dealing with the Bloc pattern?

> Sinks and streams

Sinks and Streams are part of StreamController. StreamController can be understood as a pipe.

When there is a change in the value of data after some user interaction or due to any event we add in the pipe, then we use Sink, whereas to listen to that change in data, we can use Stream.

> RxDart

RxDart is the wrapper over the StreamControllerTo use the Bloc pattern, we will add rxDart in our .yaml file. (rxdart: ^0.24.0)

> StreamBuilder

StreamBuilder is the widget provided by Flutter, which listens for the change in the stream after some event occurs and it rebuilds its child widget. Let’s dive into the code for Bloc.

We will wrap every widget from StreamBuilder on Screen to rebuild the child widget based on the event.

So here, we can compare the StreamBuilder in Bloc with Consumer in Provider.

The difference is that StreamBuilder listens to the stream and fetches the model on every change to rebuild the widget. But Consumer listens as soon as notifyListeners() executes inside the provider class.

Building a login screen using Bloc Pattern

class LoginScreenWithBloc extends StatefulWidget {
 @override
 _LoginScreenWithBlocState createState() => _LoginScreenWithBlocState();
 }
 
 class _LoginScreenWithBlocState extends State<LoginScreenWithBloc> {
 LoginBloc _bloc = LoginBloc();
 
 @override
 void initState() {
 super.initState();
 }
 
 @override
 Widget build(BuildContext context) {
 return Scaffold(
 appBar: AppBar(title: Text(‘Login’)),
 body: Padding(
 padding: const EdgeInsets.all(8.0),
 child: ListView(
 children: [StreamBuilder<String>(
 stream: _bloc.emailStream,
 builder: (context, snapshot) {
 return TextField(
 decoration: InputDecoration(
 labelText: “Email”,
 errorText: snapshot.error,
 ),
 onChanged: (String value) {
 print(‘provider.email.error ${snapshot.error});
 _bloc.emailOnChange(value);
 },
 );
 }),
 StreamBuilder<String>(
 stream: _bloc.passwordStream,
 builder: (context, snapshot) {
 return TextField(
 decoration: InputDecoration(
 labelText: “Password”,
 errorText: snapshot.error,
 ),
 onChanged: (String value) {
 print(${snapshot.error});
 _bloc.passwordOnChange(value);
 },
 );
 }),
 StreamBuilder<bool>(
 stream: _bloc.loaderStream,
 builder: (context, snapshot) {
 return Container(
 height: 150,
 child: Center(
 child: (snapshot.hasData && snapshot.data)
 ? CircularProgressIndicator()
 : Container(),
 ),
 );
 }),
 StreamBuilder<bool>(
 stream: _bloc.submitValid,
 builder: (context, snapshot) {
 return RaisedButton(
 color: Colors.blue,
 disabledColor: Colors.grey,
 child: Text(
 ‘Submit’,
 style: TextStyle(color: Colors.white),
 ),
 onPressed: (snapshot.hasData && snapshot.data)
 ? _bloc.submitLogin
 : null,
 );
 })
 ],
 ),
 ));
 }
 }

Here we are using the same number of widgets by coding different ways to perform the same kind of behavior. Here inside the Stateful widget, we have every widget inside the StreamBuilder widget attached to its respective stream to sense the changes in LoginBloc class.

Login_bloc.dart

class LoginBloc {
 
 final PublishSubject<bool> _isEmailValid = PublishSubject<bool>();
 final PublishSubject<bool> _isPasswordValid = PublishSubject<bool>();
 final PublishSubject<bool> _loadingObserver = PublishSubject<bool>();
 get loaderStream => _loadingObserver.stream;
 
 LoginBloc() {
 
 emailStream.listen((value){
 _isEmailValid.sink.add(true);
 }, onError:(error) {
 _isEmailValid.sink.add(false);
 });
 
 passwordStream.listen((value){
 _isPasswordValid.sink.add(true);
 }, onError:(error) {
 _isPasswordValid.sink.add(false);
 });
 
 }
 
 final BehaviorSubject _emailController = BehaviorSubject<String>();
 Stream<String> get emailStream => _emailController.stream.transform(validateEmail());
 Function(String) get emailOnChange => _emailController.sink.add;
 
 final BehaviorSubject _passwordController = BehaviorSubject<String>();
 Stream<String> get passwordStream => _passwordController.stream.transform(validatePassword());
 Function(String) get passwordOnChange => _passwordController.sink.add;
 
 Stream<bool> get submitValid => Rx.combineLatest2(_isEmailValid.stream, _isPasswordValid.stream, (isEmailValid, isPasswordValid) {
 if(isEmailValid is bool && isPasswordValid is bool) {
 return isEmailValid && isPasswordValid;
 }
 return false;
 });
 
 StreamTransformer validateEmail() {
 return StreamTransformer<String, String>.fromHandlers(
 handleData: (String email, EventSink<String> sink) {
 if (ValidatorType.email.hasMatch(email)){
 sink.add(email);
 
 } else if (email.isEmpty){
 sink.addError(null);
 
 } else {
 sink.addError(“Enter a valid email”);
 
 }
 }
 );
 }
 
 StreamTransformer validatePassword() {
 return StreamTransformer<String, String>.fromHandlers(
 handleData: (String password, EventSink<String> sink) {
 if (ValidatorType.password.hasMatch(password)){
 sink.add(password);
 
 } else if (password.isEmpty){
 sink.addError(null);
 
 } else {
 sink.addError(“Must have at least 8 characters”);
 
 }
 }
 );
 }
 
 Future<LoginResponse> submitLogin() async {
 
 _loadingObserver.sink.add(true);
 
 final Mapable response = await apiClient.serverDataProvider.login(_emailController.value, _passwordController.value,);
 _loadingObserver.sink.add(false);
 if (response is LoginResponse) {
 print(‘response.token ${response.token});
 
 return response;
 }
 }
 
 void dispose(){
 _emailController.close();
 _passwordController.close();
 _isEmailValid.close();
 _isPasswordValid.close();
 }
 }

And in LoginBloc class, we have _emailController for inputting the text field value, _isEmailValid for checking if the email entered is a valid email or not, and validateEmail() method validates the format of the email, which satisfies the value in email regex.

Conclusion

To sum up, let’s compare Provider and Bloc patterns:

A picture containing table

Description automatically generated

The change in the value of parameter submitValid depends upon the value of isEmailValid.stream and _isPasswordValid.stream in LoginBloc class.

It’s up to you which pattern you choose. I have tried to explain both patterns with the same functionality.

Happy Coding!

 

Author — Dilshad Haidari, DLT Labs
About the Author: Dilshad is an experienced IT Practioner who has expertise in iOS native and cross-platform Flutter.


That the contents of third-party articles/blogs published here on the website, and the interpretation of all information in the article/blogs such as data, maps, numbers, opinions etc. displayed in the article/blogs and views or the opinions expressed within the content are solely of the author's; and do not reflect the opinions and beliefs of NASSCOM or its affiliates in any manner. NASSCOM does not take any liability w.r.t. content in any manner and will not be liable in any manner whatsoever for any kind of liability arising out of any act, error or omission. The contents of third-party article/blogs published, are provided solely as convenience; and the presence of these articles/blogs should not, under any circumstances, be considered as an endorsement of the contents by NASSCOM in any manner; and if you chose to access these articles/blogs , you do so at your own risk.


DLT Labs™ is a global leader in the development and delivery of enterprise blockchain technologies and solutions, as well as a pioneer in the creation and implementation of standards for application development. With a deep track record in innovation and one of the world's largest pools of highly experienced blockchain experts, DLT Labs™ enables the transformation and innovation of complex multi-stakeholder processes.

© Copyright nasscom. All Rights Reserved.