一文读懂Flutter RouteDelagate 路由

Flutter 路由委托(RouteDelegate)深度解析

在 Flutter 中,路由委托(RouteDelegate) 是 Navigator 2.0 引入的核心路由管理机制,用于替代传统 Navigator 1.0 的命令式路由(如 push/pop),提供声明式、可预测、可测试的路由管理方案。它允许开发者完全掌控路由栈的状态、跳转逻辑和页面映射,是大型应用(如多模块、深链接、复杂权限控制)的首选路由方案。

一、核心概念:为什么需要 RouteDelegate?

Navigator 1.0 的痛点(命令式路由):

  • 路由状态分散,难以统一管理(如页面栈状态无法持久化、无法通过状态反向控制路由);
  • 不支持深链接(如 app://your-app/home/detail?id=1)的无缝集成;
  • 权限控制、路由拦截需要额外封装,逻辑冗余;
  • 测试困难,无法模拟路由状态。

Navigator 2.0 的核心改善(基于路由委托):

  • 声明式路由:路由状态与 UI 状态绑定,通过修改状态自动更新路由栈;
  • 完全可控:开发者掌控路由解析、页面创建、栈操作的全流程;
  • 深链接支持:原生支持 URL 解析与路由映射;
  • 可测试性:路由状态可模拟,便于单元测试。

核心组件关系

RouteDelegate 并非孤立存在,需与以下组件配合使用,构成完整的 Navigator 2.0 路由体系:

组件

作用

RouteDelegate

路由委托,核心逻辑载体(状态管理、路由解析、页面创建)

Router

路由容器,通过 delegate 属性关联 RouteDelegate,渲染 Navigator

RouteInformationParser

路由信息解析器,将 URL(如 /home/detail?id=1)解析为自定义路由状态

RouteInformationProvider

路由信息提供者,管理路由信息(如当前 URL),支持历史记录(前进 / 后退)

Navigator

由 RouteDelegate 创建,负责渲染页面栈(与 Navigator 1.0 功能一致)

二、RouteDelegate 核心原理

RouteDelegate 是一个抽象类,核心是通过状态驱动路由

  1. 维护一个「路由状态」(如 List<String> routes 或自定义 RouteConfig);
  2. 当路由状态变化时,RouteDelegate 的 build 方法重新执行,创建新的 Navigator 和页面栈;
  3. 外部通过 RouterDelegate.notifyListeners() 触发路由刷新;
  4. 系统通过 setNewRoutePath 方法接收外部路由信息(如深链接、浏览器前进 / 后退)。

必须实现的核心方法

abstract class RouterDelegate<T> extends Listenable {
  // 1. 构建 Navigator,定义页面栈
  Widget build(BuildContext context);

  // 2. 接收外部路由信息(如深链接、系统导航),更新路由状态
  Future<void> setNewRoutePath(T configuration);

  // 3. 获取当前路由状态(用于系统历史记录、深链接反向解析)
  T get currentConfiguration;
  
  // 4. 页面返回的时候需要重写此方法,列如Android平台在首页点击再次返回键时,直接退出应用
  Future<bool> popRoute()
}

三、完整 Demo:自定义 RouteDelegate 实现

以下通过一个完整可运行的 Demo,演示 RouteDelegate 的核心用法,包含:

  • 路由状态管理;
  • 页面映射与跳转;
  • 深链接支持;
  • 前进 / 后退功能。普通页点击返回页直接返回上一页,退到首页时点击一次返回键弹出“再按一次退出应用”的提示,2s内按返回键退出应用
  1. 项目结构

一文读懂Flutter RouteDelagate 路由

  1. 路由配置(route_config.dart)

定义路由名称、页面映射、路由状态模型:

import 'package:flutter/material.dart';
import 'home_page.dart';
import 'detail_page.dart';
import 'setting_page.dart';

class RouteNames {
  static const String home = '/';
  static const String detail = '/detail';
  static const String setting = '/setting';
}

class RouteConfig {
  final String path;
  final Map<String, String>? params;

  RouteConfig(this.path, {this.params});
}

// 修正:页面构建器只需要 context(不需要 RouteConfig,参数通过全局路由获取)
final Map<String, WidgetBuilder> routePages = {
  RouteNames.home: (context) => const HomePage(),
  RouteNames.detail: (context) => const DetailPage(),
  RouteNames.setting: (context) => const SettingPage(),
};
  1. 路由信息解析器(app_route_parser.dart)

将系统路由信息(如 URL 字符串)解析为自定义 AppRouteConfig:

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

class AppRouteParser extends RouteInformationParser<RouteConfig> {
  @override
  Future<RouteConfig> parseRouteInformation(RouteInformation routeInfo) async {
    final uri = Uri.parse(routeInfo.location ?? RouteNames.home);
    return RouteConfig(uri.path, params: uri.queryParameters);
  }

  @override
  RouteInformation restoreRouteInformation(RouteConfig config) {
    final location = config.params?.isEmpty ?? true
        ? config.path
        : '${config.path}?${Uri(queryParameters: config.params).query}';
    return RouteInformation(location: location);
  }
}
  1. 自定义 RouteDelegate(app_router_delegate.dart)

核心逻辑载体,管理路由状态、构建页面栈、处理跳转:

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'route_config.dart';

final globalRouter = AppRouterDelegate();

class AppRouterDelegate extends RouterDelegate<RouteConfig>
    with ChangeNotifier, PopNavigatorRouterDelegateMixin<RouteConfig> {
  final List<RouteConfig> _stack = [RouteConfig(RouteNames.home)];
  bool _firstBack = true;
  final _messengerKey = GlobalKey<ScaffoldMessengerState>();

  @override
  GlobalKey<NavigatorState>? get navigatorKey => GlobalKey<NavigatorState>();

  @override
  RouteConfig get currentConfiguration => _stack.last;

  // 唯一返回逻辑入口
  @override
  Future<bool> popRoute() async {
    // 子页面:出栈并刷新
    if (_stack.length > 1) {
      _stack.removeLast();
      _firstBack = true;
      notifyListeners();
      return true;
    }

    // 首页:首次返回拦截,二次返回退出
    if (_firstBack) {
      _showToast();
      _firstBack = false;
      return true; // 告知 Flutter 已处理,不触发系统退出
    } else {
      Platform.isAndroid ? SystemNavigator.pop() : exit(0);
      return true;
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      scaffoldMessengerKey: _messengerKey,
      theme: ThemeData(primarySwatch: Colors.blue),
      home: Navigator(
        key: navigatorKey,
        // 修正:调用页面构建器时只传 context(匹配 routePages 类型)
        pages: _stack.map((config) => MaterialPage(
          key: ValueKey('${config.path}_${_stack.length}'), // 确保 key 唯一
          child: routePages[config.path]!(context), // 目前类型匹配,无报错
        )).toList(),
        onPopPage: (route, result) {
          if (route.didPop(result)) {
            popRoute();
            return true;
          }
          return false;
        },
      ),
    );
  }

  @override
  Future<void> setNewRoutePath(RouteConfig config) async {
    _stack.clear();
    _stack.add(config);
    notifyListeners();
  }

  // 跳转方法(支持带参数)
  void push(String path, {Map<String, String>? params}) {
    _stack.add(RouteConfig(path, params: params));
    _firstBack = true;
    notifyListeners();
  }

  // 获取当前页面参数(供详情页使用)
  Map<String, String>? get currentParams => _stack.last.params;

  // 显示提示
  void _showToast() {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _messengerKey.currentState?.showSnackBar(
        const SnackBar(
          content: Text('再按一次返回键退出应用'),
          duration: Duration(seconds: 1),
          behavior: SnackBarBehavior.floating,
        ),
      );
    });
  }
}
  1. 页面实现(pages/)

home_page.dart(首页)

import 'package:flutter/material.dart';
import 'app_router_delegate.dart';
import 'route_config.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('首页')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () {
                // 跳转到详情页(带参数)
                globalRouter.push(
                  RouteNames.detail,
                  params: {'id': '1001', 'name': 'Flutter'},
                );
              },
              child: const Text('跳转到详情页(带参数)'),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () => globalRouter.push(RouteNames.setting),
              child: const Text('跳转到设置页'),
            ),
          ],
        ),
      ),
    );
  }
}

detail_page.dart

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

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

  @override
  Widget build(BuildContext context) {
    // 从全局路由获取参数
    final params = globalRouter.currentParams;
    final id = params?['id'] ?? '未知';
    final name = params?['name'] ?? '未知';

    return Scaffold(
      appBar: AppBar(title: const Text('详情页')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('ID: $id', style: const TextStyle(fontSize: 18)),
            Text('名称: $name', style: const TextStyle(fontSize: 18)),
          ],
        ),
      ),
    );
  }
}

setting_page.dart

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('设置页')),
      body: const Center(child: Text('设置页面')),
    );
  }
}

入口main.dart

import 'package:flutter/material.dart';
import 'app_router_delegate.dart';
import 'app_route_parser.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Route 无报错 Demo',
      theme: ThemeData(primarySwatch: Colors.blue),
      routerDelegate: globalRouter,
      routeInformationParser: AppRouteParser(),
      debugShowCheckedModeBanner: false,
    );
  }
}

四、核心功能演示

  1. 基础路由操作

操作

效果(路由栈变化)

跳转新页面

routerDelegate.push(config)

[首页] → [首页, 详情页]

替换当前页面

routerDelegate.replace(config)

[首页, 详情页] → [首页, 设置页]

清除栈并跳转

pushAndRemoveAll(config)

[首页, 详情页, 设置页] → [首页]

返回上一页

routerDelegate.pop()

[首页, 详情页] → [首页]

返回到指定页面

popUntil(RouteNames.home)

[首页, 详情页, 设置页] → [首页]

  1. 深链接支持

假设 App 支持深链接
app://flutter-route-demo/detail?id=2002&name=Test,需在原生层配置 URL Scheme(Android/iOS),然后通过 setNewRoutePath 接收路由信息:

  • Android:在 AndroidManifest.xml 中配置 <data> 标签;
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application
        android:label="test_flutter"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop"
            android:taskAffinity=""
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <!-- Specifies an Android theme to apply to this Activity as soon as
                 the Android process has started. This theme is visible to the user
                 while the Flutter UI initializes. After that, this theme continues
                 to determine the Window background behind the Flutter UI. -->
            <meta-data
              android:name="io.flutter.embedding.android.NormalTheme"
              android:resource="@style/NormalTheme"
              />
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>


            <!-- 新增:深链接配置 -->
            <intent-filter android:autoVerify="true">
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <!-- 匹配深链接:app://flutter-route-demo -->
                <!--深链接格式:app://flutter-route-demo/detail?id=2002&name=Test-->
                <data
                    android:scheme="app"
                    android:host="flutter-route-demo"
                    />

            </intent-filter>

        </activity>
        <!-- Don't delete the meta-data below.
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
    </application>
    <!-- Required to query activities that can process text, see:
         https://developer.android.com/training/package-visibility and
         https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.

         In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
    <queries>
        <intent>
            <action android:name="android.intent.action.PROCESS_TEXT"/>
            <data android:mimeType="text/plain"/>
        </intent>
    </queries>
</manifest>

原生层解析传递过来的参数,然后再通过MethodChannel 将解析的参数回传给Flutter 层,代码如下:

package com.example.test_flutter;

import io.flutter.embedding.android.FlutterActivity;




import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugin.common.MethodChannel;
import android.content.Intent;
import android.net.Uri;
import java.util.HashMap;
import java.util.Map;

public class MainActivity extends FlutterActivity {
    private static final String CHANNEL = "deep_link_channel";

    @Override
    public void configureFlutterEngine(FlutterEngine flutterEngine) {
        super.configureFlutterEngine(flutterEngine);
        handleDeepLink(getIntent(), flutterEngine);
    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        setIntent(intent);
        if (getFlutterEngine() != null) {
            handleDeepLink(intent, getFlutterEngine());
        }
    }
    String path = "/";
    private void handleDeepLink(Intent intent, FlutterEngine flutterEngine) {
        if (Intent.ACTION_VIEW.equals(intent.getAction())) {
            String dataString = intent.getDataString();
            System.out.println("原生层获取的完整深链接:" + dataString);

            Map<String, String> allParams = new HashMap<>();


            if (dataString != null && dataString.startsWith("app://flutter-route-demo")) {
                // 第一步:先对整个深链接进行 URL 解码(关键!将 %26 转为 &)
                String decodedDataString;
                try {
                    decodedDataString = Uri.decode(dataString);
                } catch (Exception e) {
                    e.printStackTrace();
                    decodedDataString = dataString; // 解码失败则使用原始字符串
                }
                System.out.println("解码后的完整深链接:" + decodedDataString);

                // 第二步:分割路径和参数部分(按 ? 分割)
                String[] urlParts = decodedDataString.split("?", 2);
                path = urlParts[0].replace("app://flutter-route-demo", "");

                // 第三步:解析参数部分
                if (urlParts.length == 2) {
                    String queryString = urlParts[1];
                    System.out.println("解码后的参数串:" + queryString); // 正常应为 id=2002&name=Test&age=31

                    // 第四步:按 & 分割参数(此时 %26 已转为 &,分割生效)
                    String[] paramPairs = queryString.split("&");
                    System.out.println("分割后的参数对数量:" + paramPairs.length); // 正常应为 3

                    // 第五步:遍历拆分 key=value
                    for (String pair : paramPairs) {
                        System.out.println("当前参数对:" + pair); // 正常应为 id=2002、name=Test、age=31
                        String[] keyValue = pair.split("=", 2);
                        if (keyValue.length == 2) {
                            String key = keyValue[0].trim();
                            String value = keyValue[1].trim();
                            // 可选:对 key 和 value 再次解码(兼容参数值中的编码字符)
                            try {
                                key = Uri.decode(key);
                                value = Uri.decode(value);
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                            allParams.put(key, value);
                        }
                    }
                }
            }

            System.out.println("原生层最终解析的所有参数:" + allParams); // 正常应为 {id=2002, name=Test, age=31}

            // 传递参数到 Flutter
            new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
                    .invokeMethod("receiveDeepLink", new HashMap<String, Object>() {{
                        put("path", path);
                        put("allParams", allParams);
                    }});
        }
    }
}

在flutter 层作参数的接收:

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'route_config.dart';
import 'app_route_parser.dart';

final globalRouter = AppRouterDelegate();

class AppRouterDelegate extends RouterDelegate<RouteConfig>
    with ChangeNotifier, PopNavigatorRouterDelegateMixin<RouteConfig> {
  final List<RouteConfig> _stack;
  late bool _firstBack;
  final GlobalKey<ScaffoldMessengerState> _messengerKey;
  final MethodChannel _deepLinkChannel;
  Map<String, String> _deepLinkAllParams;

  // 修复构造函数:使用初始化列表,确保所有属性先初始化再执行逻辑
  AppRouterDelegate()
      : _stack = [RouteConfig(RouteNames.home)],
        _firstBack = true,
        _messengerKey = GlobalKey<ScaffoldMessengerState>(),
        _deepLinkChannel = const MethodChannel("deep_link_channel"),
        _deepLinkAllParams = {} {
    // 构造函数体:初始化完成后再监听深链接(避免空指针)
    _listenDeepLink();
  }

  // 监听原生传递的所有参数(修复后逻辑)
  // 通过MethodChannel 将原生层收到的参数 传递过来,然后解析
  void _listenDeepLink() {
    // 防止 MethodChannel 未初始化(双重保险)
    if (_deepLinkChannel == null) {
      debugPrint('MethodChannel 未初始化,深链接监听失败');
      return;
    }

    _deepLinkChannel.setMethodCallHandler((call) async {
      if (call.method == "receiveDeepLink") {
        // 安全解析参数(避免空指针)
        final String path = call.arguments?["path"] as String? ?? RouteNames.home;
        final Map<dynamic, dynamic> tempParams = call.arguments?["allParams"] as Map? ?? {};

        // 转换为 Map<String, String>(兼容原生传递的参数类型)
        _deepLinkAllParams = tempParams.map(
              (key, value) => MapEntry(
            key?.toString()?.trim() ?? "",
            value?.toString()?.trim() ?? "",
          ),
        );

        // 过滤空 key 的参数(可选优化)
        _deepLinkAllParams.removeWhere((key, value) => key.isEmpty);

        // 打印日志(验证参数)
        debugPrint('Flutter 接收的路由路径:$path');
        debugPrint('Flutter 接收的所有参数:$_deepLinkAllParams');

        // 打开详情页(安全校验路径)
        if (path == RouteNames.detail && _deepLinkAllParams.isNotEmpty) {
          _stack.clear();
          _stack.add(RouteConfig(RouteNames.home)); // 保留首页
          _stack.add(RouteConfig(
            path,
            params: _deepLinkAllParams,
          ));
          notifyListeners();
        }
      }
    });
  }

  // 对外提供:获取所有深链接参数
  Map<String, String> get deepLinkAllParams => Map.unmodifiable(_deepLinkAllParams);

  // 对外提供:获取当前页面的所有参数
  Map<String, String> get currentPageParams =>
      (_stack.last.params as Map<String, String>?) ?? {};

  // 首页二次退出逻辑
  @override
  Future<bool> popRoute() async {
    if (_stack.length > 1) {
      _stack.removeLast();
      notifyListeners();
      return true;
    }

    if (_firstBack) {
      _showToast();
      // 修复:_firstBack 是 final,不能直接修改!之前的逻辑错误
      // 改用局部变量或非 final 变量,这里修正为非 final
      // (关键修复:之前 _firstBack 定义为 final,导致无法修改,二次退出失效)
      // 重新定义 _firstBack 为非 final(见上方属性定义)
      _firstBack = false; // 目前可以正常修改
      return true;
    } else {
      SystemNavigator.pop();
      return true;
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      scaffoldMessengerKey: _messengerKey,
      theme: ThemeData(primarySwatch: Colors.blue),
      home: Navigator(
        key: navigatorKey,
        pages: _stack.map((config) => MaterialPage(
          key: ValueKey('${config.path}_${config.params?.toString()}'),
          child: routePages[config.path]!(context),
        )).toList(),
        onPopPage: (route, res) => false,
      ),
    );
  }

  @override
  Future<void> setNewRoutePath(RouteConfig config) async {
    if (_stack.length == 1 && config.path == RouteNames.detail) {
      _stack.add(config);
      notifyListeners();
    }
  }

  // 页面跳转方法
  void push(String path, {Map<String, String>? params}) {
    _stack.add(RouteConfig(path, params: params));
    _firstBack = true; // 跳转后重置二次退出标记
    notifyListeners();
  }

  // 显示退出提示
  void _showToast() {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (_messengerKey.currentState != null) {
        _messengerKey.currentState!.showSnackBar(
          const SnackBar(
            content: Text('再按一次返回键退出应用'),
            duration: Duration(seconds: 1),
            behavior: SnackBarBehavior.floating,
          ),
        );
      }
    });
  }



  // 修复:navigatorKey 初始化(之前可能未显式初始化)
  @override
  GlobalKey<NavigatorState>? get navigatorKey => GlobalKey<NavigatorState>();

  @override
  RouteConfig get currentConfiguration => _stack.last;
}

detail_page.dart 页面展示 :

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

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

  @override
  Widget build(BuildContext context) {
    // 获取所有深链接参数(通用方式,支持任意参数)
    final allParams = globalRouter.currentPageParams;

    // 按需获取参数(不存在则返回默认值)
    final id = allParams?['id'] ?? '未知';
    final name = allParams?['name'] ?? '未知';
    final age = allParams?['age'] ?? '未知';
    // 新增参数直接获取(无需修改解析逻辑)
    final gender = allParams?['gender'] ?? '未知';
    final address = allParams?['address'] ?? '未知';

    return Scaffold(
      appBar: AppBar(
        title: const Text('详情页(支持任意参数)'),
        leading: IconButton(
          icon: const Icon(Icons.arrow_back),
          onPressed: () => globalRouter.popRoute(),
        ),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('深链接传递的所有参数:', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
            const SizedBox(height: 20),
            // 显示已知参数
            Text('ID: $id', style: const TextStyle(fontSize: 18)),
            const SizedBox(height: 10),
            Text('姓名: $name', style: const TextStyle(fontSize: 18)),
            const SizedBox(height: 10),
            Text('年龄: $age', style: const TextStyle(fontSize: 18)),
            const SizedBox(height: 10),
            Text('性别: $gender', style: const TextStyle(fontSize: 18)),
            const SizedBox(height: 10),
            Text('地址: $address', style: const TextStyle(fontSize: 18)),
            const SizedBox(height: 30),
            // 显示所有参数(动态遍历,不管多少个都能显示)
            Text('完整参数列表:', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
            const SizedBox(height: 10),
            ...allParams?.entries.map((entry) => Text('${entry.key}: ${entry.value}')) ?? [],
          ],
        ),
      ),
    );
  }
}

Windows PowerShell 专属命令测试方式:

adb shell am start -a android.intent.action.VIEW -d 'app://flutter-route-demo/detail?id=2002%26name=Test%26age=31'  com.example.test_flutter

注意:Windows PowerShell 中 & 是特殊字符(用于执行命令),即使加单引号,部分场景仍可能截断,%26 是 & 的 URL 编码,会被终端视为普通字符,确保链接完整传递,windows 平台不能使用如下命令:

adb shell am start -a android.intent.action.VIEW -d 'app://flutter-route-demo/detail?id=2002&name=Test&age=31'  com.example.test_flutter

其他平台测试命令:

终端类型

测试命令

Windows CMD

adb shell am start -a android.intent.action.VIEW -d 'app://flutter-route-demo/detail?id=2002%26name=Test%26age=31' com.example.test_flutter

Mac/Linux Terminal

adb shell am start -a
android.intent.action.VIEW -d '
app://flutter-route-demo/detail?id=2002&name=Test' com.example.test_flutter(直接用单引号包裹)

  1. IOS 深链接

iOS 实现深链接的核心逻辑与 Android 一致:配置 URL Scheme → 拦截深链接 → 解析参数 → 传递给 Flutter,但 iOS 的原生配置和参数拦截方式与 Android 不同。以下是完整的 iOS 实现步骤,确保支持多参数解析(如 id=2002&name=Test&age=31),与 Android 效果完全一致。

iOS 深链接依赖 URL Scheme(与 Android 的 Scheme 概念一致),本次配置与 Android 统一:

  • Scheme:app(与 Android 一样)
  • Host:flutter-route-demo(与 Android 一样)
  • 支持路径:/detail、/setting 等(任意子路径)
  • 支持多参数:自动解析 & 分隔的所有参数
  1. 步骤 1:配置 iOS URL Scheme(Info.plist)

第一在 iOS 项目中配置 URL Scheme,让系统识别你的 App 能响应 app:// 开头的链接。

配置方式(两种任选)

方式 1:通过 Xcode 可视化配置(推荐)

  1. 用 Xcode 打开 Flutter 项目的 iOS 目录:your_project/ios/Runner.xcodeproj
  2. 选中左侧 Runner → 切换到 Info 标签页 → 找到 URL Types(如果没有,点击 + 添加)
  3. 点击 URL Types 下的 + 新增配置:
  4. URL Identifier:填写唯一标识(提议与 App Bundle ID 一致,如 com.example.route_demo)
  5. URL Schemes:填写 app(与 Android 的 Scheme 统一)
  6. 其他字段留空

方式 2:直接修改 Info.plist 文件

打开 ios/Runner/Info.plist,添加以下 XML 配置(放在 <dict> 标签内):

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLName</key>
        <string>com.example.route_demo</string> <!-- 与 URL Identifier 一致 -->
        <key>CFBundleURLSchemes</key>
        <array>
            <string>app</string> <!-- Scheme 名称 -->
        </array>
    </dict>
</array>
<!-- 可选:支持 Universal Links(iOS 9+,可选,本次暂不涉及) -->
<key>NSUserActivityTypes</key>
<array>
    <string>NSUserActivityTypeBrowsingWeb</string>
</array>
  1. 步骤 2:iOS 原生层拦截深链接(AppDelegate.swift)

iOS 需通过 AppDelegate 拦截深链接事件,解析参数后通过 MethodChannel 传递给 Flutter(与 Android 的原生通信逻辑一致)。

完整代码(AppDelegate.swift)

  1. 用 Xcode 打开 ios/Runner/AppDelegate.swift
  2. 替换为以下代码(兼容 Flutter 最新版本,支持冷启动和热启动拦截):
import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
    // 与 Flutter 层一致的 MethodChannel 名称(必须统一!)
    private let CHANNEL = "deep_link_channel"
    // 存储 Flutter 引擎(用于热启动时传递参数)
    private var flutterEngine: FlutterEngine?
    
    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        // 初始化 Flutter 引擎
        flutterEngine = (UIApplication.shared.delegate as! AppDelegate).window?.rootViewController as? FlutterViewController
        
        // 1. 冷启动时拦截深链接(App 未启动,通过 URL 打开)
        if let url = launchOptions?[.url] as? URL {
            handleDeepLink(url: url)
        }
        
        // 注册 MethodChannel(必须在 super 之前)
        if let controller = window?.rootViewController as? FlutterViewController {
            let channel = FlutterMethodChannel(name: CHANNEL, binaryMessenger: controller.binaryMessenger)
            // 此处可注册回调(本次无需,仅原生向 Flutter 发消息)
        }
        
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
    
    // 2. 热启动时拦截深链接(App 已启动,再次点击 URL)
    override func application(
        _ application: UIApplication,
        open url: URL,
        options: [UIApplication.OpenURLOptionsKey: Any] = [:]
    ) -> Bool {
        handleDeepLink(url: url)
        return true
    }
    
    // 核心:解析深链接并传递给 Flutter
    private func handleDeepLink(url: URL) {
        print("iOS 原生层获取的完整深链接:(url.absoluteString)")
        
        // 验证 Scheme 和 Host(确保是我们的深链接)
        guard url.scheme == "app", url.host == "flutter-route-demo" else {
            print("不是目标深链接,忽略")
            return
        }
        
        // 提取路径(如 /detail)
        let path = url.path.isEmpty ? "/" : url.path
        // 提取所有参数(自动解析 & 分隔的多参数)
        var allParams: [String: String] = [:]
        
        // 解析 URL 中的查询参数(? 后面的部分)
        if let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems {
            for item in queryItems {
                guard let key = item.name.trimmingCharacters(in: .whitespaces),
                      let value = item.value?.trimmingCharacters(in: .whitespaces),
                      !key.isEmpty else {
                    continue
                }
                // URL 解码(支持中文、特殊字符)
                let decodedKey = key.removingPercentEncoding ?? key
                let decodedValue = value.removingPercentEncoding ?? value
                allParams[decodedKey] = decodedValue
            }
        }
        
        print("iOS 原生层解析的所有参数:(allParams)")
        
        // 传递路径和参数给 Flutter(通过 MethodChannel)
        if let controller = window?.rootViewController as? FlutterViewController {
            let channel = FlutterMethodChannel(name: CHANNEL, binaryMessenger: controller.binaryMessenger)
            // 发送参数(path + allParams)
            channel.invokeMethod("receiveDeepLink", arguments: [
                "path": path,
                "allParams": allParams
            ])
        }
    }
}

关键说明(iOS 原生层)

  1. 拦截时机
  2. 冷启动(App 未启动):通过 application(_:didFinishLaunchingWithOptions:) 拦截 launchOptions[.url];
  3. 热启动(App 已启动):通过 application(_:open:url:options:) 拦截 URL。
  4. 参数解析
  5. 用 URLComponents 解析查询参数(queryItems),自动拆分 & 分隔的多参数,无需手动分割;
  6. 自动处理 URL 解码(removingPercentEncoding),支持 %26(&)、%E6%B5%8B%E8%AF%95(测试)等编码字符。
  7. 通信方式:与 Android 一致,通过 MethodChannel 向 Flutter 发送 receiveDeepLink 事件,参数格式完全统一(path + allParams)。
  1. 步骤 3:Flutter 层适配(无需修改,直接复用 Android 逻辑)

iOS 原生层传递的参数格式与 Android 完全一致(path 是路径,allParams 是所有参数的 Map),因此 Flutter 层代码 无需任何修改,直接复用之前的 app_router_delegate.dart 即可

核心兼容点

  • MethodChannel 名称统一:iOS 和 Android 都使用 deep_link_channel;
  • 参数格式统一:都是 {“path”: “/detail”, “allParams”: {“id”: “2002”, “name”: “Test”, “age”: “31”}};
  • Flutter 接收逻辑统一:通过 _listenDeepLink() 监听事件,自动解析参数并更新路由栈。
  1. 步骤 4:iOS 测试方法(模拟器 + 真机)

方式 1:模拟器测试(推荐,无需真机)

  1. 启动 iOS 模拟器(如 iPhone 15);
  2. 打开 macOS 终端,执行以下命令(直接传递原始链接,无需编码 &):
xcrun simctl openurl booted "app://flutter-route-demo/detail?id=2002&name=Test&age=31"

方式 2:真机测试

  1. 将深链接 app://flutter-route-demo/detail?id=2002&name=Test&age=31 复制到 iOS 真机的备忘录;
  2. 长按链接 → 选择「打开方式」→ 选择你的 App(如果未显示,需先安装 App);
  3. 或通过 Safari 浏览器打开链接:在地址栏输入链接,回车后会提示 “在「你的 App」中打开”。

方式 3:Xcode 调试(查看原生日志)

  1. 用 Xcode 连接 iOS 真机 → 选择你的 App → 点击「运行」(▶️);
  2. 点击深链接打开 App,在 Xcode 的 Console 面板中查看日志:
iOS 原生层获取的完整深链接:app://flutter-route-demo/detail?id=2002&name=Test&age=31
iOS 原生层解析的所有参数:["id": "2002", "name": "Test", "age": "31"]
  1. 深链接跨平台统一总结

平台

核心配置

测试命令

关键差异

Android

AndroidManifest.xml 配置 Intent-Filter

adb shell am start -d 'app://xxx?id=2002%26name=Test' 包名

Windows PowerShell 需编码 & 为 %26

iOS

Info.plist 配置 URL Types

xcrun simctl openurl booted “app://xxx?id=2002&name=Test”

无需编码 &,原生用 URLComponents 解析

  1. 路由拦截与权限控制

在 push 方法中添加权限校验逻辑(如未登录拦截到登录页):

dart

void push(AppRouteConfig config) {// 示例:未登录时,拦截除首页和登录页外的所有路由
  bool isLogin = false; // 实际项目用状态管理(如 Provider)
  if (!isLogin && config.path != RouteNames.home && config.path != '/login') {
    _routeStack.add(AppRouteConfig(path: '/login'));} 
    else {
    _routeStack.add(config);}notifyListeners();
    }

五、RouteDelegate 高级特性

  1. 路由状态持久化

将 _routeStack 存储到本地(如 SharedPreferences),App 重启后恢复路由状态:

dart

// 保存路由状态
Future<void> saveRouteStack() async {
  final prefs = await SharedPreferences.getInstance();
  final stackJson = _routeStack.map((e) => e.toPath()).toList();
  prefs.setStringList('route_stack', stackJson);
}

// 恢复路由状态
Future<void> restoreRouteStack() async {
  final prefs = await SharedPreferences.getInstance();
  final stackJson = prefs.getStringList('route_stack');
  if (stackJson != null && stackJson.isNotEmpty) {
    _routeStack.clear();
    _routeStack.addAll(stackJson.map((path) => AppRouteConfig.fromPath(path)));
    notifyListeners();
  }
}
  1. 自定义页面转场动画

在 build 方法中,通过 PageRouteBuilder 自定义转场效果:

dart

@override
Widget build(BuildContext context) {
  return Navigator(
    key: navigatorKey,
    pages: _routeStack.map((config) {
      final pageBuilder = routePages[config.path];
      return PageRouteBuilder(
        pageBuilder: (context, animation, secondaryAnimation) {
          return pageBuilder!(context);
        },
        transitionsBuilder: (context, animation, secondaryAnimation, child) {
          // 自定义渐变转场
          return FadeTransition(
            opacity: animation,
            child: child,
          );
        },
        transitionDuration: const Duration(milliseconds: 300),
      );
    }).toList(),
    onPopPage: (route, result) => ...,
  );
}
  1. 路由监听

通过 ChangeNotifier 监听路由变化(如统计页面曝光):

dart

// 在页面中监听
@override
void initState() {
  super.initState();
  final routerDelegate = Router.of(context).delegate as AppRouterDelegate;
  routerDelegate.addListener(() {
    // 路由变化时触发(如当前页面变为栈顶)
    final currentPath = routerDelegate.currentConfiguration.path;
    if (currentPath == RouteNames.detail) {
      print('详情页曝光');
    }
  });
}

六、RouteDelegate vs Navigator 1.0 对比

特性

RouteDelegate(Navigator 2.0)

Navigator 1.0(命令式)

路由管理方式

声明式(状态驱动)

命令式(方法调用)

状态管理

聚焦管理(路由栈可持久化、可观察)

分散管理(栈状态不可直接访问)

深链接支持

原生支持(URL 解析→路由映射)

需手动封装(监听原生 Intent/URL)

测试性

高(状态可模拟,无需真实导航)

低(依赖导航上下文,难以模拟)

复杂场景适配

强(多模块、权限控制、灰度发布)

弱(需大量自定义封装)

学习成本

高(需理解组件关系和状态流转)

低(API 简单直观)

七、实际开发提议

  1. 何时使用 RouteDelegate?
  • 大型应用(多模块、复杂路由逻辑);
  • 需要支持深链接;
  • 需要路由状态持久化、可测试;
  • 需要精细的权限控制或路由拦截。
  1. 简化开发:使用路由框架

手动实现 RouteDelegate 较为繁琐,实际开发中可使用成熟框架:

  • auto_route:自动化路由生成,支持类型安全的参数传递;
  • go_router:Flutter 官方推荐,基于 Navigator 2.0,API 简洁;
  • fluro:轻量级路由框架,支持路由拦截、自定义转场。
  1. 注意事项
  • 路由键(Key):必须为每个 MaterialPage 设置唯一 ValueKey,否则页面状态可能复用错误;
  • 状态管理:路由状态提议与全局状态管理(如 Provider、Bloc)结合,避免路由委托职责过重;
  • 原生配置:深链接、URL Scheme 需在 Android/iOS 原生层单独配置;
  • 后退按钮处理:Android 物理后退按钮会触发 onPopPage,需确保路由栈逻辑正确。

八、总结

RouteDelegate 是 Flutter Navigator 2.0 的核心,通过「状态驱动路由」的思想,解决了传统命令式路由的诸多痛点。其核心价值在于路由状态的可掌控性,让开发者能够统一管理路由栈、解析外部路由信息、实现复杂的业务逻辑。

虽然手动实现 RouteDelegate 有必定学习成本,但掌握其原理后,无论是自定义路由还是使用第三方框架,都能更清晰地理解路由流转逻辑。对于中大型 Flutter 应用,RouteDelegate 及其相关组件是构建稳定、可扩展路由系统的基石。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容