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);
- creamos en el folder
src/presentation/navigation
el componenteStackNav.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>
);
};
- 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>
);
};
- 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 'axios';
import { HttpAdapter } from './http.adapter';
interface Options {
baseUrl: string;
params: Record<string, string>;
}
export class AxiosAdapter implements HttpAdapter {
private axiosInstance: AxiosInstance;
constructor(options: Options) {
this.axiosInstance = axios.create(
{
baseURL: options.baseUrl,
params: options.params,
}
);
}
async get<T>(url: string, options?: Record<string, unknown>): Promise<T> {
try {
const { data } = await this.axiosInstance.get<T>(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,
};
};