Flutter for OpenHarmony实战DAY4:从零搭建健康管家App之新增深色模式 + 登录注册 + 隐私协议,完善项目整体完整性
前面我们已经完成健康数据手动录入、编辑、删除 + fl_chart 数据图表可视化功能。账号登录 + 注册本地账号体系深色 / 浅色模式主题切换隐私协议合规页面本文从零完整实现这三大功能,基于Provider全局状态管理 + shared_preferences本地持久化,兼容 Android、鸿蒙 OpenHarmony 模拟器,代码注释齐全一、项目依赖配置flutter:二、整体功能架构本次新
Flutter for OpenHarmony实战DAY4:从零搭建健康管家App之新增深色模式 + 登录注册 + 隐私协议,完善项目整体完整性
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
前言
前面我们已经完成健康数据手动录入、编辑、删除 + fl_chart 数据图表可视化功能。
一款完整的商用级别 App,还缺少三大必备模块:
- 账号登录 + 注册本地账号体系
- 深色 / 浅色模式主题切换
- 隐私协议合规页面
本文从零完整实现这三大功能,基于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: "设置"),
],
),
);
}
}
四、功能运行说明
- 登录注册
支持账号注册、登录校验,数据本地保存;自带跳过登录按钮,方便开发调试。 - 深色模式
设置页开关一键切换,配置永久保存,重启 App 不重置。 - 隐私协议
独立页面展示隐私政策,满足 App 上架、课程设计合规要求。 - 退出登录
点击退出登录会清空登录状态,自动返回登录页。 - 全局适配
所有页面自动跟随深浅主题切换,兼容鸿蒙 OpenHarmony 模拟器。
五、项目整体优势 - 采用 Provider 全局状态管理,主题状态全局同步
- shared_preferences 实现账号、主题、登录状态持久化
- 模块解耦:登录、主题、隐私、设置独立文件,便于维护扩展
- 代码注释完善、结构规范
- 完美衔接之前的健康数据录入、图表统计模块,整套项目完整闭环
更多推荐


所有评论(0)