【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
章节内容【03】
flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件
开发背景
接上一篇已经设计好目录,我们需要一步步来对应实现,另外此前注册页面完善样式bug-增加自定义可复用组件widgets
闲话不多,开源仓库地址,可以观摩已经写好的代码:
https://gitee.com/youyacao/ff-flutter
demo下载
实战开始
lib/ ├── main.dart # 应用入口文件 ├── models/ # 数据模型类 ├── screens/ # 页面组件 │ ├── home_screen.dart # 主页 │ ├── login_screen.dart # 登录页面 │ └── register_screen.dart# 注册页面 ├── widgets/ # 可复用的小部件 │ ├── custom_button.dart # 自定义按钮 │ ├── custom_text_field.dart # 自定义文本框 │ └── ... ├── services/ # 网络请求、本地存储等服务 │ ├── api_service.dart # API 请求服务 │ └── storage_service.dart# 本地存储服务 ├── utils/ # 工具类和辅助函数 │ ├── constants.dart # 常量定义 │ ├── logger.dart # 日志记录器配置 │ └── validators.dart # 表单验证逻辑 └── theme/ # 主题配置 ├── app_theme.dart # 主题样式配置
这是上一章我们规划的目录,但是这里明显└── theme/ 我们是用不上的 ,因此我们先把其他的建立起来
第一步,我们做的首页是一个register_screen注册首页,因此我们建立 register_screen.dart文件,然后我们要把main.dart入口文件的内容和注册页面的内容分开,因此
把main文件中只保留入口文件应该有的内容,整个注册页面的内容均放在register_screen.dart页面,并且实现启动app后第一个页面显示为register_screen.dart页面的内容
我们main.dart的内容为
#file:g:\clone\ff-flutter\lib\main.dart import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'screens/register_screen.dart'; // 引入注册页面 void main() { // 初始化日志记录器 Logger.root.level = Level.ALL; Logger.root.onRecord.listen((record) { debugPrint('${record.level.name}: ${record.time}: ${record.message}'); }); runApp(const MainApp()); } class MainApp extends StatelessWidget { const MainApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, // 设置这一属性为 false title: 'freefirend', theme: ThemeData( primarySwatch: Colors.blue, ), home: const RegisterScreen(), // 设置启动页面为 RegisterScreen ); } }
然后我们注册页面的内容为:
#file:g:\clone\ff-flutter\lib\screens\register_screen.dart import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; class RegisterScreen extends StatefulWidget { const RegisterScreen({super.key}); @override State<RegisterScreen> createState() => _RegisterScreenState(); } class _RegisterScreenState extends State<RegisterScreen> { // 示例国家地区号列表 final List<String> countryCodes = ['+1', '+86', '+91', '+44', '+33']; // 默认选择的国家地区号 String selectedCountryCode = '+1'; // Checkbox 状态 bool _agreedToTerms = false; @override Widget build(BuildContext context) { final logger = Logger('RegisterScreen'); logger.info('Building RegisterScreen'); return Scaffold( backgroundColor: const Color(0xFF1E1E1E), // 设置背景色为 #1E1E1E appBar: AppBar( title: const Text( 'Free Friend', style: TextStyle( fontSize: 24.0, // 设置字体大小 fontFamily: 'PingFang SC', // 设置字体为 PingFang SC ), ), centerTitle: true, // 居中标题 ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Text( "Free Friend", style: TextStyle( color: Colors.white, fontSize: 61.87, fontFamily: "PingFang SC", fontWeight: FontWeight.w800, ), ), const SizedBox(height: 16.0), const Text( "Please login your account", style: TextStyle( color: Colors.white, fontSize: 32, fontFamily: "PingFang SC", fontWeight: FontWeight.w800, ), ), const SizedBox(height: 16.0), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, // 使用 spaceBetween 对齐方式 children: [ Flexible( flex: 1, // 给 DropdownButtonFormField 分配一部分空间 child: DropdownButtonFormField<String>( value: selectedCountryCode, onChanged: (String? newValue) { if (newValue != null) { setState(() { selectedCountryCode = newValue; }); } }, items: countryCodes.map<DropdownMenuItem<String>>((String value) { return DropdownMenuItem<String>( value: value, child: Text(value), ); }).toList(), decoration: const InputDecoration( labelText: '选择国家地区号', border: OutlineInputBorder(), ), style: const TextStyle(fontSize: 16), // 设置字体大小 ), ), const SizedBox(width: 8.0), Expanded( flex: 2, // 给 TextField 分配更多的空间 child: TextField( decoration: const InputDecoration( labelText: '请输入手机号', border: OutlineInputBorder(), hintStyle: TextStyle(color: Color(0xffa9a9a9)), ), keyboardType: TextInputType.phone, ), ), ], ), const SizedBox(height: 16.0), TextField( decoration: const InputDecoration( labelText: '请输入密码', hintStyle: TextStyle(color: Color(0xffa9a9a9)), border: OutlineInputBorder(), ), obscureText: true, ), Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ Checkbox( value: _agreedToTerms, onChanged: (bool? value) { setState(() { _agreedToTerms = value ?? false; }); }, ), const SizedBox(width: 20), Text( "You agree to our Terms", style: TextStyle( color: Colors.white, fontSize: 32, fontFamily: "PingFang SC", fontWeight: FontWeight.w500, ), ), ], ), const SizedBox(height: 24.0), ElevatedButton( onPressed: () { // 注册按钮点击事件 logger.info('注册按钮被点击'); }, style: ElevatedButton.styleFrom( backgroundColor: Color(0xffe7568c), // 设置红色背景 fixedSize: Size(630, 48), // 设置按钮宽度为 630 ), child: Text( 'Register', style: TextStyle( color: Colors.white, // 设置文字颜色为白色 fontSize: 16.0, // 可以根据需要调整字体大小 ), ), ), const SizedBox(height: 8.0), Expanded( child: Align( alignment: Alignment.bottomCenter, child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( "Already have an account?", style: TextStyle( color: Colors.white, fontSize: 32, fontFamily: "PingFang SC", fontWeight: FontWeight.w800, ), ), const SizedBox(height: 8.0), OutlinedButton( onPressed: () { // 登录按钮点击事件 logger.info('登录按钮被点击'); }, style: OutlinedButton.styleFrom( side: BorderSide(color: Colors.white, width: 2), // 设置边框颜色为白色 fixedSize: Size(630, 48), // 设置按钮宽度为 630 backgroundColor: Colors.transparent, // 去掉背景色 ), child: Text( 'login', style: TextStyle( color: Colors.white, // 设置文字颜色为白色 fontSize: 16.0, // 可以根据需要调整字体大小 ), ), ), ], ), ), ), ], ), ), ); } }
这里注意下
@override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, // 设置这一属性为 false title: 'freefirend', theme: ThemeData( primarySwatch: Colors.blue, ), home: const RegisterScreen(), // 设置启动页面为 RegisterScreen ); }
main中这里定义了第一个启动的页面,我们目前只有注册页面因此这里定制注册页,后续是肯定要改的,按照常规用户习惯,第一个界面应该是直播列表页面或者短视频列表页面才对。
很完美,不过我们发现最顶部的dubug标识消失了,但是debug内容没隐藏,因此
注释掉这段代码即可,因为这里是有用的有些页面看不出来是什么的时候。
我们注意到,两处细节要处理,第一处是地区选择这里不够显眼,第二处是输入手机号这里颜色也不对不显眼,
请输入手机号输入内容时候的文字需要改成 FFFFFF 颜色,把选择国家区号部分的数字也设置为FFFFFF颜色
我们需要对 register_screen.dart 文件进行相应的修改。具体步骤如下:
修改“请输入手机号”输入框的文字颜色:
在 TextField 的 style 属性中设置文字颜色为 FFFFFF。
修改“选择国家地区号”部分的数字颜色:
在 DropdownButtonFormField 的 style 属性中设置文字颜色为 FFFFFF。
decoration: const InputDecoration( labelText: '选择国家地区号', border: OutlineInputBorder(), ), style: const TextStyle( fontSize: 16, color: Colors.white, // 设置文字颜色为 FFFFFF ), ), ), const SizedBox(width: 8.0), Expanded( flex: 2, // 给 TextField 分配更多的空间 child: TextField( decoration: const InputDecoration( labelText: '请输入手机号', border: OutlineInputBorder(), hintStyle: TextStyle(color: Color(0xffa9a9a9)), ), style: const TextStyle(color: Colors.white), // 设置文字颜色为 FFFFFF keyboardType: TextInputType.phone, ), ),
也就是这一段代码了,
查看效果,很棒,但是这里还有问题,既然我们这个登录按钮和注册按钮在其他页面也能用的上,我们为什么不建立为自定义组件呢,因此这里我们需要复用,就要建立自定义组件,
诸多地方都要用的上,因此我们开始,等等,还发现个小问题,选择区号的,背景图颜色不对
那么修改以下代码:
decoration: const InputDecoration( labelText: '选择国家地区号', border: OutlineInputBorder(), ), style: const TextStyle( fontSize: 16, color: Colors.white, // 设置文字颜色为 FFFFFF ), dropdownColor: const Color(0xFF1E1E1E), // 设置弹窗背景色为 #1E1E1E ), ),
细节到位,
在组件目录下我们建立blackbutton.dart 黑色按钮 和 pinkbutton.dart 粉色按钮
先扩展知识学一下
在Flutter中,自定义组件非常重要,可以提高代码的复用性和可维护性。将自定义组件放在 widgets
目录下,可以更好地组织代码,使项目结构更加清晰。这里是如何利用 widgets
目录来自定义和复用组件的示例。
widgets
目录的作用
widgets
目录通常用于存放自定义的小部件,这些小部件可能在应用的多个地方使用。通过将自定义组件集中在一个目录下,可以方便地管理和查找它们。
如何自定义复用组件
以下是一个简单的步骤,来创建和复用自定义组件:
- 创建一个新的 Dart 文件:在
widgets
目录下创建一个新的 Dart 文件,例如custom_button.dart
。 - 定义自定义组件:在新文件中定义自定义组件。
- 在需要使用的地方导入和使用该组件。
示范:
lib/ |-- widgets/ | |-- custom_button.dart
自定义组件内容:
// lib/widgets/custom_button.dart import 'package:flutter/material.dart'; class CustomButton extends StatelessWidget { final String text; final VoidCallback onPressed; CustomButton({required this.text, required this.onPressed}); @override Widget build(BuildContext context) { return ElevatedButton( onPressed: onPressed, child: Text(text), ); } }
调用自定义组件:
import 'package:flutter/material.dart'; import 'package:ff_social_app/widgets/custom_button.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar( title: Text('自定义组件示例'), ), body: Center( child: CustomButton( text: '点击我', onPressed: () { print('按钮被点击了'); }, ), ), ), ); } }
我们需要把注册按钮和登录按钮做成自定义组件方便复用,因此把Register按钮的做成自定义组件并且放在widgets目录下的pinkbutton.dart文件内,把logo登录按钮做成自定义组件并且放在widgets目录下的blackbutton.dart文件,需要保留目前的样式,并且在原register_screen.dart文件中 原位置中正常调用组件。
粉色按钮自定义组件内容:
#file:g:\clone\ff-flutter\lib\widgets\pinkbutton.dart import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; class PinkButton extends StatelessWidget { final String label; final VoidCallback onPressed; const PinkButton({ required this.label, required this.onPressed, super.key, }); @override Widget build(BuildContext context) { final logger = Logger('PinkButton'); logger.info('Building PinkButton'); return ElevatedButton( onPressed: onPressed, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xffe7568c), // 设置红色背景 fixedSize: const Size(630, 48), // 设置按钮宽度为 630 ), child: Text( label, style: const TextStyle( color: Colors.white, // 设置文字颜色为白色 fontSize: 16.0, // 可以根据需要调整字体大小 ), ), ); } }
黑色按钮自定义组件内容:
#file:g:\clone\ff-flutter\lib\widgets\blackbutton.dart import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; class BlackButton extends StatelessWidget { final String label; final VoidCallback onPressed; const BlackButton({ required this.label, required this.onPressed, super.key, }); @override Widget build(BuildContext context) { final logger = Logger('BlackButton'); logger.info('Building BlackButton'); return OutlinedButton( onPressed: onPressed, style: OutlinedButton.styleFrom( side: const BorderSide(color: Colors.white, width: 2), // 设置边框颜色为白色 fixedSize: const Size(630, 48), // 设置按钮宽度为 630 backgroundColor: Colors.transparent, // 去掉背景色 ), child: Text( label, style: const TextStyle( color: Colors.white, // 设置文字颜色为白色 fontSize: 16.0, // 可以根据需要调整字体大小 ), ), ); } }
然后我们在注册页引用这两个组件,在此同时,我们就需要简化我们原先已经写好的,避免重复,代码如下:
import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:ff_flutter/lib/widgets/pinkbutton.dart'; // 引入 PinkButton import 'package:ff_flutter/lib/widgets/blackbutton.dart'; // 引入 BlackButton class RegisterScreen extends StatefulWidget { const RegisterScreen({super.key}); @override State<RegisterScreen> createState() => _RegisterScreenState(); } class _RegisterScreenState extends State<RegisterScreen> { // 示例国家地区号列表 final List<String> countryCodes = ['+1', '+86', '+91', '+44', '+33']; // 默认选择的国家地区号 String selectedCountryCode = '+1'; // Checkbox 状态 bool _agreedToTerms = false; @override Widget build(BuildContext context) { final logger = Logger('RegisterScreen'); logger.info('Building RegisterScreen'); return Scaffold( backgroundColor: const Color(0xFF1E1E1E), // 设置背景色为 #1E1E1E // appBar: AppBar( // title: const Text( // 'Free Friend', // style: TextStyle( // fontSize: 24.0, // 设置字体大小 // fontFamily: 'PingFang SC', // 设置字体为 PingFang SC // ), // ), // centerTitle: true, // 居中标题 // ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Text( "Free Friend", style: TextStyle( color: Colors.white, fontSize: 61.87, fontFamily: "PingFang SC", fontWeight: FontWeight.w800, ), ), const SizedBox(height: 16.0), const Text( "Please login your account", style: TextStyle( color: Colors.white, fontSize: 32, fontFamily: "PingFang SC", fontWeight: FontWeight.w800, ), ), const SizedBox(height: 16.0), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, // 使用 spaceBetween 对齐方式 children: [ Flexible( flex: 1, // 给 DropdownButtonFormField 分配一部分空间 child: DropdownButtonFormField<String>( value: selectedCountryCode, onChanged: (String? newValue) { if (newValue != null) { setState(() { selectedCountryCode = newValue; }); } }, items: countryCodes.map<DropdownMenuItem<String>>((String value) { return DropdownMenuItem<String>( value: value, child: Text( value, style: const TextStyle(color: Colors.white), // 设置文字颜色为 FFFFFF ), ); }).toList(), decoration: const InputDecoration( labelText: '选择国家地区号', border: OutlineInputBorder(), ), style: const TextStyle( fontSize: 16, color: Colors.white, // 设置文字颜色为 FFFFFF ), dropdownColor: const Color(0xFF1E1E1E), // 设置弹窗背景色为 #1E1E1E ), ), const SizedBox(width: 8.0), Expanded( flex: 2, // 给 TextField 分配更多的空间 child: TextField( decoration: const InputDecoration( labelText: '请输入手机号', border: OutlineInputBorder(), hintStyle: TextStyle(color: Color(0xffa9a9a9)), ), style: const TextStyle(color: Colors.white), // 设置输入文字颜色为 FFFFFF keyboardType: TextInputType.phone, ), ), ], ), const SizedBox(height: 16.0), TextField( decoration: const InputDecoration( labelText: '请输入密码', hintStyle: TextStyle(color: Color(0xffa9a9a9)), border: OutlineInputBorder(), ), obscureText: true, style: const TextStyle(color: Colors.white), // 设置输入文字颜色为 FFFFFF ), Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ Checkbox( value: _agreedToTerms, onChanged: (bool? value) { setState(() { _agreedToTerms = value ?? false; }); }, ), const SizedBox(width: 20), Text( "You agree to our Terms", style: TextStyle( color: Colors.white, fontSize: 32, fontFamily: "PingFang SC", fontWeight: FontWeight.w500, ), ), ], ), const SizedBox(height: 24.0), PinkButton( label: 'Register', onPressed: () { // 注册按钮点击事件 logger.info('注册按钮被点击'); }, ), const SizedBox(height: 8.0), Expanded( child: Align( alignment: Alignment.bottomCenter, child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( "Already have an account?", style: TextStyle( color: Colors.white, fontSize: 32, fontFamily: "PingFang SC", fontWeight: FontWeight.w800, ), ), const SizedBox(height: 8.0), BlackButton( label: 'login', onPressed: () { // 登录按钮点击事件 logger.info('登录按钮被点击'); }, ), ], ), ), ), ], ), ), ); } }
本文完成,本文对组件进行了完善修改,并且创建了自定义组件,其次还规划了我们整体目录并且创建了对应文件,下一篇我们即可把所有的注册页面写完,毕竟自定义组件都创建了,写几个页面还不是分分钟的事情?