【问题标题】:Use BLOC pattern for an authentication form将 BLOC 模式用于身份验证表单
【发布时间】:2018-09-27 21:11:42
【问题描述】:

我正在尝试在包含登录和注册的基本身份验证表单上使用 BLOC 模式,其中登录和注册之间的唯一区别是注册有一个额外的 Confirm Password 字段,这也有助于 Signup 按钮是否应该被启用。

我有两个问题: 1. 这是一个问题。目前,如果我输入一些通过Login 验证的登录名,然后切换到Signup 表单,则启用Signup 按钮这是错误的,因为Confirm Password 仍然为空。如何解决这个问题? 2. 我觉得有比我所做的更好的方法来实现Confirm Password 验证和Signup 按钮验证。我最初尝试为Confirm Password 创建一个验证器,但它需要both 密码和确认密码作为输入,但无法让它工作,因为StreamTransformer 只接受一个输入参数。这样做的更好方法是什么?

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');
    }
  );
}

【问题讨论】:

  • 我试过调查这个,如果submitValid 具有真实值,我可以验证signupValid 流具有true 值,似乎signupValid 的计算从未被执行。一种解决方法是清除文本字段并在将登录模式从登录更改为注册时向流中添加空字符串,反之亦然。请让我知道它是否有帮助,以及您是否能够找到解决方案。
  • @ssiddh 你的建议有点帮助,但整个事情仍然很不稳定。我认为为同一个小部件(登录或注册按钮)切换两个流可能从根本上是错误的。我现在已经把它停了下来,并使用了一个标准的颤振 Form,它的 reactive 较少,但至少更容易管理。
  • @ssiddh 实际上,我通过执行您的建议使其工作,即在从登录切换到注册时向流中添加一个空字符串,这禁用了注册按钮。如果您想发布答案,我当然可以投票或接受。
  • 很高兴它有帮助 :) 很快就会添加答案。

标签: flutter


【解决方案1】:

在您的情况下,执行 passwordConfirmed 的更好方法是:

Stream<String> get passwordConfirmed => _confirmPasswordController.stream
    .transform(validatePassword).doOnData((String confirmPassword){
        if(0 != _passwordController.value.compareTo(confirmPassword)){
            _confirmPasswordController.addError("Passwords do not match");
        }
});

正如 boeledi here 所建议的那样。

【讨论】:

  • 为什么这样更好?它再次进行验证,确认密码不需要。
  • @stt106 验证不是强制性的。您可以将其取下,代码将完美运行。在我看来,它更好,因为对于 confirmPasswordFielderrorText 属性,您可以只执行 snapshot.error 而不是那里的长三元表达式.
【解决方案2】:

在尝试复制该行为后,如果submitValid 具有true 值,我可以确认signupValid 流具有true 值,似乎永远不会执行signupValid 的计算。

一种解决方法是清除文本字段并在将登录模式从登录更改为注册时向流中添加一个空字符串,反之亦然。

【讨论】:

    猜你喜欢
    • 2012-10-23
    • 2013-11-22
    • 2021-10-22
    • 2013-10-30
    • 1970-01-01
    • 2011-03-12
    • 2012-03-15
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多