Skip to main content

#BuiltWithHERE: How MyRoute-app B.V. Integrated Android Auto & Apple CarPlay into their HERE Flutter SDK-based app

MRA_BWH2

Introduction

#BuiltWithHERE is a series showcasing how developers are using the HERE platform to solve location-related business problems. Each blog post highlights a company who has developed a solution that is timely, innovative, and uses HERE products and/or data. Today, we’re following up with the Dutch company MyRoute-app B.V., whom we wrote about in May 2024. 

In this two-part blog post, MyRoute-app’s lead developers Joost van Someren and Corjan Meijerink are the guest authors! They will detail how they integrated Android Auto and Apple CarPlay into their HERE SDK for Flutter app, a subject that has not been widely documented. It should be noted that while Flutter doesn't provide direct support for Android Auto and Apple CarPlay, combining the tools the HERE SDK provides and some platform code, you can implement most elements you could want. 

The HERE SDK for Flutter makes it easy to implement a fully-fledged navigation app for mobile devices, such as smartphones. Additionally, an increasing number of people are looking for integrations with their car in the form of either Android Auto or Apple CarPlay. Developing a HERE SDK for Flutter app that leverages both Android Auto and Apple CarPlay expands mobile app development. Developers can ensure their app is accessible to a wider audience, providing seamless navigation and functionality across different vehicle systems, which is increasingly important as more users rely on in-car technology. 

Our starting point will be using the HERE SDK reference application for Flutter, and will demonstrate step-by-step the changes needed to integrate with Android Auto or Apple CarPlay. The second part will culminate with implementing some basic features. By the end of both blog posts,  you'll have a solid understanding of how to integrate Android Auto or Apple CarPlay into your projects.

Note: The final codebase can be found here.

Tutorial Outline for Part 1:

This blog post will cover the following topics:

  1. Preparing the Flutter project
    a. Android Auto - Android (Kotlin)
    b. Apple CarPlay - iOS (Swift)
    c. Flutter (Dart)

Preparing the Flutter project


If you’d like to follow along, download the HERE SDK reference application for Flutter from HERE’s GitHub. Pay special attention to the prerequisites outlined in the getting started section of the README to build the app correctly.

Note: When making changes related to either Android or iOS it might be preferable to use Android Studio of XCode respectively. Especially for iOS, XCode takes care of assigning newly created files to your project.

We'll refer to the “non-Flutter” code we're going to be writing as "platform code". You can recognize it whenever it is in either Kotlin for Android, or Swift for iOS. With that sorted let's dive into it!


Android Auto - Android (Kotlin)


If you've looked around the HERE documentation you might have seen the HERE SDK for Android documentation regarding Android Auto. It lists the steps needed to let us open our application in Android Auto and show a map. Since we are using Flutter, there are some minor changes we need to make to the listed steps. All these steps will be mentioned below and are changed, where relevant, to meet our purposes. The main difference is that we'll be using Kotlin instead of Java, which is merely a matter of preference.

Now, before we delve into any code, we need a way to test what we're about to implement! For this we need the Desktop Head Unit (DHU). The HERE documentation does explain how to install this piece of software, so please follow the steps as written here.

Additional information on DHU is available from the official Android documentation.

 With that done it is time to look at some code! As noted, a lot of what you're about to read is the same as what is in the HERE Android SDK documentation. However, there are some necessary changes due to integrating Android Auto with Flutter. If you follow the steps below you shouldn't run into any problems. Let's begin:

 
Make sure the minSdkVersion Version is set to 23 or higher in the app's android/app/build.gradle file as Android Auto is only supported on Android 6.0 or higher:
Copied
        minSdkVersion 23
  

In the same file, integrate Android Auto in the dependencies closure as well as the HERE SDK:

Copied
        dependencies {
    ...

    // Android Auto
    implementation "androidx.car.app:app:1.2.0-rc01"
    compileOnly fileTree(dir: '../../plugins/here_sdk/android/libs', include: ['*.aar'])
}
  

In the AndroidManifest.xml file (android/app/src/main/AndroidManifest.xml), add the following required permissions:

Copied
        <!-- Declare Android Auto specific permissions. -->
<uses-permission android:name="androidx.car.app.ACCESS_SURFACE"/>
<uses-permission android:name="androidx.car.app.NAVIGATION_TEMPLATES"/>
  

In this same file, add the following to the <application> tag:

Copied
        <!-- Declare the CarAppService for use with Android Auto. -->
<service
    android:name=".HelloMapCarAppService"
    android:exported="true">
    <intent-filter>
        <action android:name="androidx.car.app.CarAppService"/>
        <category android:name="androidx.car.app.category.NAVIGATION"/>
    </intent-filter>
</service>

<!-- Declare the Android Auto API Level and layout template. -->
<meta-data
    android:name="androidx.car.app.minCarApiLevel"
    android:value="1"/>
<meta-data
    android:name="com.google.android.gms.car.application"
    android:resource="@xml/automotive_app_desc"
    tools:ignore="MetadataTagInsideApplicationTag"/>
  

Next, create the HelloMapCarAppService class below - as well as the required automotive_app_desc.xml template. For our example, this file has only this content:

Copied
        <?xml version="1.0" encoding="utf-8"?>
<automotiveApp>
    <uses name="template" />
</automotiveApp>
  

Create a new "xml" directory and add the following file: android/app/src/main/res/xml/automotive_app_desc.xml.

Then create the HelloMapCarAppService Kotlin class / file (place it in the same directory as your MainApplication.kt):

Copied
        package com.example.RefApp
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.ApplicationInfo
import androidx.car.app.CarAppService
import androidx.car.app.Screen
import androidx.car.app.Session
import androidx.car.app.validation.HostValidator
/**
 * Entry point for the hello map app.
 *
 * <p>{@link CarAppService} is the main interface between the app and the car host. For more
 * details, see the <a href="https://developer.android.com/training/cars/apps">Android for
 * Cars Library developer guide</a>.
 */
class HelloMapCarAppService: CarAppService() {
    @SuppressLint("PrivateResource")
    override fun createHostValidator(): HostValidator {
        return if ((applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0) {
            HostValidator.ALLOW_ALL_HOSTS_VALIDATOR
        } else {
            HostValidator.Builder(applicationContext)
               .addAllowedHosts(androidx.car.app.R.array.hosts_allowlist_sample)
               .build()
        }
    }

    override fun onCreateSession(): Session {
        return object : Session() {
            override fun onCreateScreen(intent: Intent): Screen {
                return HelloMapScreen(carContext)
            }
        }
    }
}
  

Lastly, to show a map once we start Android Auto, we're going to create the HelloMapScreen class / file. Create this in the same directory as the HelloMapCarAppService and with the following content:

Copied
        package com.example.RefApp

import androidx.car.app.AppManager
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.SurfaceCallback
import androidx.car.app.SurfaceContainer
import androidx.car.app.model.Action
import androidx.car.app.model.ActionStrip
import androidx.car.app.model.Template
import androidx.car.app.navigation.model.NavigationTemplate
import com.here.sdk.core.GeoCoordinates
import com.here.sdk.mapview.MapMeasure
import com.here.sdk.mapview.MapScheme
import com.here.sdk.mapview.MapSurface

class HelloMapScreen(carContext: CarContext) : Screen(carContext), SurfaceCallback {
    private val mapSurface: MapSurface

    init {
        carContext.getCarService(AppManager::class.java).setSurfaceCallback(this)
        // The MapSurface works the same as the HereMapController we know from Flutter. It uses
        // a surface in order to draw the map to the screen.
        mapSurface = MapSurface()
    }

    override fun onGetTemplate(): Template {
        // Add a button to exit the app.
        val actionStripBuilder = ActionStrip.Builder()
        actionStripBuilder.addAction(
            Action.Builder()
                .setTitle("Exit")
                .setOnClickListener(this::exit)
                .build()
        )

        val builder = NavigationTemplate.Builder()
        builder.setActionStrip(actionStripBuilder.build())

        return builder.build()
    }

    override fun onSurfaceAvailable(surfaceContainer: SurfaceContainer) {
        mapSurface.setSurface(
            carContext,
            surfaceContainer.surface,
            surfaceContainer.width,
            surfaceContainer.height
        )

        mapSurface.mapScene.loadScene(MapScheme.NORMAL_DAY) { mapError ->
            if (mapError == null) {
                val distanceInMeters = 1000.0 * 10
                val mapMeasureZoom = MapMeasure(MapMeasure.Kind.DISTANCE, distanceInMeters)
                mapSurface.camera.lookAt(GeoCoordinates(52.530932, 13.384915), mapMeasureZoom)
            }
        }
    }

    override fun onSurfaceDestroyed(surfaceContainer: SurfaceContainer) {
        mapSurface.destroySurface()
    }

    private fun exit() {
        carContext.finishCarApp()
    }
}
  

This is the most interesting part of their integration with Android Auto. Since we declared our app as a service to support Android Auto in our AndroidManifest.xml, it will automatically show the HelloMapScreen on a DHU launched in Android Auto.

To render content, an app needs to render to a Surface. For this, every Android Auto screen implements the SurfaceCallback to get notified when the surface is available. For convenience, the HERE SDK provides a MapSurface class that accepts a SurfaceContainer as parameter:

Copied
        mapSurface.setSurface(
           carContext,
           surfaceContainer.surface,
           surfaceContainer.width,
           surfaceContainer.height
        )
  

If you've followed all the steps you should now see a map in the DHU: Android Auto DHU showing a map              

DHU

                                                               

Apple CarPlay - iOS (Swift)

Similar to the Android Auto integration, the HERE SDK for iOS documentation regarding Apple CarPlay does explain how to implement Apple CarPlay. However, as was the case with our Android Auto integration, there are some important differences with regards to Flutter we will cover.

First, we need to make sure we can build our application. Start XCode and open the Runner target. Under "General" we need to change the minimum deployment target to 13.0. Then head on over to Signing & Capabilities, change the "Team" and "Bundle Identifier" to something you can build for. 

Create a new Property List file with the name Entitlements.plist. This file should be located in the ios/Runner directory. Edit it to contain the following:

Copied
        <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.developer.carplay-maps</key>
	<true/>
</dict>
</plist>
  

The com.apple.developer.carplay-maps key indicates an application scope that is needed for turn-by-turn navigation apps. In order for our application to make use of these entitlements we need to apply it. Once again open the **Runner** target and select **Build Settings**. On This page you should find **Code Signing Entitlements** set its path to `Runner/Entitlements.plist`.

Now add the following to your Info.plist to enable the CarPlay scene configuration. Note that for the HERE Reference app you should do this in both the Info-Debug.plist and Info-Release.plist:

Copied
        <!-- Scene configuration -->
<key>UIApplicationSceneManifest</key>
<dict>
    <key>UIApplicationSupportsMultipleScenes</key>
    <true/>
    <key>UISceneConfigurations</key>
    <dict>
        <!-- iPhone and iPad Scene Configuration -->
        <key>UIWindowSceneSessionRoleApplication</key>
        <array>
            <dict>
                <key>UISceneClassName</key>
                <string>UIWindowScene</string>
                <key>UISceneDelegateClassName</key>
                <string>$(PRODUCT_MODULE_NAME).iPhoneSceneDelegate</string>
                <key>UISceneConfigurationName</key>
                <string>Default Configuration</string>
            </dict>
        </array>
        <!-- CarPlay Scene Configuration -->
        <key>CPTemplateApplicationSceneSessionRoleApplication</key>
        <array>
            <dict>
                <key>UISceneClassName</key>
                <string>CPTemplateApplicationScene</string>
                <key>UISceneDelegateClassName</key>
                <string>$(PRODUCT_MODULE_NAME).CarPlaySceneDelegate</string>
                <key>UISceneConfigurationName</key>
                <string>Default CarPlay Configuration</string>
            </dict>
        </array>
    </dict>
</dict>
  

We need to create our CarPlaySceneDelegate and make it conform to the CPTemplateApplicationSceneDelegate protocol to manage the lifecycle events for the CarPlay scenes. CPTemplateApplicationSceneDelegate is responsible for setting up the user interface in CarPlay and handling the transitions between different states of the application when used in a CarPlay environment. This class is specified in the Info.plist under the CPTemplateApplicationSceneSessionRoleApplication key and gets called when the app interacts with CarPlay.

CPTemplateApplicationSceneDelegate requires us to implement two methods:

  1. The first one notifies when the mobile device is connected to the head unit's display - and thus, we receive a CarPlay window to show content. 
  2. The second one notifies, when the device is disconnected. The CPTemplateApplicationSceneDelegate allows us to receive a CPWindow for which we set our CarPlayViewController instance as rootViewController. This will be the base view to manage the content shown in our CarPlay window.

Below is the implementation of CarPlaySceneDelegate:

Copied
        import CarPlay

// `CarPlaySceneDelegate` manages the lifecycle events for the CarPlay scenes.
class CarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDelegate {
    var interfaceController: CPInterfaceController?
    var carPlayWindow: CPWindow?
    let carPlayMapTemplate = CPMapTemplate()
    let carPlayViewController = CarPlayViewController()

    /// Conform to `CPTemplateApplicationSceneDelegate`, needed for CarPlay.
    /// Called when the CarPlay interface controller connects and a new window for CarPlay is created.
    /// Initializes the view controller for CarPlay and sets up the root template with necessary UI elements.
    func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene,
                                  didConnect interfaceController: CPInterfaceController,
                                  to window: CPWindow) {
        self.interfaceController = interfaceController
        self.carPlayWindow = window

        // CarPlay window has been connected. Set up the view controller for it and a map template.
        interfaceController.setRootTemplate(carPlayMapTemplate, animated: true)
        // CarPlayViewController is main view controller for the provided CPWindow.
        window.rootViewController = carPlayViewController
    }

    /// Conform to `CPTemplateApplicationSceneDelegate`, needed for CarPlay.
    /// Called when the CarPlay interface is disconnected.
    /// Use this method to clean up resources related to the CarPlay interface.
    func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene,
                                  didDisconnect interfaceController: CPInterfaceController,
                                  from window: CPWindow) {
        // Handle disconnection from CarPlay.
    }
}
  

Normally Flutter automatically handles our scenes for us and we don't have to do anything further. However, now that we've defined our own Scene configuration in the Info.plist we need to make some changes. The first change is to AppDelegate.swift. Replace the existing method with the code shown below:

Copied
        override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
    
    return true
}
  

Our second change is to create our iPhoneSceneDelegate as we defined in our Info.plist. Create a new file called iPhoneSceneDelegate.swift in the same directory as the AppDelegate.swift. Add the following code:

Copied
        // `iPhoneSceneDelegate` manages the lifecycle events of a UI scene for the application.
class iPhoneSceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    /// Called when a new scene session is being created and associated with the app.
    /// This method sets up the initial content and configuration for the scene using Storyboards.
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = scene as? UIWindowScene else { return }

        window = UIWindow(windowScene: windowScene)
        
        // Manually create a FlutterEngine and FlutterViewController.
        let flutterEngine = FlutterEngine(name: "SceneDelegateEngine")
        flutterEngine.run()
        GeneratedPluginRegistrant.register(with: flutterEngine)
        let controller = FlutterViewController.init(engine: flutterEngine, nibName: nil, bundle: nil)
        window?.rootViewController = controller
        window?.makeKeyAndVisible()
    }
}
  

Now our Flutter app will again work as expected.

Finally, we need to create a UIViewController to show a map when our app is used in CarPlay. This controller is called from our CarPlaySceneDelegate. In the same directory as the AppDelegate.swift create a new file CarPlayViewController.swift and add the following:

Copied
        import heresdk
import UIKit

// This is the view controller shown on the car's head unit display with CarPlay.
class CarPlayViewController: UIViewController {

    var mapView : MapView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Initialize MapView without a storyboard.
        mapView = MapView(frame: view.bounds)
        view.addSubview(mapView)

        // Load the map scene using a map scheme to render the map with.
        mapView.mapScene.loadScene(mapScheme: MapScheme.normalDay, completion: onLoadScene)
    }

    // Completion handler when loading a map scene.
    private func onLoadScene(mapError: MapError?) {
        guard mapError == nil else {
            print("Error: Map scene not loaded, \(String(describing: mapError))")
            return
        }

        // Configure the map.
        let camera = mapView.camera
        let distanceInMeters = MapMeasure(kind: .distance, value: 1000 * 7)
        camera.lookAt(point: GeoCoordinates(latitude: 52.518043, longitude: 13.405991), zoom: distanceInMeters)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        mapView.handleLowMemory()
    }
}
  

And that should be it for iOS! To test if it works you can use the CarPlay Simulator. You should be greeted with a map upon opening the app in the CarPlay Simulator:  Apple CarPlay Simulator showing a map

CP_Sim

Read on to learn about integrating everything we've done so far with Flutter!

Flutter (Dart)

With everything related to Android Auto and Apple CarPlay completed we're finally ready to start on our integration with Flutter!
It should be mentioned again that Flutter doesn't provide direct support for Android Auto and Apple CarPlay. However by combining the tools the HERE SDK provides us and some platform code we can implement everything we could want. The HERE documentation gives us a nice overview of how we could go about integrating with Flutter. In this blog post we're going to take things one step further.

Communication between Flutter code and platform code is done using MethodChannels. We're going to use the pigeon package for this. It uses MethodChannels under the hood but formalizes the calls to make it type-safe and easier to use. Install pigeon as a dev dependency:

Copied
        flutter pub add --dev pigeon
  

After installing pigeon, create a new file in a new directory called pigeons/car.dart. This will be our pigeon definition file. We're going to leave it quite empty for now but don't worry, we'll come back to it later:

Copied
        import 'package:pigeon/pigeon.dart';

// Configure where our generated pigeon code should end up.
@ConfigurePigeon(PigeonOptions(
    input: 'pigeons/car.dart',
    // Flutter
    dartOut: 'lib/pigeons/car.pg.dart',
    // Android
    kotlinOut: 'android/app/src/main/kotlin/com/example/RefApp/Car.pg.kt',
    kotlinOptions: KotlinOptions(package: 'com.example.RefApp'),
    // iOS
    swiftOut: 'ios/Runner/Car.pg.swift',
))

// Declare an API to call from our platform code into our Flutter code.
@FlutterApi()
abstract class CarToFlutterApi {
    void setupMapView();
}
  

If you're not following along precisely, adapt the kotlinOut and kotlinOptions according to your own app needs.

For us to be able to use what we've just written we need to generate the final code. This is a easy as running this command:

Copied
        dart run pigeon --input pigeons/car.dart
  

This should have generated a Dart, Kotlin and Swift file based on our definition file. With regards to iOS, be sure to add the generated file to your XCode project. Using XCode, right-click the **Runner** directory and select **Add Files to "Runner"...**.

To make use of our newly generated code we have to make a few changes to our platform code. Let's start with iOS.

The generated CarToFlutterApi needs access to our FlutterEngine, otherwise we won't be able to call into Flutter. To accomplish this we're exposing it on the iPhoneSceneDelegate:
 

Copied
        class iPhoneSceneDelegate: UIResponder, UIWindowSceneDelegate {

    // add this line
    static private(set) var flutterEngine: FlutterEngine? = nil

    var window: UIWindow?

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

        ...

        // add this line
        iPhoneSceneDelegate.flutterEngine = flutterEngine
    }
}
  

Next up, we're giving the CarPlayViewController a big overhaul. It will no longer be responsible for managing our map. Instead we're delegating this task to the Flutter part of our codebase. Let's see how:

Copied
        import heresdk
// add this line
import here_sdk
import UIKit

class CarPlayViewController: UIViewController {

    var mapView : MapView!
    var mapViewHost : MapViewHost!

    override func viewDidLoad() {
        super.viewDidLoad()

        mapView = MapView(frame: view.bounds)
        view.addSubview(mapView)
        
        // replace `mapView.mapScene.loadScene(...)` with the code below
        if let flutterEngine = iPhoneSceneDelegate.flutterEngine {
            mapViewHost = MapViewHost(
                viewIdentifier: 123,
                binaryMessenger: flutterEngine.binaryMessenger,
                mapView: mapView
            )
            
            CarToFlutterApi(binaryMessenger: flutterEngine.binaryMessenger).setupMapView { _ in }
        }
        
     }

    // remove the `onLoadScene` method

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        mapView.handleLowMemory()
    }
}
  

The MapViewHost is what will allow us to control the map instance from our Flutter code. And CarToFlutterApi(...).setupMapView calls into our Flutter code to let it know that it should prepare our map.

We will make comparable changes to our Android code. For the HelloMapScreen do the following:

Copied
        ...
// add these 2 lines
import com.here.sdk.mapview.MapSurfaceHost
import io.flutter.embedding.engine.FlutterEngineCache

class HelloMapScreen(carContext: CarContext) : Screen(carContext), SurfaceCallback {
    private val mapSurface: MapSurface
    // add these 2 lines
    private val mapSurfaceHost: MapSurfaceHost?
    private val carToFlutterApi: CarToFlutterApi?

    init {
        carContext.getCarService(AppManager::class.java).setSurfaceCallback(this)
        mapSurface = MapSurface()

        // add the code below
        val binaryMessenger = FlutterEngineCache.getInstance()
            .get(MainActivity.FLUTTER_ENGINE_ID)
            ?.dartExecutor
            ?.binaryMessenger
        
        mapSurfaceHost = binaryMessenger?.let { MapSurfaceHost(123, it, mapSurface) }
        carToFlutterApi = binaryMessenger?.let { CarToFlutterApi(it) }
    }
    
    ...

    override fun onSurfaceAvailable(surfaceContainer: SurfaceContainer) {
        mapSurface.setSurface(
            carContext,
            surfaceContainer.surface,
            surfaceContainer.width,
            surfaceContainer.height
        )

        // replace `mapSurface.mapScene.loadScene` with the line below
        carToFlutterApi?.setupMapView { }
    }

    ...

}
  

In the code above, MapSurfaceHost fulfills the same purpose as MapViewHost did for iOS. That should be it for the platform code. Now we need a way to handle the calls that are being made into our Flutter code. In our pigeon file we defined the abstract class CarToFlutterApi. And then we used the generated code within our platform code. For Flutter we need to implement it ourself, since CarToFlutterApi is generated as an abstract class. This implementation will receive and handle all the calls made from the platform code into our Flutter code.

Lets begin by implementing CarToFlutterApi. Create a new file at lib/car/car_to_flutter.dart, this is going to be our implementation:

Copied
        import 'dart:async';

import 'package:here_sdk/core.dart';
import 'package:here_sdk/mapview.dart';
import 'package:here_sdk/navigation.dart';

import '../pigeons/car.pg.dart';
import '../positioning/positioning.dart';
import '../positioning/positioning_engine.dart';

class CarToFlutter extends CarToFlutterApi {
    late final PositioningEngine _positioningEngine;
    StreamSubscription<Location>? _locationUpdatesSubscription;

    CarToFlutter(PositioningEngine positioningEngine) {
        // Register this class instance to receive calls from the platform code.
        CarToFlutterApi.setUp(this);
        _positioningEngine = positioningEngine;
    }

    void dispose() {
        // On disposal we de-register handling of calls.
        CarToFlutterApi.setUp(null);
        _locationUpdatesSubscription?.cancel();
    }

    @override
    void setupMapView() async {
        // Notice that we use the same 'id' as we defined in our platform code.
        final mapController = HereMapController(123);

        final isInitialized = await mapController.initialize((_) {});
        if (!isInitialized) throw 'Failed to initialize HereMapController';

        // Here we load the given MapScheme for the map running on
        // our Android Auto / Apple CarPlay screen.
        mapController.mapScene.loadSceneForMapScheme(MapScheme.normalDay, (error) {
            if (error != null) {
                throw 'Failed loading scene: $error';
            }
            
            // This VisualNavigator will be applied to the Android Auto / Apple CarPlay map.
            final visualNavigator = VisualNavigator()..startRendering(mapController);

            final lastKnownLocation = _positioningEngine.lastKnownLocation;
            if (lastKnownLocation != null) {
                mapController.camera.lookAtPointWithMeasure(
                    lastKnownLocation.coordinates,
                    MapMeasure(MapMeasureKind.distance, Positioning.initDistanceToEarth),
                );

                visualNavigator.onLocationUpdated(lastKnownLocation);
            }
            _locationUpdatesSubscription =
                _positioningEngine.getLocationUpdates.listen((event) {
                    visualNavigator.onLocationUpdated(event);
                });
        });
    }
}
  

The code within the setupMapView()-method should be quite familiar to anyone who has used the HereMap widget before. This is because after initializing the HereMapController this instance will pretty much function the same as if it were a map within our Flutter widget tree. No need to write additional platform code in order to, for example, draw a MapMarker. That is the beauty of this implementation.

But just implementing CarToFlutterApi won't get us there yet. We need to make sure that our newly create class is active within our app. In our case this is as simple as adding a new provider. In our lib/main.dart file:

Copied
        ...
import 'package:here_sdk_reference_application_flutter/environment.dart';
import 'package:provider/provider.dart';

// add this line
import 'car/car_to_flutter.dart';
import 'common/application_preferences.dart';
import 'common/custom_map_style_settings.dart';
...

class _MyAppState extends State<MyApp> {

    ...

    @override
    Widget build(BuildContext context) {
        return MultiProvider(
            providers: [
                ...
                // add this provider
                Provider(
                    lazy: false,
                    create: (context) {
                        final positioningEngine =
                            Provider.of<PositioningEngine>(context, listen: false);

                        return CarToFlutter(positioningEngine);
                    },
                    dispose: (context, value) => value.dispose(),
                ),
            ],

            ...
        );
    }
}
  

By defining this provider as lazy we ensure that it gets initialized with the rest of our app.

And that should be it! It's finally time to test if everything we've done actually works. For iOS you can use the 'CarPlay Simulator' to test your CarPlay integration. For Android you should use the DHU.  You should be greeted by a map on your Android Auto / Apple CarPlay screen. Initialized and controllable from our Flutter code:    

Android Auto DHU              

Flutter_aa

 Apple CarPlay Simulator:                            

 

Flutter_cp

 

In part two of this tutorial coming next week, we'll take things a step further. Showing you some examples of how to implement Android Auto / Apple CarPlay features. Specifically, implementing a routing screen in your app along with other helpful resources! See you at the next one.

MyRoute-app works in partnership with HERE's platinum partner Localeyes: https://local-eyes.nl/

 

Aaron Falk

Aaron Falk

Principal Developer Evangelist

Have your say

Sign up for our newsletter

Why sign up:

  • Latest offers and discounts
  • Tailored content delivered weekly
  • Exclusive events
  • One click to unsubscribe