oguzhnatly/flutter_carplay

CarPlay app required Flutter App open first?

ankiimation opened this issue · 14 comments

I think CarPlay app can start itself with out starting Flutter App first

Hello @ankiimation,

That method you mentioned is not recommended because we set the root template of the CarPlay App in the Flutter App's initial state. As a result, if the flutter app is never opened after installation, you may see a black screen or crash directly. The CarPlay app may be displayed successfully after the first opening of the flutter app, but it may not function properly. That is why it is recommended to use the app. Currently, the Flutter App must be opened first to see the CarPlay app screens/templates.

I also highly recommend creating a graphic art for the carplay app when it is not open, as this will notify the user of the need to open the app first.

Hello @ankiimation,

That method you mentioned is not recommended because we set the root template of the CarPlay App in the Flutter App's initial state. As a result, if the flutter app is never opened after installation, you may see a black screen or crash directly. The CarPlay app may be displayed successfully after the first opening of the flutter app, but it may not function properly. That is why it is recommended to use the app.

I also highly recommend creating a graphic art for the carplay app when it is not open, as this will notify the user of the need to open the app first.

ok bro, love it

Hi @ankiimation,

I have found a way to launch the Flutter app when starting the app through the CarPlay interface when the app is not running on the foreground. The solution is based on this blog post: https://adapptor.com.au/blog/enhance-existing-apps-with-carplay where they solve it for ReactNative.

The main idea is to move the FlutterEngine creation to the AppDelegate, which will be run when the app is launched from anywhere (CarPlay or device). We can run the FlutterEngine in headless mode, so the dart code can run before the engine is attached to any actual view (which happens when launching the app first on the CarPlay). Then, on the SceneDelegate we reuse the engine and attach it to the actual view.

So the AppDelegate.swift will be like this:

import UIKit
import Flutter

let flutterEngine = FlutterEngine(name: "SharedEngine", project: nil, allowHeadlessExecution: true)

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application( _ application: UIApplication,
                             didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    
    flutterEngine.run()
    GeneratedPluginRegistrant.register(with: flutterEngine)

    return super.application(application, didFinishLaunchingWithOptions: launchOptions);
    
  }
}

and the SceneDelegate.swift like this:

@available(iOS 13.0, *)
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

        guard let windowScene = scene as? UIWindowScene else { return }

        window = UIWindow(windowScene: windowScene)
        let controller = FlutterViewController.init(engine: flutterEngine, nibName: nil, bundle: nil)
        window?.rootViewController = controller
        window?.makeKeyAndVisible()
    }
}

With this we have the dart code running when the user launches the app from the CarPlay interface. However, some changes are needed on this plugin as well, as it will fail to update the rootInterface if it was null when launching the CarPlayScene. The solution here is to modify the FlutterCarplayPluginSceneDelegate so it doesn't set the interfaceController to nil if the rootTemplate is not set at startup. You have to replace the templateApplicationScene with the following snippet:

func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene,
                                didConnect interfaceController: CPInterfaceController) {
    FlutterCarPlaySceneDelegate.interfaceController = interfaceController
    
    SwiftFlutterCarplayPlugin.onCarplayConnectionChange(status: FCPConnectionTypes.connected)
    let rootTemplate = SwiftFlutterCarplayPlugin.rootTemplate
    
    if rootTemplate != nil {
      FlutterCarPlaySceneDelegate.interfaceController?.setRootTemplate(rootTemplate!, animated: SwiftFlutterCarplayPlugin.animated, completion: nil)
    }
  }

Then on your dart code, after setting the root template using FlutterCarplay.setRootTemplate you have to force the plugin to update the displayed template using: _flutterCarplay.forceUpdateRootTemplate();

With this you should be able to run the app when the user opens it on the CarPlay.

I have a branch with this changes here: snipd-mikel@a4a201c

@oguzhnatly Would it be possible to merge the changes onto the main repository? Only the ones on this file FlutterCarplayPluginSceneDelegate are actually needed to be included on the plugin. The rest of the changes could be part of the README. If you are interested I can work on a PR to have this ready.

@snipd-mikel
Love your work bro. I already have made my flutter navigation app run on CarPlay without open it on the device first. The same as you, I created a static FlutterEngine in AppDelegate.

But there some issues when we run Flutter app on CarPlay without open it on the device first:

  • dart:ui: I can't turn into ui.Image and draw images(assets, network) on canvas
  • navigator: I'm using AutoRouter and cant push to other screens
  • ...
    But luckily, all other functions are still OK haha

Thanks for the info. I haven't yet run into many issues, as most of what I need on the CarPlay is not related to Flutter UI components. However, I encountered some issue where after some async event the UI was not being updated (I needed that to start the authentication flow) . I had to manually call WidgetsBinding.instance.scheduleForcedFrame(); to force it to update.

I don't think this issue should be closed. At least it should be documented. On what the recommended way should be to implement this.

I don't think this should be closed either. Have any of you encountered a bug, where there is a black screen after a splash? I think that's because initializing Flutter renderer overrides the splash, but I don't know how could I solve that

TO ANYONE WHO STRUGGLES WITH SPLASHSCREENS ON IOS

I've found out that the only thing needed here is to add
controller.loadDefaultSplashScreenView()
in @snipd-mikel solution after controller initialization. Now it works like a charm :)

Also I don't think that this is an ideal solution, won't it create two engines sometimes? Bc I keep getting some old view for a split second before the splashscreen

After these changes i cannot login with facebook when i have the fb app installed .

Thanks for the info. I haven't yet run into many issues, as most of what I need on the CarPlay is not related to Flutter UI components. However, I encountered some issue where after some async event the UI was not being updated (I needed that to start the authentication flow) . I had to manually call WidgetsBinding.instance.scheduleForcedFrame(); to force it to update.

I use GetX for status management, but when I try to open CarPlay first and then start the mobile app, I find that GetX is invalid. Have you ever experienced a similar situation?

@harryandroiddev i've solved just with this in SceneDelegate:

 if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
      appDelegate.window = window
  }