Flutter for OpenHarmony实战DAY4:从零搭建健康管家App之新增深色模式 + 登录注册 + 隐私协议,完善项目整体完整性

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

前言

前面我们已经完成健康数据手动录入、编辑、删除 + fl_chart 数据图表可视化功能。
一款完整的商用级别 App,还缺少三大必备模块:

  1. 账号登录 + 注册本地账号体系
  2. 深色 / 浅色模式主题切换
  3. 隐私协议合规页面
    本文从零完整实现这三大功能,基于Provider全局状态管理 + shared_preferences本地持久化,兼容 Android、鸿蒙 OpenHarmony 模拟器,代码注释齐全

一、项目依赖配置
打开 pubspec.yaml 添加所需依赖:

dependencies:
  flutter:
    sdk: flutter
  fl_chart: ^0.65.0
  shared_preferences: ^2.5.3
  intl: ^0.19.0
  provider: ^6.1.5

终端执行安装依赖:

flutter clean
flutter pub get

二、整体功能架构
本次新增功能亮点:
✅ 登录 + 注册 本地账号存储,无需后台接口
✅ 新增跳过登录功能,方便调试体验
✅ 全局深色 / 浅色模式一键切换,永久保存
✅ 隐私协议静态页面,合规必备
✅ 设置页统一管理:主题切换、隐私协议、退出登录
✅ 登录状态持久化,重启 App 免重复登录
✅ 完美兼容 Flutter 鸿蒙 OpenHarmony
三、核心代码实现
3.1 主题状态管理类(深色模式核心)
新建 lib/utils/theme_provider.dart

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

/// 全局主题状态管理:深色/浅色模式
class ThemeProvider extends ChangeNotifier {
  // 默认浅色模式
  bool _isDarkMode = false;

  bool get isDarkMode => _isDarkMode;

  ThemeProvider() {
    // 初始化读取本地保存的主题状态
    _loadThemeFromLocal();
  }

  /// 从本地读取主题配置
  Future<void> _loadThemeFromLocal() async {
    final prefs = await SharedPreferences.getInstance();
    _isDarkMode = prefs.getBool('dark_mode') ?? false;
    notifyListeners();
  }

  /// 切换主题模式并保存到本地
  Future<void> toggleTheme() async {
    _isDarkMode = !_isDarkMode;
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool('dark_mode', _isDarkMode);
    notifyListeners();
  }
}

3.2 登录注册页面(带跳过登录)
新建 lib/pages/login_page.dart

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

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

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

class _LoginPageState extends State<LoginPage> {
  final TextEditingController _userCtrl = TextEditingController();
  final TextEditingController _pwdCtrl = TextEditingController();
  // 标记登录/注册切换
  bool _isRegister = false;

  /// 提交登录/注册
  Future<void> _onSubmit() async {
    String username = _userCtrl.text.trim();
    String pwd = _pwdCtrl.text.trim();

    // 非空校验
    if (username.isEmpty || pwd.isEmpty) {
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text("账号密码不能为空")),
      );
      return;
    }

    final prefs = await SharedPreferences.getInstance();
    if (_isRegister) {
      // 注册:保存账号密码到本地
      await prefs.setString("app_username", username);
      await prefs.setString("app_pwd", pwd);
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text("注册成功,请前往登录")),
      );
      setState(() {
        _isRegister = false;
        _userCtrl.clear();
        _pwdCtrl.clear();
      });
    } else {
      // 登录:校验本地账号密码
      String? saveUser = prefs.getString("app_username");
      String? savePwd = prefs.getString("app_pwd");
      if (username == saveUser && pwd == savePwd) {
        await prefs.setBool("is_login", true);
        if (!mounted) return;
        Navigator.pushReplacementNamed(context, "/home");
      } else {
        if (!mounted) return;
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text("账号或密码错误")),
        );
      }
    }
  }

  /// 跳过登录,直接进入主页
  Future<void> _skipLogin() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool("is_login", true);
    if (!mounted) return;
    Navigator.pushReplacementNamed(context, "/home");
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(_isRegister ? "账号注册" : "账号登录"),
        centerTitle: true,
      ),
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          children: [
            TextField(
              controller: _userCtrl,
              decoration: const InputDecoration(
                labelText: "请输入账号",
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 15),
            TextField(
              controller: _pwdCtrl,
              obscureText: true,
              decoration: const InputDecoration(
                labelText: "请输入密码",
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 30),
            // 登录/注册按钮
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: _onSubmit,
                child: Text(_isRegister ? "立即注册" : "立即登录"),
              ),
            ),
            const SizedBox(height: 15),
            // 登录注册切换
            TextButton(
              onPressed: () {
                setState(() {
                  _isRegister = !_isRegister;
                  _userCtrl.clear();
                  _pwdCtrl.clear();
                });
              },
              child: Text(_isRegister ? "已有账号?去登录" : "没有账号?去注册"),
            ),
            // 跳过登录
            TextButton(
              onPressed: _skipLogin,
              child: const Text("跳过登录 → 直接体验App", style: TextStyle(color: Colors.grey)),
            ),
          ],
        ),
      ),
    );
  }
}

3.3 隐私协议页面
新建 lib/pages/privacy_page.dart

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("隐私政策"),
        centerTitle: true,
      ),
      body: const SingleChildScrollView(
        padding: EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              "隐私政策说明",
              style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
            ),
            SizedBox(height: 12),
            Text(
              "本健康管家App尊重并保护所有用户的个人隐私与数据安全。"
                  "所有用户录入的步数、心率、睡眠等健康数据仅保存在用户本地设备,"
                  "不会私自上传、泄露或共享给任何第三方。",
              style: TextStyle(fontSize: 15, height: 1.5),
            ),
            SizedBox(height: 15),
            Text("1. 数据收集", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
            Text("仅收集用户手动录入的健康运动数据,无后台隐私采集。"),
            SizedBox(height: 10),
            Text("2. 数据存储", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
            Text("所有数据本地持久化存储,不上传云端。"),
            SizedBox(height: 10),
            Text("3. 主题与账号", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
            Text("登录账号、深色模式配置均保存在本地,保护用户使用偏好隐私。"),
            SizedBox(height: 10),
            Text("4. 免责声明", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
            Text("本App仅作健康数据记录与展示,不具备医疗诊断功能。"),
          ],
        ),
      ),
    );
  }
}

3.4 设置页面(主题切换 + 隐私协议 + 退出登录)
新建 / 修改 lib/pages/setting_page.dar

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../utils/theme_provider.dart';
import 'privacy_page.dart';

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

  @override
  Widget build(BuildContext context) {
    final ThemeProvider themeProvider = Provider.of<ThemeProvider>(context);

    return Scaffold(
      appBar: AppBar(
        title: const Text("设置中心"),
        centerTitle: true,
      ),
      body: ListView(
        children: [
          // 深色模式开关
          SwitchListTile(
            title: const Text("深色模式"),
            subtitle: const Text("开启后切换暗黑主题"),
            value: themeProvider.isDarkMode,
            onChanged: (value) {
              themeProvider.toggleTheme();
            },
          ),
          const Divider(),
          // 隐私协议入口
          ListTile(
            title: const Text("隐私政策"),
            trailing: const Icon(Icons.arrow_forward_ios, size: 16),
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(builder: (context) => const PrivacyPage()),
              );
            },
          ),
          const Divider(),
          // 退出登录
          ListTile(
            title: const Text("退出登录", style: TextStyle(color: Colors.red)),
            onTap: () async {
              final prefs = await SharedPreferences.getInstance();
              await prefs.setBool("is_login", false);
              if (!context.mounted) return;
              Navigator.pushReplacementNamed(context, "/login");
            },
          ),
        ],
      ),
    );
  }
}

3.5 全局入口 main.dart 配置路由 + 主题

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'utils/theme_provider.dart';
import 'pages/login_page.dart';
import 'pages/home_page.dart';
import 'pages/data_page.dart';
import 'pages/health_record_page.dart';
import 'pages/setting_page.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(
    ChangeNotifierProvider(
      create: (context) => ThemeProvider(),
      child: const MyApp(),
    ),
  );
}

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

  // 判断是否已登录
  Future<bool> checkLoginStatus() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getBool("is_login") ?? false;
  }

  @override
  Widget build(BuildContext context) {
    final ThemeProvider theme = Provider.of<ThemeProvider>(context);
    return MaterialApp(
      title: "健康管家",
      debugShowCheckedModeBanner: false,
      // 浅色主题
      theme: ThemeData(
        brightness: Brightness.light,
        primarySwatch: Colors.blue,
        useMaterial3: true,
      ),
      // 深色主题
      darkTheme: ThemeData(
        brightness: Brightness.dark,
        primarySwatch: Colors.blue,
        useMaterial3: true,
      ),
      // 跟随全局状态切换
      themeMode: theme.isDarkMode ? ThemeMode.dark : ThemeMode.light,
      initialRoute: "/",
      routes: {
        "/": (context) => FutureBuilder<bool>(
          future: checkLoginStatus(),
          builder: (ctx, snapshot) {
            if (snapshot.connectionState == ConnectionState.waiting) {
              return const Scaffold(body: Center(child: CircularProgressIndicator()));
            }
            return snapshot.data == true ? const MainPage() : const LoginPage();
          },
        ),
        "/login": (context) => const LoginPage(),
        "/home": (context) => const MainPage(),
      },
    );
  }
}

// 底部导航主页面
class MainPage extends StatefulWidget {
  const MainPage({super.key});

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

class _MainPageState extends State<MainPage> {
  int currentIndex = 0;
  final List<Widget> pageList = const [
    HomePage(),
    DataPage(),
    HealthRecordPage(),
    SettingPage(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: pageList[currentIndex],
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: currentIndex,
        type: BottomNavigationBarType.fixed,
        onTap: (index) => setState(() => currentIndex = index),
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: "首页"),
          BottomNavigationBarItem(icon: Icon(Icons.bar_chart), label: "统计"),
          BottomNavigationBarItem(icon: Icon(Icons.edit_note), label: "记录"),
          BottomNavigationBarItem(icon: Icon(Icons.settings), label: "设置"),
        ],
      ),
    );
  }
}

四、功能运行说明

  1. 登录注册
    支持账号注册、登录校验,数据本地保存;自带跳过登录按钮,方便开发调试。
  2. 深色模式
    设置页开关一键切换,配置永久保存,重启 App 不重置。
  3. 隐私协议
    独立页面展示隐私政策,满足 App 上架、课程设计合规要求。
  4. 退出登录
    点击退出登录会清空登录状态,自动返回登录页。
  5. 全局适配
    所有页面自动跟随深浅主题切换,兼容鸿蒙 OpenHarmony 模拟器。
    在这里插入图片描述
    五、项目整体优势
  6. 采用 Provider 全局状态管理,主题状态全局同步
  7. shared_preferences 实现账号、主题、登录状态持久化
  8. 模块解耦:登录、主题、隐私、设置独立文件,便于维护扩展
  9. 代码注释完善、结构规范
  10. 完美衔接之前的健康数据录入、图表统计模块,整套项目完整闭环
Logo

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

更多推荐