Не скерет, что начиная с версии 10 для Android и 13 для iOS в этих операционных системах появился тёмный режим. В React Native взаимодействие с этой штукой строится через модуль Appearance.
Без долгих вступлений отправимся в бой. Для начала получение копии React Native и styled-components (по желанию):
$ npx react-native init DarkModeApp
$ cd DarkModeApp && yarn install
$ yarn add styled-components
$ cd ios && pod install && cd ../
Структура проекта. Потребуется создать директорию src
и недостающие файлы в ней. О каждом из них поговорим ниже.
$ tree -L 4
.
├── android
├── app.json
├── babel.config.js
├── index.js
├── ios
├── metro.config.js
├── package.json
└── src
├── components
│ └── StatusBarComponent.js
├── constants
│ └── colors.js
├── providers
│ └── ThemeProvider.js
└── screens
└── WelcomeScreen.js
Константы
Указание цветовой палитры для каждой из тем. Dark mode будем проверять на яблочной платформе. Можно свериться с официальным guideline для iOS, если есть жел ание.
// constants/colors.js
export const LIGHT_COLORS = {
layout: 'rgb(242, 242, 247)',
textColor: 'rgb(28, 28, 30)',
thumbColor: 'rgb(255, 255, 255)',
};
export const DARK_COLORS = {
layout: 'rgb(28, 28, 30)',
textColor: 'rgb(242, 242, 247)',
thumbColor: 'rgb(255, 255, 255)',
};
Контекст
Самое простое решение — хранить текущие параметры темы в React Context. Таким образом данные хранятся на уровне всего приложения и их не требуется прокидывать в каждый компонент: доступ есть у всех компонентов, каким это потребуется.
Создание Context и Provider.
// providers/ThemeProvider.js
import React, { useState } from 'react';
import { Appearance } from 'react-native';
import { DARK_COLORS, LIGHT_COLORS } from '../constants/colors';
// контекст с параметрами по-умолчанию
export const ThemeContext = React.createContext({
isDark: false,
colors: LIGHT_COLORS,
setColorScheme: () => {},
});
// провайдер
export const ThemeProvider = ({ children }) => {
const colorScheme = Appearance.getColorScheme();
// храним флаг isDark
const [isDark, setIsDark] = useState(colorScheme === 'dark');
const defaultTheme = {
isDark,
colors: isDark ? DARK_COLORS : LIGHT_COLORS,
// будем менять флаг isDark по требованию
setColorScheme: (scheme) => {
setIsDark(scheme === 'dark');
},
};
return (
<ThemeContext.Provider value={defaultTheme}>
{children}
</ThemeContext.Provider>
);
};
Screen
Скрин — экран приложения. Как страница в вебе.
Получение контекста может отличаться в зависимости от того используете вы компонент-класс или компонент-функцию. Пожалуй, можно привести оба примера.
Для функции легче и короче применить хук useContext
. Для класса классчиеская передача
через Context.Consumer
.
// screens/WelcomeScreen.jsx
import React, { useContext } from 'react';
import { ScrollView, Switch, Text } from 'react-native';
import styled from 'styled-components/native';
import { ThemeContext } from '../providers/ThemeProvider';
import { LIGHT_COLORS, DARK_COLORS } from '../constants/colors';
const Section = styled.View`
padding: 30px 16px;
background-color: ${(props) =>
props.isDark ? DARK_COLORS.layout : LIGHT_COLORS.layout};
`;
const SectionText = styled.Text`
margin: 12px 0;
color: ${(props) =>
props.isDark ? DARK_COLORS.textColor : LIGHT_COLORS.textColor};
`;
export const WelcomeScreen = () => {
// получить контекст
const { colors, isDark, setColorScheme } = useContext(ThemeContext);
// изменить тему
const handleChangeColorTheme = isTrue => setColorScheme(isTrue ? 'dark' : 'light');
return (
<ScrollView contentInsetAdjustmentBehavior="automatic">
<Section isDark={isDark}>
<SectionText isDark={isDark}>
{isDark ? 'Dark' : 'Light'} Mode
</SectionText>
<Switch
thumbColor={colors.thumbColor}
value={isDark}
onValueChange={handleChangeColorTheme} />
</Section>
</ScrollView>
);
};
В данном случае styled-components
делает код чище, а будни радостнее. Всё это благодаря
конструкции видаbackground-color: ${(props) => props.isDark ? 'gray' : 'white'};
.
При изменении темы цвета пересчитаются, компоненты перерендерятся.
Но у всего есть своя цена, не увлекайтесь. Для веба при подходе CSS-in-JS можно порекомендовать библиотеку linaria или другие zero-runtime решения. Увы, на момент написания заметки для RN такой возможности нет.
Перерисовка происходит и при использовании контекста, что в сейчас нам только на руку. В целом лучше передавать в Provider примитивные типы.
Что ж, логика готова. Осталось изменить настройки и не забывать после этого
перезапускать приложение (достаточно reload, cmd + r
). Автоматически при смене времени суток
мы ничего не пересчитываем.
Статус-бар
После проделанной работы цветовая схема будет меняться. Но есть ещё одна небольшая проблема: статус-бар отсанется неизменным.
Создадим компонент с доступом к контексту и по той же логике, что и раньше, будем менять его цвет.
Кстати, для iOS такой возможности не предусмотрено, поэтому придётся делать обёртку над родным StatusBar
.
// components/StatusBarComponent
import React, { useContext } from 'react';
import { StatusBar, View, StyleSheet } from 'react-native';
import { ThemeContext } from '../providers/ThemeProvider';
const STATUS_BAR_HEIGHT = 50;
export const StatusBarComponent = () => {
const { isDark, colors } = useContext(ThemeContext);
const styles = StyleSheet.create({
statusBar: {
backgroundColor: colors.layout,
height: STATUS_BAR_HEIGHT,
},
});
// задать высоту статус-бара
StatusBar.currentHeight = STATUS_BAR_HEIGHT;
return (
<View style={styles.statusBar}>
<StatusBar barStyle={isDark ? 'light-content' : 'dark-content'} />
</View>
);
};
Собрать всё вместе
Направляемся в точку входа приложения и подключаем все компоненты, предварительно обернув их провайдером.
// index.js
import React from 'react';
import { AppRegistry, SafeAreaView } from 'react-native';
import { name as appName } from './app.json';
import { ThemeProvider } from './src/providers/ThemeProvider';
import { WelcomeScreen } from './src/screens/WelcomeScreen';
import { StatusBarComponent } from './src/components/StatusBarComponent';
const App = () => (
<ThemeProvider>
<StatusBarComponent />
<SafeAreaView>
<WelcomeScreen />
</SafeAreaView>
</ThemeProvider>
);
AppRegistry.registerComponent(appName, () => App);