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 pattern, Provider 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:
-
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.
-
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.
-
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 StreamController. To 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:
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.