前言:
语法与Java很像,所以大多地方我就不注释了,有问题随时讨论,一起学习一起进步,不要只能靠卷。
一、主页(main.dart)改造
main.dart是执行的主文件,生成空项目时就存在,我们首先把它原先的代码全部删除,然后让他指向登录页面(LoginPage),上代码:
import 'package:flutter/material.dart';
import 'page/login_page.dart'; // 登录页面
void main() {
runApp(MyApp())
}
// StatelessWidget 用于那些状态不变的Widget
// StatefulWidget 用于那些状态可能会改变的Widget,并且需要管理其状态。
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Test Demo',
theme: ThemeData(
primarySwatch: Colors.blue, // 主题颜色
),
home: const LoginPage(), // 直接打开登录页
);
}
}
其中:
- StatelessWidget 用于那些状态不变的Widget
- StatefulWidget 用于那些状态可能会改变的Widget,并且需要管理其状态
二、登录页(login_page.dart)
在lib文件夹下新建一个page文件夹,在page文件夹下新建一个页面:login_page.dart。先上效果图吧,毕竟我也是根据这个图设计的。
功能拆分:
- 登录页(我这边用的标题栏展示此功能,所以要额外设置标题栏颜色与字体颜色大小)
- App测试(这个没啥好说的,Text搞定)
- 中间的白色部分(使用Container包裹即可,css直接写)
- 图片展示(我使用的svg图片,中间遇到了问题,后面再说)
- 输入框与按钮(这个也没啥好说的)
- 登录验证(我这边仅进行最简单的空值验证并做出提示)
- 登录数据存储
- 首页跳转
需要准备的引用插件:
- 引用文件是:pubspec.yaml
- 支持svg展示的插件:flutter_svg: ^2.0.10
- 图片引用:
- 插件引用之后需要执行命令安装生效,直接打开 Ternimal 输入 flutter pub get 即可
下面直接上代码:
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'home_page.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _formKey = GlobalKey<FormState>();
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
final FocusNode _usernameFocusNode = FocusNode();
final FocusNode _passwordFocusNode = FocusNode();
String? _usernameError;
String? _passwordError;
bool _isObscure = true;
final _borderRadius = 10.0; // 圆角角度
@override
void initState() {
super.initState();
_usernameFocusNode.addListener(() {
if (!_usernameFocusNode.hasFocus) {
_validateUsername();
}
});
_passwordFocusNode.addListener(() {
if (!_passwordFocusNode.hasFocus) {
_validatePassword();
}
});
}
// 用户验证
void _validateUsername() {
setState(() {
if (_usernameController.text.isEmpty) {
_usernameError = "用户名不能为空";
} else {
_usernameError = null;
}
});
}
// 密码验证
void _validatePassword() {
setState(() {
if (_passwordController.text.isEmpty) {
_passwordError = "密码不能为空";
} else {
_passwordError = null;
}
});
}
// 登录点击事件
void _login() async {
if (_formKey.currentState?.validate() ?? false) {
_validateUsername();
_validatePassword();
final userName = _usernameController.text;
final password = _passwordController.text;
if (_usernameError == null && _passwordError == null) {
// 底部消息弹框
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$userName 已登录')),
);
// 登录数据存储
final prefs = await SharedPreferences.getInstance();
prefs.setString('userName', userName);
// 跳转到下一个页面
// Navigator.push(
// context,
// MaterialPageRoute(builder: (context) => HomePage(userName: username)),
// );
// 跳转后替换整个导航堆栈,无返回按钮
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (context) => HomePage(userName: userName),
),
(route) => false, // 移除前置路由
);
}
}
}
// hex颜色
Color _hexToColor(String hexString) {
final buffer = StringBuffer();
if (hexString.length == 7 || hexString.length == 9) buffer.write('FF');
buffer.write(hexString.replaceFirst('#', ''));
return Color(int.parse(buffer.toString(), radix: 16));
}
@override
Widget build(BuildContext context) {
// 获取屏幕的宽度
// double screenWidth = MediaQuery.of(context).size.width;
return Scaffold(
appBar: AppBar(
title: Text(
"登录页",
style: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 14,
)
),
backgroundColor: _hexToColor('#037cfd'), // 标题栏颜色
foregroundColor: Colors.white,
centerTitle: true,
),
backgroundColor: _hexToColor('#037cfd'), // 修改外部背景颜色
body: Padding(
padding: const EdgeInsets.all(40.0), // 设置内边距
child: Column(
// mainAxisAlignment: MainAxisAlignment.center, // 垂直居中
// crossAxisAlignment: CrossAxisAlignment.center, // 水平居中
children: [
Text(
"App测试",
style: const TextStyle(fontSize: 30, fontWeight: FontWeight.bold, color: Colors.white),
),
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.all(16.0),
// width: screenWidth * 0.75, // 宽度设置0.75
decoration: BoxDecoration(
color: Colors.white, // 内部颜色
borderRadius: BorderRadius.circular(8.0),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
spreadRadius: 5,
blurRadius: 7,
offset: const Offset(0, 3),
),
],
),
child: Form(
key: _formKey,
child: Column(
children: [
SvgPicture.asset(
'assets/images/login.svg'
),
const SizedBox(height: 16),
TextFormField(
controller: _usernameController,
focusNode: _usernameFocusNode, // 失焦事件
style: const TextStyle(fontSize: 12, color: Colors.black87), //文字大小、颜色
//输入组件
decoration: InputDecoration(
labelText: "用户名",
// hintText: 'Username',
errorText: _usernameError,
prefixIcon: Icon(Icons.person, color: _hexToColor('#7ba8c0')),
fillColor: Colors.grey[80], //背景颜色,
filled: true, //必须设置为true,fillColor才有效
isCollapsed: true, //相当于高度包裹的意思,必须设置为true
contentPadding: const EdgeInsets.symmetric(
horizontal: 15, vertical: 15), //内容内边距
enabledBorder: OutlineInputBorder(
//设置输入框可编辑时的边框样式
borderRadius: BorderRadius.circular(_borderRadius), //输入框圆角
borderSide:
const BorderSide(color: Colors.white, width: 0) //输入框白色隐藏
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(_borderRadius), //输入框圆角
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(_borderRadius), //输入框圆角
borderSide: BorderSide(color: _hexToColor('#7ba8c0'), width: 2.0),
),
),
// validator: (value) {
// if (value == null || value.isEmpty) {
// return 'Please enter your username';
// }
// return null;
// },
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
focusNode: _passwordFocusNode, // 失焦事件
obscureText: _isObscure,
decoration: InputDecoration(
labelText: "密码",
errorText: _passwordError,
prefixIcon: Icon(Icons.lock, color: _hexToColor('#7ba8c0'),),
fillColor: Colors.grey[80], //背景颜色,必须结合filled: true,才有效
filled: true, //必须设置为true,fillColor才有效
isCollapsed: true, //相当于高度包裹的意思,必须设置为true
contentPadding: const EdgeInsets.symmetric(
horizontal: 15, vertical: 15), //内容内边距,影响高度
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(_borderRadius), //输入框圆角
borderSide: const BorderSide(color: Colors.white, width: 0) //输入框白色隐藏
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(_borderRadius), //输入框圆角
borderSide: const BorderSide(color: Colors.white, width: 0),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(_borderRadius), //输入框圆角
borderSide: BorderSide(color: _hexToColor('#7ba8c0'), width: 2.0),
),
suffixIcon: IconButton(
icon: Icon(
!_isObscure ? Icons.visibility : Icons.visibility_off,
),
onPressed: () {
setState(() {
_isObscure = !_isObscure;
});
},
),
),
// validator: (value) {
// if (value == null || value.isEmpty) {
// return 'Please enter your password';
// }
// return null;
// },
),
const SizedBox(height: 24),
ElevatedButton(
// onPressed: () {
// if (_formKey.currentState?.validate() ?? false) {
// // Perform login action
// ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar(content: Text('Logging in...')),
// );
// }
// },
onPressed: _login,
style: ElevatedButton.styleFrom(
backgroundColor: _hexToColor('#037cfd'), //修改按钮背景色
foregroundColor: Colors.white, //文本的颜色
minimumSize: const Size(double.infinity, 50), // 修改按钮大小
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(_borderRadius), // 修改按钮圆角
),
),
child: Text("登录"),
),
],
),
),
),
],
),
),
);
}
}
注意点:
flutter使用svg库时提示The <style> element is not implemented in this library。
这是svg中带有style标签导致的,我们使用的插件不支持,去除style即可,我采用的是这位大佬的方式:flutter使用svg库时提示The <style> element is not implemented in this library的解决方法 - yongfengnice - 博客园 (cnblogs.com)
三、首页(home_page.dart)
先上效果图:
功能拆分:
- 这个真没啥好说的,主要就是配置页面使用子组件
代码:
import 'package:flutter/material.dart';
import 'profile_page.dart';
class HomePage extends StatefulWidget {
final String userName;
HomePage({required this.userName});
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int _selectedIndex = 0; // 默认选第一个
final List<Widget> _pages = [
Center(child: Text('主页')),
Center(child: Text('搜索页')),
ProfilePage(), // 配置页面子组件
];
// 选中事件
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('欢迎, ${widget.userName}'),
),
body: _pages[_selectedIndex],
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: '主页',
),
BottomNavigationBarItem(
icon: Icon(Icons.search),
label: '搜索',
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: '配置',
),
],
currentIndex: _selectedIndex,
selectedItemColor: Colors.blue, // 选中颜色
unselectedItemColor: Colors.grey, // 未选中颜色
onTap: _onItemTapped, // 选中事件
),
);
}
}
四、配置页面(profile_page.dart)展示与退出登录
照例先上效果图(比较简陋。。。):
功能拆分:
- 用户头像有就展示,没有展示一个默认icon做头像(默认图片头像的代码在注释里面)
- 退出登录
上代码吧:
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'login_page.dart';
class ProfilePage extends StatefulWidget {
@override
_ProfilePageState createState() => _ProfilePageState();
}
class _ProfilePageState extends State<ProfilePage> {
final String? _profilePictureUrl = null; // 头像
String _userName = ''; // 用户名
final String _userId = 'TEST-001'; // 工号
final String _address = 'XX市 XX区 XXX'; // 地址
@override
void initState() {
super.initState();
_loadUserInfo();
}
// 获取用户信息
_loadUserInfo() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
_userName = prefs.getString('userName') ?? '未知';
});
}
// 退出登录
void _logout(BuildContext context) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const LoginPage(),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// 用户头像
// CircleAvatar(
// radius: 50,
// backgroundImage: _profilePictureUrl != null
// ? NetworkImage(_profilePictureUrl)
// : const AssetImage('assets/default_avatar.png') as ImageProvider, // 默认图片头像
// backgroundColor: Colors.grey[200], // 设置头像背景色
// ),
CircleAvatar(
radius: 50,
backgroundColor: Colors.grey[200], // 设置头像背景色
child: _profilePictureUrl != null && _profilePictureUrl.isNotEmpty
? ClipOval(
child: Image.network(
_profilePictureUrl,
fit: BoxFit.cover,
width: 100,
height: 100,
),
)
: Icon(
Icons.person,
size: 50,
color: Colors.grey[700], // 设置图标颜色
),
),
const SizedBox(height: 16),
// 用户姓名
Text(
_userName,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
// 用户识别码(ID)
Text(
'工号: $_userId',
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
),
),
const SizedBox(height: 16),
// 用户地址
Text(
'地址: $_address',
style: const TextStyle(
fontSize: 16,
),
),
const Spacer(), // 占据剩余的空间
// 退出登录按钮
ElevatedButton(
onPressed: () => _logout(context),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red, //修改按钮背景色
foregroundColor: Colors.white, //文本的颜色
minimumSize: const Size(double.infinity, 50), // 修改按钮大小
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0), // 修改按钮圆角
),
),
child: Text('退出登录'),
),
],
),
),
);
}
}
结语:
至此结束,感谢观看。
下一节为国际化配置,也是我目前已完成的最终进度。后续有什么想要研究实现的可以留言讨论一起研究哈。
PS:这三篇写完我就继续黑吗喽了,这次就不立flag了