¡Chrome cumple 10 años! Gracias por hacer que la comunidad de desarrollo web sea tan abierta, colaborativa y solidaria. DevTools se inspiró en una cantidad innumerable de otros proyectos. A continuación, te mostraremos una retrospectiva de cómo se originó DevTools, y cómo ha cambiado con el paso de los años.
En el principio, estaba Firebug
Imagina por un momento que los navegadores no incorporaban herramientas para desarrolladores. ¿Cómo se depuraba JavaScript? Básicamente, había 3 opciones:
Distribuir llamadas window.alert() en todo tu código
Comentar secciones de código
Observar el código durante un largo tiempo hasta que los dioses de JavaScript te bendijeran con una solución
¿Qué ocurría con los problemas de diseño? ¿Y los errores en la red? Una vez más, la única posibilidad era hacer experimentos cuidadosos en tu código. Esta era la realidad del desarrollo web hasta 2006. Hasta que apareció una pequeña herramienta llamada Firebug que cambió todo.
Firebug fue una extensión de Firefox que permitía depurar, editar y controlar páginas en tiempo real. Como desarrollador web, pasaste de repente de no poder visualizar tus páginas a disponer de las características principales de las herramientas modernas para desarrolladores. La capacidad de comprender exactamente por qué Firefox se comportaba como lo hacía desencadenó un desborde de creatividad en la Web. Sin Firebug, la era de la Web 2.0 no hubiera sido posible.
Web Inspector de WebKit
Alrededor de la misma época en la que se lanzó Firebug, algunos ingenieros de Google comenzaron a trabajar en un proyecto que eventualmente conduciría a Chrome. Desde el principio, Chrome fue una mashup de diferentes bibliotecas de código. Para la representación, los ingenieros de Chrome optaron por Webkit, que es el proyecto de código abierto en el que hasta hoy se basa Safari. Un beneficio adicional de usar WebKit fue que incluía una herramienta práctica llamada Web Inspector.
Al igual que el panel Net de Firebug, el Web Inspector original quizá te parezca familiar. Gran parte de su funcionalidad persiste en la actualidad, como el panel Elements en Chrome DevTools. Web Inspector se lanzó unos días después de Firebug y Safari fue el primer navegador que incluyó herramientas para desarrolladores directamente en el navegador.
La era de “Inspect Element”
Chrome aportó muchas ideas innovadoras al ecosistema de los navegadores, como el cuadro multifunción que combinaba búsqueda con la barra de direcciones, y una arquitectura multiprocesos que evitaba que una pestaña bloqueada afectara a todo el navegador. Pero la innovación que más nos gusta fue el aprovisionamiento de herramientas para desarrolladores en todas las compilaciones para todos los usuarios, que quedaban a la vista haciendo clic con el mouse.
"Inspect Element" en 2010
Antes de Chrome, las herramientas para desarrolladores eran una experiencia opcional. Se instalaba una extensión, como Firebug, o se habilitaban algunos indicadores, como aún ocurre en Safari. Chrome fue el primer navegador en facilitar herramientas para desarrolladores desde cualquier instancia del navegador. Nos gustaría alegar que fuimos visionarios para crear un navegador accesible para el desarrollador desde el principio, pero la realidad es que Chrome presentaba muchos problemas de compatibilidad por aquellos días (lo que tiene sentido, ya que nadie realizaba compilaciones asociadas con él) y necesitábamos brindar a los desarrolladores web una alternativa sencilla para solucionar esos problemas. Los desarrolladores web nos informaron que era una característica útil, por lo que la conservamos.
La era móvil
Durante los primeros años del proyecto DevTools, en esencia agregamos capítulos a las historias que iniciaron Firebug y Web Inspector. El siguiente gran paso en nuestro enfoque de DevTools ocurrió cuando quedó claro que los smartphones habían llegado para quedarse.
Nuestra primera misión en este mundo nuevo y audaz fue permitir a los desarrolladores depurar dispositivos móviles reales desde sus máquinas de desarrollo, proceso que denominamos “depuración remota”. DevTools estaba bien posicionado para controlar la depuración remota, gracias a otra consecuencia de la arquitectura multiprocesos de Chrome. En la primera etapa del proyecto DevTools, nos dimos cuenta de que un depurador solo podía acceder de forma confiable a un navegador multiprocesos a través de un protocolo cliente-servidor, en el que el navegador fuera el servidor y el depurador el cliente. Cuando surgió Chrome para dispositivos móviles, el protocolo ya estaba integrado, por lo que solo debimos hacer que el DevTools que se ejecutaba en tu máquina de desarrollo se comunicara con el Chrome que se ejecutaba en tu dispositivo móvil a través del protocolo. Ese protocolo aún forma la columna vertebral de DevTools en la actualidad y se conoce como Protocolo de Chrome DevTools.
Depuración remota
La depuración remota fue un paso en la dirección correcta, y hasta hoy es la herramienta principal para asegurarte de que tus sitios se comporten de forma razonable en dispositivos móviles reales. Sin embargo, con el tiempo, nos dimos cuenta de que la depuración remota podía ser algo tediosa. Cuando te encuentras en las primeras fases de la creación de un sitio o una característica, generalmente solo necesitas una aproximación de primer orden a la experiencia móvil. Esto nos llevó a crear un conjunto de características de simulación móvil, como las siguientes:
Emulación precisa de la vista del puerto móvil, simulación de entradas táctiles y orientación del dispositivo
Restricción de la conexión de red para simular 3G y CPU para simular hardware móvil menos potente
Falsificación de identidad de usuario-agente, ubicación geográfica, datos de acelerómetro y más.
Nos referimos a estas características de forma conjunta como Device Mode.
Un prototipo inicial de Device Mode
Device Mode en 2018
La era del rendimiento
Mientras se desplegaba la era móvil, las grandes apps como Gmail trascendían los límites conocidos de las capacidades de la Web. Los errores del nivel de Gmail requerían herramientas para el nivel de Gmail. Una de nuestras principales contribuciones al ecosistema de herramientas fue mostrar un desglose paso a paso de exactamente todo lo que Chrome debía hacer para mostrar una página.
Estas herramientas fueron un paso en la dirección correcta, pero para poder detectar oportunidades de optimización era necesario aprender los pequeños detalles relacionados con el funcionamiento de los navegadores y buscar entre una gran cantidad de datos. Últimamente, hemos realizado compilaciones sobre esta base para proporcionar más estadísticas de rendimiento guiadas. El nuevo motor Lighthouse alimenta el panel Audits y también está disponible como módulo Node para integración con sistemas de IC.
Sugerencias de rendimiento en el panel Audits
La era Node.js
Hasta aproximadamente 2014, concebíamos a DevTools ante todo como una herramienta para compilar experiencias excelentes en Chrome. El surgimiento de Node nos llevó a repensar nuestro rol en el ecosistema web. Durante los primeros años de Node, los desarrolladores de este entorno se encontraron en una situación similar a la de los desarrolladores web antes de Firebug, o a la de los desarrolladores de Gmail antes del panel Timeline; la escala de las apps para Node superó la escala de las herramientas para Node. Debido a que Node se ejecuta en el motor JavaScript de Chrome, V8, DevTools era un candidato natural para cubrir la brecha. En 2016 llegó la asistencia para depurar Node con DevTools, lo que incluye las características habituales de DevTools; entre otras, puntos de interrupción, procesamiento de código paso a paso, cajanegrismo y mapas de origen para código transpilado.
Administrador de conexiones de Node
El ecosistema del protocolo DevTools
El nombre Protocolo de Chrome DevTools (CDP) sugiere una API que solo DevTools puede usar. La realidad es más general que eso: es la API que permite acceder a Chrome mediante programación. Durante los últimos años, hemos visto que algunas bibliotecas y aplicaciones de terceros se unieron al ecosistema del protocolo:
Puppeteer lo lleva al siguiente nivel de abstracción y permite la automatización del siempre vigente e independiente navegador Chrome con la JavaScript API moderna.
Lighthouse automatiza el proceso de búsqueda alternativas para mejorar el rendimiento y la calidad de las páginas.
Nos entusiasma ver que miles de proyectos dependen de estos paquetes para permitir una interacción completa con Chrome. Si estás en el negocio de las herramientas o la automatización, te recomendamos revisar el protocolo para ver si ofrece alguna oportunidad para tu dominio. Por ejemplo, los equipos de VS Code y WebStorm lo usan para permitir la depuración de JavaScript en sus IDE respectivos.
Lo que viene
Nuestra misión principal es compilar herramientas que te ayuden a crear experiencias geniales en la Web. Dependemos en gran medida de tus comentarios para determinar los productos o las características que debemos desarrollar.
Mantente informado sobre las nuevas características con nuestras entradas de Lo que viene
Publicado por Rich Hyndman, líder de Tecnología global, Google Launchpad Launchpad Studio es un programa de aceleración para las principales startups del mundo. Los fundadores trabajan en estrecha relación con los equipos de producto de Google y Alphabet, además de expertos, a fin de solucionar desafíos técnicos específicos y optimizar sus negocios para el crecimiento con aprendizaje automático. El año pasado ...Read More
Publicado por Rich Hyndman, líder de Tecnología global, Google Launchpad Launchpad Studio es un programa de aceleración para las principales startups del mundo. Los fundadores trabajan en estrecha relación con los equipos de producto de Google y Alphabet, además de expertos, a fin de solucionar desafíos técnicos específicos y optimizar sus negocios para el crecimiento con aprendizaje automático. El año pasado, presentamos nuestro primer cohorte de aprendizaje automático aplicado centrado en la atención médica.
En el presente, damos con entusiasmo la bienvenida al nuevo cohorte de startups de finanzas seleccionadas para participar en Launchpad Studio:
Alchemy (EE. UU.) vincula la cadena de bloques y el mundo real.
Axinan (Singapur) proporciona seguros inteligentes para la economía digital.
Aye Finance (India) transforma las finanzas en India.
Celo (EE. UU.) aumenta la inclusión financiera a través de una criptomoneda que prioriza los dispositivos móviles.
Frontier Car Group (Alemania) invierte en la transformación de los mercados de automóviles usados.
Go-Jek (Indonesia) mejora el bienestar y la subsistencia de los sectores informales.
GuiaBolso (Brasil) mejora las finanzas de los brasileros.
Inclusive (Ghana) verifica identidades en todo África.
m.Paani (India) empodera a los minoristas locales y a los siguientes mil millones de usuarios de la India.
Starling Bank (R.U.) mejora las finanzas con un banco totalmente móvil.
Se invitó a estas startups de Studio de nueve países y cuatro continentes a discutir la manera en la que se puede utilizar el aprendizaje automático para la inclusión financiera, la estabilización monetaria y la prestación servicios de identificación. Están definiendo la forma en que el aprendizaje automático y la cadena de bloques pueden sumar esfuerzos a fin de brindar inclusión absoluta y garantizar una mayor prosperidad para todos. Juntos, los datos y el comportamiento de los usuarios hacen posible una economía verdaderamente global con productos inclusivos y diferenciados para servicios bancarios, seguros y crédito.
Cada startup se vincula a un administrador de productos de Google para acelerar el desarrollo de su producto, y trabaja junto a los equipos de investigación y desarrollo de aprendizaje automático de Google. Studio proporciona orientación personalizada y acceso a las personas, a la red, al liderazgo intelectual y a la tecnología de Google.
“Dos de los obstáculos más importantes para la adopción de criptomonedas a gran escala como medio de pago son la facilidad de uso y la volatilidad de la capacidad de compra. Cuando escuchamos sobre Studio y la oportunidad de trabajar con los equipos de IA de Google, nos entusiasmamos de inmediato ya que creemos que el trabajo resultante puede ser beneficioso no solo para Celo sino para la industria en su totalidad”, afirmó Rene Reinsberg, cofundador y CEO de Celo.
“Nuestra tecnología aceleró el crecimiento económico en Indonesia al elevar el estándar de vida de millones de microemprendedores, entre los que se incluyen conductores de moto-taxis, propietarios de restaurantes, pequeñas empresas y otros profesionales. Nos entusiasma trabajar con Google e indagar más respecto de cómo la inteligencia artificial y el aprendizaje automático pueden ayudarnos a fortalecer nuestras capacidades para impulsar un cambio social más positivo, no solo para indonesia sino también para la región”, dijo Kevin Aluwi, cofundador y CIO de GO-JEK.
“En Starling, creemos que los datos son la clave para lograr finanzas saludables. Nos entusiasma la oportunidad de trabajar con Google para convertir datos en estadísticas que ayuden a los consumidores a tomar decisiones financieras más acertadas y fundamentadas”, dijo Anne Boden, fundadora y CEO de Starling Bank.
“En GuiaBolso, usamos aprendizaje automático en diferentes equipos de trabajo, pero ahora estamos reforzando el aspecto tecnológico para que la experiencia de nuestros usuarios sea aún más placentera. Consideramos que Studio es una alternativa para acelerar eso”, comentó Marcio Reis, CDO de GuiaBolso.
Desde su lanzamiento, en 2015, Google Developers Launchpad se ha convertido en una red global de aceleradores y socios con la misión compartida de acelerar la innovación, y resuelve los mayores desafíos del mundo. Únete a nosotros en uno de nuestros Aceleradores regionales y sigue las prácticas recomendadas de aprendizaje automático aplicado de Launchpad suscribiéndote en The Lever.
Publicado por Johan Schalkwyk, vicepresidente, e Ignacio López Moreno, ingeniero, Google Speech
Los hogares multilingües se vuelven cada vez más comunes, con varias fuentes [ 1][ 2][ 3 ...Read More
Publicado por Johan Schalkwyk, vicepresidente, e Ignacio López Moreno, ingeniero, Google Speech
Los hogares multilingües se vuelven cada vez más comunes, con varias fuentes [1][2][3] que indican que los hablantes multilingües ya superan el número de versiones monolingües y que este número seguirá creciendo. Con esta gran población creciente de usuarios multilingües, es más importante que nunca que Google desarrolle productos que puedan admitir varios idiomas de manera simultánea para brindar a nuestros usuarios un mejor servicio.
El Asistente de Google ahora puede identificar el idioma, interpretar la búsqueda y proporcionar una respuesta usando el idioma correcto sin que el usuario deba modificar la configuración del Asistente.
Hoy lanzaremos la compatibilidad multilingüe con el Asistente de Google, que permite a los usuarios cambiar entre dos idiomas diferentes en las consultas, sin necesidad de regresar a sus configuraciones de idioma. Una vez que los usuarios seleccionan dos de los idiomas admitidos (inglés, español, francés, alemán, italiano y japonés), a partir de ese momento pueden hablar con el Asistente en cualquiera de ellos y este responderá del mismo modo. Anteriormente, los usuarios debían elegir una configuración de un único idioma para el Asistente, y cambiarla cada vez que querían usar otro idioma, pero ahora es una experiencia simple y de manos libres para entornos familiares multilingües.
Sin embargo, lograr que esto funcione no fue simple. De hecho, fue un esfuerzo de varios años que implicó resolver muchos problemas desafiantes. Al final, dividimos el problema en tres partes discretas: identificar varios idiomas, comprender varios idiomas y Optimizar el reconocimiento multilingüe para los usuarios del Asistente de Google.
Identificación de varios idiomas
Las personas pueden reconocer otro idioma cuando alguien habla, incluso si ellas mismas no lo hablan, con solo prestar atención a la acústica de la voz (entonación, registro fonético, etc.). Sin embargo, definir un marco de trabajo computacional para el reconocimiento automático de idiomas hablados es un desafío, incluso con la ayuda de sistemas de reconocimiento de voz completamente automáticos.1. En 2013, Google comenzó a trabajar con la tecnología de identificación de idiomas hablados (LangID) con redes neuronales profundas [4][5]. Hoy, nuestros modelos innovadores de LangID pueden distinguir pares de idiomas entre más de 2000 pares de idiomas alternativos a través de redes neuronales recurrentes, una familia de redes neuronales que son particularmente exitosas para problemas de modelación de secuencias, como los del reconocimiento de voz, la detección de voz y el reconocimiento de hablantes, entre otros. Uno de los desafíos que enfrentamos fue trabajar con conjuntos de audio más grandes; obtener modelos que puedan comprender automáticamente varios idiomas a escala y alcanzar un estándar de calidad que permitiera que los modelos funcionen de manera adecuada.
Comprender varios idiomas
Para comprender más de un idioma a la vez, se deben ejecutar varios procesos en paralelo, cada uno con resultados incrementales, lo cual permite que el Asistente no solo identifique el idioma en el que se dice la consulta sino también analice la consulta para crear un comando de acción. Por ejemplo, incluso en un entorno monolingüe, si un usuario pide “fijar una alarma a las 6 p. m.”, el Asistente de Google debe comprender que “fijar una alarma” implica abrir la app de reloj, cumplir con el parámetro explícito de “6 p. m.” y adicionalmente inferir que la alarma se debe fijar para hoy. Hacer que esto funcione para cualquier par de idiomas admitidos es un desafío, ya que el Asistente ejecuta el mismo trabajo que hace para el caso monolingüe, pero ahora adicionalmente debe permitir a LangID y no solo uno sino dos sistemas de reconocimiento de voz de forma simultánea (luego explicaremos más sobre la limitación actual de dos idiomas en esta publicación).
Un aspecto importante es que el Asistente de Google y otros servicios a los que se hace referencia en la consulta del usuario de modo asíncrono generen resultados incrementales en tiempo real, los cuales deben evaluarse en milisegundos. Esto se lleva a cabo con la ayuda de un algoritmo adicional que clasifica las hipótesis de transcripción proporcionadas por cada uno de los dos sistemas de reconocimiento de voz recurriendo a las probabilidades de los idiomas potenciales producidos por LangID, a nuestra confianza en la transcripción y a las preferencias del usuario (por ejemplo, artistas favoritos).
Esquema de nuestro sistema de reconocimiento de voz multilingüe usado por el Asistente de Google en comparación con el sistema estándar de reconocimiento de voz monolingüe. Se usa un algoritmo de clasificación para seleccionar las mejores hipótesis de reconocimiento de dos reconocedores de voz monolingües, con información relevante sobre el usuario y los resultados incrementales de langID.
Cuando el usuario deja de hablar, el modelo no solo determina el idioma, sino también lo dicho. Por supuesto, este proceso requiere de una arquitectura sofisticada con un mayor costo de procesamiento y la posibilidad de presentar una latencia innecesaria.
Optimización del reconocimiento multilingüe
Para minimizar estos efectos no deseados, cuanto más rápido pueda el sistema determinar el idioma hablado mejor será el resultado. Si el sistema determina con certeza el idioma hablado antes de que el usuario finalice una consulta, dejará de procesar la voz del usuario con el reconocedor improductivo y descartará la hipótesis infructuosa, lo cual reducirá la carga de procesamiento y cualquier latencia potencial. Con esto en mente, vimos varios formar de optimizar el sistema.
Tuvimos en cuenta un caso de uso en el que la gente normalmente emplea el mismo idioma en sus consultas (que es también el idioma que los usuarios quieren oír usar al Asistente), a excepción de las preguntas sobre entidades con nombres en diferentes idiomas. Esto significa que, en la mayoría de los casos, concentrarse en la primera parte de la consulta permite que el Asistente realice una conjetura preliminar del idioma hablado, incluso en oraciones con entidades en un idioma diferente. Con esta identificación preliminar, la tarea se simplifica realizando un cambio a un reconocedor único de voz monolingüe, como lo hacemos con las consultas monolingües. Para tomar una decisión rápida sobre cómo y dónde optar un único idioma, sin embargo, se requiere un último toque tecnológico: específicamente, usamos una técnica de bosque aleatorio que combina varias señales contextuales, como el tipo de dispositivo usado, el número de hipótesis de voz encontradas, la frecuencia con la que recibimos hipótesis similares, la incertidumbre de los reconocedores de voz individuales y la frecuencia con la que se usa cada idioma.
Una alternativa adicional con la que simplificamos y mejoramos la calidad del sistema consistió en limitar la lista de idiomas potenciales que los usuarios pueden seleccionar. Los usuarios pueden elegir dos idiomas entre los seis que nuestros dispositivos Home admiten actualmente, lo que nos permitirá contemplar a la mayoría de nuestros hablantes multilingües. Sin embargo, mientras seguimos mejorando nuestra tecnología esperamos poder arribar a una compatibilidad trilingüe con la certeza de que esto mejorará la experiencia de nuestra creciente base de usuarios.
Bilingüe a trilingüe
Desde el comienzo, nuestro objetivo fue lograr que el Asistente fuera naturalmente conversacional para todos los usuarios. La compatibilidad multilingüe ha sido una característica muy requerida y nuestro equipo observó esto hace unos años. Pero hoy no solo hay muchísimos hablantes bilingües en el mundo; también queremos hacer más sencilla la vida para los usuarios trilingües o las familias que vivan en hogares en los cuales se hablen más de dos idiomas.
Con la actualización de hoy, estamos en el camino correcto y esto fue posible gracias a nuestro aprendizaje automático avanzado, a nuestras tecnologías de reconocimiento de idioma y de voz y al compromiso de nuestro equipo para redefinir nuestro modelo LangID. Ahora estamos trabajando para enseñar al Asistente de Google a procesar más de dos idiomas de manera simultánea y para agregar más idiomas admitidos en el futuro. ¡No te lo pierdas! 1 Por lo general, se sabe que el reconocimiento de idiomas hablados supone un desafío mucho mayor que la identificación de idiomas en base a textos, en la cual técnicas relativamente simples basadas en diccionarios pueden hacer un buen trabajo. Los patrones de tiempo y frecuencia de las palabras orales son difíciles de comparar; estas pueden ser más difíciles de delimitar, ya que pueden articularse sin pausa y en diferentes ritmos, y los micrófonos pueden registrar ruidos de fondo además de la voz.
“Buena IU. ¿Pero cómo aborda Flutter las API específicas de la plataforma?”. Flutter te invita a crear tu app para dispositivos móviles en el lenguaje de programación Dart y a realizar compilaciones para Android e iOS. Pero Dart no realiza compilaciones para el código de bytes Dalvik de Android, ni estás bendecido con vinculaciones de Dart y Objective-C en iOS. Esto significa que tu código Dart se escribe sin acceso directo a las API específicas de la plataforma Cocoa Touch de iOS y al SDK de Android.Read More
“Buena IU. ¿Pero cómo aborda Flutter las API específicas de la plataforma?”. Flutter te invita a crear tu app para dispositivos móviles en el lenguaje de programación Dart y a realizar compilaciones para Android e iOS. Pero Dart no realiza compilaciones para el código de bytes Dalvik de Android, ni estás bendecido con vinculaciones de Dart y Objective-C en iOS. Esto significa que tu código Dart se escribe sin acceso directo a las API específicas de la plataforma Cocoa Touch de iOS y al SDK de Android.
Esto no representa un gran problema siempre que solo escribas en Dart para pintar píxeles en la pantalla. El marco de trabajo de Flutter y su motor de gráficos subyacente son capaces de hacer esto por sí solos. Tampoco es un problema si lo que haces además de pintar píxeles es archivar o conectar E/S y lógica de negocios asociada. El lenguaje, el tiempo de ejecución y las bibliotecas de Dart se ocupan de esto.
Sin embargo, las apps más complejas requieren una mayor integración con la plataforma host:
notificaciones, ciclo de vida de las apps, vínculos directos,
uso compartido de información con otras apps, lanzamiento de otras apps,
preferencias persistentes, carpetas especiales, información de dispositivos...
La lista es larga y parece ampliarse con cada actualización de la plataforma.
El acceso a todas estas API de la plataforma podría integrarse al marco de trabajo de Flutter. Sin embargo, eso expandiría mucho Flutter y le daría muchas más razones para cambiar. En la práctica, esto haría que Flutter se retrasara con respecto a la última versión de la plataforma. O expondría a los autores a agrupaciones de “mínimo denominador común” poco satisfactorias de las API de la plataforma. O confundiría a los recién llegados con abstracciones difíciles de manejar para ocultar diferencias de la plataforma. También podría existir fragmentación de versiones. O errores.
Pensándolo bien, probablemente podría producirse todo lo anterior.
El equipo de Flutter eligió un enfoque diferente. No hace todo eso, pero es sencillo y versátil, y está totalmente bajo tu control.
En primer lugar, Flutter se aloja en una app ambiente de Android o iOS. Las partes de la app que corresponden a Flutter se agrupan en componentes estándares específicos de la plataforma, como View en Android y UIViewController en iOS. Por lo tanto, cuando Flutter te invite a escribir tu app en Dart, puedes hacer todo lo que quieras en Java y Kotlin, o en Objective-C y Swift en la app host, y trabajar directamente sobre las API específicas de la plataforma.
En segundo lugar, los canales de la plataforma proporcionan un mecanismo simple de comunicación entre tu código Dart y el código específico de la plataforma de tu app host. Esto significa que puedes exponer un servicio de la plataforma en el código de tu app host e invocarlo desde Dart. O viceversa.
Y en tercer lugar, los complementos permiten crear una Dart API respaldada por una implementación en Android escrita en Java o Kotlin y una implementación en iOS escrita en Objective-C o Swift, y empaquetarla como un conjunto triple de Flutter, Android e iOS usando canales de plataforma. Esto significa que puedes volver a usar, compartir y distribuir tu opinión sobre cómo Flutter debería usar una API de plataforma específica.
En este artículo se ofrece una introducción detallada a los canales de plataformas. Comenzando por los aspectos fundamentales de la mensajería de Flutter, presentaré los conceptos de canal de mensajes, métodos y eventos, y analizaré algunas consideraciones de diseño de la API. No proporcionaré listas de API, aunque sí ejemplos de códigos cortos que podrás reutilizar mediante copia y pegado. Se proporciona una lista breve de pautas de uso en función de mi experiencia como contribuyente al repositorio flutter/plugins de GitHub, como miembro del equipo de Flutter. El artículo finaliza con una lista de recursos adicionales, que incluyen vínculos a las API de referencia DartDoc/JavaDoc/ObjcDoc.
Para la mayoría de los casos de uso, probablemente usarías canales de métodos para la comunicación de la plataforma. Sin embargo, debido a que muchas de sus propiedades derivan de canales de mensajes más simples y de aspectos fundamentales de la mensajería binaria subyacente, empezaré por allí.
Aspectos básicos: mensajería binaria asíncrona
En el nivel más básico, Flutter se comunica con el código de la plataforma usando transmisión de mensajes asíncronos con mensajes binarios, lo cual significa que la carga útil de mensajes es un búfer de bytes. Para distinguir mensajes que se usan para diferentes propósitos, cada mensaje se envía en un “canal” lógico, que es simplemente una string de nombre. En los siguientes ejemplos se usa el nombre de canal foo.
// Send a binary message from Dart to the platform.
final WriteBuffer buffer = WriteBuffer()
..putFloat64(3.1415)
..putInt32(12345678);
final ByteData message = buffer.done();
await BinaryMessages.send('foo', message);
print('Message sent, reply ignored');
En Android, un mensaje de este tipo, como java.nio.ByteBuffer, se puede recibir usando el siguiente código de Kotlin:
// Receive binary messages from Dart on Android.
// This code can be added to a FlutterActivity subclass, typically
// in onCreate.
flutterView.setMessageHandler("foo") {message, reply ->
message.order(ByteOrder.nativeOrder())
val x = message.doubleval n = message.int
Log.i("MSG", "Received: $x and $n")
reply.reply(null)
}
La ByteBuffer API admite la lectura de valores primitivos y, al mismo tiempo, anticipa automáticamente la posición de lectura actual. Con iOS ocurre algo similar; es muy bienvenida cualquier sugerencia para mejorar mi frágil Swift fu:
// Receive binary messages from Dart on iOS.// This code can be added to a FlutterAppDelegate subclass,
// typically in application:didFinishLaunchingWithOptions:.
let flutterView =
window?.rootViewController as! FlutterViewController;
flutterView.setMessageHandlerOnChannel("foo") {
(message: Data!, reply: FlutterBinaryReply) -> Void inlet x : Float64 = message.subdata(in: 0..<8)
.withUnsafeBytes { $0.pointee }
let n : Int32 = message.subdata(in: 8..<12)
.withUnsafeBytes { $0.pointee }
os_log("Received %f and %d", x, n)
reply(nil)
}
La comunicación es bidireccional, de modo que también puedes enviar mensajes en la dirección opuesta, de Java/Kotlin u Objective-C/Swift a Dart. La inversión de la dirección de la configuración anterior se ve de la siguiente manera:
var message = Data(capacity: 12)
var x : Float64 = 3.1415
var n : Int32 = 12345678
message.append(UnsafeBufferPointer(start: &x, count: 1))
message.append(UnsafeBufferPointer(start: &n, count: 1))
flutterView.send(onChannel: "foo", message: message) {(_) -> Void in
os_log("Message sent, reply ignored")
}
// Receive binary messages from the platform.
BinaryMessages.setMessageHandler('foo', (ByteData message) async {
final ReadBuffer readBuffer = ReadBuffer(message);
final double x = readBuffer.getFloat64();
final int n = readBuffer.getInt32();
print('Received $x and $n');
return null;
});
La letra chica: respuestas obligatorias. Cada envío de mensaje incluye una respuesta asíncrona del receptor. En los ejemplos anteriores, no hay valores interesantes para comunicar, pero la respuesta nula es necesaria para que se complete el futuro de Dart y para que se ejecuten las dos callbacks de la plataforma. Subprocesos: los mensajes y las respuestas se reciben, y se deben enviar, en el subproceso principal de la IU de la plataforma. En Dart, hay un solo subproceso por cada componente aislado de Dart; es decir, por cada vista de Flutter, de modo que no existe confusión con respecto a los subprocesos que deben usarse aquí. Excepciones: las excepciones no detectadas que se produzcan en un controlador de mensajes de Dart o Android son captadas por el marco de trabajo, se registran y se envía una respuesta nula al emisor. Las excepciones no detectadas que se producen en los controladores de respuestas se registran. Vida útil del controlador: los controladores de mensajes registrados se conservan y mantienen activos juntos con la vista de Flutter (es decir, el elemento aislado de Dart, la instancia de FlutterView de Android y FlutterViewController de iOS). Puedes reducir la vida del controlador al anular su registro: simplemente envía un controlador nulo (o diferente) usando el mismo nombre de canal. Exclusividad de controladores: los controladores se conservan en un mapa hash codificado por nombre de canal, de modo que pueda haber, como máximo, un controlador por canal. Un mensaje enviado en un canal para el que no se registran controladores de mensajes en el extremo de recepción se responde automáticamente usando una respuesta nula. Comunicación síncrona: la comunicación de la plataforma está disponible únicamente en el modo asíncrono. Esto evita que se realicen llamadas de bloqueo entre subprocesos y también los problemas de nivel de sistema que podrían producirse (bajo rendimiento, riesgo de interbloqueo). Al momento de este documento, no queda completamente claro si la comunicación síncrona es realmente necesaria en Flutter y, si lo fuera, la forma en que se produciría.
Al trabajar en el nivel de mensajes binarios, debes preocuparte por detalles delicados como el formato endian y por cómo representar mensajes de nivel superior, como strings o mapas, usando bytes. También debes especificar el nombre de canal correcto cada vez que quieras enviar un mensaje o registrar un controlador. Facilitar esto nos conduce a canales de plataforma: Un canal de plataforma es un objeto que reúne un nombre de canal y un códec para serializar o deserializar mensajes al formato binario y viceversa.
Canales de mensajes: nombre + códec
Supongamos que quieres enviar y recibir mensajes de strings en lugar de búferes de bytes. Esto se puede hacer usando un canal de mensajes, un tipo de canal de plataforma simple construido con un códec de strings. En el siguiente código se muestra la manera de usar canales de mensajes en ambas direcciones entre Dart, Android e iOS:
// Send message to platform and receive reply.final String reply = await channel.send('Hello, world');
print(reply);
// Receive messages from platform and send replies.
channel.setMessageHandler((String message) async {
print('Received: $message');
return 'Hi from Dart';
});
// Android side
val channel = BasicMessageChannel<String>(
flutterView, "foo", StringCodec.INSTANCE)
// Send message to Dart and receive reply.channel.send("Hello, world") { reply ->
Log.i("MSG", reply)
}
// Receive messages from Dart and send replies.channel.setMessageHandler { message, reply ->
Log.i("MSG", "Received: $message")
reply.reply("Hi from Android")
}
// iOS side
let channel = FlutterBasicMessageChannel(
name: "foo",
binaryMessenger: controller,
codec: FlutterStringCodec.sharedInstance())
// Send message to Dart and receive reply.channel.sendMessage("Hello, world") {(reply: Any?) -> Void in
os_log("%@", type: .info, reply as! String)
}
// Receive messages from Dart and send replies.
channel.setMessageHandler {
(message: Any?, reply: FlutterReply) -> Void in
os_log("Received: %@", type: .info, message as! String)
reply("Hi from iOS")
}
El nombre del canal se especifica únicamente en la construcción del canal. Luego de eso, las llamadas para enviar un mensaje o configurar un controlador de mensajes se pueden realizar sin repetir el nombre del canal. Lo que es más importante, dejamos que la clase de códec de string se ocupe de interpretar los búferes de bytes como strings y viceversa.
Estas son ventajas nobles, pero probablemente estés de acuerdo en que BasicMessageChannel no hace todo eso. Esto es intencional. El código Dart anterior equivale a la siguiente aplicación de los aspectos básicos de mensajería binaria:
const codec = StringCodec();
// Send message to platform and receive reply.final String reply = codec.decodeMessage(
await BinaryMessages.send(
'foo',
codec.encodeMessage('Hello, world'),
),
);
print(reply);
// Receive messages from platform and send replies.
BinaryMessages.setMessageHandler('foo', (ByteData message) async {
print('Received: ${codec.decodeMessage(message)}');
return codec.encodeMessage('Hi from Dart');
});
Esta observación también se aplica a las implementaciones de canales de mensajes en Android e iOS. Aquí no hay magia:
Los canales de mensajes delegan toda la comunicación a la capa de mensajería binaria.
Los canales de mensajes no realizan un seguimiento de los controladores registrados.
Los canales de mensajes son ligeros y no poseen estado.
Dos instancias de canal de mensajes creadas con el mismo nombre de canal y el mismo códec son equivalentes (y una interfiere con la comunicación de la otra).
Por varios motivos históricos, el marco de trabajo de Flutter define cuatro códecs de mensajes diferentes:
StringCodec: codifica strings usando UTF-8. Como acabamos de ver, los canales de mensajes con este códec tienen el tipo BasicMessageChannel<String> en Dart.
BinaryCodec: al implementar la asignación de identidad en los búferes de bytes, este códec te permite disfrutar de la practicidad de los objetos de canal en casos en los que no necesitas codificación ni decodificación. Los canales de mensajes Dart con este códec tienen el tipo BasicMessageChannel<ByteData>.
JSONMessageCodec Se maneja con valores de tipo JSON (strings, números, valores booleanos, valores nulos, listas de esos valores y mapas de esos valores codificados en strings). Las listas y los mapas son heterogéneos y se pueden anidar. Durante la codificación, los valores se convierten en strings JSON y luego en bytes usando UTF-8. Los canales de mensajes Dart tienen el tipo BasicMessageChannel<dynamic> con este códec.
StandardMessageCodec: se maneja con valores ligeramente más generalizados que el códec JSON, y también admite mapas y búferes de datos homogéneos (UInt8List, Int32List, Int64List y Float64List) con claves que no son strings. El manejo de números difiere de JSON con valores enteros de Dart que llegan como valores enteros firmados de 32 o 64 bits en la plataforma, según la magnitud, nunca como números de punto flotante. Los valores se codifican en un formato binario personalizado, razonablemente compacto y extensible. El códec estándar está diseñado para ser la opción predeterminada para la comunicación de canales en Flutter. En el caso de JSON, los canales de mensajes de Dart construidos con el códec estándar tienen el tipo BasicMessageChannel<dynamic>.
Como probablemente lo hayas conjeturado, los canales de mensajes funcionan con cualquier implementación de códecs de mensajes que cumplan con un contrato simple. Esto te permite incorporar tu propio códec si es necesario. Deberás implementar codificación y decodificación compatible en Dart, Java/Kotlin y Objective-C/Swift. La letra chica:la evolución de los códecs. Cada códec de mensajes está disponible en Dart, como parte del marco de trabajo de Flutter, y también en ambas plataformas, como parte de las bibliotecas expuestas por Flutter a tu código Java/Kotlin o Objective-C/Swift. Flutter usa los códecs solo para la comunicación dentro de la app, no como un formato persistente. Esto significa que la forma binaria de los mensajes puede cambiar de una versión de Flutter a la siguiente sin aviso. Por supuesto, las implementaciones de códecs en Dart, Android e iOS evolucionan juntas para garantizar que lo que codifica el emisor pueda ser codificado correctamente por el receptor, en ambas direcciones. Mensajes nulos: los códecs de mensajes deben admitir y preservar mensajes nulos, ya que esto representa la respuesta predeterminada a un mensaje enviado en un canal para el que no se ha registrado ningún controlador de mensajes en el receptor. Redacción estática de mensajes en Dart. Un canal de mensajes configurado con el códec de mensajes estándares da dinámica de tipo a los mensajes y las respuestas. Generalmente, explicitarás tus expectativas de tipo mediante la asignación a una variable tipificada:
final String reply1 = await channel.send(msg1);
final int reply2 = await channel.send(msg2);
Sin embargo, hay un inconveniente al trabajar con respuestas que incluyen parámetros de tipo genéricos:
La primera línea falla en el tiempo de ejecución, a menos que la respuesta sea nula. El códec de mensajes estándares se escribe para listas y mapas heterogéneos. En Dart tienen los tipos de tiempo de ejecución List<dynamic> y Map<dynamic, dynamic>, y Dart 2 evita que dichos valores se asignen a variables con argumentos de tipos más específicos. Esta situación es similar a la deserialización Dart JSON que produce List<dynamic> y Map<String, dynamic>, como lo hace el códec de mensajes JSON.
Las características pueden exponerte a problemas similares:
El primer método falla en el tiempo de ejecución, aun cuando la respuesta recibida sea una string. La implementación del canal crea un objeto Future<dynamic> independientemente del tipo de respuesta, y un objeto de este tipo no se puede asignar a una Future<String>. ¿Por qué lo “básico” en BasicMessageChannel? Los canales de mensajes parecen usarse solo en situaciones bastante limitadas en las que se comunica alguna forma de transmisión de eventos homogénea en un contexto implícito. Como eventos de teclado, quizás. Para la mayoría de las aplicaciones de canales de plataforma, necesitarás comunicar no solo valores, sino también lo que quieras que ocurra con cada valor, o la manera en la que desees que el receptor lo interprete. Una manera de lograrlo es hacer que el mensaje represente una llamada al método con el valor como argumento. De esta manera, te convendrá una forma estándar de separar el nombre del método del argumento en el mensaje. También te convendrá una forma estándar de distinguir respuestas de éxito y error. Esto es lo que los canales de métodos hacen por ti. Originalmente, BasicMessageChannel tenía el nombre MessageChannel, pero se le cambió el nombre para evitar confundir MessageChannel con MethodChannel en el código. Al aplicarse con más frecuencia, los canales de métodos conservaron el nombre más corto.
Canales de métodos: sobres estandarizados
Los canales de métodos son canales de plataforma diseñados para invocar partes de código con nombre en Dart y Java/Kotlin o Objective-C/Swift. Los canales de métodos usan “sobres” de mensajes estandarizados para transmitir el nombre del método y los argumentos del emisor al receptor, y para distinguir resultados correctos y erróneos en la respuesta asociada. Los sobres y la carga útil admitidos se definen con clases de códecs de métodos independientes, de la misma manera que los canales de mensajes usan códecs de mensajes. Eso es todo lo que hacen los canales de métodos: combinar un nombre de canal con un códec.
En particular, no se hacen suposiciones con respecto a qué código se ejecuta al recibir un mensaje en un canal de métodos. Incluso cuando el mensaje represente una llamada a un método, no necesitas invocar un método. Simplemente podrías cambiar el nombre del método y ejecutar unas líneas de código para cada caso. Nota al margen: esta falta de vinculación implícita o automatizada con los métodos y sus parámetros podría decepcionarte. Y está bien; la decepción puede ser productiva. Supongo que puedes crear una solución desde cero usando procesamiento de anotaciones y generación de código, o quizás puedas reutilizar partes de un marco de trabajo RPC existente. Flutter es de código abierto, ¡no dudes en contribuir! Los canales de métodos están disponibles como destino para tu generación de código, si resultan adecuados. Mientras tanto, son útiles por sí solos en el “modo artesanal”.
Los canales de métodos fueron la respuesta del equipo de Flutter al desafío de definir una API de comunicación ejecutable para que pudiera usarla el ecosistema de complementos inexistentes de ese momento. Queríamos algo que los autores de complementos pudieran comenzar a usar de inmediato, sin necesidad de un gran volumen de código estándar o configuraciones de compilación complicadas. Creo que el concepto de canal de métodos proporciona una respuesta aceptable, pero me sorprendería que continuara siendo la única.
A continuación, se muestra la forma en que usarías un canal de métodos en el caso sencillo de invocar una pequeña parte de código de la plataforma desde Dart. El código se asocia con la barra de nombre que en este caso no es, pero podría haber sido, un nombre de método. Lo que hace es construir una string de saludo y mostrársela a al emisor, de modo que podamos codificar eso con la suposición razonable de que la invocación a la plataforma no fallará (veremos el manejo de errores en más detalle más adelante):
val channel = MethodChannel(flutterView, "foo")
channel.setMethodCallHandler { call, result ->
when (call.method) {
"bar" -> result.success("Hello, ${call.arguments}")
else -> result.notImplemented()
}
}
// iOS side.
let channel = FlutterMethodChannel(
name: "foo", binaryMessenger: flutterView)
channel.setMethodCallHandler {
(call: FlutterMethodCall, result: FlutterResult) -> Void in
switch (call.method) {
case "bar": result("Hello, \(call.arguments as! String)")
default: result(FlutterMethodNotImplemented)
}
}
Al agregar casos a las construcciones de interruptores, podemos extender fácilmente lo anterior para controlar varios métodos. La cláusula predeterminada controla la situación en la que se llama a un método desconocido (probablemente debido a un error de programación).
El código Dart anterior es equivalente a lo siguiente:
const codec = StandardMethodCodec();
final ByteData reply = await BinaryMessages.send(
'foo',
codec.encodeMethodCall(MethodCall('bar', 'world')),
);
if (reply == null)
throw MissingPluginException();
else
print(codec.decodeEnvelope(reply));
Las implementaciones de canales de métodos en Android e iOS son contenedores estrechos similares en torno a llamadas a los aspectos fundamentales de la mensajería binaria. Se usa una respuesta nula para representar un resultado “no implementado”. Esto convenientemente hace que el comportamiento en el extremo receptor sea indiferente respecto de que la invocación fracase en la cláusula predeterminada en el interruptor o no se registre un controlador de llamadas a métodos en el canal.
El valor del argumento del ejemplo es el ámbito de string única. Pero el códec de método predeterminado, acertadamente denominado “códec de método estándar”, usa el códec de mensaje estándar preexistente para codificar valores de carga útil. Esto significa que todos los valores “generalizados de tipo JSON” que se describieron antes se admiten como argumentos del método y como resultados (correctos). En particular, las listas heterogéneas admiten varios argumentos, mientras que los mapas heterogéneos admiten argumentos denominados. El valor predeterminado de los argumentos es nulo. Algunos ejemplos:
StandardMethodCodec: de forma predeterminada, delega la codificación de los valores de carga útil a StandardMessageCodec. Debido a que este último es extensible, también lo es el anterior.
JSONMethodCodec: delega la codificación de los valores de carga útil a JSONMessageCodec.
Puedes configurar canales de métodos con cualquier códec de método, incluidos los personalizados. Para comprender plenamente lo que incluye la implementación de un códec, observemos la forma en que se abordan los errores a nivel de la API de canales de métodos ampliando el ejemplo anterior con un método baz falible:
// Method calls with error handling.
// Dart side.
const channel = MethodChannel('foo');
// Invoke a platform method.const name = 'bar'; // or 'baz', or 'unknown'const value = 'world';
try {
print(await channel.invokeMethod(name, value));
} on PlatformException catch(e) {
print('$name failed: ${e.message}');
} on MissingPluginException {
print('$name not implemented');
}
// Receive method invocations from platform and return results.
channel.setMethodCallHandler((MethodCall call) async {
switch (call.method) {
case 'bar':
return 'Hello, ${call.arguments}';
case 'baz':
throw PlatformException(code: '400', message: 'This is bad');
default:
throw MissingPluginException();
}
});
// Android side.
val channel = MethodChannel(flutterView, "foo")
// Invoke a Dart method.val name = "bar" // or "baz", or "unknown"val value = "world"
channel.invokeMethod(name, value, object: MethodChannel.Result {
override fun success(result: Any?) {
Log.i("MSG", "$result")
}
override fun error(code: String?, msg: String?, details: Any?) {
Log.e("MSG", "$name failed: $msg")
}
override fun notImplemented() {
Log.e("MSG", "$name not implemented")
}
})
// Receive method invocations from Dart and return results.
channel.setMethodCallHandler { call, result ->
when (call.method) {
"bar" -> result.success("Hello, ${call.arguments}")
"baz" -> result.error("400", "This is bad", null)
else -> result.notImplemented()
}
}
// iOS side.
let channel = FlutterMethodChannel(
name: "foo", binaryMessenger: flutterView)
// Invoke a Dart method.let name = "bar" // or "baz", or "unknown"let value = "world"
channel.invokeMethod(name, arguments: value) {
(result: Any?) -> Void in
iflet error = result as? FlutterError {
os_log("%@ failed: %@", type: .error, name, error.message!)
} else if FlutterMethodNotImplemented.isEqual(result) {
os_log("%@ not implemented", type: .error, name)
} else {
os_log("%@", type: .info, result as! NSObject)
}
}
// Receive method invocations from Dart and return results.channel.setMethodCallHandler {
(call: FlutterMethodCall, result: FlutterResult) -> Void inswitch (call.method) {
case "bar": result("Hello, \(call.arguments as! String)")
case "baz": result(FlutterError(
code: "400", message: "This is bad", details: nil))
default: result(FlutterMethodNotImplemented)
}
Los errores son triplos (código, mensaje y detalles) en los que el código y el mensaje son strings. El mensaje está orientado al consumo humano y el código... bueno, al código. Los detalles del error tienen un valor predeterminado, generalmente nulo, que está limitado únicamente por los tipos de valor que el códec admite. La letra chica: excepciones. Las excepciones no detectadas que se producen en un controlador de llamadas de métodos de Dart o Android son detectadas por la implementación del canal, se registran y se muestra un resultado de error al emisor. Las excepciones no detectadas que se producen en los controladores de resultados se registran. Codificación de sobres: La manera en la que un códec de método codifica sus sobres es un detalle de implementación similar a la manera en la que los códecs de mensajes convierten mensajes en bytes. A modo de ejemplo, un códec de método podría usar listas: las llamadas a métodos pueden codificarse como listas de dos elementos [nombre del método y argumentos]; los resultados correctos como listas de un elemento [resultado]; los resultados de errores como listas de tres elementos [código, mensaje y detalles]. Un códec de método luego puede implementarse simplemente mediante delegación a un códec de mensaje subyacente que admita al menos listas, strings y valores null. Los argumentos de llamada, los resultados correctos y los detalles de error de un método serían valores arbitrarios admitidos por ese códec de mensaje. Diferencias de API: en los ejemplos de código anteriores se destaca que los canales de métodos entregan resultados muy diferentes en Dart, Android e iOS:
En Dart, la invocación está controlada por un método que muestra un valor futuro. El valor futuro se completa con el resultado de la llamada en casos exitosos, con un PlatformException en casos de error y un MissingPluginException en casos sin implementación.
En Android, la invocación está controlada por un método que toma un argumento del callback. La interfaz del callback define tres métodos de los cuales se llama a uno, según el resultado. El código de cliente implementa la interfaz del callback para definir lo que debería ocurrir ante un caso exitoso, de error o sin implementación.
En iOS, la invocación se aborda de forma similar mediante un método que toma un argumento del callback. Sin embargo, aquí el callback es una función de un solo argumento que recibe una instancia FlutterError, la constante FlutterMethodNotImplemented o, en caso de que haya éxito, el resultado de la invocación. El código de cliente proporciona un bloque con lógica condicional para abordar los diferentes casos, según sea necesario.
Estas diferencias, reflejadas también en la forma en la que se escriben los controladores de llamadas de mensajes, surgieron como concesiones a los estilos de los lenguajes de programación (Dart, Java y Objective-C) utilizados para las implementaciones de canales de métodos del SDK de Flutter. Rehacer las implementaciones en Kotlin y Swift podría eliminar algunas de las diferencias, pero se debe tener precaución para evitar que resulte más difícil usar canales de métodos de Java y Objective-C.
Canales de eventos: transmisión
Un canal de eventos es un canal de plataforma especializado pensado para el caso de uso en el que se exponen eventos de la plataforma a Flutter como una transmisión de Dart. El SDK de Flutter actualmente no admite el caso simétrico de exposición de transmisiones de Dart a código de la plataforma, aunque esto podría crearse si surgiera la necesidad.
Aquí te mostramos la forma en que consumirías una transmisión de eventos de la plataforma en Dart:
En el siguiente código se muestra la manera de producir eventos en la plataforma usando eventos de sensor en Android como ejemplo. La principal inquietud es garantizar que recibamos eventos de la fuente de la plataforma (en este caso, el administrador de sensores) y los enviemos a través del canal de eventos de forma precisa cuando 1) haya al menos un receptor de transmisiones en Dart y 2) esté en ejecución la Activity ambiente. Empaquetar la lógica necesaria en una sola clase aumenta las probabilidades de hacer esto correctamente:
// Producing sensor events on Android.
// SensorEventListener/EventChannel adapter.class SensorListener(private val sensorManager: SensorManager) :
EventChannel.StreamHandler, SensorEventListener {
private var eventSink: EventChannel.EventSink? = null
// EventChannel.StreamHandler methodsoverride fun onListen(
arguments: Any?, eventSink: EventChannel.EventSink?) {
this.eventSink = eventSink
registerIfActive()
}
override fun onCancel(arguments: Any?) {
unregisterIfActive()
eventSink = null
}
// SensorEventListener methods.override fun onSensorChanged(event: SensorEvent) {
eventSink?.success(event.values)
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
if (accuracy == SensorManager.SENSOR_STATUS_ACCURACY_LOW)
eventSink?.error("SENSOR", "Low accuracy detected", null)
}
// Lifecycle methods.fun registerIfActive() {
if (eventSink == null) return
sensorManager.registerListener(
this,
sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE),
SensorManager.SENSOR_DELAY_NORMAL)
}
fun unregisterIfActive() {
if (eventSink == null) return
sensorManager.unregisterListener(this)
}
}
// Use of the above class in an Activity.class MainActivity: FlutterActivity() {
var sensorListener: SensorListener? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GeneratedPluginRegistrant.registerWith(this)
sensorListener = SensorListener(
getSystemService(Context.SENSOR_SERVICE) as SensorManager)
val channel = EventChannel(flutterView, "foo")
channel.setStreamHandler(sensorListener)
}
override fun onPause() {
sensorListener?.unregisterIfActive()
super.onPause()
}
override fun onResume() {
sensorListener?.registerIfActive()
super.onResume()
}
}
Si usas el paquete android.arch.lifecycle en tu app, podrías hacer que SensorListener sea más independiente al convertirlo en un LifecycleObserver. La letra chica:la vida útil de un controlador de transmisiones. El controlador de transmisiones de la plataforma tiene dos métodos, onListen y onCancel, que se invocan cada vez que la cantidad de receptores de la transmisión de Dart va de cero a uno y nuevamente a cero, respectivamente. Esto puede ocurrir varias veces. La implementación del controlador de transmisiones debe comenzar a enviar eventos al receptor de eventos cuando se llama al primero, y debe detenerse cuando se llama al segundo. Además, debe pausarse cuando el componente de la app ambiente no se encuentra en ejecución. El código anterior proporciona un ejemplo típico. Tras bambalinas, un controlador de transmisiones es, por supuesto, un controlador de mensajes binarios, registrado con la vista Flutter usando el nombre del canal de eventos. Códec: un canal de eventos se configura con un códec de métodos, lo que nos permite distinguir entre eventos exitosos y erróneos de la misma manera que los canales de mensajes pueden distinguir entre resultados correctos y erróneos. Argumentos y errores del controlador de transmisiones. Los métodos onListen y onCancel del controlador de transmisiones se invocan mediante invocaciones del canal de métodos. Por lo tanto, debemos controlar las llamadas a métodos de Dart a la plataforma y los mensajes de eventos en la dirección inversa, todo en el mismo canal lógico. Esta configuración permite la retransmisión de argumentos a ambos métodos de control y el informe de errores. En Dart, los argumentos (si existieran) se proporcionan en la llamada a receiveBroadcastStream. Esto significa que se especifican una sola vez, independientemente de la cantidad de invocaciones de onListen y onCancel que tengan lugar durante la duración de la transmisión. Los errores informados se registran. Fin de la transmisión. Un receptor de eventos tiene un método endOfStream que se puede invocar para indicar que no se enviarán más eventos de éxito ni error. Para esto se usa el mensaje binario null. Cuando se recibe en Dart, la transmisión se cierra. Vida útil de una transmisión: la transmisión de Dart está respaldada por un controlador de transmisiones alimentado con los mensajes entrantes del canal de la plataforma. Un controlador de mensajes binarios se registra usando el nombre del canal de eventos para recibir mensajes entrantes solo mientras hay receptores para la transmisión.
Pautas de uso
Prefijar nombres de canales por dominio para exclusividad
Los nombres de los canales son solo strings, pero deben ser únicos en todos los objetos de canal que se usen para diferentes fines en tu app. Puedes lograr esto usando cualquier esquema de denominación adecuado. Sin embargo, el enfoque recomendado para canales usados en complementos consiste en emplear un prefijo de nombre de dominio y nombre de complemento, como some.body.example.com/sensors/foo, para el canal foo empleado por el complemento de sensores desarrollado por some.body en example.com. Esto permite a los consumidores de complementos combinar cualquier cantidad de complementos en sus apps sin correr el riesgo de que ocurran colisiones entre nombres de canales.
Considerar el tratamiento de canales de plataforma como vías de comunicación dentro de módulos
El código usado para invocar llamadas de procedimientos remotos en sistemas distribuidos tiene una apariencia superficialmente similar al código que se usa en los canales de métodos: se invoca un método proporcionado por una string y se serializan los argumentos y resultados. Debido a que los componentes de los sistemas distribuidos generalmente se desarrollan e implementan de forma independiente, es fundamental controlar estrictamente las solicitudes y las respuestas, lo que normalmente se realiza siguiendo el estilo de comprobación y registro en ambos lados de la red.
Los canales de plataforma, por otro lado, integran tres partes de código que se desarrollan e implementan juntas, en un solo componente.
Java/Kotlin ↔ Dart ↔ Objective-C/Swift
De hecho, normalmente tiene sentido empaquetar un trío como este en un solo módulo de código, como un complemento de Flutter. Esto significa que la necesidad de comprobación de argumentos y resultados en invocaciones de canales de métodos debe ser comparable a la necesidad de tales comprobaciones en llamadas de métodos normales dentro del mismo módulo.
Dentro de los módulos, nuestra principal inquietud es brindar protección contra errores de programación que estén fuera del control de las comprobaciones estáticas del compilador y pasen inadvertidas en el tiempo de ejecución hasta echar a perder algo a nivel externo en el tiempo o el espacio. Un estilo de codificación razonable consiste en realizar suposiciones explícitas usando tipos o aserciones, lo cual permite que las fallas sean rápidas y limpias; p. ej. con una excepción. Los detalles varían, por supuesto, según el lenguaje de programación. Ejemplos:
Si se prevé que un valor recibido a través de un canal de plataforma tenga un tipo determinado, asígnalo de inmediato a una variable de ese tipo.
Si se prevé que un valor recibido a través de un canal de la plataforma sea no nulo, puedes configurar todo para que se eliminen las referencias de inmediato o afirmar que es no nulo antes de almacenarlo para más adelante. Según tu lenguaje de programación, podrías asignarlo a una variable de un tipo que no admita nulidad.
Dos ejemplos simples:
// Dart: we expect to receive a non-null List of integers.
for (final int n inawait channel.invokeMethod('getFib', 100)) {
print(n * n);
}
// Android: we expect non-null name and age arguments for
// asynchronous processing, delivered in a string-keyed map.
channel.setMethodCallHandler { call, result ->
when (call.method) {
"bar" -> {
val name : String = call.argument("name")
val age : Int = call.argument("age")
process(name, age, result)
}
else -> result.notImplemented()
}
}
:
fun process(name: String, age: Int, result: Result) { ... }
El código Android explota el método de argumento T (clave String), escrito genéricamente como <T>, de MethodCall que busca la clave en los argumentos, que se prevén como un mapa, y transmite el valor encontrado al tipo de destino (sitio de llamada). Si esto falla por cualquier motivo, se produce una excepción adecuada. Al producirse en un controlador de llamadas a métodos, se registraría y se enviaría un resultado de error a Dart.
No remedar canales de plataforma
(Juego de palabras intencionado). Al escribir pruebas de unidad para código Dart que usa canales de plataforma, una reacción automática puede ser simular el objeto de canal, lo que se haría con una conexión de red.
Puedes hacerlo, pero no es necesario simular los objetos de canal para que funcionen correctamente en las pruebas de unidad. Como alternativa, puedes registrar controladores de mensajes o métodos simulados para que cumplan la función de la plataforma durante una prueba determinada. Aquí te mostramos una prueba de unidad de una función hello que se supone que invoca el método de barra en el canal foo:
Para probar código que configure controladores de mensajes o métodos, puedes sintetizar mensajes entrantes usando BinaryMessages.handlePlatformMessage. Actualmente, este método no se duplica en los canales de plataforma, aunque se podría hacer fácilmente como se indica en el código siguiente. El código define una prueba de unidad de una clase Hello que se supone que recopila argumentos entrantes de llamadas a la barra de método en el canal foo, mientras muestra saludos:
test('collects incoming arguments', () async {
const channel = MethodChannel('foo');
final hello = Hello();
final String result = await handleMockCall(
channel,
MethodCall('bar', 'world'),
);
expect(result, contains('Hello, world'));
expect(hello.collectedArguments, contains('world'));
});
// Could be made an instance method on class MethodChannel.
Future<dynamic> handleMockCall(
MethodChannel channel,
MethodCall call,
) async {
dynamic result;
await BinaryMessages.handlePlatformMessage(
channel.name,
channel.codec.encodeMethodCall(call),
(ByteData reply) {
if (reply == null)
throw MissingPluginException();
result = channel.codec.decodeEnvelope(reply);
},
);
return result;
}
En los dos ejemplos anteriores se declara el objeto del canal en la prueba de unidad. Esto funciona bien, a menos que te preocupes por la duplicación del nombre del canal y el códec, ya que todos los objetos del canal con el mismo nombre y códec son equivalentes. Puedes evitar la duplicación al declarar el canal como const en algún lugar visible para tu código de producción y la prueba.
Lo que no necesitas es proporcionar una manera de inyectar un canal simulado en tu código de producción.
Considerar las pruebas automatizadas para tu interacción con plataformas
Los canales de plataforma son bastante simples, pero para hacer que todo funcione desde tu IU de Flutter a través de una Dart API personalizada respaldada por una implementación Java/Kotlin y Objective-C/Swift independiente se requiere atención. A su vez, en la práctica, para mantener la configuración funcionando mientras realizas cambios en tu app se necesitarán pruebas automatizadas a fin de brindar protección contra regresiones. Esto no se puede lograr solo con la prueba de unidades, ya que debes ejecutar una app real para los canales de plataforma a fin de poder comunicarte con ella.
Flutter incluye el marco de trabajo para pruebas de integración de flutter_driver, el cual te permite probar aplicaciones de Flutter que se ejecutan en dispositivos reales y emuladores. Pero flutter_driver no está integrado actualmente con otros marcos de trabajo para permitir pruebas en Flutter y en los componentes de plataforma. Confío en que esta es un área que Flutter mejorará en el futuro.
En algunas situaciones, puedes usar flutter_driver tal como está para probar tu uso de canales de plataforma. Esto requiere que tu interfaz de usuario de Flutter se pueda usar para activar cualquier interacción con la plataforma y que luego se actualice con suficiente detalle para permitir que tu prueba constate el resultado de la interacción.
Si no estás en esa situación, o si empaquetas tu uso de canales de plataforma como un complemento de Flutter para el que quieres una prueba de módulo, puedes escribir una app de Flutter sencilla para pruebas. Esa app debe tener las características anteriores y debe poder ejecutarse usando flutter_driver. Encontrarás un ejemplo en el repositorio de GitHub de Flutter.
Mantener la plataforma lista para llamadas síncronas entrantes
Los canales de la plataforma son únicamente asíncronos. Sin embargo, hay algunas API de plataforma que realizan llamadas síncronas a los componentes de tu app host y les solicitan información o ayuda, u ofrecen diferentes oportunidades. Un ejemplo es Activity.onSaveInstanceState, en Android. La cualidad “síncrono” implica que todo debe realizarse antes de que se muestre la llamada entrante. Ahora quizá quieras incluir información de Dart en ese procesamiento, pero es muy tarde para comenzar a enviar mensajes asíncronos una vez que la llamada síncrona está activa en el subproceso principal de la IU.
El enfoque que usa Flutter, de forma más notoria para información sobre accesibilidad y semántica, consiste en enviar de forma proactiva información actualizada (o actualizaciones) a la plataforma siempre que la información cambie en Dart. Luego, cuando llega la llamada síncrona, la información de Dart ya está presente y disponible para el código de la plataforma.
En el sitio web flutter.io se ofrece documentación sobre cómo usar canales de métodos y las conversiones de valores de Dart, Android e iOS que intervienen en el uso del códec de métodos estándar.
The Boring Flutter Development Show, Episodio 6: Packages and plugins es un video de YouTube en el que se muestra, en tiempo real, la implementación de un complemento de Flutter con canales de plataforma.
El repositorio flutter/plugins de GitHub contiene varios ejemplos de uso de canales de plataforma para implementar complementos de Flutter. El código se encuentra en la subcarpeta packages, organizada por complemento. Cada complemento incluye una app de ejemplo completa.