Saltar al contenido principal

Escribir código personalizado específico de la plataforma

Aprende cómo escribir código personalizado específico de la plataforma en tu aplicación.

Esta guía describe cómo usar código personalizado específico de la plataforma con Flutter.

Resumen

#

Puedes utilizar código específico de la plataforma en tu aplicación de Flutter. Algunas formas comunes de hacerlo incluyen:

Flutter es compatible con las siguientes plataformas y lenguajes específicos de la plataforma:

  • Android: Kotlin, Java
  • iOS: Swift, Objective-C
  • Windows: C++
  • macOS: Objective-C
  • Linux: C

Perspectiva arquitectónica de los canales de plataforma

#

Los mensajes se transmiten entre el cliente (interfaz de usuario) y el host (plataforma) mediante canales de plataforma, como se ilustra en este diagrama:

Arquitectura de canales de plataforma

En el diagrama anterior, los mensajes y las respuestas se pasan de forma asíncrona a través de canales para garantizar que la interfaz de usuario permanezca receptiva. En el lado del cliente, MethodChannel para Flutter permite enviar mensajes que corresponden a llamadas a métodos. En el lado de la plataforma, MethodChannel para Android y FlutterMethodChannel para iOS permiten recibir llamadas a métodos y devolver un resultado. Estas clases te permiten desarrollar un plugin de plataforma con muy poco código boilerplate.

Soporte de tipos de datos

#

Las API estándar de canales de plataforma y el paquete Pigeon utilizan un códec de mensajes estándar llamado StandardMessageCodec que admite una serialización binaria eficiente de valores simples similares a JSON, como booleanos, números, Strings, búferes de bytes, Lists y Maps. La serialización y deserialización de estos valores hacia y desde los mensajes ocurre automáticamente al enviar y recibir valores.

La siguiente tabla muestra cómo se reciben los valores de Dart en el lado de la plataforma y viceversa:

DartKotlin
nullnull
boolBoolean
int (<=32 bits)Int
int (>32 bits)Long
doubleDouble
StringString
Uint8ListByteArray
Int32ListIntArray
Int64ListLongArray
Float32ListFloatArray
Float64ListDoubleArray
ListList
MapHashMap
DartJava
nullnull
booljava.lang.Boolean
int (<=32 bits)java.lang.Integer
int (>32 bits)java.lang.Long
doublejava.lang.Double
Stringjava.lang.String
Uint8Listbyte[]
Int32Listint[]
Int64Listlong[]
Float32Listfloat[]
Float64Listdouble[]
Listjava.util.ArrayList
Mapjava.util.HashMap
DartSwift
nullnil (NSNull cuando está anidado)
boolNSNumber(value: Bool)
int (<=32 bits)NSNumber(value: Int32)
int (>32 bits)NSNumber(value: Int)
doubleNSNumber(value: Double)
StringString
Uint8ListFlutterStandardTypedData(bytes: Data)
Int32ListFlutterStandardTypedData(int32: Data)
Int64ListFlutterStandardTypedData(int64: Data)
Float32ListFlutterStandardTypedData(float32: Data)
Float64ListFlutterStandardTypedData(float64: Data)
ListArray
MapDictionary
DartObjective-C
nullnil (NSNull cuando está anidado)
boolNSNumber numberWithBool:
int (<=32 bits)NSNumber numberWithInt:
int (>32 bits)NSNumber numberWithLong:
doubleNSNumber numberWithDouble:
StringNSString
Uint8ListFlutterStandardTypedData typedDataWithBytes:
Int32ListFlutterStandardTypedData typedDataWithInt32:
Int64ListFlutterStandardTypedData typedDataWithInt64:
Float32List FlutterStandardTypedData typedDataWithFloat32:
Float64List FlutterStandardTypedData typedDataWithFloat64:
ListNSArray
MapNSDictionary
DartC++
nullEncodableValue()
boolEncodableValue(bool)
int (<=32 bits)EncodableValue(int32_t)
int (>32 bits)EncodableValue(int64_t)
doubleEncodableValue(double)
StringEncodableValue(std::string)
Uint8ListEncodableValue(std::vector<uint8_t>)
Int32ListEncodableValue(std::vector<int32_t>)
Int64ListEncodableValue(std::vector<int64_t>)
Float32ListEncodableValue(std::vector<float>)
Float64ListEncodableValue(std::vector<double>)
ListEncodableValue(std::vector<EncodableValue>)
Map EncodableValue(std::map<EncodableValue, EncodableValue>)
DartC (GObject)
nullFlValue()
boolFlValue(bool)
intFlValue(int64_t)
doubleFlValue(double)
StringFlValue(gchar*)
Uint8ListFlValue(uint8_t*)
Int32ListFlValue(int32_t*)
Int64ListFlValue(int64_t*)
Float32ListFlValue(float*)
Float64ListFlValue(double*)
ListFlValue(FlValue)
MapFlValue(FlValue, FlValue)

Llamar a código específico de la plataforma usando canales de plataforma

#

El siguiente código demuestra cómo llamar a una API específica de la plataforma para recuperar y mostrar el nivel actual de la batería. Utiliza la API BatteryManager de Android, la API device.batteryLevel de iOS, la API GetSystemPowerStatus de Windows y la API UPower de Linux con un único mensaje de plataforma, getBatteryLevel().

El ejemplo añade el código específico de la plataforma dentro de la propia aplicación principal. Si deseas reutilizar el código específico de la plataforma en múltiples aplicaciones, el paso de creación del proyecto es ligeramente diferente (consulta desarrollar paquetes), pero el código del canal de plataforma se sigue escribiendo de la misma manera.

Paso 1: Crear un nuevo proyecto de aplicación

#

Comienza creando una nueva aplicación:

  • En una terminal ejecuta: flutter create batterylevel

Por defecto, nuestra plantilla admite escribir código de Android utilizando Kotlin, o código de iOS utilizando Swift. Para utilizar Java o Objective-C, utiliza las banderas -i y/o -a:

  • En una terminal ejecuta: flutter create -i objc -a java batterylevel

Paso 2: Crear el cliente de plataforma de Flutter

#

La clase State de la aplicación mantiene el estado actual de la aplicación. Extiéndela para mantener el estado actual de la batería.

Primero, construye el canal. Utiliza un MethodChannel con un único método de plataforma que devuelva el nivel de la batería.

Los lados del cliente y del host de un canal se conectan a través de un nombre de canal que se pasa en el constructor del canal. Todos los nombres de canal utilizados en una misma aplicación deben ser únicos; añade un prefijo único de dominio al nombre del canal, por ejemplo: samples.flutter.dev/battery.

dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
dart
class _MyHomePageState extends State<MyHomePage> {
  static const platform = MethodChannel('samples.flutter.dev/battery');
  // Get battery level.

A continuación, invoca un método en el canal de métodos, especificando el método concreto a llamar mediante el identificador String getBatteryLevel. La llamada podría fallar (por ejemplo, si la plataforma no admite la API de la plataforma, como al ejecutar en un simulador), así que envuelve la llamada a invokeMethod en una instrucción try-catch.

Utiliza el resultado devuelto para actualizar el estado de la interfaz de usuario en _batteryLevel dentro de setState.

dart
// Get battery level.
String _batteryLevel = 'Unknown battery level.';

Future<void> _getBatteryLevel() async {
  String batteryLevel;
  try {
    final result = await platform.invokeMethod<int>('getBatteryLevel');
    batteryLevel = 'Battery level at $result % .';
  } on PlatformException catch (e) {
    batteryLevel = "Failed to get battery level: '${e.message}'.";
  }

  setState(() {
    _batteryLevel = batteryLevel;
  });
}

Finalmente, reemplaza el método build de la plantilla para que contenga una pequeña interfaz de usuario que muestre el estado de la batería en una cadena de texto, y un botón para actualizar el valor.

dart
@override
Widget build(BuildContext context) {
  return Material(
    child: Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          ElevatedButton(
            onPressed: _getBatteryLevel,
            child: const Text('Get Battery Level'),
          ),
          Text(_batteryLevel),
        ],
      ),
    ),
  );
}

Paso 3: Añadir una implementación específica de la plataforma Android

#

Comienza abriendo la parte del host de Android de tu aplicación de Flutter en Android Studio:

  1. Iniciar Android Studio

  2. Selecciona la opción de menú File > Open...

  3. Navega al directorio que contiene tu aplicación de Flutter y selecciona la carpeta android dentro de él. Haz clic en OK.

  4. Abre el archivo MainActivity.kt ubicado en la carpeta kotlin en la vista Project.

Dentro del método configureFlutterEngine(), crea un MethodChannel y llama a setMethodCallHandler(). Asegúrate de utilizar el mismo nombre de canal que se utilizó en el lado del cliente de Flutter.

MainActivity.kt
kotlin
import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity: FlutterActivity() {
  private val CHANNEL = "samples.flutter.dev/battery"

  override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)
    MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
      call, result ->
      // This method is invoked on the main thread.
      // TODO
    }
  }
}

Añade el código de Android Kotlin que utiliza las API de batería de Android para recuperar el nivel de la batería. Este código es exactamente el mismo que escribirías en una aplicación nativa de Android.

Primero, añade las importaciones necesarias en la parte superior del archivo:

MainActivity.kt
kotlin
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES

A continuación, añade el siguiente método en la clase MainActivity, debajo del método configureFlutterEngine():

MainActivity.kt
kotlin
  private fun getBatteryLevel(): Int {
    val batteryLevel: Int
    if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
      val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
      batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
    } else {
      val intent = ContextWrapper(applicationContext).registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
      batteryLevel = intent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
    }

    return batteryLevel
  }

Finalmente, completa el método setMethodCallHandler() añadido anteriormente. Debes manejar un único método de plataforma, getBatteryLevel(), así que realiza la comprobación correspondiente en el argumento call. La implementación de este método de plataforma llama al código de Android escrito en el paso anterior y devuelve una respuesta tanto para el caso de éxito como para el de error utilizando el argumento result. Si se llama a un método desconocido, notifica eso en su lugar.

Elimina el siguiente código:

MainActivity.kt
kotlin
    MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
      call, result ->
      // This method is invoked on the main thread.
      // TODO
    }

Y reemplázalo con lo siguiente:

MainActivity.kt
kotlin
    MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
      // This method is invoked on the main thread.
      call, result ->
      if (call.method == "getBatteryLevel") {
        val batteryLevel = getBatteryLevel()

        if (batteryLevel != -1) {
          result.success(batteryLevel)
        } else {
          result.error("UNAVAILABLE", "Battery level not available.", null)
        }
      } else {
        result.notImplemented()
      }
    }

Comienza abriendo la parte del host de Android de tu aplicación de Flutter en Android Studio:

  1. Iniciar Android Studio

  2. Selecciona la opción de menú File > Open...

  3. Navega al directorio que contiene tu aplicación de Flutter y selecciona la carpeta android dentro de él. Haz clic en OK.

  4. Abre el archivo MainActivity.java ubicado en la carpeta java en la vista Project.

A continuación, crea un MethodChannel y establece un MethodCallHandler dentro del método configureFlutterEngine(). Asegúrate de utilizar el mismo nombre de canal que se utilizó en el lado del cliente de Flutter.

MainActivity.java
java
import androidx.annotation.NonNull;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugin.common.MethodChannel;

public class MainActivity extends FlutterActivity {
  private static final String CHANNEL = "samples.flutter.dev/battery";

  @Override
  public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
    super.configureFlutterEngine(flutterEngine);
    new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
        .setMethodCallHandler(
          (call, result) -> {
            // This method is invoked on the main thread.
            // TODO
          }
        );
  }
}

Añade el código de Android Java que utiliza las API de batería de Android para recuperar el nivel de la batería. Este código es exactamente el mismo que escribirías en una aplicación nativa de Android.

Primero, añade las importaciones necesarias en la parte superior del archivo:

MainActivity.java
java
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;

Luego añade lo siguiente como un nuevo método en la clase activity, debajo del método configureFlutterEngine():

MainActivity.java
java
  private int getBatteryLevel() {
    int batteryLevel = -1;
    if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
      BatteryManager batteryManager = (BatteryManager) getSystemService(BATTERY_SERVICE);
      batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
    } else {
      Intent intent = new ContextWrapper(getApplicationContext()).
          registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
      batteryLevel = (intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100) /
          intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
    }

    return batteryLevel;
  }

Finalmente, completa el método setMethodCallHandler() añadido anteriormente. Debes manejar un único método de plataforma, getBatteryLevel(), así que realiza la comprobación correspondiente en el argumento call. La implementación de este método de plataforma llama al código de Android escrito en el paso anterior y devuelve una respuesta tanto para el caso de éxito como para el de error utilizando el argumento result. Si se llama a un método desconocido, notifica eso en su lugar.

Elimina el siguiente código:

MainActivity.java
java
      new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
        .setMethodCallHandler(
          (call, result) -> {
            // This method is invoked on the main thread.
            // TODO
          }
      );

Y reemplázalo con lo siguiente:

MainActivity.java
java
      new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
        .setMethodCallHandler(
          (call, result) -> {
            // This method is invoked on the main thread.
            if (call.method.equals("getBatteryLevel")) {
              int batteryLevel = getBatteryLevel();

              if (batteryLevel != -1) {
                result.success(batteryLevel);
              } else {
                result.error("UNAVAILABLE", "Battery level not available.", null);
              }
            } else {
              result.notImplemented();
            }
          }
      );

Ahora deberías poder ejecutar la aplicación en Android. Si utilizas el Emulador de Android, establece el nivel de batería en el panel Extended Controls accesible desde el botón ... en la barra de herramientas.

Paso 4: Añadir una implementación específica de la plataforma iOS

#

Comienza abriendo la parte del host de iOS de tu aplicación de Flutter en Xcode:

  1. Iniciar Xcode.

  2. Selecciona la opción de menú File > Open....

  3. Navega al directorio que contiene tu aplicación de Flutter y selecciona la carpeta ios dentro de él. Haz clic en OK.

Añadir soporte para Swift en la configuración de la plantilla estándar que utiliza Objective-C:

  1. Despliega Runner > Runner en el navegador de proyectos (Project navigator).

  2. Abre el archivo AppDelegate.swift ubicado bajo Runner > Runner en el navegador de proyectos.

Sobrescribe la función application:didFinishLaunchingWithOptions: y crea un FlutterMethodChannel vinculado al nombre de canal samples.flutter.dev/battery:

AppDelegate.swift
swift
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {

  func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
    GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)

    let batteryChannel = FlutterMethodChannel(
      name: "samples.flutter.dev/battery",
      binaryMessenger: engineBridge.applicationRegistrar.messenger()
    )

    batteryChannel.setMethodCallHandler({
      [weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in
      // This method is invoked on the UI thread.
      // Handle battery messages.
    })
  }
}

A continuación, añade el código de iOS Swift que utiliza las API de batería de iOS para recuperar el nivel de la batería. Este código es exactamente el mismo que escribirías en una aplicación nativa de iOS.

Añade lo siguiente como un nuevo método en la parte inferior de AppDelegate.swift:

AppDelegate.swift
swift
private func receiveBatteryLevel(result: FlutterResult) {
  let device = UIDevice.current
  device.isBatteryMonitoringEnabled = true
  if device.batteryState == UIDevice.BatteryState.unknown {
    result(FlutterError(code: "UNAVAILABLE",
                        message: "Battery level not available.",
                        details: nil))
  } else {
    result(Int(device.batteryLevel * 100))
  }
}

Finalmente, completa el método setMethodCallHandler() añadido anteriormente. Debes manejar un único método de plataforma, getBatteryLevel(), así que realiza la comprobación correspondiente en el argumento call. La implementación de este método de plataforma llama al código de iOS escrito en el paso anterior. Si se llama a un método desconocido, reporta eso en su lugar.

AppDelegate.swift
swift
batteryChannel.setMethodCallHandler({
  [weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in
  // This method is invoked on the UI thread.
  guard call.method == "getBatteryLevel" else {
    result(FlutterMethodNotImplemented)
    return
  }
  self?.receiveBatteryLevel(result: result)
})

Comienza abriendo la parte del host de iOS de la aplicación de Flutter en Xcode:

  1. Iniciar Xcode.

  2. Selecciona la opción de menú File > Open....

  3. Navega al directorio que contiene tu aplicación de Flutter y selecciona la carpeta ios dentro de él. Haz clic en OK.

  4. Asegúrate de que el proyecto de Xcode compile sin errores.

  5. Abre el archivo AppDelegate.m, ubicado bajo Runner > Runner en el navegador de proyectos.

Crea un FlutterMethodChannel y añade un manejador dentro del método application didFinishLaunchingWithOptions:. Asegúrate de utilizar el mismo nombre de canal que se utilizó en el lado del cliente de Flutter.

AppDelegate.m
objc
#import <Flutter/Flutter.h>
#import "GeneratedPluginRegistrant.h"

@implementation AppDelegate

- (void)didInitializeImplicitFlutterEngine:(NSObject<FlutterImplicitEngineBridge>*)engineBridge {
  [GeneratedPluginRegistrant registerWithRegistry:engineBridge.pluginRegistry];

  FlutterMethodChannel* batteryChannel = [FlutterMethodChannel
                                        methodChannelWithName:@"samples.flutter.dev/battery"
                                        binaryMessenger:engineBridge.applicationRegistrar.messenger];

  [batteryChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
    // This method is invoked on the UI thread.
    // TODO
  }];
}
@end

A continuación, añade el código de iOS Objective-C que utiliza las API de batería de iOS para recuperar el nivel de la batería. Este código es exactamente el mismo que escribirías en una aplicación nativa de iOS.

Añade el siguiente método en la clase AppDelegate, justo antes de @end:

AppDelegate.m
objc
- (int)getBatteryLevel {
  UIDevice* device = UIDevice.currentDevice;
  device.batteryMonitoringEnabled = YES;
  if (device.batteryState == UIDeviceBatteryStateUnknown) {
    return -1;
  } else {
    return (int)(device.batteryLevel * 100);
  }
}

Finalmente, completa el método setMethodCallHandler() añadido anteriormente. Debes manejar un único método de plataforma, getBatteryLevel(), así que realiza la comprobación correspondiente en el argumento call. La implementación de este método de plataforma llama al código de iOS escrito en el paso anterior y devuelve una respuesta tanto para el caso de éxito como para el de error utilizando el argumento result. Si se llama a un método desconocido, reporta eso en su lugar.

AppDelegate.m
objc
__weak typeof(self) weakSelf = self;
[batteryChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
  // This method is invoked on the UI thread.
  if ([@"getBatteryLevel" isEqualToString:call.method]) {
    int batteryLevel = [weakSelf getBatteryLevel];

    if (batteryLevel == -1) {
      result([FlutterError errorWithCode:@"UNAVAILABLE"
                                 message:@"Battery level not available."
                                 details:nil]);
    } else {
      result(@(batteryLevel));
    }
  } else {
    result(FlutterMethodNotImplemented);
  }
}];

Ahora deberías poder ejecutar la aplicación en iOS. Si utilizas el Simulador de iOS, ten en cuenta que este no admite las API de batería, y la aplicación mostrará 'Battery level not available'.

Paso 5: Añadir una implementación específica de la plataforma Windows

#

Comienza abriendo la parte del host de Windows de tu aplicación de Flutter en Visual Studio:

  1. Ejecuta flutter build windows en el directorio de tu proyecto una vez para generar el archivo de solución de Visual Studio.

  2. Iniciar Visual Studio.

  3. Selecciona Open a project or solution.

  4. Navega al directorio que contiene tu aplicación de Flutter, luego a la carpeta build, luego a la carpeta windows, y finalmente selecciona el archivo batterylevel.sln. Haz clic en Open.

Añadir la implementación en C++ del método del canal de plataforma:

  1. Despliega batterylevel > Source Files en el Solution Explorer.

  2. Abre el archivo flutter_window.cpp.

Primero, añade los includes necesarios en la parte superior del archivo, justo después de #include "flutter_window.h":

flutter_window.cpp
cpp
#include <flutter/event_channel.h>
#include <flutter/event_sink.h>
#include <flutter/event_stream_handler_functions.h>
#include <flutter/method_channel.h>
#include <flutter/standard_method_codec.h>
#include <windows.h>

#include <memory>

Edita el método FlutterWindow::OnCreate y crea un flutter::MethodChannel vinculado al nombre de canal samples.flutter.dev/battery:

flutter_window.cpp
cpp
bool FlutterWindow::OnCreate() {
  // ...
  RegisterPlugins(flutter_controller_->engine());

  flutter::MethodChannel<> channel(
      flutter_controller_->engine()->messenger(), "samples.flutter.dev/battery",
      &flutter::StandardMethodCodec::GetInstance());
  channel.SetMethodCallHandler(
      [](const flutter::MethodCall<>& call,
         std::unique_ptr<flutter::MethodResult<>> result) {
        // TODO
      });

  SetChildContent(flutter_controller_->view()->GetNativeWindow());
  return true;
}

A continuación, añade el código C++ que utiliza las API de batería de Windows para recuperar el nivel de la batería. Este código es exactamente el mismo que escribirías en una aplicación nativa de Windows.

Añade lo siguiente como una nueva función en la parte superior de flutter_window.cpp justo después de la sección #include:

flutter_window.cpp
cpp
static int GetBatteryLevel() {
  SYSTEM_POWER_STATUS status;
  if (GetSystemPowerStatus(&status) == 0 || status.BatteryLifePercent == 255) {
    return -1;
  }
  return status.BatteryLifePercent;
}

Finalmente, completa el método setMethodCallHandler() añadido anteriormente. Debes manejar un único método de plataforma, getBatteryLevel(), así que realiza la comprobación correspondiente en el argumento call. La implementación de este método de plataforma llama al código de Windows escrito en el paso anterior. Si se llama a un método desconocido, notifica eso en su lugar.

Elimina el siguiente código:

flutter_window.cpp
cpp
  channel.SetMethodCallHandler(
      [](const flutter::MethodCall<>& call,
         std::unique_ptr<flutter::MethodResult<>> result) {
        // TODO
      });

Y reemplázalo con lo siguiente:

flutter_window.cpp
cpp
  channel.SetMethodCallHandler(
      [](const flutter::MethodCall<>& call,
         std::unique_ptr<flutter::MethodResult<>> result) {
        if (call.method_name() == "getBatteryLevel") {
          int battery_level = GetBatteryLevel();
          if (battery_level != -1) {
            result->Success(battery_level);
          } else {
            result->Error("UNAVAILABLE", "Battery level not available.");
          }
        } else {
          result->NotImplemented();
        }
      });

Ahora deberías poder ejecutar la aplicación en Windows. Si tu dispositivo no tiene batería, mostrará 'Battery level not available'.

Paso 6: Añadir una implementación específica de la plataforma macOS

#

Comienza abriendo la parte del host de macOS de tu aplicación de Flutter en Xcode:

  1. Iniciar Xcode.

  2. Selecciona la opción de menú File > Open....

  3. Navega al directorio que contiene tu aplicación de Flutter y selecciona la carpeta macos dentro de él. Haz clic en OK.

Añadir la implementación en Swift del método del canal de plataforma:

  1. Despliega Runner > Runner en el navegador de proyectos (Project navigator).

  2. Abre el archivo MainFlutterWindow.swift ubicado bajo Runner > Runner en el navegador de proyectos.

Primero, añade la importación necesaria en la parte superior del archivo, justo después de import FlutterMacOS:

MainFlutterWindow.swift
swift
import IOKit.ps

Crea un FlutterMethodChannel vinculado al nombre de canal samples.flutter.dev/battery en el método awakeFromNib:

MainFlutterWindow.swift
swift
  override func awakeFromNib() {
    // ...
    self.setFrame(windowFrame, display: true)

    let batteryChannel = FlutterMethodChannel(
      name: "samples.flutter.dev/battery",
      binaryMessenger: flutterViewController.engine.binaryMessenger)
    batteryChannel.setMethodCallHandler { (call, result) in
      // This method is invoked on the UI thread.
      // Handle battery messages.
    }

    RegisterGeneratedPlugins(registry: flutterViewController)

    super.awakeFromNib()
  }
}

A continuación, añade el código de macOS Swift que utiliza las API de batería de IOKit para recuperar el nivel de la batería. Este código es exactamente el mismo que escribirías en una aplicación nativa de macOS.

Añade lo siguiente como un nuevo método en la parte inferior de MainFlutterWindow.swift:

MainFlutterWindow.swift
swift
private func getBatteryLevel() -> Int? {
  let info = IOPSCopyPowerSourcesInfo().takeRetainedValue()
  let sources: Array<CFTypeRef> = IOPSCopyPowerSourcesList(info).takeRetainedValue() as Array
  if let source = sources.first {
    let description =
      IOPSGetPowerSourceDescription(info, source).takeUnretainedValue() as! [String: AnyObject]
    if let level = description[kIOPSCurrentCapacityKey] as? Int {
      return level
    }
  }
  return nil
}

Finalmente, completa el método setMethodCallHandler añadido anteriormente. Debes manejar un único método de plataforma, getBatteryLevel(), así que realiza la comprobación correspondiente en el argumento call. La implementación de este método de plataforma llama al código de macOS escrito en el paso anterior. Si se llama a un método desconocido, notifica eso en su lugar.

MainFlutterWindow.swift
swift
batteryChannel.setMethodCallHandler { (call, result) in
  switch call.method {
  case "getBatteryLevel":
    guard let level = getBatteryLevel() else {
      result(
        FlutterError(
          code: "UNAVAILABLE",
          message: "Battery level not available",
          details: nil))
     return
    }
    result(level)
  default:
    result(FlutterMethodNotImplemented)
  }
}

Ahora deberías poder ejecutar la aplicación en macOS. Si tu dispositivo no tiene batería, mostrará 'Battery level not available'.

Paso 7: Añadir una implementación específica de la plataforma Linux

#

Para este ejemplo, necesitas instalar las cabeceras de desarrollo de upower. Es probable que estén disponibles en tu distribución, por ejemplo con:

sudo apt install libupower-glib-dev

Comienza abriendo la parte del host de Linux de tu aplicación de Flutter en el editor de tu elección. Las instrucciones a continuación son para Visual Studio Code con las extensiones "C/C++" y "CMake" instaladas, pero se pueden adaptar para otros IDE.

  1. Inicia Visual Studio Code.

  2. Abre el directorio linux dentro de tu proyecto.

  3. Elige Yes en el mensaje que pregunta: Would you like to configure project "linux"?. Esto habilita el autocompletado de C++.

  4. Abre el archivo runner/my_application.cc.

Primero, añade los includes necesarios en la parte superior del archivo, justo después de #include <flutter_linux/flutter_linux.h>:

runner/my_application.cc
c
#include <math.h>
#include <upower.h>

Añade un FlMethodChannel a la estructura _MyApplication:

runnner/my_application.cc
c
struct _MyApplication {
  GtkApplication parent_instance;
  char** dart_entrypoint_arguments;
  FlMethodChannel* battery_channel;
};

Asegúrate de limpiarlo en my_application_dispose:

runner/my_application.cc
c
static void my_application_dispose(GObject* object) {
  MyApplication* self = MY_APPLICATION(object);
  g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
  g_clear_object(&self->battery_channel);
  G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
}

Edita el método my_application_activate e inicializa battery_channel utilizando el nombre de canal samples.flutter.dev/battery, justo después de la llamada a fl_register_plugins:

runner/my_application.cc
c
static void my_application_activate(GApplication* application) {
  // ...
  fl_register_plugins(FL_PLUGIN_REGISTRY(self->view));

  g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
  self->battery_channel = fl_method_channel_new(
      fl_engine_get_binary_messenger(fl_view_get_engine(view)),
      "samples.flutter.dev/battery", FL_METHOD_CODEC(codec));
  fl_method_channel_set_method_call_handler(
      self->battery_channel, battery_method_call_handler, self, nullptr);

  gtk_widget_grab_focus(GTK_WIDGET(self->view));
}

A continuación, añade el código C que utiliza las API de batería de Linux para recuperar el nivel de la batería. Este código es exactamente el mismo que escribirías en una aplicación nativa de Linux.

Añade lo siguiente como una nueva función en la parte superior de my_application.cc justo después de la línea G_DEFINE_TYPE:

runner/my_application.cc
c
static FlMethodResponse* get_battery_level() {
  // Find the first available battery and report that.
  g_autoptr(UpClient) up_client = up_client_new();
  g_autoptr(GPtrArray) devices = up_client_get_devices2(up_client);
  if (devices->len == 0) {
    return FL_METHOD_RESPONSE(fl_method_error_response_new(
        "UNAVAILABLE", "Device does not have a battery.", nullptr));
  }

  UpDevice* device = UP_DEVICE(g_ptr_array_index(devices, 0));
  double percentage = 0;
  g_object_get(device, "percentage", &percentage, nullptr);

  g_autoptr(FlValue) result =
      fl_value_new_int(static_cast<int64_t>(round(percentage)));
  return FL_METHOD_RESPONSE(fl_method_success_response_new(result));
}

Finalmente, añade la función battery_method_call_handler referenciada en la llamada anterior a fl_method_channel_set_method_call_handler. Debes manejar un único método de plataforma, getBatteryLevel, así que realiza la comprobación correspondiente en el argumento method_call. La implementación de esta función llama al código de Linux escrito en el paso anterior. Si se llama a un método desconocido, notifica eso en su lugar.

Añade el siguiente código después de la función get_battery_level:

runner/my_application.cpp
cpp
static void battery_method_call_handler(FlMethodChannel* channel,
                                        FlMethodCall* method_call,
                                        gpointer user_data) {
  g_autoptr(FlMethodResponse) response = nullptr;
  if (strcmp(fl_method_call_get_name(method_call), "getBatteryLevel") == 0) {
    response = get_battery_level();
  } else {
    response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new());
  }

  g_autoptr(GError) error = nullptr;
  if (!fl_method_call_respond(method_call, response, &error)) {
    g_warning("Failed to send response: %s", error->message);
  }
}

Ahora deberías poder ejecutar la aplicación en Linux. Si tu dispositivo no tiene batería, mostrará 'Battery level not available'.

Llamar a código específico de la plataforma usando el paquete Pigeon

#

Puedes utilizar el paquete Pigeon como una alternativa a las API de canales de plataforma de Flutter para generar código que envíe mensajes de manera estructurada y con tipado seguro. El flujo de trabajo de Pigeon es el siguiente:

  • La aplicación de Flutter envía mensajes estructurados con tipado seguro a su host, la parte de la aplicación que no es Dart, a través de un canal de plataforma.

  • El host escucha en el canal de plataforma y recibe el mensaje. Luego realiza llamadas a cualquier número de API específicas de la plataforma utilizando el lenguaje de programación nativo y devuelve una respuesta al cliente, la parte de Flutter de la aplicación.

El uso de este paquete elimina la necesidad de hacer coincidir cadenas de texto entre el host y el cliente para los nombres y tipos de datos de los mensajes. Admite clases anidadas, agrupación de mensajes en API, generación de código contenedor (wrapper) asíncrono y envío de mensajes en cualquier dirección. El código generado es legible y garantiza que no haya conflictos entre múltiples clientes de diferentes versiones.

Con Pigeon, el protocolo de mensajería se define en un subconjunto de Dart que luego genera código de mensajería para Android, iOS, macOS o Windows. Por ejemplo:

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

class SearchRequest {
  final String query;

  SearchRequest({required this.query});
}

class SearchReply {
  final String result;

  SearchReply({required this.result});
}

@HostApi()
abstract class Api {
  @async
  SearchReply search(SearchRequest request);
}
use_pigeon.dart
dart
import 'generated_pigeon.dart';

Future<void> onClick() async {
  SearchRequest request = SearchRequest(query: 'test');
  Api api = SomeApi();
  SearchReply reply = await api.search(request);
  print('reply: ${reply.result}');
}

Puedes encontrar un ejemplo completo y más información en la página de pigeon en pub.dev.

Canales y subprocesamiento de la plataforma

#

Al invocar canales en el lado de la plataforma destinados a Flutter, invócalos en el hilo principal de la plataforma. Al invocar canales en Flutter destinados al lado de la plataforma, invócalos desde cualquier Isolate que sea el Isolate raíz, o que esté registrado como un Isolate en segundo plano. Los manejadores para el lado de la plataforma pueden ejecutarse en el hilo principal de la plataforma o pueden ejecutarse en un hilo en segundo plano si se utiliza una Task Queue. Puedes invocar los manejadores del lado de la plataforma de forma asíncrona y en cualquier hilo.

Usar plugins y canales desde un isolate en segundo plano

#

Los plugins y los canales pueden ser utilizados por cualquier Isolate, pero ese Isolate tiene que ser un Isolate raíz (el creado por Flutter) o estar registrado como un Isolate en segundo plano para un Isolate raíz.

El siguiente ejemplo muestra cómo registrar un Isolate en segundo plano para utilizar un plugin desde un Isolate en segundo plano.

dart
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';

void _isolateMain(RootIsolateToken rootIsolateToken) async {
  BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken);
  SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
  print(sharedPreferences.getBool('isDebug'));
}

void main() {
  RootIsolateToken rootIsolateToken = RootIsolateToken.instance!;
  Isolate.spawn(_isolateMain, rootIsolateToken);
}

Ejecutar manejadores de canales en un hilo en segundo plano (Android)

#

Para que el manejador del lado de la plataforma de un canal se ejecute en un hilo en segundo plano en una aplicación de Android, debes utilizar la API Task Queue.

kotlin
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
  val taskQueue =
      flutterPluginBinding.binaryMessenger.makeBackgroundTaskQueue()
  channel = MethodChannel(flutterPluginBinding.binaryMessenger,
                          "com.example.foo",
                          StandardMethodCodec.INSTANCE,
                          taskQueue)
  channel.setMethodCallHandler(this)
}
java
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
  BinaryMessenger messenger = binding.getBinaryMessenger();
  BinaryMessenger.TaskQueue taskQueue =
      messenger.makeBackgroundTaskQueue();
  channel =
      new MethodChannel(
          messenger,
          "com.example.foo",
          StandardMethodCodec.INSTANCE,
          taskQueue);
  channel.setMethodCallHandler(this);
}

Ejecutar manejadores de canales en un hilo en segundo plano (iOS)

#

Para que el manejador del lado de la plataforma de un canal se ejecute en un hilo en segundo plano en una aplicación de iOS, debes utilizar la API Task Queue.

swift
public static func register(with registrar: FlutterPluginRegistrar) {
  let taskQueue = registrar.messenger().makeBackgroundTaskQueue?()
  let channel = FlutterMethodChannel(name: "com.example.foo",
                                     binaryMessenger: registrar.messenger(),
                                     codec: FlutterStandardMethodCodec.sharedInstance(),
                                     taskQueue: taskQueue)
  let instance = MyPlugin()
  registrar.addMethodCallDelegate(instance, channel: channel)
}
objc
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
  NSObject<FlutterTaskQueue>* taskQueue =
      [[registrar messenger] makeBackgroundTaskQueue];
  FlutterMethodChannel* channel =
      [FlutterMethodChannel methodChannelWithName:@"com.example.foo"
                                  binaryMessenger:[registrar messenger]
                                            codec:[FlutterStandardMethodCodec sharedInstance]
                                        taskQueue:taskQueue];
  MyPlugin* instance = [[MyPlugin alloc] init];
  [registrar addMethodCallDelegate:instance channel:channel];
}

Saltar al hilo de interfaz de usuario (Android)

#

Para cumplir con el requisito de hilo de interfaz de usuario de los canales, es posible que debas pasar de un hilo en segundo plano al hilo de interfaz de usuario de Android para ejecutar un método de canal. En Android, puedes lograr esto enviando mediante post() un Runnable al Looper del hilo de interfaz de usuario de Android, lo que hace que el Runnable se ejecute en el hilo principal en la siguiente oportunidad.

kotlin
Handler(Looper.getMainLooper()).post {
  // Call the desired channel message here.
}
java
new Handler(Looper.getMainLooper()).post(new Runnable() {
  @Override
  public void run() {
    // Call the desired channel message here.
  }
});

Saltar al hilo principal (iOS)

#

Para cumplir con el requisito de hilo principal del canal, es posible que debas pasar de un hilo en segundo plano al hilo principal de iOS para ejecutar un método de canal. Puedes lograr esto en iOS ejecutando un bloque en la cola de despacho principal (main dispatch queue):

objc
dispatch_async(dispatch_get_main_queue(), ^{
  // Call the desired channel message here.
});
swift
DispatchQueue.main.async {
  // Call the desired channel message here.
}

Información complementaria

#

Canales y códecs comunes

#

La siguiente es una lista de algunas API de canales de plataforma comunes que puedes utilizar para escribir código específico de la plataforma:

  • MethodChannel para Flutter: Un canal con nombre que puedes utilizar para comunicarte con plugins de la plataforma utilizando llamadas a métodos asíncronos. Por defecto, este canal utiliza el códec StandardMessageCodec. Este canal no tiene tipado seguro, lo que significa que llamar y recibir mensajes depende de que el host y el cliente declaren los mismos argumentos y tipos de datos para que los mensajes funcionen.

  • BasicMessageChannel para Flutter: Un canal con nombre que admite el paso de mensajes básicos y asíncronos, utilizando un códec de mensajes compatible. No tiene tipado seguro.

  • API de Engine Embedder para plataformas: Estas API específicas de la plataforma contienen API de canales específicas de la plataforma.

Puedes crear tu propio códec o utilizar uno existente. La siguiente es una lista de algunos códecs existentes que puedes utilizar con código específico de la plataforma:

  • StandardMessageCodec: Un códec de mensajes de uso común que codifica y decodifica una amplia gama de tipos de datos en un formato binario independiente de la plataforma para su transmisión a través de canales de plataforma. La serialización y deserialización de valores hacia y desde los mensajes ocurre automáticamente cuando envías y recibes valores. Para obtener una lista de los tipos de datos admitidos, consulta Soporte de tipos de datos de canales de plataforma.

  • BinaryCodec: Un códec de mensajes que transmite datos binarios en bruto entre el lado de Dart de tu aplicación de Flutter y el lado de la plataforma nativa. No realiza ninguna codificación o decodificación de estructuras de datos de nivel superior.

  • StringCodec: Un códec de mensajes que codifica y decodifica cadenas de texto, utilizando codificación UTF-8.

  • JSONMessageCodec: Un códec de mensajes que codifica y decodifica datos con formato JSON, utilizando codificación UTF-8.

  • FirestoreMessageCodec: Un códec de mensajes que maneja el intercambio de mensajes enviados a través del canal de plataforma entre tu aplicación de Flutter y los SDK nativos de Firebase Firestore (en Android e iOS).

Separar el código específico de la plataforma del código de la interfaz de usuario

#

Si esperas utilizar tu código específico de la plataforma en múltiples aplicaciones de Flutter, podrías considerar separar el código en un plugin de plataforma ubicado en un directorio fuera de tu aplicación principal. Consulta desarrollar paquetes para obtener más detalles.

Publicar código específico de la plataforma como un paquete

#

Para compartir tu código específico de la plataforma con otros desarrolladores en el ecosistema de Flutter, consulta publicar paquetes.