Instructivos

Presentamos Cahier: Un nuevo ejemplo de GitHub para Android para la productividad y la creatividad en pantallas grandes

Lectura de 11 min
Chris Assigbe
Ingeniera de Relaciones con Desarrolladores

La API de Ink ahora está en beta y lista para integrarse en tu app. Este hito fue posible gracias a los valiosos comentarios de los desarrolladores, que permitieron realizar mejoras continuas en el rendimiento, la estabilidad y la calidad visual de la API.

Las apps de Google, como Documentos de GooglePixel StudioGoogle FotosChrome PDFYoutube Effect Maker y las funciones únicas en Android, como Buscar con un círculo, usan las APIs más recientes. 

Para celebrar este hito, nos complace anunciar el lanzamiento de Cahier, una muestra integral de una app para tomar notas optimizada para dispositivos Android de todos los tamaños, en especial tablets y teléfonos plegables.

¿Qué es Cahier?

Cahier (“cuaderno” en francés) es una app de ejemplo diseñada para demostrar cómo puedes compilar una aplicación que permita a los usuarios capturar y organizar sus ideas combinando texto, dibujos e imágenes. 

El ejemplo puede servir como referencia para mejorar la productividad y la creatividad de los usuarios en pantallas grandes. Muestra las prácticas recomendadas para crear este tipo de experiencias, lo que acelera la comprensión de los desarrolladores y la adopción de las potentes APIs y técnicas relacionadas. En esta entrada del blog, se explican las funciones principales de Cahier, las APIs clave y las decisiones de arquitectura que hacen que la muestra sea una excelente referencia para tus propias apps.

Las funciones clave que se demuestran en la muestra incluyen las siguientes:

  • Creación de notas versátil: Muestra cómo implementar un sistema de creación de contenido flexible que admita varios formatos dentro de una sola nota, incluidos texto, dibujos de formato libre y archivos adjuntos de imágenes.
  • Herramientas de escritura creativa: Implementa una experiencia de dibujo de alto rendimiento y baja latencia con la API de Ink. La muestra proporciona un ejemplo práctico de la integración de varios pinceles, un selector de color, la funcionalidad de deshacer/rehacer y una herramienta de borrador.
  • Integración de contenido fluido con la función de arrastrar y soltar: Muestra cómo controlar el contenido entrante y saliente con la función de arrastrar y soltar. Esto incluye aceptar imágenes que se suelten desde otras apps y permitir que los usuarios arrastren contenido fuera de tu app para compartirlo sin problemas.
  • Organización de notas: Marca notas como favoritas para acceder a ellas rápidamente. Filtra la vista para mantener el orden.
  • Arquitectura que prioriza la condición sin conexión: Se creó con una arquitectura que prioriza la condición sin conexión con Room, lo que garantiza que todos los datos se guarden de forma local y que la app siga siendo completamente funcional sin conexión a Internet.
  • Compatibilidad potente con Multiventana e instancias múltiples: Muestra cómo admitir instancias múltiples, lo que permite que tu app se inicie en varias ventanas para que los usuarios puedan trabajar en diferentes notas una al lado de la otra, lo que mejora la productividad y la creatividad en pantallas grandes.
  • IU adaptable para todas las pantallas: La interfaz de usuario se adapta sin problemas a diferentes tamaños y orientaciones de pantalla con ListDetailPaneScaffoldNavigationSuiteScaffold para proporcionar una experiencia del usuario optimizada en teléfonos, tablets y dispositivos plegables.
  • Integración profunda en el sistema: Proporciona una guía para convertir tu app en la app predeterminada para tomar notas en Android 14 y versiones posteriores. Para ello, responde a los intents de Notas en todo el sistema, lo que permite capturar contenido rápidamente desde varios puntos de entrada del sistema.

Diseñado para la productividad y la creatividad en pantallas grandes

Para el lanzamiento inicial, centraremos el anuncio en algunas funciones principales que hacen de Cahier un recurso de aprendizaje clave para los casos de uso de productividad y creatividad.

Una base de adaptabilidad

Cahier se creó para ser adaptable desde cero. La muestra utiliza la biblioteca material3-adaptive específicamente ListDetailPaneScaffoldNavigationSuiteScaffold para adaptar sin problemas el diseño de la app a varios tamaños y orientaciones de pantalla. Este es un elemento fundamental para una app para Android moderna, y Cahier proporciona un ejemplo claro de cómo implementarlo de manera eficaz.

Cahier adaptive UI built with Material 3 Adaptive library..gif

IU adaptable de Cahier compilada con la biblioteca adaptable de Material 3

Presentación de las APIs y las integraciones clave

El ejemplo se enfoca en mostrar potentes APIs de productividad que puedes aprovechar en tus propias aplicaciones, incluidas las siguientes:

Análisis detallado de las APIs clave

Analicemos en detalle dos de las APIs fundamentales que Cahier integra para ofrecer una experiencia de toma de notas de primer nivel.

Cómo crear experiencias de escritura a mano natural con la API de Ink

La entrada con lápiz óptico transforma los dispositivos de pantalla grande en cuadernos digitales. Para ayudarte a crear experiencias de escritura a mano fluidas y naturales, convertimos la API de Ink en la piedra angular de la muestra. La API de Ink facilita la creación, la renderización y la manipulación de trazos de tinta atractivos con la mejor latencia baja de su clase.

La API de Ink ofrece una arquitectura modular, por lo que puedes adaptarla a la pila y las necesidades específicas de tu app. Los módulos de la API incluyen lo siguiente:

  • Módulos de creación (Compose - vistas): Controla la entrada de escritura en tiempo real para crear trazos suaves con la latencia más baja que pueda proporcionar un dispositivo.
    • En DrawingSurface, Cahier usa el elemento InProgressStrokes componible recientemente introducido para controlar la entrada táctil o con lápiz óptico en tiempo real. Este módulo es responsable de capturar eventos de puntero y renderizar trazos de tinta húmeda con la menor latencia posible.
  • Módulo Trazos: Representa la entrada de tinta y su representación visual. Cuando un usuario termina de dibujar una línea, la devolución de llamada onStrokesFinished proporciona un objeto Stroke finalizado o seco a la app. Este objeto inmutable, que representa el trazo de tinta completado, se administra en DrawingCanvasViewModel.
  • Módulo de renderización: Muestra de manera eficiente los trazos de tinta, lo que permite combinarlos con Jetpack Composevistas de Android.
  • Módulos de pincel (Compose - vistas): Proporcionan una forma declarativa de definir el estilo visual de los trazos. Las actualizaciones recientes (desde la versión alpha03) incluyen un nuevo pincel de línea punteada, que es especialmente útil para funciones como la selección con lazo. DrawingCanvasViewModel contiene el estado de currentBrush. Una caja de herramientas en DrawingCanvas permite a los usuarios seleccionar diferentes familias de pinceles (como StockBrushes.pressurePen()StockBrushes.highlighter()) y cambiar los colores. El ViewModel actualiza el objeto Brush, que luego usa el elemento InProgressStrokes componible para los trazos nuevos.
  • Módulos de geometría (Compose - vistas): Admiten la manipulación y el análisis de trazos para funciones como borrar y seleccionar.
    • La herramienta de borrador dentro de la caja de herramientas y la funcionalidad en DrawingCanvasViewModel dependen del módulo de geometría. Cuando el borrador está activo, crea un MutableParallelogram alrededor de la ruta del gesto del usuario. Luego, la goma de borrar verifica las intersecciones entre la forma y los cuadros delimitadores de los trazos existentes para determinar qué trazos borrar, lo que hace que la goma de borrar se sienta intuitiva y precisa.
  • Módulo Storage: Proporciona capacidades eficientes de serialización y deserialización para los datos de tinta, lo que genera ahorros significativos en el tamaño del disco y la red. Para guardar los dibujos, Cahier persiste los objetos Stroke en su base de datos de Room. En Converters, la muestra usa la función encode del módulo de almacenamiento para serializar el StrokeInputBatch (los datos de puntos sin procesar) en un ByteArray. El array de bytes, junto con las propiedades del pincel, se guarda como una cadena JSON. La función decode se usa para reconstruir los trazos cuando se carga una nota.
orion.png

Más allá de estos módulos principales, las actualizaciones recientes expandieron las capacidades de la API de Ink:

  • Las nuevas APIs experimentales para objetos BrushFamily personalizados permiten a los desarrolladores crear tipos de pincel creativos y únicos, lo que brinda la posibilidad de crear herramientas como los pinceles LápizPuntero láser.

Cahier aprovecha pinceles personalizados, incluido el pincel musical único que se muestra a continuación, para ilustrar posibilidades creativas avanzadas.

Rainbow laser created with Ink API's custom brushes..gif

Láser de arcoíris creado con los pinceles personalizados de la API de Ink

notes.png

Pincel musical creado con los pinceles personalizados de la API de Ink

  • Los módulos de interoperabilidad nativos de Jetpack Compose optimizan la integración de las funciones de entrada manuscrita directamente en tus IU de Compose para brindar una experiencia de desarrollo más idiomática y eficiente.

La API de Ink ofrece varias ventajas que la convierten en la opción ideal para las apps de productividad y creatividad en lugar de una implementación personalizada:

  • Facilidad de uso: La API de Ink abstrae las complejidades de los gráficos y la geometría, lo que te permite enfocarte en las funciones principales de Cahier.
  • Rendimiento: La compatibilidad integrada con baja latencia y la renderización optimizada garantizan una experiencia de escritura fluida y responsiva.
  • Flexibilidad: El diseño modular te permite elegir los componentes que necesitas, lo que permite una integración perfecta de la API de Ink en la arquitectura de Cahier.

La API de Ink ya se adoptó en muchas apps de Google, como para el marcado en Documentos y para la función Busca con un círculo, así como en apps de socios, como Orion NotesPDF Scanner.

“La API de Ink fue nuestra primera opción para la función Busca con un círculo (CtS). Gracias a su extensa documentación, la integración de la API de Ink fue muy sencilla, lo que nos permitió alcanzar nuestro primer prototipo funcional en tan solo una semana. La textura y la animación personalizadas del pincel de Ink nos permitieron iterar rápidamente en el diseño de los trazos". - Jordan Komoda, ingeniero de software de Google

Cómo convertirse en la app de notas predeterminada con el rol de notas

Tomar notas es una función principal que mejora la productividad del usuario en dispositivos con pantalla grande. Con la función de rol de notas, los usuarios pueden acceder a tus apps compatibles desde la pantalla de bloqueo o mientras se ejecutan otras apps. Esta función identifica y establece apps predeterminadas para tomar notas en todo el sistema, y les otorga permiso para iniciarse y capturar contenido. 

Implementación en Cahier

La implementación del rol de notas implica algunos pasos clave, todos demostrados en la muestra:

  1. Declaración en el manifiesto: Primero, la app debe declarar su capacidad para controlar intents de toma de notas. En AndroidManifest.xml, Cahier incluye un <intent-filter> para la acción android.intent.action.CREATE_NOTE. Esto le indica al sistema que la app es candidata para el rol de notas.
  2. Verificación del estado del rolSettingsViewModel usa el RoleManager de Android para determinar el estado actual. SettingsViewModel verifica si el rol de notas está disponible en el dispositivo (isRoleAvailable) y si Cahier actualmente tiene ese rol (isRoleHeld). Este estado se expone a la IU con flujos de Kotlin.
  3. Solicitar el rol: En el archivo Settings.kt, se muestra un Button al usuario si el rol está disponible, pero no se mantiene. Cuando se hace clic en el botón, se llama a la función requestNotesRole en el ViewModel. La función crea un intent para abrir la pantalla de configuración predeterminada de la app en la que el usuario puede seleccionar Cahier. El proceso se administra con la API de rememberLauncherForActivityResult, que controla el inicio del intent y la recepción del resultado.
  4. Actualización de la IU: Después de que el usuario regresa de la pantalla de configuración, la devolución de llamada de ActivityResultLauncher activa una función en el ViewModel para actualizar el estado del rol, lo que garantiza que la IU refleje con precisión si la app ahora es la predeterminada.

Obtén más información para integrar el rol de notas en tu app en nuestra guía para crear una app para tomar notas.

helloworld.png

Cahier se inicia en una ventana flotante como la app predeterminada para tomar notas en una tablet Lenovo

Un gran avance: Lenovo habilita el rol de notas

Nos complace anunciar un gran avance en la productividad de Android en pantallas grandes: Lenovo habilitó la compatibilidad con el rol de Notas en tablets que ejecutan Android 15 y versiones posteriores. Con esta actualización, ahora puedes actualizar tus apps para tomar notas y permitir que los usuarios con dispositivos Lenovo compatibles las establezcan como predeterminadas, lo que les otorga acceso sin problemas desde la pantalla de bloqueo y desbloquea las funciones de captura de contenido a nivel del sistema.

Este compromiso de un OEM líder demuestra la creciente importancia del rol de las notas para brindar una experiencia del usuario verdaderamente integrada y productiva en Android. 

Instancias múltiples, multiventana y modo de ventanas de escritorio

La productividad en una pantalla grande se basa en la administración eficiente de la información y los flujos de trabajo. Por eso, Cahier se creó para aprovechar al máximo las capacidades avanzadas de ventanas de Android, lo que proporciona un espacio de trabajo flexible que se adapta a las necesidades del usuario. La app admite lo siguiente:

  • Multiventana: Es la capacidad fundamental de ejecutarse junto con otra app en el modo de pantalla dividida o formato libre. Esto es fundamental para tareas como hacer referencia a una página web mientras se toman notas en Cahier.
  • Instancias múltiples: Aquí es donde se destaca la verdadera capacidad multitarea. Cahier permite a los usuarios abrir varias ventanas independientes de la app de forma simultánea. Imagina comparar dos notas diferentes una al lado de la otra o consultar una nota de texto en una ventana mientras trabajas en un dibujo en otra. Cahier muestra cómo administrar estas instancias separadas, cada una con su propio estado, lo que convierte tu app en una herramienta potente y multifacética.
  • Modo de ventanas de escritorio: Cuando se conecta a una pantalla externa, el modo de escritorio de Android transforma una tablet o un dispositivo plegable en una estación de trabajo. Dado que Cahier se creó con una IU adaptable y admite instancias múltiples, la app funciona de maravilla en este entorno. Los usuarios pueden abrir, cambiar el tamaño y colocar varias ventanas de Cahier como en una computadora tradicional, lo que permite flujos de trabajo complejos que antes no eran posibles en dispositivos móviles.
cahier-desktop-windowing.webp

Cahier ejecutándose en modo de ventana de escritorio en la Pixel Tablet

Así es como implementamos estas funciones en Cahier:

Para habilitar la función de varias instancias, primero debimos indicarle al sistema que la app admite iniciarse varias veces agregando la propiedad PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI a la declaración de MainActivity en AndroidManifest:

<activity

    android:name="com.example.cahier.MainActivity"

    android:exported="true"

    android:label="@string/app_name"

    android:theme="@style/Theme.MyApplication"

    android:showWhenLocked="true"

    android:turnScreenOn="true"

    android:resizeableActivity="true"

    android:launchMode="singleInstancePerTask">


    <property

        android:name="android.window.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI"

        android:value="true"/>

    ...

</activity>

A continuación, implementamos la lógica para iniciar una instancia nueva de la app. En CahierHomeScreen.kt, cuando un usuario elige abrir una nota en una ventana nueva, creamos un Intent nuevo con marcas específicas que le indican al sistema cómo controlar el inicio de la actividad nueva. La combinación de FLAG_ACTIVITY_NEW_TASKFLAG_ACTIVITY_MULTIPLE_TASKFLAG_ACTIVITY_LAUNCH_ADJACENT garantiza que la nota se abra en una ventana nueva y separada junto a la existente.

fun openNewWindow(activity: Activity?, note: Note) {

    val intent = Intent(activity, MainActivity::class.java)

    intent.putExtra(AppArgs.NOTE_TYPE_KEY, note.type)

    intent.putExtra(AppArgs.NOTE_ID_KEY, note.id)

    intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK or

        Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT


    activity?.startActivity(intent)

}

Para admitir el modo multiventana, debíamos indicarle al sistema que la app admite el cambio de tamaño configurando el elemento <activity><application> del manifiesto.

<activity

    android:name="com.example.cahier.MainActivity"

    android:resizeableActivity="true"

    ...>

</activity>

La IU se compila con la biblioteca adaptable de Material 3, lo que le permite adaptarse sin problemas en situaciones de multiventana, como el modo de pantalla dividida de Android. 

Para mejorar la experiencia del usuario, agregamos compatibilidad con la función de arrastrar y soltar. A continuación, se muestra cómo implementamos esto en Cahier.

Arrastrar y soltar

Una app verdaderamente productiva o creativa no funciona de forma aislada, sino que interactúa sin problemas con el resto del ecosistema del dispositivo. Arrastrar y soltar es una piedra angular de esta interacción, especialmente en pantallas grandes en las que los usuarios suelen trabajar en varias ventanas de apps. Cahier adopta por completo este concepto implementando una funcionalidad intuitiva de arrastrar y soltar para agregar y compartir contenido.

  • Importación sin esfuerzo: Los usuarios pueden arrastrar imágenes desde otras aplicaciones, como un navegador web, una galería de fotos o un administrador de archivos, y soltarlas directamente en el lienzo de una nota. Para ello, Cahier usa el modificador dragAndDropTarget para definir una zona de soltar, verificar el contenido compatible (como image/*) y procesar el URI entrante.
  • Compartir simple: El contenido de Cahier es tan fácil de compartir como el de otras apps. Los usuarios pueden mantener presionada una imagen dentro de una nota de texto o mantener presionado todo el lienzo de una nota de dibujo y un compuesto de imágenes, y arrastrarlo a otra aplicación.

Análisis técnico detallado: Cómo arrastrar desde el lienzo de dibujo

Implementar el gesto de arrastre en el lienzo de dibujo presenta un desafío único. En nuestro DrawingSurface, los elementos componibles que controlan la entrada de dibujo en vivo (los InProgressStrokes de la API de Ink) y el Box que detecta el gesto de presión prolongada para iniciar un arrastre son elementos componibles hermanos.

De forma predeterminada, el sistema de entrada de puntero de Jetpack Compose está diseñado para que solo un elemento componible hermano (el primero en el orden de declaración que se superpone a la ubicación del toque) reciba el evento. En el caso de Cahier, queremos que nuestra lógica de control de entrada de arrastrar y soltar tenga la oportunidad de ejecutarse y, potencialmente, consumir entradas antes de que el elemento InProgressStrokes componible use todas las entradas no consumidas para dibujar y, luego, consuma esa entrada. Si no organizamos las cosas en el orden correcto, nuestro cuadro no detectará el gesto de presión prolongada para iniciar un arrastre, o InProgressStrokes no recibirá la entrada para dibujar.

Para resolver este problema, creamos un modificador pointerInputWithSiblingFallthrough personalizado y colocamos nuestro Box con ese modificador antes de InProgressStrokes en el código componible. Esta utilidad es un wrapper delgado alrededor del sistema estándar pointerInput, pero con un cambio fundamental: anula la función sharePointerInputWithSiblings() para devolver true. Esto le indica al framework de Compose que permita que los eventos de puntero pasen a los elementos componibles hermanos, incluso después de que se consuman.

internal fun Modifier.pointerInputWithSiblingFallthrough(

    pointerInputEventHandler: PointerInputEventHandler

) = this then PointerInputSiblingFallthroughElement(pointerInputEventHandler)


private class PointerInputSiblingFallthroughModifierNode(

    pointerInputEventHandler: PointerInputEventHandler

) : PointerInputModifierNode, DelegatingNode() {


    var pointerInputEventHandler: PointerInputEventHandler

        get() = delegateNode.pointerInputEventHandler

        set(value) {

            delegateNode.pointerInputEventHandler = value

        }


    val delegateNode = delegate(

        SuspendingPointerInputModifierNode(pointerInputEventHandler)

    )


    override fun onPointerEvent(

        pointerEvent: PointerEvent,

        pass: PointerEventPass,

        bounds: IntSize

    ) {

        delegateNode.onPointerEvent(pointerEvent, pass, bounds)

    }


    override fun onCancelPointerInput() {

        delegateNode.onCancelPointerInput()

    }


    override fun sharePointerInputWithSiblings() = true

}


private data class PointerInputSiblingFallthroughElement(

    val pointerInputEventHandler: PointerInputEventHandler

) : ModifierNodeElement<PointerInputSiblingFallthroughModifierNode>() {


    override fun create() = PointerInputSiblingFallthroughModifierNode(pointerInputEventHandler)


    override fun update(node: PointerInputSiblingFallthroughModifierNode) {

        node.pointerInputEventHandler = pointerInputEventHandler

    }


    override fun InspectorInfo.inspectableProperties() {

        name = "pointerInputWithSiblingFallthrough"

        properties["pointerInputEventHandler"] = pointerInputEventHandler

    }

}

Así se usa en DrawingSurface:

Box(

    modifier = Modifier

        .fillMaxSize()

        // Our custom modifier enables this gesture to coexist with the drawing input.

        .pointerInputWithSiblingFallthrough {

            detectDragGesturesAfterLongPress(

                onDragStart = { onStartDrag() },

                onDrag = { _, _ -> /* consume drag events */ },

                onDragEnd = { /* No action needed */ }

            )

        }

) 

// The Ink API's composable for live drawing sits here as a sibling.

InProgressStrokes(...)

Con esto, el sistema detecta correctamente los trazos de dibujo y el gesto de arrastrar con presión prolongada de forma simultánea. Una vez que se inicia la acción de arrastrar, creamos un URI content:// para compartir con FileProvider y pasamos el URI al framework de arrastrar y soltar del sistema con view.startDragAndDrop(). Esta solución garantiza una experiencia del usuario sólida e intuitiva, y muestra cómo superar conflictos complejos de gestos en IU en capas.

Construido con arquitectura moderna

Más allá de las APIs específicas, Cahier demuestra patrones arquitectónicos cruciales para compilar aplicaciones adaptables y de alta calidad.

La capa de presentación: Jetpack Compose y adaptabilidad

La capa de presentación se compila por completo con Jetpack Compose. Como se mencionó, Cahier adopta la biblioteca material3-adaptive para la adaptabilidad de la IU. La administración de estados sigue un patrón estricto de flujo unidireccional de datos (UDF), con instancias de ViewModel que se usan como contenedores de datos que contienen información de notas y estado de la IU.

La capa de datos: Repositorios y Room

Para la capa de datos, Cahier usa una interfaz NoteRepository para abstraer todas las operaciones de datos. Esta elección de diseño permite que la app cambie de forma limpia entre una fuente de datos local (Room) y un posible backend remoto futuro. El flujo de datos para una acción como editar una nota es sencillo:

  1. La IU de Jetpack Compose activa un método en el ViewModel.
  2. El ViewModel recupera la nota de NoteRepository, controla la lógica y pasa la nota actualizada al repositorio.
  3. NoteRepository guarda la actualización en una base de datos de Room.

Compatibilidad integral con la entrada de texto

Para ser una verdadera potencia de productividad, una app debe controlar una variedad de métodos de entrada sin problemas. Cahier se creó para cumplir con los lineamientos de entrada para pantallas grandes y admite lo siguiente:

  • Lápiz táctil: Integración con la API de Ink, rechazo de la palma de la mano, registro para el rol de notas, entrada con lápiz táctil en campos de texto y modo envolvente.
  • Teclado: Compatibilidad con las combinaciones y los atajos de teclado más comunes (como Ctrl + clic, Meta + clic) y una indicación clara del enfoque del teclado.
  • Mouse y panel táctil: Compatibilidad con el clic con el botón derecho y los estados de desplazamiento del mouse.

La compatibilidad con interacciones avanzadas de teclado, mouse y panel táctil es un enfoque clave para realizar más mejoras. 

Comienza hoy mismo

Esperamos que Cahier te sirva como plataforma de lanzamiento para tu próxima gran app. La creamos para que sea un recurso integral de código abierto que demuestre cómo combinar una IU adaptable, APIs potentes como Ink y el rol de notas, y una arquitectura moderna y adaptable.

¿Todo listo para comenzar?

  • Explora el código: Visita nuestro repositorio de GitHub para explorar la base de código de Cahier y ver los principios de diseño en acción.
  • Crea tu propia aplicación: Usa Cahier como base para tu propia aplicación creativa, de toma de notas o de marcado de documentos.
  • Contribuir: Agradecemos tus contribuciones. Ayúdanos a hacer de Cahier un recurso aún mejor para la comunidad de desarrolladores de Android.

Consulta las guías oficiales para desarrolladores y comienza a crear tu app de productividad y creatividad de próxima generación hoy mismo. ¡Ya queremos ver tus creaciones!

Escrito por:

Seguir leyendo