1. npx @react-native-community/cli movies

Haremos una aproximacion al patron repositorio para gestionar nuestro proyecto

La estructura de nuestras carpetas seran las siguientes:

__tests__
android
ios
src
  ├── config
  ├── core
  ├── infrastructure
  └── presentation
      ├── componentes
      ├── hooks
      ├── navigation
      └── screens
          ├── details
          │   └── DetailsScreen.tsx
          └── home
              └── HomeScreen.tsx
Main.tsx
Gemfile
README.md
app.json
babel.config.js
index.js
jest.config.js
metro.config.js
package-lock.json
package.json
tsconfig.json

El siguiente paso sera instalar el stack navigation como ya hicimos en post anteriores. usaremos la documentacion oficial de https://reactnavigation.org/

Una vez tengamos lo pre-requisitos instalamos stack navigation

1.npm install @react-navigation/stack 2. npm install react-native-gesture-handler 3. creamos en el mismo nivel que index.js el fichero gesture-handler.native.js con el siguiente contenido :

import 'react-native-gesture-handler';

y lo importamos en index.js

import { AppRegistry } from 'react-native';
import { name as appName } from './app.json';
import { Main } from './src/Main';
import './gesture-handler';



AppRegistry.registerComponent(appName, () => Main);
  1. creamos en el folder src/presentation/navigation el componente StackNav.tsx con lo siguente :
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import { HomeScreen } from '../screens/home/HomeScreen';
import { DetailsScreen } from '../screens/details/DetailsScreen';

export type RootStackParams = {
  Home: undefined;
  Details: { movieId: number };
}

const Stack = createStackNavigator<RootStackParams>();

export const StackNav = () => {
  return (
    <Stack.Navigator screenOptions={{
      headerShown: false,
    }}>
      <Stack.Screen name="Home" component={HomeScreen} />
      <Stack.Screen name="Details" component={DetailsScreen} />
    </Stack.Navigator>
  );
};
  1. En Main.tsx podemos el nuevo componente:
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { StackNav } from './presentation/navigation/StackNav';

export const Main = () => {
  return (
    <NavigationContainer>
      <StackNav />
    </NavigationContainer>
  );
};
  1. Reiniciamos la app para ver el cambio en el emulador.

Deberiamos ver la pantalla en blanco con la leyenda HomeScreen

API de peliculas

Ahora vamos a themoviedb para obtener la API KEY y nos registramos y vamos a api alli generamos la API KEY con una serie de preguntas personales y de uso de la api y nos daran una.

Una vez tengamos la key vamos a crear un .env en la raiz de nuestra app, aunque aun no tengamos configurado el uso de .env lo dejaremso preparado:

THE_MOVIE_DB_KEY=tu-key=aca

De paso creamos tambien .env.template el cual se subira al repositorio, recordemos que no debemos subbir nunca las keys a nuetro repositorio y tenemos que poner .env en .gitignore para evitar este problema, .env.template nos servira de ayuda para saber que variables necesita nuestra app. por lo que este archivo solo tendra la clave sin el valor:

THE_MOVIE_DB_KEY=

Como paso sugerido podemos poner en nuestro readme.md algo asi :

## Dev

1. clonar el proyecto 
2. instalar con `npm install`
3. clonar `.env.template` y setear las variables de entorno 
4. ejecutar `npm start`

Ahora Vamos a la documentacion de la API

usaremos la version 3 que es la estable.

En el buscador ponemos now playing y vemos como hacer las request al api. lo que mas nos interesa de esa pagina es la URL a la que le haremos la peticion. https://api.themoviedb.org/3/movie/now_playing podemos problarla con postman

Si pegamos la URL vamos a ver que da error porque no le proporcianamos la KEY, lo mandamos como parametro con el nombre de api_key y listo . Te queda asi: https://api.themoviedb.org/3/movie/now_playing?api_key=tu-key

Podemos agregar al paraemtro language=es y ahora las repsuteas seran en espaniol

HTTP adapter

Una vez tengamos lo necesario para consultar la api , vamos a crear un adaptador , no es obligatorio pero es recomendado por flexibilidad y buenas practicas, de esta manera abstraemos la menera en que nuestra app hace las consultas a las api independientemente de si usamos axios , api fetch o cualquier otra opcion.

Crearemos el fichero http.adapter.ts en :

src
  ├── config
  │   └── adapters
  │       └── http
  │           └── http.adapter.ts

Con el siguiente contenido:

export abstract class HttpAdapter {
  abstract get<T>(url: string, options?: Record<string, unknown>): Promise<T>;
}

yo no quiero instanciar cada vez , sino que lo que busco es que las implementaciones respeten la firma. explica merjor y mas detallada la oracion anterior.

Ahra instalamos npm i axios

Creamos el fichero axios.adapter.ts

src
  ├── config
  │   └── adapters
  │       └── http
  │           └── http.adapter.ts
              └── axios.adapter.ts

con el siguiente contenido:

import axios, { AxiosInstance } from &#39;axios&#39;;
import { HttpAdapter } from &#39;./http.adapter&#39;;

interface Options {
  baseUrl: string;
  params: Record&lt;string, string&gt;;
}

export class AxiosAdapter implements HttpAdapter {
  private axiosInstance: AxiosInstance;

  constructor(options: Options) {
    this.axiosInstance = axios.create(
      {
        baseURL: options.baseUrl,
        params: options.params,
      }
    );
  }
  async get&lt;T&gt;(url: string, options?: Record&lt;string, unknown&gt;): Promise&lt;T&gt; {
    try {
      const { data } = await this.axiosInstance.get&lt;T&gt;(url);
      return data;

    } catch (error) {
      throw new Error(`error fetching ${url}`);
    }
  }

}

use case movie

Ahora creemos un caso de uso que nos traiga todas las peliculas

Primero creamos los folder y el archivo necesario.

src
  ├── core
  │   └── use-cases
  │       └── movies
  │           └── index.ts
  │           └── now_playing.use-case.ts

en index.ts

export * from './now-playing.use-case.ts';

con el siguiente contenido en now_playing.use-case.ts:

import { HttpAdapter } from '../../../config/adapters/http/http.adapter';
import { NowPlayingResponse } from '../../../infrastructure/interfaces/movie-db.responses';
import { Movie } from '../../entities/movie.entity';

export const moviesNowPlayingUseCase = async (fetcher: HttpAdapter): Promise<Movie[]> => {
  try {
    const nowPlaying = await fetcher.get<NowPlayingResponse>('/now_playing');

    console.log({ nowPlaying });
    return [];
  } catch (error) {
    console.log(error);
    throw new Error('error fetching movies now_playing');
  }
};

Prestemos atencion en const nowPlaying = await fetcher.get<NowPlayingResponse>('/now_playing'); le estamos pasando la url /now_playing pero no es la url entera, eso es asi porque luego configuraremos la base_url y por eso solo pasamos lo que varia entre distintias urls.

Por otro lado vemos tenemos que craar dos interfaces Movie y NowPlayingResponse

Primero creemos NowPlayingResponse

src
  ├── infrastructure
  │   └── interfaces
  │       └── movie-db.responses.ts

con lo siguiente:

export interface NowPlayingResponse {
  dates: Dates;
  page: number;
  results: Result[];
  total_pages: number;
  total_results: number;
}

export interface Dates {
  maximum: Date;
  minimum: Date;
}

export interface Result {
  adult: boolean;
  backdrop_path: string;
  genre_ids: number[];
  id: number;
  original_language: string;
  original_title: string;
  overview: string;
  popularity: number;
  poster_path: string;
  release_date: Date;
  title: string;
  video: boolean;
  vote_average: number;
  vote_count: number;
}

Esta es la respuesta que nos da la api cuando hacemos un get a la URL

Ahora hagamos la interface Movie, para este caso vamos a crear una nosotros mismos, no usaremos NowPlayingResponse[] porque si el dia de maniana la repuesta de la api cambia y tenemos implementada esta interfaz por todos lados y seria deficil de mantener, por eso vamos a ir a :

src
  ├── core
  │   └── entities
  │       └── movie.entity.ts

Con el siguiente contenido:

export interface Movie {
  id: number;
  title: string;
  description: string;
  releaseDate: Date;
  rating: number;
  poster: string;
  backdrop: string;
}

Pero como mencionamos antes, ahora const moviesNowPlayingUseCase = async (fetcher: HttpAdapter): Promise<Movie[]> espera retornar una Movie[] (la nuestra) por lo que devolver nowPlaying.results no nos sirve, tenemos que adaptar la respueta de la api a la que nosotros creamos en movies

Custom Hook

antes de hacer las ultimas modificaciones en moviesNowPlayingUseCase vamos a probar que podamos hacer una peticion http .

Vamos a :

src
  ├── presentation
  │   └── hooks
 │       └── useMovies.ts

y ponemos :

import { useEffect, useState } from 'react';
import { Movie } from '../../core/entities/movie.entity';
import * as UseCases from '../../core/use-cases/movies/';
import { movieDBFetcher } from '../../config/adapters/http/movieDB.adapter';
export const useMovies = () => {
  const [isLoading, setIsLoading] = useState(true);
  const [nowPlaying, setNowPlaying] = useState<Movie[]>([]);

  useEffect(() => {
    initialLoad();
  }, []);

  const initialLoad = async () => {

    const nowPlayingMovies = await UseCases.moviesNowPlayingUseCase(movieDBFetcher);
  };
  return {
     nowPlaying,
     isLoading
    };
};

UseCases.moviesNowPlayingUseCase() nos pide que le pasemos algo que cumpla la interfaz HttpAdapter

asi que coo vamos a usar en varios lados el movieDBFetcher vamos a los adaptadores y cremaos movieDB.adapter.ts

src
  ├── config
  │   └── adapters
  │       └── http
  │           └── http.adapter.ts
              └── axios.adapter.ts
              └── movieDB.adapter.ts

con el siguente contenido:

import { AxiosAdapter } from './axios.adapter';

export const movieDBFetcher = new AxiosAdapter({
  baseUrl: 'https://api.themoviedb.org/3/movie',
  params: {
    api_key: 'tu-api-key-aca',
    language: 'es',
  },
});

Ahora vamos a HomeScreen e invocamos el custom hook:

import React from 'react';
import { View, Text } from 'react-native';
import { useMovies } from '../../hooks/useMovies';

export const HomeScreen = () => {
  const { } = useMovies();
  return (
    <View><Text>HomeScreen</Text></View>
  );
};

Ya con este debemos ver en la teminal algo asi :

{"nowPlaying": {"dates": {"maximum": "2025-01-22", "minimum": "2024-12-11"}, "page": 1, 
"results": [[Object], [Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object]], "total_pages": 179, "total_results": 3565}}

eso quiere decir q vamos por buen camino.

Adaptando respuesta de la api a tipo Movie

Patron mapper

Vos a :

src
  └── infrastructure
      ├── mappers
            ├── movie.mapper.ts

con el siguiente contenido :

import { Movie } from '../../core/entities/movie.entity';
import type { Result } from '../interfaces/movie-db.responses';

export class MovieMapper {
  static fromMovieDBResultToEntity(result: Result): Movie {
    return {
      id: result.id,
      title: result.title,
      description: result.overview,
      releaseDate: new Date(result.release_date),
      rating: result.vote_average,
      poster: `https://image.tmdb.org/t/p/w500${result.poster_path}`,
      backdrop: `https://image.tmdb.org/t/p/w500${result.backdrop_path}`,
    };
  }
}

Ahora podemos usarlo en el caso de uso de moviesNowPlayingUseCase

Si bien la teoria dice que nuestros casos de uso seberian ser funciones puras que resuelvan todo a traves de los argumentos vamos a implementarlo asi como una excepcion.

import { HttpAdapter } from '../../../config/adapters/http/http.adapter';
import { NowPlayingResponse } from '../../../infrastructure/interfaces/movie-db.responses';
import { MovieMapper } from '../../../infrastructure/mappers/movie.mapper';
import { Movie } from '../../entities/movie.entity';

export const moviesNowPlayingUseCase = async (fetcher: HttpAdapter): Promise<Movie[]> => {
  try {
    const nowPlaying = await fetcher.get<NowPlayingResponse>('/now_playing');

    return nowPlaying.results.map(result => MovieMapper.fromMovieDBResultToEntity(result));
  } catch (error) {
    console.log(error);
    throw new Error('error fetching movies now_playing');
  }
};

Adicionalmente podemos resumir esta linea return nowPlaying.results.map(result => MovieMapper.fromMovieDBResultToEntity(result)); asi return nowPlaying.results.map(MovieMapper.fromMovieDBResultToEntity)

Ejercicio

Tenes que crear los demas casos de uso en

src
  ├── core
  │   └── use-cases
  │       └── movies
  │           └── index.ts
  │           └── now_playing.use-case.ts

Por ejemplo

src
  ├── core
  │   └── use-cases
  │       └── movies
  │           └── index.ts
  │           └── now_playing.use-case.ts
  │           └── popular.use-case.ts

Los endpoints a apuntar son los siguientes :

  • /popular
  • /top_rated
  • /upcoming

No hace falta crear mas interfaces de respuestas para estos endpoints ya que son iguales a now_playing podes reutilizarlos.

Luego tenenes que hacer la llamada desde el customHook, todos en simultaneo sin que se bloqueen entre si.

no tenes que hacer

const nowPlayingMovies = await UseCases....;
    const popular = await UseCases.....;
    const top_rated = await UseCases.......;

Investiga la manera de hacerlo de una manera mas optima.

Solucion

creamos todos los ficheros

use-cases
  └── movies
      ├── index.ts
      ├── now-playing.use-case.ts
      ├── popular.use-case.ts
      ├── top_rated.use-case.ts
      └── upcoming.use-case.ts

en index.ts tenemos todas las exportaciones

export * from './now-playing.use-case.ts';
export * from './upcoming.use-case.ts';
export * from './top_rated.use-case.ts';
export * from './popular.use-case.ts';

En popular por ejemplo cambiamos el endpoints.

import { HttpAdapter } from '../../../config/adapters/http/http.adapter';
import { NowPlayingResponse } from '../../../infrastructure/interfaces/movie-db.responses';
import { MovieMapper } from '../../../infrastructure/mappers/movie.mapper';
import { Movie } from '../../entities/movie.entity';

export const popularUseCase = async (fetcher: HttpAdapter): Promise<Movie[]> => {
  try {
    const popular = await fetcher.get<NowPlayingResponse>('/popular');

    return popular.results.map(result => MovieMapper.fromMovieDBResultToEntity(result));
  } catch (error) {
    console.log(error);
    throw new Error('error fetching movies upcoming');
  }
};

hacemos lo mismo en cada fichero con su endpoints respectivo.

En el customhook useMovies quedaria asi :

import { useEffect, useState } from 'react';
import { Movie } from '../../core/entities/movie.entity';
import * as UseCases from '../../core/use-cases/movies/';
import { movieDBFetcher } from '../../config/adapters/http/movieDB.adapter';
export const useMovies = () => {
  const [isLoading, setIsLoading] = useState(true);
  const [nowPlaying, setNowPlaying] = useState<Movie[]>([]);
  const [popular, setPopular] = useState<Movie[]>([]);
  const [topRated, setTopRated] = useState<Movie[]>([]);
  const [upcoming, setUpcoming] = useState<Movie[]>([]);

  useEffect(() => {
    initialLoad();
  }, []);

  const initialLoad = async () => {


    const [
      nowPlayingMovies,
      upcomingMovies,
      topRatedMovies,
      popularMovies,
    ] = await Promise.all([
      UseCases.moviesNowPlayingUseCase(movieDBFetcher),
      UseCases.upcomingUseCase(movieDBFetcher),
      UseCases.topRatedUseCase(movieDBFetcher),
      UseCases.popularUseCase(movieDBFetcher),
    ]);
    setPopular(popularMovies);
    setTopRated(topRatedMovies);
    setUpcoming(upcomingMovies);
    setNowPlaying(nowPlayingMovies);

    setIsLoading(false);
  };

  return {
    isLoading,
    nowPlaying,
    popular,
    topRated,
    upcoming,
  };
};