Flutter puede hacer la mayoría de las cosas sin salir de Dart, pero hay momentos en que necesitas llamar a código nativo: usar una librería C de terceros, acceder a una API del sistema operativo que Flutter no expone directamente, o reutilizar código nativo existente que no tiene wrapper en pub.dev. Para eso existen Dart FFI y los platform channels, y cada uno sirve para un caso diferente.
Dart FFI: llamar a código C directamente
Dart FFI (Foreign Function Interface) permite llamar a funciones escritas en C (o en cualquier lenguaje que exponga una ABI de C) directamente desde Dart, sin pasar por los platform channels. Es la opción adecuada cuando necesitas integrar una librería C nativa o maximizar el rendimiento de operaciones que sería muy lento hacer en Dart puro.
// Ejemplo: llamar a una función C simple
// C code (lib_native.c):
// int add(int a, int b) { return a + b; }
// Compilada como lib_native.so / lib_native.dylib
import 'dart:ffi';
import 'dart:io';
typedef AddC = Int32 Function(Int32 a, Int32 b);
typedef AddDart = int Function(int a, int b);
void main() {
final lib = DynamicLibrary.open(
Platform.isAndroid ? 'lib_native.so' : 'lib_native.dylib',
);
final add = lib.lookupFunction<AddC, AddDart>('add');
print(add(3, 4)); // 7
}
Dart FFI es síncrono y corre en el mismo hilo que Dart. Esto significa que una función C que tarda mucho bloqueará el hilo de UI igual que cualquier otra operación síncrona. Para operaciones largas, conviene llamar la función FFI desde un Isolate.
Gestión de memoria con FFI
Cuando trabajas con FFI, tienes que gestionar la memoria manualmente para las estructuras que cruzan la frontera Dart-C:
import 'dart:ffi';
import 'package:ffi/ffi.dart'; // paquete ffi del pub.dev
void useNativeString() {
// Allocar memoria para una cadena C
final nativeStr = 'hola'.toNativeUtf8();
try {
// Usar nativeStr con alguna función C...
print(nativeStr.toDartString());
} finally {
malloc.free(nativeStr); // liberar siempre
}
}
// Con using() del paquete ffi (libera automáticamente)
void useNativeStringAuto() {
using((arena) {
final nativeStr = 'hola'.toNativeUtf8(allocator: arena);
// al salir del bloque, arena libera todo
});
}
Platform channels: hablar con Swift y Kotlin
Los platform channels son el mecanismo de Flutter para comunicarse con el código nativo de la plataforma: Swift/Objective-C en iOS y Kotlin/Java en Android. A diferencia de FFI, los platform channels son asíncronos y pasan por una serialización de mensajes.
Hay tres tipos de channels:
- MethodChannel: para llamadas bidireccionales puntuales. El más habitual.
- EventChannel: para streams de eventos desde nativo a Dart (sensores, cambios de conectividad).
- BasicMessageChannel: para mensajes continuos en ambas direcciones con codec personalizable.
// Lado Dart
import 'package:flutter/services.dart';
class BatteryInfo {
static const _channel = MethodChannel('com.ejemplo.app/battery');
static Future<int> getBatteryLevel() async {
try {
final int level = await _channel.invokeMethod('getBatteryLevel');
return level;
} on PlatformException catch (e) {
throw Exception('No se pudo obtener el nivel de batería: ${e.message}');
}
}
}
// Lado Android (Kotlin) MainActivity.kt
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import android.os.BatteryManager
import android.content.Intent
import android.content.IntentFilter
class MainActivity: FlutterActivity() {
private val CHANNEL = "com.ejemplo.app/battery"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
call, result ->
if (call.method == "getBatteryLevel") {
val batteryLevel = getBatteryLevel()
if (batteryLevel != -1) {
result.success(batteryLevel)
} else {
result.error("UNAVAILABLE", "No se puede obtener el nivel de batería", null)
}
} else {
result.notImplemented()
}
}
}
private fun getBatteryLevel(): Int {
val batteryIntent = registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
val level = batteryIntent?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
val scale = batteryIntent?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1
return if (level == -1 || scale == -1) -1 else (level * 100 / scale)
}
}
EventChannel para streams de datos nativos
// Dart escuchar un stream de eventos nativos
const _accelerometerChannel = EventChannel('com.ejemplo.app/accelerometer');
Stream<List<double>> get accelerometerStream {
return _accelerometerChannel
.receiveBroadcastStream()
.map((event) => List<double>.from(event as List));
}
// Usar en un widget
StreamBuilder<List<double>>(
stream: accelerometerStream,
builder: (context, snapshot) {
if (!snapshot.hasData) return const CircularProgressIndicator();
final values = snapshot.data!;
return Text('X: ${values[0].toStringAsFixed(2)}');
},
)
FFI vs Platform Channels: cuándo usar cada uno
La elección depende de qué código nativo necesitas usar. Si tienes una librería C (OpenCV, SQLite, un codec de audio, una librería de criptografía), FFI es la opción correcta: es más directo y eficiente. Si necesitas usar APIs de iOS o Android que requieren código Swift/Kotlin (notificaciones push avanzadas, Bluetooth con el stack del SO, autenticación biométrica), los platform channels son el camino.
Muchos plugins de pub.dev usan platform channels internamente, así que en la mayoría de los casos no necesitas escribir nada nativo directamente. Pero cuando el plugin que necesitas no existe o no cubre tu caso exacto, estos son los mecanismos que tienes.
Si el rendimiento de tu app ya va bien a nivel de renderizado pero la comunicación con código nativo es un cuello de botella, conviene revisar también el artículo sobre optimización del rendimiento con Flutter DevTools.
Imagen: Pexels / Pixabay
