Flutter数据备份服务插件lit_backup_service的使用

Flutter数据备份服务插件lit_backup_service的使用

LitBackupService 是一个 Flutter 包,允许你使用 JSON 文件创建和恢复备份。此包实现了一个非常简单的本地设备持久性(键值对)存储,应该仅用作次要存储解决方案(例如用于备份其他数据库)。

它包含一个抽象模型类,提供了如何定义可备份模型类的基本结构。因此,每个模型类的 JSON 序列化应单独执行,因为这些模型在结构上有所不同。

平台支持

Android
✔️

该插件目前仅支持 Android 设备。

截图

示例主屏幕 示例主屏幕
1 2

Android 上所需的权限

为了在应用特定目录之外读写文件,必须授予额外的权限。这些权限必须在你的应用的 <code>AndroidManifest.xml</code> 中启用,位于 <code>android/app/src/main</code> 目录下。

<!-- Android 设备前于 Android 11 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- Android 11 及以上版本可选 -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

启用所需权限将通过 <code>BackupStorage</code><code>requestPermissions()</code> 方法完成,该方法利用了 <code>permission_handler</code> 包。

更近版本的 <code>Android</code> 对存储权限进行管理,基于文件用途,并有一些限制。要在 Android 10 及更高版本上访问先前安装创建的现有备份文件,必须授予 <code>MANAGE_EXTERNAL_STORAGE</code> 权限。由于文件权限更改,当前安装无法直接访问它们。这必须在 <code>BackupStorage</code><code>useManageExternalStoragePermission</code> 标志中指定。建议使用 <code>BackupStorage</code><code>pickBackupFile()</code> 方法来选择特定文件,而无需额外权限。

备份位置

备份默认存储在设备的媒体目录中。但支持自定义文件路径。媒体目录包括 <code>/storage/emulated/0/Documents/</code> 下的文档文件夹(默认)或 <code>/storage/emulated/0/Download/</code> 下的下载文件夹。

我们建议尽可能使用文档文件夹作为备份位置以提高安全性。

但对于更高的兼容性,我们建议使用 <code>Download</code> 文件夹,因为它几乎在所有 Android 设备上都可用。

开始使用

该项目是一个 Dart 包,包含可以在多个 Flutter 或 Dart 项目中轻松共享的库模块。

有关 Flutter 入门的帮助,请参阅我们的在线文档,其中提供了教程、示例、移动开发指南以及完整的 API 参考。

示例应用

为了更好地理解如何实现 LitBackupService,请查看 <code>example</code> 文件夹中的示例应用。请随意尝试该应用。

依赖项

LitBackupService 使用以下 Dart 依赖项来实现某些功能:

  • <a href="https://pub.dev/packages/permission_handler">permission_handler</a> - <a href="https://github.com/Baseflow/flutter-permission-handler/blob/master/LICENSE" rel="ugc">许可证</a>(用于请求存储访问权限)
  • <a href="https://pub.dev/packages/file_picker">file_picker</a> - <a href="https://github.com/miguelpruivo/flutter_file_picker/blob/master/LICENSE" rel="ugc">许可证</a>(用于允许平台特定的文件选择)

致谢

LitBackupService 得到了 Flutter 项目的帮助。

许可证

本仓库中的所有内容(包括源代码)均根据 <strong>BSD 3-Clause</strong> 许可证分发,具体在 <code>LICENSE</code> 文件中说明。


示例代码

import 'dart:convert';

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

class ExampleBackup implements BackupModel {
  final String name;
  final String quote;
  final String backupDate;

  const ExampleBackup({
    required this.name,
    required this.quote,
    required this.backupDate,
  });

  factory ExampleBackup.fromJson(Map<String, dynamic> json) {
    return ExampleBackup(
      name: json['name'] as String,
      quote: json['quote'] as String,
      backupDate: json['backupDate'] as String,
    );
  }

  [@override](/user/override)
  Map<String, dynamic> toJson() {
    return {
      'quote': quote,
      'name': name,
      'backupDate': backupDate,
    };
  }
}

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'LitBackupService',
      debugShowCheckedModeBanner: false,
      home: HomeScreen(),
    );
  }
}

class HomeScreen extends StatefulWidget {
  [@override](/user/override)
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  int _currentIndex = 0;
  bool _pressedPickFile = false;

  TextEditingController _nameInput = TextEditingController(text: '');
  TextEditingController _quoteInput = TextEditingController(text: 'If you want to be happy, be.');

  ExampleBackup get _exampleBackup {
    return ExampleBackup(
      name: _nameInput.text,
      quote: _quoteInput.text,
      backupDate: DateTime.now().toIso8601String(),
    );
  }

  String _formatAsLocalizedDate(BuildContext context, DateTime date) {
    final TimeOfDay timeOfDay = TimeOfDay.fromDateTime(date);
    final String dateFormat = MaterialLocalizations.of(context).formatShortDate(date);
    final String timeFormat = MaterialLocalizations.of(context).formatTimeOfDay(timeOfDay);
    return "$dateFormat $timeFormat";
  }

  void _onPressedPick() {
    setState(() {
      _pressedPickFile = !_pressedPickFile;
    });
  }

  final BackupStorage _backupStorage = BackupStorage(
    organizationName: "MyOrganization",
    applicationName: "LitBackupService",
    fileName: "examplebackup",
    installationID: DateTime.now().millisecondsSinceEpoch.toRadixString(16),
  );

  Future<void> _writeBackup(ExampleBackup backup) async {
    setState(() {
      _backupStorage.writeBackup(backup);
    });
  }

  Future<BackupModel?> _readBackup() {
    return _backupStorage.readBackup(
      decode: (contents) => ExampleBackup.fromJson(jsonDecode(contents)),
    );
  }

  Future<void> _deleteBackup() async {
    setState(() {
      _backupStorage.deleteBackup();
    });
  }

  Future<BackupModel?> _readBackupFromPicker() {
    return _backupStorage.pickBackupFile(
      decode: (contents) => ExampleBackup.fromJson(jsonDecode(contents)),
    );
  }

  Future<void> _requestPermissions() async {
    _backupStorage.requestPermissions().then((value) => setState(() {}));
  }

  [@override](/user/override)
  void dispose() {
    _pressedPickFile = false;
    super.dispose();
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: (selected) {
          setState(() {
            _currentIndex = selected;
            _pressedPickFile = false;
          });
        },
        items: [
          BottomNavigationBarItem(
            label: "Restore",
            icon: Icon(Icons.restore),
          ),
          BottomNavigationBarItem(
            label: "Create",
            icon: Icon(Icons.create),
          ),
        ],
      ),
      appBar: AppBar(
        centerTitle: true,
        backgroundColor: Colors.black,
        title: Text("LitBackupService"),
      ),
      body: _currentIndex == 0
          ? Scaffold(
              body: Center(
                child: Padding(
                  padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 16.0),
                  child: FutureBuilder(
                    future: _backupStorage.hasPermissions(),
                    builder: (context, AsyncSnapshot<bool> hasPerSnap) {
                      if (hasPerSnap.connectionState == ConnectionState.done && hasPerSnap.hasData) {
                        return hasPerSnap.data!
                            ? Column(
                                children: [
                                  ElevatedButton(
                                    onPressed: _onPressedPick,
                                    child: Text(_pressedPickFile ? "CLEAR" : "PICK BACKUP FILE"),
                                  ),
                                  _pressedPickFile
                                      ? _BackupPreviewBuilder(
                                          backupStorage: _backupStorage,
                                          formatAsLocalizedDate: _formatAsLocalizedDate,
                                          readBackup: _readBackupFromPicker(),
                                          requestPermissions: _requestPermissions,
                                        )
                                      : SizedBox(),
                                ],
                              )
                            : Column(
                                children: [
                                  Padding(
                                    padding: const EdgeInsets.symmetric(vertical: 8.0),
                                    child: Text("Reading backup from storage denied."),
                                  ),
                                  ElevatedButton(
                                    onPressed: _requestPermissions,
                                    child: Text("Request permissions"),
                                  ),
                                ],
                              );
                      }

                      return CircularProgressIndicator();
                    },
                  ),
                ),
              ),
            )
          : Scaffold(
              body: SingleChildScrollView(
                child: Center(
                  child: Padding(
                    padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 16.0),
                    child: Column(
                      children: [
                        _InputField(
                          title: "Your name",
                          controller: _nameInput,
                        ),
                        _InputField(
                          title: "Your quote",
                          controller: _quoteInput,
                        ),
                        ElevatedButton(
                          onPressed: () => _writeBackup(_exampleBackup),
                          child: Text("BACKUP NOW"),
                        ),
                        _BackupPreviewBuilder(
                          backupStorage: _backupStorage,
                          formatAsLocalizedDate: _formatAsLocalizedDate,
                          readBackup: _readBackup(),
                          requestPermissions: _requestPermissions,
                          showMediaLocation: true,
                        ),
                        FutureBuilder(
                          future: _backupStorage.hasPermissions(),
                          builder: (context, AsyncSnapshot<bool> hasPerSnap) {
                            return hasPerSnap.hasData
                                ? hasPerSnap.data!
                                    ? ElevatedButton(
                                        onPressed: _deleteBackup,
                                        child: Text("DELETE BACKUP"),
                                      )
                                    : SizedBox()
                                : SizedBox();
                          },
                        ),
                      ],
                    ),
                  ),
                ),
              ),
            ),
    );
  }
}

class _BackupPreviewBuilder extends StatelessWidget {
  final BackupStorage backupStorage;
  final String Function(BuildContext context, DateTime datetime) formatAsLocalizedDate;
  final Future<BackupModel?> readBackup;
  final void Function() requestPermissions;
  final bool showMediaLocation;
  const _BackupPreviewBuilder({
    Key? key,
    required this.backupStorage,
    required this.formatAsLocalizedDate,
    required this.readBackup,
    required this.requestPermissions,
    this.showMediaLocation = false,
  }) : super(key: key);

  [@override](/user/override)
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: readBackup,
      builder: (context, AsyncSnapshot<BackupModel?> snap) {
        if (snap.connectionState == ConnectionState.done) {
          if (snap.hasData) {
            ExampleBackup? exampleBackup = snap.data as ExampleBackup;
            return snap.data != null
                ? Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text("Backup of ${exampleBackup.name}"),
                      Text("Quote: ${exampleBackup.quote}"),
                      Text("Last backup: " + formatAsLocalizedDate(context, DateTime.parse(exampleBackup.backupDate))),
                      Padding(
                        padding: const EdgeInsets.symmetric(vertical: 30.0),
                        child: Text("You will find your JSON file on your selected Media directory of your local device."),
                      ),
                      showMediaLocation
                          ? FutureBuilder(
                              future: backupStorage.currentMediaPath,
                              builder: (context, AsyncSnapshot<String> snap) {
                                return snap.data != null
                                    ? Padding(
                                        padding: const EdgeInsets.symmetric(vertical: 15.0),
                                        child: Text("Current media directory:\n" + snap.data!),
                                      )
                                    : SizedBox();
                              },
                            )
                          : SizedBox()
                    ],
                  )
                : Text("No backup found!");
          }

          if (snap.hasError) return Text("Error");
        } else if (snap.connectionState == ConnectionState.waiting) {
          return CircularProgressIndicator();
        }
        // Permission denied or file not found.

        return FutureBuilder(
          future: backupStorage.hasPermissions(),
          builder: (context, AsyncSnapshot<bool> hasPerSnap) {
            return hasPerSnap.hasData
                ? !hasPerSnap.data!
                    ? Column(
                        children: [
                          Padding(
                            padding: const EdgeInsets.symmetric(vertical: 8.0),
                            child: Text("Reading backup from storage denied."),
                          ),
                          ElevatedButton(
                            onPressed: requestPermissions,
                            child: Text("Request permissions"),
                          ),
                        ],
                      )
                    : Text("Backup not found!")
                : SizedBox();
          },
        );
      },
    );
  }
}

class _InputField extends StatelessWidget {
  final String title;
  final TextEditingController controller;
  const _InputField({
    Key? key,
    required this.title,
    required this.controller,
  }) : super(key: key);

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8.0),
      child: Column(
        children: [
          Text(title),
          TextField(
            controller: controller,
            decoration: InputDecoration(
              border: OutlineInputBorder(),
              hintText: 'Enter your $title...',
            ),
          ),
        ],
      ),
    );
  }
}

更多关于Flutter数据备份服务插件lit_backup_service的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter数据备份服务插件lit_backup_service的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是一个关于如何使用Flutter数据备份服务插件lit_backup_service的示例代码案例。请注意,实际使用时你可能需要根据你的应用需求进行调整,并确保插件版本与Flutter SDK版本兼容。

首先,确保你已经在pubspec.yaml文件中添加了lit_backup_service插件的依赖:

dependencies:
  flutter:
    sdk: flutter
  lit_backup_service: ^x.y.z  # 请替换为实际版本号

然后,运行flutter pub get来安装依赖。

接下来,在你的Flutter项目中,你可以按照以下步骤来使用lit_backup_service插件进行数据备份服务。

1. 导入插件

在你的Dart文件中导入插件:

import 'package:lit_backup_service/lit_backup_service.dart';

2. 初始化备份服务

在应用启动时初始化备份服务。通常,你可以在main.dartMyApp类中执行此操作:

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  
  // 初始化备份服务
  LitBackupService.initialize();

  runApp(MyApp());
}

3. 配置备份策略

在适当的时机(如用户登录或应用启动时),配置备份策略。例如,你可以指定要备份的数据路径和备份频率:

void configureBackupService() async {
  // 定义备份策略
  final backupStrategy = BackupStrategy(
    paths: [
      '/data/user/0/com.example.myapp/files/important_file.txt', // 替换为你的文件路径
    ],
    frequency: BackupFrequency.daily, // 设置备份频率,如每日备份
  );

  // 配置并启动备份服务
  await LitBackupService.configure(strategy: backupStrategy);
  await LitBackupService.start();
}

4. 触发备份

你可以在应用中的特定操作(如用户点击备份按钮)时触发立即备份:

void triggerBackup() async {
  try {
    await LitBackupService.triggerBackup();
    print('Backup triggered successfully.');
  } catch (e) {
    print('Failed to trigger backup: $e');
  }
}

5. 监听备份状态

你可以监听备份服务的状态变化,以便向用户显示相应的UI反馈:

void listenToBackupServiceStatus() {
  LitBackupService.backupStatusStream.listen((status) {
    print('Backup status: $status');
    // 根据状态更新UI,例如显示进度条或通知用户备份完成
  });
}

6. 完整示例

以下是一个完整的示例,展示了如何在Flutter应用中使用lit_backup_service插件:

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

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  LitBackupService.initialize();

  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Backup Service Example'),
        ),
        body: BackupServicePage(),
      ),
    );
  }
}

class BackupServicePage extends StatefulWidget {
  @override
  _BackupServicePageState createState() => _BackupServicePageState();
}

class _BackupServicePageState extends State<BackupServicePage> {
  @override
  void initState() {
    super.initState();
    configureBackupService();
    listenToBackupServiceStatus();
  }

  void configureBackupService() async {
    final backupStrategy = BackupStrategy(
      paths: [
        '/data/user/0/com.example.myapp/files/important_file.txt',
      ],
      frequency: BackupFrequency.daily,
    );
    await LitBackupService.configure(strategy: backupStrategy);
    await LitBackupService.start();
  }

  void listenToBackupServiceStatus() {
    LitBackupService.backupStatusStream.listen((status) {
      print('Backup status: $status');
      // 更新UI逻辑
    });
  }

  void triggerBackup() async {
    try {
      await LitBackupService.triggerBackup();
      ScaffoldMessenger.of(context).showSnackBar(SnackBar(
        content: Text('Backup triggered successfully.'),
      ));
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(SnackBar(
        content: Text('Failed to trigger backup: $e'),
      ));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: ElevatedButton(
        onPressed: triggerBackup,
        child: Text('Trigger Backup'),
      ),
    );
  }
}

请注意,上述代码中的文件路径/data/user/0/com.example.myapp/files/important_file.txt仅作为示例,你需要根据你的应用实际情况进行调整。另外,lit_backup_service插件的具体API可能会根据版本有所不同,请参考官方文档或插件的源代码以获取最新和最准确的API信息。

回到顶部