One app to rule them all — Building a white-label application with React Native

Amedeo Zucchetti
WellD Tech
Published in
12 min readMar 20, 2019

Let’s build an app that can shape into any app we need.

Motivation

It just happens that you need to make a bunch of applications, all similar to each other and sharing some features. Just think about JetBrains and their suite of developer tools; I seriously doubt the IDEs colossus implemented every application from scratch. There must be a mould.

I’m no JetBrains, but I did face a similar issue. A client recently commissioned three mobile applications, each one featuring:

  1. Several areas, accessible via navigation tab;
  2. Custom theme and colours;
  3. Custom icon and display name.

(Obviously the client requested more, but in this article I will address these points only).

The interesting part of all of this was that the every commissioned area was effectively independent from the others — that is, content appearing in area A was completely unrelated to content appearing in area B. Moreover, different applications featured the same exact areas. For instance, application 1 consisted of area A, B and C, and application 2 consisted of area A, C and D.

From a developer perspective, it was clear from the start that reproducing the same area multiple times was a waste of time and resources, and resort to copy-paste source code from one app to the other is always a bad idea. Given that all three applications had the same structure — e.g., same navigation system — and overlapping content, we had to find a smart way to dynamically generate our target applications; however, we still had to handle different themes across applications, and customise other features, such as the app icon, display name and bundle id. We decided to develop a tool that, given a configuration, would output a properly customised application: such tool is the white-label application.

In this article I will not cover all characteristics of a white-label application, but I will introduce the core functionalities that we first implemented. In particular, I will describe how to create a white-label app with the following features:

  • Modules composition. A module represents a particular user-accessible area of an application. Every application generated from the white-label app features a different combination of modules.
  • Theme selection. The totality of styles used to customise the appearance of an application. Every application has a theme, and several applications might share the same.
  • Bundle id customisation. The bundle id is a unique identifier for an application. It is not possible to install two apps on one device with the same bundle id.
  • Display name customisation. The display name is the visual name of the application.

In brief, the white-label application (WLA) is the application containing all modules and all themes. It has its own bundle id and display name. We will see how, starting from the WLA, we can generate different apps, each with its own set of modules, its theme, bundle id and display name. In particular, in this article we will build step-by-step a white-label app; at the end of the article you will find a link to the codebase.

Project setup

First of all, let’s setup our WLA: we have to initialise a new React Native project

react-native init whitelabel

This will generate our skeleton; however, we still need to apply some changes. In particular, we want to edit the bundle id of the iOS version to match the one generated for Android. In fact, react-native init will generate two mismatching bundle identifiers — com.whitelabel for Android and org.reactjs.native.example.whitelabel. To do so, open the iOS project with XCode (open ios/whitelabel.xcodeproj), then select Project navigator > whitelabel > General > Bundle identifier, and rename it to com.whitelabel.

Steps for setting the bundle identifier on iOS.

We are now all set to go. We will see in the next sections how to populate the WLA and generate customised applications.

How to compose modules

Conceptually, modules represent independent sections of an application, each one providing different functionalities and information to the user. From an engineering point of view, a module is an object with the following properties:

  • name, a unique name of the module;
  • Component, a React component that renders the module.

We will define all our modules in the directory modules, that we will create at the top level of the application. For each module we have to create a file <ModuleName>.js. Let’s see what a module looks like:

// ./modules/Foo.js
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 just created the first module Foo; as stated above, it has a name and a visual component. Now, let’s make more. Copy the content of Foo.js into Bar.js and Baz.js, and replace all occurrences of Foo with Bar and Bazrespectively — or feel free to come up with better example names. This will leave us with three modules: Foo, Bar and Baz.

Since we are building a white-label application, we need a way to get modules without referring to them explicitly. That is, in the rest of the application we don’t want to reference Foo.js, Bar.js or Baz.js directly; instead, we want to access modules as a whole. Let’s create a file index.js for the directory modules

import Foo from './Foo';
import Bar from './Bar';
import Baz from './Baz';
export default [Foo, Bar, Baz];

This file imports and exports exactly the modules we need — for the moment, we will use all of them. Now that we defined modules, we have to decide what we want to do with them. As a realistic example, we could implement a navigation system that allows to switch modules (views) through a navigation tab — see React Navigation — , but we will keep thing simple and just render all modules in one page, one below the other. We are going to do that in App.js

import React from 'react';
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

Simple white-label application. Each module is rendered on a separate line.

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

Different module configurations produce different results.

That’s all about module composition; we can easily select modules to render without needing to modify any of our components. Let’s proceed to the next subject: themes.

Themes and styles

The modules system works, but the application is a little boring. To spice things up, let’s add some colours.

We could add styles inline, but we are smart, aren’t we? We don’t want to pollute our code with noisy React styles and we don’t want to repeat ourselves — i.e., define several times the same styles — , meaning we will separate styles from components’ logic. Let’s create a theme folder, right next to modules. It will contain a different directory for each theme we are going to define and, later, use in the app. Let’s start with an all-time favourite, Solarized Dark.

First, let’s create a directory solarized-dark in themes. It will contain two stylesheet files App.js and Module.js. The first one will contain styles for the main React component

// ./theme/solarized-dark/App.js
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

// ./theme/solarized-dark/Module.js
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 use these styles, we only need to apply slight changes to our components. In particular, App.js becomes

import React from 'react';
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>
<View>
{modules.map(({ name, Component }) =>
<Component key={name} />
)}
</View>
</SafeAreaView>
);

and Foo.js, Bar.js and Baz.js are modified as follows

import React from 'react';
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,
};

Our app looks quite nice at this point

Solarized Dark theme.

But, what if we want to define another theme, say Solarized Light? First, we have to create another theme directory, solarized-light, similar to solarized-dark, but with different colours. In particular, solarized-light’s App.js will have

export default {
...
container: {
...
backgroundColor: '#fdf6e3',
}
};

and Module.js

export default {
...
accent: {
...
color: '#268bd2',
}
};

Then, we have to replace all styles imports:

  • In App.js, import styles from './theme/solarized-dark/App'; becomes import styles from './theme/solarized-light/App';;
  • In Foo.js, Bar.js and Baz.js, import styles from '../theme/solarized-dark/Module'; becomes import styles from '../theme/solarized-light/Module';.

The result is nice

A new theme for our app.

However, replacing imports in all files every time we want to switch theme is both time-consuming and error-prone.We have to find a better way to handle themes, and this is exactly what we are going to discuss in the next section.

Theme configuration

The same way we did for modules, we want a simple system to select the current theme of our application. In particular, we don’t want to explicitly specify the currently used theme in our components, but rather let the white-label app handle styles. For this to work, we need a bit of preparation.

First, let’s create a index.js file both insolarized-dark and solarized-lightdirectories. It will look like this

import App from './App';
import Module from './Module';
export default {
App,
Module,
};

As you can see, it imports all stylesheet files, and exports them grouped in an object. Nothing too shocking so far.

Next, let’s add the following index.js file to the theme directory

import styles from './solarized-light';module.exports = fileName => styles[fileName] || {};

This one is more tricky. The import determines which theme is used in the application; this is the only point in the whole app where we are explicitly importing a specific theme. This allows us to quickly switch from solarized-dark and solarized-light just by importing one or the other. The second line exports a function that will be used to retrieve some styles for the currently selected theme; this is done via module.exports instead of the usually preferred default exports for reasons we are going to discuss soon.

Finally, we need to replace all styles’ imports. In our case,

import styles from './theme/solarized-dark/App';

from App.js, will be replaced with

const styles = require('./theme')('App');

require('./theme') returns the function we exported in theme/index.js. This function takes as argument the filename of a stylesheet file — in this case, App— and returns the appropriate styles depending on the chosen theme. The syntax for importing styles might seem cumbersome at first, but we get used to it quickly; notice how we don’t need to specify at all the theme we are using, it is all taken care automatically. In addition to that, the choice of using module.exports for exporting the style-selecting function allows us to require styles on a single line, opposed to the mandatory two lines we would have needed to write, had we chosen to use export default

import getStyles from './theme';
const styles = getStyles('App');

Provided that you have updated all styles’ imports in App.js, Foo.js, Bar.js, and Baz.js with the new syntax, you can now toggle themes by changing the import in theme/index.js. Quite handy, right?

White-label configurator

So far, we have seen how to easily select and order modules, as well as to define new themes and choosing one for the application. Doing so involves modifying files modules/index.js and theme/index.js.

While it is not too complicated to manually edit these files, it would be more practical having a tool that does the job for us. This can be accomplished by writing a utility that, given a target application, a theme and some modules, generates modules/index.js and theme.index.js files.

The configurator can be developed with any language of choice — Python, Java, Prolog, etc. I will not discuss in detail my implementation here, but you can find the source code of the bash script wl-configure.sh in the repository of the project linked at the end of the article. The utility takes in input the target directory, the list of modules and the name of the selected theme, and it is used as follows

./wl-configure -a whitelabel -m Foo,Bar,Baz -t solarized-dark

from outside the whitelabel directory. This utility allows to quickly switch configuration of our white-label application without having to manually edit files.

App generation

Our wl-configure script allows us to quickly switch between themes and select modules, but for now it only operates on the white-label directory. I previously stated that I would give instructions on how to generate more applications from the WLA, but I still have not done so. I will now put remedy to that.

To generate a custom application from the WLA we need to

  1. Copy the whitelabel directory;
  2. Configure modules/index.js;
  3. Configure theme/index.js;
  4. Set bundle id;
  5. Set display name.

While step 1–3 are quite trivial — copying a directory is not a problem, and we discussed in the previous section how to automate step 2 and 3 — steps 4 and 5 require some more effort.

A custom bundle id is required in order to be able to install on the same device different applications originated from the same WLA. Would we not change it, apps with same bundle id com.whitelabel would conflict upon installation. The display name customisation is far less critical; however, it will let us distinguish more easily different apps on the same device.

To change the bundle id of an app we need to find the old one — com.whitelabel — in the following files

android/app/BUCK
android/app/build.gradle
android/app/src/main/AndroidManifest.xml
android/app/src/main/java/com/whitelabel/MainActivity.java android/app/src/main/java/com/whitelabel/MainApplication.java
ios/whitelabel.xcodeproj/project.pbxproj

and replace it with the new one. Moreover, we need to move Android files MainActivity.java and MainApplication.java from

android/app/src/main/java/com/whitelabel/MainActivity.java android/app/src/main/java/com/whitelabel/MainApplication.java

to

android/app/src/main/java/com/new/bundle/id/MainActivity.java android/app/src/main/java/com/new/bundle/id/MainApplication.java

(here we tookcom.new.bundle.id as an example of a new bundle id)

Changing display name requires a similar, yet simpler, procedure, as we only need to find and replace the old display name — whitelabel —with the new one in the following files

android/app/src/main/res/values/strings.xml
ios/whitelabel/Info.plist

Once more, it is best to create a tool that takes care of the generation of the application. As for wl-configure.sh, I provide in the example repository — that can be found at the end of the article — a wl-generate.sh script that operates in a similar fashion

./wl-generate.sh -a test -d Test -b com.test -m Bar -t solarized-dark

where it is possible to specify target bundle id and display name. Note carefully that bundle id and display name should never be changed in the whitelabel project, but only in the generated ones.

Now, we can install on devices our freshly generated application, which will coexists with the WLA build

White-label and test applications are both installed on the same device, each with a different configuration.

Conclusions

The white-label application is a useful tool for generating applications with a similar core and sharing common features. It avoids code repetition and saves up significant development time. It also provides consistency across applications. The features best suited for a WLA are modules composition and theme selection; however, it can be improved to support additional functionalities and customisation.

The white-label process can be controlled via app configuration and app generation. The configuration allows to quickly switch the white-label state, in order to test different applications without having to re-compile from scratch and keeping the codebase unified — meaning all development should be done inside whitelabel.

The app generation is the core feature of the WLA. From a white-label application it is possible to create a new one, a combination of the resources produced in the WLA.

In this article I showed what are the first steps to take in order to setup a white-label application; however, much more could be added to the generation process. Perhaps, I will discuss it in future articles.

Codebase

The white-label application produced in this article is available at the following address:


Amedeo Zucchetti, Software Developer @WellD

--

--