MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Flutter数据存储:SharedPreferences的本地存储应用

2024-06-024.4k 阅读

Flutter 本地存储简介

在移动应用开发中,数据存储是一个至关重要的环节。本地存储允许应用在设备上保存数据,以便在应用关闭和重新打开后仍能访问这些数据。Flutter 提供了多种本地存储解决方案,其中 SharedPreferences 是一种轻量级、简单易用的键值对存储方式,适用于存储少量的简单数据,如用户设置、应用配置等。

SharedPreferences 基础

SharedPreferences 是 Flutter 提供的一种持久化存储机制,它基于 Android 平台的 SharedPreferences 和 iOS 平台的 NSUserDefaults 实现。它以键值对的形式存储数据,支持的数据类型包括布尔值(bool)、整数(int)、双精度浮点数(double)、字符串(string)和字符串列表(List<String>)。

引入依赖

在使用 SharedPreferences 之前,需要在 pubspec.yaml 文件中引入依赖:

dependencies:
  shared_preferences: ^2.0.15

然后运行 flutter pub get 来获取依赖。

读写数据

读取数据

要读取 SharedPreferences 中的数据,首先需要获取 SharedPreferences 的实例。可以通过 SharedPreferences.getInstance() 方法异步获取实例:

import 'package:shared_preferences/shared_preferences.dart';

Future<void> readData() async {
  final prefs = await SharedPreferences.getInstance();
  // 读取布尔值
  final bool? boolValue = prefs.getBool('bool_key');
  // 读取整数
  final int? intValue = prefs.getInt('int_key');
  // 读取双精度浮点数
  final double? doubleValue = prefs.getDouble('double_key');
  // 读取字符串
  final String? stringValue = prefs.getString('string_key');
  // 读取字符串列表
  final List<String>? stringListValue = prefs.getStringList('string_list_key');
}

在上述代码中,getBoolgetIntgetDoublegetStringgetStringList 方法分别用于获取相应类型的数据。如果键不存在,这些方法将返回 null

写入数据

写入数据同样需要获取 SharedPreferences 的实例,然后使用相应的 set 方法来写入数据:

import 'package:shared_preferences/shared_preferences.dart';

Future<void> writeData() async {
  final prefs = await SharedPreferences.getInstance();
  // 写入布尔值
  await prefs.setBool('bool_key', true);
  // 写入整数
  await prefs.setInt('int_key', 42);
  // 写入双精度浮点数
  await prefs.setDouble('double_key', 3.14);
  // 写入字符串
  await prefs.setString('string_key', 'Hello, SharedPreferences!');
  // 写入字符串列表
  await prefs.setStringList('string_list_key', ['item1', 'item2']);
}

上述代码中,setBoolsetIntsetDoublesetStringsetStringList 方法分别用于设置相应类型的数据。这些方法返回一个 Future<bool>,表示操作是否成功。

数据类型处理细节

布尔值

布尔值的存储和读取相对简单。在存储时,使用 setBool 方法,在读取时使用 getBool 方法。注意,当读取不存在的布尔值键时,会返回 null。如果应用有默认值需求,可以在获取时进行判断并设置默认值:

Future<void> handleBoolData() async {
  final prefs = await SharedPreferences.getInstance();
  // 写入布尔值
  await prefs.setBool('is_user_logged_in', true);
  // 读取布尔值并设置默认值
  final bool isLoggedIn = prefs.getBool('is_user_logged_in') ?? false;
}

整数

整数的存储和读取使用 setIntgetInt 方法。与布尔值类似,读取不存在的键会返回 null。如果应用对整数有特定的取值范围要求,例如表示等级的整数不能小于 1,在写入时可以进行验证:

Future<void> handleIntData() async {
  final prefs = await SharedPreferences.getInstance();
  int userLevel = 5;
  // 验证并写入
  if (userLevel >= 1) {
    await prefs.setInt('user_level', userLevel);
  }
  // 读取整数并设置默认值
  final int level = prefs.getInt('user_level') ?? 1;
}

双精度浮点数

双精度浮点数用于存储小数。SharedPreferences 使用 setDoublegetDouble 方法来处理双精度浮点数。在存储一些精度要求较高的数据,如经纬度时会很有用:

Future<void> handleDoubleData() async {
  final prefs = await SharedPreferences.getInstance();
  double latitude = 34.0522;
  double longitude = -118.2437;
  await prefs.setDouble('latitude', latitude);
  await prefs.setDouble('longitude', longitude);
  final double? storedLatitude = prefs.getDouble('latitude');
  final double? storedLongitude = prefs.getDouble('longitude');
}

字符串

字符串是最常用的数据类型之一。SharedPreferences 提供了 setStringgetString 方法。字符串可以用于存储文本信息,如用户的昵称、应用的版本号等:

Future<void> handleStringData() async {
  final prefs = await SharedPreferences.getInstance();
  String username = 'JohnDoe';
  await prefs.setString('username', username);
  final String? storedUsername = prefs.getString('username');
}

字符串列表

字符串列表适用于存储一组相关的文本数据,如用户收藏的文章标题列表。SharedPreferences 使用 setStringListgetStringList 方法来处理字符串列表:

Future<void> handleStringListData() async {
  final prefs = await SharedPreferences.getInstance();
  List<String> favoriteArticles = ['Article 1', 'Article 2', 'Article 3'];
  await prefs.setStringList('favorite_articles', favoriteArticles);
  final List<String>? storedArticles = prefs.getStringList('favorite_articles');
}

应用场景示例

用户设置存储

假设应用有一个夜间模式的设置,用户可以切换夜间模式的开关。可以使用 SharedPreferences 来存储这个设置:

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

class ThemeSettings {
  static const String key = 'is_night_mode';

  static Future<bool> getIsNightMode() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getBool(key) ?? false;
  }

  static Future<void> setIsNightMode(bool value) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool(key, value);
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FutureBuilder<bool>(
      future: ThemeSettings.getIsNightMode(),
      builder: (context, snapshot) {
        if (!snapshot.hasData) {
          return Container();
        }
        final isNightMode = snapshot.data!;
        return MaterialApp(
          theme: isNightMode? ThemeData.dark() : ThemeData.light(),
          home: Scaffold(
            appBar: AppBar(
              title: Text('Theme Setting Example'),
            ),
            body: Center(
              child: Switch(
                value: isNightMode,
                onChanged: (value) async {
                  await ThemeSettings.setIsNightMode(value);
                  setState(() {});
                },
              ),
            ),
          ),
        );
      },
    );
  }
}

在上述代码中,ThemeSettings 类封装了获取和设置夜间模式的逻辑。MyApp 组件通过 FutureBuilder 根据存储的夜间模式设置来构建应用的主题。

登录状态管理

可以使用 SharedPreferences 来存储用户的登录状态。当用户登录成功后,将登录状态设置为 true,在应用每次启动时检查登录状态:

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

class LoginStatus {
  static const String key = 'is_logged_in';

  static Future<bool> getIsLoggedIn() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getBool(key) ?? false;
  }

  static Future<void> setIsLoggedIn(bool value) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool(key, value);
  }
}

class LoginPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Login Page'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () async {
            // 模拟登录成功
            await LoginStatus.setIsLoggedIn(true);
            Navigator.pushReplacementNamed(context, '/home');
          },
          child: Text('Login'),
        ),
      ),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Home Page'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () async {
            // 模拟注销
            await LoginStatus.setIsLoggedIn(false);
            Navigator.pushReplacementNamed(context, '/login');
          },
          child: Text('Logout'),
        ),
      ),
    );
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FutureBuilder<bool>(
      future: LoginStatus.getIsLoggedIn(),
      builder: (context, snapshot) {
        if (!snapshot.hasData) {
          return Container();
        }
        final isLoggedIn = snapshot.data!;
        return MaterialApp(
          initialRoute: isLoggedIn? '/home' : '/login',
          routes: {
            '/login': (context) => LoginPage(),
            '/home': (context) => HomePage(),
          },
        );
      },
    );
  }
}

在这个示例中,LoginStatus 类负责管理登录状态的存储和获取。MyApp 组件根据登录状态决定应用的初始路由。

注意事项

数据大小限制

虽然 SharedPreferences 适用于存储少量数据,但不同平台对其存储大小有一定限制。在 Android 上,一般建议存储的数据量不超过 1MB。如果存储的数据量过大,可能会导致性能问题或存储失败。因此,在使用 SharedPreferences 时,要确保存储的数据量在合理范围内。

异步操作

SharedPreferences 的读取和写入操作都是异步的。这意味着在获取 SharedPreferences 实例以及进行读写操作时,都需要使用 await 关键字或处理 Future。如果不处理异步操作,可能会导致数据读取或写入不及时,影响应用的逻辑。

数据一致性

由于 SharedPreferences 的写入操作是异步的,在多个地方同时写入数据时,可能会出现数据一致性问题。例如,一个地方写入了 A 值,另一个地方同时写入了 B 值,最终存储的值可能不确定。为了避免这种情况,可以在关键的写入操作处使用同步机制,或者在读取数据时进行验证和修复。

兼容性

虽然 SharedPreferences 基于 Android 和 iOS 原生的存储机制实现,但在不同版本的操作系统上可能会有一些细微差异。在开发过程中,要进行充分的测试,确保应用在各种目标平台和版本上都能正常工作。

性能优化

批量操作

尽量避免频繁的单次读写操作。可以将多个相关的数据读写操作合并为一次批量操作。例如,同时读取多个键的值或者同时设置多个键的值。这样可以减少异步操作的次数,提高性能。

Future<void> batchOperation() async {
  final prefs = await SharedPreferences.getInstance();
  final batch = prefs.batch();
  batch.setBool('bool_key', true);
  batch.setInt('int_key', 42);
  batch.setString('string_key', 'Batch operation');
  await batch.commit();
}

在上述代码中,使用 batch 方法创建一个 SharedPreferencesBatch 对象,然后通过该对象进行多个设置操作,最后使用 commit 方法提交这些操作。

缓存策略

对于一些不经常变化的数据,可以在内存中进行缓存。在应用启动时读取 SharedPreferences 中的数据并缓存到内存中,在需要使用这些数据时优先从内存缓存中获取。只有当缓存数据过期或者需要更新时,才重新从 SharedPreferences 中读取。这样可以减少对 SharedPreferences 的读取次数,提高应用的响应速度。

class DataCache {
  static Map<String, dynamic> _cache = {};

  static Future<dynamic> getValue(String key) async {
    if (_cache.containsKey(key)) {
      return _cache[key];
    }
    final prefs = await SharedPreferences.getInstance();
    final value = prefs.get(key);
    if (value != null) {
      _cache[key] = value;
    }
    return value;
  }

  static Future<void> setValue(String key, dynamic value) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.set(key, value);
    _cache[key] = value;
  }
}

在上述代码中,DataCache 类实现了一个简单的缓存机制。getValue 方法优先从缓存中获取数据,如果缓存中没有,则从 SharedPreferences 中读取并更新缓存。setValue 方法在更新 SharedPreferences 的同时也更新缓存。

与其他存储方式的比较

与 SQLite 的比较

  • 数据结构SharedPreferences 以简单的键值对形式存储数据,适用于存储少量、结构简单的数据。而 SQLite 是一种关系型数据库,支持复杂的数据结构和关系,适用于存储大量结构化数据,如应用的用户信息表、订单列表等。
  • 性能:对于简单的读写操作,SharedPreferences 的性能较好,因为它的操作相对简单。但在处理大量数据的复杂查询和事务时,SQLite 更具优势,它可以通过索引等机制优化查询性能,并且支持事务处理保证数据一致性。
  • 使用难度SharedPreferences 使用非常简单,只需要进行基本的键值对读写操作。而 SQLite 的使用相对复杂,需要了解 SQL 语句以及数据库的设计和管理。

与文件存储的比较

  • 数据格式SharedPreferences 只能存储特定的数据类型(布尔值、整数、双精度浮点数、字符串和字符串列表),数据格式相对固定。文件存储则更加灵活,可以存储任意格式的数据,如 JSON、XML 等序列化后的数据。
  • 存储位置和管理SharedPreferences 由系统管理存储位置,不同平台有固定的存储路径,应用开发者无需过多关心存储位置。文件存储则需要开发者自己管理文件的路径、命名和权限等,灵活性高但也增加了开发的复杂度。
  • 数据访问SharedPreferences 通过键值对直接访问数据,速度较快。文件存储则需要读取整个文件并解析数据,对于大数据量的文件读取和解析可能会消耗更多资源和时间。

高级应用

数据加密存储

在一些对数据安全性要求较高的场景下,可以对 SharedPreferences 中存储的数据进行加密。可以使用 Flutter 的加密库,如 encrypt 库。首先在 pubspec.yaml 文件中引入依赖:

dependencies:
  encrypt: ^5.0.0

然后对数据进行加密和解密操作:

import 'package:encrypt/encrypt.dart';
import 'package:shared_preferences/shared_preferences.dart';

class EncryptedPreferences {
  static final Encrypter encrypter = Encrypter(AES(Key.fromLength(32)));

  static Future<void> setEncryptedString(String key, String value) async {
    final encrypted = encrypter.encrypt(value);
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(key, encrypted.base64);
  }

  static Future<String?> getEncryptedString(String key) async {
    final prefs = await SharedPreferences.getInstance();
    final encryptedBase64 = prefs.getString(key);
    if (encryptedBase64 == null) {
      return null;
    }
    final encrypted = Encrypted.fromBase64(encryptedBase64);
    return encrypter.decrypt(encrypted);
  }
}

在上述代码中,EncryptedPreferences 类封装了对字符串数据的加密存储和读取操作。使用 AES 加密算法对字符串进行加密和解密,在存储时将加密后的结果以 Base64 编码的形式存储在 SharedPreferences 中。

数据版本控制

随着应用的更新,存储在 SharedPreferences 中的数据结构可能会发生变化。为了确保数据的兼容性,可以引入数据版本控制。在存储数据时,同时存储一个版本号。当应用启动时,检查版本号并根据版本号进行数据迁移:

import 'package:shared_preferences/shared_preferences.dart';

class DataVersion {
  static const String key = 'data_version';
  static const int currentVersion = 2;

  static Future<int> getVersion() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getInt(key) ?? 0;
  }

  static Future<void> setVersion(int version) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setInt(key, version);
  }

  static Future<void> migrateData() async {
    final version = await getVersion();
    if (version == 0) {
      // 版本 0 到版本 1 的迁移逻辑
      final prefs = await SharedPreferences.getInstance();
      final oldValue = prefs.getString('old_key');
      if (oldValue != null) {
        await prefs.setString('new_key', oldValue);
        await prefs.remove('old_key');
      }
      await setVersion(1);
    }
    if (version == 1) {
      // 版本 1 到版本 2 的迁移逻辑
      final prefs = await SharedPreferences.getInstance();
      final oldIntValue = prefs.getInt('old_int_key');
      if (oldIntValue != null) {
        final newIntValue = oldIntValue * 2;
        await prefs.setInt('new_int_key', newIntValue);
        await prefs.remove('old_int_key');
      }
      await setVersion(2);
    }
  }
}

在上述代码中,DataVersion 类管理数据的版本号。migrateData 方法根据当前数据版本号执行相应的迁移逻辑,确保数据在应用更新后仍能正常使用。

通过以上对 SharedPreferences 在 Flutter 中的详细介绍,包括基础使用、应用场景、注意事项、性能优化以及与其他存储方式的比较和高级应用等方面,开发者可以全面掌握 SharedPreferences 在 Flutter 本地存储中的应用,根据应用的需求合理选择和使用这种存储方式。