🔥Flutter 鸿蒙化身份认证实战!用户登录体系全流程开发与状态持久化(macOS+DevEco Studio)

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net


📄 文章摘要

本文基于已完成 Flutter for OpenHarmony 全功能开发的跨平台应用,完整记录了大一新生在 macOS 环境下,使用鸿蒙官方 IDE DevEco Studio,从零到一实现用户登录身份认证体系的全流程。文章涵盖登录页面 UI 设计、dio 网络请求身份验证、登录状态全局管理、结合 shared_preferences 的状态持久化、设置页面登出功能等核心模块,同时分享了开发过程中的关键注意事项。所有代码均已在 OpenHarmony 模拟器完成全流程运行验证,内容结构清晰、代码可直接复用,既符合开源鸿蒙征文规范,也针对搜索引擎 SEO 和大模型搜索做了结构化优化,适合所有 Flutter 鸿蒙化开发新手参考学习。


📋 文章目录

1 📝 前言
2 🎯 任务目标与技术方案设计
3 📦 核心代码分步实现
4 ✅ OpenHarmony 设备运行验证
5 💡 核心技术要点与开发心得
6 🎯 全文总结


📝 前言

我是一名大一新生,全程使用 macOS 电脑 + 鸿蒙官方 IDE DevEco Studio 完成本次开发!在前几篇实战文章中,我已经完整完成了老师要求的全部基础开发任务:dio 网络请求接入、列表下拉刷新 / 上拉加载、底部选项卡多页面实现、全场景动效集成、应用白屏修复、本地存储与主题 / 语言切换、深色模式适配、完整国际化(i18n)实现,项目已经形成了非常完善的业务闭环!
本次是一个全新的任务:为开源鸿蒙跨平台应用集成完整的用户身份认证能力。这意味着应用不再是一个 “谁都能进” 的展示型应用,而是有了基础的用户门槛和用户体系。
本次开发的核心内容包括:设计一个美观的登录页面、使用 dio 进行登录验证、管理登录状态、实现状态持久化(重启应用不用重新登录)、以及在设置页面添加登出功能。
本文将完整记录我从需求分析、方案设计、代码实现到设备验证的全过程,所有实现步骤和代码都可直接复用,和我一样的新手小白跟着做就能实现一个完整的登录体系❗❗❗


🎯 二、任务目标与技术方案设计

1. 本次任务核心目标
根据老师的要求,本次用户登录功能需要实现以下核心目标:
✅ UI 设计:设计一个简洁美观的登录页面,包含用户名输入框、密码输入框(支持明文 / 密文切换)、登录按钮
✅ 身份验证:使用已集成的 dio 库发送 POST 请求,与后端交互进行身份验证(本次使用模拟接口逻辑)
✅ 状态管理:实现登录状态的全局管理,未登录时显示登录页,登录成功后进入主页面
✅ 持久化:结合之前集成的 shared_preferences,实现登录状态持久化,重启应用后自动保持登录状态
✅ 登出功能:在设置页面添加登出按钮,点击后清除登录状态并返回登录页面
✅ 国际化适配:登录页面和登出功能的所有文字支持简体中文 / 英文切换
2. 技术方案选型
基于项目已有的技术栈,我设计了以下轻量级但完整的技术方案:
技术方案选型

3. 核心流程设计
核心业务流程如下:
应用启动:读取本地存储的登录状态
状态判断:
已登录 -> 直接进入主页面(用户列表)
未登录 -> 显示登录页面
登录流程:
用户输入账号密码 -> 点击登录 -> 验证通过 -> 保存状态到本地 -> 进入主页面
登出流程:
用户进入设置页 -> 点击登出 -> 确认 -> 清除本地状态 -> 返回登录页面


📦 三、核心代码分步实现

实现步骤 1:扩展本地化资源(i18n)
为了保证登录功能也支持中英文切换,首先扩展lib/utils/localization.dart文件,添加登录相关的所有文字👇

class AppLocalizations {
  final String languageCode;
  AppLocalizations(this.languageCode);
  static AppLocalizations of(String languageCode) => AppLocalizations(languageCode);

  // ... 原有文字保持不变 ...

  /// ------------------------------ 登录页面 (Login) ------------------------------
  String get loginTitle {
    switch (languageCode) {
      case 'en': return 'Login';
      case 'zh': default: return '登录';
    }
  }

  String get username {
    switch (languageCode) {
      case 'en': return 'Username';
      case 'zh': default: return '用户名';
    }
  }

  String get password {
    switch (languageCode) {
      case 'en': return 'Password';
      case 'zh': default: return '密码';
    }
  }

  String get loginButton {
    switch (languageCode) {
      case 'en': return 'Sign In';
      case 'zh': default: return '登录';
    }
  }

  String get pleaseEnterUsername {
    switch (languageCode) {
      case 'en': return 'Please enter your username';
      case 'zh': default: return '请输入用户名';
    }
  }

  String get pleaseEnterPassword {
    switch (languageCode) {
      case 'en': return 'Please enter your password';
      case 'zh': default: return '请输入密码';
    }
  }

  String get testAccountHint {
    switch (languageCode) {
      case 'en': return 'Test Account: admin / 123456';
      case 'zh': default: return '测试账号:admin / 123456';
    }
  }

  String get loginFailed {
    switch (languageCode) {
      case 'en': return 'Login failed. Wrong username or password.';
      case 'zh': default: return '登录失败,用户名或密码错误。';
    }
  }

  String get networkError {
    switch (languageCode) {
      case 'en': return 'Network Error';
      case 'zh': default: return '网络错误';
    }
  }

  /// ------------------------------ 登出功能 (Logout) ------------------------------
  String get logout {
    switch (languageCode) {
      case 'en': return 'Logout';
      case 'zh': default: return '退出登录';
    }
  }

  String get logoutConfirmTitle {
    switch (languageCode) {
      case 'en': return 'Confirm Logout';
      case 'zh': default: return '确认退出';
    }
  }

  String get logoutConfirmContent {
    switch (languageCode) {
      case 'en': return 'Are you sure you want to log out?';
      case 'zh': default: return '确定要退出当前账号吗?';
    }
  }

  String get cancel {
    switch (languageCode) {
      case 'en': return 'Cancel';
      case 'zh': default: return '取消';
    }
  }
}

实现步骤 2:扩展本地存储服务(StorageService)
接下来扩展lib/services/storage_service.dart,增加登录状态和用户名的持久化能力👇

import 'package:shared_preferences/shared_preferences.dart';

class StorageService {
  static final StorageService instance = StorageService._internal();
  factory StorageService() => instance;
  StorageService._internal();

  static late SharedPreferences _prefs;
  static bool _isInitialized = false;

  // ... 原有键名保持不变 ...
  // 【新增】登录状态与用户名的键
  static const String _keyLoginStatus = 'is_logged_in';
  static const String _keyUsername = 'saved_username';

  // ... 原有初始化、主题、语言方法保持不变 ...

  // ------------------------------ 登录状态管理 ------------------------------
  /// 保存登录状态
  static Future<bool> saveLoginStatus(bool isLoggedIn) async {
    if (!_isInitialized) return false;
    try {
      return await _prefs.setBool(_keyLoginStatus, isLoggedIn);
    } catch (e) {
      print("❌ [Storage] Save login status failed: $e");
      return false;
    }
  }

  /// 获取登录状态,默认false
  static bool getLoginStatus() {
    if (!_isInitialized) return false;
    try {
      return _prefs.getBool(_keyLoginStatus) ?? false;
    } catch (e) {
      print("❌ [Storage] Get login status failed: $e");
      return false;
    }
  }

  /// 保存用户名
  static Future<bool> saveUsername(String username) async {
    if (!_isInitialized) return false;
    try {
      return await _prefs.setString(_keyUsername, username);
    } catch (e) {
      print("❌ [Storage] Save username failed: $e");
      return false;
    }
  }

  /// 获取用户名
  static String? getUsername() {
    if (!_isInitialized) return null;
    try {
      return _prefs.getString(_keyUsername);
    } catch (e) {
      print("❌ [Storage] Get username failed: $e");
      return null;
    }
  }

  /// 登出:清除所有用户相关数据
  static Future<bool> logout() async {
    if (!_isInitialized) return false;
    try {
      await _prefs.remove(_keyLoginStatus);
      await _prefs.remove(_keyUsername);
      return true;
    } catch (e) {
      print("❌ [Storage] Logout failed: $e");
      return false;
    }
  }
}

实现步骤 3:创建登录页面(LoginPage)
这是本次开发的核心。在lib/screens目录下新建login_page.dart,实现完整的登录 UI 与逻辑👇

import 'package:flutter/material.dart';
import '../services/storage_service.dart';
import '../utils/localization.dart';

class LoginPage extends StatefulWidget {
  const LoginPage({
    super.key,
    required this.loc,
    required this.onLoginSuccess,
  });

  final AppLocalizations loc;
  final VoidCallback onLoginSuccess;

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

class _LoginPageState extends State<LoginPage> {
  final _formKey = GlobalKey<FormState>();
  final _usernameController = TextEditingController();
  final _passwordController = TextEditingController();
  
  bool _isLoading = false;
  bool _obscurePassword = true; // 控制密码是否可见

  // 执行登录
  Future<void> _handleLogin() async {
    if (!_formKey.currentState!.validate()) return;

    setState(() => _isLoading = true);

    // 模拟网络请求延迟
    await Future.delayed(const Duration(milliseconds: 800));

    try {
      // 这里替换为你的真实后端接口
      // 本次使用模拟逻辑:账号 admin,密码 123456
      final username = _usernameController.text.trim();
      final password = _passwordController.text.trim();

      if (username == 'admin' && password == '123456') {
        // 1. 持久化状态
        await StorageService.saveLoginStatus(true);
        await StorageService.saveUsername(username);
        
        // 2. 回调通知主应用刷新
        if (mounted) widget.onLoginSuccess();
      } else {
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text(widget.loc.loginFailed), backgroundColor: Colors.red),
          );
        }
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('${widget.loc.networkError}: $e'), backgroundColor: Colors.red),
        );
      }
    } finally {
      if (mounted) setState(() => _isLoading = false);
    }
  }

  
  void dispose() {
    _usernameController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Theme.of(context).scaffoldBackgroundColor,
      body: SafeArea(
        child: SingleChildScrollView(
          padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 60),
          child: Form(
            key: _formKey,
            child: Column(
              children: [
                // Logo区域
                const Icon(Icons.flutter_dash, size: 100, color: Colors.blueAccent),
                const SizedBox(height: 20),
                Text(
                  widget.loc.appName,
                  style: Theme.of(context).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold),
                ),
                const SizedBox(height: 60),

                // 用户名输入框
                TextFormField(
                  controller: _usernameController,
                  decoration: InputDecoration(
                    labelText: widget.loc.username,
                    prefixIcon: const Icon(Icons.person_outline),
                    border: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
                  ),
                  validator: (value) => (value == null || value.isEmpty) ? widget.loc.pleaseEnterUsername : null,
                  enabled: !_isLoading,
                ),
                const SizedBox(height: 20),

                // 密码输入框
                TextFormField(
                  controller: _passwordController,
                  obscureText: _obscurePassword,
                  decoration: InputDecoration(
                    labelText: widget.loc.password,
                    prefixIcon: const Icon(Icons.lock_outline),
                    suffixIcon: IconButton(
                      icon: Icon(_obscurePassword ? Icons.visibility_off : Icons.visibility),
                      onPressed: () => setState(() => _obscurePassword = !_obscurePassword),
                    ),
                    border: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
                  ),
                  validator: (value) => (value == null || value.isEmpty) ? widget.loc.pleaseEnterPassword : null,
                  enabled: !_isLoading,
                ),
                const SizedBox(height: 30),

                // 登录按钮
                SizedBox(
                  width: double.infinity,
                  height: 50,
                  child: ElevatedButton(
                    onPressed: _isLoading ? null : _handleLogin,
                    style: ElevatedButton.styleFrom(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))),
                    child: _isLoading
                        ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
                        : Text(widget.loc.loginButton, style: const TextStyle(fontSize: 16)),
                  ),
                ),
                
                const SizedBox(height: 20),
                // 测试账号提示
                Text(widget.loc.testAccountHint, style: Theme.of(context).textTheme.bodySmall),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

实现步骤 4:修改主应用入口(main.dart)
最后修改lib/main.dart,将登录状态提升到应用入口,根据状态决定显示哪个页面,并实现登出逻辑👇

import 'package:flutter/material.dart';
// ... 其他导入保持不变 ...
import 'screens/login_page.dart'; // 【新增】导入登录页

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await StorageService.init();
  // 注意:这里不要强制logout,否则每次重启都要重登。
  // 测试时如果想清除状态,可以手动调用:await StorageService.logout();
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});
  
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  bool _isDarkMode = false;
  String _currentLanguage = 'zh';
  Key _appKey = UniqueKey();
  
  // 【新增】登录状态
  bool _isLoggedIn = false;

  
  void initState() {
    super.initState();
    _loadThemeFromStorage();
    _loadLanguageFromStorage();
    // 【新增】读取登录状态
    _isLoggedIn = StorageService.getLoginStatus();
  }

  // ... 原有主题、语言方法保持不变 ...

  // 【新增】登录成功回调
  void _onLoginSuccess() {
    setState(() {
      _isLoggedIn = true;
    });
  }

  // 【新增】登出回调
  void _onLogout() async {
    await StorageService.logout();
    setState(() {
      _isLoggedIn = false;
      _appKey = UniqueKey(); // 刷新整个应用,清除页面缓存
    });
  }

  
  Widget build(BuildContext context) {
    final loc = AppLocalizations.of(_currentLanguage);
    
    return KeyedSubtree(
      key: _appKey,
      child: MaterialApp(
        // ... 主题配置保持不变 ...
        // 【关键】根据状态显示不同页面
        home: _isLoggedIn
            ? MainPage(
                loc: loc,
                // ... 其他参数保持不变 ...
                onLogout: _onLogout, // 传递登出回调
              )
            : LoginPage(
                loc: loc,
                onLoginSuccess: _onLoginSuccess,
              ),
      ),
    );
  }
}

// ------------------------------ 主页面修改 (MainPage) ------------------------------
class MainPage extends StatefulWidget {
  const MainPage({
    super.key,
    // ... 其他参数保持不变 ...
    required this.onLogout, // 【新增】
  });
  // ... 定义变量 ...
  final Function onLogout;

  
  State<MainPage> createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
  // ... 原有代码保持不变 ...
  
  
  void initState() {
    super.initState();
    _pageList = [
      UserListPage(loc: widget.loc),
      PostListPage(loc: widget.loc),
      SettingsPage(
        // ... 其他参数保持不变 ...
        onLogout: widget.onLogout, // 传递给设置页
      ),
    ];
  }
  // ... build方法保持不变 ...
}

// ------------------------------ 设置页面修改 (SettingsPage) ------------------------------
// 在SettingsPage中添加登出按钮,接收onLogout回调并调用即可,代码略
// 核心是在点击按钮时调用 widget.onLogout()

✅ 四、OpenHarmony 设备运行验证

所有代码编写完成后,我在 macOS 环境下使用 DevEco Studio 进行了全流程验证。

  1. 构建与运行
    在终端执行命令:
flutter run -d 127.0.0.1:5555

2. 验证结果
应用在 OpenHarmony 6.0 模拟器上运行完美,所有功能正常:

  1. 首次启动:应用打开后直接显示精美的登录页面,Logo、输入框、按钮布局正常。
  2. 表单验证:不输入内容直接点击登录,会提示 “请输入用户名/ 密码”。
  3. 密码切换:点击密码框右侧的眼睛图标,可以切换密码的明文 / 密文显示。
  4. 登录失败:输入错误的账号密码,会弹出红色的SnackBar 提示。
  5. 登录成功:输入测试账号 admin / 123456,按钮显示加载圈,随后自动跳转到用户列表主页面。
  6. 状态持久化:完全关闭应用(杀掉进程)后重新打开,直接进入主页面,无需重新登录。
  7. 登出功能:进入设置页面,点击底部红色的 “退出登录”按钮,弹出确认框,确认后清除状态并返回登录页面。
  8. 国际化与深色模式:登录页和登出功能完美支持中英文切换和深色模式适配。

运行效果截图

鸿蒙Flutter 登陆页面

OpenHarmony 模拟器截图,完成征文规范中的运行验证要求

OpenHarmony 模拟器截图,完成征文规范中的运行验证要求

登录页面(浅色模式):ALT 标签:Flutter 鸿蒙化登录页面浅色模式效果图
登录页面(深色模式):ALT 标签:Flutter 鸿蒙化登录页面深色模式效果图
登录成功后主页面:ALT 标签:Flutter 鸿蒙化登录成功后主页面效果图
设置页面登出功能:ALT 标签:Flutter 鸿蒙化设置页面登出功能效果图


💡 五、核心技术要点与开发心得

1. 核心技术要点

  • 状态提升 (State Lifting) :对于登录这种全局状态,最简单的方法是把状态放在最顶层的
    Widget(MyApp),然后通过构造函数把回调函数传下去。这比引入 Provider 对于小项目来说更直接。
  • Key 的使用: 在登出时使用UniqueKey()强制刷新整个应用是一个很实用的技巧,可以确保所有页面状态都被重置。
  • 异步编程: 所有和SharedPreferences相关的操作都是异步的,一定要使用await,并且配合try-catch处理异常。
  • Mounted 检查: 在网络请求或异步操作结束后调用setState之前,一定要检查mounted属性,防止页面销毁后报错。

2. 开发小贴士(避坑)

  • 方法名要仔细:定义好方法后,调用时最好复制粘贴,避免像我一开始那样把saveLoginStatus写成saveLoginState,导致状态存不进去。

  • 不要在 initState 里直接 await:initState不是 async 函数,如果要做异步操作,可以在里面调用一个独立的
    async 函数,或者在main()里先初始化好。

  • 测试账号要写清楚:在页面下方加上测试账号提示,不仅方便自己调试,也方便看文章的人复现。


🎯 六、全文总结

本次实战,我在已有完善功能的 Flutter 鸿蒙应用基础上,成功集成了完整的用户身份认证体系。
本次开发的核心成果:
✅ 设计并实现了一个美观、实用的登录页面 UI,包含表单验证和密码可见性切换。
✅ 实现了模拟登录逻辑,并结合 dio 为后续接入真实接口预留了位置。
✅ 通过 “状态提升 + 回调” 的轻量级方案,实现了登录状态的全局管理。
✅ 结合 OpenHarmony 适配版 shared_preferences,实现了登录状态的持久化。
✅ 在设置页面实现了安全的登出功能。

作为一名大一新生,通过本次开发,我不仅掌握了 Flutter 登录体系的开发流程,更深刻理解了状态管理和异步编程的重要性,这为我后续开发更复杂的应用打下了坚实的基础。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐