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

相关推荐
孤鸿玉10 分钟前
Fluter InteractiveViewer 与ScrollView滑动冲突问题解决
flutter
叽哥7 小时前
Flutter Riverpod上手指南
android·flutter·ios
BG1 天前
Flutter 简仿Excel表格组件介绍
flutter
zhangmeng1 天前
FlutterBoost在iOS26真机运行崩溃问题
flutter·app·swift
恋猫de小郭1 天前
对于普通程序员来说 AI 是什么?AI 究竟用的是什么?
前端·flutter·ai编程
卡尔特斯1 天前
Flutter A GlobalKey was used multipletimes inside one widget'schild list.The ...
flutter
w_y_fan1 天前
Flutter 滚动组件总结
前端·flutter
醉过才知酒浓1 天前
Flutter Getx 的页面传参
flutter
火柴就是我2 天前
flutter 之真手势冲突处理
android·flutter
Speed1232 天前
`mockito` 的核心“打桩”规则
flutter·dart