05 路由、网络、存储与资源
5.1 路由导航
Navigator(命令式,≈ startActivity / finish)
// 跳转并等待返回结果(≈ startActivityForResult)
final result = await Navigator.of(context).push<String>(
MaterialPageRoute(builder: (_) => DetailPage(id: id)),
);
Navigator.of(context).pop('done'); // 返回并带结果
Navigator.of(context).pushReplacement(...); // ≈ finish 当前 + start 新页面
Navigator.of(context).pushAndRemoveUntil(route, (r) => false); // ≈ CLEAR_TASK
Navigator 内部维护一个 Route 栈(Overlay 叠加),所有页面都在同一个 FlutterActivity 里。
go_router(声明式,推荐)
适合需要 deep link、Web URL、登录拦截、嵌套 Tab 导航的项目。
final router = GoRouter(
initialLocation: '/',
redirect: (context, state) { // 全局拦截(≈ 路由拦截器)
final loggedIn = authNotifier.isLoggedIn;
final goingLogin = state.matchedLocation == '/login';
if (!loggedIn && state.matchedLocation.startsWith('/me')) return '/login';
if (loggedIn && goingLogin) return '/';
return null;
},
refreshListenable: authNotifier, // 登录态变化时重新计算 redirect
routes: [
StatefulShellRoute.indexedStack( // 底部 Tab,各 Tab 独立栈并保持状态
builder: (context, state, shell) => HomeShell(shell: shell),
branches: [
StatefulShellBranch(routes: [GoRoute(path: '/', builder: (_, __) => const FeedPage())]),
StatefulShellBranch(routes: [GoRoute(path: '/me', builder: (_, __) => const MePage())]),
],
),
GoRoute(
path: '/video/:id',
builder: (_, state) => VideoPage(id: state.pathParameters['id']!,
from: state.uri.queryParameters['from']),
),
GoRoute(path: '/login', builder: (_, __) => const LoginPage()),
],
);
MaterialApp.router(routerConfig: router);
context.go('/video/123?from=feed'); // 替换为目标路由栈
context.push('/video/123'); // 压栈
context.pop();
Android 返回键:Flutter 会自动处理系统返回键(pop 当前路由;栈底时退出 Activity)。拦截返回用 PopScope:
PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
final leave = await confirmLeave(context);
if (leave && context.mounted) Navigator.of(context).pop();
},
child: ...,
);
Android 14+ 预测性返回手势(predictive back)需要 AndroidManifest.xml 中 android:enableOnBackInvokedCallback="true",使用 PopScope 才能和它正确配合;旧的 WillPopScope 已废弃。
5.2 网络:dio
final dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 15),
headers: {'Accept': 'application/json'},
));
// 拦截器 ≈ OkHttp Interceptor
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) async {
options.headers['Authorization'] = 'Bearer ${await tokenStore.read()}';
handler.next(options);
},
onError: (e, handler) async {
if (e.response?.statusCode == 401) {
// 刷新 token 后重试,注意并发请求只刷新一次(用一个共享的 Future 做锁)
}
handler.next(e);
},
));
if (kDebugMode) dio.interceptors.add(LogInterceptor(responseBody: true));
// 请求
final resp = await dio.get<Map<String, dynamic>>('/feed', queryParameters: {'page': 1});
final list = (resp.data!['list'] as List).map((e) => Feed.fromJson(e)).toList();
// 取消 ≈ Call.cancel(),页面销毁时取消
final cancel = CancelToken();
dio.get('/x', cancelToken: cancel);
cancel.cancel();
注意:
- Dart 的 HTTP 栈(
dart:io HttpClient)是 Dart 自己实现的,不走 OkHttp,因此不会使用你在 Android 侧配置的 OkHttp 拦截器、证书锁定、HttpDNS、代理设置。如需复用原生网络库(统一的鉴权签名、网络库监控、QUIC),可以: - 用
cronet_http(Android)/cupertino_http(iOS)作为http包的底层 Client; - 或通过 MethodChannel/Pigeon 把请求转发给原生网络库(会有序列化开销)。
- 抓包:Flutter 默认不读系统代理。调试时需在代码里设置
HttpClient.findProxy,或使用dio的IOHttpClientAdapter配置代理。 - 明文 HTTP:Android 9+ 默认禁止 cleartext,需要配置
network_security_config;iOS 需要 ATS 例外。
5.3 JSON 序列化
Dart 没有运行时反射(Flutter 禁用了 dart:mirrors,为了 tree shaking),所以没有 Gson 那样的反射解析,主流是代码生成。
dependencies:
json_annotation: ^4.9.0
freezed_annotation: ^3.0.0
dev_dependencies:
build_runner: ^2.4.0
json_serializable: ^6.9.0
freezed: ^3.0.0
import 'package:freezed_annotation/freezed_annotation.dart';
part 'feed.freezed.dart';
part 'feed.g.dart';
@freezed
abstract class Feed with _$Feed { // ≈ Kotlin data class + kotlinx.serialization
const factory Feed({
required String id,
required String title,
@JsonKey(name: 'cover_url') required String cover,
@Default(0) int likeCount,
Author? author,
}) = _Feed;
factory Feed.fromJson(Map<String, dynamic> json) => _$FeedFromJson(json);
}
dart run build_runner build --delete-conflicting-outputs # 生成 *.g.dart / *.freezed.dart
dart run build_runner watch # 监听变化持续生成
生成后自动获得 ==、hashCode、copyWith、toString、toJson。
5.4 本地存储
| 需求 | Flutter 包 | Android 底层 |
|---|---|---|
| 简单 KV | shared_preferences(新 API:SharedPreferencesAsync / SharedPreferencesWithCache) |
SharedPreferences / DataStore |
| 高性能 KV | mmkv(腾讯)、hive_ce |
MMKV |
| 敏感数据(token) | flutter_secure_storage |
Android Keystore / iOS Keychain |
| 关系数据库 | sqflite、drift(类型安全,≈ Room) |
SQLite |
| 文件路径 | path_provider |
getFilesDir() / getCacheDir() |
final prefs = SharedPreferencesAsync();
await prefs.setString('uid', '123');
final uid = await prefs.getString('uid');
final dir = await getApplicationDocumentsDirectory();
final file = File('${dir.path}/draft.json');
await file.writeAsString(jsonEncode(data));
5.5 资源(assets)
flutter:
assets:
- assets/images/ # 目录下所有文件(不递归子目录,子目录需单独声明)
- assets/data/config.json
多分辨率图片的目录约定(≈ drawable-xhdpi/xxhdpi):
assets/images/logo.png # 1.0x
assets/images/2.0x/logo.png
assets/images/3.0x/logo.png
Image.asset('assets/images/logo.png'); // 自动按 devicePixelRatio 选择
final json = await rootBundle.loadString('assets/data/config.json');
推荐使用 flutter_gen 生成类型安全的资源引用(≈ R.drawable.xxx),避免字符串写错:Assets.images.logo.image()。
SVG 用 flutter_svg;Lottie 用 lottie。
5.6 主题与深色模式
MaterialApp(
theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF3366FF))),
darkTheme: ThemeData(colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF3366FF), brightness: Brightness.dark)),
themeMode: ThemeMode.system, // 跟随系统
);
// 使用
final colors = Theme.of(context).colorScheme;
Container(color: colors.surface);
业务自定义的颜色/尺寸 token 用 ThemeExtension 扩展,不要在组件里硬编码颜色。
5.7 国际化
dependencies:
flutter_localizations:
sdk: flutter
intl: any
flutter:
generate: true
l10n.yaml:
arb-dir: lib/l10n
template-arb-file: app_zh.arb
output-localization-file: app_localizations.dart
lib/l10n/app_zh.arb:
{
"hello": "你好,{name}",
"@hello": { "placeholders": { "name": { "type": "String" } } }
}
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
);
Text(AppLocalizations.of(context)!.hello('Tom'));
5.8 常用三方包清单
| 功能 | 包 |
|---|---|
| 图片缓存 | cached_network_image |
| WebView | webview_flutter |
| 视频播放 | video_player(官方,Android 用 ExoPlayer/Media3)、media_kit |
| 权限 | permission_handler |
| 设备信息 | device_info_plus、package_info_plus |
| 网络状态 | connectivity_plus |
| 相机/相册 | camera、image_picker |
| 地图 | google_maps_flutter;国内用高德/百度官方 Flutter 插件 |
| 推送 | 厂商推送一般需要自己写原生插件或使用厂商/聚合平台提供的插件 |
| 埋点/崩溃 | sentry_flutter、firebase_crashlytics;国内平台大多提供 Flutter SDK |
| 日志 | logger、talker |
选包标准:pub.dev 上看 Likes / Pub Points / 是否 Flutter Favorite / 最近更新时间 / 支持的平台(是否支持鸿蒙见第 9 章)/ 是否已支持 Swift Package Manager。
动手练习
- 用 go_router 实现:首页底部 3 个 Tab,详情页
/video/:id,未登录访问/me时重定向到登录页。 - 用 dio + freezed 接入一个公开 API(如
https://jsonplaceholder.typicode.com/posts),完成列表和详情。 - 把 token 保存到
flutter_secure_storage,在 dio 拦截器中自动带上。