🧙♂️ Dark Theme com React Navigation + Typescript + React Native Paper
Nesse artigo você irá aprender a como implementar temas claros e escuros e utiliza-los em todo seu aplicativo com React Native, React Navigation, React Native Paper e Typescript.
TL;DR
Criando projeto React Native com Typescript
Para começar um projeto React Native com Typescript, basta rodar o seguinte comando.
npx react-native init NavigationTypescriptPaper --template react-native-template-typescript
Instalando React Navigation
Para instalar o React Navigation precisamos instalar os seguintes pacotes
yarn add @react-navigation/native react-native-screens react-native-safe-area-context
E dependendo do tipo de navegação que você usar, você instala apenas o pacote para aquele tipo. Para usarmos um Stack, precisamos instalar
yarn add @react-navigation/stack
Caso vocês esteja usando Mac, rode o comando
npx pod-install ios
E para Android, você precisa editar o arquivo MainActivity.java
que fica em android/app/src/main/java/<nome do projeto>/MainActivity.java
import android.os.Bundle;
// ...
public class MainActivity extends ReactActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(null);
}
// ...
}
Instalando React Native Paper
Para instalar o React Native Paper precisamos instalar os seguintes pacotes
yarn add react-native-paper react-native-vector-icons
Caso você esteja usando Mac, edite sei PodFile
e adicione o seguinte código
pod 'RNVectorIcons', :path => '../node_modules/react-native-vector-icons'
E para Android, adicione a seguinte linha no arquivo android/app/build.gradle
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
Criando o ThemeContext e ThemeContextProvider
Primeiro iremos importar os temas tanto do React Navigation como do React Native Paper
import {
DarkTheme as NavigationDarkTheme,
DefaultTheme as NavigationDefaultTheme,
NavigationContainer,
} from "@react-navigation/native";
import {
DarkTheme as PaperDarkTheme,
DefaultTheme as PaperDefaultTheme,
Provider as PaperProvider,
} from "react-native-paper";
E assim iremos dar um merge nos temas, criando um tema light
com a junção dos dois temas default e um tema dark
com a junção dos dois temas dark.
const lightTheme = {
...NavigationDefaultTheme,
...PaperDefaultTheme,
colors: {
...NavigationDefaultTheme.colors,
...PaperDefaultTheme.colors,
},
};
const darkTheme = {
...NavigationDarkTheme,
...PaperDarkTheme,
colors: {
...NavigationDarkTheme.colors,
...PaperDarkTheme.colors,
},
};
Assim teremos os dois temas definidos com os valores default dos dois pacotes e poderemos adicionar nossas próprias cores caso desejado.
Em seguida já podemos definir 2 tipos que usaremos sobre o nosso tema. O primeiro é criar um tipo que definira o nosso tema, utilizando o typeof
do lightTheme
, assim caso adicionemos alguma configuração a mais no nosso tema, ele é refletido para o tipo.
export type Theme = typeof lightTheme;
E também definiremos os tipos de temas que teremos, que no caso será light
e dark
.
export type ThemeType = "dark" | "light";
Assim já podemos definir quais dados teremos no nosso context. Passaremos o tema atual, assim como seu tipo, um booleano indicando se o tema é dark, para facilitar a comparação na hora de utilizar, uma função para alternar o valor do tema e outra para atualizar diretamente o tema caso seja necessário.
export interface ThemeContextValue {
theme: Theme;
themeType: ThemeType;
isDarkTheme: boolean;
toggleThemeType: () => void;
setThemeType: React.Dispatch<React.SetStateAction<ThemeType>>;
}
E assim utilizaremos React.createContext
para criar o contexto e passaremos valores default para cara propriedade.
export const ThemeContext = React.createContext<ThemeContextValue>({
theme: lightTheme,
themeType: "light",
isDarkTheme: false,
setThemeType: () => {},
toggleThemeType: () => {},
});
Como vamos utilizar hooks, já podemos criar o nosso próprio hook que chamaremos de useTheme
, simplesmente para facilitar a utilização desse contexto.
export const useTheme = () => useContext(ThemeContext);
Agora iremos para a implementação do context, onde criaremos um componente ThemeContextProvider
e a interface para suas props.
export interface ThemeContextProviderProps {
children: React.ReactNode;
}
export const ThemeContextProvider = ({
children,
}: ThemeContextProviderProps) => {
// ...
};
Dentro dele utilizaremos o useColorScheme
para saber se o celular está no modo normal ou dark mode e passaremos esse valor para um useState
onde armazenaremos o tipo do tema.
export const ThemeContextProvider = ({
children,
}: ThemeContextProviderProps) => {
const colorScheme = useColorScheme();
const [themeType, setThemeType] = useState<ThemeType>(colorScheme || "light");
// ...
};
Criaremos uma simples função para alternar o tipo do tema.
export const ThemeContextProvider = ({
children,
}: ThemeContextProviderProps) => {
// ...
const toggleThemeType = useCallback(() => {
setThemeType((prev) => (prev === "dark" ? "light" : "dark"));
}, []);
// ...
};
E também definiremos isDarkTheme
e o tema em si a ser utilizado
export const ThemeContextProvider = ({
children,
}: ThemeContextProviderProps) => {
// ...
const isDarkTheme = useMemo(() => themeType === "dark", [themeType]);
const theme = useMemo(
() => (isDarkTheme ? darkTheme : lightTheme),
[isDarkTheme]
);
// ...
};
Agora que temos todos os valores do nosso context definidos, podemos renderizar o NavigationContainer
e o PaperProvider
para passar o tema e também nosso provider com os valores do context.
O componente completo ficaria da seguinte forma
export const ThemeContextProvider = ({
children,
}: ThemeContextProviderProps) => {
const colorScheme = useColorScheme();
const [themeType, setThemeType] = useState<ThemeType>(colorScheme || "light");
const toggleThemeType = useCallback(() => {
setThemeType((prev) => (prev === "dark" ? "light" : "dark"));
}, []);
const isDarkTheme = useMemo(() => themeType === "dark", [themeType]);
const theme = useMemo(
() => (isDarkTheme ? darkTheme : lightTheme),
[isDarkTheme]
);
return (
<NavigationContainer theme={theme}>
<PaperProvider theme={theme}>
<ThemeContext.Provider
value={{
theme,
themeType,
isDarkTheme,
setThemeType,
toggleThemeType,
}}
>
{children}
</ThemeContext.Provider>
</PaperProvider>
</NavigationContainer>
);
};
Utilizando o Context e alterando o tema
Em nosso App.tsx
iremos renderizar o ThemeContextProvider
e dentro dele utilizaremos um stack para a navegação através do createStackNavigator
. Dentro desse stack teremos uma tela apenas para demonstrar que o tema está funcionando.
const TestScreen = () => {
// ...
};
const Stack = createStackNavigator();
const App = () => {
return (
<ThemeContextProvider>
<Stack.Navigator>
<Stack.Screen name="Test" component={TestScreen} />
</Stack.Navigator>
</ThemeContextProvider>
);
};
Dentro da nossa tela de teste, podemos utilizar o custom hook que criamos useTheme
para pegar os valores do context e utilizarmos da forma que preferirmos.
const TestScreen = () => {
const { toggleThemeType, themeType, isDarkTheme, theme } = useTheme();
return (
<View>
<Button mode="contained" onPress={toggleThemeType}>
Toggle Theme
</Button>
<Headline>{themeType}</Headline>
<Headline>isDarkTheme: {`${isDarkTheme}`}</Headline>
<Headline>Primary: {theme.colors.primary}</Headline>
</View>
);
};
Assim, apertando o botão podemos ver que o tema muda.
Podemos analisar também que se você colocar o celular no modo escuro, o aplicativo já inicia o tema como dark
.