在这里插入图片描述

收藏数据是用户的宝贵资产,防止数据丢失至关重要。备份与恢复功能让用户可以将收藏数据导出到本地文件,在更换设备或重装应用后快速恢复数据。本文将从备份操作、恢复流程、文件管理等角度,详细讲解如何在 Flutter for OpenHarmony 项目中实现一个可靠的备份与恢复页面。

一、备份与恢复的核心价值

数据备份不仅是安全保障,更是用户信任的基础:

  • 数据安全:防止因设备故障、应用卸载导致的数据丢失
  • 设备迁移:更换新设备时快速迁移收藏数据
  • 版本回退:出现问题时可以恢复到之前的状态
  • 多设备同步:通过备份文件在多个设备间同步数据

这些需求决定了备份与恢复页面需要操作简单、流程清晰、安全可靠

一、页面布局设计

1. Scaffold 基础结构

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

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('备份与恢复')),

顶部导航栏显示"备份与恢复"标题,让用户明确当前所在页面。在完整版本中,可以在右侧添加"帮助"图标,解释备份与恢复的使用方法。

2. Padding 内边距

      body: Padding(
        padding: EdgeInsets.all(16.w),

为整个页面添加 16 像素的内边距,确保内容不会紧贴屏幕边缘。这是移动端 UI 的标准边距。

3. Column 垂直布局

        child: Column(
          children: [

使用 Column 垂直排列备份卡片、恢复卡片、备份列表三部分。

二、备份卡片实现

1. Card 容器

            Card(
              child: ListTile(
                leading: Icon(Icons.backup, size: 40, color: Colors.blue),

Card 组件的优势

  • 自带阴影和圆角,让备份操作区域独立清晰
  • 使用 ListTile 快速构建左中右三栏布局
  • 符合 Material Design 规范,用户体验一致

leading 图标设计

  • 使用 Icons.backup 备份图标,符合功能主题
  • size: 40 设置较大的图标,让操作更醒目
  • color: Colors.blue 使用蓝色,与备份的积极含义相符

2. 标题与说明

                title: const Text('备份数据'),
                subtitle: const Text('将收藏数据备份到本地'),

title 标题设计

  • 简洁明了地说明这是备份操作
  • 使用默认字体大小和颜色,保持简洁

subtitle 说明文字

  • 解释备份的作用:将数据保存到本地文件
  • 帮助用户理解这个操作的目的
  • 可以添加更多细节:
subtitle: Text('将收藏数据备份到本地\n包括手办信息、照片、设置等'),

3. 备份按钮

                trailing: ElevatedButton(
                  onPressed: () {},
                  child: const Text('备份'),
                ),

按钮设计

  • 使用 ElevatedButton 而非 TextButton,更醒目
  • 当前 onPressed 为空实现,实际应执行备份逻辑
  • 可以添加加载状态:
trailing: _isBackingUp
  ? SizedBox(
      width: 20,
      height: 20,
      child: CircularProgressIndicator(strokeWidth: 2),
    )
  : ElevatedButton(
      onPressed: () => _performBackup(),
      child: const Text('备份'),
    ),

备份过程中显示加载动画,避免用户重复点击。

三、恢复卡片实现

1. 间距控制

            SizedBox(height: 12.h),

在备份卡片和恢复卡片之间添加 12 像素的垂直间距,形成视觉分隔。

2. Card 容器

            Card(
              child: ListTile(
                leading: Icon(Icons.restore, size: 40, color: Colors.green),

leading 图标设计

  • 使用 Icons.restore 恢复图标,与备份图标形成对应
  • color: Colors.green 使用绿色,与备份的蓝色形成区分
  • 绿色通常代表"恢复"、"重置"等含义

3. 标题与说明

                title: const Text('恢复数据'),
                subtitle: const Text('从备份文件恢复数据'),

说明文字设计

  • 解释恢复的作用:从备份文件中读取数据
  • 可以添加警告信息:
subtitle: Text(
  '从备份文件恢复数据\n⚠️ 将覆盖当前数据',
  style: TextStyle(fontSize: 12.sp),
),

提醒用户恢复操作会覆盖当前数据,避免误操作。

4. 恢复按钮

                trailing: ElevatedButton(
                  onPressed: () {},
                  child: const Text('恢复'),
                ),

按钮设计

  • 与备份按钮保持一致的样式
  • 实际应弹出文件选择器,让用户选择备份文件:
trailing: ElevatedButton(
  onPressed: () async {
    final result = await FilePicker.platform.pickFiles(
      type: FileType.custom,
      allowedExtensions: ['db', 'backup'],
    );
    
    if (result != null) {
      _showRestoreConfirmDialog(result.files.single.path!);
    }
  },
  child: const Text('恢复'),
),

四、备份列表展示

1. 间距与标题

            SizedBox(height: 24.h),
            const Text('最近备份'),

标题设计

  • 在恢复卡片和备份列表之间添加 24 像素的间距,形成明显的视觉分隔
  • 使用 Text 组件显示"最近备份"标题
  • 可以优化标题样式:
Text(
  '最近备份',
  style: TextStyle(
    fontSize: 16.sp,
    fontWeight: FontWeight.bold,
  ),
),

2. Expanded 自适应高度

            Expanded(
              child: ListView.builder(
                itemCount: 5,

Expanded 的作用

  • 让 ListView 占据 Column 中除备份卡片、恢复卡片、标题外的所有剩余空间
  • 这样备份列表可以滚动查看,不会超出屏幕
  • 使用 ListView.builder 按需渲染列表项

3. 备份文件列表项

                itemBuilder: (context, index) {
                  return ListTile(
                    leading: const Icon(Icons.folder),

leading 图标设计

  • 使用 Icons.folder 文件夹图标,表示这是一个文件
  • 使用默认大小和颜色,保持简洁
  • 可以根据文件类型使用不同图标:
leading: Icon(
  backup.isAutoBackup ? Icons.cloud_done : Icons.folder,
  color: backup.isAutoBackup ? Colors.blue : Colors.grey,
),

自动备份显示云图标,手动备份显示文件夹图标。

                    title: Text('备份_2024_0${index + 1}_15.db'),

title 文件名显示

  • 显示备份文件的名称,包含日期信息
  • 当前使用模拟数据,实际应显示真实文件名
  • 文件名格式建议:backup_YYYYMMDD_HHMMSS.db
                    subtitle: Text('大小: ${(index + 1) * 2}MB'),

subtitle 文件大小显示

  • 显示备份文件的大小,帮助用户了解存储占用
  • 当前使用模拟数据,实际应计算真实文件大小
  • 可以添加更多信息:
subtitle: Text(
  '大小: ${formatFileSize(backup.size)}\n创建时间: ${formatDate(backup.createTime)}',
),

同时显示文件大小和创建时间。

                    trailing: IconButton(
                      icon: const Icon(Icons.delete),
                      onPressed: () {},
                    ),

trailing 删除按钮

  • 使用 IconButton 而非 ElevatedButton,节省空间
  • Icons.delete 删除图标是通用标识
  • 实际应添加二次确认:
trailing: IconButton(
  icon: const Icon(Icons.delete),
  onPressed: () {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('确认删除'),
        content: Text('确定要删除这个备份文件吗?'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: Text('取消'),
          ),
          TextButton(
            onPressed: () {
              _deleteBackup(backup.id);
              Navigator.pop(context);
            },
            child: Text('删除', style: TextStyle(color: Colors.red)),
          ),
        ],
      ),
    );
  },
),

五、备份数据模型

为了支持备份与恢复功能,需要定义备份数据模型:

class Backup {
  final String id;
  final String fileName;
  final String filePath;
  final int size; // 字节
  final DateTime createTime;
  final bool isAutoBackup;
  final String? description;
  
  Backup({
    required this.id,
    required this.fileName,
    required this.filePath,
    required this.size,
    required this.createTime,
    this.isAutoBackup = false,
    this.description,
  });
  
  String get formattedSize {
    if (size < 1024) {
      return '$size B';
    } else if (size < 1024 * 1024) {
      return '${(size / 1024).toStringAsFixed(2)} KB';
    } else {
      return '${(size / 1024 / 1024).toStringAsFixed(2)} MB';
    }
  }
  
  factory Backup.fromJson(Map<String, dynamic> json) {
    return Backup(
      id: json['id'],
      fileName: json['file_name'],
      filePath: json['file_path'],
      size: json['size'],
      createTime: DateTime.parse(json['create_time']),
      isAutoBackup: json['is_auto_backup'] ?? false,
      description: json['description'],
    );
  }
}

这样可以存储完整的备份信息。

六、备份与恢复逻辑

1. 备份实现

Future<void> performBackup() async {
  try {
    // 1. 获取数据库文件路径
    final dbPath = await getDatabasePath();
    
    // 2. 生成备份文件名
    final timestamp = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now());
    final backupFileName = 'backup_$timestamp.db';
    
    // 3. 获取备份目录
    final backupDir = await getApplicationDocumentsDirectory();
    final backupPath = '${backupDir.path}/backups/$backupFileName';
    
    // 4. 创建备份目录
    final backupFolder = Directory('${backupDir.path}/backups');
    if (!await backupFolder.exists()) {
      await backupFolder.create(recursive: true);
    }
    
    // 5. 复制数据库文件
    final dbFile = File(dbPath);
    await dbFile.copy(backupPath);
    
    // 6. 保存备份记录
    final backup = Backup(
      id: uuid.v4(),
      fileName: backupFileName,
      filePath: backupPath,
      size: await File(backupPath).length(),
      createTime: DateTime.now(),
    );
    await saveBackupRecord(backup);
    
    // 7. 显示成功提示
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('备份成功:$backupFileName')),
    );
  } catch (e) {
    print('备份失败: $e');
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('备份失败,请重试')),
    );
  }
}

2. 恢复实现

Future<void> performRestore(String backupPath) async {
  try {
    // 1. 验证备份文件
    final backupFile = File(backupPath);
    if (!await backupFile.exists()) {
      throw Exception('备份文件不存在');
    }
    
    // 2. 关闭当前数据库连接
    await closeDatabase();
    
    // 3. 获取数据库文件路径
    final dbPath = await getDatabasePath();
    
    // 4. 备份当前数据库(以防恢复失败)
    final currentDbFile = File(dbPath);
    final tempBackupPath = '$dbPath.temp';
    if (await currentDbFile.exists()) {
      await currentDbFile.copy(tempBackupPath);
    }
    
    // 5. 恢复备份文件
    await backupFile.copy(dbPath);
    
    // 6. 重新打开数据库
    await openDatabase();
    
    // 7. 删除临时备份
    final tempFile = File(tempBackupPath);
    if (await tempFile.exists()) {
      await tempFile.delete();
    }
    
    // 8. 显示成功提示
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('恢复成功,请重启应用')),
    );
    
    // 9. 重启应用
    Phoenix.rebirth(context);
  } catch (e) {
    print('恢复失败: $e');
    
    // 恢复失败时,尝试恢复原数据库
    final tempFile = File('$dbPath.temp');
    if (await tempFile.exists()) {
      await tempFile.copy(dbPath);
      await tempFile.delete();
    }
    
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('恢复失败,数据已回滚')),
    );
  }
}

七、状态管理集成

1. 创建 BackupProvider

class BackupProvider extends ChangeNotifier {
  List<Backup> _backups = [];
  bool _isBackingUp = false;
  bool _isRestoring = false;
  
  List<Backup> get backups => _backups;
  bool get isBackingUp => _isBackingUp;
  bool get isRestoring => _isRestoring;
  
  Future<void> loadBackups() async {
    final backupDir = await getApplicationDocumentsDirectory();
    final backupFolder = Directory('${backupDir.path}/backups');
    
    if (await backupFolder.exists()) {
      final files = await backupFolder.list().toList();
      _backups = files
        .where((file) => file.path.endsWith('.db'))
        .map((file) {
          final stat = file.statSync();
          return Backup(
            id: file.path,
            fileName: file.path.split('/').last,
            filePath: file.path,
            size: stat.size,
            createTime: stat.modified,
          );
        })
        .toList();
      
      _backups.sort((a, b) => b.createTime.compareTo(a.createTime));
      notifyListeners();
    }
  }
  
  Future<void> createBackup() async {
    _isBackingUp = true;
    notifyListeners();
    
    try {
      await performBackup();
      await loadBackups();
    } finally {
      _isBackingUp = false;
      notifyListeners();
    }
  }
  
  Future<void> restoreBackup(String backupPath) async {
    _isRestoring = true;
    notifyListeners();
    
    try {
      await performRestore(backupPath);
    } finally {
      _isRestoring = false;
      notifyListeners();
    }
  }
  
  Future<void> deleteBackup(String backupId) async {
    final file = File(backupId);
    if (await file.exists()) {
      await file.delete();
      await loadBackups();
    }
  }
}

2. 页面改造

body: Consumer<BackupProvider>(
  builder: (context, provider, child) {
    return Padding(
      padding: EdgeInsets.all(16.w),
      child: Column(
        children: [
          Card(
            child: ListTile(
              leading: Icon(Icons.backup, size: 40, color: Colors.blue),
              title: const Text('备份数据'),
              subtitle: const Text('将收藏数据备份到本地'),
              trailing: provider.isBackingUp
                ? SizedBox(
                    width: 20,
                    height: 20,
                    child: CircularProgressIndicator(strokeWidth: 2),
                  )
                : ElevatedButton(
                    onPressed: () => provider.createBackup(),
                    child: const Text('备份'),
                  ),
            ),
          ),
          SizedBox(height: 12.h),
          Card(
            child: ListTile(
              leading: Icon(Icons.restore, size: 40, color: Colors.green),
              title: const Text('恢复数据'),
              subtitle: const Text('从备份文件恢复数据'),
              trailing: ElevatedButton(
                onPressed: () => _showRestoreDialog(context, provider),
                child: const Text('恢复'),
              ),
            ),
          ),
          SizedBox(height: 24.h),
          const Text('最近备份'),
          Expanded(
            child: provider.backups.isEmpty
              ? Center(child: Text('暂无备份'))
              : ListView.builder(
                  itemCount: provider.backups.length,
                  itemBuilder: (context, index) {
                    final backup = provider.backups[index];
                    return ListTile(
                      leading: const Icon(Icons.folder),
                      title: Text(backup.fileName),
                      subtitle: Text('大小: ${backup.formattedSize}\n${DateFormat('yyyy-MM-dd HH:mm').format(backup.createTime)}'),
                      trailing: IconButton(
                        icon: const Icon(Icons.delete),
                        onPressed: () => _showDeleteDialog(context, provider, backup),
                      ),
                    );
                  },
                ),
          ),
        ],
      ),
    );
  },
),

通过 Consumer 监听备份状态变化,自动刷新页面。

八、OpenHarmony 适配要点

1. 文件权限

在 OpenHarmony 上需要申请存储权限:

{
  "reqPermissions": [
    {
      "name": "ohos.permission.READ_USER_STORAGE"
    },
    {
      "name": "ohos.permission.WRITE_USER_STORAGE"
    }
  ]
}

2. 自动备份

使用 WorkManager 实现定时自动备份:

Future<void> scheduleAutoBackup() async {
  await Workmanager().registerPeriodicTask(
    'auto_backup',
    'autoBackupTask',
    frequency: Duration(days: 1),
  );
}

九、实战经验总结

备份与恢复页面的实现要点:

  • 操作简单:一键备份、一键恢复,降低用户操作门槛
  • 安全可靠:备份前验证数据完整性,恢复前备份当前数据
  • 文件管理:显示备份列表,支持删除旧备份
  • 状态反馈:备份和恢复过程中显示加载动画

通过本文的实战讲解,你已经掌握了在 Flutter for OpenHarmony 项目中构建备份与恢复功能的核心技巧。


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

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐