面试备战 Flutter 23:GoRouter、Dio 与混合工程技术选型
面试备战 Flutter 23:GoRouter 与 Dio 工程化
go_router 和 dio 都不是“会用 API”就够。面试更关心的是:
- 路由是否能承接深链、登录态、底部 Tab、多页面栈?
- 网络层是否有统一鉴权、错误映射、取消请求、重试和可测试边界?
一句话结论:
go_router 解决的是 URL 驱动的声明式路由和嵌套路由治理;Dio 解决的是请求生命周期、拦截器、取消、错误映射和网络层工程化。
1. go_router 是什么?
go_router 是 Flutter 官方 packages 体系下的声明式路由包,基于 Router API,强调 location 和页面状态的映射。
传统 Navigator 更像命令式:
push A
push B
pop
go_router 更强调:
当前 location = /users/42
-> 应用应该展示 UserDetailPage(id: 42)
这让它天然适合:
- Web URL。
- 深链。
- 登录重定向。
- 嵌套路由。
- 底部 Tab 独立导航栈。
- 页面参数标准化。
2. go 和 push 的区别
go
go 表示“把应用切换到某个 location”。
context.go('/home');
它更像声明当前目标位置,常用于:
- 登录成功进入首页。
- 退出登录回登录页。
- 底部 Tab 切换。
- 深链落点。
- 根级页面切换。
push
push 表示“在当前导航栈上再压一个页面”。
context.push('/users/42');
常用于:
- 详情页。
- 编辑页。
- 临时流程页。
- 从列表进入二级页面。
面试话术
go更偏 URL 状态切换,会让路由栈匹配当前 location;push更像传统 Navigator push,在当前栈上叠加页面。Tab、登录态切换、深链通常用 go;详情、编辑、临时流程通常用 push。
3. redirect 怎么设计?
登录拦截不应该散落在页面 build 里,而应该在路由层统一处理。
final router = GoRouter(
refreshListenable: authState,
redirect: (context, state) {
final loggedIn = authState.loggedIn;
final loggingIn = state.matchedLocation == '/login';
if (!loggedIn && !loggingIn) {
return '/login';
}
if (loggedIn && loggingIn) {
return '/home';
}
return null;
},
routes: routes,
);
注意点:
- redirect 应尽量是纯判断,不要在里面发请求。
- 登录态变化后 router 要能 refresh。
- 防止循环重定向。
- 保存原始目标页,登录成功后跳回。
例如:
if (!loggedIn && !loggingIn) {
final from = Uri.encodeComponent(state.uri.toString());
return '/login?from=$from';
}
4. path 参数和 query 参数
路径参数适合资源身份:
/users/:id
query 参数适合筛选条件:
/search?q=flutter&page=2
示例:
GoRoute(
path: '/users/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return UserPage(id: id);
},
);
参数应该尽早转换成强类型对象,不要让字符串散落到业务层。
5. ShellRoute 和 StatefulShellRoute
ShellRoute
适合给一组子路由套公共壳:
- 底部导航。
- 顶部 AppBar。
- 侧边栏。
- 登录后的主框架。
StatefulShellRoute
适合底部 Tab 独立页面栈。
例如:
Tab A: /home -> /home/detail
Tab B: /message -> /message/chat
Tab C: /profile
用户切换 Tab 后,每个 Tab 的历史栈应该保留。StatefulShellRoute 比普通 ShellRoute 更适合这种场景。
6. go_router 常见坑
- redirect 里做异步副作用。
- 登录状态变化不触发 router refresh。
go和push混用导致返回栈不符合预期。- 多层 Navigator 下 dialog/bottomSheet context 用错。
- path 字符串散落,缺少统一 RouteName。
- extra 传复杂对象,刷新或深链后丢失。
- 页面参数没有强类型转换。
建议:
- 路由集中定义。
- name/location 封装。
- 参数对象化。
- 鉴权统一 redirect。
- Tab 使用 StatefulShellRoute。
7. 项目实战:为什么我们没有用 go_router?
我们自己的混合工程没有使用 go_router,而是使用了一套 自研 URL Scheme 路由表 + Navigator 1.0 + Native 容器路由插件。
核心链路是:
RouterTools.openFlutterPage
-> Navigator.of(context).push(MaterialPageRoute)
-> RouteSettings(name: url)
-> urlPageMap[url](url, params, uniqueId)
核心文件:
flutter_smartstorehome_package/lib/common/router_tools.dart
flutter_smartstorehome_package/lib/common/router_uri.dart
flutter_smartstorehome_package/lib/page_url_map.dart
flutter_smartstorephone_portal/lib/main.dart
flutter_smartstorephone_portal/lib/routers/url_page_map.dart
路由地址统一定义在 RouterUri:
class RouterUri {
static const String scheme = "beikesteward";
static const String pageSearchTaskPage = "$scheme://page/search/task";
}
页面注册集中在 urlPageMap:
var urlPageMap = <String, PageBuilder>{
RouterUri.pageSearchTaskPage: (pageName, params, _) {
return SearchTaskPage(params: params);
},
};
Flutter 内部跳转使用:
RouterTools.openFlutterPage(
context,
url: RouterUri.pageSearchTaskPage,
params: {'decorateSearchType': type},
);
跨 Native 容器打开 Flutter 页面时,会包装成容器协议:
bhsmartcontent://flutter/beike/container?flutter_url=...
Flutter 壳工程 flutter_smartstorephone_portal 负责把多个业务包的页面表注册给 FlutterRunnerHelper:
FlutterRunnerHelper.singleton.registerPageBuilders(homeUrlPage);
FlutterRunnerHelper.singleton.registerPageBuilders(storeUrlPage);
FlutterRunnerHelper.singleton.registerPageBuilders(contentUrlPage);
这说明我们项目的路由不是纯 Flutter App 内导航,而是服务于 Native 主导的混合容器。
为什么这个方案合理?
go_router 的强项是声明式 URL、deep link、redirect、ShellRoute 和 Web 友好。但我们项目的主战场不是纯 Flutter App,而是:
Native App
-> Flutter 容器
-> 多 Flutter 业务包
-> H5
-> Native 页面
-> 后端下发跳转 URL
这种场景更需要一套跨端统一的 URL Scheme,而不是只服务 Flutter 内部的声明式路由。
当前方案的优势:
- 适合混合工程:Native、Flutter、H5 都可以用同一套 scheme 描述页面。
- 兼容后端下发跳转:按钮、运营位、任务卡可以下发
btnUrl,Flutter 根据 http/scheme 分发。 - 和 Native Router 打通:Native 页面、Flutter 页面、H5 页面可以互跳。
- 保留 Navigator Future 回传:搜索页、选择页可以直接
await RouterTools.openFlutterPage(...)获取返回值。 - 壳工程统一承载:Flutter 业务包不需要各自维护
MaterialApp、RouterDelegate 和 engine 容器。 - 业务接入成本低:新增页面只要补
RouterUri和urlPageMap。
当前方案的代价
这套方案也有明显短板:
- 字符串路由不是类型安全的:url 写错、参数 key 写错,编译期发现不了。
- 参数是 Map:页面自己解析,类型错误运行时才暴露。
- 未注册路由缺少兜底:如果
urlPageMap[url]为空,容易空调用崩溃。 - 手写 query 解析不够稳:应该优先使用
Uri.parse,避免中文、转义、&、=解析问题。 - 直接 Navigator 调用仍然存在:部分页面绕过
RouterTools,导致埋点、返回、栈治理不统一。 - 声明式能力弱:登录 redirect、Tab 多栈、URL 状态恢复不如 go_router 系统。
所以结论不是“go_router 更高级,我们落后”,而是:
我们项目选择的是面向混合容器的 URL Router,而不是纯 Flutter 声明式 Router。它牺牲了部分类型安全和声明式能力,换来了 Native/Flutter/H5/后端下发 URL 的统一调度能力。
更务实的优化方向不是直接替换 go_router,而是在现有路由体系上补治理:
RouterUri 常量
-> Typed RouteArgs
-> Uri.parse 参数解析
-> UnknownRoutePage 兜底
-> RouteInterceptor / RouteGuard
-> 路由埋点和日志
-> Native/Flutter 协议文档
8. Dio 应该放在哪一层?
不要让 UI 直接调用 Dio。
推荐分层:
Widget
-> Notifier / Controller
-> Repository
-> ApiClient
-> Dio
-> Interceptor / HttpClientAdapter
UI 关心状态,不关心 HTTP 细节。
Repository 关心业务语义:
abstract interface class UserRepository {
Future<User> fetchUser(String id);
}
ApiClient 关心接口协议:
class UserApi {
UserApi(this._dio);
final Dio _dio;
Future<User> fetchUser(String id) async {
final response = await _dio.get('/users/$id');
return User.fromJson(response.data as Map<String, dynamic>);
}
}
9. Dio Interceptor 做什么?
Interceptor 适合统一横切逻辑:
- base header。
- token 注入。
- traceId。
- 日志。
- 错误映射。
- refresh token。
- mock。
- 重试。
示例:
dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
final token = tokenStore.accessToken;
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
},
onError: (error, handler) async {
handler.next(error);
},
),
);
10. refresh token 怎么避免死循环?
刷新 token 最容易踩坑。
错误设计:
请求 A 401
-> 拦截器调用 refresh
-> refresh 也走同一个 Dio
-> refresh 也被拦截
-> 递归/死循环
更稳的方案:
- refresh 使用单独 Dio 实例。
- refresh 接口标记 skipAuth。
- 同一时间只允许一个 refresh。
- 其他 401 请求等待 refresh 结果。
- refresh 失败统一退出登录。
伪流程:
request -> 401
if refreshing:
wait refresh result
else:
start refresh
retry original request with new token
11. CancelToken 为什么重要?
取消请求不是锦上添花,而是页面生命周期治理。
场景:
- 页面退出。
- 搜索关键字变化。
- Tab 切换。
- 下拉刷新覆盖旧请求。
- 重复点击。
示例:
final cancelToken = CancelToken();
final response = await dio.get(
'/search',
queryParameters: {'q': keyword},
cancelToken: cancelToken,
);
cancelToken.cancel('keyword changed');
和 Riverpod 结合:
final searchProvider = FutureProvider.autoDispose.family<SearchResult, String>((ref, keyword) async {
final cancelToken = CancelToken();
ref.onDispose(() => cancelToken.cancel('disposed'));
final response = await dio.get(
'/search',
queryParameters: {'q': keyword},
cancelToken: cancelToken,
);
return SearchResult.fromJson(response.data as Map<String, dynamic>);
});
12. 错误建模
不要把 DioException 直接扔给 UI。
应该统一映射成业务错误:
sealed class NetworkError {
const NetworkError();
}
class TimeoutError extends NetworkError {
const TimeoutError();
}
class UnauthorizedError extends NetworkError {
const UnauthorizedError();
}
class ServerError extends NetworkError {
const ServerError(this.statusCode);
final int? statusCode;
}
好处:
- UI 不依赖 dio。
- 错误文案统一。
- 重试策略统一。
- 埋点统一。
13. Dio 工程化检查清单
- baseUrl 是否按环境区分。
- token 是否统一注入。
- refresh token 是否防并发。
- CancelToken 是否接入页面生命周期。
- 错误是否统一映射。
- 日志是否脱敏。
- 超时是否配置。
- 是否有 requestId/traceId。
- Repository 是否屏蔽 HTTP 细节。
- 测试是否可以替换 ApiClient。
14. 项目实战:为什么我们没有用 Dio?
我们项目的 Flutter 业务包没有使用 Dio。Flutter 侧网络请求走的是:
RequestTools.get/post
-> HomeFlutterPluginAdapter.netGet/netPost
-> MethodChannel(home_flutter_plugin / beikesmartstorepad_flutter_plugin)
-> DecorationSteward 原生壳插件
-> DWHomeNetworkService / BKBPNetworkAPI / DWCommonApiService
-> 公司网关 / 后端
Flutter 侧只描述业务请求:
class RequestTools {
static Future<BKBPResult> get(String url, {Map<String, dynamic> params, String bizId}) async {
final map = await HomeFlutterPluginAdapter.netGet(
url: url,
params: params,
bizId: bizId,
);
return BKBPResult.fromJson(map);
}
}
真正发 HTTP 的不是 Flutter,而是 Native 壳工程 DecorationSteward。
为什么这么设计?
核心原因是:我们是 Native 主导的混合工程,不是纯 Flutter App。
原生壳已经有成熟网络基础设施,包括:
- 登录态和 token。
- 角色和城市信息。
- 环境网关。
- Authorization 签名。
- Cookie 注入。
- Device 信息。
- 统一错误码处理。
- 登录过期处理。
- 安全策略。
- 埋点和日志。
如果 Flutter 再用 Dio,就会出现第二套网络体系:
Native 网络体系
Flutter Dio 网络体系
这会带来一批重复建设:
- token 如何同步?
- refresh token 谁负责?
- 登录过期谁弹登录页?
- 角色切换后 Flutter header 如何刷新?
- 环境切换如何保持一致?
- Authorization 签名规则是否要在 Dart 再实现一次?
- Cookie / WebView / Native 请求如何保持一致?
- 网络埋点和 requestId 如何统一?
这些问题本质上都属于壳工程能力。让 Flutter 复用 Native 网络层,是更符合当前架构的选择。
当前方案的优势
- 登录态只维护一套:Flutter 不需要单独管理 token。
- 安全逻辑不散落到 Dart:签名、设备信息、防抓包等留在原生层。
- 环境切换统一:测试、预发、生产由原生壳控制。
- Native/H5/Flutter 请求口径一致:Cookie、User-Agent、角色、城市等统一。
- 业务包更轻:Flutter 只关心接口 path、参数和模型解析。
- 多端适配更自然:iOS/Android/Harmony 可以在各自插件里适配平台差异。
- 复用公司已有网络 SDK:不需要在 Flutter 重造网关、签名、错误治理。
当前方案的代价
- 每次请求都经过 MethodChannel,有编解码和跨端调用成本。
- Flutter 侧缺少 Dio 生态能力,例如 Interceptor、CancelToken、下载进度、请求队列。
- 请求取消能力弱,页面销毁后 Native 请求未必能取消。
- 返回值类型弱,Native 返回 JSON string,Flutter 再
jsonDecode。 - 调试链路更长,需要同时看 Flutter、Plugin、Native 网络层。
- 单测不够直接,业务请求依赖 MethodChannel,需要额外 mock。
所以我们的选择可以这样总结:
不用 Dio 不是因为 Dio 不好,而是因为网络治理中心在 Native 壳。Flutter 通过 MethodChannel 委托 Native 发请求,复用登录态、签名、网关、Cookie、环境、安全和埋点能力。这对混合工程是合理取舍。
后续更适合的优化不是“全量迁移 Dio”,而是把现有协议做强:
netGet/netPost
-> netRequest
-> RequestOptions(method/url/params/body/bizId/requestId/timeout)
-> Native 标准响应(code/message/data/requestId/traceId/httpStatus)
-> 支持 cancelRequest(requestId)
15. 面试怎么回答?
可以这样答:
go_router 我会把它当成 URL 驱动的声明式路由系统,而不是 Navigator 替代品。
go用于切换当前 location,适合登录态、Tab、深链;push用于在当前栈上叠加页面,适合详情和编辑。登录鉴权放 redirect,底部 Tab 独立栈用 StatefulShellRoute,参数尽早转成强类型对象。Dio 我不会让 UI 直接用,而是放在 ApiClient/Repository 下面。Interceptor 处理 token、日志、错误映射和 refresh token;CancelToken 接页面生命周期,尤其是搜索和 autoDispose provider;DioException 不直接暴露给 UI,而是映射成业务错误。这样网络层可测、可替换、可治理。
但在我们的混合工程里,路由和网络的技术决策不完全按纯 Flutter App 来选。路由上,我们使用 URL Scheme +
urlPageMap+FlutterRunnerHelper,是为了打通 Native、Flutter、H5 和后端下发 URL;网络上,我们没有使用 Dio,而是通过 MethodChannel 委托 Native 壳发请求,是为了复用原生已有的 token、签名、网关、Cookie、角色、环境和安全能力。它牺牲了一些 Flutter 侧的类型安全和取消能力,但换来了混合工程的一致性和治理成本优势。
参考资料
- go_router package: https://pub.dev/packages/go_router
- Flutter Navigation and routing: https://docs.flutter.dev/ui/navigation
- Flutter deep linking: https://docs.flutter.dev/ui/navigation/deep-linking
- Dio package: https://pub.dev/packages/dio
- Flutter networking overview: https://docs.flutter.dev/data-and-backend/networking