我要提问
PROJECT CASE / 005

跨平台移动应用实战

Flutter 3 + Dart + Riverpod,一套代码多端运行的资讯 App 工程实践。

跨平台移动应用实战:Flutter 资讯 App 从开发到上架

跨平台移动应用实战项目示意图

项目背景

某内容平台的移动端原本是 iOS、Android 各一套原生代码,两个团队 6 个人维护,每次新功能都要双端各排一次期,发版节奏跟不上。更麻烦的是双端交互细节经常不一致,用户反馈「为什么安卓没有 iOS 的那个动画」。运营的诉求是统一体验、提效发版。

本次目标是用 Flutter 重构资讯类 App,一套 Dart 代码同时出 iOS/Android 两端,团队缩到 3 人,双端功能与视觉强一致,首屏渲染低于 1.2s,包体积控制在 18MB 内。技术栈选 Flutter 3 + Riverpod(状态管理)+ Dio(网络)+ go_router(路由)。

跨平台不是「写一遍跑两端」那么轻松,真正的工程量在状态管理、原生通信与包体积优化这三处,UI 反而最简单。

架构设计

采用分层架构:UI 层只渲染不持有业务;状态层用 Riverpod Provider 承载;数据层封装 Dio 与本地缓存;平台层用 MethodChannel 处理原生能力。各层单向依赖,方便单测与替换。

lib/
├── main.dart              # 入口,挂 ProviderScope 与路由
├── ui/                    # 页面与组件(不写业务逻辑)
│   ├── pages/
│   └── widgets/
├── providers/             # Riverpod 状态(article、user、settings)
├── data/                  # 网络仓库 + 本地缓存
│   ├── remote/            # Dio 实现
│   └── local/             # SharedPreferences / Hive
├── platform/              # MethodChannel 原生通信
└── router.dart            # go_router 路由表

关键设计:所有网络请求经 Repository 封装,UI 只读 Provider 不直接调网络;路由用 go_router 声明式管理,深链接直接对应路由路径;原生能力(推送、生物识别)收敛到 platform/ 一处,便于双端各自实现。

核心实现

1)状态管理:用 Riverpod 的 AsyncNotifier 承载文章列表的「加载中/成功/失败」三态,UI 用 ref.watch 订阅,ref.read 触发刷新,避免 setState 满天飞。

// providers/article_provider.dart —— 文章列表状态
class ArticleListNotifier extends AsyncNotifier<List<Article>> {
  @override
  Future<List<Article>> build() => ref.read(articleRepo).fetchList();

  Future<void> refresh() async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(
      () => ref.read(articleRepo).fetchList(),
    );
  }
}

final articleListProvider =
    AsyncNotifierProvider<ArticleListNotifier, List<Article>>(
  ArticleListNotifier.new);

// UI 使用
final list = ref.watch(articleListProvider);
return list.when(
  loading: () => const LoadingShimmer(),
  error:   (e, _) => ErrorRetry(onRetry: () => ref.refresh(articleListProvider)),
  data:    (items) => ArticleListView(items: items),
);

2)原生通信:生物识别登录用 MethodChannel 调原生,Flutter 侧定义统一接口,iOS/Android 各自实现,失败降级到密码登录。

// platform/biometric.dart —— 原生通信封装
const _channel = MethodChannel('app.biometric');

Future<bool> authenticate(String reason) async {
  try {
    final ok = await _channel.invokeMethod<bool>('authenticate', {'reason': reason});
    return ok ?? false;
  } on PlatformException {
    return false; // 原生不可用时降级
  }
}

// Android 侧(Kotlin)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "app.biometric")
    .setMethodCallHandler { call, result ->
        when (call.method) {
            "authenticate" -> biometricPrompt(result)
            else -> result.notImplemented()
        }
    }

3)包体积优化:Flutter 默认产物偏大,我们通过「移除未用字体子集 + 按需 import 图标 + --split-per-abi 拆 ABI + 资源托管远端按需下载」四步把包从 32MB 压到 17MB。

  • 首屏渲染:2.4s → 1.1s(预编译 + 骨架屏)
  • 包体积:32MB → 17MB(--split-per-abi 后单架构)
  • 发版人力:6 人 → 3 人
  • 新功能双端交付周期:2 周 → 5 天

技术难点

难点一:长列表性能。资讯 Feed 滑动时掉帧明显。用 DevTools 的 Performance 面板定位到是 ListView.builder 里每项都重建了重型组件。改用 const 构造、抽取 itemExtent 固定高度、对图片用 cached_network_image 做内存缓存后,滑动稳定 60fps。

// 长列表优化:固定 itemExtent + const 组件
ListView.builder(
  itemExtent: 96,                       // 固定高度,跳过测量
  itemCount: items.length,
  itemBuilder: (_, i) => ArticleTile(item: items[i]),  // ArticleTile 用 const 构造
)

// ArticleTile 内部图片走缓存
CachedNetworkImage(
  imageUrl: item.cover,
  memCacheWidth: 200,                   // 限制解码尺寸
  placeholder: (_, __) => const Skeleton(),
)

难点二:WebView 与原生混排。部分活动页是 H5,要用 webview_flutter 嵌入并做 JS Bridge 通信。难点是滚动嵌套时手势冲突,最终通过 NeverScrollableScrollPhysics 让 WebView 高度自适应内容、由外层 ListView 统一滚动解决。

难点三:iOS 与 Android 字体渲染差异。同一字号在两端视觉大小不一致。我们定义了统一的 ThemeData,对 TextTheme 按 Platform 微调 letterSpacingheight,保证双端视觉一致。

踩坑复盘

坑 1:Riverpod 误用 ref.watch 在 build 之外。在事件回调里写了 ref.watch 导致状态不更新且报错。规范是「build 内 watch、回调内 read」,团队加了一条 lint 规则后杜绝。

坑 2:Dio 拦截器里抛异常被吞。统一错误处理拦截器里 catch 后没 rethrow,导致上层永远拿到成功。改成 rethrow 后错误能正确传到 UI 层。教训:拦截器处理完异常要决定「吞掉」还是「上抛」,不能模棱两可。

坑 3:上架被拒——iOS 因「使用未声明权限」被打回。是某个三方插件静默引入了定位权限。用 pod install 后检查 Info.plist 权限声明、移除未用插件后过审。教训:上线前必须 review 最终权限列表,三方插件的权限要逐一确认。

Flutter 工程化的胜负手:状态管理要规范、长列表要 const、包体积要拆 ABI——这三件做扎实,跨平台才真正省人力。

项目总结

App 上线后双端体验完全一致,团队从 6 人缩到 3 人,新功能双端交付周期从 2 周缩到 5 天,包体积比原双端各自方案还小 30%。整个项目最大的认知更新是:跨平台的价值不在「少写一份 UI」,而在「统一状态、统一交互、统一发版节奏」带来的工程效率提升。

  • 首屏渲染:2.4s → 1.1s
  • 包体积:32MB → 17MB
  • 维护人力:6 人 → 3 人
  • 功能交付周期:2 周 → 5 天
返回项目列表