<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Yayo Code</title>
        <link>https://yayocode.com/es</link>
        <description>Tutoriales de programación</description>
        <lastBuildDate>Thu, 16 Apr 2026 17:52:37 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>es</language>
        <image>
            <title>Yayo Code</title>
            <url>https://yayocode.com/es/favicon.png</url>
            <link>https://yayocode.com/es</link>
        </image>
        <copyright>Todos los derechos reservados 2026, Yayo Code</copyright>
        <category>Ciencia y Tecnología</category>
        <item>
            <title><![CDATA[Flutter: Ejercicios 2: Ciclos]]></title>
            <link>https://yayocode.com/es/post/oaphvdcdtv0wmzmxhdzj</link>
            <guid>oaphvdcdtv0wmzmxhdzj</guid>
            <pubDate>Mon, 04 Dec 2023 23:59:31 GMT</pubDate>
            <description><![CDATA[En esta sección encontraremos algunos ejercicios básicos de ciclos y listas]]></description>
            <content:encoded><![CDATA[<img src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2Foaphvdcdtv0wmzmxhdzj%2F972fe429-8298-4c80-9d31-192cacc6a26e.png?alt=media&token=31faceb9-4802-4e0e-8cc4-791e80b32233" alt="Cover image"> 
 En esta sección encontraremos algunos ejercicios básicos de ciclos y listas. Todos los ejercicios tienen solución. Recuerda que los ejercicios son para principiantes por lo que el código no está optimizado.

## Ejercicio: Imprimir 10 veces
Crear un programa que dado un texto lo imprima **10** veces. Por ejemplo, dado el texto **Hola mundo** lo deberá mostrar diez veces.

<HideShow 
  shownText="Ocultar solución"
  hiddenText="Mostrar solución"> 

<DartPad 
   id="a10ceea428143c07547c5546844b2aa1" 
   width="100%" 
   height="500px" 
   split="55" 
   instantLoad="true"
/>
</HideShow>
---

## Ejercicio: Imprimir números impares
Crear un programa que dado un número entero positivo imprima todos los números impares desde **1** hasta ese número. Por ejemplo, dado el numero **10** el programa imprimirá los números **1, 3, 5, 7, 9**.

<HideShow 
  shownText="Ocultar solución"
  hiddenText="Mostrar solución"> 
<DartPad 
   id="a9e666f0b2c00fcf2db860c59e641063" 
   width="100%" 
   height="500px" 
   split="45" 
   instantLoad="true"
/>
</HideShow>
---

## Ejercicio: Cuenta regresiva
Crear un programa que dado un número entero positivo imprima la cuenta regresiva de ese número hasta cero. Por ejemplo, dado el numero **10** el programa imprimirá los números **10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0**.


<HideShow 
  shownText="Ocultar solución"
  hiddenText="Mostrar solución"> 
<DartPad 
   id="6cc710f5b03409f05399553223b91b12" 
   width="100%" 
   height="550px" 
   split="45"
   instantLoad="true"
/>
</HideShow>
---

## Ejercicio: Copiar e invertir
Crear un programa que, dada una lista con números, copie los valores a otra lista y los invierta. Por ejemplo, dada la lista **[1, 2, 3, 4, 5]** el programa deberá imprimir:
- La lista original es: **[1, 2, 3, 4, 5]**
- La copia es: **[5, 4, 3, 2, 1]**

<HideShow 
  shownText="Ocultar solución"
  hiddenText="Mostrar solución"> 
<DartPad 
   id="056da71f6a4280439d9d0f86f1d3cbf6" 
   width="100%" 
   height="445px" 
   split="75"
   instantLoad="true"
/>
</HideShow>
---

## Ejercicio: Suma de números pares y nones
Crear un programa que dada una lista de números enteros positivos imprima la suma de los pares y la suma de los nones. Por ejemplo, dados los siguientes números **[3, 2, 3, 4, 5, 6, 1, 7]** la suma de pares es **12**, y la suma de nones es **19**.

<HideShow 
  shownText="Ocultar solución"
  hiddenText="Mostrar solución"> 
<DartPad 
   id="00e9efcaecc79c9dc4eb9a12f9352b3d" 
   width="100%" 
   height="600px" 
   split="75"
   instantLoad="true"
/>
</HideShow>
---

## Ejercicio: Triángulo rectángulo
Crear un programa que dada la altura imprima un triángulo rectángulo como el siguiente:

```Dart
// Altura: 5
*
**
***
****
*****
```

<HideShow 
  shownText="Ocultar solución"
  hiddenText="Mostrar solución"> 
<DartPad 
   id="efc55b62c55d6beb98a39b6bc2f09b63" 
   width="100%" 
   height="550px" 
   split="58"
   instantLoad="true"
/>
</HideShow>
---

## Ejercicio: El Árbol de Navidad
Crear un programa que dada la altura imprima un árbol de navidad como el siguiente:

```Dart
// Altura: 5
    *
   ***
  *****
 *******
*********
```

<HideShow 
  shownText="Ocultar solución"
  hiddenText="Mostrar solución"> 
<DartPad 
   id="17402cde24af48d7d8bf993a65f8f639" 
   width="100%" 
   height="550px" 
   split="75"
   instantLoad="true"
/>
</HideShow>
---

## Ejercicio: El Árbol de Navidad con números
Crear un programa que dada la altura imprima un árbol de navidad como el siguiente:

```Dart
// Altura: 5
    1
   222
  33333
 4444444
555555555
```

<HideShow 
  shownText="Ocultar solución"
  hiddenText="Mostrar solución"> 
<DartPad 
   id="9ed94a172f65721011ba6fac3ff1b0f7" 
   width="100%" 
   height="565px" 
   split="70"
   instantLoad="true"
/>
</HideShow>
---

## Ejercicio: La sucesión de Fibonacci
Crear un programa que imprima los primeros **'n'** números de Fibonacci. Por ejemplo, si el valor de **'n'** es **10** los números impresos serán: **0, 1, 1, 2, 3, 5, 8, 13, 21, 34**.

<HideShow 
  shownText="Ocultar solución"
  hiddenText="Mostrar solución"> 
<DartPad 
   id="5ffaa5904c6f764ccf91b692e035e8c7" 
   width="100%" 
   height="600px" 
   split="57"
   instantLoad="true"
/>
</HideShow>]]></content:encoded>
            <category>Curso Dart</category>
            <category>Dart</category>
            <category>Flutter</category>
            <enclosure url="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2Foaphvdcdtv0wmzmxhdzj%2F972fe429-8298-4c80-9d31-192cacc6a26e.png?alt=media&amp;token=31faceb9-4802-4e0e-8cc4-791e80b32233" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Cubit en Práctica: Creando una Aplicación de Noticias Responsiva y con Soporte de Múltiples Idiomas. Parte 3]]></title>
            <link>https://yayocode.com/es/post/r1ym2otazt5mc0z8e91n</link>
            <guid>r1ym2otazt5mc0z8e91n</guid>
            <pubDate>Wed, 25 Oct 2023 15:44:36 GMT</pubDate>
            <description><![CDATA[Descubre cómo crear una app de noticias responsiva y multilingüe con Cubit, con arquitectura escalable y pruebas unitarias y de widgets.]]></description>
            <content:encoded><![CDATA[<img src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2Fr1ym2otazt5mc0z8e91n%2F15ff4733-4486-4335-bc6d-4cdeb1edb31d.png?alt=media&token=f4b7140c-1baf-4635-b6dd-2e40f8ac39cd" alt="Cover image"> 
 Esta es la tercera parte de esta serie de artículos, donde creamos una aplicación de noticias responsiva con soporte para tres idiomas: español, inglés y chino. Además, implementaremos una arquitectura escalable y añadiremos pruebas unitarias y de widgets.

Recuerda que puedes encontrar el código fuente en [GitHub][sourceCode]. Y no olvides visitar los otros artículos de esta serie de tutoriales:

- [Parte 1: Introducción, arquitectura y las fuentes de datos.][parte1] 

- [Parte 2: Inyección de dependencias, repositorios y la lógica de negocios.][parte2]

- [Parte 3: Capa de presentación y soporte de múltiples idiomas.][parte3](este artículo)

### Capa de presentación: La interfaz de usuario

Como mencionamos antes, la capa de presentación se encarga de las interacciones y de lo que el usuario puede ver: botones, imágenes, texto, etc.

Tenemos que crear una aplicación con soporte para tres tamaños de pantalla diferentes:

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2Fr1ym2otazt5mc0z8e91n%2F0ced4830-43bb-4ce3-914f-0a1f4d9940e7.png?alt=media&token=c829fc6a-31ed-4e8d-8078-f59a74f9f4d3"
    width="80%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="1280/496"
    description="App responsive con soporte para tres tamaños"
/>

#### Interfaz de usuario: Computadora

Comenzaremos con el tamaño de pantalla más grande que sería para una computadora de escritorio. Vamos a descomponer la interfaz de usuario en diferentes partes:

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2Fr1ym2otazt5mc0z8e91n%2F18919e08-2f99-41f2-92f6-602604a797a3.png?alt=media&token=8619f788-5980-435b-947e-d201f2825b68"
    width="65%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="817/648"
/>

Podemos identificar tres partes principales:

1. **Barra de búsqueda:** El usuario puede introducir texto para buscar noticias de su interés.

2. **Lista de noticias:** Se compone de varias noticias; al dar clic a una de ellas, veremos los detalles de la noticia.

3. **Detalles de la noticia:** El usuario puede ver los detalles de la noticia, y al dar clic en "ver más", se abre el navegador con la página de la noticia.

Cada una de estas partes se puede descomponer en partes aún más pequeñas, pero por el momento vamos a trabajar con estas tres. Para la barra de búsqueda, vamos a crear un widget llamado `SearchWidget`. Para los detalles de las noticias, creamos un widget llamado `DetailsWidget`, y para cada ítem del listado de noticias, el widget se llamará `ListItem`.

Primero creamos el `SearchWidget`:

```Dart
// En lugar de usar un StatefulWidget, usamos Flutter Hooks.
class SearchWidget extends HookWidget {
  const SearchWidget({
    super.key,
    required this.onChanged,
    required this.onClearPress,
  });

  // "Callback" que se ejecuta cuando presionamos el botón de borrar en la barra de búsqueda.
  final VoidCallback onClearPress;

  // "Callback" que se ejecuta cada vez que el término de búsqueda cambia.
  final ValueChanged<String> onChanged;

  @override
  Widget build(BuildContext context) {
    // Creamos un TextEditingController con Flutter Hooks.
    final controller = useTextEditingController();

    // Estado que nos ayuda a mostrar u ocultar el botón de borrar
    // si en la barra de búsqueda hay texto o no.
    final searchTermEmpty = useState(controller.text.isEmpty);

    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: TextFormField(
        controller: controller,
        onChanged: (text) {

          // Retrasamos la llamada a "onChanged" para evitar múltiples llamadas a la API.
          EasyDebounce.debounce(
            'search-news',
            const Duration(milliseconds: 300),
            () => onChanged.call(text),
          );
          searchTermEmpty.value = controller.text.isEmpty;
        },
        decoration: InputDecoration(
          prefixIcon: const Icon(Icons.search),
          labelText: LocaleKeys.search_news.tr(),
          
          // Ocultamos o mostramos el botón de borrar si hay texto o no.
          suffixIcon: searchTermEmpty.value
              ? null
              : IconButton(
                  onPressed: () {
                    controller.clear();
                    onClearPress.call();
                  },
                  icon: const Icon(Icons.clear),
                ),
          border: const OutlineInputBorder(),
        ),
      ),
    );
  }
}
```

<Note
   title="Aprende más"
   text="
En el widget `SearchWidget` hemos utilizado los siguientes paquetes:

- **[Flutter Hooks](https://pub.dev/packages/flutter_hooks)**: Nos ayuda a eliminar código repetitivo y lo hace más fácil de leer. Aprende más sobre él en este [videotutorial.](https://youtu.be/WYXLNNSqjqs)

- **[EasyDebounce](https://pub.dev/packages/easy_debounce)**: Este paquete evita hacer llamadas excesivas a la API cuando el usuario escribe en la barra de búsqueda.
"/>


Ahora procederemos a crear el `DetailsWidget`:

```Dart
class DetailsWidget extends StatelessWidget {
  
  // En el constructor, inicializamos la noticia de la cual mostraremos los detalles.
  const DetailsWidget({
    Key? key,
    required this.article,
  });

  // La noticia de la que mostraremos los detalles.
  final Article article;

  @override
  Widget build(BuildContext context) {

    // Usamos `double.infinity` en la altura del `SizedBox` para ocupar toda la altura disponible.
    return SizedBox(
      height: double.infinity,
      child: SingleChildScrollView(
        child: Column(
          children: [
            // Si la noticia no tiene imagen, mostramos un contenedor de color rojo;
            // de lo contrario, utilizamos `CachedNetworkImage` para mostrarla.
            article.urlToImage == null
                ? Container(color: Colors.red, height: 250)
                : CachedNetworkImage(
                    width: 450,
                    imageUrl: article.urlToImage!,
                    placeholder: (context, url) => const Center(
                      child: CircularProgressIndicator(),
                    ),
                    errorWidget: (context, url, error) =>
                        const Icon(Icons.error),
                  ),
            Text(
              article.title,
              style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
            ),
            const SizedBox(height: 8),
            Text('${article.content}'),
            const SizedBox(height: 8),

            // Al presionar el botón, abriremos la noticia en el navegador.
            ElevatedButton(
              onPressed: () => launchUrlString(article.url),
              child: const Text('Ver más'),
            ),
            const SizedBox(height: 16),
          ],
        ),
      ),
    );
  }
}
```

Ahora vamos a crear el widget `ListItem`:

```Dart
// Constante de ayuda que define el ancho y la altura de la imagen 
const _imageSize = 120.0;

class ListItem extends StatelessWidget {
  
  // En el constructor, inicializamos la noticia de la que mostraremos los detalles.
  const ListItem({
    Key? key,
    required this.article,
    required this.onTap,
  });

  // La noticia de la que mostraremos los detalles.
  final Article article;

  // Función de devolución de llamada que se ejecuta cuando se toca este widget.
  final GestureTapCallback onTap;

  @override
  Widget build(BuildContext context) {
    // GestureDetector nos ayuda a detectar si este widget ha sido presionado.
    return GestureDetector(
      onTap: onTap,
      child: Card(
        margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
        child: Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // `_Image` es un widget privado que nos ayuda a mostrar la imagen o
            // mostrar un contenedor vacío si la noticia no tiene imagen.
            _Image(url: article.urlToImage),
            Expanded(
              child: Padding(
                padding: const EdgeInsets.all(8.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    // Mostramos el título de la noticia.
                    Text(
                      article.title,
                      maxLines: 2,
                      overflow: TextOverflow.ellipsis,
                      style: const TextStyle(
                        fontWeight: FontWeight.bold,
                        fontSize: 16,
                      ),
                    ),
                    // Si la noticia tiene descripción, la mostramos.
                    if (article.description != null) ...[
                      const SizedBox(height: 16),
                      Text(
                        article.description!,
                        maxLines: 3,
                        overflow: TextOverflow.ellipsis,
                      )
                    ]
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// `_Image` es un widget privado que nos ayuda a mostrar la imagen de la noticia.
class _Image extends StatelessWidget {

  const _Image({
    required this.url,
  });

  final String? url;

  @override
  Widget build(BuildContext context) {
    
    // Si la URL es nula, mostramos un contenedor de color rojo.
    return url == null
        ? Container(
            width: _imageSize,
            height: _imageSize,
            color: Colors.red,
          )
         
        // Si la URL no es nula, utilizamos `CachedNetworkImage` para mostrar la imagen.
        : CachedNetworkImage(
            width: _imageSize,
            height: _imageSize,
            imageUrl: url!,
            fit: BoxFit.cover,
            
            // Mientras la imagen se carga, mostramos un CircularProgressIndicator.
            placeholder: (context, url) => const SizedBox(
              width: _imageSize,
              height: _imageSize,
              child: Center(
                child: CircularProgressIndicator(),
              ),
            ),
            
            // En caso de algún error, mostramos el icono de error.
            errorWidget: (context, url, error) => const Icon(Icons.error),
          );
  }
}
```

Ya hemos construido las tres partes principales: la barra de búsqueda `SearchWidget`, los detalles de la noticia `DetailsWidget`, y el widget que representa cada una de las noticias en la lista de noticias `ListItem`. El siguiente paso es integrar `NewsCubit` con la interfaz de usuario.

### Integrar NewsCubit con la interfaz de usuario

Vamos a crear la pantalla de inicio, que se mostrará cuando los usuarios abran la aplicación. La llamaremos `NewsScreen`.

```Dart
class NewsScreen extends StatelessWidget {
  const NewsScreen({Key? key});

  @override
  Widget build(BuildContext context) {
    
    // Obtenemos el "locale" actual de la aplicación.
    final locale = Localizations.localeOf(context);

    // Usamos BlocProvider para crear un NewsCubit y agregarlo al árbol de widgets.
    return BlocProvider<NewsCubit>(

      // Creamos un NewsCubit con el "locale" actual y llamamos la función searchNews
      // para iniciar la búsqueda en cuanto la pantalla NewsScreen sea visible.
      create: (context) => NewsCubit(locale)..searchNews(),
      child: Scaffold(
        appBar: AppBar(
          title: Text('Aplicación de noticias'),
        ),

        // Usamos BlocBuilder para actuar de acuerdo con los estados de NewsCubit.
        body: BlocBuilder<NewsCubit, NewsState>(
          builder: (context, state) {
            
           // Dependiendo del estado, vamos a devolver diferentes widgets.
           return ?????;

          },
        ),
      ),
    );
  }
}
```

Ahora podemos mostrar la pantalla `NewsScreen` en cuanto la aplicación sea lanzada de la siguiente forma:

```Dart
void main() async {

  // Inyectamos las dependencias al iniciar la aplicación.
  injectDependencies();

  runApp(

    // Creamos un MaterialApp que muestra la pantalla NewsScreen.
    const MaterialApp(
      title: 'Flutter Demo',
      home: NewsScreen(),
    ),

  );
}
```

Dependiendo del estado actual del cubit, debemos devolver un widget diferente en la función `builder` del BlocBuilder:

```Dart
    BlocBuilder<NewsCubit, NewsState>(
      builder: (context, state) {

        // Usamos una columna para alinear los widgets.
        return Column(
          children: [

            // El primer widget es la barra de búsqueda.
            SearchWidget(
              // Cuando el texto cambia, si es mayor de tres caracteres
              // llamamos searchNews para realizar una nueva búsqueda.
              onChanged: (text) {
                context
                    .read<NewsCubit>()
                    .searchNews(search: text.length > 3 ? text : null);
              },

              // Cuando presionamos el botón de borrar, llamamos clearSearch para hacer una nueva búsqueda sin término de búsqueda.                 
              onClearPress: context
                  .read<NewsCubit>()
                  .clearSearch,
            ),

            // Luego tenemos un "Expanded" porque el contenido después de la 
            // barra de búsqueda ocupará el espacio restante.
            Expanded(
              // El "Builder" no es necesario, pero ayuda a usar "if else" de forma más organizada.
              child: Builder(
                builder: (context) {
                  if (state is NewsSuccessState) {
                    // Si el estado es exitoso, mostramos el widget _BigSize.
                    return _BigSize(news: state.news);

                  } else if (state is NewsErrorState) {

                    // Si el estado es de error, mostramos un texto con el error.
                    return Text(state.message);

                  } else {

                    // Si el estado es de carga, mostramos un indicador de carga.
                    return const Center(
                      child: CircularProgressIndicator(),
                    );
                  }
                },
              ),
            ),
          ],
        );
      },
    )
```

Muy bien, hemos implementado el contenido del BlocBuilder pero hay un widget que no hemos creado aun. `_BigSize` es el widget que contiene la estructura de la interfaz de usuario para pantallas grandes. Veamos como implementarlo:

```Dart
class _BigSize extends HookWidget {

  // En el constructor inicializamos la lista de noticias
  const _BigSize({required this.news});

  final List<Article> news;

  @override
  Widget build(BuildContext context) {

    // Usamos hooks para mantener el estado de la noticia seleccionada
    final currentNew = useState(news.firstOrNull);


    // Usamos Row para separar la pantalla en dos. El listado de noticias 
    // del lado izquierdo y los detalles de la noticia del derecho
    return Row(
      children: [
        // El listado de noticias tiene un ancho fijo de 450
        SizedBox(
          width: 450,
          child: ListView.builder(
            itemCount: news.length,

            // Al presionar un item de la lista actualizamos el estado local con la noticia seleccionada
            itemBuilder: (_, int index) => ListItem(
              article: news[index],
              onTap: () => currentNew.value = news[index],
            ),
          ),
        ),
        // Mostramos los detalles de la noticia seleccionada
        if (currentNew.value != null)
          Expanded(
            child: DetailsWidget(
              article: currentNew.value!,
            ),
          ),
      ],
    );
  }
}
```

Ahora podemos correr la aplicacion y ver el resultado. 

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2Fr1ym2otazt5mc0z8e91n%2F18c53949-fb6c-42eb-b935-942e7916ff41.webp?alt=media&token=5558dfb3-85f6-4609-981d-1884b146c2e4"
    width="65%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="640/480"
    description="Aplicación sin soporte para Tabletas o iPads ni teléfonos celulares"
/>

La aplicación se ve bien, podemos seleccionar diferentes noticias, pero el problema es que la aplicación no se adapta a los cambios de pantalla y muestra errores de desbordamiento. Continuemos agregando soporte para otros tamaños de pantalla. 

#### Interfaz de usuario: Tablet o iPad

Es hora de agregar soporte para tabletas y iPad, en este caso el listado de noticias y los detalles van a estar en pantallas diferentes como podemos ver en la siguiente imagen

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2Fr1ym2otazt5mc0z8e91n%2Fef3533ba-460d-4c2f-b946-4c82f6c53f7f.png?alt=media&token=2ca6df97-91c3-4bd3-b900-90939a40ea1d"
    width="75%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="1219/575"
    description="1. Al presionar una noticia del listado navegamos a los detalles de la noticia"
/>
 
Vamos a crear una pantalla nueva que llamare `NewsDetailScreen` y nos servirá para ver los detalles de la noticia:

```Dart

// Definimos un tipo que usaremos para pasar la noticia
// como argumento durante la navegación
typedef NewsDetailScreenArgs = ({Article article});

class NewsDetailScreen extends StatelessWidget {
  const NewsDetailScreen({super.key});

  @override
  Widget build(BuildContext context) {
    // Obtenemos la noticia
    final article = context.args<NewsDetailScreenArgs>().article;

    // Creamos un Scaffold con un AppBar y usamos el widget DetailsWidget
    // para mostrar los detalles de la noticia
    return Scaffold(
      appBar: AppBar(
        title: const Text('News Details'),
      ),
      body: DetailsWidget(article: article),
    );
  }
}
```

En el código anterior podemos ver que la clase `NewsDetailScreen` es muy sencilla ya que estamos reutilizando el widget `DetailsWidget`.

Para la navegación vamos a usar rutas por lo que vamos a crear una nueva clase llamada `Routes` que se encargara de administrar las rutas. 

```Dart
class Routes {
  // Ruta raiz
  static const newsScreen = '/';

  // Ruta de detalles de la noticia
  static const detailsScreen = '/details';

  static Route routes(RouteSettings settings) {

    // Función de ayuda para crear MaterialPageRoute y no
    // tener mucho código repetitivo
    MaterialPageRoute buildRoute(Widget widget) {
      return MaterialPageRoute(builder: (_) => widget, settings: settings);
    }

    // Usamos un switch para navegar a la ruta deseada, si la ruta 
    // no existe lanzamos una excepción
    return switch (settings.name) {
      newsScreen => buildRoute(const NewsScreen()),
      detailsScreen => buildRoute(const NewsDetailScreen()),
      _ => throw Exception('Route does not exists'),
    };
  }
}
```

También tenemos que modificar `MaterialApp` para usar `onGenerateRoute` en lugar de `home`. 

```Dart
    return MaterialApp(
      title: 'Flutter Demo',
      onGenerateRoute: Routes.routes,
    );
```

Muy bien, ya tenemos lista la navegación, pero también tenemos que agregar el soporte para tabletas y iPads, vamos a crear un nuevo widget privado que se encargara de mostrar el listado en pantallas medianas:

```Dart
class _MediumSize extends StatelessWidget {

  // En el constructor inicializamos la lista de noticias
  const _MediumSize({required this.news});

  final List<Article> news;

  @override
  Widget build(BuildContext context) {

    // Creamos el listado de noticias
    return ListView.builder(
      itemCount: news.length,
      itemBuilder: (_, int index) => ListItem(
        article: news[index],
        onTap: () {
          // Al presionar un ítem de la lista vamos a 
          // navegar a la pantalla de detalles
          final args = (article: news[index]);
          Navigator.pushNamed(context, Routes.detailsScreen, arguments: args);
        },
      ),
    );
  }
}
``` 

Y por último para decidir si mostrar el widget `_BigSize` o el widget `_MediumSize` vamos a utilizar un `LayoutBuilder` de la siguiente forma:

```Dart
    return LayoutBuilder(
      builder: (context, constraints) {
        // Si el ancho máximo disponible es menor de 900 unidades entonces
        // mostraremos _MediumSize de lo contrario mostramos _BigSize
        return switch (constraints.maxWidth) {
          < 900 => _MediumSize(news: state.news),
          _ => _BigSize(news: state.news),
        };
      },
    );
```

Si corremos la aplicación y cambiamos el tamaño de la pantalla podemos ver que la interfaz de usuario se adapta correctamente. 


#### Interfaz de usuario: Celular/Móvil

Es hora de agregar soporte para teléfonos celulares. Pero esta parte es una tarea para el lector así que vamos como es la interfaz de usuario:

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2Fr1ym2otazt5mc0z8e91n%2Fcd3fbd5e-df7a-4c41-9a3f-bfd4ee9b004a.png?alt=media&token=34b97aaa-dccd-4545-8827-4308dfdb0a3a"
    width="65%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="661/575"
    description="1. Al presionar una noticia del listado navegamos a los detalles de la noticia"
/>

La interfaz de usuario es muy parecida a la interfaz de usuario para tabletas así que no será muy difícil de crear, pero aquí te dejo unos consejos:

- Hay que crear un nuevo widget para representar cada noticia del listado de noticias

- Hay que crear un nuevo widget para mostrar el listado de noticias. Para tener consistencia en el código yo lo llamaría `_SmallSize`

- Hay que modificar la lógica del `LayoutBuilder` para mostrar el widget `_SmallSize`

- **NO** no hay que crear ninguna pantalla ni rutas nuevas. 

Si tienes algún problema recuerda que el código fuente lo puedes descargar de [Github][sourceCode]. 

### Soporte de múltiples idiomas

Ahora vamos a agregar soporte para múltiples idiomas a la aplicación. Utilizaremos el paquete de [EasyLocalization][easyLocalization] que simplifica el proceso de agregar múltiples idiomas. Aprende más de este paquete en este [videotutorial.][easyLocalizationYoutube]

Primero tenemos que decidir que idiomas queremos soportar, podemos ver la documentación de las dos APIS que vamos a soportar:

1. [/v2/everything:][newsApiEverything] Soporta el parámetro `language` con valores como **es**, **en**, **zh** y muchos más.

2. [/v2/top-headlines:][newsApiTop] Soporta el parámetro `country` con valores como **mx**, **ve**, **us**, **tw** y muchos más.

En esta aplicación solo vamos a agregar algunos idiomas y países. Comenzamos creando una lista de "Locales" suportados:

```Dart
const spanishMx = Locale('es', 'MX'); // Español de México
const spanishVe = Locale('es', 'VE'); // Español de Venezuela
const englishUs = Locale('en', 'US'); // Ingles de EUA
const chineseTw = Locale('zh', 'TW'); // Chino de Taiwán

// Mapa con el nombre de los idiomas soportados
final supportedLocales = <Locale, String>{
  englishUs: 'English - US',
  spanishMx: 'Español - MX',
  spanishVe: 'Español - VE',
  chineseTw: '中文 - TW',
};
```

También necesitamos tres archivos JSON con las traducciones de los idiomas soportados. 

Para ingles tenemos `en.json`

```JSON
{
  "top_headlines": "Top Headlines",
  "search_news": "Search News",
  "view_more": "View more"
}
```

Para español tenemos `es.json`

```JSON
{
  "top_headlines": "Titulares Principales",
  "search_news": "Buscar Noticias",
  "view_more": "Ver más"
}
```

Y para el chino tenemos `zh.json`

```JSON
{
  "top_headlines": "最新的新聞",
  "search_news": "找新聞",
  "view_more": "看更多"
}
```

[EasyLocalization][easyLocalization] soporta generación de código que crea cada una de las llaves en un mapa y es más fácil acceder a ellas desde el código. Por ejemplo:

```Dart
// Sin generación de código podemos comente errores como el siguiente:
Text('top_headLines'.tr());

// ¿Encontraste el error? En vez de escribir 'top_headlines' escribimos 'top_headLines'.

// Pero con generación de código no podemos cometer ningún error:
Text(LocaleKeys.top_headlines.tr())
```

Vamos a correr el siguiente comando para generar todo el código necesario:
```sh
flutter pub run easy_localization:generate -f keys -S resources/translations -O lib/src/localization -o locale_keys.g.dart
flutter pub run easy_localization:generate -S resources/translations -O lib/src/localization
```

<Note
   title="Nota"
   text="
No te preocupes si no entiendes el comando anterior. En él [código fuente](https://tinyurl.com/rytwmvvu) puedes encontrar el archivo **generate_code.sh** y ejecutarlo desde la terminal o consola y generara todo el código necesario.
"/>

Cuando ejecutemos el comando anterior, se van a generar los archivos `codegen_loader.g.dart` y `locale_keys.g.dart` dentro de la carpeta `lib/src/localization`. 

Después de generar el código necesario vamos a inicializar el paquete [EasyLocalization][easyLocalization] para esto vamos a modificar la función `main()` de la siguiente manera:

```Dart
void main() async {

  // Nos aseguramos de inicializar WidgetsFlutterBinding y EasyLocalization
  WidgetsFlutterBinding.ensureInitialized();
  await EasyLocalization.ensureInitialized();

  injectDependencies();
  runApp(const MyApp());
}

```

También tenemos que configurar [EasyLocalization][easyLocalization] y encerrar nuestro `MaterialApp` con el widget `EasyLocalization`:

```Dart
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return EasyLocalization(
      // El idioma de respaldo es el inglés de EUA
      fallbackLocale: englishUs,
      useFallbackTranslations: true,

      // Pasamos la lista de los idiomas que soportamos
      supportedLocales: supportedLocales.keys.toList(),

      // path no es necesario porque estamos usando código generado
      path: 'xxx',

      // Pasamos la clase generada
      assetLoader: const CodegenLoader(),

      // Encerramos MaterialApp con el widget EasyLocalization
      child: const _MaterialApp(),
    );
  }
}

class _MaterialApp extends StatelessWidget {
  const _MaterialApp();

  @override
  Widget build(BuildContext context) {
    // Configuramos MaterialApp para soportar diferentes idiomas
    return MaterialApp(
      title: 'Flutter Demo',
      locale: context.locale,
      onGenerateRoute: Routes.routes,
      debugShowCheckedModeBanner: false,
      supportedLocales: context.supportedLocales,
      localizationsDelegates: context.localizationDelegates,
    );
  }
}
```

Ahora vamos a regresar a la clase `NewsScreen` y hacer dos modificaciones, la primera modificación es usar la extensión de [EasyLocalization][easyLocalization] para obtener el `locale` actual. Así que vamos a reemplazar el siguiente código:

```Dart
// Reemplazamos esta línea de código 
final locale = Localizations.localeOf(context);

// por
final locale = context.locale;
```

La diferencia en el código anterior es que usar la extensión de [EasyLocalization][easyLocalization] es mucho más corto. 

También tenemos que agregar un `PopupMenuButton` que nos permita seleccionar entre los idiomas que soporta nuestra aplicación: 

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2Fr1ym2otazt5mc0z8e91n%2F74dde9c5-cb7a-4f2b-bc28-ba5da26d6194.png?alt=media&token=3fd028c4-67bc-4188-8b63-d84caf73200a"
    width="40%"
    priority="false"
    smallScreenWidth="60%"
    aspectRatio="427/432"
    description="PopupMenuButton para seleccionar el idioma"
/>

Vamos a crean un widget llamado `_LanguagePopUpMenu` que nos permitirá mostrar un menú para poder seleccionar entre los idiomas soportados:

```Dart
class _LanguagePopUpMenu extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Creamos un PopupMenuButton con el icono de traducir
    return PopupMenuButton<Locale>(
      icon: const Icon(Icons.translate),
      itemBuilder: (context) {
        // Iteramos cada uno de los idiomas soportados para crear
        // una lista de PopupMenuItem
        return supportedLocales.entries.map((it) {
          return PopupMenuItem(
            value: it.key,
            child: Text(it.value),
          );
        }).toList();
      },
      // Cada vez que seleccionamos un idioma del meno actualizamos
      // el idioma de la aplicación y del cubit. 
      onSelected: (selectedLocale) {
        context.setLocale(selectedLocale);
        context.read<NewsCubit>().setLocale(selectedLocale);
      },
    );
  }
}
```

Después lo agregaremos al **AppBar** en la propiedad de `actions:`

```Dart
 // Agregamos el widget _LanguagePopUpMenu al AppBar
 appBar: AppBar(
   title: Text(LocaleKeys.top_headlines.tr()),
   actions: [
      _LanguagePopUpMenu(),
   ],
 ),
```

Ahora podemos correr la aplicación y tendremos la opción de seleccionar el idioma lo que hace cambiar el texto de los componentes de la interfaz de usuario y además podremos hacer búsquedas en cualquiera de los idiomas soportados. 

Con esto finalizamos esta serie de artículos así que recuerda que puedes encontrar el código fuente en [GitHub][sourceCode]. Y no olvides visitar los otros artículos de esta serie de tutoriales:

- [Parte 1: Introducción, arquitectura y las fuentes de datos.][parte1] 

- [Parte 2: Inyección de dependencias, repositorios y la lógica de negocios.][parte2]

- [Parte 3: Capa de presentación y soporte de múltiples idiomas.][parte3](este artículo)

[parte1]: /post/j66drgpxoelhslflwkvw
[parte2]: /post/6xxh4e60lq4x8fwwfkhu
[parte3]: /post/r1ym2otazt5mc0z8e91n
[sourceCode]: https://tinyurl.com/rytwmvvu
[easyLocalizationYoutube]: https://youtu.be/XAptuhecO6I
[easyLocalization]: https://pub.dev/packages/easy_localization
[newsApiEverything]: https://newsapi.org/docs/endpoints/everything
[newsApiTop]: https://newsapi.org/docs/endpoints/top-headlines]]></content:encoded>
            <category>Cubit</category>
            <category>Dart</category>
            <category>Flutter</category>
            <enclosure url="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2Fr1ym2otazt5mc0z8e91n%2F15ff4733-4486-4335-bc6d-4cdeb1edb31d.png?alt=media&amp;token=f4b7140c-1baf-4635-b6dd-2e40f8ac39cd" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Cubit en Práctica: Creando una Aplicación de Noticias Responsiva y con Soporte de Múltiples Idiomas. Parte 2]]></title>
            <link>https://yayocode.com/es/post/6xxh4e60lq4x8fwwfkhu</link>
            <guid>6xxh4e60lq4x8fwwfkhu</guid>
            <pubDate>Wed, 25 Oct 2023 15:30:11 GMT</pubDate>
            <description><![CDATA[Descubre cómo crear una app de noticias responsiva y multilingüe con Cubit, con arquitectura escalable y pruebas unitarias y de widgets.]]></description>
            <content:encoded><![CDATA[<img src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2F6xxh4e60lq4x8fwwfkhu%2Fb6529598-6104-4dbb-808e-77c712c187d5.png?alt=media&token=4f674a56-b263-4837-9eae-be7dcca09dd1" alt="Cover image"> 
 Esta es la segunda parte de esta serie de artículos, donde creamos una aplicación de noticias responsiva con soporte para tres idiomas: español, inglés y chino. Además, implementaremos una arquitectura escalable y añadiremos pruebas unitarias y de widgets.

Recuerda que puedes encontrar el código fuente en [GitHub][sourceCode]. Y no olvides visitar los otros artículos de esta serie de tutoriales:

- [Parte 1: Introducción, arquitectura y las fuentes de datos.][parte1] 

- [Parte 2: Inyección de dependencias, repositorios y la lógica de negocios.][parte2](este artículo)

- [Parte 3: Capa de presentación y soporte de múltiples idiomas.][parte3]


## Inyección de dependencias con [GetIt][getIt]

[GetIt][getIt] es un localizador de servicios que nos ayuda a cumplir con el [Principio de Inversión de Dependencia][wikiDI], el cual tiene como objetivo reducir el acoplamiento entre los componentes de software y promover la flexibilidad y la facilidad de mantenimiento.

Vamos a crear un nuevo archivo llamado `dependency_injection.dart`, donde inicializaremos [GetIt][getIt] y crearemos una función de ayuda para inyectar `NewsDataSource`:

```Dart
final getIt = GetIt.instance;

Future<void> injectDependencies() async {
  getIt.registerLazySingleton(() => NewsDataSource());
}
```

Luego, en la función `main()`, llamamos a `injectDependencies()` para registrar las dependencias:

```Dart
void main() async {
 
  // Inyectamos las dependencias al iniciar la aplicación
  injectDependencies();

  runApp(const MyApp());
}
```

¿Por qué debemos usar inyección de dependencias en nuestra aplicación? Más adelante, cuando escribamos pruebas unitarias, podemos crear una clase "Mock", por ejemplo, `MockNewsDataSource`, e inyectarla en las pruebas, lo que nos permitirá simular diferentes escenarios.

## La capa de datos

### Repositorio: NewsRepository

En esta pequeña aplicación, el repositorio solo es un intermediario entre las fuentes de datos y los cubits. 

<Note
   title="Nota"
   text="
Recuerda que, en aplicaciones más grandes, el repositorio tiene varias funciones como gestionar los datos que provienen de la fuente de datos, transformarlos, guardarlos en la memoria caché o en la base de datos local, entre otras funciones  
"/>

Creamos la clase `NewsRepository` con las funciones `fetchEverything` y `fetchTopHeadlines` que llamaran a la fuente de datos para obtener los datos:

```Dart
class NewsRepository {
  // Obtiene la fuente de datos que inyectamos anteriormente
  final NewsDataSource _dataSource = getIt();

  // Llama a la fuente de datos para obtener todas las noticias
  Future<List<Article>> fetchEverything({
    required Locale locale,
    String? search,
  }) =>
      _dataSource.fetchEverything(
        language: locale.languageCode,
        search: search,
      );

  // Llama a la fuente de datos para obtener los últimos encabezados
  Future<List<Article>> fetchTopHeadlines({
    required Locale locale,
  }) =>
      _dataSource.fetchTopHeadlines(
        country: locale.countryCode!,
      );
}
```

Por fin hemos terminado de codificar la capa de datos donde hemos creado la clase NewsDataSource, que actúa como la puerta de enlace a nuestras fuentes de datos externas, y hemos establecido un sólido conjunto de pruebas para garantizar su funcionamiento correcto en diversas situaciones. También a prendimos a utilizar la inyección de dependencias con [GetIt][getIt], lo que nos brinda flexibilidad y facilidad de prueba.

## La capa de la aplicación.

Esta capa es la que se encarga de manejar la lógica de negocio y la interacción directa con los usuarios. Esta capa interactúa directamente con la capa de datos para obtener y guardar información. Esta capa es una parte fundamental para escribir pruebas unitarias ya que la mayoría de la lógica de negocio se encuentra aquí. 

### Lógica de negocios: NewsCubit

La clase `NewsCubit` se encargará de mantener el estado de la interfaz de usuario, así como llamar al repositorio para obtener las noticias que se mostraran al usuario. Pero antes de comenzar a codificar el cubit, veamos los posibles estados de la aplicación:

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2F6xxh4e60lq4x8fwwfkhu%2Fb765dbb0-9e83-47f8-8fd4-427e60c512d0.png?alt=media&token=851daff7-4dca-4ef8-9635-d9ce79c4f156"
    width="75%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="1030/615"
    description="Estados de la aplicación: de carga, exitoso y de error"
/>

- **Estado de carga**: Durante este estado mostraremos una animación de carga en la interfaz de usuario. Durante este estado se está realizando una petición a la API. Lo representaremos con una clase llamada `NewsLoadingState`.

- **Estado exitoso**: Cuando la respuesta de la API es exitosa, entramos al estado exitoso y mostraremos una lista de noticias en la interfaz de usuario. Lo representaremos con una clase llamada `NewsSuccessState`.

- **Estado error**: Cuando la respuesta de la API es fallida, entramos al estado de error y mostramos el mensaje de error en la interfaz de usuario. Este estado lo representaremos con una clase llamada `NewsErrorState`.

Cuando creamos un [cubit][cubit] es necesario indicar el tipo del estado, por ejemplo, si el estado es un numero como en el ejemplo del contador el cubit se crea con el tipo `int`: 

```Dart
class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);
}
```

Pero nosotros tenemos tres estados que son: `NewsLoadingState`, `NewsSuccessState` y `NewsErrorState` ¿cómo podemos asignarle al cubit los tres?. La respuesta es usando herencia:

```Dart

// Todos los estados heredan de la clase NewsState 
sealed class NewsState extends Equatable {
  @override
  List<Object> get props => [];
}

// Estado de carga
class NewsLoadingState extends NewsState {}

// Estado exitoso
class NewsSuccessState extends NewsState {

  // Contiene la lista de noticias
  final List<Article> news;

  NewsSuccessState(this.news);

  @override
  List<Object> get props => [news];
}

// Estado de error
class NewsErrorState extends NewsState {
  
  // Contiene el mensaje de error 
  final String message;

  NewsErrorState(this.message);

  @override
  List<Object> get props => [message];
}

```

Ya hemos definidos los estados, así que ahora podemos crear la clase `NewsCubit` de la siguiente manera:

```Dart
class NewsCubit extends Cubit<NewsState> {
  // El cubit tiene una dependencia en el repositorio
  final NewsRepository _repository = getIt();

  NewsCubit() : super(NewsLoadingState());
}
```

Para completar la implementación del cubit `NewsCubit` debemos entender que tipo de interacciones puede tener el usuario con la aplicación. Veamos la siguiente imagen:

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2F6xxh4e60lq4x8fwwfkhu%2Fa8d03799-fa2b-4844-bd95-c6ebc28f2358.png?alt=media&token=dac86ea7-533d-4e1d-8e96-0d23c07eab2d"
    width="65%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="625/615"
    description="Puntos donde el usuario puede interactuar con la aplicación"
/>

Al analizar la imagen anterior podemos ver que hay tres lugares donde el usuario puede interactuar con la aplicación:

1. **Barra de búsqueda**: El usuario puede introducir texto para buscar noticias de su interés.

2. **Selección de idioma**: Permite al usuario cambiar el idioma de la aplicación.

3. **Botón borrar**: Borra el contenido de la barra de búsqueda y muestra en la aplicación las ultimas noticias.

Ahora podemos crear tres funciones en nuestro cubit para manejar los eventos de búsqueda, cambio de idioma y presionar el botón borrar.

```Dart
class NewsCubit extends Cubit<NewsState> {
  final NewsRepository _repository = getIt();

  // Variable de ayuda que contiene el texto que estamos buscando
  String? _currentSearch;

  // Variable de ayuda que contiene el idioma y país de las noticias que estamos buscando
  Locale _currentLocale;

  // Inicializamos "locale" en el constructor
  NewsCubit(Locale locale)
      : _currentLocale = locale,
        super(NewsLoadingState());

  // Cuando el usuario introduce un término de búsqueda llamamos esta función
  Future<void> searchNews({
    String? search,
  }) async {
    _currentSearch = search;
    return _search();
  }


  // Cuando el usuario presiona el botón borrar llamamos esta función
  Future<void> clearSearch() async {
    _currentSearch = null;
    return _search();
  }

  // Cuando el usuario cambia el idioma de la aplicación llamamos esta función
  Future<void> setLocale(Locale newLocale) async {
    if (_currentLocale == newLocale) return;
    _currentLocale = newLocale;
    return _search();
  }

  // Esta función abstrae la lógica de buscar para que el código no se repita en 
  // las funciones [searchNews], [clearSearch] y [setLocale]
  Future<void> _search() async {
    
  }
}
```

Ya hemos creado las tres funciones necesarias para que la capa de presentación pueda enviar los eventos generados por el usuario al cubit `NewsCubit`. También podemos ver que las tres funciones tienen en común la lógica de buscar que vamos a crear en la función privada `_search()`:

```Dart
  Future<void> _search() async {
    // El código está dentro de un try-catch para capturar 
    // las excepciones lanzadas desde la capa de datos
    try {

      // Emitimos el estado de cargando para que la capa de presentación muestre la interfaz de carga
      emit(NewsLoadingState());

      // Usando un switch, si [_currentSearch] es nulo llamamos la API de las ultimas noticias
      // pero si no es nulo llamamos la API de buscar todas las noticias
      final news = await switch (_currentSearch) {
        null => _repository.fetchTopHeadlines(
            locale: _currentLocale,
          ),
        _ => _repository.fetchEverything(
            locale: _currentLocale,
            search: _currentSearch,
          )
      };

      // Emitimos el estado de éxito con la lista de noticias para que la capa de presentación muestre las noticias
      emit(NewsSuccessState(news));

    } on Exception catch (e) {
      
      // En caso de cualquier excepción la capturamos y emitimos estado de error para que la capa
      // de presentación muestre el error 
      if (e is MissingApiKeyException) {
        emit(NewsErrorState('Please check the API key'));
      } else if (e is ApiKeyInvalidException) {
        emit(NewsErrorState('The api key is not valid'));
      } else {
        emit(NewsErrorState('Unknown error'));
      }
    }
  }
```

Ya hemos completado todas las funciones, variables, etc. que conforman la clase `NewsCubit`. Si ponemos todo junto, el código será:

```Dart
class NewsCubit extends Cubit<NewsState> {
  final NewsRepository _repository = getIt();
  String? _currentSearch;
  Locale _currentLocale;

  NewsCubit(Locale locale)
      : _currentLocale = locale,
        super(NewsLoadingState());

  Future<void> searchNews({
    String? search,
  }) async {
    _currentSearch = search;
    return _search();
  }

  Future<void> clearSearch() async {
    _currentSearch = null;
    return _search();
  }

  Future<void> setLocale(Locale newLocale) async {
    if (_currentLocale == newLocale) return;
    _currentLocale = newLocale;
    return _search();
  }

  Future<void> _search() async {
    try {
      emit(NewsLoadingState());

      final news = await switch (_currentSearch) {
        null => _repository.fetchTopHeadlines(
            locale: _currentLocale,
          ),
        _ => _repository.fetchEverything(
            locale: _currentLocale,
            search: _currentSearch,
          )
      };

      emit(NewsSuccessState(news));

    } on Exception catch (e) {

      if (e is MissingApiKeyException) {
        emit(NewsErrorState('Please check the API key'));
      } else if (e is ApiKeyInvalidException) {
        emit(NewsErrorState('The api key is not valid'));
      } else {
        emit(NewsErrorState('Unknown error'));
      }
    }
  }
}
```

#### Pruebas a la clase NewsCubit

Es hora de probar que la clase `NewsCubit` funciona como esperamos. Para hacer pruebas a un cubit podemos usar el paquete de ayuda [bloc_test][blocTest] que facilita hacer pruebas en cubits y blocs. Creamos un archivo llamado `news_cubit_test.dart` dentro de la carpeta de test y establecemos la estructura básica:

```Dart

// Mock nos permitirá regresar mock data al cubit desde el repositorio
class MockNewsRepository extends Mock implements NewsRepository {}


// Mock Locale que usaremos para pasar al cubit en el constructor y la función setLocale
const mockLocale = Locale('en', 'US');

// Llamar a la función fetchTopHeadlines regresara este articulo
const mockTopArticle = Article(title: "TopArticle", url: "someUrl");

// Llamar a la función fetchEverything regresara este articulo
const mockEverythingArticle = Article(title: "Everything", url: "someUrl");

void main() {
  late MockNewsRepository mockRepo;
  
  // setUp se llama antes de cada prueba.
  setUp(() async {
    mockRepo = MockNewsRepository();

    // Inyectamos MockNewsRepository
    getIt.registerSingleton<NewsRepository>(mockRepo);
  });

  // tearDown se llama después de cada prueba.
  tearDown(() async {
    // Reiniciamos getIt a su estado inicial
    await getIt.reset();
  });
}
```

Ahora tenemos que configurar `MockNewsRepository` para que las funciones `fetchTopHeadlines` y `fetchEverything` regresen un artículo cuando sean llamadas:

```Dart
  setUp(() async {
    mockRepo = MockNewsRepository();
    getIt.registerSingleton<NewsRepository>(mockRepo);
    
    // Cuando la función fetchEverything es llamada con mockLocale y cualquier
    // termino de búsqueda regresa una lista con el articulo mockEverythingArticle
    when(() => mockRepo.fetchEverything(
          locale: mockLocale,
          search: any(named: 'search'),
        )).thenAnswer((_) async => [mockEverythingArticle]);

    // Cuando la función fetchTopHeadlines es llamada con mockLocale 
    // regresa una lista con el articulo mockTopArticle
    when(() => mockRepo.fetchTopHeadlines(locale: mockLocale))
        .thenAnswer((_) async => [mockTopArticle]);
  });
```


La primera prueba que vamos a hacer es verificar que llamar la función `searchNews` emita los estados correctos y llame a la función del repositorio `fetchTopHeadlines` correctamente:

```Dart
  blocTest<NewsCubit, NewsState>(
      'When the search term is null '
      'fetchTopHeadlines will be called '
      'and the state will contain the mockTopArticle',
      
      // Creamos el cubit con el mockLocale
      build: () => NewsCubit(mockLocale),

      // Llamamos la función searchNews
      act: (cubit) async => cubit.searchNews(),

      // Los estados deben ser emitidos en orden correcto
      expect: () => [
            NewsLoadingState(),
            NewsSuccessState(const [mockTopArticle])
          ],

      // Verificamos que la función fetchTopHeadlines fuera llamada 1 vez con el
      // argumento mockLocale
      verify: (cubit) {
        verify(() => mockRepo.fetchTopHeadlines(locale: mockLocale)).called(1);
      });
```

En la segunda prueba a la función `searchNew` le vamos a pasar un texto de búsqueda por lo que la función del repositorio `fetchEverything` debe ser llamada y los estados deben ser emitidos correctamente:

```Dart
  blocTest<NewsCubit, NewsState>(
      'When the search term is not null '
      'fetchEverything will be called '
      'and the state will contain the mockEverythingArticle',

      // Creamos el Cubit con el mockLocale
      build: () => NewsCubit(mockLocale),

      // Llamamos la función searchNews con un texto de búsqueda
      act: (cubit) async => cubit.searchNews(search: 'Hello world'),

      // Los estados deben ser emitidos en orden correcto
      expect: () => [
            NewsLoadingState(),
            NewsSuccessState(const [mockEverythingArticle])
          ],

      // Verificamos que la función fetchEverything fuera llamada 1 vez con el
      // argumento mockLocale y 'Hello world'
      verify: (cubit) {
        verify(
          () => mockRepo.fetchEverything(
            locale: mockLocale,
            search: 'Hello world',
          ),
        ).called(1);
      });
```

La tercera prueba vamos a llamar a la función `searchNews` con un texto de búsqueda y después llamaremos a las función `clearSearch`, entonces debemos verificar que las funciones del repositorio `fetchEverything` y `fetchTopHeadlines` sean llamadas correctamente y además que los estados de `NewsCubit` sean emitidos correctamente:

```Dart
  blocTest<NewsCubit, NewsState>(
      'When the search term is not null '
      'fetchEverything will be called '
      'and then clearing the search will trigger a new search '
      'then fetchTopHeadlines will be called '
      'and states will be emitted correctly',

      // Creamos el cubit con el mockLocale
      build: () => NewsCubit(mockLocale),

      // Llamamos la función searchNews con un texto de búsqueda
      // y después llamamos la función clearSearch
      act: (cubit) async {
        await cubit.searchNews(search: 'Hello world');
        await cubit.clearSearch();
      },

      // Los estados deben ser emitidos en orden correcto
      expect: () => [
            NewsLoadingState(),
            NewsSuccessState(const [mockEverythingArticle]),
            NewsLoadingState(),
            NewsSuccessState(const [mockTopArticle])
          ],

      // Verificamos que la función fetchEverything y fetchTopHeadlines
      // fueran llamadas 1 vez con los argumentos correctos
      verify: (cubit) {
        verify(
          () => mockRepo.fetchEverything(
            locale: mockLocale,
            search: 'Hello world',
          ),
        ).called(1);
        verify(() => mockRepo.fetchTopHeadlines(locale: mockLocale)).called(1);
      });
```

Vamos a agregar una última prueba, en esta prueba queremos verificar que el estado de error sea emitido correctamente si el repositorio lanza una excepción:

```Dart
  blocTest<NewsCubit, NewsState>(
    'When the Api key is not valid exception is handled correctly',
    
    build: () {

      // Configuramos el mockRepo para lanzar una excepción ApiKeyInvalidException
      // cuando la función fetchTopHeadlines sea llamada
      when(() => mockRepo.fetchTopHeadlines(locale: mockLocale))
          .thenAnswer((_) async => throw ApiKeyInvalidException());
      
      // Creamos el cubit con el mockLocale
      return NewsCubit(englishUs);
    },
    // Llamamos la función searchNews
    act: (cubit) async => cubit.searchNews(),
    
    // Los estados deben ser emitidos en orden correcto y 
    // el ultimo estado es el estado de error
    expect: () => [
      NewsLoadingState(),
      NewsErrorState('The api key is not valid'),
    ],
  );
```

Muy bien, ya hemos terminado la clase `NewsCubit` que se encargará de mantener el estado de la aplicación y también hemos agregado pruebas para asegurarnos de que funcione como esperamos.

En el siguiente artículo trabajaremos con la capa de presentación donde aprenderemos cómo crear una aplicación responsiva y con soporte de múltiples idiomas.

Recuerda que puedes encontrar el código fuente en [GitHub][sourceCode]. Y no olvides visitar los otros artículos de esta serie de tutoriales:

- [Parte 1: Introducción, arquitectura y las fuentes de datos.][parte1] 

- [Parte 2: Inyección de dependencias, repositorios y la lógica de negocios.][parte2](este artículo)

- [Parte 3: Capa de presentación y soporte de múltiples idiomas.][parte3]


[parte1]: /post/j66drgpxoelhslflwkvw
[parte2]: /post/6xxh4e60lq4x8fwwfkhu
[parte3]: /post/r1ym2otazt5mc0z8e91n
[sourceCode]: https://tinyurl.com/rytwmvvu 
[blocTest]: https://pub.dev/packages/bloc_test
[getIt]: https://pub.dev/packages/get_it
[cubit]: https://bloclibrary.dev/#/coreconcepts?id=cubit
[wikiDI]: https://es.wikipedia.org/wiki/Principio_de_inversi%C3%B3n_de_la_dependencia
]]></content:encoded>
            <category>Cubit</category>
            <category>Dart</category>
            <category>Flutter</category>
            <enclosure url="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2F6xxh4e60lq4x8fwwfkhu%2Fb6529598-6104-4dbb-808e-77c712c187d5.png?alt=media&amp;token=4f674a56-b263-4837-9eae-be7dcca09dd1" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Cubit en Práctica: Creando una Aplicación de Noticias Responsiva y con Soporte de Múltiples Idiomas. Parte 1]]></title>
            <link>https://yayocode.com/es/post/j66drgpxoelhslflwkvw</link>
            <guid>j66drgpxoelhslflwkvw</guid>
            <pubDate>Wed, 25 Oct 2023 14:49:43 GMT</pubDate>
            <description><![CDATA[Descubre cómo crear una app de noticias responsiva y multilingüe con Cubit, con arquitectura escalable y pruebas unitarias y de widgets.]]></description>
            <content:encoded><![CDATA[<img src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2Fj66drgpxoelhslflwkvw%2F0cac044b-665b-4c58-8d20-ba601392fe25.png?alt=media&token=53a64d26-4440-452a-86ef-b999987ee4f2" alt="Cover image"> 
 En esta serie de artículos, crearemos una aplicación de noticias responsiva con soporte para tres idiomas: español, inglés y chino. Además, implementaremos una arquitectura escalable y añadiremos pruebas unitarias y de widgets. En la aplicación, mostraremos los últimos titulares y ofreceremos la opción de buscar noticias de tu interés, así como de seleccionar tu idioma preferido.

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2Fj66drgpxoelhslflwkvw%2F9af2e264-b90a-40f1-a2e4-76740150f149.webp?alt=media&token=f4889f6b-b73b-4674-8505-50ef83ee03df"
    width="75%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="640/480"
    description="App de noticias responsiva con soporte para tres diferentes tamaños."
/>

Puedes encontrar el código fuente en [GitHub][sourceCode]. Y no olvides visitar los otros artículos de esta serie de tutoriales:

- [Parte 1: Introducción, arquitectura y las fuentes de datos.][parte1] (este artículo)

- [Parte 2: Inyección de dependencias, repositorios y la lógica de negocios.][parte2]

- [Parte 3: Capa de presentación y soporte de múltiples idiomas.][parte3]

## Propósito de este tutorial:

- Crear una app responsiva con soporte para dispositivos pequeños, medianos y grandes.

- Aprender a usar [cubit][cubit] como manejador de estados.

- Crear una arquitectura sencilla que sea escalable y nos permita hacer pruebas.

- Aprender cómo agregar pruebas unitarias y de widgets a nuestro proyecto.

- Realizar peticiones a una API REST usando [http](https://pub.dev/packages/http).

- Aplicar inyección de dependencias con [GetIt][getIt] (no confundir con GetX).


## Prerrequisitos (Opcional)

- Obtener una API key de [https://newsapi.org/](https://newsapi.org/).

## Arquitectura de la aplicación

Cuando creamos una aplicación, definir una arquitectura es muy importante porque tiene muchos beneficios, como:

- **Mantenibilidad**: Una arquitectura sólida facilita la detección y corrección de errores, así como la incorporación de nuevas actualizaciones y características.

- **Facilita el trabajo en equipo**: Una arquitectura bien definida es como una guía para los desarrolladores, porque todos saben cómo es la estructura del proyecto, lo que reduce los conflictos.

- **Calidad del código**: Con una arquitectura, podemos definir estándares y mejores prácticas que todo el equipo debe seguir, lo que crea consistencia y calidad en el proyecto.

- **Reusabilidad del código**: Una buena arquitectura nos ayuda a abstraer secciones de código que podemos reutilizar en diferentes partes del proyecto.

- **Código más testeable**: Una buena arquitectura permite separar las diferentes partes de la aplicación en secciones con responsabilidades específicas, lo que facilita probar cada sección de forma aislada sin depender de otras secciones.

Hay muchas otras razones para tener una arquitectura que también son muy importantes, pero en resumen, definir una arquitectura nos ayuda a crear código mantenible, escalable, testeable y que promueve la colaboración del equipo.

La arquitectura de este proyecto es muy similar a la que se puede encontrar en la documentación de [Flutter Bloc](https://bloclibrary.dev/#/architecture).

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2Fj66drgpxoelhslflwkvw%2Fdfa7dd95-5c13-4374-8774-df5f86aebf4a.png?alt=media&token=d0f72bcb-d077-4558-9c62-a84635c2e393"
    width="85%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="862/330"
    description="Arquitectura: fuentes de datos, repositorios, lógica de negocios y capa de presentación"
/>

Como podemos ver en la imagen anterior, la arquitectura está dividida en tres partes: las fuentes de datos externas, la capa de datos y la capa de la aplicación. ¿Cuál es la responsabilidad de cada una?

- **Las fuentes externas de datos:** Se refieren a las diferentes fuentes de donde una aplicación puede obtener información, por ejemplo, una base de datos, APIs web, archivos locales, sensores del dispositivo, etc.

- **La capa de datos:** Se encarga de interactuar con las fuentes de datos y permite que otras partes de la aplicación accedan a los datos de manera organizada y segura. Esta capa se divide a su vez en dos:

  - **Fuentes de datos:** Son las encargadas de conectarse directamente con las fuentes de datos externas.

  - **Repositorios:** Son intermediarios entre la capa de datos y la capa de la aplicación. Se encargan de gestionar los datos que provienen de la fuente de datos, transformarlos, guardarlos en la memoria caché o en la base de datos local, entre otras funciones.

- **La capa de la aplicación:** Se encarga de manejar la lógica de negocio y la interacción directa con los usuarios. Se divide en dos partes:

  - **Lógica de negocios:** Aquí es donde se encuentran nuestros blocs y cubits. Es donde vamos a escribir acerca de la lógica de negocios y también manejar el estado de nuestros widgets.

  - **Capa de presentación:** Aquí es donde están todos nuestros widgets y se encarga de las interacciones y de lo que el usuario puede ver: botones, imágenes, texto, etc.

<Note
   title="Nota"
   text="
En este artículo, **no** abordaremos la parte punteada, que implica conectarnos a una base de datos local para almacenar noticias y poder acceder a ellas cuando no tengamos conexión.
"/>

## Fuente de datos externa: [News API][newsApi]

En nuestra aplicación, la fuente de datos externa es [News API][newsApi], y hay dos API que nos interesan:

1. [/v2/everything:][newsApiEverything] Esta API busca a través de millones de artículos y la utilizaremos cuando el usuario introduzca un texto en la barra de búsqueda.

2. [/v2/top-headlines:][newsApiTop] Esta API proporciona las noticias principales y titulares de última hora para un país, y la usaremos en la página de inicio.

En la [documentación](https://newsapi.org/docs/endpoints/everything), podemos ver que al llamar cualquiera de estas dos APIs, cuando la respuesta es exitosa, retornan un JSON como este:

```JSON
{
  "status": "ok",
  "totalResults": 1,
  "articles": [
    {
      "source": {
        "id": "cbs-news",
        "name": "CBS News"
      },
      "author": "Sophie Lewis",
      "title": "\"Bachelor\" host Chris Harrison is temporarily \"stepping aside\" after \"excusing historical racism\" - CBS News",
      "description": "\"By excusing historical racism, I defended it,\" Harrison wrote, announcing he will not host the \"After the Final Rose\" live special.",
      "url": "https://www.cbsnews.com/news/chris-harrison-the-bachelor-stepping-aside-comments-rachel-lindsay-backlash/",
      "urlToImage": "https://cbsnews1.cbsistatic.com/hub/i/r/2021/02/12/0cf2794b-2b04-4ced-84ce-1d72e26ed2cb/thumbnail/1200x630/8d464cc0fdf23566bbea2e01c41401ab/gettyimages-1204035255.jpg",
      "publishedAt": "2021-02-14T12:18:00Z",
      "content": "Chris Harrison, the longtime host of \"The Bachelor\" and \"The Bachelorette,\" announced Saturday that he is \"stepping aside\" from the franchise for an undisclosed \"period of time.\" The bombshell announ… [+7754 chars]"
    }
  ]
}

```

Pero cuando la petición no es exitosa, la respuesta contiene un código de error (ver [aquí][newsApiError]), y el JSON es como este:

```JSON
{
  "status": "error",
  "code": "apiKeyMissing",
  "message": "Your API key is missing. Append this to the URL with the apiKey param, or use the x-api-key HTTP header."
}
```

Si analizamos ambas respuestas, podemos notar que necesitamos dos clases para modelar la respuesta. La primera clase la vamos a llamar `Article` y representará todo lo que está dentro de la lista de `articles`:

```Dart
@JsonSerializable()
class Article extends Equatable {
  final String title;
  final String? author;
  final String? description;
  final String? urlToImage;
  final String? content;
  final String url;

  const Article({
    required this.title,
    required this.url,
    this.author,
    this.content,
    this.urlToImage,
    this.description,
  });

  factory Article.fromJson(Map<String, dynamic> json) =>
      _$ArticleFromJson(json);

  Map<String, dynamic> toJson() => _$ArticleToJson(this);

  @override
  List<Object?> get props =>
      [title, author, description, urlToImage, content, url];
}
```

<Note
   title="Nota"
   text="
Podemos ver que en la clase **Article** estamos usando los siguientes paquetes:

- **Equatable**: Es un paquete que simplifica el proceso de comparar dos objetos de la misma clase sin sobreescribir `==` y `hashCode`. Puedes visitar [este artículo](/post/VK2XfvNw9Ac9Eqxo9MHF) para aprender más.

- **JsonSerializable**: Es un paquete para generar código que nos ayudará a convertir el JSON a objetos de la clase **Article**.
"/>

La segunda clase se encarga de representar los valores de la respuesta, ya sea exitosa o fallida. Como puedes ver, también estamos utilizando los paquetes [Equatable][equatable] y [JsonSerializable][jsonSerializable]:

```Dart
@JsonSerializable()
class ApiResponse extends Equatable {
  final String status;
  final String? code;
  final List<Article>? articles;

  const ApiResponse(
    this.status,
    this.code,
    this.articles,
  );

  factory ApiResponse.fromJson(Map<String, dynamic> json) =>
      _$ApiResponseFromJson(json);

  Map<String, dynamic> toJson() => _$ApiResponseToJson(this);

  @override
  List<Object?> get props => [status, code, articles];
}
```

Ya hemos definido las clases que nos ayudarán a representar el JSON en objetos de Dart. Ahora podemos continuar con la clase que se encargará de hacer las peticiones a la API.

<Note
   title="Recuerda"
   text="
Recuerda ejecutar el comando para que el paquete [JsonSerializable](https://pub.dev/packages/json_serializable) genere el código necesario. 
`flutter pub run build_runner watch --delete-conflicting-outputs`
"/>

## La capa de datos

Como mencionamos anteriormente, esta capa es responsable de interactuar con las fuentes de datos, ya sea una API externa o una base de datos local. También permite que otras partes de la aplicación accedan a los datos de manera organizada y segura. Se divide en fuentes de datos y repositorios.

### Fuente de datos: NewsDataSource

La clase `NewsDataSource` se encargará de hacer peticiones a las APIs de [News API][newsApi]. Para esto vamos a usar el paquete [http][http] que nos permite hacer peticiones HTTP. Vamos a crear la clase `NewsDataSource` e inicializar algunas variables:

```Dart
class NewsDataSource {
  
  // Visita la página de https://newsapi.org/ para obtener tu API key
  static const String apiKey = 'xxxxxxxxxxxxxxxxxxxxxxxxx';

  static const String baseUrl = 'newsapi.org';
  static const String everything = '/v2/everything';
  static const String topHeadlines = '/v2/top-headlines';

  final Client _httpClient;

  NewsDataSource({Client? httpClient}) : _httpClient = httpClient ?? Client();

}
```

Hemos agregado la URL base de [News API][newsApi], así como la ruta de las dos APIs que vamos a utilizar. También podemos ver que tenemos la **API KEY**, la cual podemos obtener en el sitio web de [News API][newsApi]. Por último, tenemos un objeto `Client` que se encargará de hacer peticiones a las APIs.

Si eres observador, te preguntarás por qué estamos inicializando el cliente de la siguiente forma:

```Dart
  final Client _httpClient;

  NewsDataSource({Client? httpClient}) : _httpClient = httpClient ?? Client();
```

En vez de hacerlo así:

```Dart
  final Client _httpClient = Client();

  NewsDataSource();
```

La respuesta es sencilla. Pasar `Client` en el constructor nos permite pasar un **Mock** que nos permitirá hacer pruebas unitarias a la clase `NewsDataSource`.

Ahora vamos a crear una función de ayuda que es responsable de hacer peticiones a las APIs y regresar un objeto de tipo `ApiResponse` con la respuesta:

```Dart
  Future<ApiResponse> _callGetApi({
    required String endpoint,
    required Map<String, String> params,
  }) async {
    
    // Creamos la URI con la que hacemos una petición a la API
    final uri = Uri.https(baseUrl, endpoint, params);
    final response = await _httpClient.get(uri);

    // Usamos `json.decode` para convertir el 'body' de la respuesta a un objeto Json
    final result = ApiResponse.fromJson(json.decode(response.body));

    // Si la respuesta contiene un error, lanzamos una excepción
    if (result.status == 'error') {
      if (result.code == 'apiKeyMissing') throw MissingApiKeyException();
      if (result.code == 'apiKeyInvalid') throw ApiKeyInvalidException();
      throw Exception();
    }

    // Si no hay ningún error, regresamos el resultado de la API
    return result;
  }

// Definimos algunas excepciones personalizadas que podemos
// manejar en la capa de la aplicación. 
class MissingApiKeyException implements Exception {}
class ApiKeyInvalidException implements Exception {}

```

La función `_callGetApi` será llamada siempre que hagamos una petición a cualquiera de las APIs. Los códigos de error los podemos encontrar en la [documentación de News API][newsApiError]. Para este ejemplo, solo creamos excepciones para `apiKeyMissing` y `apiKeyInvalid`.

Ahora vamos a crear dos funciones que nos ayudarán a llamar las APIs que nos interesan:

- [/v2/everything][newsApiEverything]
- [/v2/top-headlines][newsApiTop]

```Dart
  // Realiza una petición a /v2/top-headline. Recibe el país
  Future<List<Article>> fetchTopHeadlines({
    required String country,
  }) async {
    final params = {
      'apiKey': apiKey,
      'country': country,
    };

    final result = await _callGetApi(
      endpoint: topHeadlines,
      params: params,
    );
    return result.articles!;
  }

  // Realiza una petición a /v2/everything. Recibe el idioma y un texto de búsqueda
  Future<List<Article>> fetchEverything({
    required String language,
    String? search,
  }) async {
    final params = {
      'apiKey': apiKey,
      'language': language,
    };

    if (search != null) params['q'] = search;

    final result = await _callGetApi(
      endpoint: everything,
      params: params,
    );
    return result.articles!;
  }
```

Hemos creado dos funciones, `fetchTopHeadlines` y `fetchEverything`, cada una con sus respectivos parámetros, y cada una de ellas construye los parámetros `params` de acuerdo a lo que requiere cada API.

Ya hemos completado todas las funciones, variables, etc. que conforman la clase `NewsDataSource`. Si ponemos todo junto, el código será:

```Dart
class MissingApiKeyException implements Exception {}
class ApiKeyInvalidException implements Exception {}

class NewsDataSource {
  // Visita la página de https://newsapi.org/ para obtener tu API key
  static const String apiKey = 'xxxxxxxxxxxxxxxxxxxxxxxxx';

  static const String baseUrl = 'newsapi.org';
  static const String everything = '/v2/everything';
  static const String topHeadlines = '/v2/top-headlines';

  final Client _httpClient;

  NewsDataSource({Client? httpClient}) : _httpClient = httpClient ?? Client();

  Future<List<Article>> fetchTopHeadlines({
    required String country,
  }) async {
    final params = {
      'apiKey': apiKey,
      'country': country,
    };

    final result = await _callGetApi(
      endpoint: topHeadlines,
      params: params,
    );
    return result.articles!;
  }

  Future<List<Article>> fetchEverything({
    required String language,
    String? search,
  }) async {
    final params = {
      'apiKey': apiKey,
      'language': language,
    };

    if (search != null) params['q'] = search;

    final result = await _callGetApi(
      endpoint: everything,
      params: params,
    );
    return result.articles!;
  }

  Future<ApiResponse> _callGetApi({
    required String endpoint,
    required Map<String, String> params,
  }) async {
    final uri = Uri.https(baseUrl, endpoint, params);

    final response = await _httpClient.get(uri);
    final result = ApiResponse.fromJson(json.decode(response.body));

    if (result.status == 'error') {
      if (result.code == 'apiKeyMissing') throw MissingApiKeyException();
      if (result.code == 'apiKeyInvalid') throw ApiKeyInvalidException();
      throw Exception();
    }

    return result;
  }
}
```

#### Pruebas a la clase NewsDataSource

Ahora vamos a realizar pruebas en la clase `NewsDataSource`. Para ello, utilizaremos el paquete [Mockito][mockito], que sirve para crear "mocks" y simular peticiones exitosas y fallidas a las APIs.

Crearemos tres archivos **.json** que contendrán las respuestas simuladas de la API. El primer archivo contiene una respuesta fallida con el error `apiKeyInvalid`. Con este archivo, probaremos si la excepción `ApiKeyInvalidException` se lanza correctamente:

```Json
{
  "status": "error",
  "code": "apiKeyInvalid",
  "message": "Your API key is invalid or incorrect. Check your key, or go to https://newsapi.org to create a free API key."
}
```

El segundo archivo contiene el error `apiKeyMissing`, y con él, probaremos si la excepción `MissingApiKeyException` se lanza correctamente:

```Json
{
  "status": "error",
  "code": "apiKeyMissing",
  "message": "Your API key is missing. Append this to the URL with the apiKey param, or use the x-api-key HTTP header."
}
```

El tercer archivo simula la respuesta exitosa de la API y contiene dos noticias:

```Json
{
  "status": "ok",
  "totalResults": 2,
  "articles": [
    {
      "source": {
        "id": "cbs-news",
        "name": "CBS News"
      },
      "author": "Sophie Lewis",
      "title": "\"Bachelor\" host Chris Harrison is temporarily \"stepping aside\" after \"excusing historical racism\" - CBS News",
      "description": "\"By excusing historical racism, I defended it,\" Harrison wrote, announcing he will not host the \"After the Final Rose\" live special.",
      "url": "https://www.cbsnews.com/news/chris-harrison-the-bachelor-stepping-aside-comments-rachel-lindsay-backlash/",
      "urlToImage": "https://cbsnews1.cbsistatic.com/hub/i/r/2021/02/12/0cf2794b-2b04-4ced-84ce-1d72e26ed2cb/thumbnail/1200x630/8d464cc0fdf23566bbea2e01c41401ab/gettyimages-1204035255.jpg",
      "publishedAt": "2021-02-14T12:18:00Z",
      "content": "Chris Harrison, the longtime host of \"The Bachelor\" and \"The Bachelorette,\" announced Saturday that he is \"stepping aside\" from the franchise for an undisclosed \"period of time.\" The bombshell announ… [+7754 chars]"
    },
    {
      "source": {
        "id": null,
        "name": "KOCO Oklahoma City"
      },
      "author": "KOCO Staff",
      "title": "Winter storm brings heavy snow, causing hazardous driving conditions across Oklahoma - KOCO Oklahoma City",
      "description": "A winter storm moving across Oklahoma Sunday morning is dumping heavy snow, causing hazardous driving conditions.",
      "url": "https://www.koco.com/article/winter-storm-brings-heavy-snow-causing-hazardous-driving-conditions-across-oklahoma/35500508",
      "urlToImage": "https://kubrick.htvapps.com/htv-prod-media.s3.amazonaws.com/images/poster-image-2021-02-14t060232-115-1613304161.jpg?crop=1.00xw:1.00xh;0,0&resize=1200:*",
      "publishedAt": "2021-02-14T12:17:00Z",
      "content": "OKLAHOMA CITY —A winter storm moving across Oklahoma Sunday morning is dumping heavy snow, causing hazardous driving conditions. \r\n[Check latest weather alerts in your area | Check live traffic condi… [+2381 chars]"
    }
  ]
}
``` 

Vamos a crear un archivo llamado **data_source_test.dart** dentro de la carpeta de **test** y establecemos la estructura básica:

```Dart
// Mocks que nos permitirán simular peticiones exitosas y fallidas
class MockClient extends Mock implements Client {}
class FakeUri extends Fake implements Uri {}

// Ruta donde están los archivos .json
const mockPath = 'test/data_source_test';

void main() {
  late MockClient mockClient;
  late NewsDataSource dataSource;

  setUpAll(() {
    registerFallbackValue(FakeUri());
  });

  // setUp se llama antes de cada prueba.
  setUp(() {
    mockClient = MockClient();

    // Creamos un objeto NewsDataSource y le pasamos el mockClient
    dataSource = NewsDataSource(httpClient: mockClient);
  });

  // Agrupamos las pruebas de la función fetchEverything
  group('When calling fetchEverything', () {
     // Aquí escribiremos las pruebas de este grupo
  });

  // Agrupamos las pruebas de la función fetchTopHeadlines
  group('When calling fetchTopHeadlines', () {
     // Aquí escribiremos las pruebas de este grupo
  });
}

// Función de ayuda para leer los archivos .json como Strings. 
Future<String> getMockBody(String filePath) => File(filePath).readAsString();
```

<Note
   title="Importante"
   text="
En este artículo solo vamos a hacer pruebas a la función `fetchEverything`, y como tarea, puedes agregar las pruebas a la función `fetchTopHeadlines`.
"/>

La primera prueba que vamos a hacer es verificar que llamar a la API sea exitosa, y como el archivo JSON que creamos para hacer pruebas exitosas contiene dos noticias, podemos verificar que al llamar a la función `fetchEverything`, contenga dos noticias:

```Dart
    test('The response contains two news', () async {
      // Obtenemos el string del JSON con la respuesta exitosa
      final response = await getMockBody('$mockPath/success_response.json');

      // Le decimos al mockClient que cuando reciba una petición GET
      // regrese el JSON con la respuesta exitosa y el estatus 200. 
      when(() => mockClient.get(any())).thenAnswer((_) async {
        return Response(response, 200, headers: headers);
      });

      // Llamamos la función fetchEverything
      final articles = await dataSource.fetchEverything(language: 'es');

      // El resultado esperado son dos noticias, 
      // cada una con un autor diferente
      expect(articles.length, 2);
      expect(articles[0].author, 'Sophie Lewis');
      expect(articles[1].author, 'KOCO Staff');
    });

``` 

En la prueba anterior, verificamos que llamar a la función `fetchEverything` regrese el resultado esperado. En la siguiente prueba, vamos a verificar que al llamar la función `fetchEverything` con los argumentos `language:'es'` y `search:'Hello world'`, el `Client` reciba los argumentos exitosamente:

```Dart
    test('The request contains the expected parameters', () async {
      // Obtenemos el contenido JSON de la respuesta exitosa
      final response = await getMockBody('$mockPath/success_response.json');

      // Configuramos el mockClient para que, al recibir una petición GET,
      // retorne el JSON de la respuesta exitosa y un estado 200. 
      when(() => mockClient.get(any())).thenAnswer((_) async {
        return Response(response, 200, headers: headers);
      });

      // Argumentos que debe recibir el Client
      const language = 'es';
      const searchTerm = 'Hello world';

      // Llamamos a la función fetchEverything
      await dataSource.fetchEverything(
        language: language,
        search: searchTerm,
      );

      // Creamos la Uri esperada con los argumentos proporcionados
      final uri = Uri.https(
        NewsDataSource.baseUrl,
        NewsDataSource.everything,
        {
          'apiKey': NewsDataSource.apiKey,
          'language': language,
          'q': searchTerm,
        },
      );

      // Verificamos que el Client fue llamado con la Uri esperada
      verify(() => mockClient.get(uri)).called(1);
    });
```

Ahora vamos a crear la prueba donde la respuesta es fallida y la función lanza la excepción `MissingApiKeyException`:

```Dart
    test('Correctly throws Missing API Key Exception for non-successful response', () async {
      // Obtenemos el contenido JSON de la respuesta fallida
      final response = await getMockBody('$mockPath/api_key_missing.json');

      // Configuramos el mockClient para que, al recibir una petición GET,
      // retorne el JSON de la respuesta fallida y un estado 200. 
      when(() => mockClient.get(any())).thenAnswer((_) async {
        return Response(response, 200);
      });

      // Al llamar la función fetchEverything, esperamos que
      // la excepción MissingApiKeyException sea lanzada
      expect(
          () => dataSource.fetchEverything(language: 'es'),
          throwsA(
              predicate((exception) => exception is MissingApiKeyException)));
    });
```

Y por último la prueba donde la respuesta es fallida y la función lanza la excepción `ApiKeyInvalidException`:

```Dart
    test('Correctly throws Invalid API Key Exception for non-successful response', () async {
      // Obtenemos el contenido JSON de la respuesta fallida
      final response = await getMockBody('$mockPath/api_key_invalid.json');

      // Configuramos el mockClient para que, al recibir una petición GET,
      // retorne el JSON de la respuesta fallida y un estado 200. 
      when(() => mockClient.get(any())).thenAnswer((_) async {
        return Response(response, 200);
      });

      // Al llamar la función fetchEverything, esperamos que
      // la excepción ApiKeyInvalidException sea lanzada
      expect(
          () => dataSource.fetchEverything(language: 'es'),
          throwsA(
              predicate((exception) => exception is ApiKeyInvalidException)));
    });
```

Muy bien, hemos terminado la clase `NewsDataSource` y también hemos agregado pruebas para asegurarnos de que funcione como esperamos.

En el siguiente artículo veremos cómo usar inyección de dependencias para inicializar la clase `NewsDataSource` para que pueda ser usada por el repositorio.

Recuerda que puedes encontrar el código fuente en [GitHub][sourceCode]. Y no olvides visitar los otros artículos de esta serie de tutoriales:

- [Parte 1: Introducción, arquitectura y las fuentes de datos.][parte1] (este artículo)

- [Parte 2: Inyección de dependencias, repositorios y la lógica de negocios.][parte2]

- [Parte 3: Capa de presentación y soporte de múltiples idiomas.][parte3]


[parte1]: /post/j66drgpxoelhslflwkvw
[parte2]: /post/6xxh4e60lq4x8fwwfkhu
[parte3]: /post/r1ym2otazt5mc0z8e91n
[sourceCode]: https://tinyurl.com/rytwmvvu
[equatable]: https://pub.dev/packages/equatable
[jsonSerializable]: https://pub.dev/packages/json_serializable
[http]: https://pub.dev/packages/http
[mockito]: https://pub.dev/packages/mockito
[getIt]: https://pub.dev/packages/get_it
[cubit]: https://bloclibrary.dev/#/coreconcepts?id=cubit
[newsApi]: https://newsapi.org
[newsApiError]: https://newsapi.org/docs/errors
[newsApiEverything]: https://newsapi.org/docs/endpoints/everything
[newsApiTop]: https://newsapi.org/docs/endpoints/top-headlines]]></content:encoded>
            <category>Cubit</category>
            <category>Dart</category>
            <category>Flutter</category>
            <enclosure url="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2Fj66drgpxoelhslflwkvw%2F0cac044b-665b-4c58-8d20-ba601392fe25.png?alt=media&amp;token=53a64d26-4440-452a-86ef-b999987ee4f2" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Flame: Cuerpos y Sprites]]></title>
            <link>https://yayocode.com/es/post/91HWShtzuHkfaOj9gPP3</link>
            <guid>91HWShtzuHkfaOj9gPP3</guid>
            <pubDate>Thu, 15 Jun 2023 12:42:53 GMT</pubDate>
            <description><![CDATA[En este artículo aprenderemos como dibujar imágenes en objetos utilizando Forge2D y Flame.]]></description>
            <content:encoded><![CDATA[<img src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2F91HWShtzuHkfaOj9gPP3%2F7cf855db-5936-4e64-b8c1-88535473b205.png?alt=media&token=d38f2777-9573-427b-9c87-eee5534ed267" alt="Cover image"> 
 En este artículo, aprenderemos como dibujar imágenes en objetos utilizando Forge2D.

Como en los artículos anteriores, vamos a aprender con un ejemplo. Creemos una escena en Forge2D donde se mostrará una caja o una pelota en la parte superior de la pantalla cada vez que hagamos clic en ella.

Comenzaremos creando una clase llamada `Box` que representa la caja:

```dart
class Box extends BodyComponent {

  @override
  Body createBody() {
    final bodyDef = BodyDef(
      position: Vector2(worldSize.x / 2, 0),
      type: BodyType.dynamic,
    );

    final shape = PolygonShape()..setAsBoxXY(.25, .25);
    final fixtureDef = FixtureDef(shape)
      ..density = 5
      ..friction = .5
      ..restitution = .5;
    return world.createBody(bodyDef)
      ..createFixture(fixtureDef)
      ..angularVelocity = radians(180);
  }
}
```

Hemos agregado una velocidad angular, `angularVelocity`, para que la caja rote sobre su eje. Todo lo demás es lo que hemos estado haciendo en tutoriales anteriores.

Ahora vamos a crear la clase `Ball` que representa la pelota:

```dart
class Ball extends BodyComponent {

  @override
  Body createBody() {
    final bodyDef = BodyDef(
      position: Vector2(worldSize.x / 2, 0),
      type: BodyType.dynamic,
    );

    final shape = CircleShape()..radius = .25;
    final fixtureDef = FixtureDef(shape)
      ..density = 5
      ..friction = .5
      ..restitution = .5;
    return world.createBody(bodyDef)
      ..createFixture(fixtureDef)
      ..angularVelocity = radians(180);
  }
}
```

Una vez más, no hay nada especial en esta clase. Esta pelota también girará sobre su eje.

Cada vez que hagamos clic en la pantalla, queremos agregar una pelota o una caja al azar. Agreguemos este código:

```dart
class GameLesson05 extends MyGame with TapDetector {
  @override
  Future<void> onLoad() async {
    super.onLoad();
    add(Floor());
  }

  @override
  void onTapDown(TapDownInfo info) {
    super.onTapDown(info);
    if (Random().nextBool()) {
      add(Ball());
    } else {
      add(Box());
    }
  }
}
```

Si ejecutamos el código anterior, el resultado es el siguiente:

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2F91HWShtzuHkfaOj9gPP3%2F1e6e8b9e-3b3c-41d2-a2ef-d4c7c4cac5ac.gif?alt=media&token=bc478fa8-b94d-4f50-9ced-eba2af196c0b"
    width="65%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="600/367"

/>

## Agregando sprites

Es hora de mejorar la escena de cajas y pelotas blancas agregando sprites de una caja y una pelota. Primero, agregaremos estas dos imágenes a la carpeta de assets:

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2F91HWShtzuHkfaOj9gPP3%2F79d2fb97-12cf-4682-9f37-365d25b68a2f.png?alt=media&token=264d9fa1-bfbd-41a4-8f19-79d0c35cff93"
    width="65%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="294/101"
    description="Sprites: caja y pelota"
/>

Luego, actualizamos el archivo `pubspec.yaml` para agregar la ruta de los assets.

```yaml
  assets:
    - assets/images/
```

Cargaremos los sprites y los almacenaremos en la memoria caché. En la clase `GameLesson05`, agregamos este código a la función `onLoad()`:

```dart
  Future<void> onLoad() async {
    super.onLoad();
    await loadSprite('ball.png');
    await loadSprite('box.png');

    add(Floor());
  }
```

Ahora debemos recuperar los sprites cargados de la memoria caché y agregarlos a los cuerpos de la caja y la pelota.

Vamos a sobrescribir la función `onLoad()` de la clase `Ball`:

```dart
  @override
  Future<void> onLoad() async {
    await super.onLoad();
    final sprite = Sprite(gameRef.images.fromCache('ball.png'));
    add(
      SpriteComponent(
        sprite: sprite,
        size: Vector2(.5, .5),
        anchor: Anchor.center,
      ),
    );
  }
```

El código anterior recupera la imagen de la memoria caché y crea un nuevo `Sprite`, luego crea un nuevo `SpriteComponent` que se encargará de dibujar la imagen sobre la pelota.

El código para dibujar el spritede la caja es muy similar al código de la pelota:

```dart
  @override
  Future<void> onLoad() async {
    await super.onLoad();
    final sprite = Sprite(gameRef.images.fromCache('box.png'));
    add(
      SpriteComponent(
        sprite: sprite,
        size: Vector2(.5, .5),
        anchor: Anchor.center,
      ),
    );
  }
```

Al ejecutar el código actualizado, obtendremos el siguiente resultado:

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2F91HWShtzuHkfaOj9gPP3%2Fa0ff670b-53d9-4de7-ae7e-a8aea4e54e68.gif?alt=media&token=d3e82baa-8bed-4fe0-a8f2-15fdcbd9b337"
    width="65%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="600/367"
/>

¿Se ve mejor, verdad? Podemos ver cómo Flame dibuja los sprites sobre los cuerpos, incluyendo la actualización de la posición, rotación, etc.

## Conclusión

Este es un pequeño artículo en el que aprendimos cómo usar sprites para dibujar imágenes en nuestra escena de Forge2D.

Por último, recuerda que puedes encontrar el código fuente de todos los tutoriales en [Github][2], y también puedes probar todos los ejemplos en tu navegador.

<MyIframe
  src="https://yayo-arellano.github.io/flutter_games_compilation/flutter_learn_flame"
  aspectRatio="4/3"
  width="70%"
  widthSmallScreen="100%"  
/>

## Videotutorial en YouTube

<Youtube id="EsAiUfpdzxc"/>

[2]: https://github.com/Yayo-Arellano/flutter_games_compilation/tree/main/flutter_learn_flame 'Source code']]></content:encoded>
            <category>Flutter</category>
            <category>Flame</category>
            <enclosure url="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2F91HWShtzuHkfaOj9gPP3%2F7cf855db-5936-4e64-b8c1-88535473b205.png?alt=media&amp;token=d38f2777-9573-427b-9c87-eee5534ed267" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Flame: Fuerzas, impulsos y velocidad lineal]]></title>
            <link>https://yayocode.com/es/post/yPFeIgyd6Jnz8Pgby6FU</link>
            <guid>yPFeIgyd6Jnz8Pgby6FU</guid>
            <pubDate>Tue, 13 Jun 2023 14:05:03 GMT</pubDate>
            <description><![CDATA[En este artículo, aprenderemos los conceptos de fuerza, impulso y velocidad lineal en Flame y Forge2D]]></description>
            <content:encoded><![CDATA[<img src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FyPFeIgyd6Jnz8Pgby6FU%2Fc5467dd0-6902-49c4-a709-dab66fffc4af.png?alt=media&token=037e1b53-8979-4ff2-9efb-6af843fdf50f" alt="Cover image"> 
 En Forge2D la fuerza, el impulso y la velocidad lineal son conceptos fundamentales para simular el movimiento y la interacción de los cuerpos, y la mejor forma de aprender la diferencia entre de cada uno ellos es con un ejemplo práctico.

En este ejemplo, agregaremos 3 pelotas al mundo. A la primera le aplicaremos una fuerza, a la segunda un impulso y a la tercera una velocidad lineal. De esta manera, podremos observar el comportamiento de cada uno.

Comenzaremos creando la clase `Ball`, que recibirá la posición en el eje X:

```dart
class Ball extends BodyComponent {
  final double positionX;

  Ball(this.positionX);

  @override
  Body createBody() { 
    final bodyDef = BodyDef(
      position: Vector2(positionX, worldSize.y - 1),
      type: BodyType.dynamic,
    );

    final shape = CircleShape()..radius = .35;
    final fixtureDef = FixtureDef(shape)
      ..density = 5
      ..friction = .5
      ..restitution = .5;
    return world.createBody(bodyDef)..createFixture(fixtureDef);
  }
}
```

A continuación, agregaremos las pelotas al juego. Sin embargo, como queremos hacer un seguimiento de las pelotas para poder aplicar una fuerza cada vez que se hace clic en la pantalla, almacenaremos los objetos de las bolas en una lista. También crearemos un vector llamado speed que contendrá el valor que aplicaremos en el eje Y.

```dart
class GameLesson04 extends MyGame with TapDetector {
  
  // Almacenamos las pelotas en una lista
  final balls = [Ball(2), Ball(6), Ball(10)];

  // Contiene el valor que aplicaremos en el eje Y
  final speed = Vector2(0, -9);

  @override
  Future<void> onLoad() async {
    super.onLoad();

    add(Floor());
    addAll(balls);
  }

  @override
  void onTapDown(TapDownInfo info) {
    super.onTapDown(info);

    // Cada vez que se presiona la pantalla
    // aplicamos las diferentes fuerzas a las pelotas
    balls[0].body.applyForce(speed);
    balls[1].body.applyLinearImpulse(speed);
    balls[2].body.linearVelocity = speed;
  }
}
```

La lista `balls` contiene tres pelotas ubicadas horizontalmente en las posiciones `X = 2`, `X = 6` y `X = 10`.

El vector `speed` representa el valor que utilizaremos para aplicar una fuerza al cuerpo. En este caso, el cuerpo se moverá únicamente verticalmente porque el valor en el eje Y es **-9**, mientras que el valor en el eje X es **0**, lo que significa que el cuerpo no se moverá horizontalmente.

Estamos utilizando `TapDetector` y sobreescribiendo la función `onTapDown()`, de manera que cada vez que se hace clic en la pantalla, interactuamos con los cuerpos.

<Note
   title="Recuerda"
   text="
En Flame, las coordenadas (0, 0) se encuentran en la parte superior izquierda de la pantalla. Por lo tanto, un valor negativo de Y en el vector `speed` hace que los cuerpos se muevan hacia arriba.
"/>

Después de ejecutar el código, el resultado será el siguiente:

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FyPFeIgyd6Jnz8Pgby6FU%2F2ed52df6-32b4-40a6-8e08-f3627f284ab2.png?alt=media&token=ee4b97d2-1fa6-4b91-85eb-a94da82fcdca"
    width="65%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="1419/848"
    description="Las pelotas están en reposo en el suelo"
/>

## Aplicando fuerzas

Vamos a interactuar con las bolas, tocamos o hacemos clic en la pantalla. Se ejecutará el siguiente código:

```dart
balls[0].body.applyForce(speed);
balls[1].body.applyLinearImpulse(speed);
balls[2].body.linearVelocity = speed;
```

Y el resultado se verá así:

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FyPFeIgyd6Jnz8Pgby6FU%2F1f05f7e3-b735-451b-b6c1-1b74b041bf7d.gif?alt=media&token=cfcb6a19-5026-42be-83be-1d9b2825215b"
    width="65%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="0/0"
    description="Pelotas reaccionando a las fuerzas"
/>

Podemos observar que el movimiento de cada una de las pelotas es diferente. Veamos la diferencia entre cada una de las fuerzas:

**applyForce:** A la pelota de la izquierda, le estamos aplicando una fuerza. Las fuerzas cambian la velocidad de un cuerpo gradualmente con el tiempo. Por ejemplo, si empujamos una roca de manera constante, se moverá lentamente y con el tiempo alcanzará la velocidad deseada.

**applyLinearImpulse:** A la pelota del medio, le aplicamos un impulso lineal. Los impulsos realizan cambios inmediatos en la velocidad del cuerpo. Por ejemplo, patear un balón de fútbol cambiará inmediatamente la velocidad del balón y lo hará moverse lejos, pero ¿qué sucede si pateas una roca Probablemente no se moverá muy lejos.

**linearVelocity:** A la pelota de la derecha, le establecemos la velocidad, por lo que no importa el tamaño ni la masa del cuerpo, simplemente la pelota se movera a la velocidad deseada.

<Note
   title="Nota"
   text="
Por defecto, estas fuerzas se aplican en el centro de los cuerpos, por lo que se moverán hacia arriba y no rotarán.
"/>

## Conclusión
En este articulo aprendimos la diferencia entre la fuerza, el impulso y la velocidad lineal en Forge2D. Más adelante podremos usar estos conceptos para hacer que los personajes de nuestros juegos se muevan dentro del mundo. 

Por último, recuerda que puedes encontrar el código fuente de todos los tutoriales en [Github][2], y también puedes probar todos los ejemplos en tu navegador.

<MyIframe
  src="https://yayo-arellano.github.io/flutter_games_compilation/flutter_learn_flame"
  aspectRatio="4/3"
  width="70%"
  widthSmallScreen="100%"  
/>

## Videotutorial en YouTube

<Youtube id="CkgO5lq7dw4"/>

[1]: /page/3vhEGisL7CzJFTw0kc2A 'introduccion'
[2]: https://github.com/Yayo-Arellano/flutter_games_compilation/tree/main/flutter_learn_flame 'Source code']]></content:encoded>
            <category>Flutter</category>
            <category>Flame</category>
            <enclosure url="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FyPFeIgyd6Jnz8Pgby6FU%2Fc5467dd0-6902-49c4-a709-dab66fffc4af.png?alt=media&amp;token=037e1b53-8979-4ff2-9efb-6af843fdf50f" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Flame: Atributos de los cuerpos: fricción, densidad y restitución]]></title>
            <link>https://yayocode.com/es/post/Gt8E8u7W9ftte6nRp1fu</link>
            <guid>Gt8E8u7W9ftte6nRp1fu</guid>
            <pubDate>Sun, 11 Jun 2023 12:36:38 GMT</pubDate>
            <description><![CDATA[En este artículo, aprenderemos sobre los atributos de los cuerpos en Flame y Forge2D: densidad, fricción y restitución ]]></description>
            <content:encoded><![CDATA[<img src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FGt8E8u7W9ftte6nRp1fu%2Fc9b41179-6f34-4632-96dc-17e87e21fcfa.png?alt=media&token=b0e28c76-b827-45a5-989a-bc388b7fdd38" alt="Cover image"> 
 En Forge2D los cuerpos tienen tres atributos muy importantes: densidad, fricción y restitución. Estos atributos determinan el comportamiento de los cuerpos dentro del mundo.

## Densidad

La densidad se refiere a la cantidad de masa que tiene por unidad de área. Se expresa en kilogramos por metro cuadrado (kg/m²). Una densidad mayor significa que el cuerpo es más pesado en relación a su tamaño. Por ejemplo, un objeto metálico tendría una densidad alta, mientras que un objeto liviano como una globo tendría una densidad baja ya que está principalmente esta lleno de aire.

## Fricción

La fricción entra en juego cuando dos cuerpos se deslizan entre sí. Un valor de fricción más alto significa que el cuerpo tendrá una mayor resistencia al deslizamiento. Por ejemplo un bloque de hielo tendría una fricción muy baja, pero una pelota de goma tendría una fricción alta. 

## Restitución

La restitución determina cuánto rebotará un cuerpo después de una colisión. Por ejemplo, una roca tendría una restitución muy baja y no rebotaría mucho, mientras que una pelota de baloncesto tendría una restitución alta y rebotaría fácilmente.

## Ejemplo

La mejor forma de aprender es a través de un ejemplo práctico. En este caso, utilizaremos un pequeño ejemplo para analizar el comportamiento de un cuerpo al modificar sus atributos. Vamos a crear un suelo inclidado y dejaremos caer un cuerpo sobre él. Observaremos su comportamiento y luego vamos a ajustar sus atributos. Despues, dejaremos caer el objeto otra vez para ver los cambios y observar cómo se comporta en esta nueva configuración.

Para modificar la fricción, la densidad o la restitución de un objeto, se puede utilizar el accesorio correspondiente de la siguiente manera:

```dart
final fixtureDef = FixtureDef(shape)
	..density = 10
	..friction = .5
	..restitution = .5;
```

Comenzaremos creando la clase `Box` que representará el cuerpo que dejaremos caer sobre el suelo. Utilizaremos la forma `PolygonShape` para definir su forma:

```dart
class Box extends BodyComponent {
  @override
  Body createBody() {
    final bodyDef = BodyDef(
      position: Vector2(10, 0),
      type: BodyType.dynamic,
    );

    final shape = PolygonShape()..setAsBoxXY(.25, .25);
    final fixtureDef = FixtureDef(shape)
      ..density = 10
      ..friction = .5
      ..restitution = .5;
    return world.createBody(bodyDef)..createFixture(fixtureDef);
  }
}
``` 

Luego, crearemos la clase `Floor` que representará el suelo. Utilizaremos la forma `EdgeShape` para definir su forma:

```dart
class Floor extends BodyComponent {
  @override
  Body createBody() {
    final bodyDef = BodyDef(
      position: Vector2(0, worldSize.y - .75),
      type: BodyType.static,
    );

    final shape = EdgeShape()..set(Vector2.zero(), Vector2(worldSize.x, -2));
    final fixtureDef = FixtureDef(shape)..friction = .1;
    return world.createBody(bodyDef)..createFixture(fixtureDef);
  }
}
```

Ahora vamos a añadir el objeto `Floor` y el objeto `Box` al juego:

```dart
class GameLesson03 extends MyGame {
  @override
  Future<void> onLoad() async {
    super.onLoad();
    add(Floor());
    add(Box());
  }
}
```

Vamos a ejecutar el código y, como resultado, obtendremos lo siguiente:

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fen%2Fposts%2FGt8E8u7W9ftte6nRp1fu%2Ff30a6649-70b6-48da-b083-abd537efbbca.gif?alt=media&token=99f67b43-b140-4f1d-8fcb-4c11df9d6767"
    width="65%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="600/367"
    description="Caja rebotando muy bajo"
/>

Veamos qué sucede si modificamos el valor de restitución por `restitution = 1`. Podemos realizar este cambio de la siguiente manera:

```dart
final fixtureDef = FixtureDef(shape)
	..density = 10
	..friction = .5
	..restitution = 1;
```

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fen%2Fposts%2FGt8E8u7W9ftte6nRp1fu%2F93b0cf64-1e07-4bf3-8e26-e5331fc516d5.gif?alt=media&token=2f354a78-81f2-4e5d-82d5-975a19706b5d"
    width="65%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="600/367"
    description="Caja rebotando muy alto"
/>

¿Has notado la diferencia? Cuando el valor de restitución se establece en `restitution = 1`, la caja rebotará a una altura considerablemente mayor. Ahora puedes experimentar con los valores de densidad, fricción y restitución para obtener resultados diferentes.

## Conclusión

En este articulo aprendimos que la densidad, la fricción y la restitución son atributos clave en Forge2D que influyen en cómo se mueven, chocan y reaccionan los cuerpos en nuestro juego. Tambien vimos que cambiar el valor de alguno de ellos puede hacer una gran diferencia en el comportamiento de los cuerpos

Por último, recuerda que puedes encontrar el código fuente de todos los tutoriales en [Github][2], y también puedes probar todos los ejemplos en tu navegador.

<MyIframe
  src="https://yayo-arellano.github.io/flutter_games_compilation/flutter_learn_flame"
  aspectRatio="4/3"
  width="70%"
  widthSmallScreen="100%"  
/>

## Videotutorial en YouTube

<Youtube id="AhU5ifkRrI4"/>

[2]: https://github.com/Yayo-Arellano/flutter_games_compilation/tree/main/flutter_learn_flame 'Source code']]></content:encoded>
            <category>Flutter</category>
            <category>Flame</category>
            <enclosure url="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FGt8E8u7W9ftte6nRp1fu%2Fc9b41179-6f34-4632-96dc-17e87e21fcfa.png?alt=media&amp;token=b0e28c76-b827-45a5-989a-bc388b7fdd38" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Flame: Tipos de cuerpos: dinámicos, estáticos y cinemáticos]]></title>
            <link>https://yayocode.com/es/post/7hp4dLXobT8GJ3NtzVOS</link>
            <guid>7hp4dLXobT8GJ3NtzVOS</guid>
            <pubDate>Sun, 11 Jun 2023 01:02:07 GMT</pubDate>
            <description><![CDATA[En este artículo, aprenderemos sobre los tipos de cuerpos en Flame y Forge2D: dinámicos, estáticos y cinemáticos ]]></description>
            <content:encoded><![CDATA[<img src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2F7hp4dLXobT8GJ3NtzVOS%2F70b09a69-5ce4-4f2b-b898-272431ffa2d3.png?alt=media&token=5bd5c531-b355-44e7-a743-acd51fe5e720" alt="Cover image"> 
 En Forge2D existen tres tipos diferentes de cuerpos: estáticos, dinámicos y cinemáticos.

## Cuerpos estáticos

Los cuerpos estáticos no se mueven, no reaccionan a ninguna fuerza, impulso o colisión. Los cuerpos estáticos son perfectos para representar el suelo, las paredes y cualquier objeto que no necesite moverse.

Vamos a crear una nueva clase llamada `FloorStatic` que contiene un cuerpo estático y se encargará de representar el suelo de nuestro mundo:

```dart
class FloorStatic extends BodyComponent {
  @override
  Body createBody() {
    final bodyDef = BodyDef(
      position: Vector2(0, worldSize.y - .75),
      type: BodyType.static,
    );

    final shape = EdgeShape()..set(Vector2.zero(), Vector2(worldSize.x, 0));
    final fixtureDef = FixtureDef(shape);
    return world.createBody(bodyDef)..createFixture(fixtureDef);
  }
}
```

Podemos ver que el tipo del cuerpo es `BodyType.static` y la forma es `EdgeShape`. Este tipo de forma es ideal para representar el suelo porque es un simple segmento de línea.

Ahora agreguemos el objeto `FloorStatic` al juego:

```dart
class GameLesson02 extends MyGame {
  @override
  Future<void> onLoad() async {
    super.onLoad();
    add(FloorStatic());
  }
}
```

Después de ejecutar el código, veremos que hay una línea estática que cruza la pantalla horizontalmente.

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2F7hp4dLXobT8GJ3NtzVOS%2Fa4bd8d7c-8c4a-49e7-a29a-75ba060f1d49.png?alt=media&token=71e1bc65-4f0a-453f-a148-b3d42f952d0b"
    width="65%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="1454/972"
/>

## Cuerpos dinámicos

Los cuerpos dinámicos son los que se mueven y son afectados por las fuerzas físicas como la gravedad, impulsos, colisiones y otros eventos del mundo.

Vamos a crear una nueva clase llamada `BallDynamic` que contiene un cuerpo dinámico y se encargará de representar una pelota que cae al suelo:

```dart
class BallDynamic extends BodyComponent {
  @override
  Body createBody() {
    final bodyDef = BodyDef(
      position: Vector2(worldSize.x / 2, 0),
      type: BodyType.dynamic,
    );

    final shape = CircleShape()..radius = .35;
    final fixtureDef = FixtureDef(shape);
    return world.createBody(bodyDef)..createFixture(fixtureDef);
  }
}
```

En esta ocasión, el tipo del cuerpo es `BodyType.dynamic` y la forma es `CircleShape`. La pelota se posiciona verticalmente en la parte superior de la pantalla y está centrada horizontalmente.

Agreguemos el objeto `BallDynamic` al juego:

```dart
class GameLesson02 extends MyGame {
  @override
  Future<void> onLoad() async {
    super.onLoad();
    add(FloorStatic());
    add(BallDynamic());
  }
}
```

Cuando ejecutamos el código, podemos ver que la pelota cae de arriba hacia abajo hasta llegar al suelo.

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2F7hp4dLXobT8GJ3NtzVOS%2F44291bc6-5952-4bb0-9604-843c3fb2984d.gif?alt=media&token=3185350a-7dbb-4a66-bf7a-02bd16b6a632"
    width="65%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="600/369"
    description="Pelota cayendo desde su posicion inicial"
/>

## Cuerpos cinemáticos

Los cuerpos cinemáticos son muy similares a los cuerpos estáticos y dinámicos. ¿En qué sentido? Al igual que los cuerpos estáticos, no reaccionan a las fuerzas, pero al igual que los cuerpos dinámicos, pueden moverse. Para mover un cuerpo cinemático, debemos establecer su velocidad o posición.

Creemos la clase `BoxKinematic` que contendrá un cuerpo cinemático:

```dart
class BoxKinematic extends BodyComponent {
  @override
  Body create

Body() {
    final bodyDef = BodyDef(
      position: Vector2(worldSize.x / 2, worldSize.y / 2),
      type: BodyType.kinematic,
    );

    final shape = PolygonShape()..setAsBoxXY(.15, 1.25);

    final fixtureDef = FixtureDef(shape);
    return world.createBody(bodyDef)
      ..createFixture(fixtureDef)
      ..angularVelocity = radians(180);
  }
}
```

En esta ocasión, el tipo de cuerpo es `BodyType.kinematic` y la forma es `PolygonShape`. La posición del cuerpo está en el centro de la pantalla y tiene una `angularVelocity`, lo que significa que rotará sobre su eje.

Agreguemos el objeto `BoxKinematic` al juego:

```dart
class GameLesson02 extends MyGame {
  @override
  Future<void> onLoad() async {
    super.onLoad();
    add(FloorStatic());
    add(BallDynamic());
    add(BoxKinematic());
  }
}
```

Cuando ejecutamos el código, podemos ver que la pelota cae, colisiona con la caja en rotación y finalmente alcanza el suelo y sale de la pantalla.

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2F7hp4dLXobT8GJ3NtzVOS%2Fabac5621-ae6f-4dda-b4b2-9e01397bb0e8.gif?alt=media&token=7e2179b6-2b25-4f74-9dd2-7ed0a6184690"
    width="65%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="600/369"
    description="Cuerpo cinemático rotando en el centro de la pantalla"
/>

Ten en cuenta que el cuerpo cinemático no se ve afectado por la gravedad ni por la colisión con la pelota. Siempre se mantendrá en la misma posición.

<Note
   title="Importante"
   text="
Recuerda que Forge2D trabaja con radianes y no con grados.
"/>

## Conclusión

Forge2D proporciona diferentes tipos de cuerpos para simular distintos comportamientos físicos en un mundo de simulación.

Los cuerpos **estáticos** no se mueven y son ideales para representar objetos inmóviles como el suelo y las paredes. Los cuerpos **dinámicos** son afectados por fuerzas físicas como la gravedad y pueden moverse en respuesta a ellas. Por último, los cuerpos **cinemáticos** pueden ser controlados manualmente estableciendo su posición o velocidad. 

Por último, recuerda que puedes encontrar el código fuente de todos los tutoriales en [Github][2], y también puedes probar todos los ejemplos en tu navegador.

<MyIframe
  src="https://yayo-arellano.github.io/flutter_games_compilation/flutter_learn_flame"
  aspectRatio="4/3"
  width="70%"
  widthSmallScreen="100%"  
/>

## Videotutorial en YouTube

<Youtube id="YV3eV0odBgM"/>

[2]: https://github.com/Yayo-Arellano/flutter_games_compilation/tree/main/flutter_learn_flame 'Source code']]></content:encoded>
            <category>Flutter</category>
            <category>Flame</category>
            <enclosure url="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2F7hp4dLXobT8GJ3NtzVOS%2F70b09a69-5ce4-4f2b-b898-272431ffa2d3.png?alt=media&amp;token=5bd5c531-b355-44e7-a743-acd51fe5e720" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Flame: El mundo, cuerpos, figuras y accesorios]]></title>
            <link>https://yayocode.com/es/post/pD63d2bqXJnaRU8Er80B</link>
            <guid>pD63d2bqXJnaRU8Er80B</guid>
            <pubDate>Fri, 09 Jun 2023 00:30:16 GMT</pubDate>
            <description><![CDATA[En este artículo, aprenderemos sobre el mundo, los cuerpos, las formas y los accesorios en Flame y Forge2D.]]></description>
            <content:encoded><![CDATA[<img src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FpD63d2bqXJnaRU8Er80B%2F6ec9361b-a6fd-4632-b2e7-e1bbc6e8c0d1.png?alt=media&token=8c2ea163-006e-4240-8823-829a45eb512f" alt="Cover image"> 
 ## El mundo (world)

En Forge2D, el mundo es como un contenedor donde suceden las simulaciones de objetos físicos. En él, podemos agregar diferentes objetos y ver cómo interactúan entre sí. Por ejemplo, imagina que estás jugando un videojuego donde puedes lanzar una pelota y ver cómo rebota en las paredes o choca con otros objetos. El mundo se encarga de calcular cómo se moverá la pelota, cómo se comportará al colisionar con otros objetos y cómo interactuará con la gravedad.

Tal vez no te diste cuenta, pero en el [artículo anterior](/post/XPECczhhAZgJDGcy69AO) ya creamos un mundo.

En Flame, para crear un mundo, debemos extender la clase `Forge2DGame` e internamente se creará el mundo.

```dart
class MyGame extends Forge2DGame {

}
```

También debemos definir la gravedad de nuestro mundo:

```dart
MyGame() : super(gravity: Vector2(0, 15)); 
```

La gravedad simula la fuerza de atracción que experimentan los objetos en la vida real. La gravedad es un vector que tiene una dirección y una magnitud, y afecta a los cuerpos dentro del mundo de Forge2D.

Podemos ajustar la gravedad de acuerdo con los requisitos específicos de la simulación. Se pueden tener valores de gravedad cero para crear un entorno sin gravedad, o se pueden establecer en diferentes direcciones y magnitudes para simular situaciones especiales.

En el fragmento de código anterior, definimos una gravedad con el vector `Vector2(0, 15)`, en otras palabras, no hay gravedad horizontal y los objetos son atraídos hacia abajo. Recuerda que en Forge2D, una unidad es igual a un metro.

Si deseas un mundo sin gravedad, entonces el código se verá así:

```dart
MyGame() : super(gravity: Vector2(0, 0)); 
```

## Cuerpos (bodies)

En Forge2D, los objetos se llaman **cuerpos** y son las piezas fundamentales en el mundo. Los podemos imaginar como objetos físicos reales, como una pelota, una caja o cualquier otra cosa que puedas tocar y mover.

Cada cuerpo tiene diferentes características, como su forma, tamaño, posición, velocidad y masa. Podemos decirle a un cuerpo cómo debe moverse, cómo debe reaccionar ante las fuerzas y cómo debe interactuar con otros cuerpos.

Por ejemplo, si tenemos un cuerpo que representa una pelota y le aplicamos una fuerza, el cuerpo se moverá según las leyes de la física. Si hay otros cuerpos cerca, podrían colisionar y rebotar entre sí, tal como lo harían objetos reales.

Además, los cuerpos en Box2D pueden tener diferentes tipos, como cuerpos estáticos que no se mueven, cuerpos dinámicos que responden a las fuerzas aplicadas y cuerpos cinemáticos que se mueven según una trayectoria específica.

En Flame, para crear un cuerpo, necesitamos crear una clase que extienda de `BodyComponent`.

```dart
class Ball extends BodyComponent {  
  @override  
  Body createBody() {  
     
  }  
}
```

Dentro de la función `createBody()`, creamos un `BodyDef` que contendrá los datos necesarios para crear el cuerpo:

```dart
final bodyDef = BodyDef(  
  position: Vector2(worldSize.x / 2, 0),  
  type: BodyType.dynamic,  
);
```

Podemos ver que este será un cuerpo **dinámico**, posicionado verticalmente en la parte superior de la pantalla y centrado horizontalmente.

## Formas (shapes)

En Forge2D, las formas determinan la apariencia y las propiedades físicas de los cuerpos en la simulación. Pueden ser círculos, rectángulos o polígonos, y tienen atributos como la densidad, la fricción y la restitución que influyen en cómo interactúan con otros cuerpos.

Si queremos crear una forma circular con radio de 0.35 el código es el siguiente: 

```dart
final shape = CircleShape()..radius = .35;
```

## Accesorios (fixtures)

Los accesorios son componentes asociados a los cuerpos que les dan forma, propiedades físicas y características de colisión. Podemos pensar en ellos como una especie de "accesorio" que se adjunta a un cuerpo para definir su comportamiento físico.

Un accesorio tiene una forma, densidad, fricción y restitución asociadas.

- La **densidad** determina la masa del cuerpo y cómo interactúa con otros cuerpos durante las colisiones. Un cuerpo con mayor densidad será más difícil de mover o empujar.
- La **fricción** determina la resistencia al deslizamiento de un cuerpo cuando entra en contacto con otros objetos.
- La **restitución** se refiere a la elasticidad de un cuerpo y determina cómo rebota después de una colisión.

Veamos cómo podemos crear un accesorio de forma circular:

```dart
final shape = CircleShape()..radius = .35;
final fixtureDef = FixtureDef(shape);
```

Ahora vamos a crear un cuerpo al cual le vamos a adjuntar el accesorio que hemos creado. El código se verá así:

``` Dart
final body = world.createBody(bodyDef)..createFixture(fixtureDef);
```

Juntando todo el código, la clase `Ball` se verá así:

```dart
class Ball extends BodyComponent {
  @override
  Body createBody() {
    final bodyDef = BodyDef(
      position: Vector2(worldSize.x / 2, 0),
      type: BodyType.dynamic,
    );

    final shape = CircleShape()..radius = .35;
    final fixtureDef = FixtureDef(shape);
    return world.createBody(bodyDef)..createFixture(fixtureDef);
  }
}
```

## Agregar la pelota al juego

Finalmente, debemos agregar la `Ball` al juego. Para ello, crearemos una nueva clase llamada `GameLesson01` que extienda de `MyGame`.

```dart
class GameLesson01 extends MyGame {

  @override
  Future<void> onLoad() async {
    super.onLoad();
    add(Ball());
  }
}
```

En esta nueva clase, sobreescribimos el método `onLoad()` para agregar una nueva instancia de `Ball` al juego utilizando la función `add()`.

Ahora, al ejecutar el código, podremos ver la pelota en acción, cayendo desde la parte superior hasta la parte inferior de la pantalla.

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FpD63d2bqXJnaRU8Er80B%2F7f78df68-ee74-43c8-b727-bb2ac2d241bd.gif?alt=media&token=ecdbd308-6d92-42d0-bd3d-dffcdc64325f"
    width="65%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="600/371"
/>

Puedes experimentar cambiando los valores de gravedad, el radio de la pelota, entre otros. ¿Cuáles son los resultados?

## Conclusión

Este artículo nos enseñó sobre el mundo de Forge2D y cómo agregar cuerpos a él. También aprendimos que un cuerpo puede tener diferentes formas y atributos.

Por último, recuerda que puedes encontrar el código fuente de todos los tutoriales en [Github][2], y también puedes probar todos los ejemplos en tu navegador.

<MyIframe
  src="https://yayo-arellano.github.io/flutter_games_compilation/flutter_learn_flame"
  aspectRatio="4/3"
  width="70%"
  widthSmallScreen="100%"  
/>

## Videotutorial en YouTube

<Youtube id="JOQAiuO7jq8"/>

[2]: https://github.com/Yayo-Arellano/flutter_games_compilation/tree/main/flutter_learn_flame 'Source code']]></content:encoded>
            <category>Flutter</category>
            <category>Flame</category>
            <enclosure url="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FpD63d2bqXJnaRU8Er80B%2F6ec9361b-a6fd-4632-b2e7-e1bbc6e8c0d1.png?alt=media&amp;token=8c2ea163-006e-4240-8823-829a45eb512f" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Flame: Comenzando: Ajustes iniciales]]></title>
            <link>https://yayocode.com/es/post/XPECczhhAZgJDGcy69AO</link>
            <guid>XPECczhhAZgJDGcy69AO</guid>
            <pubDate>Tue, 06 Jun 2023 13:14:40 GMT</pubDate>
            <description><![CDATA[Aprende Flutter Flame: Crea juegos 2D con Flutter y Dart en este curso]]></description>
            <content:encoded><![CDATA[<img src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FXPECczhhAZgJDGcy69AO%2Fcaf277e0-829b-4e66-ae2f-71663a506961.png?alt=media&token=2136f349-052c-46d4-ac83-f119bca6200d" alt="Cover image"> 
 ## Viewport

Antes de comenzar a codificar, debemos aprender algunos conceptos relacionados con la representación del juego en Flame. 

Supongamos que tenemos un dispositivo cuyo tamaño de pantalla abarca toda el área roja de la siguiente imagen:

<MyImage src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FXPECczhhAZgJDGcy69AO%2F26d109c9-c7ab-4207-bff9-75aed21a17bb.png?alt=media&token=e530646c-9842-42f3-b611-1ba0de34c10e"
    width="65%"
    priority="true"
    smallScreenWidth="100%"
    aspectRatio="838/419"
/>

**Canvas:** El área roja es el lienzo (canvas) y es el espacio disponible en el que podemos dibujar formas, imágenes, etc. La mayoría de las veces, el tamaño del canvas será igual al tamaño de la pantalla del dispositivo. Podemos utilizar el tamaño del canvas para conocer el tamaño del dispositivo en píxeles.

**Viewport:** El área negra es el viewport. Dado que tenemos que lidiar con diferentes pantallas, a menudo es necesario decidir cómo se manejarán diferentes tamaños de pantalla y resoluciones. El viewport intenta unificar múltiples tamaños de pantalla en una configuración única para tu juego mediante la traducción y redimensionamiento del lienzo.

En estos tutoriales, para facilitar la comprensión de todos los ejemplos, utilizaremos el `FixedResolutionViewport`. Este viewport permite trabajar con un tamaño de pantalla virtual. Esto significa que podemos asumir que la pantalla siempre tiene el mismo tamaño: `virtualWidth x virtualHeight`. 

Para mantener siempre la misma resolución, se agregarán barras rojas, como se muestra en la siguiente animación:

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FXPECczhhAZgJDGcy69AO%2Ff6158766-1d1a-42ee-83f8-daf320b78ea3.gif?alt=media&token=44bd3e09-32fe-4be7-97e5-99ba6696d6ee"
    width="65%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="600/345"
    description="Para mantener siempre la misma resolución, se agregarán barras rojas"
/>

En la imagen anterior, observa cómo al redimensionar la ventana se muestra el canvas rojo debajo; es decir, "agrega barras rojas" para mantener la relación de aspecto.

Al trabajar con Forge2D, recuerda que 1 unidad es igual a 1 metro. Esto significa que si tenemos un `FixedResolutionViewport` con valores de 1280 de ancho x 720 de altura y decidimos agregar un árbol que tenga la mitad de la altura, entonces para Forge2D tendremos un árbol de 360 metros, lo cual no tiene sentido. Para solucionar esto, agregamos un valor de zoom de 100 para que el tamaño del mundo se convierta en 12.8 de ancho x 7.2 de altura. Ahora, agregar un árbol de 3.6 metros tiene más sentido.

## Hora de escribir código

En estos tutoriales, asumimos que ya tienes conocimientos sobre cómo crear proyectos de Flutter en tu IDE favorito, por lo que no repasaremos los pasos de configuración y creación de un proyecto de Flutter.

El primer paso consiste en crear un nuevo proyecto de Flutter y agregar Flame. Solo debes agregar los paquetes necesarios al archivo `pubspec.yaml`:

```yaml
flame: ^1.8.0
flame_forge2d: ^0.14.0
```

Luego, crearemos una clase `MyGame`.

```dart
// Tamaño fijo del viewport
final screenSize = Vector2(1280, 720);

// Tamaño del viewport con zoom de 100
final worldSize = Vector2(12.8, 7.2);

class MyGame extends Forge2DGame {
  
  // Muestra el número de cuerpos en el mundo.
  final totalBodies = TextComponent(position: Vector2(5, 690))
    ..positionType = PositionType.viewport;

  // Muestra los cuadros por segundo (FPS)
  final fps = FpsTextComponent(position: Vector2(5, 665));

  // Escala el tamaño de la pantalla establece la gravedad en 15
  MyGame() : super(zoom: 100, gravity: Vector2(0, 15));

  @override
  Future<void> onLoad() async {
    
    // Establece el FixedResolutionViewport
    camera.viewport = FixedResolutionViewport(screenSize);

    // Agrega un fondo negro al viewport
    add(_Background(size: screenSize)..positionType = PositionType.viewport);

    add(fps);
    add(totalBodies);
  }

  @override
  void update(double dt) {
    super.update(dt);
    
    // Actualiza el número de cuerpos en el mundo
    totalBodies.text = 'Cuerpos: ${world.bodies.length}';
  }

  @override
  Color backgroundColor() {
    
    // Pinta el fondo del canvas en color rojo
    return Colors.red;
  }
}

// Componente auxiliar que pinta un fondo negro
class _Background extends PositionComponent {
  _Background({required Size size}) : super(size: size);

  @override
  void render(Canvas canvas) {
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), blackPaint);
  }
}
```

Los componentes `totalBodies` y `fps` pertenecen a la clase `TextComponent` y se utilizan para mostrar en pantalla el número de cuerpos en el mundo y los cuadros por segundo, respectivamente. Puedes ver un ejemplo de cómo se muestran en la siguiente imagen:

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FXPECczhhAZgJDGcy69AO%2F9ed1d6b3-16e6-4679-8a79-1351aacdf733.png?alt=media&token=7f0b8057-ae0d-4f5f-ab28-3cff7b823a5d"
    width="65%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="811/543"
    description="Cuadros por segundo y número de cuerpos en el mundo"
/>

En el siguiente artículo, aprenderemos que son los cuerpos (body). Por ahora, podemos decir que los cuerpos son todos los componentes que forman parte de Forge2D y participan en simulaciones, colisiones, y demás. Normalmente, estos cuerpos son del tipo `PositionType.game`.

Por otro lado, existen componentes que no forman parte de Forge2D, como los componentes de la interfaz de usuario, el fondo, etc. Estos suelen ser del tipo `PositionType.viewport`.

## Conclusión

En este artículo, hemos aprendido sobre dos conceptos clave en la representación de juegos en Flame: el **lienzo (canvas)** y el **viewport**.

Se ha introducido el `FixedResolutionViewport`, que nos permite trabajar con un tamaño de pantalla virtual y mantener una resolución constante en diferentes dispositivos.

En cuanto al código, hemos creado una clase llamada `MyGame` que extiende `Forge2DGame`. Esta clase la utilizaremos en cada uno de los ejemplos que creemos a lo largo de este curso.

Por último, recuerda que puedes encontrar el código fuente de todos los tutoriales en [Github][2], y también puedes probar todos los ejemplos en tu navegador.

<MyIframe
  src="https://yayo-arellano.github.io/flutter_games_compilation/flutter_learn_flame"
  aspectRatio="4/3"
  width="70%"
  widthSmallScreen="100%"  
/>

## Videotutorial en YouTube

<Youtube id="MngIOZLf4lM"/>

[1]: https://flame-engine.org/ 'Flame'
[2]: https://github.com/Yayo-Arellano/flutter_games_compilation/tree/main/flutter_learn_flame 'Source code']]></content:encoded>
            <category>Flame</category>
            <category>Flutter</category>
            <enclosure url="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FXPECczhhAZgJDGcy69AO%2Fcaf277e0-829b-4e66-ae2f-71663a506961.png?alt=media&amp;token=2136f349-052c-46d4-ac83-f119bca6200d" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Flutter: Estructuras de control: expresión switch]]></title>
            <link>https://yayocode.com/es/post/hdiustpxfcmwautndxda</link>
            <guid>hdiustpxfcmwautndxda</guid>
            <pubDate>Sun, 28 May 2023 09:14:21 GMT</pubDate>
            <description><![CDATA[Aprende que es la expresión switch y como se usa en Dart y Flutter]]></description>
            <content:encoded><![CDATA[<img src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2Fhdiustpxfcmwautndxda%2Fbe4a0be4-e919-4206-a5ad-fe68e5291507.png?alt=media&token=3608dbd6-a647-48cc-91a2-0f4a38cd5e92" alt="Cover image"> 
 Anteriormente aprendimos sobre la estructura [switch case][2] que nos ayuda a asignar o ejecutar una lógica específica de acuerdo al resultado de la comparación evaluada.

Dart 3 introdujo la [expresión switch][1] que es una mejora al clásico [switch case][2]. Esta versión mejorada permite utilizar el switch como una expresión, lo que significa que puede producir un valor y asignarse a una variable o usarse en cualquier lugar donde se espere una expresión

Veamos un sencillo ejemplo, suponiendo que el primer día de la semana es lunes y es el día uno, martes es el día dos y así sucesivamente hasta llegar al domingo. Dado un número entero, queremos imprimir a que día corresponde. Usando el clásico [switch case][2], el código se verá así:

```Dart
// Ejemplo usando el clásico switch case
void main() {
  final diaActual = obtenerDia(5);
  print('Hoy es $diaActual'); // Hoy es Viernes
}

String obtenerDia(int dia) {
  switch (dia) {
    case 1:
      return 'Lunes';
    case 2:
      return 'Martes';
    case 3:
      return 'Miércoles';
    case 4:
      return 'Jueves';
    case 5:
      return 'Viernes';
    case 6:
      return 'Sábado';
    case 7:
      return 'Domingo';
    default:
      return 'El dia no existe';
  }
}
```

En el código anterior, hemos definido múltiples case con todos los valores de los días de la semana posibles y además incluimos un default para los valores que no coincidan con alguno de los días de la semana.

Utilizando la expresión switch, podemos hacer el código más compacto y fácil de leer:


```Dart
// Ejemplo usando la nueva expresión switch
void main() {
  final diaActual = obtenerDia(5);
  print('Hoy es $diaActual'); // Hoy es Viernes
}

String obtenerDia(int dia) {
  return switch (dia) {
    1 => 'Lunes',
    2 => 'Martes',
    3 => 'Miércoles',
    4 => 'Jueves',
    5 => 'Viernes',
    6 => 'Sábado',
    7 => 'Domingo',
    _ => 'El dia no existe',
  };
}
```

En este ejemplo, la expresión `dia` se evalúa y se compara con distintos casos utilizando la sintaxis `=>`. Si el valor de `dia` es 1, el resultado es `'Lunes'`. El guion bajo `_` se utiliza como un caso por defecto para cualquier otro valor que no coincida con los casos anteriores.

## Coincidencia de patrones ( pattern matching)

La coincidencia de patrones es una técnica que permite verificar si un valor cumple con un patrón específico y realizar acciones en función de ello. Se utiliza para realizar comparaciones más complejas y flexibles que las simples igualdades o desigualdades.

Si queremos saber si el día es "entre semana" o es un "fin de semana" podemos escribir el siguiente código:

```Dart
void main() {
  final diaActual = obtenerDia(4);
  print('Hoy es $diaActual'); // Entre semana
}

String obtenerDia(int dia) {
  return switch (dia) {
    >= 1 && <= 5 => 'Entre semana',
    6 || 7 => 'Fin de semana',
    _ => 'El dia no existe',
  };
}
```

También podemos asignar el valor producido por la expresión switch a una variable. El código anterior quedaría así:

```Dart
void main() {
  final dia = 5;
  
  final diaActual = switch (dia) {
    >= 1 && <= 5 => 'Entre semana',
    6 || 7 => 'Fin de semana',
    _ => 'El dia no existe',
  };
  
  print('Hoy es $diaActual'); // Entre semana
}
```

## Clases selladas (sealed class)

Las clases selladas en Dart se utilizan a menudo en combinación con las expresiones **switch**. La estructura **switch** puede hacer uso exhaustivo de las subclases de una clase sellada, asegurando que se manejen todos los casos posibles.

```Dart
void main() {
  print('El area del circulo es: ${calcularArea(Circulo(5))}');
  print('El area del cuadrado es: ${calcularArea(Cuadrado(10))}');
}

double calcularArea(Figura figura) {
  return switch (figura) {
    Cuadrado(lado: final lado) => lado * lado,
    Circulo(radio: final radio) => 3.14 * radio * radio,
  };
}

sealed class Figura {}

class Cuadrado extends Figura {
  Cuadrado(this.lado);
  final double lado;
}

class Circulo extends Figura {
  Circulo(this.radio);
  final double radio;
}
```

El ejemplo anterior funciona porque la clase `Figura` es **sealed**. Si no lo fuera tendríamos un error [non_exhaustive_switch_expression](https://dart.dev/tools/diagnostic-messages#non_exhaustive_switch_expression) 

## Cláusulas de guardia (Guard clauses)

Podemos usar la palabra clave **when** en conjunto con el **switch** para especificar una cláusula de guardia. Por ejemplo, si queremos deserializar un JSON:

```Dart
void main() {
  var animal1Json = {
    'nombre': 'Max',
    'tipo': 'Gato',
  };

  var animal2Json = {
    'nombre': 'Coco',
    'tipo': 'Perro',
  };

  var otroJson = {
    'nombre': 'Yayo',
    'edad': '23',
  };

  print(parseJson(animal1Json)); // El gato es: Max
  print(parseJson(animal2Json)); // El perro es: Coco
  print(parseJson(otroJson)); // Exception: Error al Deserializar el JSON
}

Animal parseJson(Map<String, dynamic> map) {
  return switch (map) {
    {
      'nombre': String nombre,
      'tipo': String tipo,
    }
        when tipo == 'Gato' => Gato(nombre),
    {
      'nombre': String nombre,
      'tipo': String tipo,
    }
        when tipo == 'Perro' => Perro(nombre),
    _ => throw Exception('Error al Deserializar el JSON')
  };
}

class Animal {
  Animal(this.nombre);
  final String nombre;
}

class Gato extends Animal {
  Gato(String nombre) : super(nombre);

  @override
  String toString() => 'El gato es: $nombre';
}

class Perro extends Animal {
  Perro(String nombre) : super(nombre);

  @override
  String toString() => 'El perro es: $nombre';
}

```

En el fragmento de código anterior tenemos tres JSON, uno es del tipo perro,  otro es de tipo gato y el ultimo no tiene ningun tipo. En el switch usamos la palabra `when tipo == 'Perro'` o `when tipo == 'Gato'` para evaluar si el JSON es de tipo perro o gato y crear una instancia de sus respectivos clases `Perro` o `Gato`. Y en caso de que el JSON tenga un formato no soportado, lanzamos una excepción. 

## Conclusión

En conclusión, las **expresiones switch** en Dart 3 ofrecen una mejora significativa sobre las estructuras de **switch case** tradicionales. Esta nueva versión permite utilizar el switch como una expresión, lo que simplifica el código y lo hace más legible. 

También se introduce la capacidad de realizar coincidencia de patrones, lo que brinda mayor flexibilidad en las comparaciones. Además, podemos hacer uso de las clases selladas en combinación con el switch, asegurando que todos los casos posibles sean manejados. 

Por último, las cláusulas de guardia, utilizando la palabra clave **when**, nos permiten realizar comprobaciones adicionales para deserializar JSON o manejar casos especiales. 

En general, estas mejoras en las expresiones switch en Dart 3 facilitan el desarrollo de aplicaciones más concisas y robustas.


[1]: https://dart.dev/language/branches#switch-expressions 'switch expression'
[2]: /post/6oGfE6g8vcJLJo5Fp1nc 'switch case']]></content:encoded>
            <category>Dart</category>
            <category>Curso Dart</category>
            <category>Flutter</category>
            <enclosure url="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2Fhdiustpxfcmwautndxda%2Fbe4a0be4-e919-4206-a5ad-fe68e5291507.png?alt=media&amp;token=3608dbd6-a647-48cc-91a2-0f4a38cd5e92" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[¿Qué hay de nuevo en Dart 3? expresión switch, patterns, records, modificadores de clases, etc.]]></title>
            <link>https://yayocode.com/es/post/xjnmnrp0opiwuclcfnyd</link>
            <guid>xjnmnrp0opiwuclcfnyd</guid>
            <pubDate>Sat, 20 May 2023 14:01:19 GMT</pubDate>
            <description><![CDATA[Este artículo es una introducción a las novedades de Dart 3. expresión switch, patterns, records, modificadores de clases, etc.]]></description>
            <content:encoded><![CDATA[<img src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2Fxjnmnrp0opiwuclcfnyd%2F31860103-64c3-4540-9714-e55e66283019.png?alt=media&token=60a71972-dd16-4473-8974-12723eb27ab9" alt="Cover image"> 
 Dart, el lenguaje de programación desarrollado por Google, ha experimentado importantes mejoras y novedades con el lanzamiento de su versión 3. Esta actualización trae consigo características y funcionalidades que hacen de Dart un lenguaje más eficiente, moderno y fácil de usar para el desarrollo de aplicaciones web y móviles.

Dart 3 fue presentado por Google durante el **Google I/O 2023** y fue anunciado como el lanzamiento más grande hasta la fecha.

## 100% Verificación de nulos (100% Sound null safety)

A partir de Dart 2.12, se introdujo una nueva característica llamada verificación de nulos (null safety). Esta característica tiene como objetivo principal mejorar la seguridad y la estabilidad del código al proporcionar verificaciones de nulos más sólidas. Si intentas asignar un valor nulo a una variable no nula, el compilador generará un error en tiempo de compilación. Esto ayuda a reducir la posibilidad de errores relacionados con nulos y a mejorar la robustez del código.

Ahora en Dart 3 el lenguaje ahora cuenta con un 100% de verificación de nulos.

## Records

Una nueva característica de Dart 3 son los [Records](https://dart.dev/language/records) que permiten que un solo objeto pueda contener varios objetos. Un caso de uso es cuando queremos regresar dos o más valores de una función.

Anteriormente cuando queríamos regresar más de un objeto de una función teníamos que crear una clase extra o agregar un paquete como [Tuple](https://pub.dev/packages/tuple). 

El siguiente fragmento de código muestra que para regresar la `edad` y el `nombre` de una función tenemos que crear una clase llamada `Usuario`:

```Dart
// Ejemplo sin usar records
void main() {
  
  final usuario = obtenerInformacion();

  print('Edad: ${usuario.edad}');
  print('Nombre: ${usuario.nombre}');
}

Usuario obtenerInformacion() {
  return Usuario(18, 'Yayo');
}

class Usuario {
  final int edad;
  final String nombre;

  Usuario(this.edad, this.nombre);
}
```

Ahora veamos la diferencia utilizando records:

```Dart
// Ejemplo usando records
void main() {

  final usuario = obtenerInformacion();
  
  print('Edad: ${usuario.$1}');
  print('Nombre: ${usuario.$2}');
}

(int, String) obtenerInformacion() {
  return (18, 'Yayo');
}
```

Podemos ver que usando **records**, nos hemos ahorrado unas cuantas líneas de código y ya no es necesario crear la clase `Usuario`.

## Patterns

Retomando el ejemplo anterior, un problema al usar **records** es que al no crear la clase `Usuario` no vamos a escribir `usuario.nombre` para acceder al nombre sino que vamos a escribir `usuario.$1`, y esto hace el código más difícil de comprender. Pero utilizando [patterns](https://dart.dev/language/patterns) podemos volver a escribir un código fácil de comprender.

Usando **patterns** podemos mejorar la comprensión del código del ejemplo anterior:

```Dart
void main() {
  final usuario = obtenerInformacion();

  print('Edad: ${usuario.edad}');
  print('Nombre: ${usuario.nombre}');
}

({int edad, String nombre}) obtenerInformacion() {
  return (edad: 12, nombre: 'Yayo');
}
```

Podemos ver que usando **records** y **patterns** podemos tener un código más legible y sin tener que crear una clase `Usuario` podemos utilizar `usuario.nombre` y `usuario.edad`.

### Patterns: desestructuración

Incluso podemos utilizar la **desestructuración** que es una técnica de **patterns** para extraer y asignar valores de una estructura de datos en variables individuales de forma concisa y eficiente. El código quedaría así:

```Dart
void main() {
  final (:edad, :nombre) = obtenerInformacion();

  print('Edad: $edad');
  print('Nombre: $nombre');
}

({int edad, String nombre}) obtenerInformacion() {
  return (edad: 12, nombre: 'Yayo');
}
```

### Patterns: coincidencia de patrones (matching)

La coincidencia de patrones es una técnica que permite verificar si un valor cumple con un patrón específico y realizar acciones en función de ello. Se utiliza para realizar comparaciones más complejas y flexibles que las simples igualdades o desigualdades.

En el siguiente fragmento de código, podemos ver que ahora podemos utilizar condiciones dentro de in `switch`:


```Dart
void main() {
  int numero = 15;

  switch (numero) {
    case 1:
      print('El numero es uno');

    case > 1 && < 20:
      print('El numero es mayor que diez y menor que 20');

    case > 20:
      print('El numero es mayor que 20');
  }
}
```

También podemos usar **patterns** para ver si los `case` de un `switch` coinciden con una colección:

```Dart
void main() {
  final usuarios = ['Yayo', 'Carlos'];

  switch (usuarios) {
      
    case ['Yayo', 'Carlos']:
      print('La lista contiene a Yayo y Carlos');

    case ['Diego']:
      print('La lista contiene a Diego');

    case ['Diana']:
      print('La lista contiene a Diana');
  }
}

```

#### Validar un JSON con patterns

Podemos usar **patterns** para validar un JSON y obtener sus valores. Supongamos que tenemos el siguiente JSON:

```Dart
var json = {
  'user': ['Lily', 13]
};
```

Si queremos evitar errores en tiempo de ejecución tenemos que hacer varias validaciones como por ejemplo ver si el tipo de dato es correcto, si el JSON no está vacío, etc. El código sin usar **patterns** se vería así:

```Dart
// Ejemplo sin usar patterns
if (json is Map<String, Object?> &&
    json.length == 1 &&
    json.containsKey('user')) {
  var user = json['user'];
  if (user is List<Object> &&
      user.length == 2 &&
      user[0] is String &&
      user[1] is int) {
    var nombre = user[0] as String;
    var edad = user[1] as int;
    print('El usuario $nombre tiene $edad años');
  }
}
```

Pero si utilizamos **patterns** en 3 lineas de código podemos validar el JSON:

```Dart
if (json case {'user': [String nombre, int edad]}) {
  print('El usuario $nombre tiene $edad años');
}
```

Todas las validaciones necesarias se realizan dentro del **if-case**:

- Se valida que `json` es un objeto tipo **Map**
  - Al ser un objeto de tipo **Map** automáticamente se valida que no sea `null`
- Se valida que `json` contiene la llave **user**
- Los tipos de dato de la lista son **String** e **int**
- Se crean las variables locales `nombre` y `edad`

## Expresión switch (switch expression)

Todos conocemos la estructura [switch case](3) que nos ayuda a asignar o ejecutar una lógica especifica de acuerdo al resultado de la comparación evaluada.

Veamos un sencillo ejemplo, suponiendo que el primer día de la semana es lunes y es el día uno, martes es el día dos y así hasta llegar al domingo. Dado un numero entero queremos imprimir a que día corresponde. Usando el clásico [switch case](3) el código se verá así:

```Dart
// Ejemplo usando el clásico switch case
void main() {
  final diaActual = obtenerDia(5);
  print('Hoy es $diaActual'); // Hoy es Viernes
}

String obtenerDia(int dia) {
  switch (dia) {
    case 1:
      return 'Lunes';
    case 2:
      return 'Martes';
    case 3:
      return 'Miércoles';
    case 4:
      return 'Jueves';
    case 5:
      return 'Viernes';
    case 6:
      return 'Sábado';
    case 7:
      return 'Domingo';
    default:
      return 'El dia no existe';
  }
}
```

Podemos ver que tuvimos que escribir varias veces `case` y `return`. Pero con Dart 3 y [switch expression][2] podemos hacer el código más compacto y simple de leer:

```Dart
// Ejemplo usando el nuevo expresión switch
void main() {
  final diaActual = obtenerDia(5);
  print('Hoy es $diaActual'); // Hoy es Viernes
}

String obtenerDia(int dia) {
  return switch (dia) {
    1 => 'Lunes',
    2 => 'Martes',
    3 => 'Miércoles',
    4 => 'Jueves',
    5 => 'Viernes',
    6 => 'Sábado',
    7 => 'Domingo',
    _ => 'El dia no existe',
  };
}
```

En este ejemplo, la expresión `dia` se evalúa y se compara con distintos casos utilizando la sintaxis `=>`. Si el valor de `dia` es 1, el resultado es `'Lunes'`. El guion bajo `_` se utiliza como un caso por defecto para cualquier otro valor que no coincida con los casos anteriores.

Y si queremos solo saber si es día es "entre semana" o es un "fin de semana" podemos escribir el siguiente código:

```Dart
void main() {
  final diaActual = obtenerDia(4);
  print('Hoy es $diaActual'); // Entre semana
}

String obtenerDia(int dia) {
  return switch (dia) {
    >= 1 && <= 5 => 'Entre semana',
    6 || 7 => 'Fin de semana',
    _ => 'El dia no existe',
  };
}
```

## Clases selladas (sealed class)

En Dart 3, la palabra clave utilizada para definir una [clase sellada][4] es `sealed`. Al igual que en otros lenguajes de programación, una clase sellada en Dart es una clase que no puede ser heredada por otras clases fuera de su declaración. Esto quiere decir que las subclases de la clase sellada deben ser declaradas en el mismo archivo. 

Las clases selladas en Dart se utilizan a menudo en combinación con las expresiones **switch**. La estructura **switch** puede hacer uso exhaustivo de las subclases de una clase sellada, asegurando que se manejen todos los casos posibles.

Aquí tienes un ejemplo de cómo utilizar una clase sellada en una expresión **switch**:

```Dart
void main() {
  final resultado = evaluarResultado(Success());
  print(resultado); // Exito: Peticion exitosa
}

String evaluarResultado(Result result) {
  return switch (result) {
    Success(mensaje: final mensaje) => 'Exito: $mensaje',
    Error(mensaje: final mensaje) => 'Error: $mensaje',
  };
}

sealed class Result {
  final String mensaje;

  Result(this.mensaje);
}

class Success extends Result {
  Success() : super('Peticion exitosa');
}

class Error extends Result {
  Error() : super('Peticion no exitosa');
}
```

<Note
   title="Recuerda que"
   text="
El ejemplo anterior funciona porque la clase `Result` es **sealed**. Si no lo fuera tendríamos un error [non_exhaustive_switch_expression](https://dart.dev/tools/diagnostic-messages#non_exhaustive_switch_expression) 
"/>

## Modificadores de clase

Los modificadores de clase son palabras reservadas utilizadas para controlar la visibilidad y el comportamiento de una clase. 

En Dart 3 se han agregados los siguientes modificadores de clase:

- `base`
- `final`
- `interface`
- `sealed`

En este articulo no hablaremos de cada uno de ellos, pero podemos visitar la [documentación][5] para ver en detalle el funcionamiento. 

## Conclusión

En Dart 3 se han agregado varias características que van a hacer la vida de los programadores mucho más fácil al hacer que Dart sea un lenguaje más expresivo.

Para mí en particular lo mejor son **patterns** y **records** porque ahora puedo hacer más con mucho menos código.

Espero que te haya gustado este articulo y si tienes alguna pregunta no olvides seguirme en mis redes sociales.

[2]: https://dart.dev/language/branches#switch-expressions 'switch expression'
[3]: /post/6oGfE6g8vcJLJo5Fp1nc 'switch case'
[4]: https://dart.dev/language/class-modifiers#sealed 'sealed class'
[5]: https://dart.dev/language/class-modifiers 'modificadores de clase']]></content:encoded>
            <category>Dart</category>
            <category>Flutter</category>
            <enclosure url="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2Fxjnmnrp0opiwuclcfnyd%2F31860103-64c3-4540-9714-e55e66283019.png?alt=media&amp;token=60a71972-dd16-4473-8974-12723eb27ab9" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Cambia el icono de tu aplicación creada en Flutter sin usar ningún paquete]]></title>
            <link>https://yayocode.com/es/post/YnM2AP2q4mSwepvuBd32</link>
            <guid>YnM2AP2q4mSwepvuBd32</guid>
            <pubDate>Mon, 26 Dec 2022 07:00:34 GMT</pubDate>
            <description><![CDATA[Cambiar el icono de una aplicación creada en Flutter es tan fácil que no debe tomar más de 5 minutos en cambiarlo para iOS y Android]]></description>
            <content:encoded><![CDATA[<img src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FYnM2AP2q4mSwepvuBd32%2Fb20e4ae8-f3b1-4376-9458-d83d04547167.png?alt=media&token=5f97863a-cc30-4ee3-8921-74d8b7a2172a" alt="Cover image"> 
 <Youtube id="hzdwzpvKX6M"/>

En este articulo vamos a cambiar los iconos de iOS y Android en una aplicación creada con Flutter. En caso de tener un icono vamos a usar una herramienta para generar las diferentes resoluciones de los iconos, pero si no tenemos ningún icono vamos a crear uno desde cero.

## ¿Por qué necesitamos iconos en diferentes resoluciones?

Tanto Android como iOS tienen diferentes requerimientos en el tamaño de los iconos para dar soporte a dispositivos con diferentes resoluciones. Puedes encontrar más información en la documentación oficial:

- Android: [Diferentes densidades de píxeles][1]
- iOS: [App Icons][2]

A demás de ser importante para el soporte a dispositivos con diferentes resoluciones, hay casos que Apple o Google no van a aceptar tu aplicación en sus tiendas si no agregas los iconos necesarios.

## App Icon Generator

Ya tengo un icono. ¿Cómo genero las diferentes resoluciones?. La respuesta es [App Icon Generator.][3] Supongamos que nada más tenemos un icono de 1024x1024 pixeles como el de la siguiente imagen:

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FYnM2AP2q4mSwepvuBd32%2F02321d20-1df3-45cb-b849-9c44badd6c2b.png?alt=media&token=841109b5-dffd-4abc-8249-0fa435d98439"
    width="30%"
    priority="true"
    smallScreenWidth="50%"
    aspectRatio="1024/1024"
    description="Icono de 1024x1024"
/>

Y queremos crear las diferentes resoluciones de iconos para iOS y Android, una opción es utilizar un editor de imágenes como Adobe Photoshop y cambiar el tamaño, pero como son muchos iconos el proceso es muy tardado. Otra opción es utilizar [App Icon Generator][3] que con unos cuantos clics genera los iconos necesarios. 

[App Icon Generator][3] es una herramienta que simplifica la creación de los iconos y todas las resoluciones necesarias para iOS y Android.

En la página web de [App Icon Generator][3] agregamos nuestro icono, seleccionamos las plataformas **iPhone**, **iPad** y **Android** y damos clic en **"Generate"**

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FYnM2AP2q4mSwepvuBd32%2F65f7dcdf-2c76-4dd3-a966-4ed249cd8eeb.png?alt=media&token=e0b737bb-d153-4fc4-a7de-1fbb495dff09"
    width="65%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="1024/633"
    description="Página web de App Icon Generator"
/>

Al dar clic en **"Generate"** Se va a descargar un archivo comprimido que contiene los siguientes archivos:

- **android**: Esta carpeta contiene los iconos necesarios para Android
- **Assets.xcassets**: Esta carpeta contiene los iconos necesarios para iOS
- **appstore.png**: Este icono se puede usar en la App Store
- **playstore.png**: Este icono se puede usar en la Google Play

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FYnM2AP2q4mSwepvuBd32%2F278a3656-a8a8-4986-87ba-809f2977a4f2.png?alt=media&token=4cb84e57-ea7b-4d0a-b6dd-8f563f82ab09"
    width="65%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="1024/602"
    description="Archivos generados por App Icon Generator"
/>

Una vez que hemos generado los iconos en sus diferentes resoluciones podemos agregarlos al proyecto de Flutter.

## IconKitchen

No tengo ningún icono. ¿Cómo creo uno y genero las diferentes resoluciones? La respuesta es [IconKitchen][4].

[IconKitchen][4] es una herramienta que nos permite crear iconos para iOS, Android, Web, etc. Y además genera todas las resoluciones necesarias.

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FYnM2AP2q4mSwepvuBd32%2F7612b6d3-908e-4465-87af-dab97f8c94b2.png?alt=media&token=52f31d2d-28b9-46f3-a121-de80fe26fb7d"
    width="65%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="1024/556"
    description="Página web de IconKitchen"
/>

En [IconKitchen][4] podemos crear un icono a partir de iconos predefinidos, texto o imágenes y también nos permite cambiar propiedades como el color del fondo, la textura, el relleno, la figura, etc. 

Una vez que tengamos listo el icono damos clic en **"Download"** y se va a descargar un archivo comprimido que contiene los siguientes archivos:

- **android**: Esta carpeta contiene los iconos necesarios para Android
- **ios**: Esta carpeta contiene los iconos necesarios para iOS

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FYnM2AP2q4mSwepvuBd32%2F8afee37f-da28-41ad-9f3d-e069b03a972c.png?alt=media&token=68ea7d5f-e9d1-4668-adfd-a2a9f0dc8f40"
    width="65%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="1024/645"
    description="Archivos generados por IconKitchen"
/>

Una vez que hemos creado y generados los iconos en sus diferentes resoluciones podemos agregarlos al proyecto de Flutter.

## Cambiar icono en iOS

No importa si utilizaste [IconKitchen][4] o [App Icon Generator][3] para generar los iconos de iOS. El contenido es muy similar, lo único que puede variar es el nombre de las imágenes. En este tutorial vamos a usar las imágenes generadas por [IconKitchen][4], veamos el contenido:

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FYnM2AP2q4mSwepvuBd32%2F0dda8aa9-922e-4058-9182-741ad1ba81d1.png?alt=media&token=cc5e42cd-9572-467d-8054-6972228f0c19"
    width="65%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="1024/700"
    description="Iconos para iOS creados con IconKitchen"
/>

Para cambiar el icono en iOS tenemos que abrir el proyecto de **ios** en Xcode. Si usas Visual Studio Code sigue las siguientes instrucciones o puedes saltar a la siguiente sección si usas Android Studio:

### Abrir proyecto de **ios** en Xcode con Visual Studio Code

- 1- Damos clic derecho en la carpeta de **ios**
- 2- Damos clic en "Open in Xcode"

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FYnM2AP2q4mSwepvuBd32%2F047b084f-aa7d-4f0b-a558-03f8d7c7c16d.png?alt=media&token=92016585-85f9-4f6f-be20-6378bed5866c"
    width="65%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="1024/903"
    description="Abrir proyecto de ios en Xcode con Visual Studio Code"
/>

### Abrir proyecto de **ios** en Xcode con Android Studio

- 1- Damos clic derecho en la carpeta de **ios**
- 2- Seleccionamos la sección de Flutter
- 3- Damos clic en "Open iOS module in Xcode"

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FYnM2AP2q4mSwepvuBd32%2F44c6c17d-0e2e-4e56-b85a-a373629724a7.png?alt=media&token=e5baac0a-08fd-4867-8161-34157578dda0"
    width="65%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="1024/1155"
    description="Abrir proyecto de ios en Xcode con Android Studio"
/>

### Reemplazar iconos en Xcode

Ya tenemos el proyecto de **ios** abierto en Xcode, vamos a abrir la carpeta **AppIcon** que contiene los iconos actuales:

- 1- Abrimos la carpeta de "Runner"
- 2- Seleccionamos la carpeta de "Assets"
- 3- Damos clic en "AppIcon"

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FYnM2AP2q4mSwepvuBd32%2F1f06cff4-b3bb-40e1-8b05-7ddfe1356c12.png?alt=media&token=e667761e-4ba1-47ab-aef1-11433fb9b6b1"
    width="70%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="1024/731"
/>

Por último, solo tenemos que arrastrar los iconos que queremos reemplazar hacia Xcode como en la siguiente animación:

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FYnM2AP2q4mSwepvuBd32%2Fd5336b31-3b34-43f9-a93d-c96d4ca45083.gif?alt=media&token=b1d0e738-7c00-46a6-8d3e-cbdacb1652c7"
    width="75%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="1280/720"
    description="Reemplazando iconos de iOS"
/>

Muy bien, ya podemos cerrar Xcode y correr el proyecto de iOS con los nuevos iconos.

## Cambiar iconos en Android

Cambiar el icono en Android tambien es muy facil. En este tutorial vamos a usar las imágenes generadas por [IconKitchen][4], veamos el contenido:

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FYnM2AP2q4mSwepvuBd32%2Fa882ce45-f3ed-492c-b950-eb65232bba58.png?alt=media&token=a3328fe6-51d3-44f8-a267-ae12abdf56f0"
    width="65%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="1024/446"
/>

¿Dónde están los iconos? Bueno, los iconos están dentro de la carpeta **res** pero si abrimos esta carpeta podemos encontrar las siguientes carpetas:

- mipmap-anydpi-v26
- mipmap-mdpi
- mipmap-hdpi
- mipmap-xhdpi
- mipmap-xxhdpi
- mipmap-xxhdpi

Dentro de cada una de estas carpetas vamos a encontrar un conjunto de imágenes que crean nuestro icono, por ejemplo:

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FYnM2AP2q4mSwepvuBd32%2Fb4682b93-2f90-47bb-a8f3-888a85e08a17.png?alt=media&token=a245a4d5-68d4-4141-b7d6-9fa8423c8623"
    width="65%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="1024/446"
    description="Contenido de la carpeta mipmap-hdpi"
/>

¿Porque hay varias imágenes? En Android existen cientos de miles de dispositivos con diferentes "Android Launchers" que se encargan de mostrar la mejor forma de tu icono. Estos "Android Launchers" toman estas imágenes y crean el mejor icono posible para el dispositivo. Pero para cambiar el icono solo nos vamos a enfocar en todas las carpetas dentro de **res**.

<Note
   title="Android Launcher"
   text="
Básicamente es una aplicación que se encarga de mostrar la pantalla de inicio, ya sean los iconos, widgets, etc.
"/>

Vamos a abrir el proyecto de Flutter en nuestro IDE favorito no importa si es Visual Studio Code o Android Studio, el procedimiento es el mismo: 

- 1- Abrimos la carpeta **res** que se encuentra en **/android/app/src/main/res**
- 2- Borramos todas las carpetas que empiecen con el nombre de **mipmap-**

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FYnM2AP2q4mSwepvuBd32%2F5d2b753d-ec7d-4f6b-913d-5cafa93073bb.png?alt=media&token=680e9c22-d57f-4046-99c4-926ce446db48"
    width="65%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="1024/789"
    description="Borramos todas las carpetas que empiecen con el nombre de mipmap"
/>

Por último, solo tenemos que arrastrar los iconos que queremos agregar a la carpeta **res** como en la siguiente animación:

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FYnM2AP2q4mSwepvuBd32%2F0675a447-e57c-456c-9533-3b5c5f7ebe53.gif?alt=media&token=e8581a63-6fcf-4063-aedd-60d1569bbbb0"
    width="65%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="1280/720"
/>

Muy bien, ya podemos correr el proyecto de Android con los nuevos iconos.

## Conclusion y videotutorial en YouTube

Corramos el proyecto de iOS y Android para ver el resultado final:

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FYnM2AP2q4mSwepvuBd32%2F00c135b5-acc8-417d-b742-5f15df87dcf5.png?alt=media&token=150dc6bd-64a5-4821-8b5a-dfbb3099134a"
    width="65%"
    priority="false"
    smallScreenWidth="100%"
    aspectRatio="1024/1078"
    description="Izquierda icono en iOS, derecha icono en Android"
/>

En este tutorial creamos un icono para iOS y Android en sus diferentes resoluciones, y aprendimos que para agregarlos a un proyecto de Flutter es muy sencillo, unos cuantos clics y listo. 

[1]: https://developer.android.com/training/multiscreen/screendensities
[2]: https://developer.apple.com/design/human-interface-guidelines/foundations/app-icons/
[3]: https://www.appicon.co/
[4]: https://icon.kitchen/]]></content:encoded>
            <category>Flutter</category>
            <enclosure url="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FYnM2AP2q4mSwepvuBd32%2Fb20e4ae8-f3b1-4376-9458-d83d04547167.png?alt=media&amp;token=5f97863a-cc30-4ee3-8921-74d8b7a2172a" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Comparar objetos en Flutter con el paquete equatable]]></title>
            <link>https://yayocode.com/es/post/VK2XfvNw9Ac9Eqxo9MHF</link>
            <guid>VK2XfvNw9Ac9Eqxo9MHF</guid>
            <pubDate>Tue, 20 Dec 2022 11:06:31 GMT</pubDate>
            <description><![CDATA[Aprende como funciona la igualdad en Flutter y Dart y como nos ayuda el paquete Equatable al comparar dos objetos de la misma clase]]></description>
            <content:encoded><![CDATA[<img src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FVK2XfvNw9Ac9Eqxo9MHF%2F302fde3b-f2bf-4e0e-9155-33cf73efdff0.png?alt=media&token=6b25e523-3074-4f01-8f8e-415c80eca5ec" alt="Cover image"> 
 <Youtube id="ZSiIiR4UPC8"/>

En este artículo, vamos a ver cómo funciona la igualdad en Flutter y Dart y como el paquete [Equatable][1] nos ayuda a tener un código más limpio sin necesidad de escribir tanto código repetitivo.

Ya sabemos que si queremos comparar dos variables podemos usar el operador `==`. Por ejemplo, si queremos comparar dos cadenas de texto, el código es:

```dart
  final car1 = "Toyota";
  final car2 = "Toyota";
  print(car1 == car2); // Resultado: true
```

Y si queremos comparar dos números, el código es:

```dart
  final phone1 = 52123456;
  final phone2 = 52123456;
  print(phone1 == phone2); // Resultado: true
```

Veamos el resultado del código anterior en DartPad:

<DartPad
   id="b1c8dcf24198fc9dc4f235670315bbf7" 
   width="100%" 
   height="430px" 
   split="65" 
/>

## Comparar dos objetos de la misma clase

En los ejemplos anteriores vimos que es muy sencillo comparar dos variables usando `==` pero cuando queremos comparar dos objetos de la misma clase ya no es tan sencillo. 

Si tenemos la siguiente clase:

```dart
class Car {
  final String brand;

  Car(this.brand);
}
```
 
Y creamos dos objetos:

```dart
  final car1 = Car('Toyota');
  final car2 = Car('Toyota');
  print(car1 == car2); // false
```

A simple vista podemos decir que `car1` y `car2` son iguales, pero no es cierto. El resultado de comparar `car1` y `car2` es `false`

Corramos el código  anterior en DartPad:

<DartPad
   id="ec1720c52dff2370e58a84e56b1fc593" 
   width="100%" 
   height="470px" 
   split="72" 
/>

¿Porque pasa esto si todos sus valores son iguales? La respuesta es simple, en Dart por defecto el operador `==` verifica la posición en memoria y no las propiedades de cada objeto. Y ya que `car1` y `car2` están en posiciones de memoria diferente, el resultado de la comparación es `false`.

## Usando el constructor `const`

Utilizar el constructor `const` le dice a Dart que solo debe crear una instancia de una clase para un conjunto de valores dados en tiempo de compilación. Corramos el código  anterior en DartPad pero esta vez utilizando constructores `const`:

<DartPad
   id="22513e68ef1aa42e8ee69be896f68cf5" 
   width="100%" 
   height="470px" 
   split="75" 
/>

Podemos ver que usar el constructor `const` resuelve el problema al comparar dos objetos, pero, esto solo funciona para objetos de los cuales conocemos su valor en tiempo de compilación. No hay forma de utilizar el constructor `const` en objetos creados en tiempo de ejecución. Es aquí donde entra el paquete [Equatable][1]

## El paquete Equatable

Sin utilizar el paquete [Equatable][1] para comparar las propiedades de dos objetos y ver si son iguales tenemos que sobreescribir `==` y `hashCode`. El codigo de la clase `Car` sera:

```dart
class Car {
  final String brand;

  const Car(this.brand);

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is Car && 
    runtimeType == other.runtimeType && 
    brand == other.brand;

  @override
  int get hashCode => brand.hashCode;
}
```

[Equatable][1] es un paquete que simplifica el proceso de comparar dos objetos de la misma clase sin sobreescribir `==` y `hashCode`.

Internamente [Equatable][1] sobreescribe `==` y `hashCode` para que nosotros no tengamos que perder tiempo escribiendo codigo repetitivo.

Utilizando [Equatable][1] el codigo anterior sera:

```dart
import 'package:equatable/equatable.dart';

class Car extends Equatable {
  const Car(this.brand);
  
  final String brand;

  @override
  List<Object> get props => [brand];
}
```

y si agregamos mas propiedades como `name` el codigo sera:

```dart
class Car extends Equatable {
  const Car(this.brand, this.name);

  final String brand;
  final String name;

  @override
  List<Object> get props => [brand, name];
}
```

Corramos el codigo en DartPad para ver el resultado:

<Note
   title="Imporante"
   text="
Hay un Bug en DartPad que no permite correr el ejemplo con Equatable. [https://github.com/dart-lang/dart-pad/issues/2356](https://github.com/dart-lang/dart-pad/issues/2356)
"/>



<DartPad
   id="b93b70338e0d6db5aaf062f225645781" 
   width="100%" 
   height="570px" 
   split="80" 
/>

Podemos ver que el resultado es `true` cuando los valores de las propiedades son los mismos. Intenta cambiando los valores de `car2` ¿Cuál es el resultado?

## Conclusiones

El paquete [Equatable][1] es muy importante si queremos comparar dos objetos por el valor de sus propiedades, sin el tendríamos que escribir mucho código repetitivo.

{/*

## Videotutorial en YouTube

Puedes ver el siguiente video donde explico paso a paso la igualdad en Dart y el uso del paquete [Equatable][1].

<Youtube id="xxxxxxx"/>

*/}
[1]: <https://pub.dev/packages/equatable> 'equatable'
]]></content:encoded>
            <category>Flutter Tip</category>
            <category>Dart</category>
            <enclosure url="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FVK2XfvNw9Ac9Eqxo9MHF%2F302fde3b-f2bf-4e0e-9155-33cf73efdff0.png?alt=media&amp;token=6b25e523-3074-4f01-8f8e-415c80eca5ec" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Flutter Tip: Multiplicar en vez de dividir]]></title>
            <link>https://yayocode.com/es/post/T5tGKVR3vJvqAQI7gj5H</link>
            <guid>T5tGKVR3vJvqAQI7gj5H</guid>
            <pubDate>Thu, 15 Dec 2022 00:41:32 GMT</pubDate>
            <description><![CDATA[En este artículo te muestro como calcular la altura y ancho de un widget de una forma sencilla]]></description>
            <content:encoded><![CDATA[<img src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FT5tGKVR3vJvqAQI7gj5H%2F564711c8-6402-4340-840f-728e762a632f.png?alt=media&token=5af87685-a46e-4f89-a270-7048a07f105d" alt="Cover image"> 
 Hay veces que queremos calcular el ancho o la altura de un widget y tomamos como referencia el espacio de pantalla disponible. Por ejemplo en el siguiente fragmento de código, la altura de cada  `Container` es la mitad del tamaño de la pantalla o `MediaQuery.of(context).size.height / 2`. 

```dart
Column(
       children: [
          Container(
            color: Colors.blue,
            height: MediaQuery.of(context).size.height / 2,
          ),
          Container(
            color: Colors.green,
            height: MediaQuery.of(context).size.height / 2,
          )
        ],
      )
```

Como podemos ver cada container ocupa la mitad de la pantalla:

<MyImage
    src="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FT5tGKVR3vJvqAQI7gj5H%2F76a74821-6ef7-4836-8913-e7b3c172267e.png?alt=media&token=c6d3d08a-d018-4415-84c2-1b9520b4ef98"
    width="50%"
    priority="true"
    smallScreenWidth="60%"
    aspectRatio="1370/2534"
/>

## Multiplicar en vez de dividir

Pero si el fragmento de código `MediaQuery.of(context).size.height / 2` lo cambiamos por una multiplicación `MediaQuery.of(context).size.height * 0.5` vamos a obtener algunas ventajas: 

- Es más fácil decir "La altura del widget es el 50% del tamaño de la pantalla" (0.5). 
- En varios post en stackoverflow se habla de que multiplicar es una operación menos costosa que una división [post 1](https://stackoverflow.com/questions/18165047/multiply-by-0-5-rather-than-divide-by-2), [post 2](https://stackoverflow.com/questions/4125033/floating-point-division-vs-floating-point-multiplication), [post 3](https://stackoverflow.com/questions/226465/should-i-use-multiplication-or-division).
 
Así que, el código después de reemplazarlo por la multiplicación quedaria asi: 

```dart
Column(
        children: [
          Container(
            color: Colors.blue,
            height: MediaQuery.of(context).size.height * 0.5,
          ),
          Container(
            color: Colors.green,
            height: MediaQuery.of(context).size.height * 0.5,
          )
        ],
      )
```

¿Cómo quedaría el código si quiero que la altura del `Container` sea el 25% de la altura y el verde el 75% de la altura? Así:

```dart
Column(
        children: [
          Container(
            color: Colors.blue,
            height: MediaQuery.of(context).size.height * 0.25,
          ),
          Container(
            color: Colors.green,
            height: MediaQuery.of(context).size.height * 0.75,
          )
        ],
      )
```

## Videotutorial en YouTube

Corre el código en tu [navegador con dartpad](https://dartpad.dev/?id=585c4466b8ec94fa4b5f59b1712c7923) y ve los resultados por ti mismo. También puedes ver el videotutorial de este artículo en youtube:

<Youtube id="9DrvWMoZ9Eg"/>]]></content:encoded>
            <category>Flutter</category>
            <category>Flutter Tip</category>
            <enclosure url="https://firebasestorage.googleapis.com/v0/b/yayo-code-blog-prod.appspot.com/o/locales%2Fes%2Fposts%2FT5tGKVR3vJvqAQI7gj5H%2F564711c8-6402-4340-840f-728e762a632f.png?alt=media&amp;token=5af87685-a46e-4f89-a270-7048a07f105d" length="0" type="image/png"/>
        </item>
    </channel>
</rss>