In today's blog post, we will explore the concept of building themeable mobile applications using React Native. We'll discuss how to create a white-label app that allows for easy customization of themes, templating, and module composition. By following this approach, you can save development time and resources while ensuring consistency across multiple applications. Let's dive in!
In many cases, you may need to develop multiple mobile applications that share common features and functionalities while also having their unique branding and themes. For instance, a client may commission three mobile applications, each featuring different areas accessible via navigation tabs, custom themes and colors, and unique icons and display names. As a developer, it's crucial to find an efficient way to create these applications without duplicating code and ensure easy customization of themes and other branding elements.
To address this problem, we can leverage the power of React Native and employ a dynamic generation approach for themable applications. By following the steps outlined below, we can achieve the desired outcome:
To begin, initialize a new React Native project using the following command:
react-native init whitelabel
After project setup, we'll make some adjustments to ensure consistent bundle identifiers for both Android and iOS versions.
Conceptually, modules represent independent sections of an application, each offering different functionalities and information to the user. Instead of duplicating module code, we'll define modules as objects with unique names and corresponding React components. These modules will be stored in a modules
directory.
Here's an example of defining a module:
import React from 'react';
import { Text, View } from 'react-native';
const FooComponent = () => (
<View>
<Text> Module Foo</Text>
</View>
);
export default {
name: 'Foo',
Component: FooComponent,
};
We can create multiple modules following this pattern, such as Bar
and Baz
. To access these modules collectively, we'll create an index.js
file in the modules
directory:
import Bar from './Bar';
import Baz from './Baz';
export default [Foo, Bar, Baz];
We will keep things simple and just render all modules in one page, one below the other. We are going to do that in App.js
import { Text, View, SafeAreaView } from 'react-native';
import modules from './modules';
const styles = require('./theme')('App');
export default () => (
<SafeAreaView style={{ flex: 1, backgroundColor: '#fff' }}>
<Text>
White-Label App
</Text>
<View>
{modules.map(({ name, Component }) =>
<Component key={name} />
)}
</View>
</SafeAreaView>
);
If we run the application, we should get something like
Nothing too fancy, but it gets the job done. Interestingly, we can render any combination of modules by simply changing the exported array in modules/index.js. For instance, exporting [Foo, Baz], [Baz, Bar] and []will produce, respectively, the following results.
This approach allows us to easily select and render any combination of modules in our application without modifying the components.
To enhance the visual appeal of our application, we'll introduce theming and styles. Instead of defining styles inline or duplicating them, we'll separate the styles from the components' logic. We'll create a theme
folder alongside the modules
directory.
For example, let's create a solarized-dark
theme:It will contain two stylesheet files App.js
and Module.js
. The first one will contain styles for the main React component
import { StyleSheet } from 'react-native';
export default StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
backgroundColor: '#002b36',
},
title: {
paddingHorizontal: 16,
color: '#657b83',
fontSize: 20,
fontWeight: 'bold',
},
});
and the latter will style modules
import { StyleSheet } from 'react-native';
export default StyleSheet.create({
container: {
height: 100,
borderWidth: 1,
borderColor: '#657b83',
margin: 16,
justifyContent: 'center',
alignItems: 'center',
},
text: {
color: '#657b83',
},
accent: {
color: '#268bd2',
fontWeight: 'bold',
},
});
To apply these styles, we'll make some changes to our components. For instance, in App.js
:
import { Text, SafeAreaView, View } from 'react-native';
import modules from './modules';
import styles from './theme/solarized-dark/App';
export default () => (
<SafeAreaView style={styles.container}>
<Text style={styles.title}>White-Label App</Text>
{modules.map((module) => (
<View key={module.name}>
<Text>{module.name}</Text>
<module.Component />
</View>
))}
</SafeAreaView>
);
export default App;
and Foo.js
, Bar.js
and Baz.js
are modified as follows
import { Text, View } from 'react-native';
import styles from '../theme/solarized-dark/Module';
const FooComponent = () => (
<View style={styles.container}>
<Text style={styles.text}>
Module <Text style={styles.accent}>Foo</Text>
</Text>
</View>
);
export default {
name: 'Foo',
Component: FooComponent,
};
Now, we'll implement the dynamic theming functionality. We can achieve this by creating a ThemeProvider
component that accepts a theme name as a prop. The ThemeProvider
component will render its children components wrapped within a ThemeProviderContext.Provider
, passing the selected theme as the context value.
import React, { createContext, useState } from 'react';
export const ThemeProviderContext = createContext();
export const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('solarized-light); // Default theme
const changeTheme = (newTheme) => {
setTheme(newTheme);
};
return (
<ThemeProviderContext.Provider value={{ theme, changeTheme }}>
{children}
</ThemeProviderContext.Provider>
);
};
Wrap the App
component with the ThemeProvider
in index.js
:
import { AppRegistry } from 'react-native';
import { ThemeProvider } from './ThemeProvider';
import App from './App';
const Main = () => (
<ThemeProvider>
<App />
</ThemeProvider>
);
AppRegistry.registerComponent(appName, () => Main);
To allow users to switch between themes, we can create a separate screen or settings panel. This screen will display a list of available themes and update the selected theme in the ThemeProvider
context when a new theme is chosen. Here's an example of a theme selection screen:
import React, { useContext } from 'react';
import { View, Button, Text } from 'react-native';
import { ThemeProviderContext } from '../ThemeProvider';
const themes = ['solarized-dark', 'light', 'custom'];
const ThemeSelection = () => {
const { changeTheme } = useContext(ThemeProviderContext);
const handleThemeChange = (newTheme) => {
changeTheme(newTheme);
};
return (
<View>
<Text>Select Theme:</Text>
{themes.map((theme) => (
<Button
key={theme}
title={theme}
onPress={() => handleThemeChange(theme)}
/>
))}
</View>
);
};
export default ThemeSelection;
Make sure to define the ThemeSelection
screen as a navigable component and provide access to it from the main app.
By following this approach, you can easily create themable mobile applications using React Native. The modular structure allows for efficient code reuse, while the dynamic theming capability enables easy customization of themes and branding elements. With this setup, you can quickly build multiple applications with shared features while maintaining consistency and flexibility. Remember to explore additional possibilities, such as integrating a theming library like react-native-paper
or allowing users to create custom themes. With React Native's flexibility, the possibilities are endless. Happy theming and happy coding!