Lecciones de interoperabilidad y la API de Compose aprendidas durante la compilación de Maps Compose
Este año, lanzamos la biblioteca de Maps Compose, una biblioteca de Jetpack Compose que te permite agregar Google Maps a tu app. En esencia, Maps Compose es una biblioteca de interoperabilidad para el SDK de Maps para Android que expone API compatibles con Compose. A fin de crear los elementos que admiten composición de Maps Compose, debíamos trabajar con las limitaciones de la API existente del SDK de Maps y las funciones de interoperabilidad disponibles de Compose.
Se realizaron diferentes iteraciones de diseño e implementación en Maps Compose antes de llegar a la versión inicial. En esta entrada de blog, hablaré sobre el contexto de Maps Compose: cómo se creó y qué aprendimos durante su desarrollo. Esta entrada es especialmente relevante para desarrolladores de SDK que quieran brindar compatibilidad con Compose. No obstante, también debería serlo para los lectores curiosos sobre el tema. Si entras en alguno de estos segmentos, sigue leyendo.
Contexto
Antes de que Maps Compose estuviera disponible, ya era posible utilizar el SDK de Maps en Compose gracias a sus API de interoperabilidad; en concreto, el elemento que admite composición AndroidView. Los casos de uso sencillos, como la visualización de un mapa con un solo marcador, eran claros, pero para las integraciones algo complejas con muchas personalizaciones y dibujos era necesario escribir una gran cantidad de código de interoperabilidad. Por lo tanto, el uso no era tan simple.
A fin de estimar la utilidad que podría tener una biblioteca de Compose para los desarrolladores de Android, tuiteé lo siguiente:
Para mi sorpresa, muchos desarrolladores respondieron con mensajes como: “¡Sí, por favor!”, “¡Genial!” y “Esto reemplazaría cientos de líneas de código”. 🤯 Como formo parte del grupo de ingenieros de Relaciones con Desarrolladores que están trabajando en el SDK de Maps, me pareció adecuado priorizar el lanzamiento de elementos que admiten composición para el SDK de Maps. A raíz de eso, propuse de forma interna un diseño y una implementación de Maps Compose, y colaboré con Adam Powell, Leland Richardson y Ben Trengrove para llegar a la meta.
Los comentarios que recibimos marcan la diferencia y generan cambios reales en el producto. ¡Nos encanta conocer tu opinión!
Lecciones aprendidas
Lección n.º 1: Reutiliza las clases en el SDK de Maps básico
El SDK de Maps existe hace más de 10 años y muchas apps lo usan. Mientras que Compose presenta una forma totalmente distinta de compilar IU, una versión que admite composición de mapas aún debería parecer familiar. Si tienes conocimientos sobre Compose y usaste el SDK de Maps en el pasado, los mapas componibles deberían resultarte intuitivos.
Para lograr que la API sea intuitiva, reutilizamos las clases subyacentes del SDK de Maps cuando fue posible. Por ejemplo, para llevar a cabo actualizaciones de la cámara en Maps Compose, puedes seguir empleando la clase CameraUpdate existente creada a partir del objeto CameraUpdateFactory.
No obstante, hubo algunas instancias en las que las clases existentes del SDK de Maps no podían usarse tal como estaban. Por ejemplo, no tenía sentido reutilizar la clase UiSettings, ya que solo se puede recuperar una vez que se creó el mapa. Idealmente, deberías poder crear una instancia de esta clase y pasarla al elemento componible GoogleMap. A modo de solución, se duplicó la clase en un tipo de Maps Compose, MapUiSettings. El nombre se asemeja al de la clase existente UiSettings, con el prefijo “Map” para ayudar con la visibilidad de la API. MapUiSettings tiene iguales propiedades y valores predeterminados que UiSettings. La diferencia es que se puede crear y pasar en el elemento componible GoogleMap, en lugar de obtenerlo y mutarlo desde otra superficie de control:
Hay otras propiedades del mapa que se pueden cambiar durante el tiempo de ejecución (por ejemplo, setBuildingsEnabled(boolean)). Una consideración que tuvimos fue exponer estas propiedades como parámetros individuales en el elemento componible GoogleMap. Sin embargo, eso aumentaba de forma significativa la cantidad de parámetros, ya que hay muchas propiedades que se pueden activar o desactivar. En cambio, elegimos crear una clase separada, MapProperties, que contiene estas propiedades configuradas durante el tiempo de ejecución:
En el caso de los objetos que se pueden dibujar en el mapa (marcadores, polilíneas, etc.), el enfoque imperativo basado en View es llamar a métodos add* como GoogleMap.addMarker(MarkerOptions) en el objeto GoogleMap. Para convertir esto en Compose, el elemento componible GoogleMap podría aceptar un parámetro de lista correspondiente a cada dibujo. No obstante, podría ser difícil usar esta API para integraciones que contengan muchos objetos dibujados con lógica compleja. En cambio, decidimos exponer una API compatible con Compose, una lambda de contenido genérica en la que se pueda invocar a los dibujos como elementos componibles separados.
¿Necesitas dibujar un marcador, una polilínea o algún otro objeto compatible en el mapa? Llama al marcador, a la polilínea o a alguna otra función que admita composición de decorador en la lambda de contenido del elemento GoogleMap componible de esta manera:
Lección n.º 2: Aprovecha las funciones de Kotlin
El SDK de Maps se desarrolló antes de que Kotlin fuera un lenguaje de primera clase destinado a escribir apps para Android. Además, el SDK de Maps se desarrolló sobre todo en Java. Por otro lado, Jetpack Compose se escribió en su totalidad en Kotlin y depende bastante de expresiones idiomáticas de Kotlin. Para Maps Compose, también decidimos acercarnos a funciones del lenguaje de Kotlin, como las corrutinas, para exponer una API idiomática de Kotlin.
Por ejemplo, Compose usa las funciones de suspensión de corrutinas de Kotlin para las API de animación. Por lo tanto, tenía sentido ofrecer una API similar para las API subyacentes basadas en devoluciones de llamada en el SDK de Maps. Por ejemplo, con un permiso de corrutina, se puede animar la cámara y esperar a que se complete.
Consulta la lista de otras expresiones idiomáticas de Kotlin que se usan ampliamente en Compose en Kotlin para Jetpack Compose.
Lección n.º 3: Mantén la coherencia con otras API de kit de herramientas de Compose
Mantener la coherencia con otras API de kit de herramientas de Compose posibilita una buena ergonomía de los desarrolladores. De ese modo, es más sencillo usar las funciones y más rápido desarrollar con ellas, ya que sigue una convención familiar con otras API. Ya sea que desarrolles bibliotecas o apps, esta coherencia es esencial para fomentar la facilidad de uso. Los lineamientos de la API de Compose son un excelente recurso para aprender las convenciones que siguen las API de Compose.
Estos son algunos patrones que se detallan en los lineamientos que adopta Maps Compose:
- El elemento componible GoogleMap cumple con lo detallado en Los elementos aceptan un parámetro Modifier y lo respetan.
- El elemento componible MarkerInfoWindow cumple con este punto, detallado en Diseños de IU de Compose: “Las funciones de diseño deben colocar su parámetro de función @Composable principal o más común [debe ser contenido con nombre] en la última posición para permitir el uso de la sintaxis de expresión lambda final de Kotlin”.
Hubo algunas instancias en las que mis diseños iniciales diferían de estos lineamientos y me resultó muy útil hacer referencia a ellos para ajustar las decisiones sobre la API a fin de alinearme mejor con las prácticas recomendadas de Compose.
Lección n.º 4: Las clases sin formato son la mejor opción para la compatibilidad con objetos binarios
Las clases de datos de Kotlin son una forma eficiente de contener datos. Ofrecen diferentes métodos generados que no necesitas escribir, lo que te ahorra bastantes líneas de código por clase. Sin embargo, si escribes una biblioteca, las clases de datos tienen un costo oculto, ya que los cambios futuros en ellas rompen la compatibilidad con objetos binarios. Si agregas nuevas propiedades, cambiará la firma de método generado para copy() y, según dónde se haya agregado la nueva propiedad, también podrían romperse las funciones de desestructuración, lo que alejaría a los usuarios. A fin de solucionar el problema, Maps Compose usa clases sin formato para MapUiSettings y MapProperties. Felicito a Jake Wharton por señalar esto en la entrada de blog Desafíos de la API pública en Kotlin.
Lección n.º 5: Usa tipos comunes de Compose
Para personalizar los colores de un objeto dibujado en el SDK de Maps, le proporcionas un valor entero de color. Por ejemplo, para personalizar el color de relleno de un elemento Circle, le proporcionas un valor entero de color cuando construyes el objeto CircleOptions. En cambio, el elemento Circle de Maps Compose usa la clase Color que brinda Compose.
Una de las funciones geniales de Compose es su compatibilidad integrada para aplicar temas de Material en tu app. Entonces, si usas la clase Color que brinda Compose, cuando los elementos componibles de GoogleMap y sus elementos secundarios se usan dentro de un MaterialTheme, los colores se adaptan automáticamente a los colores de tu tema cuando la apariencia del sistema cambia al modo claro o al modo oscuro.
Lección n.º 6: Las subcomposiciones son potentes
En las primeras etapas del desarrollo, descubrimos que agregar y quitar decoraciones de mapas mediante las API de efecto secundario a lo largo del tiempo para que coincidieran con el modelo de datos de la app era tedioso y podía generar errores. La necesidad de administrar un árbol de elementos es, en efecto, el mismo problema que administrar un árbol de IU que admite composición. Por lo tanto, una solución más eficaz sería usar las mismas herramientas subyacentes para actualizar de forma directa los elementos a medida que cambia el estado con el tiempo. Este método resultó ser mucho más claro e intuitivo que utilizar efectos secundarios.
Para lograrlo, usamos la clase Applier y el elemento componible ComposeNode a fin de brindar compatibilidad con la API basada en elementos secundarios (lambda de contenido) que permite agregar objetos dibujados (marcadores, polilíneas, polígonos, etc.) en el mapa. La implementación que obtenemos crea una nueva subcomposición que administra el estado del mapa, en lugar de los nodos de IU de Compose.
Si tomamos un marcador como ejemplo, con la subcomposición podemos garantizar que se actualice el estado del mapa a medida que ocurre la recomposición. Por ejemplo, si un elemento componible Marker estaba en la composición y se quitó después, podemos aprovechar el método de eliminación de nodos apropiado para asegurarnos de que el objeto subyacente Marker del SDK de Maps también se quite del mapa.
Si quieres examinar el código, consulta la implementación de MapApplier y Marker para obtener más información sobre cómo usar estas API.
Conclusión
En general, me impresionó cómo las API disponibles de interoperabilidad de Compose hicieron posible la compatibilidad con el SDK de Maps en Compose. Si bien sería ideal una implementación nativa del SDK de Maps, Maps Compose cierra la brecha para muchos desarrolladores de Maps que usan Compose.
Espero que esta entrada te haya resultado esclarecedora, y que hayas aprendido algo sobre el diseño de API de Compose y cómo lograr que Compose funcione con el código de View existente.
Si recién comienzas a usar Compose o Maps Compose, consulta las apps de ejemplo para obtener más información:
Si hay algún tema que quieras ver en el futuro, deja un comentario más abajo.