Flutter本地数据持久化的几种方式

目录

前言

一、shared_preferences

1.添加依赖

2.保存数据

3.读取数据

4.移除数据

5.Shared_preferences的优缺点

6.完整的示例代码

二、path_provider

1.导入path_provider

2.创建文件读写的目录

3.向文件中写入数据

4.从文件中读取数据

5.完整的示例代码

三、sqlite数据库

1.导入sqlite

2.创建数据库助手类

3.完整示例代码

四、参考博客


前言

这篇文章主要介绍下Flutter中本地数据持久化的几种方式。

一、shared_preferences

如果你要存储的键值集合相对较少,则可以用 shared_preferences 插件。

当我们要存储一些简单的数据,例如app的系统设置,一些简单的用户信息等,可以考虑使用shared_preferences插件。

我们以一个切换主题的Demo为例,看一下shared_preferences的用法。

1.添加依赖

pubspec.yaml

pubspec.yaml文件中添加shared_preferences依赖。

终端运行pub get命令,安装shared_preferences插件。

pub get

2.保存数据

Flutter中我们通过ThemeMode对象获取当前的主题模式。它是一个枚举类型,有system,light,dark三个主题。

要存储数据,请使用 SharedPreferences 类的 setter 方法。 Setter方法可用于各种基本数据类型,例如 setIntsetBoolsetString

Setter 方法做两件事:首先,同步更新 key-value 到内存中,然后保存到磁盘中。

在使用shared_preferences保存当前主题的时候,首先创建一个SharedPreferences对象,然后使用一个bool值表示当前的主题类型,调用setBool方法把当前的主题保存到内存中。

Dart 复制代码
  Future<void> _toggleTheme() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    bool isDarkMode = _themeMode == ThemeMode.dark;
    await prefs.setBool('isDarkMode', !isDarkMode);
    setState(() {
      _themeMode = !isDarkMode ? ThemeMode.dark : ThemeMode.light;
    });
  }

3.读取数据

当App启动的时候,我们调用SharedPreferences对象的get方法获取上次保存的主题。

要读取数据,请使用 SharedPreferences 类相应的 getter 方法。对于每一个 setter 方法都有对应的 getter 方法。例如,你可以使用 getIntgetBoolgetString 方法。

在我们的例子中,我们调用getBool方法获取上次保存的主题。

Dart 复制代码
  Future<void> _loadTheme() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    bool isDarkMode = prefs.getBool('isDarkMode') ?? false;
    setState(() {
      _themeMode = isDarkMode ? ThemeMode.dark : ThemeMode.light;
    });
  }

4.移除数据

上面的两个方法是SharedPreferences常用的两个方法,当然有时候我们还需要清空保存在内存中的信息,这个时候我们可以调用对象的remove方法,移除保存在内存中的key-value键值对。

Dart 复制代码
final prefs = await SharedPreferences.getInstance();

// 移除某个值
await prefs.remove('counter');

5.Shared_preferences的优缺点

shared_preferences的优点就是通过键值对的方式存取数据简单方便,但是也有以下的局限性:

  1. 只能用于基本数据类型: intdoubleboolstringList<String>
  2. 不是为存储大量数据而设计的。
  3. 不能确保应用重启后数据仍然存在。

6.完整的示例代码

Dart 复制代码
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

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

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

  @override
  MyAppState createState() => MyAppState();
}

class MyAppState extends State<MyApp> {
  ThemeMode _themeMode = ThemeMode.light;

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

  Future<void> _loadTheme() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    bool isDarkMode = prefs.getBool('isDarkMode') ?? false;
    setState(() {
      _themeMode = isDarkMode ? ThemeMode.dark : ThemeMode.light;
    });
  }

  Future<void> _toggleTheme() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    bool isDarkMode = _themeMode == ThemeMode.dark;
    await prefs.setBool('isDarkMode', !isDarkMode);
    setState(() {
      _themeMode = !isDarkMode ? ThemeMode.dark : ThemeMode.light;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Theme Demo',
      theme: ThemeData.light(),
      darkTheme: ThemeData.dark(),
      themeMode: _themeMode,
      home: MyHomePage(
        themeMode: _themeMode,
        onThemeChanged: _toggleTheme,
      ),
    );
  }
}

class MyHomePage extends StatelessWidget {
  final ThemeMode themeMode;
  final VoidCallback onThemeChanged;

  const MyHomePage({super.key, required this.themeMode, required this.onThemeChanged});

  @override
  Widget build(BuildContext context) {
    bool isDarkMode = themeMode == ThemeMode.dark;
    return Scaffold(
      appBar: AppBar(
        title: const Text('Theme Demo'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Center(
            child: Text(
              isDarkMode ? "当前模式:暗黑模式" : "当前模式:白天模式",
              style: const TextStyle(fontSize: 24),
            ),
          ),
          const SizedBox(height: 20),
          ElevatedButton(
            onPressed: onThemeChanged,
            child: const Text(
              '切换当前模式',
              style: TextStyle(fontSize: 18),
            ),
          ),
        ],
      ),
    );
  }
}

二、path_provider

有时候我们需要把一些数据以文件的形式保存到本地,这个时候path_provider就派上用场了。

path_provider提供一种平台无关的方式以一致的方式访问设备的文件位置系统。该 plugin 当前支持访问两种文件位置系统:

磁盘文件的读写操作可能会相对方便地实现某些业务场景。它常见于应用启动期间产生的持久化数据,或者从网络下载数据供离线使用。

临时文件夹: 这是一个系统可以随时清空的临时(缓存)文件夹。在 iOS 上对应 NSCachesDirectory 的返回值;在 Android 上对应 getCacheDir() 的返回值。

Documents 目录: 供应用使用,用于存储只能由该应用访问的文件。只有在删除应用时,系统才会清除这个目录。在 iOS 上,这个目录对应于 NSDocumentDirectory。在 Android 上,则是 AppData 目录。

我们以计数器为例,当我们点击计时器的时候,把当前的计时器的值保存到文件中。

看看如何实现这个实例:

1.导入path_provider

path_provider: ^2.1.3

2.创建文件读写的目录

以Document目录为例,首先我们确认文件的目录:

Dart 复制代码
import 'package:path_provider/path_provider.dart';
// ···
  Future<String> get _localPath async {
    final directory = await getApplicationDocumentsDirectory();

    return directory.path;
  }

然后创建这个文件目录位置的引用:

Dart 复制代码
Future<File> get _localFile async {
  final path = await _localPath;
  return File('$path/counter.txt');
}

3.向文件中写入数据

当我们点击按钮之后,把点击次数的值转成字符串保存到文件中即可。

Dart 复制代码
Future<File> writeCounter(int counter) async {
  final file = await _localFile;

  // Write the file
  return file.writeAsString('$counter');
}

4.从文件中读取数据

我们使用File类获取文件中的数据。

Dart 复制代码
Future<int> readCounter() async {
  try {
    final file = await _localFile;

    // Read the file
    final contents = await file.readAsString();

    return int.parse(contents);
  } catch (e) {
    // If encountering an error, return 0
    return 0;
  }
}

5.完整的示例代码

Dart 复制代码
import 'dart:async';
import 'dart:io';

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

void main() {
  runApp(
    MaterialApp(
      title: 'Reading and Writing Files',
      home: FlutterDemo(storage: CounterStorage()),
    ),
  );
}

class CounterStorage {
  Future<String> get _localPath async {
    final directory = await getApplicationDocumentsDirectory();

    return directory.path;
  }

  Future<File> get _localFile async {
    final path = await _localPath;
    return File('$path/counter.txt');
  }

  Future<int> readCounter() async {
    try {
      final file = await _localFile;

      // Read the file
      final contents = await file.readAsString();

      return int.parse(contents);
    } catch (e) {
      // If encountering an error, return 0
      return 0;
    }
  }

  Future<File> writeCounter(int counter) async {
    final file = await _localFile;

    // Write the file
    return file.writeAsString('$counter');
  }
}

class FlutterDemo extends StatefulWidget {
  const FlutterDemo({super.key, required this.storage});

  final CounterStorage storage;

  @override
  State<FlutterDemo> createState() => _FlutterDemoState();
}

class _FlutterDemoState extends State<FlutterDemo> {
  int _counter = 0;

  @override
  void initState() {
    super.initState();
    widget.storage.readCounter().then((value) {
      setState(() {
        _counter = value;
      });
    });
  }

  Future<File> _incrementCounter() {
    setState(() {
      _counter++;
    });

    // Write the variable as a string to the file.
    return widget.storage.writeCounter(_counter);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Reading and Writing Files'),
      ),
      body: Center(
        child: Text(
          'Button tapped $_counter time${_counter == 1 ? '' : 's'}.',
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

三、sqlite数据库

如果你正在编写一个需要持久化且查询大量本地设备数据的 app,可考虑采用数据库,而不是本地文件夹或关键值库。总的来说,相比于其他本地持久化方案来说,数据库能够提供更为迅速的插入、更新、查询功能。

这个熟悉数据库的同学们对这个应该比较熟悉。我们以一个demos为例,看一下sqlite的用法

1.导入sqlite

和另外两种方式的导入方式差不多,我们在yaml中配置sqlite和path_provider.

path_provider: ^2.1.3

sqflite: ^2.3.3+1

2.创建数据库助手类

这一步,我们创建一个名为database_helper.dart的文件,用于管理数据库的创建和操作:

Dart 复制代码
import 'dart:async';
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

class DatabaseHelper {
  static final DatabaseHelper _instance = DatabaseHelper._internal();
  factory DatabaseHelper() => _instance;
  static Database? _database;

  DatabaseHelper._internal();

  Future<Database> get database async {
    if (_database != null) return _database!;
    _database = await _initDatabase();
    return _database!;
  }

  Future<Database> _initDatabase() async {
    String path = join(await getDatabasesPath(), 'demo.db');
    return await openDatabase(
      path,
      version: 1,
      onCreate: _onCreate,
    );
  }

  Future _onCreate(Database db, int version) async {
    await db.execute('''
      CREATE TABLE items (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT
      )
    ''');
  }

  Future<int> insertItem(String name) async {
    Database db = await database;
    return await db.insert('items', {'name': name});
  }

  Future<List<Map<String, dynamic>>> getItems() async {
    Database db = await database;
    return await db.query('items');
  }

  Future<int> deleteItem(int id) async {
    Database db = await database;
    return await db.delete('items', where: 'id = ?', whereArgs: [id]);
  }
}

3.完整示例代码

我们在UI文件中,使用数据库管理类进行数据库的增删改查操作:

Dart 复制代码
import 'package:flutter/material.dart';
import 'database_helper.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'SQLite Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

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

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final DatabaseHelper _dbHelper = DatabaseHelper();
  final TextEditingController _controller = TextEditingController();
  late Future<List<Map<String, dynamic>>> _items;

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

  void _refreshItems() {
    setState(() {
      _items = _dbHelper.getItems();
    });
  }

  void _addItem() async {
    if (_controller.text.isNotEmpty) {
      await _dbHelper.insertItem(_controller.text);
      _controller.clear();
      _refreshItems();
    }
  }

  void _deleteItem(int id) async {
    await _dbHelper.deleteItem(id);
    _refreshItems();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('SQLite Demo'),
      ),
      body: Column(
        children: <Widget>[
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextField(
              controller: _controller,
              decoration: const InputDecoration(
                labelText: 'Item name',
                border: OutlineInputBorder(),
              ),
            ),
          ),
          ElevatedButton(
            onPressed: _addItem,
            child: const Text('Add Item'),
          ),
          Expanded(
            child: FutureBuilder<List<Map<String, dynamic>>>(
              future: _items,
              builder: (context, snapshot) {
                if (snapshot.connectionState == ConnectionState.waiting) {
                  return const Center(child: CircularProgressIndicator());
                } else if (snapshot.hasError) {
                  return const Center(child: Text('Error'));
                } else if (!snapshot.hasData || snapshot.data!.isEmpty) {
                  return const Center(child: Text('No items'));
                } else {
                  return ListView.builder(
                    itemCount: snapshot.data!.length,
                    itemBuilder: (context, index) {
                      final item = snapshot.data![index];
                      return ListTile(
                        title: Text(item['name']),
                        trailing: IconButton(
                          icon: const Icon(Icons.delete),
                          onPressed: () => _deleteItem(item['id']),
                        ),
                      );
                    },
                  );
                }
              },
            ),
          ),
        ],
      ),
    );
  }
}

四、参考博客

1.shared_preferences

2.sqflite

3.path_provider

相关推荐
jcLee9516 小时前
Flutter/Dart:使用日志模块Logger Easier
flutter·log4j·dart·logger
tmacfrank16 小时前
Flutter 异步编程简述
flutter
tmacfrank17 小时前
Flutter 基础知识总结
flutter
叫我菜菜就好17 小时前
【Flutter_Web】Flutter编译Web第三篇(网络请求篇):dio如何改造方法,变成web之后数据如何处理
前端·网络·flutter
AiFlutter21 小时前
Flutter-底部分享弹窗(showModalBottomSheet)
java·前端·flutter
m0_748247802 天前
Flutter Intl包使用指南:实现国际化和本地化
前端·javascript·flutter
迷雾漫步者2 天前
Flutter组件————PageView
flutter·跨平台·dart
迷雾漫步者2 天前
Flutter组件————FloatingActionButton
前端·flutter·dart
coder_pig2 天前
📝小记:Ubuntu 部署 Jenkins 打包 Flutter APK
flutter·ubuntu·jenkins
捡芝麻丢西瓜3 天前
flutter自学笔记5- dart 编码规范
flutter·dart