Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save brianegan/45a46c85f318f3d1f43420810878aa6a to your computer and use it in GitHub Desktop.
Save brianegan/45a46c85f318f3d1f43420810878aa6a to your computer and use it in GitHub Desktop.
ComputedValueNotifier concept
import 'package:flutter/foundation.dart';
/// A class that can be used to derive a value based on data from another
/// Listenable or Listenables.
///
/// The value will be recomputed when the provided [listenable] notifies the
/// listeners that values have changed.
///
/// ### Simple Example
///
/// ```dart
/// final email = ValueNotifier<String>('a');
///
/// // Determine whether or not the email is valid using a (hacky) validator.
/// final emailValid = ComputedValueNotifier(
/// email,
/// () => email.value.contains('@'),
/// );
///
/// // The function provided to ComputedValueNotifier is immediately executed,
/// // and the computed value is available synchronously.
/// print(emailValid); // prints 'false'.
///
/// // When the email ValueNotifier is changed, the function will be run again!
/// email.value = 'a@b.com';
/// print(emailValid); // prints 'true'.
/// ```
///
/// ### Deriving data from multiple listenables
///
/// In this case, we can use the `Lisetenable.merge` function provided by
/// Flutter to merge several variables.
///
/// ```dart
/// final email = ValueNotifier<String>('');
/// final password = ValueNotifier<String>('');
///
/// // Determine whether the email is valid, and make that a Listenable!
/// final emailValid = ComputedValueNotifier<bool>(
/// email,
/// () => email.value.contains('@'),
/// );
///
/// // Determine whether the password is valid, and make that a Listenable!
/// final passwordValid = ComputedValueNotifier<bool>(
/// password,
/// () => password.value.length >= 6,
/// );
///
/// // Now, we will only enable the "Login Button" when the email and
/// // password are valid. To do so, we can listen to the emailValid and
/// // passwordValid ComputedValueNotifiers.
/// final loginButtonEnabled = ComputedValueNotifier<bool>(
/// Listenable.merge([emailValid, passwordValid]),
/// () => emailValid.value && passwordValid.value,
/// );
///
/// // Update the email
/// print(emailValid.value); // false
/// print(loginButtonEnabled.value); // false
/// email.value = 'a@b.com';
/// print(emailValid.value); // true
/// print(loginButtonEnabled.value); // false
///
/// // Update the password
/// print(passwordValid.value); // false
/// password.value = '123456';
/// print(passwordValid.value); // true
/// print(loginButtonEnabled.value); // true
/// ```
class ComputedValueNotifier<T> extends ChangeNotifier
implements ValueListenable {
final Listenable listenable;
final T Function() compute;
T value;
ComputedValueNotifier(this.listenable, this.compute) {
_updateValue();
listenable.addListener(_updateValue);
}
@override
void dispose() {
listenable.removeListener(_updateValue);
super.dispose();
}
void _updateValue() {
value = compute();
}
}
import 'package:auth_scoped_model/login_value_notifier/computed_value_notifier.dart';
import 'package:flutter/foundation.dart';
enum LogInMode { login, signup }
/// This version uses the Listenable class once again, but in this case each
/// field is an individual Listenable (ValueNotifier).
///
/// This means that any data that can be changed over time needs to be an
/// individual ValueNotifier, rather than a simple method, such as you will find
/// in the ChangeNotifier example.
///
/// Pros:
/// 1. Multiple Listenables means the Widget Tree can use different
/// AnimatedBuilders that subscribe to the exact data they need.
/// 2. Very few LOC. Only a few more than the ChangeNotifier example.
/// 3.
class ValueNotifierLoginController {
ValueNotifier<String> email;
ValueNotifier<String> password;
ValueNotifier<String> confirmPassword;
ValueNotifier<LogInMode> mode;
ComputedValueNotifier<bool> buttonEnabled;
ComputedValueNotifier<String> emailErrorText;
ComputedValueNotifier<String> confirmPasswordErrorText;
ComputedValueNotifier<String> passwordErrorText;
ValueNotifierLoginController() {
email = ValueNotifier<String>('');
password = ValueNotifier<String>('');
confirmPassword = ValueNotifier<String>('');
mode = ValueNotifier<LogInMode>(LogInMode.login);
buttonEnabled = ComputedValueNotifier<bool>(
Listenable.merge([email, password, confirmPassword, mode]),
() => isLogin ? _loginValid : _signUpValid,
);
emailErrorText = ComputedValueNotifier<String>(
email,
() => _emailValid ? null : 'Email is not valid',
);
confirmPasswordErrorText = ComputedValueNotifier<String>(
Listenable.merge([password, confirmPassword]),
() => isSignup && !_passwordsEqual ? "Two passwords don't match" : null,
);
passwordErrorText = ComputedValueNotifier<String>(
password,
() => _passwordValid ? null : 'Password must be at least 5 characters',
);
}
bool get isLogin => mode.value == LogInMode.login;
bool get isSignup => mode.value == LogInMode.signup;
bool get _emailValid => email.value.contains('@');
bool get _isPasswordCorrect => password.value == '12345';
bool get _loginValid => _emailValid && _passwordValid;
bool get _passwordsEqual => password.value == confirmPassword.value;
bool get _passwordValid => password.value.length >= 5;
bool get _signUpValid => _passwordsEqual && _loginValid;
void dispose() {
email.dispose();
password.dispose();
confirmPassword.dispose();
mode.dispose();
emailErrorText.dispose();
passwordErrorText.dispose();
confirmPasswordErrorText.dispose();
buttonEnabled.dispose();
}
Future<bool> logIn() async => _loginValid && _isPasswordCorrect;
Future<bool> signUp() async {
return _signUpValid && _isPasswordCorrect;
}
void toggleMode() {
mode.value =
mode.value == LogInMode.login ? LogInMode.signup : LogInMode.login;
}
}
import 'package:auth_scoped_model/login_value_notifier/login_controller.dart';
import 'package:flutter/material.dart';
class ConfirmPasswordTextField extends StatelessWidget {
final ValueNotifierLoginController _controller;
const ConfirmPasswordTextField({
Key key,
@required ValueNotifierLoginController controller,
}) : _controller = controller,
super(key: key);
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller.confirmPasswordErrorText,
builder: (context, _) {
return TextField(
obscureText: true,
onChanged: (confirmPassword) =>
_controller.confirmPassword.value = confirmPassword,
decoration: InputDecoration(
labelText: 'Confirm Password',
hintText: 'enter the same password to confirm',
errorText: _controller.confirmPasswordErrorText.value,
prefixIcon: Icon(Icons.lock),
),
);
},
);
}
}
class EmailTextField extends StatelessWidget {
final ValueNotifierLoginController _controller;
const EmailTextField({
Key key,
@required ValueNotifierLoginController controller,
}) : _controller = controller,
super(key: key);
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller.emailErrorText,
builder: (context, _) {
return TextField(
keyboardType: TextInputType.emailAddress,
onChanged: (email) => _controller.email.value = email,
decoration: InputDecoration(
hintText: 'Email',
//labelText: 'Email',
errorText: _controller.emailErrorText.value,
prefixIcon: Icon(Icons.email),
),
);
},
);
}
}
class LoginButton extends StatelessWidget {
final ValueNotifierLoginController _controller;
const LoginButton({
Key key,
@required ValueNotifierLoginController controller,
}) : _controller = controller,
super(key: key);
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller.buttonEnabled,
builder: (context, _) {
return RaisedButton(
onPressed: _controller.buttonEnabled.value
? () async {
void showErrorSnackbar() {
Scaffold.of(context).showSnackBar(
SnackBar(content: Text('Error: Login Failed')));
}
try {
final loginSuccess = await (_controller.isLogin
? _controller.logIn()
: _controller.signUp());
if (loginSuccess) {
Navigator.pushReplacementNamed(context, '/dashboard');
} else {
showErrorSnackbar();
}
} catch (e) {
showErrorSnackbar();
}
}
: null,
child: Text(
_controller.isLogin ? 'Log in' : 'Sign up',
),
);
},
);
}
}
class LoginForm extends StatelessWidget {
final ValueNotifierLoginController _controller;
const LoginForm({
Key key,
@required ValueNotifierLoginController controller,
}) : _controller = controller,
super(key: key);
@override
Widget build(BuildContext context) {
return ListView(
padding: EdgeInsets.all(16.0),
children: <Widget>[
EmailTextField(controller: _controller),
PasswordTextField(controller: _controller),
AnimatedBuilder(
animation: _controller.mode,
builder: (context, _) => _controller.isLogin
? Container()
: ConfirmPasswordTextField(controller: _controller),
),
LoginButton(controller: _controller),
InkWell(
onTap: _controller.toggleMode,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Text(_controller.isLogin
? 'Need to register? Sign Up.'
: 'Have an account? Log in.'),
),
),
)
],
);
}
}
class PasswordTextField extends StatelessWidget {
final ValueNotifierLoginController _controller;
const PasswordTextField({
Key key,
@required ValueNotifierLoginController controller,
}) : _controller = controller,
super(key: key);
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller.passwordErrorText,
builder: (context, _) {
return TextField(
onChanged: (password) => _controller.password.value = password,
obscureText: true,
decoration: InputDecoration(
hintText: 'Password',
errorText: _controller.passwordErrorText.value,
prefixIcon: Icon(Icons.lock),
),
);
},
);
}
}
class ValueNotifierLoginScreen extends StatefulWidget {
@override
_ValueNotifierLoginScreenState createState() =>
_ValueNotifierLoginScreenState();
}
class _ValueNotifierLoginScreenState extends State<ValueNotifierLoginScreen> {
final _controller = ValueNotifierLoginController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// Use AnimatedBuilders to Listen for Changes to the Controller.
title: AnimatedBuilder(
animation: _controller.mode,
builder: (context, _) {
return Text(
_controller.isLogin ? 'VN Login' : 'VN Signup',
);
},
),
),
body: LoginForm(controller: _controller),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment