Flutter电商实战:从零开发通用型登录页面
在电商类移动应用中,登录页面是用户触达应用核心功能的关键入口,其交互体验、功能完整性直接影响用户的初始使用感受与留存率。本文基于Flutter 3.x技术栈,结合跨平台开发实战经验,从零实现一款适配电商场景的通用型登录页面,涵盖手机号验证、60秒验证码倒计时、防重复登录、协议勾选、第三方登录等核心功能,同时解决开发过程中常见的倒计时异常、输入框遮挡、按钮重复点击等问题,完成从数据模型设计到个人中心集成的全流程开发,实现登录功能的高可用与高体验。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

- Flutter电商实战:从零开发通用型登录页面
-
- 一、项目基础说明
-
- [1.1 技术栈与项目结构](#1.1 技术栈与项目结构)
- [1.2 核心功能需求](#1.2 核心功能需求)
- [1.3 整体开发流程](#1.3 整体开发流程)
- 二、数据模型与API接口封装
-
- [2.1 用户信息数据模型](#2.1 用户信息数据模型)
- [2.2 登录相关API接口封装](#2.2 登录相关API接口封装)
- 三、登录页面UI完整实现
-
- [3.1 页面状态管理初始化](#3.1 页面状态管理初始化)
- [3.2 核心UI组件实现](#3.2 核心UI组件实现)
- [3.3 核心交互逻辑实现](#3.3 核心交互逻辑实现)
- 四、个人中心业务集成
-
- [4.1 未登录状态展示](#4.1 未登录状态展示)
- [4.2 登录成功后数据处理](#4.2 登录成功后数据处理)
- 五、开发常见问题及解决方案
- 六、项目优化建议
-
- [6.1 功能优化](#6.1 功能优化)
- [6.2 性能与体验优化](#6.2 性能与体验优化)
- [6.3 合规性优化](#6.3 合规性优化)
- 七、总结
- 八、参考资料
一、项目基础说明
1.1 技术栈与项目结构
- Flutter版本:3.x
- 项目整体架构:底部导航栏+多页面架构(首页、分类、购物车、个人中心)
- 核心依赖:Dio(网络请求)、原生状态管理(StatefulWidget)
- 开发目标:实现高内聚、低耦合的电商用户登录功能,支持后续跨平台适配与功能扩展
1.2 核心功能需求
本次开发的登录页面需满足电商场景的基础登录诉求,所有功能模块均做合理性校验与交互优化,具体功能要求如下:
| 功能模块 | 核心要求 |
|---|---|
| 手机号输入 | 仅支持11位数字输入,做格式合法性校验,非11位手机号禁止获取验证码 |
| 验证码输入 | 6位纯数字输入,与手机号绑定完成登录校验 |
| 验证码倒计时 | 点击获取后启动60秒倒计时,期间按钮置灰不可点击,防止重复获取 |
| 登录按钮 | 防重复点击,点击后显示加载状态;协议未勾选/信息未填时按钮置灰 |
| 协议勾选 | 必选项,未勾选时无法触发登录操作,适配合规性要求 |
| 第三方登录 | 提供微信、QQ第三方快捷登录入口,适配电商用户多样化登录习惯 |
1.3 整体开发流程
遵循需求分析→数据模型设计→API接口封装→UI页面实现→状态管理→业务集成→问题调试的开发流程,保证代码的规范性与开发的逻辑性,具体流程如下:
需求分析 → 数据模型设计 → API接口封装 → 登录页面UI开发 → 个人中心业务集成 → 状态管理与交互优化 → 问题排查与测试
二、数据模型与API接口封装
2.1 用户信息数据模型
针对电商场景的用户基础信息需求,设计轻量型UserInfo数据模型,包含用户ID、手机号、昵称、头像等核心字段,支持JSON与模型的相互转换,便于登录后数据的存储与传递。
文件路径:lib/viewmodels/user.dart
dart
// 电商用户基础信息模型
class UserInfo {
final String id; // 用户唯一ID
final String? nickname; // 用户昵称(可选,第三方登录可能暂未设置)
final String? avatar; // 用户头像(可选)
final String phone; // 绑定手机号(必选)
UserInfo({
required this.id,
this.nickname,
this.avatar,
required this.phone,
});
// 从JSON解析为UserInfo模型
factory UserInfo.fromJSON(Map<String, dynamic> json) {
return UserInfo(
id: json['id'] ?? '',
nickname: json['nickname'],
avatar: json['avatar'],
phone: json['phone'] ?? '',
);
}
// 将UserInfo模型转换为JSON
Map<String, dynamic> toJSON() {
return {
'id': id,
'nickname': nickname,
'avatar': avatar,
'phone': phone,
};
}
}
2.2 登录相关API接口封装
基于Dio实现网络请求的封装,抽离登录场景的核心接口:获取验证码、手机号+验证码登录、退出登录,为便于本地测试,所有接口均添加模拟延迟,实际项目中可直接替换为真实后端接口地址与参数。
文件路径:lib/api/login.dart
dart
import 'package:dio/dio.dart';
import 'package:your_project_name/utils/DioRequest.dart'; // 自定义Dio配置
import 'package:your_project_name/viewmodels/user.dart'; // 导入用户信息模型
/// 获取短信验证码接口
/// [phone]:用户输入的11位手机号
Future<void> getVerifyCodeAPI(String phone) async {
// 实际项目替换为真实接口
// await dioRequest.post('/auth/send-sms-code', data: {'phone': phone});
// 模拟网络请求延迟500ms
await Future.delayed(const Duration(milliseconds: 500));
}
/// 手机号+验证码登录接口
/// [phone]:用户手机号
/// [code]:6位验证码
/// 返回值:UserInfo用户信息模型
Future<UserInfo> loginAPI(String phone, String code) async {
// 实际项目替换为真实接口
// final result = await dioRequest.post('/auth/phone-login', data: {'phone': phone, 'code': code});
// return UserInfo.fromJSON(result.data);
// 模拟网络请求延迟1s
await Future.delayed(const Duration(seconds: 1));
// 返回模拟用户数据,实际项目删除此部分
return UserInfo(
id: '100001',
nickname: 'Flutter电商用户',
avatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=200',
phone: phone,
);
}
/// 退出登录接口
Future<void> logoutAPI() async {
// 实际项目替换为真实接口
// await dioRequest.post('/auth/logout');
// 模拟网络请求延迟500ms
await Future.delayed(const Duration(milliseconds: 500));
}
三、登录页面UI完整实现
登录页面采用Scaffold+SafeArea+SingleChildScrollView 的布局结构,适配不同屏幕尺寸,同时解决键盘弹出时的页面遮挡问题;整体配色以橙色(主色值0xFFFF6B00)为主色调,契合电商应用的视觉风格,文件路径为lib/pages/login/index.dart。
3.1 页面状态管理初始化
使用Flutter原生StatefulWidget实现页面状态管理,初始化输入框控制器、状态标识、倒计时变量等核心数据,统一管理页面所有交互状态。
dart
import 'package:flutter/material.dart';
import 'package:your_project_name/api/login.dart';
import 'package:your_project_name/viewmodels/user.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
// 输入框控制器
final TextEditingController _phoneController = TextEditingController();
final TextEditingController _codeController = TextEditingController();
// 状态标识
bool _agreeToTerms = false; // 是否同意用户协议
bool _isLoggingIn = false; // 是否正在登录(防止重复点击)
int _countdown = 0; // 验证码倒计时秒数
late Timer _countdownTimer; // 倒计时定时器
// 页面主色调
final Color _primaryColor = const Color(0xFFFF6B00);
// 页面销毁时释放资源
@override
void dispose() {
_phoneController.dispose();
_codeController.dispose();
if (_countdown > 0) {
_countdownTimer.cancel(); // 销毁定时器,防止内存泄漏
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 40),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildLogoAndTitle(), // Logo与标题区域
const SizedBox(height: 48),
_buildPhoneInput(), // 手机号输入框
const SizedBox(height: 16),
_buildCodeInput(), // 验证码输入框+倒计时按钮
const SizedBox(height: 24),
_buildLoginButton(), // 登录按钮
const SizedBox(height: 20),
_buildTermsCheck(), // 协议勾选框
const SizedBox(height: 40),
_buildThirdPartyLogin(), // 第三方登录区域
],
),
),
),
);
}
}
3.2 核心UI组件实现
(1)Logo与标题区域
简洁的视觉入口,包含应用Logo与登录标题,提升页面辨识度。
dart
// Logo与标题
Widget _buildLogoAndTitle() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: _primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
image: const DecorationImage(
image: NetworkImage('https://img-blog.csdnimg.cn/20240105112827959.png'),
fit: BoxFit.contain,
),
),
),
const SizedBox(height: 16),
const Text(
'欢迎登录',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const Text(
'输入手机号即可快速登录',
style: TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
],
);
}
(2)手机号输入框
限制11位数字输入,隐藏默认字符计数器,添加电话图标前缀,优化输入体验;背景采用浅灰色,提升视觉层次感。
dart
// 手机号输入框
Widget _buildPhoneInput() {
return Container(
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
)
],
),
child: TextField(
controller: _phoneController,
keyboardType: TextInputType.phone,
maxLength: 11,
style: const TextStyle(fontSize: 16),
decoration: const InputDecoration(
hintText: '请输入11位手机号',
hintStyle: TextStyle(color: Colors.grey, fontSize: 16),
prefixIcon: Icon(Icons.phone_outlined, color: Colors.grey),
counterText: '', // 隐藏字符计数
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 18),
),
onChanged: (value) {
setState(() {}); // 输入变化刷新页面,适配按钮状态
},
),
);
}
(3)验证码输入框+倒计时按钮
采用行布局,左侧为6位数字验证码输入框,右侧为倒计时按钮;按钮根据倒计时状态自动置灰/激活,点击后触发验证码获取与倒计时逻辑。
dart
// 验证码输入框+倒计时按钮
Widget _buildCodeInput() {
bool isPhoneValid = _isValidPhone(_phoneController.text);
return Row(
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
)
],
),
child: TextField(
controller: _codeController,
keyboardType: TextInputType.number,
maxLength: 6,
style: const TextStyle(fontSize: 16),
enabled: isPhoneValid, // 手机号不合法时输入框置灰
decoration: const InputDecoration(
hintText: '请输入6位验证码',
hintStyle: TextStyle(color: Colors.grey, fontSize: 16),
prefixIcon: Icon(Icons.lock_outline, color: Colors.grey),
counterText: '',
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 18),
),
onChanged: (value) {
setState(() {});
},
),
),
),
const SizedBox(width: 12),
SizedBox(
width: 120,
height: 56,
child: ElevatedButton(
onPressed: (_countdown > 0 || !isPhoneValid) ? null : _getVerifyCode,
style: ElevatedButton.styleFrom(
backgroundColor: _primaryColor,
disabledBackgroundColor: Colors.grey[300],
foregroundColor: Colors.white,
disabledForegroundColor: Colors.white.withOpacity(0.6),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
padding: EdgeInsets.zero,
),
child: Text(
_countdown > 0 ? '${_countdown}s' : '获取验证码',
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
),
),
],
);
}
(4)登录按钮
实现防重复点击与加载状态,当手机号不合法/验证码未填/协议未勾选/正在登录时,按钮自动置灰;点击后触发登录逻辑,显示加载动画。
dart
// 登录按钮
Widget _buildLoginButton() {
bool canLogin = _isValidPhone(_phoneController.text) &&
_codeController.text.length == 6 &&
_agreeToTerms &&
!_isLoggingIn;
return SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton(
onPressed: canLogin ? _handleLogin : null,
style: ElevatedButton.styleFrom(
backgroundColor: _primaryColor,
disabledBackgroundColor: Colors.grey[300],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
padding: EdgeInsets.zero,
),
child: _isLoggingIn
? const CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
)
: const Text(
'立即登录',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500),
),
),
);
}
(5)协议勾选与第三方登录
协议勾选为必选组件,关联登录按钮状态;第三方登录提供微信、QQ图标入口,适配电商用户快捷登录需求,样式简洁统一。
dart
// 协议勾选
Widget _buildTermsCheck() {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Checkbox(
value: _agreeToTerms,
onChanged: (value) {
setState(() {
_agreeToTerms = value ?? false;
});
},
activeColor: _primaryColor,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
Expanded(
child: RichText(
text: TextSpan(
style: const TextStyle(fontSize: 12, color: Colors.grey),
children: [
const TextSpan(text: '我已阅读并同意'),
TextSpan(
text: '《用户服务协议》',
style: TextStyle(color: _primaryColor),
),
const TextSpan(text: '和'),
TextSpan(
text: '《隐私政策》',
style: TextStyle(color: _primaryColor),
),
],
),
),
),
],
);
}
// 第三方登录
Widget _buildThirdPartyLogin() {
return Column(
children: [
const Text(
'其他登录方式',
style: TextStyle(fontSize: 12, color: Colors.grey),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 微信登录
_buildThirdPartyItem(Icons.wechat, Colors.green),
const SizedBox(width: 40),
// QQ登录
_buildThirdPartyItem(Icons.question_answer, Colors.blue),
],
),
],
);
}
// 第三方登录子项
Widget _buildThirdPartyItem(IconData icon, Color color) {
return GestureDetector(
onTap: () {
// 第三方登录逻辑,实际项目自行实现
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${icon.name}登录功能开发中')),
);
},
child: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(24),
),
child: Icon(icon, color: color, size: 24),
),
);
}
3.3 核心交互逻辑实现
(1)手机号合法性校验
封装手机号校验方法,仅允许11位纯数字手机号通过校验,作为获取验证码与登录的前置条件。
dart
// 手机号合法性校验
bool _isValidPhone(String phone) {
if (phone.length != 11) return false;
// 正则匹配11位手机号(以13/14/15/16/17/18/19开头)
final RegExp phoneReg = RegExp(r'^1[3-9]\d{9}$');
return phoneReg.hasMatch(phone);
}
(2)验证码倒计时逻辑
点击获取验证码后,启动60秒倒计时,通过Timer实现秒数递减,页面实时刷新;倒计时结束后自动重置按钮状态,同时做定时器判空,防止页面销毁后定时器继续运行。
dart
// 获取验证码并启动倒计时
Future<void> _getVerifyCode() async {
String phone = _phoneController.text;
try {
// 调用获取验证码API
await getVerifyCodeAPI(phone);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('验证码发送成功,请注意查收')),
duration: const Duration(seconds: 2),
);
// 启动60秒倒计时
setState(() {
_countdown = 60;
});
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
setState(() {
_countdown--;
if (_countdown <= 0) {
timer.cancel(); // 倒计时结束销毁定时器
}
});
});
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('验证码发送失败:$e')),
backgroundColor: Colors.red,
);
}
}
(3)登录核心逻辑
点击登录后,先做本地参数校验,再调用登录API;请求过程中置为"正在登录"状态,防止重复点击;请求成功后返回用户信息并跳转至个人中心,失败则提示错误信息。
dart
// 处理登录逻辑
Future<void> _handleLogin() async {
String phone = _phoneController.text;
String code = _codeController.text;
try {
// 设置登录状态,防止重复点击
setState(() {
_isLoggingIn = true;
});
// 调用登录API
UserInfo userInfo = await loginAPI(phone, code);
// 登录成功,返回上一页并传递用户信息
if (mounted) {
Navigator.pop(context, userInfo);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('登录成功')),
);
}
} catch (e) {
// 登录失败,提示错误
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('登录失败:$e'), backgroundColor: Colors.red),
);
}
} finally {
// 无论成功失败,重置登录状态
if (mounted) {
setState(() {
_isLoggingIn = false;
});
}
}
}
四、个人中心业务集成
登录功能最终需与个人中心联动,实现未登录状态显示登录入口→点击跳转登录页→登录成功后返回个人中心并刷新用户信息的完整业务流程,核心实现逻辑如下:
4.1 未登录状态展示
在个人中心页面判断用户登录状态,若未登录则显示登录/注册入口,点击后跳转至登录页面。
dart
// 个人中心未登录状态
Widget _buildUnLoginWidget() {
return GestureDetector(
onTap: () async {
// 跳转登录页,并接收返回的用户信息
UserInfo? userInfo = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const LoginPage()),
);
// 登录成功,刷新用户信息
if (userInfo != null) {
setState(() {
_currentUser = userInfo; // _currentUser为个人中心的用户信息变量
});
}
},
child: Container(
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(vertical: 40),
child: const Column(
children: [
Icon(Icons.person_outline, size: 60, color: Colors.grey),
SizedBox(height: 16),
Text(
'请登录账号',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500),
),
SizedBox(height: 8),
Text(
'登录后可查看我的订单、收藏等功能',
style: TextStyle(fontSize: 14, color: Colors.grey),
),
],
),
),
);
}
4.2 登录成功后数据处理
登录页面通过Navigator.pop(context, userInfo)将用户信息回传至个人中心,个人中心接收后更新本地状态,刷新页面展示用户头像、昵称、手机号等信息,完成登录状态的同步。
五、开发常见问题及解决方案
在登录页面开发过程中,会遇到多个交互与性能问题,以下为电商场景中最常见的5个问题及针对性解决方案,保证页面的稳定性与体验性。
问题1:验证码倒计时重复触发
问题原因 :快速多次点击"获取验证码"按钮,导致多个定时器同时运行,倒计时混乱。
解决方案 :按钮点击条件添加_countdown > 0判断,倒计时期间按钮置灰不可点击;同时在获取验证码方法中做前置校验,确保同一时间仅能启动一个定时器。
问题2:页面销毁后倒计时继续运行
问题原因 :定时器未及时销毁,导致页面销毁后仍在执行,造成内存泄漏。
解决方案 :在dispose生命周期中判空并销毁定时器;同时在定时器回调与网络请求中添加mounted判断,确保页面未销毁时再执行状态更新。
问题3:登录按钮重复点击
问题原因 :网络请求存在延迟,用户多次点击按钮导致多次调用登录API。
解决方案 :添加_isLoggingIn状态标识,点击后立即置为true,按钮置灰;请求结束(成功/失败)后再置为false,恢复按钮状态。
问题4:TextField字符计数器遮挡
问题原因 :Flutter默认的字符计数器会占用输入框底部空间,导致布局拥挤,影响视觉体验。
解决方案 :设置counterText: '',隐藏默认字符计数器;通过maxLength限制输入长度,结合输入框提示文字引导用户输入正确位数。
问题5:键盘弹出遮挡输入框
问题原因 :键盘弹出后,页面高度不足,输入框被键盘遮挡,无法看到输入内容。
解决方案 :使用SingleChildScrollView包裹页面所有内容,键盘弹出时页面可滚动;同时使用SafeArea避开系统状态栏与底部导航栏,保证布局适配。
六、项目优化建议
6.1 功能优化
- 添加密码登录方式,适配不同用户登录习惯;
- 增加验证码过期提醒 与重新获取功能,提升容错性;
- 实现记住手机号功能,基于SharedPreferences存储,下次登录自动填充;
- 添加图形验证码校验,防止短信验证码被恶意刷取。
6.2 性能与体验优化
- 替换原生StatefulWidget为Provider/BLoC状态管理,便于跨页面共享登录状态;
- 对输入框做防抖处理,减少不必要的页面刷新;
- 添加输入框焦点管理,手机号输入完成后自动聚焦至验证码输入框;
- 优化网络请求,添加请求拦截器 与错误统一处理,提升代码可维护性。
6.3 合规性优化
- 协议勾选添加单独点击跳转功能,适配合规性要求;
- 增加隐私政策弹窗,首次打开应用时强制展示;
- 对用户手机号做脱敏处理(如138****1234),保护用户隐私。
七、总结
本文基于Flutter 3.x实现了电商场景下的通用型登录页面,完成了数据模型设计、API接口封装、UI页面开发、状态管理、业务集成的全流程开发,同时解决了倒计时异常、按钮重复点击、输入框遮挡等开发常见问题。
本登录页面具备高可用、高体验、高可扩展的特点,所有功能均做了合理性校验与交互优化,可直接适配电商、社交、工具类等各类Flutter跨平台应用;同时代码结构清晰,模块划分明确,便于后续的功能扩展与跨平台适配(如鸿蒙、iOS、Android)。
在实际项目开发中,可根据业务需求在此基础上做个性化定制,重点关注状态管理的合理性、网络请求的稳定性、用户体验的流畅性 与合规性要求,打造更符合产品定位的登录页面。
八、参考资料
- Flutter官方文档:https://flutter.dev/docs
- Dio网络请求官方文档:https://pub.dev/packages/dio
- Flutter跨平台开发实战:鸿蒙与Flutter混合开发指南
- 电商APP登录页面设计规范与交互体验白皮书
✨ 坚持用 清晰的图解 +易懂的硬件架构 + 硬件解析, 让每个知识点都 简单明了 !
🚀 个人主页 :一只大侠的侠 · CSDN
💬 座右铭 : "所谓成功就是以自己的方式度过一生。"

