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:
The last logged-in username can be auto-filled (if available).
To prevent password input errors, the password field should have a toggle to view the plaintext.
Local validity checks should be performed on the username and password fields before calling the login API (e.g., they cannot be empty).
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 .