pub.dev:pub.dev/packages/sh…
github:github.com/flutter/plu…
开源协议:BSD-3-Clause LICENSE
SharedPreferences是pub上非常受欢迎的一个插件,也一定会是你必不可少的一个插件。它能够通过key-value的形式来持久化我们的数据,简单易学,且十分常用。
SharedPreferences直译过来是共享偏好的意思,我们可以将他当做一个简单的小型数据库来使用,它可以将我们需要的数据进行持久化保存,在flutter支持的所有平台上它都能够调用响应的实现来进行数据的持久化(具体的实现类查看附件)。我们经常会用来存放一些用户信息、App的参数配置等内容。相比sqlite等一些小型数据库,它更加的方便,不用创建数据库,创建表,简单上手,用来存放一些参数正合适。但由于他只是简单的key-value的存储方式,不能使用sql语句查询,对于业务数据的存放就不合适了,所以他并不能取代sqlite等小型数据库。
flutter中的SharedPreferences支持存放boolean,int,double,string,string数组,一共5中数据类型。为了保持全局的唯一性,减少内存的开销,SharedPreferences采用单例模式设计。
SharedPreferences的使用非常的简单,实例化对象=>存取数据,我们来看一个简单的实例。
安装
//控制台安装
flutter pub add shared_preferences
//pubspec.yaml安装
dependencies:
shared_preferences: ^2.0.15
//页面引用
import 'package:shared_preferences/shared_preferences.dart';
简单使用
我新建了一个StatefulWidget页面,在_ViewState中声明了一个SharedPreferences? _prefs;页面中有一个按钮,点击按钮后会初始化SharedPreferences,然后设置一些变量,最后使用setState刷新页面。
//声明对象
SharedPreferences? _prefs;
...
ElevatedButton(
onPressed: () async {
//初始化对象
_prefs = await SharedPreferences.getInstance();
//存入数据
_prefs?.setInt("int", 1);
_prefs?.setString("string", "string");
_prefs?.setBool("bool", true);
_prefs?.setDouble("double", 1.0);
_prefs?.setStringList("stringList", ["第一个字符串", "第二个字符串"]);
setState(() {});
},
child: const Text('初始化数据'),
),
...
//读取数据
Text('int: ${_prefs?.getInt("int")}'),
Text('string: ${_prefs?.getString("string")}'),
Text('bool: ${_prefs?.getBool("bool")}'),
Text('double: ${_prefs?.getDouble("double")}'),
Text('stringList: ${_prefs?.getStringList("stringList")}'),
Text('用户姓名: ${_user?.name}'),
最后点击以后是这样的效果。
不同页面可以共享参数
我们新增一个页面,使用同样的方式来获取SharedPreferences对象,不对对象赋值,只在页面上取值,我们来看看是不是能获取到上一个页面设置的内容。
ElevatedButton(
onPressed: () async {
_prefs = await SharedPreferences.getInstance();
setState(() {});
},
child: const Text('获取Shard实例'),
),
我们能看到页面上正常的获得到我们上一个页面设置的内容。
使用需要注意SharedPreferences的key是不能重复的,如果有相同的key,那么后存入的数据,会覆盖之前的数据。所以不要使用SharedPreferences进行页面传值,这是很危险的。
也许你会使用到的一些方法
获取所有存入的key
_prefs?.getKeys().forEach((element) {
print(element);
});
判断保存数据中是否包含某个key
var hasKey = _prefs?.containsKey("name");
print("name hasKey: $hasKey");
hasKey = _prefs?.containsKey("string");
print("string hasKey: $hasKey");
删除保存数据中的某个key
_prefs?.remove("string");
setState(() {});
清除所有持久化的数据
_prefs?.clear();
setState(() {});
重新加载所有数据
_prefs?.reload();
封装
SharedPreferences的使用非常的简单,但是在真实的项目中我们还是要做一些封装才能使用的,要不然每次时候的时候要初始化,还要考虑异步调用,确实还是很麻烦了。
首先我们的封装类应该是一个单例的,我们给他起个名字就叫SharedService吧。因为他在全局应该只有一个,在封装类中应保持一个SharedPreferences的引用,当然他也是单例的。这里我们使用dart的factory关键字来实现一个单例的声明。当然你也可以使用你喜欢的单例方法来做,只要保证是单例就可以了。
class SharedService {
//单例生命
factory SharedService() => _instance;
SharedService._internal();
static final SharedService _instance = SharedService._internal();
//保持一个SharedPreferences的引用
static late final SharedPreferences _sp;
//初始化方法,只需要调用一次。
static Future<SharedService> getInstance() async {
_sp = await SharedPreferences.getInstance();
return _instance;
}
...
}
\
既然是单例对象,那么我们在全局只需要实例化一次,实例化的位置当然是程序入口最合适,所以在我们的main函数中,来对我们的封装类进行实例化。
await SharedService.getInstance();
这个时候,你可能会看到这样的报错内容。内容很长也很生涩,大概是说只有在绑定初始化以后,ServicesBinding binding mixin上的“实例” getter才可用,这个时间通长应该在WidgetsFlutterBinding.ensureInitialized()或者runApp()之后。所以我们在我们的初始化方法之前来执行一下WidgetsFlutterBinding.ensureInitialized()。我们的代码就能正常工作了。
The "instance" getter on the ServicesBinding binding mixin is only available once that binding has been initialized.
Typically, this is done by calling "WidgetsFlutterBinding.ensureInitialized()" or "runApp()" (the latter calls the former). Typically this call is done in the "void main()" method. The "ensureInitialized" method is idempotent; calling it multiple times is not harmful. After calling that method, the "instance" getter will return the binding.
In a test, one can call "TestWidgetsFlutterBinding.ensureInitialized()" as the first line in the test's "main()" method to initialize the binding.
If ServicesBinding is a custom binding mixin, there must also be a custom binding class, like WidgetsFlutterBinding, but that mixes in the selected binding, and that is the class that must be constructed before using the "instance" getter.
就像这样处理一下就可以了。
Future<void> main() async {
// 初始化插件前需调用初始化代码 runApp()函数之前
WidgetsFlutterBinding.ensureInitialized();
//初始化SharedPreferences
await SharedService.getInstance();
...
}
接下来在SharedService中我们封装一下写入数据的方法。我找了很久如何让Map类型也可以在switch中使用,一直没有找到好办法,所以就单独对map类型行了一个处理。虽然不太好看,但是他工作起来都是正常的。
static void set<T>(String key, T value) {
Type type = value.runtimeType;
switch (type) {
case String:
_sp.setString(key, value as String);
break;
case int:
_sp.setInt(key, value as int);
break;
case bool:
_sp.setBool(key, value as bool);
break;
case double:
_sp.setDouble(key, value as double);
break;
case List<String>:
_sp.setStringList(key, value as List<String>);
break;
}
///Map类型有些特殊,不能直接判断Type的类型
///因为他是一个_InternalLinkedHashMap
///是一个私有的类型,我没有办法引用出来。
if (value is Map) {
_sp.setString(key, json.encode(value));
return;
}
}
然后是我们获取数据的方法,这里我们对String类型尝试进行json转换后返回,如果因为转换类型失败,那么会原样返回。
static Object? get<T>(String key) {
var value = _sp.get(key);
if (value is String) {
try {
return const JsonDecoder().convert(value as String)
as Map<String, dynamic>;
} on FormatException catch (e) {
return value;
}
}
return value;
}
最后我们对一些其他的方法进行个封装。
/// 获取数据中所有的key
static Set<String> getKeys() {
return _sp.getKeys();
}
/// 判断数据中是否包含某个key
static bool containsKey(String key) {
return _sp.containsKey(key);
}
/// 删除数据中某个key
static Future<bool> remove(String key) async {
return await _sp.remove(key);
}
/// 清除所有数据
static Future<bool> clear() async {
return await _sp.clear();
}
/// 重新加载
static Future<void> reload() async {
return await _sp.reload();
}
这样就差不多了,最后我们写个页面来测试一下。
///写入数据
SharedService.set("service_string", "service_string");
SharedService.set("service_int", 123);
SharedService.set("service_double", 2.2);
SharedService.set("service_bool", true);
SharedService.set("service_stringlist", ["a", "b"]);
SharedService.set("service_map", r'{"a": "1", "b": 2}');
setState(() {});
界面上读取数据.
Text(
SharedService.get("service_string")?.toString() ?? "null",
),
Text(
SharedService.get("service_int")?.toString() ?? "null",
),
Text(
SharedService.get("service_double")?.toString() ?? "null",
),
Text(
SharedService.get("service_bool")?.toString() ?? "null",
),
Text(
SharedService.get("service_stringlist")?.toString() ?? "null",
),
Text(
SharedService.get("service_map")?.toString() ?? "null",
),
Text(
(SharedService.get("service_map") as Map)["a"],
),
能看到这个界面就算OK了。
附件
不同平台的具体实现
源码分享地址:gitee.com/radium/flut…
文字分享地址:juejin.cn/user/350569…
视频分享地址:space.bilibili.com/1159595523
一般我会在白天思考要分享的什么样的内容,晚上整理代码和文字内容,一般会在当天晚上同时发布源码和文字内容,然后在第二天的晚上来录制视频。
分享内容的发布顺序,1源码->2文字->3视频,录制视频不易,一般会比文字晚1天发布。
\