In this blog post, we'll explore efficient state management in Flutter applications using ValueNotifier and EfficientBuilder within the MVVM (Model-View-ViewModel) architectural pattern.
The MVVM architecture consists of three key components:
- Model: Represents the data and business logic
- View: Handles the UI presentation
- ViewModel: Manages state and business logic between Model and View
In our approach, we’ll be refining the ViewModel by separating its states into a dedicated State class. This ensures that the ViewModel focuses solely on managing logic while the State class handles state representation. Traditionally, in MVVM, the ViewModel is responsible for both state management and business logic. However, by decoupling these concerns, we create a more structured and maintainable architecture.
No more talking. Lets, implement it together.
1. Efficient Builder:
We are going to use Efficient Builder package for this. So, go to pub.dev. Add the package to you pubspec.yaml file.
dependencies:
flutter:
sdk: flutter
efficient_builder: ^1.0.2
2. UI design:
Here, we are going to make a login screen app. So, it will consist an AppBar, two TextField and a Button for login. But the button will be clickable, whenever there is no validation error and user filled all the TextFiled.
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginUiState();
}
class _LoginUiState extends State<LoginScreen> {
final _viewModel = LoginViewModel();
@override
void dispose() {
_viewModel.onDispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("SIGN IN"),
),
body: _buildBody(context),
);
}
Widget _buildBody(BuildContext context) {
return Container(
margin: const EdgeInsets.fromLTRB(20, 20, 20, 0),
child: Column(
children: [
LoginEmailField(viewModel: _viewModel),
const SizedBox(height: 24),
LoginPasswordField(viewModel: _viewModel),
const SizedBox(height: 40),
LoginButton(viewModel: _viewModel),
],
),
);
}
}
class LoginEmailField extends StatefulWidget {
final LoginViewModel viewModel;
const LoginEmailField({
super.key,
required this.viewModel,
});
@override
State<LoginEmailField> createState() => _LoginEmailFieldState();
}
class _LoginEmailFieldState extends State<LoginEmailField> {
final _emailController = TextEditingController();
@override
void dispose() {
_emailController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return EfficientBuilder(
buildWhen: (p, n) {
return p.emailError != n.emailError || p.email != n.email;
},
valueListenable: widget.viewModel.loginStates,
builder: (context, state, _) {
return CustomTextField(
initialText: state.email,
textFieldName: 'Email',
textFieldType: TextFieldType.email,
errorText: state.emailError?.getError(),
onChanged: (email) => widget.viewModel.onChangedEmail(email: email),
);
},
);
}
}
class LoginPasswordField extends StatefulWidget {
final LoginViewModel viewModel;
const LoginPasswordField({
super.key,
required this.viewModel,
});
@override
State<LoginPasswordField> createState() => _LoginPasswordFieldState();
}
class _LoginPasswordFieldState extends State<LoginPasswordField> {
final _passwordController = TextEditingController();
@override
void dispose() {
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return widget.viewModel.loginStates.build(
buildWhen: (p, n) {
return p.passwordError != n.passwordError || p.password != n.password;
},
builder: (context, state) {
return CustomTextField(
initialText: state.password,
textFieldName: 'Password',
errorText: state.passwordError?.getError(),
textFieldType: TextFieldType.password,
onChanged: (password) {
widget.viewModel.onChangedPassword(password: password);
},
);
},
);
}
}
class LoginButton extends StatelessWidget {
final LoginViewModel viewModel;
const LoginButton({
super.key,
required this.viewModel,
});
@override
Widget build(BuildContext context) {
return viewModel.loginStates.buildFor(
select: (state) => state.showButton,
builder: (context, state) {
return PrimaryButton(
label: "LOG IN",
onPressed: () {
viewModel.onTapLoginButton();
},
minWidth: double.infinity,
isDisabled: !state.showButton,
);
},
);
}
}
3. State Class:
Login State class will hold, all the states that will be needed in Login Screen UI.
class LoginStates {
final String email;
final String password;
final bool showButton;
final ValidationError? emailError;
final ValidationError? passwordError;
LoginStates({
required this.email,
required this.password,
required this.showButton,
this.emailError,
this.passwordError,
});
factory LoginStates.initial() {
return LoginStates(
email: '',
password: '',
showButton: false,
);
}
LoginStates copyWith({
String? email,
String? password,
bool? showButton,
ValidationError? emailError,
ValidationError? passwordError,
}) {
return LoginStates(
email: email ?? this.email,
password: password ?? this.password,
showButton: showButton ?? this.showButton,
emailError: emailError ?? this.emailError,
passwordError: passwordError ?? this.passwordError,
);
}
}
4. ViewModel class:
Login ViewModel will be responsible for holding up bussiness logics. It will determine, how UI state will chnage and when should change a state value.
In this LoginViewModel class, you will see a valueNotifier is holding up the Login State class and a Valuelistenable for UI to listen the changes in Valuenotifier.
class LoginViewModel {
final _loginStates = ValueNotifier<LoginStates>(LoginStates.initial());
LoginStates get _states => _loginStates.value;
ValueListenable<LoginStates> get loginStates => _loginStates;
void onChangedEmail({required String? email}) {
final emailError = EmailValidator.getEmailValidation(email);
_loginStates.value = _states.copyWith(
emailError: emailError,
email: email,
);
_checkUpdateButtonState();
}
void onChangedPassword({required String? password}) {
final passwordError = PasswordValidator.getValidationError(password);
_loginStates.value = _states.copyWith(
passwordError: passwordError,
password: password,
);
_checkUpdateButtonState();
}
void _checkUpdateButtonState() {
final hasAllFieldsFilled =
_states.email.isNotEmpty && _states.password.isNotEmpty;
final hasNoErrors = _states.emailError == ValidationError.none &&
_states.passwordError == ValidationError.none;
_loginStates.value = _states.copyWith(
showButton: hasNoErrors && hasAllFieldsFilled,
);
}
void onTapLoginButton() {
_loginStates.value = _states.copyWith(
showButton: false,
);
}
void onDispose() {
_loginStates.dispose();
}
}
At last, don't forget to Dispose the listener and TextFields. Else, you will face memory leaks in your Application.
Also, you can check the whole example code, Login Example.
Comments
Post a Comment