Tiempo estimado para completar: 30 minutos, dependiendo de su experiencia con Flutter.
El SDK de dispositivos Atlas para Flutter permite crear aplicaciones multiplataforma con Dart y Flutter. Este tutorial se basa en la aplicación de plantilla de sincronización flexible de Flutter. flutter.todo.flex, que ilustra la creación de una aplicación de tareas pendientes. Esta aplicación permite a los usuarios:
Registrar su correo electrónico como una nueva cuenta de usuario.
Inicie sesión en su cuenta con su correo electrónico y contraseña (y cierre sesión más tarde).
Ver, crear, modificar y eliminar elementos de tarea.
Ver todas las tareas, incluso aquellas en las que el usuario no es el propietario.
La aplicación de plantilla también proporciona un interruptor que simula el dispositivo en modo sin conexión. Este interruptor permite probar rápidamente la sincronización del dispositivo, simulando que el usuario no tiene conexión a internet. Sin embargo, es probable que este interruptor se elimine en una aplicación de producción.
Este tutorial complementa la aplicación de plantilla. Agregará un nuevo priority campo al Item modelo existente y actualizará la suscripción de Sincronización Flexible para mostrar solo elementos dentro de un rango de prioridades.
Objetivos de aprendizaje
Este tutorial ilustra cómo adaptar la aplicación de plantilla a sus necesidades. Dada la estructura actual de la aplicación, no es necesario realizar este cambio.
En este tutorial aprenderás a:
Actualice un modelo de objeto de Realm con un cambio ininterrumpido.
Actualizar una suscripción de sincronización de dispositivos
Agregue un campo consultable a la configuración de sincronización del dispositivo en el servidor para cambiar qué datos se sincronizan.
Si prefieres comenzar con tu propia aplicación en lugar de seguir un tutorial guiado, consulta la Guía deinicio rápido de Flutter. Incluye ejemplos de código copiables y la información esencial necesaria para configurar una aplicación con el SDK de Flutter.
Requisitos previos
Debes tener experiencia previa en la implementación de una aplicación Flutter en un emulador de Android, un simulador de iOS y/o un dispositivo físico.
Este tutorial comienza con una aplicación de plantilla. Necesita una cuenta de Atlas, una clave API y la CLI de App Services para crearla.
Puede obtener más información sobre cómo crear una cuenta de Atlas en la documentación de introducción a Atlas. Para este tutorial, necesita una cuenta de Atlas con un clúster de nivel gratuito.
También necesita una clave API de Atlas para la cuenta de MongoDB Cloud con la que desea iniciar sesión. Debe ser propietario del proyecto para crear una aplicación de plantilla mediante la CLI de App Services.
Para obtener más información sobre la instalación de la CLI de App Services, consulte Instalar la CLI de App Services. Después de la instalación, ejecute el comando de inicio de sesión con la clave API de su proyecto Atlas.
Nota
Plataformas compatibles
Puedes compilar esta aplicación de tutorial en las siguientes plataformas:
iOS
Android
macOS
Windows
Linux
El SDK de Flutter no admite la creación de aplicaciones web.
Comience con la plantilla
Este tutorial se basa en la aplicación de plantilla de sincronización flexible de Flutter llamada flutter.todo.flex. Comenzamos con la aplicación predeterminada y creamos nuevas funciones a partir de ella.
Para obtener más información sobre las aplicaciones de plantilla, consulte Aplicaciones de plantilla.
Si aún no tiene una cuenta Atlas, regístrese para implementar una aplicación de plantilla.
Siga el procedimiento descrito en la guía Crear una aplicación de servicios de aplicaciones y seleccione Create App from TemplateSeleccione la plantilla Real-time Sync. Esto crea una aplicación de Servicios de Aplicaciones preconfigurada para usarla con uno de los clientes de la plantilla de Sincronización de Dispositivos.
Después de crear una aplicación de plantilla, la interfaz de usuario muestra un modal con la etiqueta Get the Front-end Code for your Template. Este modal proporciona instrucciones para descargar el código del cliente de la aplicación de plantilla como un archivo .zip o usar la CLI de App Services para obtener el cliente.
Después de seleccionar el método .zip o la CLI de App Services, siga las instrucciones en pantalla para obtener el código de cliente. Para este tutorial, seleccione el código de cliente Dart (Flutter).
Descomprime la aplicación descargada y verás la aplicación Flutter.
Nota
La utilidad ZIP predeterminada de Windows podría mostrar el archivo .zip vacío. Si esto ocurre, use un programa de compresión de terceros disponible.
El comando de creación de aplicaciones appservices configura el backend y crea una aplicación de plantilla Flutter para que la uses como base para este tutorial.
Ejecute el siguiente comando en una ventana de terminal para crear una aplicación llamada "MyTutorialApp" que se implementa en la región US-VA con su entorno configurado en "desarrollo" (en lugar de producción o control de calidad).
appservices app create \ --name MyTutorialApp \ --template flutter.todo.flex \ --deployment-model global \ --environment development
El comando crea un nuevo directorio en su ruta actual con el mismo nombre que el valor del indicador --name.
Puedes bifurcar y clonar un repositorio de GitHub que contenga el código del cliente de sincronización de dispositivos. El código del cliente de Flutter está disponible en https://github.com/mongodb/template-app-dart-flutter-todo.
Si usa este proceso para obtener el código del cliente, debe crear una aplicación de plantilla para usarla con el cliente. Siga las instrucciones de Crear una aplicación de plantilla para usar la interfaz de usuario de Atlas App Services, la CLI de App Services o la API de administración para crear una aplicación de plantilla de backend de Device Sync.
Clona el repositorio y sigue las instrucciones en README para agregar el ID de la aplicación de backend a la aplicación Flutter.
Configurar la aplicación de plantilla
Abre la aplicación
Abra la aplicación Flutter con su editor de código.
Si descargaste el cliente como archivo .zip o clonaste su repositorio de GitHub, debes insertar manualmente el ID de la aplicación de App Services en el lugar correspondiente del cliente. Sigue las instrucciones Configuration del cliente README.md para saber dónde insertar el ID de la aplicación.
Explorar la estructura de la aplicación
En tu editor de código, dedica unos minutos a explorar la organización del proyecto. Se trata de una aplicación Flutter multiplataforma estándar, modificada para nuestro uso específico. En concreto, los siguientes archivos contienen usos importantes del SDK de Flutter:
Archivo | Propósito |
|---|---|
| Punto de entrada a la aplicación. Contiene enrutamiento y gestión de estado. |
| Define el esquema de la base de datos de Realm. |
| Clase de objeto Realm generada. |
| Maneja la interacción con Atlas App Services. |
| Maneja la interacción con la base de datos Realm y Atlas Device Sync. |
| Partes componentes de una aplicación con widgets de Flutter. |
| Páginas de la aplicación. |
Ejecutar la aplicación
Sin realizar ningún cambio en el código, debería poder ejecutar la aplicación en el emulador de Android, el simulador de iOS, el dispositivo móvil físico o el emulador de escritorio.
Primero, instale todas las dependencias ejecutando lo siguiente en su terminal:
flutter pub get
Luego, conéctelo a un dispositivo y ejecute la aplicación Flutter.
Una vez que la aplicación esté ejecutándose, registre una nueva cuenta de usuario y luego agregue un nuevo elemento a su lista de tareas pendientes.
Tip
Para obtener más información sobre cómo ejecutar una aplicación Flutter con herramientas de desarrollo, consulte la documentación de Flutter Test Drive.
Comprueba el Backend
Inicie sesión en MongoDB Atlas. En la pestaña, haga Data Services clic Browse Collections en. En la lista de bases de datos, busque y expanda la todo base de datos y, a continuación, la Item colección. Debería ver el documento que creó en esta colección.
Modificar la aplicación
Agregar una nueva propiedad
Agregar una nueva propiedad al modelo
Ahora que ha confirmado que todo funciona correctamente, podemos realizar cambios. En este tutorial, hemos decidido añadir una propiedad de "prioridad" a cada elemento para poder filtrarlos según su prioridad.
Para ello siga estos pasos:
En el proyecto
flutter.todo.flex, abra el archivolib/realm/schemas.dart.Agregue la siguiente propiedad a la clase
_Item:lib/realm/schemas.dartlate int? priority; Regenerar la clase de objeto Realm
Item:dart run realm generate
Establecer la prioridad al crear y actualizar elementos
En
lib/realm/realm_services.dart, agrega lógica para establecer y actualizarpriority. También agrega una clase abstractaPriorityLeveldebajo de la claseRealmServicespara restringir los valores posibles.lib/realm/realm_services.dart// ... imports class RealmServices with ChangeNotifier { static const String queryAllName = "getAllItemsSubscription"; static const String queryMyItemsName = "getMyItemsSubscription"; bool showAll = false; bool offlineModeOn = false; bool isWaiting = false; late Realm realm; User? currentUser; App app; // ... RealmServices initializer and updateSubscriptions(), // sessionSwitch() and switchSubscription() methods void createItem(String summary, bool isComplete, int? priority) { final newItem = Item(ObjectId(), summary, currentUser!.id, isComplete: isComplete, priority: priority); realm.write<Item>(() => realm.add<Item>(newItem)); notifyListeners(); } void deleteItem(Item item) { realm.write(() => realm.delete(item)); notifyListeners(); } Future<void> updateItem(Item item, {String? summary, bool? isComplete, int? priority}) async { realm.write(() { if (summary != null) { item.summary = summary; } if (isComplete != null) { item.isComplete = isComplete; } if (priority != null) { item.priority = priority; } }); notifyListeners(); } Future<void> close() async { if (currentUser != null) { await currentUser?.logOut(); currentUser = null; } realm.close(); } void dispose() { realm.close(); super.dispose(); } } abstract class PriorityLevel { static int severe = 0; static int high = 1; static int medium = 2; static int low = 3; } Añade un nuevo archivo que contenga un widget para establecer la prioridad de un elemento. Crea el archivo
lib/components/select_priority.dart.lib/componentes/select_priority.dartimport 'package:flutter/material.dart'; import 'package:flutter_todo/realm/realm_services.dart'; class SelectPriority extends StatefulWidget { int priority; void Function(int priority) setFormPriority; SelectPriority(this.priority, this.setFormPriority, {Key? key}) : super(key: key); State<SelectPriority> createState() => _SelectPriorityState(); } class _SelectPriorityState extends State<SelectPriority> { Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(top: 15), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Priority'), DropdownButtonFormField<int>( onChanged: ((int? value) { final valueOrDefault = value ?? PriorityLevel.low; widget.setFormPriority(valueOrDefault); setState(() { widget.priority = valueOrDefault; }); }), value: widget.priority, items: [ DropdownMenuItem( value: PriorityLevel.low, child: const Text("Low")), DropdownMenuItem( value: PriorityLevel.medium, child: const Text("Medium")), DropdownMenuItem( value: PriorityLevel.high, child: const Text("High")), DropdownMenuItem( value: PriorityLevel.severe, child: const Text("Severe")), ], ), ], ), ); } } Ahora, añade el widget
SelectPrioritya los widgetsCreateItemyModifyItem. También necesitas añadir lógica adicional para gestionar la configuración de la prioridad. El código que debes añadir se muestra a continuación.Algunas secciones de los archivos que estás agregando se reemplazan con comentarios en los siguientes ejemplos de código para enfocarse en las secciones de código relevantes que se modifican.
Edita el widget
CreateItemFormenlib/components/create_item.dart:lib/componentes/create_item.dart// ... other imports import 'package:flutter_todo/components/select_priority.dart'; // ... CreateItemAction widget class CreateItemForm extends StatefulWidget { const CreateItemForm({Key? key}) : super(key: key); createState() => _CreateItemFormState(); } class _CreateItemFormState extends State<CreateItemForm> { final _formKey = GlobalKey<FormState>(); late TextEditingController _itemEditingController; int _priority = PriorityLevel.low; void _setPriority(int priority) { setState(() { _priority = priority; }); } // ... initState() and dispose() @override functions Widget build(BuildContext context) { TextTheme theme = Theme.of(context).textTheme; return formLayout( context, Form( key: _formKey, child: Column( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: <Widget>[ // ... Text and TextFormField widgets SelectPriority(_priority, _setPriority), // ... Padding widget ], ), )); } void save(RealmServices realmServices, BuildContext context) { if (_formKey.currentState!.validate()) { final summary = _itemEditingController.text; realmServices.createItem(summary, false, _priority); Navigator.pop(context); } } } Edita el widget
ModifyItemFormenlib/components/modify_item.dart:lib/componentes/modificar_elemento.dart// ... other imports import 'package:flutter_todo/components/select_priority.dart'; class ModifyItemForm extends StatefulWidget { final Item item; const ModifyItemForm(this.item, {Key? key}) : super(key: key); _ModifyItemFormState createState() => _ModifyItemFormState(item); } class _ModifyItemFormState extends State<ModifyItemForm> { final _formKey = GlobalKey<FormState>(); final Item item; late TextEditingController _summaryController; late ValueNotifier<bool> _isCompleteController; late int? _priority; void _setPriority(int priority) { setState(() { _priority = priority; }); } _ModifyItemFormState(this.item); void initState() { _summaryController = TextEditingController(text: item.summary); _isCompleteController = ValueNotifier<bool>(item.isComplete) ..addListener(() => setState(() {})); _priority = widget.item.priority; super.initState(); } void dispose() { _summaryController.dispose(); _isCompleteController.dispose(); super.dispose(); } Widget build(BuildContext context) { TextTheme myTextTheme = Theme.of(context).textTheme; final realmServices = Provider.of<RealmServices>(context, listen: false); return formLayout( context, Form( key: _formKey, child: Column( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: <Widget>[ // ... Text and TextFormField widgets SelectPriority(_priority ?? PriorityLevel.medium, _setPriority), // ... StatefulBuilder widget Padding( padding: const EdgeInsets.only(top: 15), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ cancelButton(context), deleteButton(context, onPressed: () => delete(realmServices, item, context)), okButton(context, "Update", onPressed: () async => await update( context, realmServices, item, _summaryController.text, _isCompleteController.value, _priority)), ], ), ), ], ))); } Future<void> update(BuildContext context, RealmServices realmServices, Item item, String summary, bool isComplete, int? priority) async { if (_formKey.currentState!.validate()) { await realmServices.updateItem(item, summary: summary, isComplete: isComplete, priority: priority); Navigator.pop(context); } } void delete(RealmServices realmServices, Item item, BuildContext context) { realmServices.deleteItem(item); Navigator.pop(context); } } Ahora, agregue un indicador visual de prioridad en el widget
ItemCarddelib/components/todo_item.dart. Cree un nuevo widget_PriorityIndicatorque proporcione un indicador visual de la prioridad del elemento.Añade un widget
_PriorityIndicatorque acabas de crear al widgetTodoItem.lib/componentes/todo_item.dart// ... imports enum MenuOption { edit, delete } class TodoItem extends StatelessWidget { final Item item; const TodoItem(this.item, {Key? key}) : super(key: key); Widget build(BuildContext context) { final realmServices = Provider.of<RealmServices>(context); bool isMine = (item.ownerId == realmServices.currentUser?.id); return item.isValid ? ListTile( // ... leading property and child content title: Row( children: [ Padding( padding: const EdgeInsets.only(right: 8.0), child: _PriorityIndicator(item.priority), ), SizedBox(width: 175, child: Text(item.summary)), ], ), // ... subtitle, trailing, and shape properties with child content ) : Container(); } // ... handleMenuClick() function } class _PriorityIndicator extends StatelessWidget { final int? priority; const _PriorityIndicator(this.priority, {Key? key}) : super(key: key); Widget getIconForPriority(int? priority) { if (priority == PriorityLevel.low) { return const Icon(Icons.keyboard_arrow_down, color: Colors.blue); } else if (priority == PriorityLevel.medium) { return const Icon(Icons.circle, color: Colors.grey); } else if (priority == PriorityLevel.high) { return const Icon(Icons.keyboard_arrow_up, color: Colors.orange); } else if (priority == PriorityLevel.severe) { return const Icon( Icons.block, color: Colors.red, ); } else { return const SizedBox.shrink(); } } Widget build(BuildContext context) { return getIconForPriority(priority); } }
Ejecutar y probar
Antes de volver a ejecutar la aplicación, realice un reinicio en caliente. Esto garantiza que la sesión de sincronización se reinicie con el nuevo esquema y evita errores de sincronización.
Luego, Iniciar sesión usando la cuenta que creaste anteriormente en este tutorial. Verás el único elemento que creaste anteriormente. Agrega un nuevo elemento, y verás que ahora puedes establecer la prioridad. Elija High para la prioridad y guarde el elemento.
Ahora, regrese a la página de datos del Atlas en su navegador y actualice la Item colección. Debería ver el nuevo elemento con el priority campo agregado y configurado como. También observará que el elemento existente ahora también tiene 1 un priority campo y está configurado como nulo, como se muestra en la siguiente captura de pantalla:

Nota
¿Por qué no se rompió esta sincronizar?
Añadir una propiedad a un objeto Realm no supone un cambio drástico y, por lo tanto, no requiere restablecer el cliente. La aplicación de plantilla tiene habilitado el modo de desarrollo, por lo que los cambios en el objeto Realm del cliente se reflejan en el esquema del servidor. Para obtener más información, consulte Modo de desarrollo y Actualizar el modelo de datos.
Cambiar la suscripción
Ahora que agregamos el campo de prioridad, queremos actualizar la suscripción de sincronización del dispositivo para sincronizar solo los elementos marcados como prioridad alta o grave.
Actualizar la suscripción
En el archivo lib/realm/realm_services.dart, definimos la suscripción de Sincronización Flexible que define qué documentos sincronizamos con el dispositivo y la cuenta del usuario. Actualmente, sincronizamos todos los documentos cuya propiedad owner coincide con el usuario autenticado.
La suscripción actual:
Future<void> updateSubscriptions() async { realm.subscriptions.update((mutableSubscriptions) { mutableSubscriptions.clear(); if (showAll) { mutableSubscriptions.add(realm.all<Item>(), name: queryAllName); } else { mutableSubscriptions.add( realm.query<Item>(r'owner_id == $0', [currentUser?.id]), name: queryMyItemsName); } }); await realm.subscriptions.waitForSynchronization(); }
Ahora vamos a cambiar la suscripción para que solo sincronice elementos de prioridad alta y grave. Como recordará, el campo de prioridad es de tipo int, donde la prioridad más alta ("Grave") tiene un valor de 0 y la más baja ("Baja") tiene un valor de 3.
Podemos realizar comparaciones directas entre un int y la propiedad de prioridad. Para ello, refactorizaremos la consulta de suscripción para incluir elementos cuya prioridad sea menor o igual a PriorityLevel.high (o 1). También asignaremos a la suscripción el nuevo nombre "getMyHighPriorityItemsSubscription".
Actualiza la suscripción para borrar la suscripción antigua y crear una nueva que utilice prioridad:
// ... imports class RealmServices with ChangeNotifier { static const String queryAllName = "getAllItemsSubscription"; static const String queryMyItemsName = "getMyItemsSubscription"; static const String queryMyHighPriorityItemsName = "getMyHighPriorityItemsSubscription"; bool showAll = false; bool offlineModeOn = false; bool isWaiting = false; late Realm realm; User? currentUser; App app; RealmServices(this.app) { if (app.currentUser != null || currentUser != app.currentUser) { currentUser ??= app.currentUser; realm = Realm(Configuration.flexibleSync(currentUser!, [Item.schema])); showAll = (realm.subscriptions.findByName(queryAllName) != null); // Check if subscription previously exists on the realm final subscriptionDoesNotExists = (realm.subscriptions.findByName(queryMyHighPriorityItemsName) == null); if (realm.subscriptions.isEmpty || subscriptionDoesNotExists) { updateSubscriptions(); } } } Future<void> updateSubscriptions() async { realm.subscriptions.update((mutableSubscriptions) { mutableSubscriptions.clear(); if (showAll) { mutableSubscriptions.add(realm.all<Item>(), name: queryAllName); } else { mutableSubscriptions.add( realm.query<Item>( r'owner_id == $0 AND priority <= $1', [currentUser?.id, PriorityLevel.high], ), name: queryMyHighPriorityItemsName); } }); await realm.subscriptions.waitForSynchronization(); } // ... other methods }
Ejecutar y probar
Ejecute la aplicación de nuevo. Inicie sesión con la cuenta que creó anteriormente en este tutorial.
Después de un momento inicial cuando Realm vuelve a sincronizar la colección de documentos, es posible que vea un mensaje de error similar al siguiente:
The following RangeError was thrown building StreamBuilder<RealmResultsChanges<Item>>(dirty, state: _StreamBuilderBaseState<RealmResultsChanges<Item>, AsyncSnapshot<RealmResultsChanges<Item>>>#387c4): RangeError (index): Invalid value: Only valid value is 0: 3
Este error puede ocurrir con el widget StreamBuilder al actualizarse la suscripción. En una aplicación de producción, se podría añadir gestión de errores. Sin embargo, para este tutorial, basta con realizar una actualización rápida y el error desaparecerá.
Ahora deberías ver el nuevo elemento de alta prioridad que creaste.
Tip
Cambiar suscripciones con el modo de desarrollador habilitado
En este tutorial, al cambiar la suscripción y la consulta en el campo de prioridad por primera vez, este se añade automáticamente a la sincronización de dispositivos Collection Queryable Fields. Esto se debe a que la aplicación de plantilla tiene el modo de desarrollo habilitado de forma predeterminada. Si el modo de desarrollo no estuviera habilitado, tendría que añadir manualmente el campo como consultable para usarlo en una consulta de sincronización del lado del cliente.
Para obtener más información, consulte Campos consultables.
Si desea probar la funcionalidad con más detalle, puede crear elementos con diferentes prioridades. Verá que un nuevo elemento con una prioridad menor aparece brevemente en la lista de elementos y luego desaparece. Realm crea el elemento localmente, lo sincroniza con el backend y lo elimina porque no cumple con las reglas de suscripción. Esto se denomina escritura compensatoria.
También notará que el documento que creó inicialmente no está sincronizado porque tiene una prioridad de null. Si desea que este elemento se sincronice, puede editarlo en la interfaz de usuario de Atlas y agregar un valor al campo de prioridad, o bien, puede cambiar su suscripción para incluir documentos con valores nulos. También le asignaremos a la suscripción el nuevo nombre "getUserItemsWithHighOrNoPriority".
class RealmServices with ChangeNotifier { static const String queryAllName = "getAllItemsSubscription"; static const String queryMyItemsName = "getMyItemsSubscription"; static const String queryMyHighPriorityItemsName = "getMyHighPriorityItemsSubscription"; static const String queryMyHighOrNoPriorityItemsName = "getMyHighOrNoPriorityItemsSubscription"; bool showAll = false; bool offlineModeOn = false; bool isWaiting = false; late Realm realm; User? currentUser; App app; RealmServices(this.app) { if (app.currentUser != null || currentUser != app.currentUser) { currentUser ??= app.currentUser; realm = Realm(Configuration.flexibleSync(currentUser!, [Item.schema])); // Check if subscription previously exists on the realm final subscriptionDoesNotExists = realm.subscriptions.findByName(queryMyHighOrNoPriorityItemsName) == null; if (realm.subscriptions.isEmpty || subscriptionDoesNotExists) { updateSubscriptions(); } } } Future<void> updateSubscriptions() async { realm.subscriptions.update((mutableSubscriptions) { mutableSubscriptions.clear(); if (showAll) { mutableSubscriptions.add(realm.all<Item>(), name: queryAllName); } else { mutableSubscriptions.add( realm.query<Item>( r'owner_id == $0 AND priority IN {$1, $2, $3}', [currentUser?.id, PriorityLevel.high, PriorityLevel.severe, null], ), name: queryMyHighPriorityItemsName); } }); await realm.subscriptions.waitForSynchronization(); } // ... other methods }
Nuevamente, cuando se produce un error StreamBuilder la primera vez que abres la aplicación con la nueva suscripción, realiza una actualización rápida para ver los datos esperados.
Conclusión
Agregar una propiedad a un objeto Realm existente es un cambio importante y el modo de desarrollo garantiza que el cambio de esquema se refleje en el lado del servidor.
¿Que sigue?
Lee nuestra documentación del Flutter SDK.
Encuentre publicaciones de blog orientadas a desarrolladores y tutoriales de integración en MongoDB Developer Hub.
Únase a las comunidades de MongoDB en Reddit o Stack Overflow para aprender de otros desarrolladores y expertos técnicos de MongoDB.
Explore proyectos de ingeniería y ejemplos proporcionados por expertos.
Nota
Compartir comentarios
¿Cómo te fue? Usa el Rate this page widget en la parte inferior derecha de la página para evaluar su efectividad. O reporta un problema en el repositorio de GitHub si tuviste algún problema.