一、为什么选择Flutter做登录页?

在移动开发中,登录页是用户接触产品的第一印象。Flutter凭借以下优势成为UI开发的利器:

  • 🚀 高性能渲染:Skia引擎直接绘制,60fps流畅动画
  • 🎨 跨平台一致性:一套代码同时适配iOS/Android/Web
  • 🔥 热重载调试:UI调整实时可见,效率提升50%+
  • 📦 丰富组件库:Material/Cupertino双风格支持

本文基于Flutter 3.19稳定版开发,所有代码均可直接运行


二、核心功能拆解

功能模块 技术要点 实现效果
渐变背景 LinearGradient + BoxDecoration 动态色彩过渡
智能表单 Form + TextFormField 实时验证/错误提示
状态切换 StatefulWidget + isLoading 登录中加载动画
响应式布局 MediaQuery + Padding 适配不同屏幕尺寸

三、代码实战(附详细注释)

1. 创建基础项目

flutter create flutter_login_demo
cd flutter_login_demo

2. 核心文件:lib/main.dart

✅ 步骤1:搭建渐变背景框架
Container(
  decoration: BoxDecoration(
    // 核心:线性渐变背景
    gradient: LinearGradient(
      begin: Alignment.topLeft,    // 起点:左上
      end: Alignment.bottomRight,  // 终点:右下
      colors: [
        Color(0xFF4776E6),  // 蓝色起点
        Color(0xFF8E54E9)   // 紫色终点
      ],
    ),
  ),
  child: SafeArea(  // 避免刘海屏遮挡
    child: Padding(
      padding: const EdgeInsets.all(24.0),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          // 后续添加内容
        ],
      ),
    ),
  ),
)

https://img-blog.csdnimg.cn/direct/3a7b3a6e4b0d4f9c8e0a0b3e3d0c3e3d.png

💡 技巧:使用Color(0xFF...)十六进制写法比Colors.blue更精准控制色彩

✅ 步骤2:实现动态表单验证
Form(
  key: _formKey,  // 表单唯一标识
  child: Column(
    children: [
      // 邮箱输入框
      _buildInputField(
        icon: Icons.email,
        hintText: "邮箱地址",
        validator: (value) {
          if (value == null || value.isEmpty) {
            return "邮箱不能为空";
          }
          if (!RegExp(r"^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\.[a-zA-Z]+").hasMatch(value)) {
            return "邮箱格式错误";
          }
          return null;
        },
      ),
      
      SizedBox(height: 16),
      
      // 密码输入框
      _buildInputField(
        icon: Icons.lock,
        hintText: "密码",
        isPassword: true,
        validator: (value) {
          if (value == null || value.length < 6) {
            return "密码至少6位";
          }
          return null;
        },
      ),
    ],
  ),
)

自定义输入框组件(关键样式封装):

Widget _buildInputField({
  IconData icon,
  String hintText,
  bool isPassword = false,
  FormFieldValidator<String> validator,
}) {
  return TextFormField(
    obscureText: isPassword,  // 密码掩码
    decoration: InputDecoration(
      prefixIcon: Icon(icon, color: Colors.blue[300]),
      hintText: hintText,
      hintStyle: TextStyle(color: Colors.grey[300]),
      // 核心:圆角边框+半透明背景
      filled: true,
      fillColor: Colors.white.withOpacity(0.2),
      border: InputBorder.none,  // 去掉默认边框
      contentPadding: EdgeInsets.symmetric(vertical: 18, horizontal: 20),
    ),
    style: TextStyle(color: Colors.white),
    validator: validator,
  );
}

https://img-blog.csdnimg.cn/direct/5c7b3a6e4b0d4f9c8e0a0b3e3d0c3e3d.png

⚠️ 注意obscureText必须配合isPassword参数使用,避免密码明文显示

✅ 步骤3:实现智能登录按钮
ElevatedButton(
  onPressed: _isLoading ? null : _submitForm,  // 加载中禁用点击
  style: ElevatedButton.styleFrom(
    backgroundColor: Colors.white,
    foregroundColor: Color(0xFF4776E6),
    padding: EdgeInsets.symmetric(vertical: 16),
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(12),  // 圆角按钮
    ),
    elevation: _isLoading ? 0 : 6,  // 加载时去除阴影
  ),
  child: _isLoading
      ? SizedBox(
          width: 24,
          height: 24,
          child: CircularProgressIndicator(
            strokeWidth: 2,
            valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF4776E6)),
          ),
        )
      : Text(
          "登录",
          style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
        ),
)

https://img-blog.csdnimg.cn/direct/7a7b3a6e4b0d4f9c8e0a0b3e3d0c3e3d.png

✅ 步骤4:添加社交登录选项
Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    _buildSocialButton(Icons.facebook, Colors.blue[800]),
    SizedBox(width: 20),
    _buildSocialButton(Icons.g_mobiledata, Colors.red),
  ],
)

// 社交按钮组件
Widget _buildSocialButton(IconData icon, Color color) {
  return Container(
    decoration: BoxDecoration(
      shape: BoxShape.circle,
      border: Border.all(color: Colors.white.withOpacity(0.5), width: 1),
    ),
    child: IconButton(
      icon: Icon(icon, color: Colors.white, size: 28),
      onPressed: () => print("点击${icon.toString()}登录"),
    ),
  );
}

https://img-blog.csdnimg.cn/direct/9a7b3a6e4b0d4f9c8e0a0b3e3d0c3e3d.png


四、完整源码(可直接运行)

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter 登录演示',
      theme: ThemeData(useMaterial3: true),
      home: const LoginPage(),
      debugShowCheckedModeBanner: false,
    );
  }
}

class LoginPage extends StatefulWidget {
  const LoginPage({super.key});

  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final _formKey = GlobalKey<FormState>();
  bool _isLoading = false;

  void _submitForm() {
    if (_formKey.currentState!.validate()) {
      setState(() => _isLoading = true);
      
      // 模拟网络请求
      Future.delayed(const Duration(seconds: 2), () {
        setState(() => _isLoading = false);
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text("登录成功!")),
        );
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        decoration: const BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topLeft,
            end: Alignment.bottomRight,
            colors: [Color(0xFF4776E6), Color(0xFF8E54E9)],
          ),
        ),
        child: SafeArea(
          child: Padding(
            padding: const EdgeInsets.all(24.0),
            child: Form(
              key: _formKey,
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  // 顶部Logo
                  const Icon(Icons.lock, size: 100, color: Colors.white),
                  const SizedBox(height: 10),
                  const Text(
                    "欢迎登录",
                    style: TextStyle(
                      color: Colors.white,
                      fontSize: 24,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 40),
                  
                  // 邮箱输入
                  _buildInputField(
                    icon: Icons.email,
                    hintText: "邮箱地址",
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return "邮箱不能为空";
                      }
                      if (!RegExp(r"^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\.[a-zA-Z]+")
                          .hasMatch(value)) {
                        return "邮箱格式错误";
                      }
                      return null;
                    },
                  ),
                  
                  const SizedBox(height: 16),
                  
                  // 密码输入
                  _buildInputField(
                    icon: Icons.lock,
                    hintText: "密码",
                    isPassword: true,
                    validator: (value) {
                      if (value == null || value.length < 6) {
                        return "密码至少6位";
                      }
                      return null;
                    },
                  ),
                  
                  const SizedBox(height: 24),
                  
                  // 登录按钮
                  SizedBox(
                    width: double.infinity,
                    child: ElevatedButton(
                      onPressed: _isLoading ? null : _submitForm,
                      style: ElevatedButton.styleFrom(
                        backgroundColor: Colors.white,
                        foregroundColor: const Color(0xFF4776E6),
                        padding: const EdgeInsets.symmetric(vertical: 16),
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(12),
                        ),
                        elevation: _isLoading ? 0 : 6,
                      ),
                      child: _isLoading
                          ? SizedBox(
                              width: 24,
                              height: 24,
                              child: CircularProgressIndicator(
                                strokeWidth: 2,
                                valueColor: const AlwaysStoppedAnimation<Color>(
                                    Color(0xFF4776E6)),
                              ),
                            )
                          : const Text(
                              "登录",
                              style: TextStyle(
                                  fontSize: 18, fontWeight: FontWeight.bold),
                            ),
                    ),
                  ),
                  
                  const SizedBox(height: 20),
                  
                  // 社交登录
                  const Text(
                    "第三方登录",
                    style: TextStyle(color: Colors.white70),
                  ),
                  const SizedBox(height: 12),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      _buildSocialButton(Icons.facebook, Colors.blue[800]!),
                      const SizedBox(width: 20),
                      _buildSocialButton(Icons.g_mobiledata, Colors.red),
                    ],
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildInputField({
    required IconData icon,
    required String hintText,
    bool isPassword = false,
    required FormFieldValidator<String> validator,
  }) {
    return TextFormField(
      obscureText: isPassword,
      decoration: InputDecoration(
        prefixIcon: Icon(icon, color: Colors.blue[300]),
        hintText: hintText,
        hintStyle: const TextStyle(color: Colors.grey),
        filled: true,
        fillColor: Colors.white.withOpacity(0.2),
        border: InputBorder.none,
        contentPadding: const EdgeInsets.symmetric(vertical: 18, horizontal: 20),
      ),
      style: const TextStyle(color: Colors.white),
      validator: validator,
    );
  }

  Widget _buildSocialButton(IconData icon, Color color) {
    return Container(
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        border: Border.all(color: Colors.white.withOpacity(0.5), width: 1),
      ),
      child: IconButton(
        icon: Icon(icon, color: Colors.white, size: 28),
        onPressed: () => print("点击${icon.toString()}登录"),
      ),
    );
  }
}

五、关键技巧总结

  1. 渐变色调试
    使用Adobe Color在线生成渐变配色,替换colors数组即可

  2. 输入框焦点效果
    添加onEditingComplete回调实现回车切换焦点:

    onEditingComplete: () => FocusScope.of(context).nextFocus(),
    
  3. 响应式安全区域
    必须使用SafeArea避免刘海屏遮挡关键内容

  4. 加载状态优化
    通过elevation: _isLoading ? 0 : 6实现平滑过渡效果

  5. 表单验证增强
    使用RegExp实现更精准的邮箱/密码验证


六、拓展学习建议

  1. 进阶动画:添加Hero动画实现页面跳转过渡
  2. 状态管理:集成Provider管理全局登录状态
  3. 主题定制:使用ThemeData统一管理颜色/字体
  4. 网络请求:通过dio实现真实登录API对接

源码获取点击此处下载完整项目(记得给个Star哦✨)


最后思考

你觉得登录页还应该包含哪些实用功能?
欢迎在评论区留言讨论,点赞过100将更新注册页+密码找回完整实现!

https://img-blog.csdnimg.cn/direct/1a7b3a6e4b0d4f9c8e0a0b3e3d0c3e3d.png

本文已同步至GitHub精选项目 🌟
关注公众号【Flutter实验室】回复"登录"获取高清设计稿+动效资源
下期预告:《Flutter动画三剑客:Hero/AnimatedContainer/AnimationController实战》

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐