iosfluttercarplay

Access Flutter app from CarPlay screen without opening app first


I have Flutter application - simple online player that uses flutter_carplay and audio_service. I release this app to iOS only.

  1. When the app starts, it loads items that can be played. This works as expected on mobile device.
  2. In case I start the app and then I turn it on through CarPlay as well, CarPlay screen shows all items correctly.
  3. In case app is not running and I open it only through CarPlay, the screen is empty. It seems to me that the app needs to be started first, before it can work through CarPlay. Can somebody advise how to achieve this behaviour (so it can be started from CarPlay directly)? I believe it can be helpful for many developers, as the CarPlay functionality in Flutter is still not documented well.

This the entry point of the app:

void main() async {
  await App.initApp(); // basic initialization (Firebase, ...)
  runApp(const App());
}

This is App widget.

class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        builder: (BuildContext context, Widget? child) {},
        home: HomePage()
    );
  }
}

All the CarPlay related code can be found in HomePage:

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final FlutterCarplay _flutterCarplay = FlutterCarplay();

  @override
  void initState() {
    super.initState();
    initCarPlay();
  }

  @override
  Widget build(BuildContext context) {
    return ...;
  }

  void initCarPlay() {
    FlutterCarplay.setRootTemplate(
      rootTemplate: CPListTemplate(
        sections: [
          CPListSection(header: "Section 1", items: [
            CPListItem(text: "Item 1"),
            CPListItem(text: "Item 2"),
            CPListItem(text: "Item 3"),
          ]),
          CPListSection(header: "Section 2", items: [
            CPListItem(text: "Item 4"),
            CPListItem(text: "Item 5"),
            CPListItem(text: "Item 6"),
          ])
        ],
        showsTabBadge: false,
        systemIcon: "house.fill",
      ),
      animated: true,
    );

    _flutterCarplay.forceUpdateRootTemplate();
    _flutterCarplay.addListenerOnConnectionChange(_onCarplayConnectionChange);
  }

  void _onCarplayConnectionChange(CPConnectionStatusTypes status) {
    //
  }

  @override
  void dispose() {
    _flutterCarplay.removeListenerOnConnectionChange();
    super.dispose();
  }
}

I have tried to call initCarPlay() in the main() directly, but the result was the same.


Solution

  • Firstly, you need to make sure that your initCarPlay method is called even when the app is started from CarPlay. Furthermore, handle CarPlay-specific lifecycle events to initialize your CarPlay interface appropriately and ensure the necessary data and services are initialized in the background, so the app is ready when launched from CarPlay.

    Modified main function

    void main() async {
      WidgetsFlutterBinding.ensureInitialized();
      await App.initApp(); 
      runApp(const App());
    }
    

    App Widget

    class App extends StatelessWidget {
      const App({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          builder: (BuildContext context, Widget? child) {
           return MediaQuery(
             data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
             child: child!,
           );
          },
          home: const HomePage(),
       );
      }
    }
    

    Modified HomePage Screen

    Ensure initCarPlay is called regardless of how the app is launched.

    class HomePage extends StatefulWidget {
      const HomePage({super.key});
    
      @override
      State<HomePage> createState() => _HomePageState();
    }
    
    class _HomePageState extends State<HomePage> {
      final FlutterCarplay _flutterCarplay = FlutterCarplay();
    
      @override
      void initState() {
        super.initState();
        WidgetsBinding.instance?.addPostFrameCallback((_) => initApp());
        _flutterCarplay.addListenerOnConnectionChange(_onCarplayConnectionChange);
      }
    
      Future<void> initApp() async {
        initCarPlay();
      }
    
      void initCarPlay() {
        FlutterCarplay.setRootTemplate(
          rootTemplate: CPListTemplate(
            sections: [
              CPListSection(header: "Section 1", items: [
                CPListItem(text: "Item 1"),
                CPListItem(text: "Item 2"),
                CPListItem(text: "Item 3"),
              ]),
              CPListSection(header: "Section 2", items: [
                CPListItem(text: "Item 4"),
                CPListItem(text: "Item 5"),
                CPListItem(text: "Item 6"),
              ])
            ],
            showsTabBadge: false,
            systemIcon: "house.fill",
          ),
          animated: true,
        );
    
        _flutterCarplay.forceUpdateRootTemplate();
      }
    
      void _onCarplayConnectionChange(CPConnectionStatusTypes status) {
        if (status == CPConnectionStatusTypes.connected) {
          initCarPlay();
        }
      }
    
      @override
      void dispose() {
        _flutterCarplay.removeListenerOnConnectionChange();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Center(
            child: Text('Home Page Content'),
          ),
        );
      }
    }