← All posts

5 min read

Bringing a Flutter App to Apple CarPlay and Android Auto

What it actually takes to extend a Flutter audio app into the car — why your Flutter UI doesn't run there, how audio_service bridges Android Auto, and the native CarPlay work you can't avoid.

  • Flutter
  • Apple CarPlay
  • Android Auto
  • Audio
Cover illustration for Bringing a Flutter App to Apple CarPlay and Android Auto

When I worked on Nightingale, one of the most requested features was simple to describe: “Let me use it in my car.” Supporting Apple CarPlay and Android Auto turned out to be one of the most interesting projects I’ve done in Flutter — mostly because it’s the one place where Flutter’s “write once, run everywhere” promise doesn’t apply.

If you’re planning car support for a Flutter app, this is the guide I wish I’d had.

The first surprise: your Flutter UI doesn’t run in the car

CarPlay and Android Auto don’t display your app’s screens. They display templates — lists, grids, now-playing screens — rendered by the car system itself. You provide the data (titles, artwork, which items are playable), and the platform decides how it looks.

This is deliberate. Both platforms enforce driver-safety rules: limited list lengths, large touch targets, no free-form UI, no video. It means:

  • None of your Flutter widgets appear on the car display.
  • You’re working with each platform’s native APIs and templates.
  • The good news: your business logic, audio playback and data layer can all stay in Dart.

Architecture: one audio handler, two car surfaces

The design that worked well for us:

┌─────────────────────────┐
│ Flutter UI (phone)      │
└───────────┬─────────────┘
            │
┌───────────▼─────────────┐      ┌──────────────────┐
│ AudioHandler (Dart)     │◄────►│ Android Auto     │
│ playback + media tree   │      │ (MediaBrowser)   │
└───────────┬─────────────┘      └──────────────────┘
            │ method channel
┌───────────▼─────────────┐
│ CarPlay scene (Swift)   │
│ CPListTemplate, etc.    │
└─────────────────────────┘

A single Dart audio handler owns playback state and exposes a browsable tree of media items. The phone UI, Android Auto and CarPlay are all just different views of that handler.

Android Auto: mostly free with audio_service

For audio apps, Android Auto is built on Android’s standard MediaBrowserService. The audio_service package already implements it, so Android Auto support is largely a matter of describing your content tree.

You extend BaseAudioHandler and override getChildren, which Android Auto calls as the user browses:

class NightingaleAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
  @override
  Future<List<MediaItem>> getChildren(String parentMediaId, [Map<String, dynamic>? options]) async {
    switch (parentMediaId) {
      case AudioService.browsableRootId:
        return const [
          MediaItem(id: 'recent', title: 'Recently played', playable: false),
          MediaItem(id: 'library', title: 'Library', playable: false),
        ];
      case 'recent':
        return _library.recentItems();   // playable: true items
      case 'library':
        return _library.allItems();
      default:
        return const [];
    }
  }

  @override
  Future<void> playFromMediaId(String mediaId, [Map<String, dynamic>? extras]) async {
    final item = await _library.findById(mediaId);
    await _player.play(item);
    mediaItem.add(item);
  }
}

A few Android-side details:

  • Declare Android Auto support in AndroidManifest.xml with the com.google.android.gms.car.application meta-data pointing to an automotive_app_desc.xml resource that declares media usage.
  • Keep the tree shallow. Drivers shouldn’t be navigating four levels deep.
  • Provide artwork URIs that the car can load; large images slow down browsing.
  • Test with the Desktop Head Unit (DHU) from the Android SDK — it emulates a car display on your computer.

CarPlay: native Swift, with Dart as the brain

CarPlay has no Flutter plugin that covers everything, so this part is native. Audio apps need the CarPlay audio entitlement from Apple (you request it through your developer account — allow time for approval), and you implement a CPTemplateApplicationSceneDelegate in Swift.

The approach that kept the codebase sane was treating the Swift side as a thin rendering layer:

  1. On connect, Swift asks Dart for the root items over a method channel.
  2. Swift builds CPListTemplates with CPListItems from that data.
  3. When the driver taps an item, Swift calls back into Dart (playFromMediaId), and the same audio handler that serves Android Auto starts playback.
  4. CarPlay’s Now Playing screen reads from MPNowPlayingInfoCenter and MPRemoteCommandCenter, which audio_service already updates.

Because playback, queue management and data all stayed in Dart, the Swift code was small, and a bug fix in playback logic fixed it on the phone, in Android Auto and in CarPlay at once.

One important iOS detail: CarPlay can launch your app’s scene while the phone is locked and your Flutter UI has never opened. Make sure the Flutter engine and your audio handler can initialize without the main UI, or the car shows an empty list.

Offline matters even more in a car

Cars drive through tunnels, rural dead zones and underground car parks. For Nightingale, robust offline caching wasn’t a nice-to-have:

  • Recently played and queued items were downloaded ahead of time.
  • The browse tree was served from the local cache, not fetched live on each tap.
  • Playback continued seamlessly when the connection dropped mid-track.

A car app that shows a spinner while the driver is at a traffic light is worse than no car app.

Testing without a car

  • Android: the Desktop Head Unit covers most scenarios, including day/night mode and touch vs rotary input.
  • iOS: the Xcode Simulator has a CarPlay window (I/O → External Displays → CarPlay).
  • Real cars: test in at least one real vehicle per platform before release. Head units differ — some are slow, some have unusual screen sizes, some handle artwork differently.

Store review

Both Apple and Google review car experiences against driver-distraction guidelines. The rejections I’ve seen or heard of were about:

  • Content hierarchy that was too deep.
  • Text-heavy items or long titles.
  • Features that need the phone screen to complete while driving (sign-in prompts, for example — handle login on the phone before the user gets in the car).

Takeaways

  • Car displays use platform templates; your Flutter UI won’t appear there.
  • Keep playback and data in a single Dart audio handler shared by every surface.
  • Android Auto comes almost for free with audio_service and a well-designed getChildren.
  • CarPlay needs native Swift and an Apple entitlement — keep that layer thin.
  • Offline support is essential in a car.

Car support was one of the features that made Nightingale feel like a finished product. If you’re considering CarPlay or Android Auto for your Flutter app, I’d be glad to help.

Comments

Questions, corrections or your own experience — leave a comment below (GitHub sign-in).