Implementando Android Auto no seu projeto Flutter

Vinicius Oliveira
Flutter Brasil
Published in
6 min readMay 14, 2024

O que é o Android auto?

O Android Auto, desenvolvido pelo Google, transforma carros comuns em veículos inteligentes. Ele conecta smartphones Android aos sistemas de controle dos carros, permitindo fazer chamadas, usar mapas e enviar mensagens sem tocar no celular. Com todos os apps e recursos do telefone na tela do carro, é possível fazer várias coisas com segurança enquanto dirige.

Como vamos fazer essa integração?

Atualmente, não é possível criar telas do Android Auto usando Flutter. No entanto, podemos comunicar-nos por meio do MethodChannel e EventChannel para obter o estado do aplicativo na nossa aplicação.

Declare o com.google.android.gms.car.application meta-data

  • File name: automotive_app_desc.xml
  • Resource type: XML
  • Root element: automotiveApp
<?xml version="1.0" encoding="utf-8"?>
<automotiveApp xmlns:android="http://schemas.android.com/apk/res/android">
<uses name="template"/>
</automotiveApp>

Para tornar seu aplicativo compatível com Android auto, devemos incluir os seguintes metadados em nosso arquivo AndroidManifest.xml.

<metadados android
:name= "com.google.android.gms.car.application"
android:resource= "@xml/automotive_app_desc" />

<uses-feature android:name="android.hardware.type.automotive"
android:required="true" />

<uses-feature android:name="android.software.car.templates_host"
android:required="true" />

Estes elementos declara que o aplicativo requer o software de host de modelos de carro do Android, que é essencial para a integração adequada com o Android Auto. O atributo android:required="true" indica novamente que esta característica é obrigatória para o funcionamento correto do aplicativo.

Essas declarações no arquivo AndroidManifest.xml asseguram que o aplicativo seja instalado e funcione apenas em dispositivos compatíveis com o Android Auto. Isso garante uma experiência consistente e adequada para os usuários, pois o aplicativo é projetado para interagir especificamente com sistemas de infoentretenimento automotivo, garantindo sua funcionalidade e desempenho ideais.

Nosso manifest vai ficar assim:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature
android:name="android.hardware.type.automotive"
android:required="true" />
<uses-feature
android:name="android.software.car.templates_host"
android:required="true" />
<application
android:label="flutter_with_android_auto"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<meta-data
android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc" />
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">

<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>

</activity>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<service
android:name=".FlutterCarAppService"
android:exported="true"
>
<intent-filter>
<action android:name="androidx.car.app.CarAppService" />
<category android:name="androidx.car.app.category.IOT" />
</intent-filter>
</service>
<meta-data
android:name="androidx.car.app.minCarApiLevel"
android:value="6" />
</application>
</manifest>
        <service
android:name=".FlutterCarAppService"
android:exported="true"
>
<intent-filter>
<action android:name="androidx.car.app.CarAppService" />
<category android:name="androidx.car.app.category.IOT" />
</intent-filter>
</service>

Este código no arquivo AndroidManifest.xml é essencial para tornar o seu aplicativo compatível com o Android Auto. Aqui está a explicação:

Declaração do Serviço (<service>):

  • android:name=".FlutterCarAppService": Especifica o serviço que será utilizado pelo Android Auto. No seu caso, é o serviço FlutterCarAppService.
  • android:exported="true": Indica que este serviço pode ser acessado por outros aplicativos. No contexto do Android Auto, isso permite que o Android Auto interaja com o seu aplicativo.

Filtro de Intenção (<intent-filter>):

  • <action android:name="androidx.car.app.CarAppService" />: Define a ação que aciona este serviço como sendo destinada ao Android Auto. Isso garante que o Android Auto possa reconhecer e interagir com o seu serviço.
  • <category android:name="androidx.car.app.category.IOT" />: Especifica a categoria do serviço como sendo "Internet of Things" (IoT). Esta categoria é importante para indicar que o seu aplicativo é destinado a dispositivos de Internet das Coisas, como os sistemas de infoentretenimento de carros compatíveis com o Android Auto.

Em resumo, essas configurações no manifesto são necessárias para que o Android Auto reconheça e interaja corretamente com o seu aplicativo, garantindo uma experiência consistente e adequada para os usuários

Você não poderá ver seu aplicativo no Android Auto, a menos que tenha baixado da Play Store. Se quiser testar seu aplicativo para Android Auto, você pode instalar DHU (Desktop Head Unit) em seu IDE. Este link contém instruções simples para instalar o DHU.

Agora temos que escrever algumas classes em Kotlin para funcionar o Android Auto.

class FlutterCarAppService  : CarAppService() {

override fun createHostValidator(): HostValidator {
return HostValidator.ALLOW_ALL_HOSTS_VALIDATOR
}

override fun onCreateSession(): Session {
return AndroidAutoSession()
}
}

class AndroidAutoSession : Session() {
override fun onCreateScreen(intent: Intent): Screen {
return MainScreen(carContext)
}
}

A classe abstrata CarAppService implementa os métodos Service como onBind e onUnbind, evitando futuras substituições para garantir a compatibilidade com aplicativos host. Você só precisa implementar createHostValidator e onCreateSession.

O método createHostValidator retorna um HostValidator que é usado durante a vinculação para garantir que o host seja confiável. Para facilitar o desenvolvimento e testes, você pode usar ALLOW_ALL_HOSTS_VALIDATOR, mas na produção, é importante configurá-lo adequadamente. O método onCreateSession geralmente retorna uma instância de Session e pode ser usado para inicializar recursos de longa duração, como métricas e clientes de registro.

Em seguida vamos criar uma classe simples:

class MainScreen(carContext: CarContext) : Screen(carContext) {
companion object {
lateinit var screen:MainScreen
var counter: Int = 0
}
lateinit var template: Template
override fun onGetTemplate(): Template {
screen = this
screen.template = MessageTemplate.Builder("Counter: ${counter}")
.addAction(
Action.Builder()
.setTitle("Add")
.setOnClickListener {
counter += 1
AndroidAutoChannel.setCounter(counter)
}
.build())
.setHeaderAction(Action.APP_ICON)
.build()
return screen.template
}

fun updateCounter(value:Int) {
counter = value
invalidate()
}
}

Esta classe que estende Screen e recebe um contexto de carro CarContext como parâmetro no construtor. A variável screen mantém uma referência à própria instância da tela. A propriedade counter mantém o contador atual.

O método onGetTemplate retorna um modelo de mensagem MessageTemplateque exibe o valor atual do contador. Ele também define um cabeçalho de ação Action.APP_ICONpara a mensagem.

O método updateCounter atualiza o valor do contador e chama invalidate() para notificar que a tela precisa ser redesenhada com o novo valor do contador.

Na nossa MainActivity, vamos criar um methodchannel e eventchannel. Este canal será usado para enviar um inteiro que será utilizado para atualizar o nosso aplicativo no Android Auto.

class MainActivity: FlutterActivity() {

override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
AndroidAutoChannel.initialize(flutterEngine,context)
}
}


object AndroidAutoChannel {
private val CHANNEL_NAME = "androidAuto"
private val EVENT_NAME = "androidAutoStatus"
private var methodChannel: MethodChannel? = null
private var carPropertyManagerChannel: EventChannel? = null
fun setCounter(counter: Int) {
val data = mapOf("counter" to counter)
methodChannel?.invokeMethod("setCounter", data)
}
fun initialize(flutterEngine: FlutterEngine, context: Context) {
methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_NAME)
methodChannel?.setMethodCallHandler { call, result ->
when (call.method) {
"setCounter" -> {
val counter = call.arguments as Map<String, Any>
MainScreen.screen?.updateCounter(counter["counter"] as Int)
result.success(null)
}
}
}
carPropertyManagerChannel = EventChannel(flutterEngine.dartExecutor.binaryMessenger, EVENT_NAME)
carPropertyManagerChannel?.setStreamHandler(SimplesAndroidAutoConnectionEvent(context))
}
}

class SimplesAndroidAutoConnectionEvent(private val context: Context) : EventChannel.StreamHandler {
companion object {
private var androidAutoEventSink:EventChannel.EventSink? = null
fun onCarConnectionChange(status:String) {
androidAutoEventSink?.success(status)
}
}
override fun onListen(args: Any?, events: EventChannel.EventSink) {
androidAutoEventSink = events
}


override fun onCancel(arguments: Any?) {
androidAutoEventSink = null
}
}

Agora estamos na etapa de transição para o Dart, onde vamos definir a instância do nosso methodchannel e eventchannel desenvolver o contador no Flutter.

void main() {
runApp(const MyApp());
}

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

// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(),
);
}
}

class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
final _counter = ValueNotifier<int>(0);
final _methodChannel = const MethodChannel('androidAuto');
final _eventChannel = const EventChannel('androidAutoStatus');

void _incrementCounter() {
_counter.value++;
_methodChannel.invokeMethod('setCounter', {'counter': _counter.value});
}

@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_methodChannel.setMethodCallHandler((call) async {
debugPrint('Method: ${call.method}');
if (call.method == 'setCounter') {
if (call.arguments case {'counter': final counter}) {
_counter.value = int.parse(counter.toString());
_methodChannel.invokeMethod('setCounter', {'counter': _counter.value});
}
}
});
_eventChannel.receiveBroadcastStream().listen((event) {
debugPrint('Event: $event');
});
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter Demo'),
),
body: ValueListenableBuilder(
valueListenable: _counter,
builder: (context, counter, child) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'You have pushed the button this many times:',
),
Text(
'$counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
);
}),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
Vídeo demonstrativo do funcionamento.

Link do repositório do github aqui.

Link para a comunidade flutter do discord:

Participe da comunidade Flutter no Discord! Junte-se a nós em discussões e colaborações sobre Flutter. Clique aqui para acessar. Além disso, recomendo seguir criadores de conteúdo como Toshi Ossada, Brunno Marques e Edson Souza para mais conteúdo de qualidade.

Referência:

--

--