第15章 跨平台架构设计:Android Auto与CarPlay的共性抽象、业务逻辑复用、UI层分离

做车载开发这些年,我最大的体会就是:别把平台当上帝。Android Auto 和 CarPlay 虽然底层机制完全不同,但业务逻辑其实高度相似。你想想看,导航、音乐、电话、消息——这些核心功能在两个平台上几乎一模一样。

我见过太多团队,先做 Android Auto 版本,再从头搞 CarPlay。结果代码重复率高达 60% 以上。维护两个独立代码库,改个 bug 要改两遍,加个功能要加两遍。说白了,这就是在浪费生命。

15.1 共性抽象:找到两个平台的“最大公约数”

跨平台架构的核心,就是抽象。把 Android Auto 和 CarPlay 的共性提取出来,形成一个中间层。这个中间层不依赖任何平台 API,只处理业务逻辑。

核心原则:业务逻辑层永远不要直接引用 Android 或 iOS 的命名空间。一旦引用了,你就被绑死了。

我习惯把共性抽象分为三个层次:

  • 数据模型层:定义统一的媒体项、导航指令、联系人等数据结构
  • 服务接口层:定义播放控制、导航引导、消息通知等行为接口
  • 状态管理层:管理播放状态、导航状态、连接状态等

举个例子,媒体播放的共性抽象:

// 跨平台媒体项定义
public class MediaItem {
    public String id;
    public String title;
    public String artist;
    public String albumArtUrl;
    public long duration;
    public boolean isPlayable;
}

// 跨平台播放控制接口
public interface IMediaPlayer {
    void play();
    void pause();
    void skipToNext();
    void skipToPrevious();
    void seekTo(long position);
    MediaState getCurrentState();
}

这个 MediaItemIMediaPlayer 在 Android Auto 和 CarPlay 两端都能用。我在项目中就是这么干的,效果出奇的好。

15.2 业务逻辑复用:一次编写,两处运行

业务逻辑复用,说白了就是把“怎么做”和“在哪做”分开。业务层只管“怎么做”,平台层只管“在哪做”。

我建议采用策略模式 + 依赖注入的方式来实现复用。核心业务逻辑写成抽象策略,平台差异通过注入的方式传入。

我的经验:把业务逻辑拆成一个个独立的 Service,每个 Service 只做一件事。比如 NavigationService 只处理导航逻辑,MediaService 只处理播放逻辑。这样测试起来也方便。

来看一个导航业务复用的例子:

// 跨平台导航服务
public class NavigationService {
    private INavigationPlatform platform;
    
    public NavigationService(INavigationPlatform platform) {
        this.platform = platform;
    }
    
    public void startNavigation(Route route) {
        // 业务逻辑:计算路线、检查交通状况
        Route optimizedRoute = optimizeRoute(route);
        
        // 平台调用:交给具体平台去显示
        platform.displayRoute(optimizedRoute);
        platform.startGuidance(optimizedRoute);
    }
    
    private Route optimizeRoute(Route route) {
        // 这里全是纯业务逻辑,不依赖任何平台
        // 可以写单元测试
        return route;
    }
}

// 平台接口
public interface INavigationPlatform {
    void displayRoute(Route route);
    void startGuidance(Route route);
    void updateTrafficInfo(TrafficInfo info);
}

你看,NavigationService 里全是纯业务逻辑。Android Auto 和 CarPlay 各自实现 INavigationPlatform 接口就行。我曾经在一个项目里,用这种方式把 80% 的导航代码变成了共享代码。

15.3 UI层分离:各平台各玩各的

UI 层就别想着复用了。Android Auto 用模板化 UI,CarPlay 用 UIKit 或 SwiftUI,两者差异太大。强行统一 UI 层,只会让你陷入无尽的适配地狱。

我的原则是:UI 层完全分离,只通过 ViewModel 或 Presenter 与业务层通信

注意:千万不要在 UI 层写业务逻辑。哪怕是一个简单的“如果播放状态为暂停,显示播放按钮”,也要放到 ViewModel 里处理。UI 层只负责渲染和事件传递。

来看一个推荐的架构分层:

┌─────────────────────────────────────────┐
│            UI 层(平台专属)              │
│  Android Auto: 模板化 Layout / Fragment  │
│  CarPlay:      CPInterfaceController     │
└──────────────────┬──────────────────────┘
                   │ 数据绑定 / 事件回调
┌──────────────────▼──────────────────────┐
│         ViewModel / Presenter 层         │
│         (平台无关,但可感知平台)          │
└──────────────────┬──────────────────────┘
                   │ 接口调用
┌──────────────────▼──────────────────────┐
│          业务逻辑层(完全复用)            │
│    MediaService / NavigationService      │
└──────────────────┬──────────────────────┘
                   │ 数据访问
┌──────────────────▼──────────────────────┐
│           数据层(部分复用)              │
│    网络请求 / 本地存储 / 数据库           │
└─────────────────────────────────────────┘

嗯,这里要注意:ViewModel 层虽然平台无关,但需要感知平台的生命周期。比如 Android Auto 的 onCreateonDestroy,CarPlay 的 templateWillAppeartemplateDidDisappear。我习惯在 ViewModel 里定义统一的生命周期回调接口,由平台层去调用。

15.4 架构全景图

下面这张图是我自己项目里用的架构,你可以参考一下:

跨平台架构全景图 UI 层(平台专属) Android Auto: 模板化 Layout / CarPlay: CPInterfaceController ViewModel / Presenter 层 平台无关,通过接口与 UI 通信 业务逻辑层(完全复用) MediaService / NavigationService / MessageService 数据层(部分复用) 网络请求 / 本地存储 / 数据库 依赖方向

15.5 避坑指南

我曾经踩过不少坑,这里分享几个最痛的:

  • 别在业务层用平台线程:Android 的 Handler、iOS 的 DispatchQueue 都不要出现在业务层。统一用 ExecutorTask 抽象。
  • 数据模型别太“胖”:我见过有人把 UI 相关的字段塞进数据模型,结果换平台时改得想哭。数据模型只放业务数据,UI 展示逻辑交给 ViewModel。
  • 接口粒度要适中:太细了调用链长,太粗了耦合度高。我一般按功能模块划分,一个模块一个接口。
  • 别忘了错误处理:两个平台的错误类型不同,业务层要定义统一的错误码。我曾经因为忽略这点,导致 CarPlay 端崩溃了三天才找到原因。

我的建议:先画好接口图再写代码。把每个 Service 的输入输出、依赖关系理清楚。磨刀不误砍柴工,这个步骤省不得。

15.6 总结

跨平台架构设计,说白了就是三个字:分、抽、合。分的是 UI 层,抽的是共性接口,合的是业务逻辑。做到这三点,你的代码就能在两个平台间自由穿梭。

我个人习惯在项目初期就搭好这个架子。虽然前期多花点时间,但后期维护起来真的省心。你想想看,改一个 bug 只需要改一处,加一个功能只需要加一个 Service——这种爽感,只有经历过的人才知道。


公众号:蓝海资料掘金营,微信deep3321