在这里插入图片描述
在这里插入图片描述

概述

在社交应用开发中,路由参数的持久化是一个重要的需求。当用户从一个页面跳转到另一个页面时,如果应用在跳转过程中被系统杀死或发生异常重启,我们希望能够恢复之前的路由参数状态,确保用户体验的连续性。

路由参数持久化主要解决以下问题:

  • 应用重启后恢复用户之前浏览的页面和参数
  • 处理页面间跳转时的异常中断情况
  • 支持应用冷启动时直接打开特定页面

核心概念

持久化存储机制

持久化存储是将数据保存到磁盘或其他非易失性存储介质中,确保应用关闭后数据不会丢失。在 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:保存参数到 SharedPreferences
  • getParams:获取已保存的参数
  • 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 完整的关系型数据库、支持复杂查询 配置复杂、性能较低 参数历史、数据分析
文件存储 灵活、自定义格式 需要自己管理文件 大文件、特殊格式

总结

路由参数持久化是确保应用稳定性和用户体验连续性的重要技术。通过合理选择持久化方案和实现策略,我们可以:

  1. 应用重启后恢复页面状态:使用 SharedPreferences 或 Hive 存储参数,在应用启动时检查并恢复
  2. 处理异常中断情况:在页面跳转前保存参数,发生异常时从存储中恢复
  3. 支持后台任务导航:后台任务完成后根据保存的参数跳转到相应页面
  4. 管理参数版本:当参数结构变化时进行版本兼容处理
  5. 保护敏感信息:不将敏感数据持久化到本地存储

在实际开发中,我们应该根据项目需求和参数复杂度选择合适的持久化方案,并注意性能优化和数据安全问题。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐