第8讲:构建一个完整的登录页面实战

综合运用布局Widget,打造一个美观实用的登录界面。

你好,欢迎回到《Flutter入门到精通》专栏。通过前七讲的学习,我们已经掌握了Flutter的核心概念和主要布局Widget。今天,我们将通过构建一个完整的登录页面,把所有的知识点串联起来,让你体验真实的开发流程。

一、项目分析与设计

在开始编码之前,让我们先规划一下登录页面需要包含哪些元素:

页面组成元素

  1. 顶部区域:Logo和应用名称

  2. 表单区域

    • 用户名/邮箱输入框

    • 密码输入框(带显示/隐藏切换)

    • 记住我选项

    • 忘记密码链接

  3. 按钮区域

    • 登录按钮

    • 第三方登录选项

    • 注册链接

  4. 装饰元素

    • 背景设计

    • 间距和边框

布局结构规划

text

Column (整个页面滚动布局)
├── SizedBox (顶部间距)
├── Logo区域
├── SizedBox (间距)
├── 表单Card
│   ├── 标题文本
│   ├── 邮箱输入框
│   ├── 密码输入框
│   ├── Row (记住我和忘记密码)
│   └── 登录按钮
├── 分割线
├── 第三方登录按钮
└── 注册链接

二、逐步实现登录页面

让我们一步一步地构建这个登录页面。

步骤1:创建基础页面结构

首先创建一个新的Dart文件 login_page.dart

dart

import 'package:flutter/material.dart';

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

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

class _LoginPageState extends State<LoginPage> {
  // 控制输入框的控制器
  final TextEditingController _emailController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();
  
  // 状态变量
  bool _rememberMe = false;
  bool _obscurePassword = true; // 控制密码是否明文显示

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.grey[50],
      body: SafeArea(
        child: SingleChildScrollView(
          padding: const EdgeInsets.all(24.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              // 这里将逐步添加各个组件
            ],
          ),
        ),
      ),
    );
  }
}

步骤2:实现Logo和标题区域

在Column的children中添加Logo区域:

dart

// 返回按钮和Logo区域
_buildHeader(context),
SizedBox(height: 40),

// 欢迎文本
_buildWelcomeText(),
SizedBox(height: 32),

然后实现对应的私有方法:

dart

// 构建头部区域
Widget _buildHeader(BuildContext context) {
  return Row(
    children: [
      // 返回按钮
      Container(
        width: 40,
        height: 40,
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(12),
          boxShadow: [
            BoxShadow(
              color: Colors.grey.withOpacity(0.1),
              blurRadius: 8,
              offset: const Offset(0, 2),
            ),
          ],
        ),
        child: IconButton(
          icon: const Icon(Icons.arrow_back, size: 20),
          onPressed: () => Navigator.of(context).pop(),
        ),
      ),
      const Spacer(),
      // Logo
      Container(
        width: 50,
        height: 50,
        decoration: BoxDecoration(
          color: Colors.blue,
          borderRadius: BorderRadius.circular(12),
        ),
        child: const Icon(
          Icons.flutter_dash,
          color: Colors.white,
          size: 30,
        ),
      ),
      const Spacer(),
      const SizedBox(width: 40), // 用于平衡布局的占位空间
    ],
  );
}

// 构建欢迎文本
Widget _buildWelcomeText() {
  return const Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Text(
        '欢迎回来',
        style: TextStyle(
          fontSize: 28,
          fontWeight: FontWeight.bold,
          color: Colors.black87,
        ),
      ),
      SizedBox(height: 8),
      Text(
        '请登录您的账户继续使用',
        style: TextStyle(
          fontSize: 16,
          color: Colors.grey,
        ),
      ),
    ],
  );
}

步骤3:构建表单卡片

现在创建主要的表单区域:

dart

// 表单卡片
_buildLoginForm(),
SizedBox(height: 24),

实现表单构建方法:

dart

Widget _buildLoginForm() {
  return Container(
    padding: const EdgeInsets.all(24),
    decoration: BoxDecoration(
      color: Colors.white,
      borderRadius: BorderRadius.circular(20),
      boxShadow: [
        BoxShadow(
          color: Colors.grey.withOpacity(0.1),
          blurRadius: 15,
          offset: const Offset(0, 5),
        ),
      ],
    ),
    child: Column(
      children: [
        // 邮箱输入框
        _buildEmailField(),
        const SizedBox(height: 20),
        
        // 密码输入框
        _buildPasswordField(),
        const SizedBox(height: 16),
        
        // 记住我和忘记密码
        _buildRememberAndForgot(),
        const SizedBox(height: 24),
        
        // 登录按钮
        _buildLoginButton(),
      ],
    ),
  );
}

步骤4:实现表单字段

现在实现各个表单字段的详细构建方法:

dart

// 构建邮箱输入框
Widget _buildEmailField() {
  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      const Text(
        '邮箱地址',
        style: TextStyle(
          fontSize: 14,
          fontWeight: FontWeight.w500,
          color: Colors.black87,
        ),
      ),
      const SizedBox(height: 8),
      Container(
        decoration: BoxDecoration(
          color: Colors.grey[50],
          borderRadius: BorderRadius.circular(12),
          border: Border.all(color: Colors.grey[300]!),
        ),
        child: TextField(
          controller: _emailController,
          keyboardType: TextInputType.emailAddress,
          decoration: const InputDecoration(
            prefixIcon: Icon(Icons.email_outlined, color: Colors.grey),
            border: InputBorder.none,
            hintText: '请输入您的邮箱',
            contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 16),
          ),
        ),
      ),
    ],
  );
}

// 构建密码输入框
Widget _buildPasswordField() {
  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      const Text(
        '密码',
        style: TextStyle(
          fontSize: 14,
          fontWeight: FontWeight.w500,
          color: Colors.black87,
        ),
      ),
      const SizedBox(height: 8),
      Container(
        decoration: BoxDecoration(
          color: Colors.grey[50],
          borderRadius: BorderRadius.circular(12),
          border: Border.all(color: Colors.grey[300]!),
        ),
        child: TextField(
          controller: _passwordController,
          obscureText: _obscurePassword,
          decoration: InputDecoration(
            prefixIcon: const Icon(Icons.lock_outline, color: Colors.grey),
            suffixIcon: IconButton(
              icon: Icon(
                _obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined,
                color: Colors.grey,
              ),
              onPressed: () {
                setState(() {
                  _obscurePassword = !_obscurePassword;
                });
              },
            ),
            border: InputBorder.none,
            hintText: '请输入您的密码',
            contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
          ),
        ),
      ),
    ],
  );
}

// 构建记住我和忘记密码行
Widget _buildRememberAndForgot() {
  return Row(
    children: [
      // 记住我复选框
      Row(
        children: [
          SizedBox(
            width: 24,
            height: 24,
            child: Checkbox(
              value: _rememberMe,
              onChanged: (value) {
                setState(() {
                  _rememberMe = value!;
                });
              },
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(4),
              ),
            ),
          ),
          const SizedBox(width: 8),
          const Text(
            '记住我',
            style: TextStyle(fontSize: 14, color: Colors.grey),
          ),
        ],
      ),
      const Spacer(),
      // 忘记密码链接
      GestureDetector(
        onTap: () {
          // 处理忘记密码逻辑
          _showForgotPasswordDialog();
        },
        child: const Text(
          '忘记密码?',
          style: TextStyle(
            fontSize: 14,
            color: Colors.blue,
            fontWeight: FontWeight.w500,
          ),
        ),
      ),
    ],
  );
}

步骤5:实现登录按钮和其他操作区域

dart

// 构建登录按钮
Widget _buildLoginButton() {
  return SizedBox(
    width: double.infinity,
    child: ElevatedButton(
      onPressed: _performLogin,
      style: ElevatedButton.styleFrom(
        backgroundColor: Colors.blue,
        foregroundColor: Colors.white,
        padding: const EdgeInsets.symmetric(vertical: 16),
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(12),
        ),
        elevation: 0,
      ),
      child: const Text(
        '登录',
        style: TextStyle(
          fontSize: 16,
          fontWeight: FontWeight.w600,
        ),
      ),
    ),
  );
}

// 构建第三方登录和注册区域
Widget _buildThirdPartyAndRegister() {
  return Column(
    children: [
      // 分割线
      _buildDivider(),
      const SizedBox(height: 24),
      
      // 第三方登录
      _buildThirdPartyLogin(),
      const SizedBox(height: 32),
      
      // 注册链接
      _buildRegisterLink(),
    ],
  );
}

// 构建分割线
Widget _buildDivider() {
  return Row(
    children: [
      Expanded(
        child: Container(height: 1, color: Colors.grey[300]),
      ),
      Padding(
        padding: const EdgeInsets.symmetric(horizontal: 16),
        child: Text(
          '或使用其他方式登录',
          style: TextStyle(
            fontSize: 14,
            color: Colors.grey[500],
          ),
        ),
      ),
      Expanded(
        child: Container(height: 1, color: Colors.grey[300]),
      ),
    ],
  );
}

// 构建第三方登录按钮
Widget _buildThirdPartyLogin() {
  return Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      _buildThirdPartyButton(
        icon: Icons.g_mobiledata,
        onPressed: () => _loginWithGoogle(),
      ),
      const SizedBox(width: 16),
      _buildThirdPartyButton(
        icon: Icons.apple,
        onPressed: () => _loginWithApple(),
      ),
      const SizedBox(width: 16),
      _buildThirdPartyButton(
        icon: Icons.wechat,
        onPressed: () => _loginWithWechat(),
      ),
    ],
  );
}

// 构建单个第三方登录按钮
Widget _buildThirdPartyButton({
  required IconData icon,
  required VoidCallback onPressed,
}) {
  return Container(
    width: 50,
    height: 50,
    decoration: BoxDecoration(
      color: Colors.white,
      borderRadius: BorderRadius.circular(12),
      border: Border.all(color: Colors.grey[300]!),
    ),
    child: IconButton(
      icon: Icon(icon, color: Colors.grey[600]),
      onPressed: onPressed,
    ),
  );
}

// 构建注册链接
Widget _buildRegisterLink() {
  return Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      const Text(
        '还没有账户?',
        style: TextStyle(fontSize: 14, color: Colors.grey),
      ),
      const SizedBox(width: 4),
      GestureDetector(
        onTap: () {
          // 跳转到注册页面
          _navigateToRegister();
        },
        child: const Text(
          '立即注册',
          style: TextStyle(
            fontSize: 14,
            color: Colors.blue,
            fontWeight: FontWeight.w600,
          ),
        ),
      ),
    ],
  );
}

步骤6:实现业务逻辑方法

最后,实现所有的业务逻辑方法:

dart

// 执行登录操作
void _performLogin() {
  final email = _emailController.text.trim();
  final password = _passwordController.text;

  // 简单的表单验证
  if (email.isEmpty || password.isEmpty) {
    _showErrorDialog('请输入邮箱和密码');
    return;
  }

  if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(email)) {
    _showErrorDialog('请输入有效的邮箱地址');
    return;
  }

  // 显示加载指示器
  showDialog(
    context: context,
    barrierDismissible: false,
    builder: (context) => const Center(
      child: CircularProgressIndicator(),
    ),
  );

  // 模拟网络请求
  Future.delayed(const Duration(seconds: 2), () {
    Navigator.of(context).pop(); // 关闭加载指示器
    
    // 模拟登录成功
    if (email == 'test@example.com' && password == '123456') {
      _showSuccessDialog();
    } else {
      _showErrorDialog('邮箱或密码错误');
    }
  });
}

// 显示成功对话框
void _showSuccessDialog() {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Row(
        children: [
          Icon(Icons.check_circle, color: Colors.green),
          SizedBox(width: 8),
          Text('登录成功'),
        ],
      ),
      content: const Text('欢迎回来!您已成功登录。'),
      actions: [
        TextButton(
          onPressed: () => Navigator.of(context).pop(),
          child: const Text('确定'),
        ),
      ],
    ),
  );
}

// 显示错误对话框
void _showErrorDialog(String message) {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Row(
        children: [
          Icon(Icons.error_outline, color: Colors.red),
          SizedBox(width: 8),
          Text('登录失败'),
        ],
      ),
      content: Text(message),
      actions: [
        TextButton(
          onPressed: () => Navigator.of(context).pop(),
          child: const Text('确定'),
        ),
      ],
    ),
  );
}

// 显示忘记密码对话框
void _showForgotPasswordDialog() {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('忘记密码'),
      content: const Text('我们将向您的注册邮箱发送密码重置链接。'),
      actions: [
        TextButton(
          onPressed: () => Navigator.of(context).pop(),
          child: const Text('取消'),
        ),
        TextButton(
          onPressed: () {
            Navigator.of(context).pop();
            // 这里可以添加发送重置邮件的逻辑
          },
          child: const Text('发送'),
        ),
      ],
    ),
  );
}

// 第三方登录方法
void _loginWithGoogle() {
  // 实现Google登录逻辑
}

void _loginWithApple() {
  // 实现Apple登录逻辑
}

void _loginWithWechat() {
  // 实现微信登录逻辑
}

void _navigateToRegister() {
  // 跳转到注册页面
}

三、在main.dart中使用登录页面

修改 lib/main.dart 来使用我们的登录页面:

dart

import 'package:flutter/material.dart';
import 'login_page.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(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const LoginPage(), // 设置登录页面为首页
    );
  }
}

四、功能亮点总结

通过这个完整的登录页面,我们实践了:

  1. 复杂布局:综合运用了Column、Row、Stack、Container、SizedBox等布局Widget

  2. 表单处理:使用TextEditingController管理输入框状态

  3. 交互反馈:实现密码显示/隐藏、复选框、按钮点击等交互

  4. 状态管理:使用setState管理页面状态

  5. 对话框:显示加载指示器、成功/错误提示

  6. 样式美化:使用BoxDecoration创建卡片阴影、圆角等视觉效果

结语

恭喜!你已经成功构建了一个功能完整、界面美观的登录页面。这个项目综合运用了前面所有课程的知识点,是你Flutter学习之路上的一个重要里程碑。

在下一讲中,我们将开始学习如何在Flutter中展示动态数据,重点学习 ListView和GridView 的使用,让你能够构建可以滚动和网格布局的列表界面。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

移动端开发者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值