KQED In Your Car: A CarPlay and Android Auto Journey

Background

KQED Inc. is a San Francisco-based non-profit public media organization serving Northern California. Founded in 1953, it began airing in 1954 as one of the earliest U.S. educational television stations. KQED operates two PBS television stations and the NPR radio station at 88.5 FM. Known for pioneering public television fundraising and originating its first daily news program, its radio station was the nation's most-listened-to public radio by 2011.

Since 2021, Uptech Studio has been the mobile development partner for KQED, helping to bring the range of amazing content KQED produces to Apple and Android devices.

Introduction

When you're driving and want to catch up on your favorite KQED podcast or tune into live radio, the last thing you should worry about is fumbling with your phone.

This is the problem we set out to solve when we decided to bring KQED directly to the car dashboard via Apple CarPlay and Android Auto.

For those unfamiliar, Apple CarPlay and Android Auto transform your car's built-in display into a driver-friendly interface that mirrors select apps from your iPhone or Android device. Instead of glancing at your phone or navigating complex menus, you can access your favorite content through your car's touchscreen, steering wheel controls, or even Siri, all designed to keep your eyes on the road and hands on the wheel.

This is not your typical implementation discussion about CarPlay and Android Auto. The KQED mobile app is built using 100% Flutter which brought its own set of challenges to the table. We adore Flutter for its cross-platform support and ease of use. Flutter has enabled us to move fast and ship a high quality app that is the gold standard amongst Public Radio organizations. For the in-car experience to be successful, CarPlay and Android Auto had to work together with Flutter form a seamless experience.

This is the story of how we brought KQED's award-winning journalism, podcasts, and live radio programming into the car dashboards of thousands of vehicles across California and beyond.

First a plug: make sure to download the KQED iOS App or the KQED Android App.

What We Wanted to Accomplish

Our primary goal was simple but profound: meet people where they are. That meant making KQED’s content as accessible in the car as it is from the phone.

Many listeners consume KQED content during their commute. According to listener surveys, the car is one of the top three places people engage with public radio. Before building CarPlay support, accessing the app while driving meant either:

  • Pulling over to navigate your phone (inconvenient) … or not pulling over to do that (unsafe)
  • Using voice commands that didn't always work reliably
  • Giving up and switching to traditional FM radio (missing out on on-demand podcast content)

We wanted to create an experience that felt natural and safe, as intuitive as turning on your car radio, but with all the power of on-demand digital content at your fingertips.

For our users, this meant we had to deliver:

  • One-tap access to live KQED 88.5 FM radio while driving
  • Easy browsing of the latest podcast episodes without leaving Android Auto
  • Seamless transitions from listening on their phone to continuing in the car
  • Safe, distraction-free interaction with our content
  • Familiar navigation organized into clear sections: Live Radio, Episodes, and Shows

The Benefits of Auto Integration

Adding CarPlay and Android Auto support brings a variety of benefits both to listeners and KQED.

Listener Benefits

  • Safety first: Voice commands and simplified, car-optimized interfaces keep drivers focused on the road
  • Convenience: No need to dig through your phone while driving; everything is accessible through your car's display
  • Continuity: Start listening at home, continue in the car without missing a beat or losing your place
  • Discovery: Browse new podcast episodes and shows directly from your car's touchscreen
  • Always current: See what's playing live on KQED 88.5 with program artwork and information

KQED Benefits

  • Expanded reach: Meet our audience where they are, in their cars during commute times
  • Increased engagement: Make it easier for listeners to consume more content
  • Platform parity: Offer the same great experience to both iOS and Android users
  • Modern expectations: Stay competitive with other media apps that support in-car listening
  • Data insights: Track how and when listeners connect to better understand our mobile audience

Assumptions Going In

Every product initiative begins with some assumptions. Here were ours:

Audience Assumptions

  • Support. A significant portion of mobile app users have CarPlay and Android Auto-enabled vehicles.
  • Preference. Listeners would prefer using CarPlay or Android Auto over traditional FM radio if given the option.
  • Better Experience for Podcast Listeners. Podcast listeners, in particular, would benefit from easy access to episode libraries while driving.

Technical Assumptions

  • Flutter Support: Flutter had mature CarPlay and Android Auto support through available plugins.
  • Flutter compatibility: We assumed the existing audio playback architecture in our Flutter app would work well with Android Auto, since we were already using the audio_service package from the just_audio suite.
  • Easy Approval: The app approval process would be straightforward for a public radio app.
  • UI Flexibility on Android: We initially thought we'd have more control over the visual presentation in Android Auto, similar to other in-app experiences.
  • Testing Simplicity: We assumed testing would be straightforward using simulators on the desktop.
  • Single codebase advantage: One of Flutter's promises is write-once, deploy-everywhere. We expected to reuse most of our existing audio handling code without platform-specific modifications.
  • Plug-and-play integration: We thought enabling Android Auto would be as simple as adding a manifest entry and implementing a few callbacks.

CarPlay Challenges

While some of the challenges we faced were common to CarPlay and Android, we approached the overall project by building for CarPlay first, so that we could address those challenges with that release before moving on to Android Auto and applying what we learned from the CarPlay implementation.

The Information Hierarchy Puzzle

In the mobile app experience, we can show rich program descriptions, beautiful imagery, scrollable content, and complex navigation. CarPlay, by design, strips a lot of that away. Apple restricts how much text you can display, how deep your navigation can go, and what interactive elements you can include.

The problem: How do you represent KQED's diverse content catalog of live radio, daily news, talk shows, investigative podcasts, arts programs in a simple way that drivers can safely navigate?

Our approach: We organized content into three clear tabs:

  • Live Radio: Always front and center, featuring what's currently on air
  • Episodes: The latest episodes across all the podcasts
  • Shows: The full podcast catalog, tappable to see episode lists

This structure meant that whether someone wanted to jump right into live radio or catch up on a specific podcast show, they were never more than two taps away from listening.

The "Smart" Dashboard Dilemma

Our mobile app stays updated with what's currently playing on KQED 88.5, shows timely content recommendations, and refreshes based on user behavior. But in CarPlay, we couldn't assume a constant data connection or that the app would always be in an active state.

The problem: How do we keep the CarPlay interface current when someone might connect their phone to their car once a week, or when they drive through areas with poor cell coverage?

Our approach: We implemented a smart syncing system that:

  • Updates the current radio program information whenever the phone connects to CarPlay.
  • Refreshes content intelligently in the background without disrupting playback.

This meant that when you connect your phone, you see what's actually playing right now, not what was playing yesterday.

Feature Flags in a Constrained Environment

We use feature flags (or toggles) that let us turn features on or off for testing, support phased rollouts, or run A/B experiments. But CarPlay doesn't allow for complex UI updates or sudden interface changes while someone is driving.

The problem: How do we maintain our agile development approach and testing flexibility while respecting CarPlay's stability requirements?

Our approach: We built a feature flag system that controls which tabs appear in CarPlay:

  • carPlayTheLatestPodcastIsOn: Shows/hides "The Latest" section
  • carPlayLatestPodcastsIsOn: Shows/hides the Episodes tab
  • carPlayPodcastsTabIsOn: Shows/hides the Shows tab

This gave us the flexibility to test different configurations with small user groups before rolling out to everyone, while ensuring that changes only happened when someone first connected to CarPlay, not mid-drive.

The Nested Navigation Problem

One of our most head-scratching bugs involved a simple loading spinner. When someone tapped on a podcast show, then tapped on an episode within that show, the loading spinner would appear and never go away, even though the audio was playing perfectly.

The problem: CarPlay's navigation system works differently than traditional mobile navigation. When you nest screens several levels deep (Show List → Individual Show → Episode), certain UI callbacks don't fire as expected.

Our observation (noted in the code): "For some reason this isn't removing the loading spinner even though it gets called. Maybe because it's nested a few templates deep?"

Our workaround: We called the completion callback explicitly, but acknowledged this was an area that needed more investigation. Sometimes in product development, you ship with known quirks and iterate based on real-world usage.

Connection State Juggling

CarPlay connections aren't as simple as "connected" or "disconnected." A phone might connect to a car multiple times during a single drive (tunnel interference, Bluetooth hiccups, phone restarts). We needed to track connection state without triggering duplicate analytics events or refreshing the interface unnecessarily.

The problem: How do we distinguish between the first meaningful connection of a drive versus temporary reconnections?

Our approach: We implemented a state tracking system that only logs a "CarPlay Connected" event the first time in a session, avoiding analytics pollution and unnecessary interface refreshes.

Android Auto Challenges

Understanding the MediaBrowserService Architecture

Android Auto doesn't work like a typical app screen. Instead, it uses a service-based architecture where your app responds to requests from the Android Auto system. Think of it like a waiter taking orders: Android Auto asks "what media do you have?" and your app needs to respond with a menu of options. This required a mental shift from our usual Flutter development patterns.

Limited UI Customization

Unlike building a typical Flutter screen where we have complete control over layout and design, Android Auto strictly controls the user interface. We can only provide content organized as lists or grids. All the beautiful custom UI we built for the main app couldn't be used in Android Auto. We had to embrace this constraint and focus on content organization rather than visual polish.

Hierarchical Content Navigation

Android Auto expects content to be organized in a tree structure: a root level with folders, and folders containing either more folders or playable items. Designing this hierarchy to match how listeners think about our content took careful planning. Should we prioritize shows or episodes? How do we surface live radio quickly while still making podcasts discoverable?

Testing in Real Environments

The Android Auto desktop simulator has limitations. To truly test the experience, we needed to test in actual vehicles or with physical head units. This meant enabling developer mode on Android devices (which requires tapping a version number ten times, like unlocking a secret level in a video game) and configuring "unknown sources" to see our development builds.

Bridging Native and Flutter Code

While Flutter excels at cross-platform development, Android Auto requires deep integration with native Android APIs. The Audio Service package provides a bridge, but we still needed to understand how the Java-based MediaBrowserServiceCompat communicates with our Dart code. Getting this handshake right was crucial for a smooth experience.

Analytics Differences

We discovered that tracking Android Auto usage differs significantly from CarPlay. Android Auto triggers connection events not just when users open the app in their car, but also when they're already listening and plug in. Understanding these nuances was important for accurately measuring feature adoption.

KQED CarPlay Experience

KQED Android Auto Experience

CarPlay Technical Approach

We built our CarPlay integration using Flutter, a cross-platform app framework that allows us to maintain a single codebase for iOS and Android. We utilized the flutter_carplay plugin, which bridges Flutter's Dart language with Apple's native CarPlay APIs.

At the time of building the CarPlay feature, the flutter_carplay plugin did not do everything we needed it to so we forked the plugin code and updated the package as needed. Since then they have added more functionality, including support for Android Auto. We do not use the Android Auto features of this plugin because one of our audio packages Audio Service supports Android Auto. We’ll talk about this in-depth on the Android Auto version of this blog which is coming soon.

The Architecture

Our implementation follows a simple but effective pattern:

1. Template-Based UI. CarPlay uses predefined templates. We chose:

  • CPTabBarTemplate: The main container with multiple tabs
  • CPListTemplate: For displaying lists of content
  • CPListSection: For grouping related items (like "Live Radio" vs "The Latest")
  • CPListItem: Individual tappable items (shows, episodes)

2. State Management. We use Riverpod to:

  • Track the current radio program
  • Monitor feature flag changes
  • Manage the podcast catalog
  • Coordinate playback state

3. Dynamic Content Loading. The interface listens to several data sources:

  • CurrentRadioProgramNotifier: What's playing on KQED 88.5 right now
  • sortedPodcastsProvider: The podcast catalog, sorted by recency
  • FeaturesNotifier: Feature flags that control what appears

When any of these change, the CarPlay interface automatically rebuilds with fresh content.

The User Flow

Here's what happens when someone connects their iPhone to their car:

  1. Connection Detected: The app receives a CarPlay connection event
  2. Content Sync: We fetch the latest radio program and podcast episodes
  3. Interface Built: Based on enabled features, we construct the tab bar with appropriate content
  4. Analytics Logged: We record the connection event (but only once per session)
  5. Ready to Play: The interface appears on the car's display, ready for interaction

When someone taps an item:

  1. Playback Starts: Our audio player begins streaming the content
  2. Now Playing Opens: CarPlay's standard Now Playing interface appears
  3. Playback Controls Active: Users can pause, skip, or control audio through their car's controls

Android Auto Technical Approach

How Android Auto Works: A High-Level Overview

To understand our implementation, it helps to know how Android Auto functions under the hood.

When you plug your Android phone into a compatible car (or connect wirelessly), Android Auto takes over your car's display. But it doesn't mirror your phone screen. Instead, it communicates with apps on your phone that have declared support for Android Auto.

For media apps like KQED, this communication happens through something called a MediaBrowserService. Think of it as a specialized customer service representative that knows everything about your app's audio content. Android Auto calls this service and asks questions:

  • "What content do you have?" (answered by the getChildren method)
  • "The user wants to play this item, start playback" (handled by the playFromMediaId method)

The service responds with organized lists of content, complete with titles, artwork, and whether each item is playable or just a folder containing more items.

Flutter Meets Android Auto

One of KQED's key technical decisions was building our mobile app with Flutter, Google's UI framework for creating cross-platform applications. This choice has served us well, allowing us to maintain a single codebase for both iOS and Android while delivering native performance.

For Android Auto integration, we leveraged the audio_service package, part of the just_audio suite of Flutter packages. This package does the heavy lifting of implementing Android's MediaBrowserServiceCompat in native Java code, then provides Flutter-friendly APIs that let us write our logic in Dart.

The Architecture: Centralized and Reusable

We designed our Android Auto implementation around a central handler class called AndroidAutoHandler. This was a deliberate architectural choice to avoid code duplication. Here's why this matters:

Imagine a user is listening to a podcast when they plug into their car. When Android Auto launches, we want to show not just podcasts, but our entire content library: live radio, all shows, and all episodes. If we had implemented Android Auto logic separately in each audio handler (podcast, live radio, articles), we'd be maintaining the same code in multiple places.

Instead, our podcast handler, live radio handler, and other audio handlers all delegate to the same AndroidAutoHandler. This centralized approach means:

  • One source of truth for how content is organized
  • Easier updates when we want to change the Android Auto experience
  • Consistent behavior regardless of what the user was listening to when they connected

Content Organization: The Three-Tab Structure

Based on our experience implementing CarPlay first, we followed the same pattern for the Android Auto experience by focusing on a three-tab experience:

  1. Live Radio: This tab gives immediate access to the KQED 88.5 live stream, showing the current program with its artwork. We also surface the most recent episode of "The Latest" here, recognizing that listeners tuning into live radio often want quick access to breaking news.
  2. Episodes: This tab shows the newest episode from each podcast show, perfect for listeners who want to catch up on their favorites without navigating through show folders.
  3. Shows: This tab lists all KQED podcast shows. Tapping a show reveals all its episodes, organized chronologically.

The Two Key Methods: Browse and Play

Our implementation centers on two critical methods that respond to Android Auto's requests:

getChildren: The Browse Method

When Android Auto wants to know what content is available, it calls this method with a "parent ID." Think of it like asking "what's in this folder?"

  • Called with root → We return our three tabs: Live Radio, Episodes, and Shows
  • Called with liveRadio → We return KQED 88.5 and The Latest
  • Called with podEpisodes → We return the newest episode from each show
  • Called with podShows → We return all podcast shows
  • Called with a specific showId → We return all episodes for that show

Each item we return includes metadata: title, artwork URL, and crucially, whether it's playable or just another folder to browse into.

playFromMediaId: The Play Method

When a user taps something playable, Android Auto calls this method with the item's unique ID. Our job is to:

  1. Figure out what type of content it is (live radio or podcast episode)
  2. Load any necessary data (like the current radio schedule or podcast details)
  3. Create a playable object
  4. Hand it off to our existing playback system

This is where Flutter's cross-platform architecture shines. The playback logic we'd already built for the mobile app worked seamlessly for Android Auto. We didn't need to write separate playback code; we just needed to bridge Android Auto's requests to our existing Flutter playback system using Riverpod for state management.

Smart Analytics: Understanding Usage Patterns

We implemented thoughtful analytics to track Android Auto adoption. When a user first opens KQED in Android Auto, we log a connection event. But we don't want to spam our analytics every time someone's phone reconnects or switches apps.

Our solution: track the timestamp of each connection event. If a new connection happens within 10 minutes of the last one, we skip logging it. This gives us accurate adoption data without overcounting brief disconnects or app switches.

We also learned an interesting behavioral difference: Android Auto connection events fire more frequently than CarPlay events because Android Auto can launch when users are already listening and then plug in, not just when they open the app while connected. This insight helps us interpret our analytics data correctly.

Preserving Listening Progress

One often-overlooked detail makes a huge difference in user experience: remembering where someone left off in a podcast episode. When a user selects a podcast in Android Auto, our code checks if they've previously listened to that episode. If they have, we automatically seek to their saved position before starting playback.

This small touch prevents the frustration of restarting a 45-minute podcast from the beginning when you'd already listened to 30 minutes during your morning walk.

Flutter's Role: Write Once, Enhance Everywhere

While implementing Android Auto required platform-specific work, Flutter's architecture made it far more manageable than it could have been. Here's how:

Shared Business Logic: All our logic for fetching podcasts, managing playback state, and organizing content lives in Dart code that works identically on iOS and Android. When we fetch the list of podcast shows for Android Auto, we're using the exact same code that populates our mobile app screens.

Riverpod State Management: The Riverpod package gave us a clean way to access app-wide state from our Android Auto handlers. Need the current podcast list? Just read from the sortedPodcastsProvider. Need to start playback? Call the PlayerNotifier. The same state management that powers our mobile app seamlessly extends to Android Auto.

The Audio Service Bridge: This Flutter package exemplifies the framework's approach to platform integration. It handles all the complex native Java code for implementing MediaBrowserServiceCompat, exposing simple Dart methods like getChildren and playFromMediaId. We write Dart code, and Audio Service ensures it works with Android's native media systems.

What We Learned

Simplicity is Powerful

One of the most valuable lessons was that constraints breed creativity. The intentional restrictions within CarPlay and Android Auto of limited text, simple navigation and restricted UI elements forced us to make hard decisions about what matters most. This made the experience better, not worse.

Our mobile app has dozens of features. Our car interfaces have three tabs. And for the driving use case, those three tabs are exactly what's needed.

Platform Guidelines Exist for Good Reasons

Apple's CarPlay Human Interface Guidelines and Android Auto’s Design for Driving initially felt restrictive. Why can't we show album artwork larger? Why can't we have custom buttons? Why is navigation so limited?

Then we drove with the app. And it made perfect sense. Every guideline is designed around one principle: keep the driver's attention on the road. Once we embraced that principle, our design decisions became obvious.

Analytics Tell Unexpected Stories

We expected CarPlay and Android Auto users to primarily listen to live radio, our "lean back" experience. The data showed strong engagement with podcast episodes and show browsing. People were using their drives to actively explore our content catalog, not just passively consume what was on air.

This insight is now influencing how we think about content curation and recommendations for future updates.

Iteration Matters, Even in Cars

We launched with a minimum viable product: live radio, The Latest, and podcast access. Based on usage patterns and feedback, we've been able to iterate with confidence, knowing the foundation was solid.

Feature flags gave us the flexibility to test changes with small groups before rolling out to everyone, this is crucial when you're building for a safety-critical environment.

The Benefits: Why This All Matters

Listener Safety

The most important benefit is safety. According to the National Highway Traffic Safety Administration, distracted driving claimed 3,308 lives in 2022 alone. By bringing KQED into the CarPlay interface, we're reducing the temptation for listeners to interact with their phones while driving.

With these integrations, listeners can:

  • Start live radio with a single tap
  • Switch to a podcast episode without looking at their phone
  • Use voice commands to control Live Radio playback
  • Access content through their car's steering wheel controls

Content Discovery

The CarPlay and Android Auto interfaces opened up new opportunities for content discovery. Before, if someone was listening to live radio in their car, they might not know about our podcast catalog. Now, they can browse shows and episodes right from their dashboard.

We surface "The Latest" daily news podcast prominently, introducing listeners who might only know KQED as a radio station to our on-demand journalism.

Listener Engagement

Early metrics suggest that CarPlay and Android Auto users show different engagement patterns than mobile-only users:

  • Longer listening sessions (because they're on longer drives)
  • Higher completion rates for podcast episodes
  • More diverse content consumption (exploring beyond their usual shows)

KQED’s Mission

As a public media organization, KQED's mission is to inform, inspire, and involve the communities they serve. CarPlay helps fulfill that mission by making quality journalism and storytelling accessible in one of the places people spend significant time: their cars.

During breaking news events, commuters can now stay informed with live coverage. During long drives, they can dive into investigative podcasts. And during school drop-offs, they can catch five-minute news updates from The Latest.

Looking Forward

Our initial support of auto platforms is just a start and only one piece of KQED’s broader mobile strategy.As cars become increasingly connected and voice-first interfaces become more sophisticated, we're excited to continue evolving how listeners experience public radio on the go.

The lessons learned from this implementation, particularly around content organization, seamless transitions, and cross-platform architecture, will inform how we approach future in-car and connected device experiences.

For now, whether you're an iOS user with CarPlay or an Android user with Android Auto, KQED is ready to accompany you on your journey.

On the Roadmap

  • Better recommendations: Using listening history to surface relevant episodes
  • Chapter markers: Letting listeners skip to specific segments in long episodes
  • Offline queuing: Pre-downloading episodes for areas with poor coverage
  • Enhanced metadata: Showing more context about what's playing

What We're Watching

  • User behavior: How do listening patterns differ across CarPlay and Android Auto vs. mobile?
  • Platform updates: What new capabilities do Apple & Google add to CarPlayand Android Auto each year?
  • Technology changes: How will the shift to electric vehicles (with larger screens and more automation) change expectations?

Questions We're Asking

  • How can we make content discovery even more effortless?
  • What role should personalization play in a shared environment (like a family car)?
  • How do we balance automation with user control?

Recommendations for Other Product Teams

If your team is considering adding CarPlay or Android Auto support, here are our recommendations:

Start Simple: Don't try to replicate your entire mobile app. Focus on the core use case that makes sense while driving.

Respect the Context: Driving is fundamentally different from sitting on a couch with your phone. Design for glanceability and one-tap actions.

Test in Real Cars: The simulator is useful, but nothing beats real-world testing in actual vehicles with different screen sizes, connectivity strengths, and driving conditions.

Use Feature Flags: Build flexibility into your implementation so you can iterate safely.

Talk to Your Users: Our best insights came from listening to people who actually used CarPlay and Android Auto during their commutes.

Be Patient with Platform Limitations: Some things you want to do simply aren't possible. Work with the constraints rather than fighting them.

Conclusion

Building CarPlay and Android Auto support for the KQED app wasn't about checking a feature box or keeping up with competitors. It was about meeting the mission: bringing quality journalism, storytelling, and information to people where they are.

Every product decision, every technical challenge, and every line of code was in service of one goal: making it effortless and safe for listeners to access KQED while driving.

The result is an experience that feels natural, stays out of the way, and lets the content shine. When someone connects their phone and KQED appears on their dashboard, they don't think about the technology. They just tap "Live Radio" and listen.

And that, ultimately, is the mark of good product design: becoming invisible so the experience can be everything.

The KQED Android mobile app is built with Flutter and available for iOS and Android. Android Auto support requires Android 6.0 or later and a compatible vehicle or head unit. To use CarPlay, you'll need an iPhone and a compatible vehicle or aftermarket CarPlay system.

We make great products
Looking for a partner to help you create a successful business and amazing software products? Get in touch with Uptech Studio today.
Get Started