Flutter (103): Login page

Time: Column:Mobile & Frontend views:242

We mentioned that GitHub has various login methods. For simplicity, we will only implement login via username and password. When implementing the login page, there are four points to note:

  1. The last logged-in username can be auto-filled (if available).

  2. To prevent password input errors, the password field should have a toggle to view the plaintext.

  3. Local validity checks should be performed on the username and password fields before calling the login API (e.g., they cannot be empty).

  4. User information needs to be updated upon successful login.

Note: GitHub has now disallowed direct password logins for security reasons. Instead, users must generate a login token on GitHub and log in using their account and token. For details on creating a token, please refer to GitHub's official guide (opens new window). For ease of description, the term "password" in this instance specifically refers to the user token.

The implementation code is as follows:

import '../index.dart';

class LoginRoute extends StatefulWidget {
  @override
  _LoginRouteState createState() => _LoginRouteState();
}

class _LoginRouteState extends State<LoginRoute> {
  TextEditingController _unameController = TextEditingController();
  TextEditingController _pwdController = TextEditingController();
  bool pwdShow = false;
  GlobalKey _formKey = GlobalKey<FormState>();
  bool _nameAutoFocus = true;

  @override
  void initState() {
    // Auto-fill the last logged-in username, then focus on the password input field
    _unameController.text = Global.profile.lastLogin ?? "";
    if (_unameController.text.isNotEmpty) {
      _nameAutoFocus = false;
    }
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    var gm = GmLocalizations.of(context);
    return Scaffold(
      appBar: AppBar(title: Text(gm.login)),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Form(
          key: _formKey,
          autovalidateMode: AutovalidateMode.onUserInteraction,
          child: Column(
            children: <Widget>[
              TextFormField(
                  autofocus: _nameAutoFocus,
                  controller: _unameController,
                  decoration: InputDecoration(
                    labelText: gm.userName,
                    hintText: gm.userName,
                    prefixIcon: Icon(Icons.person),
                  ),
                  // Validate username (cannot be empty)
                  validator: (v) {
                    return v == null || v.trim().isNotEmpty ? null : gm.userNameRequired;
                  }),
              TextFormField(
                controller: _pwdController,
                autofocus: !_nameAutoFocus,
                decoration: InputDecoration(
                    labelText: gm.password,
                    hintText: gm.password,
                    prefixIcon: Icon(Icons.lock),
                    suffixIcon: IconButton(
                      icon: Icon(pwdShow ? Icons.visibility_off : Icons.visibility),
                      onPressed: () {
                        setState(() {
                          pwdShow = !pwdShow;
                        });
                      },
                    )),
                obscureText: !pwdShow,
                // Validate password (cannot be empty)
                validator: (v) {
                  return v == null || v.trim().isNotEmpty ? null : gm.passwordRequired;
                },
              ),
              Padding(
                padding: const EdgeInsets.only(top: 25),
                child: ConstrainedBox(
                  constraints: BoxConstraints.expand(height: 55.0),
                  child: ElevatedButton(
                    onPressed: _onLogin,
                    child: Text(gm.login),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  void _onLogin() async {
    // First, validate each form field
    if ((_formKey.currentState as FormState).validate()) {
      showLoading(context);
      User? user;
      try {
        user = await Git(context).login(_unameController.text, _pwdController.text);
        // Since the home page will build after returning from the login page, we pass false to avoid triggering updates when we update user.
        Provider.of<UserModel>(context, listen: false).user = user;
      } on DioError catch (e) {
        // Prompt on login failure
        if (e.response?.statusCode == 401) {
          showToast(GmLocalizations.of(context).userNameOrPasswordWrong);
        } else {
          showToast(e.toString());
        }
      } finally {
        // Hide loading indicator
        Navigator.of(context).pop();
      }
      // Return on successful login
      if (user != null) {
        Navigator.of(context).pop();
      }
    }
  }
}

The code is simple, with key points commented. Below we look at the running effect, as shown in Figure .

Flutter (103): Login page