Ở bài viết trước, mình đã hướng dẫn các bạn cách khởi tạo và cấu hình một dự án Flutter theo mô hình MVVM. Ở bài viết này, mình sẽ hướng dẫn các bạn cách xây dựng màn hình Login cho dự án Flutter.
Để tiếp nối bài này, bạn cần phải đọc và thực hành từ bài viết trước, bạn vào đây để tham khảo bài viết.
Chuẩn bị
Bạn truy cập link github để tải logo của app về “chatbot.png”
Truy cập vào link github để coppy mục “color_extention.dart” để lấy các mã màu và size chữ của dự án mà chúng ta sẽ sử dụng.
Xây dựng màn hình Login cho dự án
Xây dựng login trong ViewModel
Trong bài viết trước, mình có cùng xây dựng mô hình dự án MVVM, các bạn truy cập vào folder ViewModel và tạo file login_view_model.dart.
Ý tưởng xây dựng file login_view_model
Màn hình login của chúng ta sẽ gồm có logo app, ô nhập username, ô nhập password (password ẩn và hiển thị khi chúng ta nhấp vào icon bên cạnh), nút đăng nhập và đăng nhập bằng hình thức khác( thường là tài khoản của bên thứ ba như Google, Facebook, Apple,…),
Mình sẽ demo sản phẩm cho các bạn có thể hình dung rõ hơn sau đó chúng ta sẽ đi xây dựng layout (cách trình bày) sản phẩm:
Ở file login_viewmodel.dart của chúng ta sẽ xử lý các câu lệnh vê logic và quản lý trạng thái của View (cụ thể ở đây là sẽ màn login_view.dart(file sắp tới sẽ tạo)).
Xây dựng file login_view_model.dart
class LoginViewModel extends GetxController {
RxBool isObscured = true.obs;
final formKey = GlobalKey<FormState>();
final showPassword = false.obs;
final isLoading = false.obs;
void toggleObscureText() {
isObscured.value = !isObscured.value;
}
void showHidePassword() {
showPassword.value = !showPassword.value;
}
void onChangeUsername(username) {
formKey.currentState?.validate();
}
void onChangePassword(password) {
formKey.currentState?.validate();
}
bool containsSpecialCharacters(String text) {
final allowedSpecialCharacters = RegExp(r'[!#\$%^&*(),?":{}|<>]');
return allowedSpecialCharacters.hasMatch(text);
}
String? validatorUsername(username) {
if ((username ?? "").isEmpty) {
return "Username không được để trống";
} else if (containsSpecialCharacters(username!)) {
return "Username không đúng định dạng";
} else {
return null;
}
}
String? validatePassword(password) {
if ((password ?? "").isEmpty) {
return "Password không được để trống";
} else {
return null;
}
}
}
Giải thích:
- void toggleObscureText(): Khi gọi hàm này, nó sẽ chuyển đổi giữa ẩn và hiện trường văn bản (ở đây là trường mật khẩu).
- void showHidePassword(): Dùng để thay đổi trạng thái hiển thị của mật khẩu khi người dùng nhấn vào biểu tượng “mắt” trong trường mật khẩu.
- void onChangeUsername(username): Nó gọi hàm validate() để xác thực form mỗi khi có thay đổi trong trường tên đăng nhập.
- void onChangePassword(password): Tương tự như onChangeUsername, hàm này cũng gọi hàm validate() để xác thực form mỗi khi có thay đổi trong trường mật khẩu.
- bool containsSpecialCharacters(String text): Nó sử dụng biểu thức chính quy để xác định các ký tự đặc biệt. Nếu có, hàm trả về true, ngược lại trả về false.
- String? validatorUsername(username): Hàm này kiểm tra nếu trường tên đăng nhập rỗng hoặc chứa ký tự đặc biệt, nó sẽ trả về một thông báo lỗi. Ngược lại, nó sẽ trả về null.
- String? validatePassword(password): Hàm này kiểm tra nếu trường mật khẩu rỗng và trả về thông báo lỗi nếu có. Nếu mật khẩu hợp lệ, nó trả về null.
Tiếp theo chúng ta sẽ xây dựng file login_view.dart để hiện thị giao diện mà mình đã demo ở đầu tiên.
Ý tưởng xây dựng file login_view.dart
Ở file này chúng ta sẽ hiển thị cho người dùng các hình ảnh, ô nhập liệu, các button.
Xây dựng file login_view.dart
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final TextEditingController username = TextEditingController();
final TextEditingController password = TextEditingController();
@override
void initState() {
super.initState();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.leanBack);
}
@override
void dispose() {
username.dispose();
password.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final loginController = Get.put(ViewModel());
return Scaffold(
backgroundColor: TColor.bg,
body: SafeArea(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
const SizedBox(height: 80),
_textLogin(), //login text
const SizedBox(height: 120),
Obx(
() => _formUsernameAndPassword(
loginController), //Returns a form that includes two input fields for username and password
),
const SizedBox(height: 40),
// button login
LoginButton(
loginController: loginController,
context: context,
usernameController: username,
passwordController: password,
onInvalidCredentials: () {
setState(() {
alert_dialog = true;
});
}),
const SizedBox(
height: 60,
),
_textOrContinueWith(),
const SizedBox(
height: 30,
),
_logoAppleAndGoogle(),
alert_dialog
? const Text(
"Invalid credentials.",
style: TextStyle(color: Colors.redAccent),
)
: Container(),
],
),
),
),
);
}
Logo _logoAppleAndGoogle() {
return const Logo();
}
OrContinueWith _textOrContinueWith() {
return const OrContinueWith();
}
//form input fields for username and password
Form _formUsernameAndPassword(ViewModel loginController) {
return Form(
key: loginController.formKey,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 25),
child: Column(
children: [
_formUsername(loginController),
const SizedBox(height: 20),
_formPassword(loginController),
],
),
),
);
}
//form input field for password
TextFormField _formPassword(ViewModel loginController) {
return TextFormField(
controller: password,
obscureText: loginController.isObscured.value,
textAlign: TextAlign.center,
decoration: InputDecoration(
border: const OutlineInputBorder(),
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.lightBlueAccent, width: 0.0),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.blueAccent, width: 2.0),
),
hintText: 'Password',
hintStyle: TextStyle(color: TColor.primaryText28),
suffixIcon: IconButton(
icon: Icon(
loginController.isObscured.value
? Icons.visibility
: Icons.visibility_off,
),
onPressed: loginController.toggleObscureText,
),
),
onChanged: loginController.onChangePassword,
validator: loginController.validatePassword,
style:
TextStyle(color: TColor.primaryText28, fontWeight: FontWeight.bold),
);
}
//form field for username
TextFormField _formUsername(ViewModel loginController) {
return TextFormField(
controller: username,
obscureText: false,
textAlign: TextAlign.center,
decoration: InputDecoration(
border: const OutlineInputBorder(),
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.lightBlueAccent, width: 0.0),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.blueAccent, width: 2.0),
),
hintText: 'Username',
hintStyle: TextStyle(color: TColor.primaryText28),
suffixIcon: const Icon(Icons.person)),
onChanged: loginController.onChangeUsername,
validator: loginController.validatorUsername,
style:
TextStyle(color: TColor.primaryText28, fontWeight: FontWeight.bold),
);
}
//login text
Text _textLogin() {
return Text(
'Login',
style: TextStyle(
fontSize: 60,
fontWeight: FontWeight.bold,
color: TColor.primaryText28,
),
);
}
}
Trong file login_View.dart, đây là file giao diện nên code sẽ rất dài và điều này khiến cho lập trình viên cũng như những người quản lý code khó sử dụng và fix bug nếu như sau này cần chỉnh sửa, vì vậy chúng ta sẽ tách nhỏ các file code để dễ quản lý và bảo trì.
Ở đây ngoài file code chính mình đã tách giao diện thành 3 file nhỏ hơn lần lượt là loginbutton.dart (chứa nút button để sau kiểm tra dữ liệu sẽ chuyển đến màn tiếp theo hoặc hiển thị lỗi nếu sai thông tin), logo.dart (chứa logo của app), login_text.dart (nơi chứa các văn bản dạng text).
Các file này mình sẽ chứa trong foder Widgets, khi sử dụng đến chỉ cần gọi đến là có thể sử dụng được.
Cụ thể:
file loginbutton.dart
class LoginButton extends StatelessWidget {
final ViewModel loginController;
final BuildContext context;
final TextEditingController usernameController;
final TextEditingController passwordController;
final VoidCallback onInvalidCredentials;
const LoginButton({
super.key,
required this.loginController,
required this.context,
required this.usernameController,
required this.passwordController,
required this.onInvalidCredentials,
});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
if (loginController.formKey.currentState!.validate()) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Chat(
username: usernameController.text,
password: passwordController.text,
),
),
);
} else {
onInvalidCredentials();
}
},
style: ElevatedButton.styleFrom(
backgroundColor: TColor.darkGray,
elevation: 2,
shadowColor: Colors.black,
padding: const EdgeInsets.symmetric(
horizontal: 40,
vertical: 10,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
),
child: const Icon(
Icons.door_front_door_outlined,
color: Colors.lightBlueAccent,
),
);
}
}
file logo.dart
class LogoApp extends StatelessWidget {
const Logo({super.key});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
ImagesAssset.logoApple,
height: 100,
),
const SizedBox(
width: 20,
),
Image.asset(
ImagesAssset.logoGG,
height: 90,
),
],
);
}
}
file login_text.dart
class OrContinueWith extends StatelessWidget {
const OrContinueWith({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 25.0),
child: Row(
children: [
Expanded(
child: Divider(
thickness: 0.5,
color: Colors.grey[400],
),
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 10.0),
child: Text(
'Or continue with',
style: TextStyle(color: Color(0xFF616161)),
),
),
Expanded(
child: Divider(
thickness: 0.5,
color: Colors.grey[400],
),
),
],
),
);
}
}
Tổng kết
Vừa rồi thì mình đã hướng dẫn các bạn cách xây dựng màn login cho dự án chat realtime. Ở bài viết sau mình sẽ hướng dẫn các bạn khởi tạo dự án Sever.
link github dự án: https://github.com/nguyendao2101/freechat (truy cập branch “new_main” để tham khảo dự án)
Bình luận