Most of the work involved in making a native app feel polished comes from having well-designed components that can communicate a strong visual identity within the user-experience conventions of the platform. For example, iOS applications tend to rely on bottom tab navigation. The lefthand drawer or the Snackbar notifications are typically seen in Android.
Building a cross-platform application will probably mean making certain design choices that balance user experience, platform conventions, and technical complexity. These tips should help you make those choices more easily.
Maintaining a growing stylesheet is a challenge in any web application. Native applications are no different. Fortunately, React components allow us to create a unit of code that combines everything required for a user interface element to render correctly.
In the last few years, the debate around how to organize web styles has led to all sorts of semantics for describing what something is supposed to look like. Whether you are familiar with Object-Oriented CSS, SMACCS, Tachyons, or BEM, any of these design choices rely on the language’s ability to compose stylesheet declarations.
React Native does not support CSS. CSS is a language for describing how something looks, with syntax that reduces the effort in defining common styles. This section illustrates how we can achieve many of the features of CSS using simple JavaScript declarations.
How do we reuse as many styles as possible and keep the application’s look and feel consistent?
All applications will have a common set of applicable fonts, colors, and component styles. These might include how rounded a button corner should be, or what the appropriate padding should be between typographic elements. I like to keep these bits of style information in a styles.js file in my project root with key sections that will broadly define the aesthetic of my application:
Color Palette
Typography Choices
Global Styles
Here’s an example of what a styles.js file might look like:
import
{
Dimensions
}
from
'react-native'
;
const
{
width
,
height
}
=
Dimensions
.
get
(
'window'
);
// COLOR
export
const
colors
=
{
PRIMARY
:
'#005D64'
,
SECONDARY
:
'#CA3F27'
,
}
// TYPOGRAPHY
const
scalingFactors
=
{
small
:
40
,
normal
:
30
,
big
:
20
,
}
export
const
fontSizes
=
{
H1
:
{
fontSize
:
width
/
scalingFactors
.
big
,
lineHeight
:
(
width
/
scalingFactors
.
big
)
*
1.3
,
},
P
:
{
fontSize
:
width
/
scalingFactors
.
normal
,
lineHeight
:
(
width
/
scalingFactors
.
normal
)
*
1.3
,
},
SMALL
:
{
fontSize
:
width
/
scalingFactors
.
small
,
},
}
// GLOBAL STYLES
export
const
globalStyles
=
{
textHeader
:
{...
fontSizes
.
H1
,
color
:
colors
.
PRIMARY
,
paddingTop
:
20
,
fontWeight
:
'bold'
,
},
}
The textHeader
component illustrates a classic form of composition. It relies
on the fontSizes.H1
key as a basis for the textHeader
. If we need to change
the overall size of the primary header in our application, we need only change
the scaling factors to see these adjustments happen everywhere.
By importing the Dimensions
library from React Native, we can perform some
simple math operations in our definition of these fontSizes
, ensuring that the
typography feels the same across platforms and device sizes.1
The biggest benefit to this approach is that all the styles are defined using the same programming language we use to build the rest of the application.
With global styles defined, they can be referenced in your own flavor of the
base components. For example, here is a definition for <TextHeading />
and
<SecondaryTextHeading />
components:
import
React
from
'react'
;
import
{
Text
,
}
from
'react-native'
;
import
{
globalStyles
,
colors
}
from
'../styles'
;
export
function
TextHeading
(
props
)
{
return
<
Text
style
=
{
globalStyles
.
textHeader
}
>
{
props
.
children
}
<
/Text>
}
export
function
SecondaryTextHeading
(
props
)
{
return
<
Text
style
=
{[
globalStyles
.
textHeader
,
{
color
:
colors
.
SECONDARY
}
]}
>
{
props
.
children
}
<
/Text>
}
In the preceding example, rather than implement a class that extends
React.Component
, I use a shorthand for a pure function—a function with no
side effects—which supports two JSX components. This syntax provides a hint to
the developer that this function will not have any local state.
The <SecondaryTextHeading />
component overrides the color
declaration with a style
array attribute. Each item in the array is merged together, with the last item
in the array overriding any previous declarations. The style
attribute in this case will be:
{
fontSize
:
width
/
scalingFactors
.
big
,
lineHeight
:
(
width
/
scalingFactors
.
big
)
*
1.3
,
color
:
colors
.
SECONDARY
,
paddingTop
:
20
,
fontWeight
:
'bold'
,
}
There are some great component libraries in the React Native ecosystem. react-native-elements provides an excellent set of cross-platform components with some of the most common components. NativeBase accomplishes the same goals with a more featureful component library. These libraries are a great way of ensuring that your app will be functional and consistent.
react-native-material-kit aims to provide a complete component library based on Google’s Material Design.
If you find yourself customizing every component, you might be better off developing your own component library.
Your app will run on a number of different form factors and device sizes. This means that setting up a pixel-based design will result in a lot of testing and per-device rework. Avoid most of those headaches by using a flexbox layout.
How do you build a flexible layout system that will work with different device sizes? Using just a handful of style declarations we can build complex views like the one in Figure 3-1.
The layout in Figure 3-1 was rendered using this simple component. While I would recommend
using the StyleSheet
class for performance and reusability, writing the styles
inline helps illustrate how each parent <View />
configures the flow direction
of the child <View />
:
import
React
,
{
Component
}
from
'react'
;
import
{
Text
,
View
}
from
'react-native'
;
export
default
class
ThreeColumns
extends
Component
{
sidebar
()
{
const
avatarStyle
=
{
width
:
40
,
height
:
40
,
borderRadius
:
40
,
justifyContent
:
'center'
,
backgroundColor
:
'#A0'
}
return
<
View
style
=
{{
flex
:
0.2
,
backgroundColor
:
'#333'
}}
>
<
View
style
=
{{
flex
:
0.2
,
backgroundColor
:
'#666'
,
flexDirection
:
'row'
}}
>
<
View
style
=
{{
width
:
50
,
padding
:
5
,
backgroundColor
:
'#000'
}}
>
<
View
style
=
{
avatarStyle
}
/>
<
/View>
<
/View>
<
View
style
=
{{
flex
:
0.8
}}
/>
<
/View>
}
body
()
{
return
<
View
style
=
{{
flex
:
0.5
,
backgroundColor
:
'#FFF'
}}
>
<
Text
style
=
{{
padding
:
40
,
fontSize
:
22
}}
>
Lorem
ipsum
dolor
sit
amet
,
consectetur
adipiscing
elit
.
Fusce
vestibulum
tempor
nisl
.
<
/Text>
<
/View>
}
rightBar
()
{
return
<
View
style
=
{{
flex
:
0.1
,
backgroundColor
:
'#FFA'
}}
><
/View>
}
render
()
{
return
(
<
View
style
=
{{
flexDirection
:
'row'
,
flex
:
1
,
backgroundColor
:
'#FFF'
}}
>
{
this
.
sidebar
()}
{
this
.
body
()}
{
this
.
rightBar
()}
<
/View>
);
}
}
The main render()
function wraps a sidebar()
, body()
, and rightBar()
component with a flexDirection: "row"
style attribute. The flexDirection
will dictate whether block elements should stack vertically or horizontally. By
default, a <View />
will stack vertically. The default flexDirection
in
React Native is column and not row (like in CSS).
In this case, we want our outer container to flow like a row: with the sidebar
, body
, and rightBar
appearing next to each other. The flex
value indicates the relative size of
the container. There are two commonly used conventions for flex
values: 1
or
10
. In this case, the outer view has a container size of 1
. sidebar()
will take up 20% of the component size with a flex
value of 0.2
. The
body()
function will return a <View />
with a flex
value of 0.5
,
accounting for 50% of the view. The remaining rightBar()
will fill 10%.
There are some other flexbox style declarations for handling alignment and what
to do with excess space in the layout. Once you have the right blocks in place,
use justifyContent
and alignItems
to position the child elements. Flexbox
views also work well with pixel-based views like the avatarStyle
in the preceding code.
Flexbox layouts originated in the web design community as a mechanism for handling the challenge of an ever-changing browser window. Fortunately for web developers, you are probably already familiar with the CSS implementation of flexbox, so you should have little trouble adjusting to React Native’s implementation.
The React Native documentation provides a helpful guide for laying out flexbox views.
Your app will start coming alive once you include icons and other design cues.
Fortunately we can use libraries like
react-native-vector-icons
.
How do you decide the best way to display vector images in your application?
Working with images and binaries is easily done with require()
statements, but vectors and
icons are special. They do not render out of the box in Android or iOS.
Different solutions exist depending on whether you have a number of vectors files, the complexity of the design, whether or not there are multiple colors in the design, and if you need to target a number of platforms.
The simplest solution in some cases is simply to convert the file into a rasterized file format, like PNG or JPG. The React Native packager is smart enough to detect these dependencies and bundle them together. In order for the file to render correctly for different screen densities, it’s helpful to provide alternative versions of the same file. In this case, I have a vector of a lightbulb, bulb.svg, which has been converted into a number of different pixel density equivalent images:
components └── images ├── bulb.svg ├── [email protected] ├── [email protected] ├── [email protected] ├── [email protected] ├── [email protected] ├── [email protected] ├── [email protected] └── index.js
Vector editing programs like Adobe Illustrator provide an “Export to Screens” function, making exporting different pixel densities easy, as shown in Figure 3-2.
The index.js file uses a require()
statement that can infer the
correct image and platform to load:
import
React
,
{
Component
}
from
'react'
;
import
{
Image
}
from
'react-native'
;
export
const
Bulb
=
()
=>
<
Image
source
=
{
require
(
'./bulb.png'
)}
/>
In the main application, you can now reference the image as though it were any other React component:
import
{
Bulb
}
from
'./components/images'
export
default
class
App
extends
Component
<
{}
>
{
render
()
{
return
(
<
View
style
=
{{
flex
:
1
,
justifyContent
:
'center'
,
alignItems
:
'center'
}}
>
<
Bulb
/>
<
/View>
);
}
}
There are a couple of solutions to vectors: converting them to SVG markup and using a library or converting them to fonts.
react-native-vector-icons
provides a set of React components for describing an
SVG using React Native components. At the time of this writing, certain attributes such as clip-path
are partially supported. This approach requires essentially redrawing the icon
in the application.
The same lightbulb can be exported as the following SVG file:
<?xml version="1.0" encoding="UTF-8"?>
<svg
xmlns=
"http://www.w3.org/2000/svg"
id=
"Layer_1"
data-name=
"Layer 1"
viewBox=
"0 0 86 114"
>
<defs>
<style>
.cls-1{fill:#dcdfe1;}.cls-1,.cls-4,.cls-5{stroke:#555e65; stroke-miterlimit:10; stroke-width:2px;}.cls-2{fill:#fff;}.cls-3{fill:#faf7de;} .cls-4,.cls-5{fill:none;} .cls-4{opacity:0.5;}</style>
</defs>
<title>
bulb</title>
<g
id=
"Lightbulb"
>
<ellipse
class=
"cls-1"
cx=
"43"
cy=
"96.61"
rx=
"6.77"
ry=
"5.42"
/>
<ellipse
class=
"cls-1"
cx=
"43"
cy=
"92.55"
rx=
"10.16"
ry=
"5.42"
/>
<ellipse
class=
"cls-1"
cx=
"43"
cy=
"88.48"
rx=
"10.16"
ry=
"5.42"
/>
<ellipse
class=
"cls-1"
cx=
"43"
cy=
"84.42"
rx=
"10.16"
ry=
"5.42"
/>
<path
class=
"cls-2"
d=
"M70.08,39.06A27.09,27.09,0,1,0,23.44,58,20,20,
0,0,1,29, 72.21v3.41c0,5.61,6.52,10.16,14,
10.16s14-4.55,14-10.16v-3.4a19.94,19.94,0,0,1,
5.52-14.16A26.78,26.78,0,0,0,70.08,39.06Z"
/>
<path
class=
"cls-3"
d=
"M44.5,85.1C38.15,85.1,33,81.45,33,77V73.57a22.24,
22.24,0,0,0-6.45-15.62,25,25,0,0,1,
16-42.52q.9-.06,1.82-.06a25.08,25.08,0,0,1,25,
25.05A24.83,24.83,0,0,1,62.27,58,22,22,0,0,0,56,
73.57V77C56,81.45,50.85,85.1,44.5,85.1Z"
/>
<path
class=
"cls-4"
d=
"M34.2,79c0-3,3.94-5.42,8.8-5.42S51.8,76,51.8,79"
/>
<path
class=
"cls-5"
d=
"M50.45,42.44h.15A4.62,4.62,0,0,0,46,
47.18V52h4.45a4.77,4.77,0,0,0,4.74-4.78v0A4.76,
4.76,0,0,0,50.45,42.44Z"
/>
<path
class=
"cls-5"
d=
"M35.55,42.44h-.15A4.62,4.62,0,0,1,40,
47.18V52H35.55a4.77,4.77,0,0,1-4.74-4.78v0A4.76,
4.76,0,0,1,35.55,42.44Z"
/>
<polyline
class=
"cls-5"
points=
"46 79 46 52 40 52 40 79"
/>
<path
class=
"cls-5"
d=
"M70.08,39.06A27.09,27.09,0,1,0,23.44,58,20,20,
0,0,1,29,72.21v3.41c0,5.61,6.52,10.16,14,
10.16s14-4.55,14-10.16v-3.4a19.94,19.94,0,0,1,
5.52-14.16A26.78,26.78,0,0,0,70.08,39.06Z"
/>
</g>
</svg>
Because react-native-vector-icons
supports a subset of the SVG specification,
it would need to be redrawn without the style reference:
import
React
,
{
Component
}
from
'react'
;
import
Svg
,{
Ellipse
,
Path
,
Polyline
,
}
from
'react-native-svg'
;
export
default
function
()
{
return
<
Svg
height
=
"130"
width
=
"100"
>
<
Ellipse
cx
=
"43"
cy
=
"96.61"
rx
=
"6.77"
ry
=
"5.42"
fill
=
"#dcdfe1"
stroke
=
"#555e65"
strokeWidth
=
"2"
/>
<
Ellipse
cx
=
"43"
cy
=
"92.55"
rx
=
"10.16"
ry
=
"5.42"
fill
=
"#dcdfe1"
stroke
=
"#555e65"
strokeWidth
=
"2"
/>
<
Ellipse
cx
=
"43"
cy
=
"88.48"
rx
=
"10.16"
ry
=
"5.42"
fill
=
"#dcdfe1"
stroke
=
"#555e65"
strokeWidth
=
"2"
/>
<
Ellipse
cx
=
"43"
cy
=
"84.42"
rx
=
"10.16"
ry
=
"5.42"
fill
=
"#dcdfe1"
stroke
=
"#555e65"
strokeWidth
=
"2"
/>
<
Path
fill
=
"#Faf7de"
d
=
"M70.08,39.06A27.09,27.09,0,1,0,23.44,58,20,20,0,
0,1,29,72.21v3.41c0,5.61,6.52,10.16,14,10.16s14-4.55,
14-10.16v-3.4a19.94,19.94,0,0,1,
5.52-14.16A26.78,26.78,0,0,0,70.08,39.06Z"
/>
<
Path
fill
=
"none"
d
=
"M44.5,85.1C38.15,85.1,33,81.45,33,77V73.57a22.24,22.24,
0,0,0-6.45-15.62,25,25,0,0,1,16-42.52q.9-.06,
1.82-.06a25.08,25.08,0,0,1,25,25.05A24.83,24.83,
0,0,1,62.27,58,22,22,0,0,0,56,73.57V77C56,81.45,50.85,
85.1,44.5,85.1Z"
/>
<
Path
fill
=
"none"
d
=
"M34.2,79c0-3,3.94-5.42,8.8-5.42S51.8,76,51.8,79"
/>
<
Path
stroke
=
"#555e65"
strokeWidth
=
"2"
fill
=
"none"
d
=
"M50.45,42.44h.15A4.62,
4.62,0,0,0,46,47.18V52h4.45a4.77,4.77,
0,0,0,4.74-4.78v0A4.76,4.76,0,0,0,
50.45,42.44Z"
/>
<
Path
stroke
=
"#555e65"
strokeWidth
=
"2"
fill
=
"none"
d
=
"M35.55,42.44h-.15A4.62,
4.62,0,0,1,40,47.18V52H35.55a4.77,4.77,0,0,
1-4.74-4.78v0A4.76,4.76,0,0,1,35.55,42.44Z"
/>
<
Polyline
stroke
=
"#555e65"
strokeWidth
=
"2"
fill
=
"none"
points
=
"46 79 46 52 40 52 40 79"
/>
<
Path
stroke
=
"#555e65"
strokeWidth
=
"2"
fill
=
"none"
d
=
"M70.08,39.06A27.09,
27.09,0,1,0,23.44,58,20,20,0,0,1,29,72.21v3.41c0,
5.61,6.52,10.16,14,10.16s14-4.55,14-10.16v-3.4a19.94,
19.94,0,0,1,5.52-14.16A26.78,26.78,0,0,0,70.08,39.06Z"
/>
<
/Svg>
}
The added benefit of this approach is that every attribute can be edited and animated using the rest of the React Native ecosystem. In some cases this kind of effort makes a lot of sense; for example, if you want a vector image to change based on user interaction.
If you plan on using the vector in multiple colors and it doesn’t contain any color details, consider making a custom font. IcoMoon makes it easy to turn your vector art into a single font (Figure 3-3).
This approach harkens to the Wyndings font developed by Microsoft decades ago and uses the font file format to represent vector images.
The
react-native-vector-icons
library provides a set of font wrapper functions in addition to commonly used
icon sets like FontAwesome, MaterialIcons, and Ionicons.
Install it like any other React Native package via NPM:
$>
npm install react-native-vector-icons --save$>
react-native link
A folder will be created in android/app/src/main/assets/fonts for Android as shown in Figure 3-4.
The linker should also add a Resources folder to your iOS project file that contains the set of free fonts. I suggest making sure that the free fonts provided are rendering correctly in your application before loading any custom fonts.
To add an icon set you’ve downloaded from IcoMoon, you will need two files from the ZIP file provided by IcoMoon: selection.json and icomoon.ttf. The IcoMoon package will compile all your vector images into different character keys of a font.
For iOS, you will then need to reference the icomoon.ttf file in the Resources folder and include it as part of the list of Fonts provided by application in the info.plist as shown in Figure 3-5. For Android, copy the icomoon.ttf file to the android/app/src/main/assets/fonts folder.
You can now reference the component by icon name. Following is an example of using the icomoon.ttf
file with an icon called webinar
next to a FontAwesome icon called rocket
:
import
FontAwesomeIcon
from
'react-native-vector-icons/FontAwesome'
;
// Custom IcoMoon Icon
import
{
createIconSetFromIcoMoon
}
from
'react-native-vector-icons'
;
import
icoMoonConfig
from
'./fonts/selection.json'
;
const
Icon
=
createIconSetFromIcoMoon
(
icoMoonConfig
);
export
default
class
App
extends
Component
<
{}
>
{
render
()
{
return
(
<
View
style
=
{{
flex
:
1
,
justifyContent
:
'center'
,
alignItems
:
'center'
}}
>
<
Icon
name
=
'webinar'
size
=
{
30
}
color
=
'#F00'
/>
<
FontAwesomeIcon
name
=
'rocket'
size
=
{
30
}
color
=
'#333'
/>
<
/View>
);
}
}
In Recipe 2.2, we used the react-native-progress
component to build
a pie chart that would change progress amounts based on a user tapping <TouchableHighlight />
.
Indeterminate progress can be presented to the user by combining the Animated
library provided by React Native and the
react-native-progress
component. By combining these two libraries, we can build a simple component that will loop forever.
Indeterminate progress indicators help you buy time while your application finishes loading. Let’s start by defining a constructor with a local state variable in the components/loading.js file:
constructor
(
props
)
{
super
(
props
);
this
.
state
=
{
loop
:
new
Animated
.
Value
(
0
),
};
}
The loop
variable will refer to an instance of Animated.Value
that increments from 0 to 1.
componentDidMount()
is a special function React will call before it renders a
component for the first time. We will use this hook into the render loop to configure our loop:
componentDidMount
()
{
Animated
.
loop
(
Animated
.
timing
(
this
.
state
.
loop
,
{
toValue
:
1
,
duration
:
500
,
}),
).
start
();
}
Finally we will set up an interpolation function so that a corresponding rotation degree
results from every value of this.state.loop
between 0 and 1. We do not have a direct reference
to the animation loop because all interpolation is happening within native components that we
are configuring. This approach ensures smooth animations across platforms.
The render()
function relies on react-native-progress
first presented in
Recipe 2.2:
render
()
{
const
interpolation
=
this
.
state
.
loop
.
interpolate
({
inputRange
:
[
0
,
1
],
outputRange
:
[
'0deg'
,
'360deg'
]
})
const
animationStyle
=
{
transform
:
[
{
rotate
:
interpolation
}
]
}
return
<
View
>
<
Animated
.
View
style
=
{
animationStyle
}
>
<
Pie
borderWidth
=
{
2
}
progress
=
{
0.2
}
size
=
{
100
}
color
=
'#2224FF'
/>
<
/Animated.View>
<
/View>
}
The completed <Loading />
component looks like this:
import
React
,
{
Component
}
from
'react'
;
import
{
Animated
,
View
}
from
'react-native'
;
import
Progress
,
{
Pie
}
from
'react-native-progress'
;
export
default
class
Loading
extends
Component
{
constructor
(
props
)
{
super
(
props
);
this
.
state
=
{
loop
:
new
Animated
.
Value
(
0
),
};
}
componentDidMount
()
{
Animated
.
loop
(
Animated
.
timing
(
this
.
state
.
loop
,
{
toValue
:
1
,
duration
:
500
,
}),
).
start
();
}
render
()
{
const
interpolation
=
this
.
state
.
loop
.
interpolate
({
inputRange
:
[
0
,
1
],
outputRange
:
[
'0deg'
,
'360deg'
]
})
const
animationStyle
=
{
transform
:
[
{
rotate
:
interpolation
}
]
}
return
<
View
>
<
Animated
.
View
style
=
{
animationStyle
}
>
<
Pie
borderWidth
=
{
2
}
progress
=
{
0.2
}
size
=
{
100
}
color
=
'#2224FF'
/>
<
/Animated.View>
<
/View>
}
}
In this example, you will notice that the animation is applied to an <Animated.View />
component instead of a regular <View />
component. These components are
designed to accept values from either an interpolation or an Animated.Value
component. This approach avoids calling on the React.js render pipeline, which
would increase the overhead required to render a single frame of the animation.
You should be able to include the <Loading />
component in your application
and watch a spinning pie animation.
The React Native documentation provides an extensive guide explaining some of the design choices. There are also plenty of examples.
See the React Native Animation Guide.
1 See Chapter 9 of Learning React Native, 1E (O’Reilly Media) for more about responsive design and font sizes.
3.137.173.249