В этой статье вы узнаете, как реализовать светлые и темные темы и использовать их во всем приложении с помощью React Native, React Navigation, React Native Paper и Typescript.
Вы также можете посмотреть этот видеоурок и репозиторий на Github.
Создание проекта React Native с помощью Typescript
Чтобы запустить проект React Native с помощью Typescript, просто выполните следующую команду.
npx react-native init NavigationTypescriptPaper --template react-native-template-typescript
Ссылка на документацию
Установка React Navigation
Для установки React Navigation нам необходимо установить следующие пакеты
yarn add @react-navigation/native react-native-screens react-native-safe-area-context
И в зависимости от типа навигации, который вы используете, вы устанавливаете только пакет для этого типа. Чтобы использовать стек, нам необходимо установить
yarn add @react-navigation/stack
Если вы используете Mac, выполните команду
npx pod-install ios
А для Android необходимо отредактировать файл MainActivity.java
, который находится по адресу android/app/src/main/java/<имя проекта>/MainActivity.java
.
import android.os.Bundle;
// ...
public class MainActivity extends ReactActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(null);
}
// ...
}
Ссылка на документацию
Установка React Native Paper
Для установки React Native Paper нам необходимо установить следующие пакеты
yarn add react-native-paper react-native-vector-icons
Ссылка на документацию
Если вы используете Mac, отредактируйте файл PodFile
и добавьте следующий код
pod 'RNVectorIcons', :path => '../node_modules/react-native-vector-icons'
А для Android добавьте следующую строку в файл android/app/build.gradle
.
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
Ссылка на документацию
Создание ThemeContext и ThemeContextProvider
Сначала мы импортируем темы как из React Navigation, так и из 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';
Поэтому мы объединим темы, создав тему light
с объединением двух тем по умолчанию и тему dark
с объединением двух темных тем.
const lightTheme = {
...NavigationDefaultTheme,
...PaperDefaultTheme,
colors: {
...NavigationDefaultTheme.colors,
...PaperDefaultTheme.colors,
},
};
const darkTheme = {
...NavigationDarkTheme,
...PaperDarkTheme,
colors: {
...NavigationDarkTheme.colors,
...PaperDarkTheme.colors,
},
};
Таким образом, у нас будет две темы, определенные со значениями по умолчанию для двух пакетов, и мы можем добавить свои собственные цвета, если захотим.
Далее мы можем определить 2 типа, которые мы будем использовать в нашей теме. Первое — это создание типа, который будет определять нашу тему, используя typeof
из lightTheme
, так что если мы добавим какие-либо дополнительные настройки в нашу тему, это будет отражено в типе.
export type Theme = typeof lightTheme;
Также мы определим типы тем, которые у нас будут, в данном случае это будут light
и dark
.
export type ThemeType = 'dark' | 'light';
Теперь мы можем определить, какие данные мы будем иметь в нашем контексте. Мы передадим текущую тему, а также ее тип, булево значение, указывающее, является ли тема темной, чтобы было легче сравнивать при ее использовании, функцию для переключения значения темы и функцию для прямого обновления темы, если это необходимо.
export interface ThemeContextValue {
theme: Theme;
themeType: ThemeType;
isDarkTheme: boolean;
toggleThemeType: () => void;
setThemeType: React.Dispatch<React.SetStateAction<ThemeType>>;
}
Поэтому мы будем использовать React.createContext
для создания контекста и передачи значений по умолчанию в свойства лица.
export const ThemeContext = React.createContext<ThemeContextValue>({
theme: lightTheme,
themeType: 'light',
isDarkTheme: false,
setThemeType: () => {},
toggleThemeType: () => {},
});
Поскольку мы будем использовать хуки, мы уже можем создать свой хук, который назовем useTheme
, просто для того, чтобы было проще использовать этот контекст.
export const useTheme = () => useContext(ThemeContext);
Теперь мы перейдем к реализации контекста, где создадим компонент ThemeContextProvider
и интерфейс для его реквизитов.
export interface ThemeContextProviderProps {
children: React.ReactNode;
}
export const ThemeContextProvider = ({children}: ThemeContextProviderProps) => {
// ...
}
Внутри него мы будем использовать useColorScheme
, чтобы узнать, находится ли мобильник в нормальном режиме или темном, и передадим это значение в useState
, где мы будем хранить тип темы.
export const ThemeContextProvider = ({children}: ThemeContextProviderProps) => {
const colorScheme = useColorScheme();
const [themeType, setThemeType] = useState<ThemeType>(colorScheme || 'light');
// ...
}
Мы создадим простую функцию для переключения типа темы.
export const ThemeContextProvider = ({children}: ThemeContextProviderProps) => {
// ...
const toggleThemeType = useCallback(() => {
setThemeType(prev => (prev === 'dark' ? 'light' : 'dark'));
}, []);
// ...
}
Также мы определим isDarkTheme
и саму тему, которая будет использоваться
export const ThemeContextProvider = ({children}: ThemeContextProviderProps) => {
// ...
const isDarkTheme = useMemo(() => themeType === 'dark', [themeType]);
const theme = useMemo(
() => (isDarkTheme ? darkTheme : lightTheme),
[isDarkTheme],
);
// ...
}
Теперь, когда мы установили все значения контекста, мы можем создать NavigationContainer
и PaperProvider
, чтобы передать тему, а также наш провайдер со значениями контекста.
Полный компонент будет выглядеть следующим образом
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>
);
};
Использование контекста и изменение темы
В нашем App.tsx
мы создадим ThemeContextProvider
и внутри него будем использовать стек для навигации с помощью createStackNavigator
. Внутри этого стека у нас будет экран, просто чтобы продемонстрировать, что тема работает.
const TestScreen = () => {
// ...
};
const Stack = createStackNavigator();
const App = () => {
return (
<ThemeContextProvider>
<Stack.Navigator>
<Stack.Screen name="Test" component={TestScreen} />
</Stack.Navigator>
</ThemeContextProvider>
);
};
Внутри нашего тестового экрана мы можем использовать созданный нами пользовательский хук useTheme
для захвата значений контекста и использования их по своему усмотрению.
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>
);
};
Нажав на кнопку, мы видим, что тема меняется.
Мы также можем проанализировать, что если перевести телефон в темный режим, приложение уже запускает тему как dark
.