flutter_for_openharmony家庭相册app实战+隐私设置实现

隐私设置是应用中非常重要的一部分,用户可以在这里管理应用锁、私密相册、照片信息和数据导出等功能。一个完善的隐私设置能让用户更放心地使用应用。今天我们来实现这个功能。

设计思路

隐私设置页面采用分组列表的形式,把相关的设置项放在一起。包括安全设置、相册隐私、照片信息和数据管理四个部分。每个部分都有对应的开关或操作项。

创建页面结构

先搭建基本框架:

dart 复制代码
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:provider/provider.dart';
import '../providers/settings_provider.dart';

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

  @override
  State<PrivacyScreen> createState() => _PrivacyScreenState();
}

class _PrivacyScreenState extends State<PrivacyScreen> {
  bool _appLock = false;
  bool _hidePrivateAlbums = true;
  bool _faceRecognition = true;
  bool _locationInfo = true;
  bool _biometricAuth = false;

  @override
  void initState() {
    super.initState();
    _loadSettings();
  }

  Future<void> _loadSettings() async {
    final settings = context.read<SettingsProvider>();
    setState(() {
      _appLock = settings.appLock;
      _hidePrivateAlbums = settings.hidePrivateAlbums;
      _faceRecognition = settings.faceRecognition;
      _locationInfo = settings.locationInfo;
      _biometricAuth = settings.biometricAuth;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('隐私设置'),
        elevation: 0,
      ),
      body: ListView(
        children: [
          _buildSecuritySection(),
          _buildAlbumPrivacySection(),
          _buildPhotoInfoSection(),
          _buildDataManagementSection(),
        ],
      ),
    );
  }
}

用StatefulWidget来管理多个开关状态,initState里加载保存的设置。ListView包含四个分组,每个分组对应一类隐私设置。

安全设置分组

应用锁和生物识别相关的设置:

dart 复制代码
Widget _buildSecuritySection() {
  return _buildSection(
    title: '安全',
    children: [
      _buildSwitchTile(
        icon: Icons.lock_outlined,
        iconColor: const Color(0xFFF44336),
        title: '应用锁',
        subtitle: '启动应用时需要验证',
        value: _appLock,
        onChanged: (value) {
          setState(() => _appLock = value);
          context.read<SettingsProvider>().setAppLock(value);
          if (value && !_hasPassword()) {
            _showSetPasswordDialog();
          }
        },
      ),
      if (_appLock) ...[
        _buildListTile(
          icon: Icons.vpn_key_outlined,
          iconColor: const Color(0xFF2196F3),
          title: '修改密码',
          subtitle: '修改应用锁密码',
          onTap: () => _showChangePasswordDialog(),
        ),
        _buildSwitchTile(
          icon: Icons.fingerprint,
          iconColor: const Color(0xFF4CAF50),
          title: '生物识别',
          subtitle: '使用指纹或面部识别',
          value: _biometricAuth,
          onChanged: (value) {
            setState(() => _biometricAuth = value);
            context.read<SettingsProvider>().setBiometricAuth(value);
          },
        ),
      ],
    ],
  );
}

bool _hasPassword() {
  return context.read<SettingsProvider>().hasPassword;
}

应用锁开关打开时,如果还没设置密码就弹出设置密码对话框。开启应用锁后才显示修改密码和生物识别选项。

相册隐私分组

私密相册相关的设置:

dart 复制代码
Widget _buildAlbumPrivacySection() {
  return _buildSection(
    title: '相册隐私',
    children: [
      _buildSwitchTile(
        icon: Icons.visibility_off_outlined,
        iconColor: const Color(0xFF9C27B0),
        title: '隐藏私密相册',
        subtitle: '私密相册不在主页显示',
        value: _hidePrivateAlbums,
        onChanged: (value) {
          setState(() => _hidePrivateAlbums = value);
          context.read<SettingsProvider>().setHidePrivateAlbums(value);
        },
      ),
      _buildListTile(
        icon: Icons.folder_special_outlined,
        iconColor: const Color(0xFFFF9800),
        title: '管理私密相册',
        subtitle: _getPrivateAlbumCount(),
        onTap: () => _navigateToPrivateAlbums(),
      ),
    ],
  );
}

String _getPrivateAlbumCount() {
  final count = context.read<SettingsProvider>().privateAlbumCount;
  return '$count 个私密相册';
}

隐藏私密相册的开关可以控制私密相册是否在主页显示。管理私密相册选项显示当前有多少个私密相册。

照片信息分组

照片识别和位置信息相关的设置:

dart 复制代码
Widget _buildPhotoInfoSection() {
  return _buildSection(
    title: '照片信息',
    children: [
      _buildSwitchTile(
        icon: Icons.face_outlined,
        iconColor: const Color(0xFF00BCD4),
        title: '人脸识别',
        subtitle: '自动识别照片中的家人',
        value: _faceRecognition,
        onChanged: (value) {
          setState(() => _faceRecognition = value);
          context.read<SettingsProvider>().setFaceRecognition(value);
        },
      ),
      _buildSwitchTile(
        icon: Icons.location_on_outlined,
        iconColor: const Color(0xFF4CAF50),
        title: '位置信息',
        subtitle: '保存照片的拍摄位置',
        value: _locationInfo,
        onChanged: (value) {
          setState(() => _locationInfo = value);
          context.read<SettingsProvider>().setLocationInfo(value);
        },
      ),
      _buildListTile(
        icon: Icons.info_outlined,
        iconColor: const Color(0xFF3F51B5),
        title: '照片元数据',
        subtitle: '查看和管理照片的详细信息',
        onTap: () => _showMetadataInfo(),
      ),
    ],
  );
}

人脸识别开关控制是否自动识别照片中的家人。位置信息开关控制是否保存照片的拍摄位置。

数据管理分组

数据导出和账户删除等操作:

dart 复制代码
Widget _buildDataManagementSection() {
  return _buildSection(
    title: '数据管理',
    children: [
      _buildListTile(
        icon: Icons.download_outlined,
        iconColor: const Color(0xFF009688),
        title: '导出数据',
        subtitle: '导出所有个人数据',
        onTap: () => _showExportDialog(),
      ),
      _buildListTile(
        icon: Icons.history,
        iconColor: const Color(0xFF795548),
        title: '数据使用记录',
        subtitle: '查看数据访问历史',
        onTap: () => _showDataUsageHistory(),
      ),
      _buildListTile(
        icon: Icons.delete_forever_outlined,
        iconColor: const Color(0xFFF44336),
        title: '删除账户',
        subtitle: '永久删除账户和所有数据',
        onTap: () => _showDeleteAccountDialog(),
      ),
    ],
  );
}

数据管理分组包含三个重要操作:导出数据、查看数据使用记录和删除账户。删除账户用红色图标表示这是危险操作。

通用组件方法

统一的分组和列表项样式:

dart 复制代码
Widget _buildSection({
  required String title,
  required List<Widget> children,
}) {
  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Padding(
        padding: EdgeInsets.fromLTRB(16.w, 20.h, 16.w, 8.h),
        child: Text(
          title,
          style: TextStyle(
            fontSize: 13.sp,
            color: Colors.grey[600],
            fontWeight: FontWeight.w600,
          ),
        ),
      ),
      Container(
        decoration: BoxDecoration(
          color: Colors.white,
          border: Border(
            top: BorderSide(color: Colors.grey[200]!, width: 0.5),
            bottom: BorderSide(color: Colors.grey[200]!, width: 0.5),
          ),
        ),
        child: Column(children: children),
      ),
    ],
  );
}

Widget _buildSwitchTile({
  required IconData icon,
  required Color iconColor,
  required String title,
  required String subtitle,
  required bool value,
  required ValueChanged<bool> onChanged,
}) {
  return SwitchListTile(
    secondary: Container(
      padding: EdgeInsets.all(8.w),
      decoration: BoxDecoration(
        color: iconColor.withOpacity(0.1),
        borderRadius: BorderRadius.circular(8.r),
      ),
      child: Icon(icon, color: iconColor),
    ),
    title: Text(title),
    subtitle: Text(subtitle),
    value: value,
    onChanged: onChanged,
    activeColor: const Color(0xFFE91E63),
  );
}

Widget _buildListTile({
  required IconData icon,
  required Color iconColor,
  required String title,
  required String subtitle,
  required VoidCallback onTap,
}) {
  return ListTile(
    leading: Container(
      padding: EdgeInsets.all(8.w),
      decoration: BoxDecoration(
        color: iconColor.withOpacity(0.1),
        borderRadius: BorderRadius.circular(8.r),
      ),
      child: Icon(icon, color: iconColor),
    ),
    title: Text(title),
    subtitle: Text(subtitle),
    trailing: const Icon(Icons.chevron_right, color: Colors.grey),
    onTap: onTap,
  );
}

提取通用的组件方法,减少重复代码。分组标题用小号灰色字体,设置项容器用白色背景加上下边框。

密码对话框

设置和修改密码的对话框:

dart 复制代码
void _showSetPasswordDialog() {
  final passwordController = TextEditingController();
  final confirmController = TextEditingController();
  
  showDialog(
    context: context,
    barrierDismissible: false,
    builder: (dialogContext) => AlertDialog(
      title: const Text('设置密码'),
      content: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          TextField(
            controller: passwordController,
            obscureText: true,
            decoration: InputDecoration(
              labelText: '输入密码',
              hintText: '6-16位数字或字母',
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(8.r),
              ),
            ),
          ),
          SizedBox(height: 12.h),
          TextField(
            controller: confirmController,
            obscureText: true,
            decoration: InputDecoration(
              labelText: '确认密码',
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(8.r),
              ),
            ),
          ),
        ],
      ),
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16.r),
      ),
      actions: [
        TextButton(
          onPressed: () {
            setState(() => _appLock = false);
            Navigator.pop(dialogContext);
          },
          child: const Text('取消'),
        ),
        TextButton(
          onPressed: () {
            final password = passwordController.text;
            final confirm = confirmController.text;
            
            if (password.isEmpty || password.length < 6) {
              _showError('密码长度至少6位');
              return;
            }
            
            if (password != confirm) {
              _showError('两次输入的密码不一致');
              return;
            }
            
            context.read<SettingsProvider>().setPassword(password);
            Navigator.pop(dialogContext);
            _showSuccess('密码设置成功');
          },
          child: const Text('确定'),
        ),
      ],
    ),
  );
}

void _showChangePasswordDialog() {
  final oldPasswordController = TextEditingController();
  final newPasswordController = TextEditingController();
  final confirmController = TextEditingController();
  
  showDialog(
    context: context,
    builder: (dialogContext) => AlertDialog(
      title: const Text('修改密码'),
      content: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          TextField(
            controller: oldPasswordController,
            obscureText: true,
            decoration: InputDecoration(
              labelText: '当前密码',
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(8.r),
              ),
            ),
          ),
          SizedBox(height: 12.h),
          TextField(
            controller: newPasswordController,
            obscureText: true,
            decoration: InputDecoration(
              labelText: '新密码',
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(8.r),
              ),
            ),
          ),
          SizedBox(height: 12.h),
          TextField(
            controller: confirmController,
            obscureText: true,
            decoration: InputDecoration(
              labelText: '确认新密码',
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(8.r),
              ),
            ),
          ),
        ],
      ),
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16.r),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(dialogContext),
          child: const Text('取消'),
        ),
        TextButton(
          onPressed: () {
            final oldPassword = oldPasswordController.text;
            final newPassword = newPasswordController.text;
            final confirm = confirmController.text;
            
            if (!context.read<SettingsProvider>().verifyPassword(oldPassword)) {
              _showError('当前密码错误');
              return;
            }
            
            if (newPassword.isEmpty || newPassword.length < 6) {
              _showError('新密码长度至少6位');
              return;
            }
            
            if (newPassword != confirm) {
              _showError('两次输入的新密码不一致');
              return;
            }
            
            context.read<SettingsProvider>().setPassword(newPassword);
            Navigator.pop(dialogContext);
            _showSuccess('密码修改成功');
          },
          child: const Text('确定'),
        ),
      ],
    ),
  );
}

设置密码对话框包含两个输入框,修改密码需要先验证当前密码。密码长度至少6位,两次输入必须一致。

导出数据功能

让用户导出所有个人数据:

dart 复制代码
void _showExportDialog() {
  showDialog(
    context: context,
    builder: (dialogContext) => AlertDialog(
      title: const Text('导出数据'),
      content: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text('将导出以下数据:'),
          SizedBox(height: 12.h),
          _buildCheckItem('照片和视频'),
          _buildCheckItem('家人信息'),
          _buildCheckItem('活动和回忆'),
          _buildCheckItem('相册分类'),
        ],
      ),
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16.r),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(dialogContext),
          child: const Text('取消'),
        ),
        TextButton(
          onPressed: () {
            Navigator.pop(dialogContext);
            _startExport();
          },
          child: const Text('开始导出'),
        ),
      ],
    ),
  );
}

Widget _buildCheckItem(String text) {
  return Padding(
    padding: EdgeInsets.only(bottom: 8.h),
    child: Row(
      children: [
        const Icon(Icons.check_circle, color: Color(0xFF4CAF50), size: 18),
        SizedBox(width: 8.w),
        Text(text),
      ],
    ),
  );
}

void _startExport() {
  showDialog(
    context: context,
    barrierDismissible: false,
    builder: (_) => const Center(
      child: CircularProgressIndicator(),
    ),
  );
  
  Future.delayed(const Duration(seconds: 2), () {
    if (mounted) {
      Navigator.pop(context);
      _showSuccess('数据导出完成');
    }
  });
}

导出数据对话框列出将要导出的内容。点击开始导出后显示进度提示,完成后通知用户。

删除账户功能

删除账户需要多重确认:

dart 复制代码
void _showDeleteAccountDialog() {
  showDialog(
    context: context,
    builder: (dialogContext) => AlertDialog(
      title: const Row(
        children: [
          Icon(Icons.warning_amber_rounded, color: Color(0xFFF44336)),
          SizedBox(width: 8),
          Text('删除账户'),
        ],
      ),
      content: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text('此操作不可撤销,将永久删除:'),
          SizedBox(height: 12.h),
          _buildDeleteItem('所有照片和视频'),
          _buildDeleteItem('家人信息和关系'),
          _buildDeleteItem('活动和回忆记录'),
          _buildDeleteItem('账户和个人资料'),
        ],
      ),
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16.r),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(dialogContext),
          child: const Text('取消'),
        ),
        TextButton(
          onPressed: () {
            Navigator.pop(dialogContext);
            _showFinalConfirmDialog();
          },
          child: const Text(
            '删除',
            style: TextStyle(color: Color(0xFFF44336)),
          ),
        ),
      ],
    ),
  );
}

Widget _buildDeleteItem(String text) {
  return Padding(
    padding: EdgeInsets.only(bottom: 8.h),
    child: Row(
      children: [
        const Icon(Icons.close, color: Color(0xFFF44336), size: 18),
        SizedBox(width: 8.w),
        Text(text),
      ],
    ),
  );
}

void _showFinalConfirmDialog() {
  final confirmController = TextEditingController();
  
  showDialog(
    context: context,
    builder: (dialogContext) => AlertDialog(
      title: const Text('最后确认'),
      content: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          const Text('请输入"删除账户"以确认操作'),
          SizedBox(height: 12.h),
          TextField(
            controller: confirmController,
            decoration: InputDecoration(
              hintText: '删除账户',
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(8.r),
              ),
            ),
          ),
        ],
      ),
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16.r),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(dialogContext),
          child: const Text('取消'),
        ),
        TextButton(
          onPressed: () {
            if (confirmController.text == '删除账户') {
              Navigator.pop(dialogContext);
              _performDeleteAccount();
            } else {
              _showError('输入不正确');
            }
          },
          child: const Text(
            '确认删除',
            style: TextStyle(color: Color(0xFFF44336)),
          ),
        ),
      ],
    ),
  );
}

void _performDeleteAccount() {
  showDialog(
    context: context,
    barrierDismissible: false,
    builder: (_) => const Center(
      child: CircularProgressIndicator(),
    ),
  );
  
  Future.delayed(const Duration(seconds: 2), () {
    if (mounted) {
      Navigator.pop(context);
      Navigator.pop(context);
      _showSuccess('账户已删除');
    }
  });
}

删除账户需要两次确认,第二次需要输入"删除账户"文字。这样可以防止用户误操作。

辅助方法

通用的提示方法:

dart 复制代码
void _showError(String message) {
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      content: Text(message),
      backgroundColor: Colors.red,
      behavior: SnackBarBehavior.floating,
    ),
  );
}

void _showSuccess(String message) {
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      content: Text(message),
      backgroundColor: const Color(0xFF4CAF50),
      behavior: SnackBarBehavior.floating,
    ),
  );
}

void _showMetadataInfo() {
  showDialog(
    context: context,
    builder: (_) => AlertDialog(
      title: const Text('照片元数据'),
      content: const Text('照片元数据包括拍摄时间、地点、设备信息等。'),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('知道了'),
        ),
      ],
    ),
  );
}

void _navigateToPrivateAlbums() {
  // 导航到私密相册管理页面
}

void _showDataUsageHistory() {
  // 显示数据使用记录
}

提取通用的提示方法,减少重复代码。错误提示用红色,成功提示用绿色。

小结

隐私设置页面通过分组列表的形式,清晰地展示了各类隐私相关的设置。应用锁、私密相册、照片信息和数据管理四个部分覆盖了用户对隐私保护的主要需求。通过开关、对话框和确认流程,确保用户的隐私数据得到妥善保护。

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

相关推荐
2601_949593652 小时前
React Native 鸿蒙跨平台开发:LinearGradient 渐变动画效果
javascript·react native·react.js
我命由我123452 小时前
Android 开发 Room 数据库升级问题:A migration from 6 to 7 was required but not found.
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
灰灰勇闯IT2 小时前
Flutter for OpenHarmony:进度条与加载指示器 —— 构建流畅、可感知的异步交互体验
flutter·交互
qq_177767372 小时前
React Native鸿蒙跨平台音乐播放器涉及实时进度更新、播放控制、列表交互、状态管理等核心技术点
javascript·react native·react.js·ecmascript·交互·harmonyos
灰灰勇闯IT2 小时前
Flutter for OpenHarmony:下拉刷新(RefreshIndicator)—— 构建即时、可信的数据同步体验
flutter·华为·交互
雨季6662 小时前
Flutter 三端应用实战:OpenHarmony “极简文本字符计数器”——量化表达的尺度
开发语言·flutter·ui·交互·dart
2501_920931702 小时前
React Native鸿蒙跨平台实现了简单的商品图片轮播功能,为用户提供了直观的商品图片浏览体验,帮助用户全面了解商品外观
javascript·react native·react.js·ecmascript·harmonyos
小哥Mark2 小时前
Flutter无状态和有状态组件在鸿蒙应用程序中的实战示例
flutter·华为·harmonyos
小哥Mark2 小时前
Flutter下拉刷新和滚动条组件在鸿蒙应用程序实战示例
flutter·华为·harmonyos