目录
前言
这篇文章主要介绍下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方法可用于各种基本数据类型,例如 setInt
、setBool
和 setString
。
Setter 方法做两件事:首先,同步更新 key-value 到内存中,然后保存到磁盘中。
在使用shared_preferences保存当前主题的时候,首先创建一个SharedPreferences对象,然后使用一个bool值表示当前的主题类型,调用setBool方法把当前的主题保存到内存中。
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 方法。例如,你可以使用 getInt
、getBool
和 getString
方法。
在我们的例子中,我们调用getBool方法获取上次保存的主题。
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键值对。
final prefs = await SharedPreferences.getInstance();
// 移除某个值
await prefs.remove('counter');
5.Shared_preferences的优缺点
shared_preferences的优点就是通过键值对的方式存取数据简单方便,但是也有以下的局限性:
- 只能用于基本数据类型:
int
、double
、bool
、string
和List<String>
。 - 不是为存储大量数据而设计的。
- 不能确保应用重启后数据仍然存在。
6.完整的示例代码
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目录为例,首先我们确认文件的目录:
import 'package:path_provider/path_provider.dart';
// ···
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');
}
3.向文件中写入数据
当我们点击按钮之后,把点击次数的值转成字符串保存到文件中即可。
Future<File> writeCounter(int counter) async {
final file = await _localFile;
// Write the file
return file.writeAsString('$counter');
}
4.从文件中读取数据
我们使用File类获取文件中的数据。
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.完整的示例代码
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
的文件,用于管理数据库的创建和操作:
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文件中,使用数据库管理类进行数据库的增删改查操作:
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']),
),
);
},
);
}
},
),
),
],
),
);
}
}
四、参考博客
2.sqflite