路由参数持久化_Flutter在鸿蒙平台保存导航状态


概述
在社交应用开发中,路由参数的持久化是一个重要的需求。当用户从一个页面跳转到另一个页面时,如果应用在跳转过程中被系统杀死或发生异常重启,我们希望能够恢复之前的路由参数状态,确保用户体验的连续性。
路由参数持久化主要解决以下问题:
- 应用重启后恢复用户之前浏览的页面和参数
- 处理页面间跳转时的异常中断情况
- 支持应用冷启动时直接打开特定页面
核心概念
持久化存储机制
持久化存储是将数据保存到磁盘或其他非易失性存储介质中,确保应用关闭后数据不会丢失。在 Flutter 中,常用的持久化方案包括:
- SharedPreferences:轻量级键值对存储,适用于简单参数
- SQLite:关系型数据库,适用于复杂数据结构
- Hive:轻量级 NoSQL 数据库,性能优于 SQLite
- 文件存储:直接写入文件,适用于大文件或自定义格式
参数持久化流程
路由参数持久化的典型流程如下:
用户发起跳转 → 保存参数到持久化存储 → 打开目标页面 → 页面初始化时恢复参数 → 页面关闭时清理参数
持久化时机选择
不同的持久化时机适用于不同场景:
- 跳转前持久化:在 Navigator.push 之前保存参数,确保参数不会丢失
- 页面初始化时持久化:在页面 initState 中保存参数,适用于需要记录用户访问历史的场景
- 后台切换时持久化:监听应用生命周期,在应用进入后台时保存当前页面参数
代码实现
方式一:使用 SharedPreferences 存储参数
SharedPreferences 是 Flutter 中最常用的轻量级持久化方案,适合存储简单的键值对数据。
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
class ParamsStorage {
static const String _paramsKey = "route_params";
static const String _timestampKey = "params_timestamp";
static Future<void> saveParams(Map<String, dynamic> params) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_paramsKey, json.encode(params));
await prefs.setInt(_timestampKey, DateTime.now().millisecondsSinceEpoch);
}
static Future<Map<String, dynamic>?> getParams() async {
final prefs = await SharedPreferences.getInstance();
final jsonString = prefs.getString(_paramsKey);
if (jsonString != null) {
return json.decode(jsonString) as Map<String, dynamic>;
}
return null;
}
static Future<void> clearParams() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_paramsKey);
await prefs.remove(_timestampKey);
}
static Future<bool> isParamsExpired(int maxAgeMinutes) async {
final prefs = await SharedPreferences.getInstance();
final timestamp = prefs.getInt(_timestampKey);
if (timestamp == null) return true;
final ageMinutes = (DateTime.now().millisecondsSinceEpoch - timestamp) / 60000;
return ageMinutes > maxAgeMinutes;
}
}
在上面的代码中,我们封装了一个 ParamsStorage 类,提供了以下功能:
saveParams:保存参数到 SharedPreferencesgetParams:获取已保存的参数clearParams:清除已保存的参数isParamsExpired:检查参数是否过期
方式二:在页面初始化时恢复参数
在目标页面的 initState 方法中,我们可以检查路由参数是否存在,如果不存在则从持久化存储中恢复。
class RestoreParamsPage extends StatefulWidget {
const RestoreParamsPage({super.key});
State<RestoreParamsPage> createState() => _RestoreParamsPageState();
}
class _RestoreParamsPageState extends State<RestoreParamsPage> {
String userId = "";
String userName = "";
bool isLoading = true;
void initState() {
super.initState();
_restoreParams();
}
void _restoreParams() async {
setState(() {
isLoading = true;
});
final args = ModalRoute.of(context)?.settings.arguments;
if (args is Map<String, dynamic>) {
setState(() {
userId = args["userId"] ?? "";
userName = args["userName"] ?? "";
});
await ParamsStorage.saveParams(args);
} else {
final savedParams = await ParamsStorage.getParams();
final isExpired = await ParamsStorage.isParamsExpired(30);
if (savedParams != null && !isExpired) {
setState(() {
userId = savedParams["userId"] ?? "";
userName = savedParams["userName"] ?? "";
});
}
}
setState(() {
isLoading = false;
});
}
void dispose() {
ParamsStorage.clearParams();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("用户资料")),
body: isLoading
? const Center(child: CircularProgressIndicator())
: Center(child: Text("用户: " + userName)),
);
}
}
方式三:使用路由生成器统一处理
通过自定义路由生成器,我们可以在路由跳转前统一处理参数持久化逻辑。
Route<dynamic> generateRoute(RouteSettings settings) {
final routeName = settings.name;
final arguments = settings.arguments;
if (arguments != null) {
ParamsStorage.saveParams(arguments as Map<String, dynamic>);
}
switch (routeName) {
case "/profile":
return MaterialPageRoute(
builder: (context) => const RestoreParamsPage(),
settings: settings,
);
case "/message":
return MaterialPageRoute(
builder: (context) => const MessagePage(),
settings: settings,
);
default:
return MaterialPageRoute(builder: (context) => const HomePage());
}
}
方式四:封装持久化导航工具类
为了提高代码复用性,我们可以封装一个持久化导航工具类。
class PersistentNavigator {
static Future<T?> pushNamed<T extends Object?>(
BuildContext context,
String routeName, {
Object? arguments,
bool persist = true,
}) async {
if (persist && arguments != null) {
if (arguments is Map<String, dynamic>) {
await ParamsStorage.saveParams(arguments);
} else {
await ParamsStorage.saveParams({
"data": arguments.toString(),
"type": arguments.runtimeType.toString(),
});
}
}
return Navigator.pushNamed(context, routeName, arguments: arguments);
}
static void popAndPushNamed(
BuildContext context,
String routeName, {
Object? arguments,
}) {
Navigator.pop(context);
pushNamed(context, routeName, arguments: arguments);
}
static Future<void> restoreLastRoute(BuildContext context) async {
final params = await ParamsStorage.getParams();
final isExpired = await ParamsStorage.isParamsExpired(60);
if (params != null && !isExpired) {
final routeName = params["routeName"] ?? "/";
Navigator.pushNamed(context, routeName, arguments: params);
}
}
}
实际应用场景
场景一:应用重启后恢复页面
当用户正在浏览某个页面时,如果应用被系统杀死,重启后我们希望能够恢复到之前的页面。
class App extends StatelessWidget {
const App({super.key});
Widget build(BuildContext context) {
return MaterialApp(
home: FutureBuilder(
future: _checkLastRoute(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const SplashScreen();
}
final routeName = snapshot.data as String? ?? "/";
return HomePage(initialRoute: routeName);
},
),
);
}
Future<String?> _checkLastRoute() async {
final params = await ParamsStorage.getParams();
final isExpired = await ParamsStorage.isParamsExpired(120);
if (params != null && !isExpired) {
return params["routeName"] as String?;
}
return null;
}
}
场景二:页面间跳转异常恢复
在页面跳转过程中,如果发生异常导致页面无法正常打开,我们可以从持久化存储中恢复参数。
class SafeNavigatePage extends StatelessWidget {
const SafeNavigatePage({super.key});
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () async {
try {
await Navigator.pushNamed(
context,
"/target",
arguments: {"id": "123", "name": "test"},
);
} catch (e) {
final savedParams = await ParamsStorage.getParams();
if (savedParams != null) {
await Navigator.pushNamed(context, "/target", arguments: savedParams);
}
}
},
child: const Text("安全跳转"),
),
),
);
}
}
场景三:后台任务完成后导航
当后台任务完成后,我们可以根据之前保存的参数跳转到相应页面。
class BackgroundService {
static void handleTaskComplete(Map<String, dynamic> result) async {
final params = await ParamsStorage.getParams();
if (params != null) {
params["taskResult"] = result;
await ParamsStorage.saveParams(params);
}
}
static void navigateToResultPage(BuildContext context) async {
final params = await ParamsStorage.getParams();
if (params != null) {
Navigator.pushNamed(context, "/result", arguments: params);
await ParamsStorage.clearParams();
}
}
}
进阶用法
使用 Hive 存储复杂对象
对于复杂的对象参数,Hive 是一个更好的选择,它支持直接存储对象。
import 'package:hive/hive.dart';
class UserParams {
final String userId;
final String userName;
final int age;
UserParams({
required this.userId,
required this.userName,
required this.age,
});
}
class UserParamsAdapter extends TypeAdapter<UserParams> {
final typeId = 1;
UserParams read(BinaryReader reader) {
return UserParams(
userId: reader.readString(),
userName: reader.readString(),
age: reader.readInt(),
);
}
void write(BinaryWriter writer, UserParams obj) {
writer.writeString(obj.userId);
writer.writeString(obj.userName);
writer.writeInt(obj.age);
}
}
class HiveParamsStorage {
static const String _boxName = "route_params";
static Future<void> init() async {
Hive.registerAdapter(UserParamsAdapter());
}
static Future<void> saveUserParams(UserParams params) async {
final box = await Hive.openBox<UserParams>(_boxName);
await box.put("user", params);
}
static Future<UserParams?> getUserParams() async {
final box = await Hive.openBox<UserParams>(_boxName);
return box.get("user");
}
static Future<void> clearUserParams() async {
final box = await Hive.openBox<UserParams>(_boxName);
await box.delete("user");
}
}
使用 SQLite 存储参数历史
对于需要记录参数访问历史的场景,SQLite 是一个合适的选择。
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
class ParamsHistoryDB {
static Database? _database;
static Future<Database> get database async {
if (_database != null) return _database!;
_database = await initDB();
return _database!;
}
static Future<Database> initDB() async {
final path = join(await getDatabasesPath(), "params_history.db");
return await openDatabase(
path,
version: 1,
onCreate: (db, version) async {
await db.execute('''
CREATE TABLE params_history(
id INTEGER PRIMARY KEY AUTOINCREMENT,
route_name TEXT,
params TEXT,
timestamp INTEGER
)
''');
},
);
}
static Future<void> insertHistory(String routeName, Map<String, dynamic> params) async {
final db = await database;
await db.insert(
"params_history",
{
"route_name": routeName,
"params": json.encode(params),
"timestamp": DateTime.now().millisecondsSinceEpoch,
},
);
}
static Future<List<Map<String, dynamic>>> getHistory(int limit) async {
final db = await database;
return await db.query(
"params_history",
orderBy: "timestamp DESC",
limit: limit,
);
}
static Future<void> clearHistory() async {
final db = await database;
await db.delete("params_history");
}
}
参数版本管理
当参数结构发生变化时,我们需要进行版本管理,确保旧版本参数能够正确解析。
class VersionedParams {
static const int currentVersion = 2;
final String userId;
final String userName;
final int age;
final String? email;
final int version;
VersionedParams({
required this.userId,
required this.userName,
this.age = 0,
this.email,
this.version = currentVersion,
});
factory VersionedParams.fromMap(Map<String, dynamic> map) {
final version = map["version"] as int? ?? 1;
switch (version) {
case 2:
return VersionedParams(
userId: map["userId"] ?? "",
userName: map["userName"] ?? "",
age: map["age"] ?? 0,
email: map["email"],
version: 2,
);
case 1:
default:
return VersionedParams(
userId: map["userId"] ?? "",
userName: map["userName"] ?? "",
age: map["age"] ?? 0,
version: 1,
);
}
}
Map<String, dynamic> toMap() {
return {
"userId": userId,
"userName": userName,
"age": age,
"email": email,
"version": version,
};
}
}
注意事项
敏感信息处理
路由参数中可能包含敏感信息,如用户 token、密码等。这些信息不应该持久化到本地存储中。
class SecureParamsStorage {
static Future<void> saveParams(Map<String, dynamic> params) async {
final sanitizedParams = Map<String, dynamic>.from(params);
sanitizedParams.remove("token");
sanitizedParams.remove("password");
sanitizedParams.remove("authCode");
await ParamsStorage.saveParams(sanitizedParams);
}
}
参数过期策略
为了防止脏数据,我们需要设置参数过期时间。
class ExpiringParamsStorage {
static const int defaultExpireMinutes = 30;
static Future<void> saveParams(Map<String, dynamic> params, {int expireMinutes = defaultExpireMinutes}) async {
final now = DateTime.now();
final expireTime = now.add(Duration(minutes: expireMinutes));
final paramsWithExpire = Map<String, dynamic>.from(params);
paramsWithExpire["expireTime"] = expireTime.millisecondsSinceEpoch;
await ParamsStorage.saveParams(paramsWithExpire);
}
static Future<Map<String, dynamic>?> getParams() async {
final params = await ParamsStorage.getParams();
if (params == null) return null;
final expireTime = params["expireTime"] as int?;
if (expireTime != null && DateTime.now().millisecondsSinceEpoch > expireTime) {
await ParamsStorage.clearParams();
return null;
}
return params;
}
}
多页面参数冲突
当多个页面同时使用持久化存储时,可能会发生参数冲突。我们可以通过在参数中添加路由名称来区分。
class MultiPageParamsStorage {
static Future<void> saveParams(String routeName, Map<String, dynamic> params) async {
final paramsWithRoute = Map<String, dynamic>.from(params);
paramsWithRoute["routeName"] = routeName;
await ParamsStorage.saveParams(paramsWithRoute);
}
static Future<Map<String, dynamic>?> getParams(String routeName) async {
final params = await ParamsStorage.getParams();
if (params == null) return null;
final savedRouteName = params["routeName"] as String?;
if (savedRouteName != routeName) {
return null;
}
return params;
}
}
性能考虑
频繁的持久化操作会影响应用性能,我们可以使用以下策略优化:
class DebouncedParamsStorage {
static int? _lastSaveTime;
static const int debounceMilliseconds = 500;
static Future<void> saveParams(Map<String, dynamic> params) async {
final now = DateTime.now().millisecondsSinceEpoch;
if (_lastSaveTime != null && now - _lastSaveTime! < debounceMilliseconds) {
return;
}
_lastSaveTime = now;
await ParamsStorage.saveParams(params);
}
}
对比其他方案
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| SharedPreferences | 轻量、简单、无需额外依赖 | 仅支持基本类型、查询功能有限 | 简单参数、配置信息 |
| Hive | 支持对象存储、高性能 | 需要注册适配器 | 复杂对象、频繁读写 |
| SQLite | 完整的关系型数据库、支持复杂查询 | 配置复杂、性能较低 | 参数历史、数据分析 |
| 文件存储 | 灵活、自定义格式 | 需要自己管理文件 | 大文件、特殊格式 |
总结
路由参数持久化是确保应用稳定性和用户体验连续性的重要技术。通过合理选择持久化方案和实现策略,我们可以:
- 应用重启后恢复页面状态:使用 SharedPreferences 或 Hive 存储参数,在应用启动时检查并恢复
- 处理异常中断情况:在页面跳转前保存参数,发生异常时从存储中恢复
- 支持后台任务导航:后台任务完成后根据保存的参数跳转到相应页面
- 管理参数版本:当参数结构变化时进行版本兼容处理
- 保护敏感信息:不将敏感数据持久化到本地存储
在实际开发中,我们应该根据项目需求和参数复杂度选择合适的持久化方案,并注意性能优化和数据安全问题。
更多推荐



所有评论(0)