Themes

documentacion oficial en react-navigation Vamos a hacer otra implementacion para que nos sirva independientemente de si estamos usando reactnavigation o cualquier otra alternativa.

usaremos el context API nativo.

En src/config/theme/theme.ts

import { StyleSheet } from 'react-native';

export interface ThemeColors {
  primary: string;
  text: string;
  background: string;
  cardBackground: string;
  buttonTextColor: string;
}

export const colors: ThemeColors = {
  primary: '#5856D6',
  text: 'black',

  background: '#F3F2F7',
  cardBackground: 'white',
  buttonTextColor: 'white',
};

export const lightColors: ThemeColors = {
  primary: '#5856D6',
  text: 'black',
  background: '#F3F2F7',
  cardBackground: 'white',
  buttonTextColor: 'white',
};

export const darkColors: ThemeColors = {
  primary: '#5856D6',
  text: 'white',
  background: '#090909',
  cardBackground: '#2d2d2d',
  buttonTextColor: 'white',
};

export const globalStyles = StyleSheet.create({
  title: {
    fontSize: 30,
    fontWeight: 'bold',
    color: colors.text,
  },
  input: {
    height: 40,
    margin: 12,
    borderWidth: 1,
    padding: 10,
    borderColor: 'rgba(0,0,0,.3)',
    borderRadius: 10,
    color: colors.text,
  },
  subTitle: {
    fontSize: 20,
    fontWeight: 'bold',
    color: colors.text,
  },

  mainContainer: {
    flex: 1,
    backgroundColor: colors.background,
  },
  globalMargin: {
    paddingHorizontal: 20,
    flex: 1,
  },

  btnPrimary: {
    backgroundColor: colors.primary,
    borderRadius: 10,
    padding: 10,
    alignItems: 'center',
  },
  btnPrimaryText: {
    color: colors.text,
    fontSize: 16,
  },
});

ThemeContext

Creamos dentro de presentation/context/ThemeContext.tsx

import React from 'react';
import { PropsWithChildren, createContext } from 'react';
import { ThemeColors, lightColors } from '../../config/theme/theme';

type ThemeColor = 'light' | 'dark'

interface ThemeContextProps {
  currentTheme: ThemeColor;
  colors: ThemeColors;
  setTheme: (theme: ThemeColor) => void
}

export const ThemeContext = createContext({} as ThemeContextProps);

export const ThemeProvider = ({ children }: PropsWithChildren) => {
  const setTheme = (theme: ThemeColor) => {
    console.log({ theme });
  };

  return (
    <ThemeContext.Provider value={{
      currentTheme: 'light',
      colors: lightColors,
      setTheme: setTheme,
    }
    }>
      {children}
    </ThemeContext.Provider >
  );
};

En App.tsx

import { NavigationContainer } from '@react-navigation/native';
import React, { PropsWithChildren } from 'react';
import { StackNav } from './presentation/navigation/StackNav';
import { ThemeProvider } from './presentation/context/ThemeContext';

const AppState = ({ children }: PropsWithChildren) => {
  return (
    <NavigationContainer>
      <ThemeProvider>
        {children}
      </ThemeProvider>
    </NavigationContainer>
  );
};

export const App = () => {
  return (
    <AppState>
      <StackNav />
    </AppState>
  );
};

Creamos ChangeThemeScreen:

import React, { useContext } from 'react';
import CustomView from '../../ui/CustomView';
import { Title } from '../../ui/Title';
import Button from '../../ui/Button';
import { ThemeContext } from '../../context/ThemeContext';
import { Text } from 'react-native';

const ChangeThemeScreen = () => {
  const { setTheme, currentTheme, colors } = useContext(ThemeContext);

  return (
    <CustomView margin>
      <Title text={`cambiar tema : ${currentTheme}`} />
      <Button
        text="light"
        onPress={() => setTheme('light')}
      />

      <Button
        text="dark"
        onPress={() => setTheme('dark')}
      />
      <Text style={{ color:colors.text}}>
        {
          JSON.stringify(colors, null, 2)
        }
      </Text>
    </CustomView>
  );
};

export default ChangeThemeScreen;

Acordate de agregar la ruta a StackNav <Stack.Screen name="ChangeThemeScreen" component={ChangeThemeScreen} />

Hagamos que ThemeContext cambie los valores:

import React, { useState } from 'react';
import { PropsWithChildren, createContext } from 'react';
import { ThemeColors, darkColors, lightColors } from '../../config/theme/theme';

type ThemeColor = 'light' | 'dark'

interface ThemeContextProps {
  currentTheme: ThemeColor;
  colors: ThemeColors;
  setTheme: (theme: ThemeColor) => void
}

export const ThemeContext = createContext({} as ThemeContextProps);

export const ThemeProvider = ({ children }: PropsWithChildren) => {
  const [currentTheme, setCurrentTheme] = useState<ThemeColor>('light');
  const setTheme = (theme: ThemeColor) => {
    setCurrentTheme(theme);
  };

  return (
    <ThemeContext.Provider value={{
      currentTheme: currentTheme,
      colors: (currentTheme === 'light' ? lightColors : darkColors),
      setTheme: setTheme,
    }
    }>
      {children}
    </ThemeContext.Provider >
  );
};```

Ahora los botones deben cambiar el title de `ChangeThemeScreen`

Ahora hay q usar el `colors` en todos lados , por ejemplo en `CustomView` lo usamos asi :

```tsx
import { StyleProp, View, ViewStyle } from 'react-native';
import React, { ReactNode, useContext } from 'react';
import { globalStyles } from '../../config/theme/theme';
import { ThemeContext } from '../context/ThemeContext';

interface Props {
  style?: StyleProp<ViewStyle>
  children?: ReactNode;
  margin?: boolean;
}
const CustomView = ({ children, style, margin = false }: Props) => {
  const { colors } = useContext(ThemeContext);
  return (
    <View style={[
      globalStyles.mainContainer,
      { backgroundColor: colors.background },
      style,
      margin ? { margin: 10 } : undefined,
    ]}>
      {children}
    </View>
  );
};

export default CustomView;

Si estamos usando react nativation , es problable que al cambiar de pantallas veamos un destello. Para arreglarlo modificamos App.tsx

import { DarkTheme, DefaultTheme, NavigationContainer } from '@react-navigation/native';
import React, { PropsWithChildren, useContext } from 'react';
import { StackNav } from './presentation/navigation/StackNav';
import { ThemeContext, ThemeProvider } from './presentation/context/ThemeContext';


const AppNavigation = ({ children }: PropsWithChildren) => {
  const { isDark } = useContext(ThemeContext);
  return (

    <NavigationContainer theme={isDark ? DarkTheme : DefaultTheme}>
      {children}
    </NavigationContainer>
  );
};

const AppTheme = ({ children }: PropsWithChildren) => {
  return (
    <ThemeProvider>
      <AppNavigation>
        {children}
      </AppNavigation>
    </ThemeProvider>
  );
};

export const App = () => {
  return (
    <AppTheme>
      <StackNav />
    </AppTheme>
  );
};

y Agregamos isDark al ThemeContext por ahora ignora lo del useEffect y el useColorScheme.

import React, { useEffect, useState } from 'react';
import { PropsWithChildren, createContext } from 'react';
import { ThemeColors, darkColors, lightColors } from '../../config/theme/theme';
import { useColorScheme } from 'react-native';

type ThemeColor = 'light' | 'dark'

interface ThemeContextProps {
  currentTheme: ThemeColor;
  colors: ThemeColors;
  isDark: boolean;
  setTheme: (theme: ThemeColor) => void
}

export const ThemeContext = createContext({} as ThemeContextProps);

export const ThemeProvider = ({ children }: PropsWithChildren) => {

  const colorScheme = useColorScheme();
  const [currentTheme, setCurrentTheme] = useState<ThemeColor>('light');
  const isDark = currentTheme === 'dark';
  const colors = isDark ? darkColors : lightColors;

  const setTheme = (theme: ThemeColor) => {
    setCurrentTheme(theme);
  };

  useEffect(() => {
    if (colorScheme === 'dark') {
      setCurrentTheme('dark');
    } else {
      setCurrentTheme('light');
    }
  }, [colorScheme]);

  return (
    <ThemeContext.Provider value={{
      currentTheme: currentTheme,
      isDark: isDark,
      colors: colors,
      setTheme: setTheme,
    }
    }>
      {children}
    </ThemeContext.Provider >
  );
};

Tema basado en sistema operativo

en ThemeContext usamos


  const colorScheme = useColorScheme();

  useEffect(() =&gt; {
    if (colorScheme === &#39;dark&#39;) {
      setCurrentTheme(&#39;dark&#39;);
    } else {
      setCurrentTheme(&#39;light&#39;);
    }
  }, [colorScheme]);

App state

Esto nos permite saber si la app esta en primer o segundo plano y detectar el cambio:

documentacion oficial

Si vien appstate nos sirve para muchas cosas como detectar en un chat el estado activo o inactivo podemos usarlo como alternativa si no nos funiona bien la deteccion de color del sistema.

import React, { useEffect, useState } from 'react';
import { PropsWithChildren, createContext } from 'react';
import { ThemeColors, darkColors, lightColors } from '../../config/theme/theme';
import { AppState, Appearance, useColorScheme } from 'react-native';

type ThemeColor = 'light' | 'dark'

interface ThemeContextProps {
  currentTheme: ThemeColor;
  colors: ThemeColors;
  isDark: boolean;
  setTheme: (theme: ThemeColor) => void
}

export const ThemeContext = createContext({} as ThemeContextProps);

export const ThemeProvider = ({ children }: PropsWithChildren) => {

  // const colorScheme = useColorScheme();
  const [currentTheme, setCurrentTheme] = useState<ThemeColor>('light');
  const isDark = currentTheme === 'dark';
  const colors = isDark ? darkColors : lightColors;

  const setTheme = (theme: ThemeColor) => {
    setCurrentTheme(theme);
  };

  // useEffect(() => {
  //   if (colorScheme === 'dark') {
  //     setCurrentTheme('dark');
  //   } else {
  //     setCurrentTheme('light');
  //   }
  // }, [colorScheme]);

  useEffect(() => {
    const subscription = AppState.addEventListener('change', nextAppState => {
      const colorScheme = Appearance.getColorScheme();
      setCurrentTheme(colorScheme === 'dark' ? 'dark' : 'light');
    });

    return () => {
      subscription.remove();
    };
  }, []);


  return (
    <ThemeContext.Provider value={{
      currentTheme: currentTheme,
      isDark: isDark,
      colors: colors,
      setTheme: setTheme,
    }
    }>
      {children}
    </ThemeContext.Provider >
  );
};

Hasta aca estamos, si queres podes simplificar app.tsx asi :

import React from 'react';
import { StackNav } from './presentation/navigation/StackNav';
import { ThemeProvider } from './presentation/context/ThemeContext';


export const App = () => {
  return (
    <ThemeProvider>
      <StackNav />
    </ThemeProvider>
  );
};
import React, { useEffect, useState } from 'react';
import { PropsWithChildren, createContext } from 'react';
import { ThemeColors, darkColors, lightColors } from '../../config/theme/theme';
import { AppState, Appearance, useColorScheme } from 'react-native';
import { DarkTheme, DefaultTheme, NavigationContainer } from '@react-navigation/native';

type ThemeColor = 'light' | 'dark'

interface ThemeContextProps {
  currentTheme: ThemeColor;
  colors: ThemeColors;
  isDark: boolean;
  setTheme: (theme: ThemeColor) => void
}

export const ThemeContext = createContext({} as ThemeContextProps);

export const ThemeProvider = ({ children }: PropsWithChildren) => {

  // const colorScheme = useColorScheme();
  const [currentTheme, setCurrentTheme] = useState<ThemeColor>('light');
  const isDark = currentTheme === 'dark';
  const colors = isDark ? darkColors : lightColors;

  const setTheme = (theme: ThemeColor) => {
    setCurrentTheme(theme);
  };

  // useEffect(() => {
  //   if (colorScheme === 'dark') {
  //     setCurrentTheme('dark');
  //   } else {
  //     setCurrentTheme('light');
  //   }
  // }, [colorScheme]);

  useEffect(() => {
    const subscription = AppState.addEventListener('change', nextAppState => {
      const colorScheme = Appearance.getColorScheme();
      setCurrentTheme(colorScheme === 'dark' ? 'dark' : 'light');
    });

    return () => {
      subscription.remove();
    };
  }, []);


  return (
    <NavigationContainer theme={isDark ? DarkTheme : DefaultTheme}>
      <ThemeContext.Provider value={{
        currentTheme: currentTheme,
        isDark: isDark,
        colors: colors,
        setTheme: setTheme,
      }
      }>
        {children}
      </ThemeContext.Provider >
    </NavigationContainer>
  );
};

tanstack query peticiones y cache

documentacion oficial

  1. npm i @tanstack/react-query
  2. En App
import React from 'react';
import { StackNav } from './presentation/navigation/Navigator';
import { ThemeContextProvider } from './presentation/context/ThemeContext';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();
  
const App = () => {
  return (
    <QueryClientProvider client={queryClient}>
      <ThemeContextProvider>
        <StackNav />
      </ThemeContextProvider>
    </QueryClientProvider>
  );
};

export default App;

Vamos a HomeScreen

import { ActivityIndicator, View } from 'react-native';
import React from 'react';
import { Button, Text } from 'react-native-paper';
import { getPokemons } from '../../../actions/pokemons';
import { useQuery } from '@tanstack/react-query';

const HomeScreen = () => {
  const { isLoading, data } = useQuery({
    queryKey: ['pokemons'],
    queryFn: () => getPokemons(),
    staleTime: 1000 * 60 * 60, //60 min
  });

  return (
    <View>
      {isLoading ? <ActivityIndicator /> : (
        <Button icon="camera" mode="contained" onPress={() => console.log('Pressed')}>
          Press me
        </Button>
      )}
    </View>
  );
};

export default HomeScreen;

1. useQuery Hook

const { isLoading, data } = useQuery({
  queryKey: ['pokemons'],
  queryFn: () => getPokemons(),
  staleTime: 1000 * 60 * 60, // 60 min
});

El hook useQuery se usa para realizar solicitudes de datos y manejar automáticamente su estado (como cargando, éxito, error, etc.). Aquí, se configura para obtener una lista de Pokémon.

Parámetros de useQuery

  1. queryKey:

    • Una clave única que identifica esta consulta.
    • Se utiliza internamente para almacenar en caché los datos y reconocer cuándo invalidarlos.
    • En este caso, la clave es ['pokemons'].
  2. queryFn:

    • Una función que se ejecuta para obtener los datos.
    • Aquí, se define como una función anónima que llama a getPokemons().
    • getPokemons() es probablemente una función que realiza la solicitud HTTP para obtener los datos (por ejemplo, usando fetch o axios).
  3. staleTime:

    • Define cuánto tiempo los datos se consideran “frescos” antes de volverse “obsoletos”.
    • Durante este tiempo, TanStack Query no volverá a ejecutar la función de consulta (queryFn) si los datos ya están en caché.
    • Aquí, se establece en 60 minutos (1000 ms * 60 s * 60 min).

2. Propiedades que retorna useQuery

El hook retorna un objeto con múltiples propiedades para manejar el estado y los datos de la consulta. En este caso, se usan:

  1. isLoading:

    • Indica si la consulta está en proceso de cargar los datos.
    • Es true cuando la consulta se está ejecutando y no hay datos en caché disponibles.
  2. data:

    • Contiene los datos obtenidos de la función queryFn (getPokemons).
    • Si la consulta falla, este valor será undefined.

3. Flujo de trabajo

  1. Primera ejecución:

    • Cuando useQuery se ejecuta por primera vez, verifica si ya hay datos en caché asociados con la clave ['pokemons'].
    • Si no hay datos (o son obsoletos), ejecuta la función queryFn (getPokemons) para obtenerlos.
  2. Estado de carga:

    • Mientras los datos se están obteniendo, isLoading es true.
    • Una vez que la consulta se completa, isLoading cambia a false.
  3. Caché y staleTime:

    • Los datos obtenidos se almacenan en caché bajo la clave ['pokemons'].
    • Durante el tiempo definido en staleTime (60 minutos), los datos se consideran frescos.
    • Si vuelves a usar useQuery con la misma clave dentro de este período, no ejecutará la consulta nuevamente; en su lugar, usará los datos en caché.
  4. Actualización automática:

    • Cuando los datos se vuelven “obsoletos” después de 60 minutos, useQuery automáticamente ejecutará queryFn nuevamente para actualizar los datos.

Ventajas de esta configuración

  1. Caché eficiente:

    • TanStack Query almacena los datos para evitar llamadas repetidas a la API si no es necesario.
  2. Control de frescura:

    • El parámetro staleTime permite mantener datos frescos por un tiempo específico, reduciendo la carga en el servidor.
  3. Estado automático:

    • useQuery maneja automáticamente estados como isLoading y data, eliminando la necesidad de escribir lógica adicional para manejar solicitudes de datos.
  4. Sincronización y reintentos:

    • TanStack Query incluye reintentos automáticos en caso de error, así como sincronización cuando se detectan cambios en la red o en el enfoque de la aplicación.

Interfaces de PokeApi

En infraestructure/interfaces/ creamos pokeApi.interfaces.ts

export interface PokeAPIPaginatedResponse {
  count: number;
  next: string;
  previous: string;
  results: Result[];
}

export interface Result {
  name: string;
  url: string;
}

export interface PokeAPIPokemon {
  abilities: Ability[];
  base_experience: number;
  forms: Species[];
  game_indices: GameIndex[];
  height: number;
  held_items: any[];
  id: number;
  is_default: boolean;
  location_area_encounters: string;
  moves: Move[];
  name: string;
  order: number;
  past_abilities: any[];
  past_types: any[];
  species: Species;
  sprites: Sprites;
  stats: Stat[];
  types: Type[];
  weight: number;
}

export interface Ability {
  ability: Species;
  is_hidden: boolean;
  slot: number;
}

export interface Species {
  name: string;
  url: string;
}

export interface GameIndex {
  game_index: number;
  version: Species;
}

export interface Move {
  move: Species;
  version_group_details: VersionGroupDetail[];
}

export interface VersionGroupDetail {
  level_learned_at: number;
  move_learn_method: Species;
  version_group: Species;
}

export interface GenerationV {
  'black-white': Sprites;
}

export interface GenerationIv {
  'diamond-pearl': Sprites;
  'heartgold-soulsilver': Sprites;
  platinum: Sprites;
}

export interface Versions {
  'generation-i': GenerationI;
  'generation-ii': GenerationIi;
  'generation-iii': GenerationIii;
  'generation-iv': GenerationIv;
  'generation-v': GenerationV;
  'generation-vi': { [key: string]: Home };
  'generation-vii': GenerationVii;
  'generation-viii': GenerationViii;
}

export interface Other {
  dream_world: DreamWorld;
  home: Home;
  'official-artwork': OfficialArtwork;
  showdown: Sprites;
}

export interface Sprites {
  back_default: string;
  back_female: null;
  back_shiny: string;
  back_shiny_female: null;
  front_default: string;
  front_female: null;
  front_shiny: string;
  front_shiny_female: null;
  other?: Other;
  versions?: Versions;
  animated?: Sprites;
}

export interface GenerationI {
  'red-blue': RedBlue;
  yellow: RedBlue;
}

export interface RedBlue {
  back_default: string;
  back_gray: string;
  back_transparent: string;
  front_default: string;
  front_gray: string;
  front_transparent: string;
}

export interface GenerationIi {
  crystal: Crystal;
  gold: Gold;
  silver: Gold;
}

export interface Crystal {
  back_default: string;
  back_shiny: string;
  back_shiny_transparent: string;
  back_transparent: string;
  front_default: string;
  front_shiny: string;
  front_shiny_transparent: string;
  front_transparent: string;
}

export interface Gold {
  back_default: string;
  back_shiny: string;
  front_default: string;
  front_shiny: string;
  front_transparent?: string;
}

export interface GenerationIii {
  emerald: OfficialArtwork;
  'firered-leafgreen': Gold;
  'ruby-sapphire': Gold;
}

export interface OfficialArtwork {
  front_default: string;
  front_shiny: string;
}

export interface Home {
  front_default: string;
  front_female: null;
  front_shiny: string;
  front_shiny_female: null;
}

export interface GenerationVii {
  icons: DreamWorld;
  'ultra-sun-ultra-moon': Home;
}

export interface DreamWorld {
  front_default: string;
  front_female: null;
}

export interface GenerationViii {
  icons: DreamWorld;
}

export interface Stat {
  base_stat: number;
  effort: number;
  stat: Species;
}

export interface Type {
  slot: number;
  type: Species;
}

Y modificamos actions/get-pokemons

import { pokeApi } from '../../config/api/pokeApi';
import { Pokemon } from '../../domain/entities/pokemon';
import type { PokeAPIPaginatedResponse, PokeAPIPokemon } from '../../infraestructure/interfaces/pokeApi.interfaces';

export const getPokemons = async (page: number, limit: number = 20): Promise<Pokemon[]> => {
  try {
    const url = `/pokemon?offset=${page * 10}&limit=${limit}`;
    const { data } = await pokeApi.get<PokeAPIPaginatedResponse>(url);

    const pokemonPromises = data.results.map((info) => {
      return pokeApi.get<PokeAPIPokemon>(info.url);
    });

    const pokeApiPokemons = await Promise.all(pokemonPromises);

    console.log(pokeApiPokemons);
    return [];

  } catch (error) {
    throw new Error('Error getting pokemons');
  }
};

Mapeo de PokeApi a entidad.

Creamos en /infraestructure/mappers al pokemon.mapper.ts

import { Pokemon } from '../../domain/entities/pokemon';
import { PokeAPIPokemon } from '../interfaces/pokeApi.interfaces';

export class PokemonMapper {
  static pokeApiPokemonToEntity(data: PokeAPIPokemon): Pokemon {

    const sprites = PokemonMapper.getSprites(data);
    const avatar = `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${data.id}.png`;

    return {
      id: data.id,
      name: data.name,
      avatar: avatar,
      sprites: sprites,
      types: data.types.map(type => type.type.name),
    };
  }

  static getSprites(data: PokeAPIPokemon): string[] {
    const sprites: string[] = [
      data.sprites.front_default,
      data.sprites.back_default,
      data.sprites.front_shiny,
      data.sprites.back_shiny,
    ];

    if (data.sprites.other?.home.front_default) { sprites.push(data.sprites.other?.home.front_default); }
    if (data.sprites.other?.['official-artwork'].front_default) { sprites.push(data.sprites.other?.['official-artwork'].front_default); }
    if (data.sprites.other?.['official-artwork'].front_shiny) { sprites.push(data.sprites.other?.['official-artwork'].front_shiny); }
    if (data.sprites.other?.showdown.front_default) { sprites.push(data.sprites.other?.showdown.front_default); }
    if (data.sprites.other?.showdown.back_default) { sprites.push(data.sprites.other?.showdown.back_default); }

    return sprites;
  }
}

En get-pokemons

import { pokeApi } from '../../config/api/pokeApi';
import { Pokemon } from '../../domain/entities/pokemon';
import type { PokeAPIPaginatedResponse, PokeAPIPokemon } from '../../infraestructure/interfaces/pokeApi.interfaces';
import { PokemonMapper } from '../../infraestructure/mappers/pokemon.mapper';

export const getPokemons = async (page: number, limit: number = 20): Promise<Pokemon[]> => {
  try {
    const url = `/pokemon?offset=${page * 10}&limit=${limit}`;
    const { data } = await pokeApi.get<PokeAPIPaginatedResponse>(url);

    const pokemonPromises = data.results.map((info) => {
      return pokeApi.get<PokeAPIPokemon>(info.url);
    });

    const pokeApiPokemons = await Promise.all(pokemonPromises);
    const pokemons = pokeApiPokemons.map((item) => PokemonMapper.pokeApiPokemonToEntity(item.data));
    return pokemons;

  } catch (error) {
    throw new Error('Error getting pokemons');
  }
};

y en HomeScreen

import { ActivityIndicator, Image, ScrollView, View } from 'react-native';
import React from 'react';
import { Button, Text } from 'react-native-paper';
import { getPokemons } from '../../../actions/pokemons';
import { useQuery } from '@tanstack/react-query';

const HomeScreen = () => {
  const { isLoading, data = [] } = useQuery({
    queryKey: ['pokemons'],
    queryFn: () => getPokemons(0), staleTime: 1000 * 60 * 60, //60 min
  });

  return (
    <View>
      <Text>HomeScreen</Text>
      {isLoading ? <ActivityIndicator /> : (
        <Button icon="camera" mode="contained" onPress={() => console.log('Pressed')}>
          Press me
        </Button>
      )}
      <ScrollView>
        {
          data?.map(pokemon => (
            <Image src={pokemon.avatar} key={pokemon.id} width={100} height={100} />
          ))
        }

      </ScrollView>
    </View>
  );
};

export default HomeScreen;

Deberias ver los pokimoneee.

Diseno HomeScreen

Parte 1

import { StyleSheet, View } from 'react-native';
import React from 'react';
import { getPokemons } from '../../../actions/pokemons';
import { useQuery } from '@tanstack/react-query';
import PokeballBg from '../../components/ui/PomkeonBg';

const HomeScreen = () => {
  const { isLoading, data = [] } = useQuery({
    queryKey: ['pokemons'],
    queryFn: () => getPokemons(0), staleTime: 1000 * 60 * 60, //60 min
  });

  return (
    <View>
      <PokeballBg style={styles.imgPosition} />
    </View>
  );
};

export default HomeScreen;

const styles = StyleSheet.create({
  imgPosition: {
    position: 'absolute',
    top: -100,
    right: -100,
  },
});

En presentation/components/ui creamos : PaokeballBg

import { Image, ImageStyle, StyleProp } from 'react-native';
import React, { useContext } from 'react';
import { ThemeContext } from '../../context/ThemeContext';

interface Props {
  style?: StyleProp<ImageStyle>
}

const PokeballBg = ({ style }: Props) => {
  const { isDark } = useContext(ThemeContext);
  const pokeball = isDark ?
    require('../../../assets/pokeball-light.png')
    : require('../../../assets/pokeball-dark.png');

  return (
    <Image
      source={pokeball}
      style={
        [
          { width: 300, height: 300, opacity: 0.3 },
          style,
        ]}
    />
  );
};

export default PokeballBg;

Parte 2

import { FlatList, StyleSheet, View } from 'react-native';
import React from 'react';
import { getPokemons } from '../../../actions/pokemons';
import { useQuery } from '@tanstack/react-query';
import PokeballBg from '../../components/ui/PomkeonBg';
import { Text } from 'react-native-paper';
import { globalTheme } from '../../../config/theme/global-theme';
import PokemonCard from '../../components/pokemons/PokemonCard';

const HomeScreen = () => {
  const { isLoading, data: pokemons = [] } = useQuery({
    queryKey: ['pokemons'],
    queryFn: () => getPokemons(0), staleTime: 1000 * 60 * 60, //60 min
  });

  return (
    <View style={globalTheme.globalMargin}>
      <PokeballBg style={styles.imgPosition} />
      <FlatList
        data={pokemons}
        keyExtractor={(pokemon, index) => `${pokemon.id}-${index}`}
        numColumns={2}
        style={{ paddingTop: 16 }}
        ListHeaderComponent={() => (
          <Text variant="displayMedium" >Pokedex</Text>
        )}
        renderItem={({ item }) => (
          <PokemonCard pokemon={item} />
        )}
      />
    </View>
  );
};

export default HomeScreen;

const styles = StyleSheet.create({
  imgPosition: {
    position: 'absolute',
    top: -100,
    right: -100,
  },
});

En components/pokemon creamos PokemonCard.tsx

import { Image, StyleSheet, View } from 'react-native';
import React from 'react';
import { Pokemon } from '../../../domain/entities/pokemon';
import { Card, Text } from 'react-native-paper';

interface Props {
  pokemon: Pokemon;
}

const PokemonCard = ({ pokemon }: Props) => {
  return (
    <Card style={[
      styles.cardContainer,
    ]}>
      <Text style={styles.name} variant="bodyLarge" lineBreakMode="middle" >
        {pokemon.name}
        {'\n#' + pokemon.id}
      </Text>
      <View style={styles.pokeballContainer}>
        <Image source={require('../../../assets/pokeball-light.png')} style={styles.pokeball} />
      </View>
      <Image source={{ uri: pokemon.avatar }} style={styles.pokemonImage} />
      <Text style={[styles.name, { marginTop: 35 }]}>{pokemon.types[0]}</Text>
    </Card>
  );
};

export default PokemonCard;



const styles = StyleSheet.create({
  cardContainer: {
    marginHorizontal: 10,
    backgroundColor: 'grey',
    height: 120,
    flex: 0.5,
    marginBottom: 25,
    borderRadius: 10,
    shadowColor: '#000',
    shadowOffset: {
      width: 0,
      height: 2,
    },
    shadowOpacity: 0.25,
    shadowRadius: 3.84,

    elevation: 5,
  },
  name: {
    color: 'white',
    top: 10,
    left: 10,
  },
  pokeball: {
    width: 100,
    height: 100,
    right: -25,
    top: -25,
    opacity: 0.4,
  },
  pokemonImage: {
    width: 120,
    height: 120,
    position: 'absolute',
    right: -15,
    top: -30,
  },

  pokeballContainer: {
    alignItems: 'flex-end',
    width: '100%',
    position: 'absolute',

    overflow: 'hidden',
    opacity: 0.5,
  },
});