全栈开发知识库

05 路由、网络、存储与资源

5.1 路由导航

// 跳转并等待返回结果(≈ 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。

动手练习

  1. 用 go_router 实现:首页底部 3 个 Tab,详情页 /video/:id,未登录访问 /me 时重定向到登录页。
  2. 用 dio + freezed 接入一个公开 API(如 https://jsonplaceholder.typicode.com/posts),完成列表和详情。
  3. 把 token 保存到 flutter_secure_storage,在 dio 拦截器中自动带上。
本页由 Markdown 自动渲染 · 共 628 篇笔记