React Native’de karanlık/aydınlık tema nasıl yapılır?
Günümüzde mobil cihazların da tema desteği vermesi ile birçok uygulamada karanlık/aydınlık tema özelliği bulunuyor. Bugün de sizlere bu özelliği uygulamanıza nasıl entegre edebileceğinizi anlatacağım.
Projenin oluşturulması
Öncelikle projeminizi oluşturalım ve boş haliyle çalıştıralım:
npx react-native init SampleRNDarkTheme --template react-native-template-typescript
cd SampleRNDarkTheme
npx react-native run-ios
Cihazın sistem temasına bağlı olarak uygulamanın temasının da değiştirilebilmesi için, native tarafa erişebilen bir kütüphane eklememiz gereklidir. react-native-dark-mode kütüphanesi, sistem temasını anlık olarak dinleyerek, karanlık/aydınlık tema bilgisini geri döndürebilmektedir. Bu sayede sistemin teması değiştiğinde otomatik olarak uygulamanın da bu değişimden haberdar olmasını sağlar. Çeşitli birçok özelliği bulunsa da, bizim için sadece karanlık/aydınlık tema bilgisini döndürmesi yetecektir. Diğer özellikleri isteğinize göre kullanabilirsiniz.
Şimdi projemize react-native-dark-mode kütüphanesini ekleyelim, pod’ları yükleyelim, projeyi git’e ekleyelim ve vscode ile açalım:
yarn add react-native-dark-mode
npx pod-install
git init
git add .
git commit -m "First commit"
code .
Not: Android tarafında tema geçişinde activity’nin restart etmesini engellemek için AndroidManifest.xml dosyasında MainActivity de yer alan android:configChanges kısmına uiMode özelliğini ekleyebilirsiniz. Ben eklemedim ve bir problem olmadı ama olursa bu şekilde çözebilirsiniz.
Temaların oluşturulması
Öncelikle stillerin barındırılabilmesi için Theme ve Themes tiplerini oluşturalım. Themes tipinde; siyah, koyu ve aydınlık temalar için değişkenler bulunacaktır. Theme tipinde ise ilgili temaya ait olacak şekilde uygulama içerisinde kullanılacak olan yazı ve arkaplan renkleri gibi değişkenler yer alacaktır.
type Theme = {
primary: string;
secondary: string;
text: string;
textSecondary: string;
background: string;
planet: ImageRequireSource;
};type Themes = {
black: Theme;
dark: Theme;
light: Theme;
};
Şimdi Themes tipine sahip olacak şekilde themes adında bir obje oluşturalım:
const themes: Themes = {
black: {
primary: '#1da1f2',
secondary: '#8ed0f9',
text: '#d9d9d9',
textSecondary: '#6e767d',
background: '#000000',
planet: require('./img/moon_black.png'),
},
dark: {
primary: '#1da1f2',
secondary: '#8ed0f9',
text: '#ffffff',
textSecondary: '#8899a6',
background: '#15202b',
planet: require('./img/moon_dark.png'),
},
light: {
primary: '#1da1f2',
secondary: '#8ed0f9',
text: '#14171a',
textSecondary: '#657786',
background: '#ffffff',
planet: require('./img/sun.png'),
},
};
Tema bazında parametrik stillerin oluşturulması
Şimdi tema bazında parametrik olarak stillerin oluşturulabilmesi için customStyles fonksiyonunu oluşturalım:
const customStyles = (t: Theme) =>
StyleSheet.create({
container: {
backgroundColor: t.background,
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
title: {
color: t.text,
fontSize: 22,
marginTop: 12,
},
description: {
color: t.textSecondary,
marginTop: 0,
},
img: {
width: 100,
height: 100,
},
});
Stillerin App bileşeninde kullanılması
Telefonda kullanılan mevcut temanın isminin alınabilmesi için useDarkModeContext() metodu kullanılır. Metodun sonucu olarak ‘dark’ veya ‘light’ ifadesi geri döndürülür. Şimdi bu metodu App bileşeni içerisinde kullanalım:
const App = () => {
const themeString = useDarkModeContext();
const barStyle = themeString === 'dark' ? 'dark-content' : 'light-content';
const theme = themes[themeString];
const styles = customStyles(theme);return (
<>
<StatusBar barStyle={barStyle} />
<SafeAreaView style={styles.container}>
<Image source={theme.planet} style={styles.img} />
<Text style={styles.title}>Title</Text>
<Text style={styles.description}>Description</Text>
</SafeAreaView>
</>
);
};
Uygulamayı çalıştırıp yukarıdaki hızlı menü kısmından temayı değiştirdiğinizde, uygulama içerisindeki renklerin de otomatik olarak güncellendiğini göreceksiniz:
App.tsx’in mevcut hali aşağıdaki gibidir:
import React from 'react';
import {
StyleSheet,
Text,
Image,
SafeAreaView,
StatusBar,
ImageRequireSource,
} from 'react-native';import {useDarkModeContext} from 'react-native-dark-mode';type Theme = {
primary: string;
secondary: string;
text: string;
textSecondary: string;
background: string;
planet: ImageRequireSource;
};type Themes = {
black: Theme;
dark: Theme;
light: Theme;
};const App = () => {
const themeString = useDarkModeContext();
const barStyle = themeString === 'dark' ? 'dark-content' : 'light-content';
const theme = themes[themeString];
const styles = customStyles(theme);
return (
<>
<StatusBar barStyle={barStyle} />
<SafeAreaView style={styles.container}>
<Image source={theme.planet} style={styles.img} />
<Text style={styles.title}>Title</Text>
<Text style={styles.description}>Description</Text>
</SafeAreaView>
</>
);
};const themes: Themes = {
black: {
primary: '#1da1f2',
secondary: '#8ed0f9',
text: '#d9d9d9',
textSecondary: '#6e767d',
background: '#000000',
planet: require('./img/moon_black.png'),
},
dark: {
primary: '#1da1f2',
secondary: '#8ed0f9',
text: '#ffffff',
textSecondary: '#8899a6',
background: '#15202b',
planet: require('./img/moon_dark.png'),
},
light: {
primary: '#1da1f2',
secondary: '#8ed0f9',
text: '#14171a',
textSecondary: '#657786',
background: '#ffffff',
planet: require('./img/sun.png'),
},
};const customStyles = (t: Theme) =>
StyleSheet.create({
container: {
backgroundColor: t.background,
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
title: {
color: t.text,
fontSize: 22,
marginTop: 12,
},
description: {
color: t.textSecondary,
marginTop: 0,
},
img: {
width: 100,
height: 100,
},
});export default App;
Burada dark ve light tema gösterebiliyoruz ama siyah temayı görüntüleyemiyoruz. Çünkü sistem ayarlarında siyah tema gibi bir seçenek bulunmuyor. Bunun için uygulama üzerinde tema seçimi işlemini gerçekleştirmemiz lazım.
Daha önceki yazımda oluşturduğum RadioButton bileşenini bunun için kullanabiliriz.
RadioButton bileşeni ile tema seçimi
Proje içerisinde src dizini oluşturalım ve yazıda yer alan RadioButton ve RadioGroup bileşenlerini src dizini altına taşıyalım. Devamında RadioGroupProps ve RadioButtonProps tiplerini de App.tsx’e taşıyalım ve ilgili şekilde import kısımlarını değiştirelim. Ayrıca RadioGroup.tsx’te FlatList’i çevreleyen View’ı kaldıralım ve FlatList’in horizontal özelliğini true haline getirelim. RadioButton.tsx ve RadioGroup.tsx aşağıdaki gibi görünecektir:
RadioButton.tsx:
import React from 'react';
import {StyleSheet, View, Text, TouchableOpacity} from 'react-native';
import {RadioButtonProps} from '../App';
const RadioButton = (props: RadioButtonProps) => {
const {item, selected, onSelected} = props;
return (
<TouchableOpacity
style={styles.radioButton}
onPress={() => onSelected(item)}>
<Text>{item.name}</Text>
<View style={styles.button}>
{selected?.id === item.id && <View style={styles.selectedButton} />}
</View>
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
radioButton: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: '#e5e5e5',
padding: 12,
},
button: {
height: 24,
width: 24,
borderRadius: 24,
marginStart: 8,
borderWidth: 2,
borderColor: '#999',
alignItems: 'center',
justifyContent: 'center',
},
selectedButton: {
width: 14,
height: 14,
borderRadius: 14,
backgroundColor: '#1976d2',
},
});
export default RadioButton;
RadioGroup.tsx:
import React from 'react';
import {FlatList} from 'react-native';
import {RadioGroupProps} from '../App';
import RadioButton from './RadioButton';
const RadioGroup = (props: RadioGroupProps) => {
const {items, selected, onSelected} = props;
return (
<FlatList
horizontal={true}
data={items}
keyExtractor={(item) => item.id.toString()}
renderItem={({item}) => (
<RadioButton item={item} selected={selected} onSelected={onSelected} />
)}
/>
);
};
export default RadioGroup;
App.tsx içerisinde de RadioButton implementasyonunu gerçekleştirdiğimizde son hali aşağıdaki gibi olacaktır:
import React, {useState} from 'react';
import {
StyleSheet,
View,
Text,
Image,
SafeAreaView,
StatusBar,
ImageRequireSource,
} from 'react-native';import {useDarkModeContext} from 'react-native-dark-mode';
import RadioGroup from './src/RadioGroup';type Theme = {
primary: string;
secondary: string;
text: string;
textSecondary: string;
background: string;
planet: ImageRequireSource;
};type Themes = {
black: Theme;
dark: Theme;
light: Theme;
};export type Item = {
id: string;
name: string;
};
export type RadioGroupProps = {
items: Item[];
selected?: Item;
onSelected(item: Item): void;
};
export type RadioButtonProps = {
item: Item;
selected?: Item;
onSelected(item: Item): void;
};const App = () => {
const items: Item[] = [
{id: 'system', name: 'System'},
{id: 'black', name: 'Black'},
{id: 'dark', name: 'Dark'},
{id: 'light', name: 'Light'},
];
const [selected, setSelected] = useState<Item>(items[0]);
const systemThemeString = useDarkModeContext();
const themeString =
selected.id === 'system' ? systemThemeString : selected.id;
const barStyle = themeString === 'dark' ? 'dark-content' : 'light-content';
const theme = themes[themeString];
const styles = customStyles(theme);const onSelected = (item: Item) => {
setSelected(item);
};return (
<>
<StatusBar barStyle={barStyle} />
<SafeAreaView style={styles.safeAreaView}>
<View style={styles.container}>
<Image source={theme.planet} style={styles.img} />
<Text style={styles.title}>Title</Text>
<Text style={styles.description}>Description</Text>
</View>
<View style={styles.bottomBar}>
<RadioGroup
selected={selected}
onSelected={onSelected}
items={items}
/>
</View>
</SafeAreaView>
</>
);
};const themes: Themes = {
black: {
primary: '#1da1f2',
secondary: '#8ed0f9',
text: '#d9d9d9',
textSecondary: '#6e767d',
background: '#000000',
planet: require('./img/moon_black.png'),
},
dark: {
primary: '#1da1f2',
secondary: '#8ed0f9',
text: '#ffffff',
textSecondary: '#8899a6',
background: '#15202b',
planet: require('./img/moon_dark.png'),
},
light: {
primary: '#1da1f2',
secondary: '#8ed0f9',
text: '#14171a',
textSecondary: '#657786',
background: '#ffffff',
planet: require('./img/sun.png'),
},
};const customStyles = (t: Theme) =>
StyleSheet.create({
safeAreaView: {
flex: 1,
justifyContent: 'space-between',
backgroundColor: t.background,
},
container: {
justifyContent: 'center',
alignItems: 'center',
flex: 1,
},
title: {
color: t.text,
fontSize: 22,
marginTop: 12,
},
description: {
color: t.textSecondary,
marginTop: 0,
marginBottom: 12,
},
img: {
width: 100,
height: 100,
},
bottomBar: {
alignItems: 'center',
},
});export default App;
Sonuç olarak
Uygulamalarda koyu/karanlık tema ve aydınlık tema geçişlerini yönetmek oldukça kolay. Siz de bu yazıdaki yönergeleri izleyerek üçüncü parti bir kütüphane kullanmaksızın koyu temayı entegre edebilirsiniz.
Projenin bitmiş halini react-native-dark-theme-sample reposunda bulabilirsiniz. Bu yazı hakkında soru ve görüşlerinizi aşağıdaki yorumlar kısmından yazabilirsiniz. Bana destek vermek için alkış simgesine tıklayabilirsiniz. Sonraki yazımda görüşmek üzere…