Created
November 16, 2018 13:43
-
-
Save brianegan/45a46c85f318f3d1f43420810878aa6a to your computer and use it in GitHub Desktop.
ComputedValueNotifier concept
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 = '[email protected]'; | |
/// 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 = '[email protected]'; | |
/// 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(); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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