Reputation: 3669
I am trying to use BLOC pattern on a basic authentication form which contains both login and signup, where the only difference between login and signup is that signup has an additional Confirm Password
field which also contributes to whether Signup
button should be enabled.
I have two questions:
1. This is a problem. Currently, if I enter some login passing the Login
validation, then switch to Signup
form, the Signup
button is enabled which is wrong because Confirm Password
is still empty. How to fix this?
2. I feel like there is a better way than what I have done to achieve the Confirm Password
validation and Signup
button validation. I initially tried to create a validator for Confirm Password
but it shall take both password and confirm password as an input but couldn't get it working as StreamTransformer
only take one input parameter. What's the better way of doing this?
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:rxdart/rxdart.dart';
void main() => runApp(AuthProvider(child: MaterialApp(home: Auth())));
enum AuthMode { Signup, Login }
class Auth extends StatefulWidget {
@override
_AuthState createState() => _AuthState();
}
class _AuthState extends State<Auth> {
AuthMode authMode = AuthMode.Login;
bool get _isLoginMode => authMode == AuthMode.Login;
TextEditingController confirmPasswordCtrl = TextEditingController();
@override
Widget build(BuildContext context) {
final bloc = AuthProvider.of(context);
return Scaffold(
body: Container(
margin: EdgeInsets.all(20.0),
child: Column(
children: <Widget>[
emailField(bloc),
passwordField(bloc),
confirmPasswordField(bloc),
Container(
margin: EdgeInsets.only(top: 40.0),
),
FlatButton(
child: Text('Switch to ${_isLoginMode ? 'Signup' : 'Login'}'),
onPressed: swithAuthMode,
),
loginOrSignupButton(bloc),
],
),
),
);
}
void swithAuthMode() {
setState(() {
authMode = authMode == AuthMode.Login ? AuthMode.Signup : AuthMode.Login;
});
}
Widget confirmPasswordField(AuthBloc bloc) {
return _isLoginMode
? Container()
: StreamBuilder(
stream: bloc.passwordConfirmed,
builder: (context, snapshot) {
return TextField(
obscureText: true,
onChanged: bloc.changeConfirmPassword,
keyboardType: TextInputType.text,
decoration: InputDecoration(
labelText: 'Confirm Password',
errorText: snapshot.hasData && !snapshot.data ? 'password mismatch' : null,
),
);
},
);
}
Widget emailField(AuthBloc bloc) {
return StreamBuilder(
stream: bloc.email,
builder: (context, snapshot) {
return TextField(
keyboardType: TextInputType.emailAddress,
onChanged: bloc.changeEmail,
decoration: InputDecoration(
hintText: 'your email',
labelText: 'Email',
errorText: snapshot.error,
),
);
},
);
}
Widget loginOrSignupButton(AuthBloc bloc) {
return StreamBuilder(
stream: _isLoginMode ? bloc.submitValid : bloc.signupValid,
builder: (context, snapshot) {
print('hasData: ${snapshot.hasData}, data: ${snapshot.data}');
return RaisedButton(
onPressed: // The problem is, after entering some login details then switching from login to signup, the Signup button is enabled.
!snapshot.hasData || !snapshot.data ? null : () => onSubmitPressed(bloc, context),
color: Colors.blue,
child: Text('${_isLoginMode ? 'Log in' : 'Sign up'}'),
);
},
);
}
void onSubmitPressed(AuthBloc bloc, BuildContext context) async {
var response = await bloc.submit(_isLoginMode);
if (response.success) {
Navigator.pushReplacementNamed(context, '/home');
} else {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Error'),
content: Text(response.message),
actions: <Widget>[
FlatButton(
child: Text('Ok'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
});
}
}
Widget passwordField(AuthBloc bloc) {
return StreamBuilder(
stream: bloc.password,
builder: (_, snapshot) {
return TextField(
obscureText: true,
onChanged: bloc.changePassword,
decoration: InputDecoration(
labelText: 'Password',
errorText: snapshot.error,
hintText: 'at least 6 characters',
),
);
},
);
}
}
class AuthProvider extends InheritedWidget {
final bloc;
AuthProvider({Key key, Widget child}) :
bloc = AuthBloc(), super(key:key, child: child);
@override
bool updateShouldNotify(InheritedWidget oldWidget) => true;
static AuthBloc of(BuildContext context) => (context.inheritFromWidgetOfExactType(AuthProvider) as AuthProvider).bloc;
}
class Repository {
// this will call whatever backend to authenticate users.
Future<AuthResult> signupUser(String email, String password) => null;
Future<AuthResult> loginUser(String email, String password) => null;
}
class AuthBloc extends Object with AuthValidator {
final _emailController = BehaviorSubject<String>();
final _passwordController = BehaviorSubject<String>();
final _confirmPasswordController = BehaviorSubject<String>();
final _signupController = PublishSubject<Map<String, dynamic>>();
final Repository _repository = Repository();
Stream<String> get email => _emailController.stream.transform(validateEmail);
Stream<String> get password =>
_passwordController.stream.transform(validatePassword);
Stream<bool> get submitValid =>
Observable.combineLatest2(email, password, (e, p) => true);
// Is there a better way of doing passwordConfirmed and signupValid?
Stream<bool> get passwordConfirmed =>
Observable.combineLatest2(password, _confirmPasswordController.stream, (p, cp) => p == cp);
Stream<bool> get signupValid =>
Observable.combineLatest2(submitValid, passwordConfirmed, (s, p) => s && p);
// sink
Function(String) get changeEmail => _emailController.sink.add;
Function(String) get changePassword => _passwordController.sink.add;
Function(String) get changeConfirmPassword =>
_confirmPasswordController.sink.add;
Future<AuthResult> submit(bool isLogin) async {
final validEmail = _emailController.value;
final validPassword = _passwordController.value;
if (!isLogin)
return await _repository.signupUser(validEmail, validPassword);
return await _repository.loginUser(validEmail, validPassword);
}
void dispose() {
_emailController.close();
_passwordController.close();
_signupController.close();
_confirmPasswordController.close();
}
}
class AuthResult {
bool success;
String message;
AuthResult(this.success, this.message);
}
// demo validator
class AuthValidator {
final validateEmail = StreamTransformer<String, String>.fromHandlers(
handleData: (email, sink) {
if (email.contains('@')) sink.add(email);
else sink.addError('Email is not valid');
}
);
final validatePassword = StreamTransformer<String, String>.fromHandlers(
handleData: (password, sink) {
if (password.length >= 6) sink.add(password);
else sink.addError('Password must be at least 6 characters');
}
);
}
Upvotes: 3
Views: 6000
Reputation: 106
After a few days of analyzing I got a more effective way to handle auth using bloc. You can follow.
1st Update Login Info Stream under Sign In Bloc
// Handle User Name Info From Login UI
final _userNameController = BehaviorSubject<String>();
Stream<String> get userNameStream => _userNameController.stream;
void updateUserName(String userName) {
if (userName.isEmpty) {
_userNameController.addError("Please Enter Username");
return;
}
_userNameController.sink.add(userName);
return;
}
// handle User Password From Login UI
final _passwordController = BehaviorSubject<String>();
Stream<String> get passwordStrem => _passwordController.stream;
void updatePassword(String password) async {
if (password.isEmpty) {
_passwordController.addError("Please Enter Password");
return;
} else if (password.length < 6) {
_passwordController.addError("Password Must be At least 6");
return;
}
_passwordController.sink.add(password);
return;
}
// User Login button validation bloc code starts here
Stream<bool> get isValidButton => Rx.combineLatest2(
passwordStrem,
userNameStream,
(a, b,) => true );
2d Make sure properly injected your Login Bloc in Login Page. Now Call Bloc To check user Login, Auth
StreamBuilder<String>(
stream: context.read<SignInBloc>().userNameStream,
builder: (context, snapshot) {
return TextFormField(
controller: usernameController,
decoration: InputDecoration(
errorText: snapshot.error as String?,
),
onChanged: (value) {
context.read<SignInBloc>() .updateUserName(value);
},
);
}),
StreamBuilder<String>(
stream: context.read<SignInBloc>().passwordStrem,
builder: (context, snapshot) {
return TextFormField(
controller: passwordController,
cursorColor:
const Color(0xff538234),
obscureText: showPass,
decoration: InputDecoration(
errorText:snapshot.error as String?,
),
onChanged: (value) {
context.read<SignInBloc>().updatePassword(value);
},
);
}),
3rd is Login Button Validation
StreamBuilder<bool>(
stream: context.read<SignInBloc().isValidButton,
builder: (context, snapshot) {
bool isValid =snapshot.data ?? false;
return ButtonPrimary(
name: S.of(context).login,
onPressed: !isValid
? null
: () async {
// CALL YOUR LOGIN API WHEN BUTTON IS VALID
}
);
}),
Upvotes: 0
Reputation: 1121
In your case, a better way to do passwordConfirmed would be:
Stream<String> get passwordConfirmed => _confirmPasswordController.stream
.transform(validatePassword).doOnData((String confirmPassword){
if(0 != _passwordController.value.compareTo(confirmPassword)){
_confirmPasswordController.addError("Passwords do not match");
}
});
As suggested by boeledi here.
Upvotes: 2
Reputation: 520
After trying to replicate the behavior I could confirm that signupValid
stream has a true
value if submitValid
has a true
value, seems like the computation for signupValid
is never being performed.
One work around would be to clear the text fields and adding an empty string to the streams on changing the login mode from login to sign up and vice versa.
Upvotes: 1