Flutter—— 本地存储(shared_preferences)

23 阅读4分钟

一、简介

shared_preferences 是 Flutter 官方提供的键值对(Key-Value) 本地存储插件,本质是对原生平台存储的封装:

  • iOS:封装 NSUserDefaults
  • Android:封装 SharedPreferences
  • 桌面端(Windows/macOS):封装本地 JSON 文件
  • 核心特点:轻量、API 简单、持久化(APP 重启 / 卸载前数据不丢失)、仅支持基础数据类型

二、支持的数据类型

类型对应 API 方法说明
字符串setString()/getString()存储 token、用户名等
布尔值setBool()/getBool()存储开关状态、是否登录等
整数setInt()/getInt()存储计数、ID 等
浮点数setDouble()/getDouble()存储版本号、数值配置等
字符串列表setStringList()/getStringList()存储历史记录、标签等

三、基础使用步骤

1. 安装依赖

pub.dev/packages/sh…

pubspec.yaml 中添加:

dependencies:
  flutter:
    sdk: flutter
  shared_preferences: ^2.5.4 # 推荐使用最新稳定版

2. API 使用

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

void main() => runApp(const MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'SharedPreferences 详解',
      home: const SPExamplePage(),
    );
  }
}

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

  @override
  State<SPExamplePage> createState() => _SPExamplePageState();
}

class _SPExamplePageState extends State<SPExamplePage> {
  // 存储的测试数据
  String _userToken = "";
  bool _isDarkMode = false;
  int _loginCount = 0;
  double _appVersion = 1.0;
  List<String> _historyList = [];

  // 初始化:页面加载时读取存储的数据
  @override
  void initState() {
    super.initState();
    _loadAllData();
  }

  // ========== 核心方法1:读取数据 ==========
  Future<void> _loadAllData() async {
    // 1. 获取 SharedPreferences 实例(必须异步)
    SharedPreferences prefs = await SharedPreferences.getInstance();

    // 2. 读取数据:第二个参数是「默认值」(key不存在时返回)
    setState(() {
      _userToken = prefs.getString("user_token") ?? ""; // 字符串默认空
      _isDarkMode = prefs.getBool("dark_mode") ?? false; // 布尔默认false
      _loginCount = prefs.getInt("login_count") ?? 0; // 整数默认0
      _appVersion = prefs.getDouble("app_version") ?? 1.0; // 浮点数默认1.0
      _historyList = prefs.getStringList("browse_history") ?? []; // 列表默认空
    });
  }

  // ========== 核心方法2:保存数据 ==========
  Future<void> _saveAllData() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();

    // 1. 保存单个数据
    await prefs.setString("user_token", "abc123456789");
    await prefs.setBool("dark_mode", true);
    await prefs.setInt("login_count", _loginCount + 1); // 计数+1
    await prefs.setDouble("app_version", 2.1);
    await prefs.setStringList("browse_history", ["首页", "我的", "设置"]);

    // 2. 保存后刷新页面数据
    _loadAllData();

    // 提示用户
    if (mounted) { // 防止页面销毁后调用context
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text("数据保存成功 ✅")),
      );
    }
  }

  // ========== 核心方法3:删除数据 ==========
  Future<void> _deleteData() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();

    // 方式1:删除单个key
    await prefs.remove("user_token");

    // 方式2:清空所有数据(谨慎使用!)
    // await prefs.clear();

    // 刷新数据
    _loadAllData();
    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text("数据删除成功 ❌")),
      );
    }
  }

  // ========== 核心方法4:检查key是否存在 ==========
  Future<void> _checkKeyExists() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    bool hasToken = prefs.containsKey("user_token");
    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text("user_token 是否存在:$hasToken")),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("SharedPreferences 详解")),
      body: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 显示存储的数据
            Text("用户Token:$_userToken"),
            Text("深色模式:$_isDarkMode"),
            Text("登录次数:$_loginCount"),
            Text("APP版本:$_appVersion"),
            Text("浏览历史:${_historyList.join(", ")}"),
            const SizedBox(height: 30),

            // 操作按钮
            Row(
              children: [
                ElevatedButton(
                  onPressed: _saveAllData,
                  child: const Text("保存数据"),
                ),
                const SizedBox(width: 10),
                ElevatedButton(
                  onPressed: _deleteData,
                  style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
                  child: const Text("删除Token"),
                ),
              ],
            ),
            const SizedBox(height: 10),
            ElevatedButton(
              onPressed: _checkKeyExists,
              child: const Text("检查Token是否存在"),
            ),
          ],
        ),
      ),
    );
  }
}

四、进阶技巧

1. 封装工具类(避免重复代码)

实际项目中建议封装成单例工具类,统一管理存储的 key 和方法:

import 'package:shared_preferences/shared_preferences.dart';

class SPUtil {
  // 单例模式
  static SPUtil? _instance;
  static SharedPreferences? _prefs;

  // 私有化构造函数
  SPUtil._();

  // 获取单例
  static Future<SPUtil> getInstance() async {
    if (_instance == null) {
      _instance = SPUtil._();
    }
    if (_prefs == null) {
      _prefs = await SharedPreferences.getInstance();
    }
    return _instance!;
  }

  // ========== 定义存储的key(统一管理,避免拼写错误) ==========
  static const String KEY_USER_TOKEN = "user_token";
  static const String KEY_DARK_MODE = "dark_mode";
  static const String KEY_LOGIN_COUNT = "login_count";

  // ========== 封装常用方法 ==========
  // 保存字符串
  Future<void> setString(String key, String value) async {
    await _prefs?.setString(key, value);
  }

  // 读取字符串
  String getString(String key, {String defaultValue = ""}) {
    return _prefs?.getString(key) ?? defaultValue;
  }

  // 保存布尔值
  Future<void> setBool(String key, bool value) async {
    await _prefs?.setBool(key, value);
  }

  // 读取布尔值
  bool getBool(String key, {bool defaultValue = false}) {
    return _prefs?.getBool(key) ?? defaultValue;
  }

  // 删除单个key
  Future<void> remove(String key) async {
    await _prefs?.remove(key);
  }

  // 清空所有数据
  Future<void> clear() async {
    await _prefs?.clear();
  }
}

// 使用示例
void useSPUtil() async {
  SPUtil spUtil = await SPUtil.getInstance();
  // 保存
  await spUtil.setString(SPUtil.KEY_USER_TOKEN, "123456");
  // 读取
  String token = spUtil.getString(SPUtil.KEY_USER_TOKEN);
  print("Token:$token");
}

2. 存储复杂对象(序列化 / 反序列化)

shared_preferences 不支持直接存储对象,需先转 JSON 字符串:

import 'dart:convert';

// 定义用户模型
class User {
  String name;
  int age;
  String email;

  User({required this.name, required this.age, required this.email});

  // 转JSON字符串
  String toJson() {
    Map<String, dynamic> map = {
      "name": name,
      "age": age,
      "email": email,
    };
    return json.encode(map);
  }

  // 从JSON字符串转对象
  static User fromJson(String jsonStr) {
    Map<String, dynamic> map = json.decode(jsonStr);
    return User(
      name: map["name"],
      age: map["age"],
      email: map["email"],
    );
  }
}

// 存储/读取对象
Future<void> saveUser() async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
  // 1. 创建对象
  User user = User(name: "张三", age: 25, email: "zhangsan@example.com");
  // 2. 转JSON字符串保存
  await prefs.setString("user_info", user.toJson());
  // 3. 读取并转对象
  String userJson = prefs.getString("user_info") ?? "";
  if (userJson.isNotEmpty) {
    User savedUser = User.fromJson(userJson);
    print("用户名:${savedUser.name},年龄:${savedUser.age}");
  }
}

五、避坑指南(常见问题)

1. 同步 / 异步问题(最容易踩坑)

  • ❌ 错误:在 initState 中同步调用 getStringgetInstance 是异步的)

    @override
    void initState() {
      super.initState();
      // 错误!SharedPreferences.getInstance() 是异步,不能直接同步调用
      String token = SharedPreferences.getInstance().then((prefs) => prefs.getString("token"));
    }
    
  • ✅ 正确:用 async/awaitthen 处理异步

    @override
    void initState() {
      super.initState();
      _loadData(); // 异步方法
    }
    
    Future<void> _loadData() async {
      SharedPreferences prefs = await SharedPreferences.getInstance();
      String token = prefs.getString("token") ?? "";
    }
    

2. 页面销毁后调用 setState

  • 问题:异步操作完成后页面已销毁,调用 setState 会报错

  • 解决:用 mounted 判断页面是否挂载

    Future<void> _loadData() async {
      SharedPreferences prefs = await SharedPreferences.getInstance();
      if (mounted) { // 关键:判断页面是否还在
        setState(() {
          _token = prefs.getString("token") ?? "";
        });
      }
    }
    

3. 数据未及时刷新

  • 问题:保存数据后,UI 没有实时更新

  • 解决:保存后重新调用读取方法,触发 setState

    await prefs.setString("token", "new_token");
    _loadData(); // 重新读取,刷新UI