Chapter 4. Managing Hardware Platforms
You want the best experience for your users. With a mobile device packed with sensors, why not tap into the raw power of the machine running your code to deliver the best possible interaction? With a little bit of work, you can take advantage of the accelerometer, the GPS, the camera, and the hardware on the device. This chapter will survey some of the more common use cases and lessons learned managing the underlying hardware platform.
4.1 Asking for Permission to Use Device Hardware (iOS)
Whether you are snapping photos for a social media app or scanning a QR code in a corporate lobby, the deviceâs camera is one of the most powerful sensors at your disposal.
In some cases, a card or a modal screen with a button that triggers the hardware will help someone understand why they need to provide access like the two-step wireframe depicted in Figure 4-1. This permission flow is common in iOS applications, where permission requests can be delayed until they are required by the application.
Hardware Requires Hardware
The simulator can do some hardware testing; however, especially when dealing with camera data, there is no substitute for a real device.
The Apple App Store will review your application and flag any permissions that have not been explicitly declared. In some cases, such as with HealthKit, making background web requests, or using the location information when the app is not active, there may be some other capabilities that will also need to be enabled.
Variations Emerge Closer to the Metal
Most examples in this book assume cross-platform support. Some components, like
react-native-camera
, try (successfully) to abstract the implementation
differences between Android and iOS. In some cases, such as Apple Pay or
HealthKit, this will not be possible. You will probably end up having to write
two implementations in React Native or writing your own React Native bridge.
Solution
Letâs use the
react-native-camera
component. Another library,
react-native-permissions
,
will provide us with a standard API for seeing whether we can start using the
camera. Begin by installing it from the command line:
$
>
npm
install
react
-
native
-
camera
--
save
$
>
npm
install
react
-
native
-
permissions
--
save
$
>
react
-
native
link
Now add a description under âPrivacy - Camera Usage Descriptionâ in the Info.plist file in Xcode as shown in Figure 4-2.
Create your own <SimpleCamera />
component that will wrap some basic functionality:
// modalCamera.js
import
React
,
{
Component
}
from
'react'
;
import
{
StyleSheet
,
Text
,
Image
,
View
,
TextInput
,
TouchableHighlight
}
from
'react-native'
import
Camera
from
'react-native-camera'
;
export
default
class
ModalCamera
extends
Component
{
constructor
(
props
)
{
super
(
props
)
this
.
state
=
{
cameraType
:
Camera
.
constants
.
Type
.
back
}
}
async
capturePhoto
()
{
const
data
=
await
this
.
camera
.
capture
();
this
.
props
.
onPhoto
(
data
);
}
switchCamera
=
()
=>
{
const
{
Type
}
=
Camera
.
constants
;
const
cameraType
=
this
.
state
.
cameraType
===
Type
.
back
?
Type
.
front
:
Type
.
back
;
this
.
setState
({
cameraType
});
}
takePicture
=
()
=>
{
this
.
capturePhoto
();
}
render
()
{
return
<
View
style
=
{{
flex
:
1
,
backgroundColor
:
'blue'
}}
>
<
Camera
ref
=
{(
cam
)
=>
{
this
.
camera
=
cam
;
}}
aspect
=
{
Camera
.
constants
.
Aspect
.
fill
}
captureTarget
=
{
Camera
.
constants
.
CaptureTarget
.
disk
}
captureAudio
=
{
false
}
style
=
{
styles
.
container
}
type
=
{
this
.
state
.
cameraType
}
>
<
View
style
=
{
styles
.
buttonRow
}
>
<
TouchableHighlight
style
=
{
styles
.
button
}
onPress
=
{
this
.
switchCamera
}
>
<
Text
style
=
{
styles
.
buttonText
}
>
Switch
<
/Text>
<
/TouchableHighlight>
<
TouchableHighlight
style
=
{
styles
.
button
}
onPress
=
{
this
.
takePicture
}
>
<
Text
style
=
{
styles
.
buttonText
}
>
Snap
Dish
<
/Text>
<
/TouchableHighlight>
<
/View>
<
/Camera>
<
/View>
}
}
const
styles
=
StyleSheet
.
create
({
container
:
{
flex
:
1
,
backgroundColor
:
"transparent"
,
},
buttonRow
:
{
flexDirection
:
"row"
,
position
:
'absolute'
,
bottom
:
25
,
right
:
0
,
left
:
0
,
justifyContent
:
"center"
},
button
:
{
padding
:
20
,
borderWidth
:
3
,
borderColor
:
"#FF0000"
,
margin
:
15
},
buttonText
:
{
color
:
"#FFF"
,
fontWeight
:
'bold'
},
})
The App.js file will highlight some of the potential states for the camera
hardware on the device. By using react-native-permissions
, we can create a
user experience where someone is alerted only when the camera request needs to
be made. This library also claims to support the latest Android permission
checks:
// App.js
import
React
,
{
Component
}
from
'react'
;
import
{
Alert
,
StyleSheet
,
TouchableHighlight
,
View
,
Text
}
from
'react-native'
import
SimpleCamera
from
'./simpleCamera'
import
Permissions
from
'react-native-permissions'
export
default
class
App
extends
Component
{
constructor
(
props
)
{
super
(
props
);
this
.
state
=
{
cameraPermission
:
null
};
}
componentDidMount
()
{
this
.
determinePermission
();
}
async
determinePermission
(){
const
cameraPermission
=
await
Permissions
.
check
(
'camera'
)
this
.
setState
({
cameraPermission
});
}
async
requestCamera
()
{
const
cameraPermission
=
await
Permissions
.
request
(
'camera'
);
this
.
setState
({
cameraPermission
});
}
photoTaken
=
({
path
})
=>
{
Alert
.
alert
(
`Photo Path:
${
path
}
`
)
}
requestPermission
=
()
=>
{
this
.
requestCamera
();
}
renderDenied
()
{
return
<
View
>
<
Text
style
=
{
styles
.
textHeading
}
>
Looks
like
you
do
not
want
to
take
any
photos
.
<
/Text>
<
Text
style
=
{
styles
.
textHeading
}
>
Please
enable
camera
functionality
in
your
application
settings
<
/Text>
<
/View>
}
renderCameraRequest
()
{
return
<
View
>
<
Text
style
=
{
styles
.
textHeading
}
>
Let
Pastry
Cookbook
share
your
dishes
with
the
world
!
<
/Text>
<
TouchableHighlight
style
=
{
styles
.
button
}
onPress
=
{
this
.
requestPermission
}
>
<
Text
style
=
{
styles
.
buttonText
}
>
Enable
Camera
<
/Text>
<
/TouchableHighlight>
<
/View>
}
render
()
{
const
{
cameraPermission
}
=
this
.
state
;
return
<
View
style
=
{
styles
.
container
}
>
{
cameraPermission
===
"undetermined"
&&
this
.
renderCameraRequest
()
}
{
cameraPermission
===
"authorized"
&&
<
SimpleCamera
onPhoto
=
{
this
.
photoTaken
}
/>
}
{
cameraPermission
===
"denied"
&&
this
.
renderDenied
()
}
<
/View>
}
}
const
styles
=
StyleSheet
.
create
({
container
:
{
flex
:
1
,
paddingTop
:
30
,
backgroundColor
:
'#000'
,
},
buttonRow
:
{
flexDirection
:
'row'
,
position
:
'absolute'
,
bottom
:
25
,
right
:
0
,
left
:
0
,
justifyContent
:
'center'
},
button
:
{
padding
:
20
,
borderWidth
:
3
,
borderColor
:
'#FFF'
,
borderRadius
:
20
,
backgroundColor
:
'#2445A2'
,
margin
:
15
},
buttonText
:
{
color
:
'#FFF'
,
fontWeight
:
'bold'
,
textAlign
:
'center'
,
},
textHeading
:
{
color
:
'#44CAE5'
,
fontSize
:
24
,
padding
:
20
,
fontWeight
:
'bold'
,
textAlign
:
'center'
,
}
})
You can see the different application states in Figure 4-3 and Figure 4-4.
When a screenshot is taken, an Alert
presents the user with the filepath on
the device. In Recipe 4.4 we will explore the filesystem in more depth.
Using Async/Await Instead of Promise()
Working with device hardware is rarely synchronous. In other words, the user
interface will not block to wait until data from a sensor returns correctly.
The result of this asynchronicity is often seen in a series of nested then( ()
=> {})
statements. In order to get around this, Iâve decided to present this
example using async
and await
. If you feel more comfortable with chaining
then()
instead, the examples should work just the same.
See Also
This example only scratches the surface of requesting permission from users. If your application will play music in the background, enable payments, track users, or anything that can be deemed invasive, expect to spend development time informing users.
Learn more about requesting permissions from the react-native-permissions GitHub page and the PermissionsAndroid React Native guide.
4.2 Fetching Paginated Requests
The infinite scroll is an endless feast of content. Just as your palette is about to surrender, you find yourself faced with a new batch of morsels to tempt further consumption.
Most applications rely on the networking infrastructure on the mobile device to make asynchronous calls to a web server, oftentimes to get a list of records. This interaction pattern is seen in most applications that present a list of choices to a user. Whether itâs a series of photos from people you follow, or the latest restaurant choices in your area, an ever-growing list of content keeps people engaged.
Problem
How do you present a paginated list of content that can be constantly refreshed?
Solution
Before we tackle the pagination challenge, we need a data provider that we can connect to. Building a web server falls outside the scope of this book, so we will rely on the JSONPlaceholder, a REST API for prototyping and testing, instead of rolling our own.
See the FlatList
in Figure 4-5, which renders a paginated set of results.
This example relies on two components as shown in Figure 4-5: <ListItem />
and the container <App
/>
.
The <ListItem />
is just a simple function that returns JSX. The overlay
effect is achieved by relying on absolute positioning of the {title}
and a
backing <View />
, which is semitransparent. The React Native guides recommend
always passing height
and width
information for dynamic images. In this
case, weâre relying on a third-party web service called LoremPixel, and we can
dictate what format we require:
//listItem.js
import
React
,
{
Component
}
from
'react'
;
import
{
Image
,
StyleSheet
,
View
,
Text
}
from
'react-native'
;
export
default
function
({
url
,
title
,
width
})
{
return
<
View
style
=
{
styles
.
card
}
>
<
Image
resizeMode
=
'cover'
source
=
{
{
uri
:
url
}
}
style
=
{[
styles
.
image
,
{
width
,
height
:
200
}]
}
/>
<
View
style
=
{[
styles
.
overlay
,
{
width
}]}
/>
<
Text
style
=
{
styles
.
text
}
>
{
title
}
<
/Text>
<
/View>
}
const
styles
=
StyleSheet
.
create
({
card
:
{
borderBottomWidth
:
5
,
borderTopWidth
:
2
,
borderBottomColor
:
'#222'
,
borderTopColor
:
'#CACACA'
,
borderStyle
:
'solid'
,
height
:
207
,
},
overlay
:
{...
StyleSheet
.
absoluteFillObject
,
height
:
30
,
top
:
170
,
position
:
'absolute'
,
backgroundColor
:
'rgba(2,2,2,0.8)'
,
},
text
:
{
fontSize
:
14
,
height
:
30
,
top
:
170
,
color
:
'#FFF'
,
backgroundColor
:
'transparent'
,
},
image
:
{...
StyleSheet
.
absoluteFillObject
,
}
});
This component illustrates the critical relationship between <RefreshControl
/>
and <FlatList />
. The fetchRecords()
method asynchrously fetches JSON
results and appends them to this.state.list
. fetchRecords()
is called on
first load, componentDidMount()
, when a refresh happens from a Pull to
Refresh event and when the user scrolls to the bottom of the list.
appendResults()
copies the retrieved list of posts into a new array with the
existing list after performing a small set of transformations.
Note
In order for iOS to make a web request, the URL must either use SSL (begin with
https) or the domain must be explicitly set as exempt in the Info.plist file
under NSExceptionDomains
. This may be required if you are running a web
development server locally without SSL.
//App.js
import
React
,
{
Component
}
from
'react'
;
import
{
StyleSheet
,
FlatList
,
Dimensions
,
RefreshControl
,
View
,
Text
}
from
'react-native'
;
import
ListItem
from
'./listItem'
const
{
width
}
=
Dimensions
.
get
(
'window'
);
const
API
=
'https://jsonplaceholder.typicode.com'
;
export
default
class
App
extends
Component
<
{}
>
{
constructor
(
props
)
{
super
(
props
)
this
.
state
=
{
refreshing
:
false
,
page
:
1
,
list
:
[]
}
}
resultToListItem
({
id
,
title
})
{
const
{
page
}
=
this
.
state
;
const
url
=
`https://lorempixel.com/
${
width
}
/200/food/
${
title
}
/`
return
{
id
:
`
${
page
}
-
${
id
}
`
,
title
:
`
${
page
}
-
${
title
}
`
,
width
,
url
}
}
appendResults
(
results
)
{
let
list
=
[];
Object
.
keys
(
results
).
forEach
(
(
photoKey
)
=>
{
list
.
push
(
this
.
resultToListItem
(
results
[
photoKey
]));
});
this
.
setState
(
(
prevState
)
=>
({
list
:
prevState
.
list
.
concat
(
list
),
refreshing
:
false
,
page
:
(
prevState
.
page
+
1
)
}));
}
async
fetchRecords
()
{
this
.
setState
({
refreshing
:
true
});
const
resp
=
await
fetch
(
`
${
API
}
/posts?_limit=5`
)
const
results
=
await
resp
.
json
();
this
.
appendResults
(
results
);
}
onRefresh
=
()
=>
{
this
.
setState
({
list
:
[],
page
:
1
});
this
.
fetchRecords
();
}
onEndReached
=
()
=>
{
this
.
fetchRecords
()
}
componentDidMount
()
{
this
.
fetchRecords
()
}
render
()
{
const
refreshControl
=
<
RefreshControl
refreshing
=
{
this
.
state
.
refreshing
}
onRefresh
=
{
this
.
onRefresh
}
/>
return
<
View
style
=
{
styles
.
container
}
>
<
View
style
=
{
styles
.
header
}
>
{
this
.
state
.
refreshing
?
<
Text
style
=
{
styles
.
headerText
}
>
Refreshing
...
<
/Text> :
<
Text
style
=
{
styles
.
headerText
}
>
{
this
.
state
.
list
.
length
}
Meals
<
/Text> }
<
/View>
<
FlatList
renderItem
=
{({
item
})
=>
<
ListItem
{...
item
}
/>
}
refreshControl
=
{
refreshControl
}
keyExtractor
=
{({
id
})
=>
id
}
data
=
{
this
.
state
.
list
}
onEndReached
=
{
this
.
onEndReached
}
/>
<
/View>
}
}
const
styles
=
StyleSheet
.
create
({
container
:
{
flex
:
1
,
paddingTop
:
30
,
backgroundColor
:
'#FFF'
,
},
headerText
:
{
color
:
'#144595'
,
fontSize
:
16
,
fontWeight
:
'bold'
,
textAlign
:
'center'
,
},
header
:
{
borderBottomWidth
:
1
,
borderBottomColor
:
'#222'
,
borderStyle
:
"solid"
,
}
})
Discussion
Implementing this pattern requires a little bit of care. Because the user can trigger a refresh at any point, and because the list of content is expanding, itâs best to use some native data structures for handling the content. Furthermore, the server may return the same content twice, making it necessary to handle duplicates. Page sizes may also vary and so the resulting list on the client might be a mash of slightly different queries to an API.
See Also
For lists that have a section that sticks to the top of the view, consider using
the SectionList
.
The API is very similar.
Managing records inside of a component can also lead to some confusion. For example, if you wanted to tap into one of these records to retrieve further information or perform a route navigation (see Recipe 2.4), then some global state management with Flux, Mobx, Redux, Apollo, or Relay may be worth considering.
4.3 Save Application State with Redux and Local Storage
Redux is one of the most popular state management libraries in the React ecosystem. Unidirectional data flow architectures like Flux, Mobx, and Redux go with React like peanut butter and strawberry jam. But what happens when a user closes your app, taps a notification, or shifts the application from a foreground state to a background state? How do we ensure that data persists in these cases?
There are many strategies for persisting data on mobile. Each app has access to some file storage; however, for data, the AsyncStorage module provides a simple API for keeping track of important information.
This example combines one of the most popular state management libraries (Redux) with the most commonly used persistence module in React Native: AsyncStorage.
Problem
You are already using Redux and have decided to adopt it for your mobile application. You noticed that users like to have data cached locally even after they have closed the application.
Solution
The redux-persist
library is an excellent starting point in resolving this
issue. This NPM mobile was conceived with support from AsyncStorage. As your
Redux architecture grows, some of the most recent design changes in version
5.x of redux-persist
will come in handy. This example relies on the project
started in Recipe 2.5, but any Redux application should work.
In our case, we begin by installing redux-persist
:
$>
npm i redux-persist --save
By adjusting the src/appContainer.js and the reduxStore.js files from Recipe 2.5, our appication will automatically store the username and application state in asynchronous storage.
reduxStore.js used to rely on the combineReducers()
method. This has been replaced with
persistCombineReducers()
, which includes a config
parameter. storage
will automatically
resolve to AsyncStorage
with React Native:
// reduxStore.js
import
*
as
reducers
from
'./src/reducers'
;
import
{
createStore
,
applyMiddleware
,
combineReducers
,
compose
}
from
'redux'
;
import
{
persistCombineReducers
}
from
'redux-persist'
;
import
storage
from
'redux-persist/es/storage'
;
import
logger
from
'redux-logger'
;
const
config
=
{
key
:
'root'
,
storage
,
};
export
default
createStore
(
combineReducers
(
reducers
),
persistCombineReducers
(
config
,
reducers
),
applyMiddleware
(
logger
)
);
redux-persist
includes a <PersistGate />
component, which is intended to limit rendering of
your application until the application state has been completely hydrated:
// src/appContainer.js
import
React
,
{
Component
}
from
'react'
;
import
AppContainer
from
'./src/appContainer'
;
import
{
Provider
}
from
'react-redux'
;
import
store
from
'./reduxStore'
;
// newly-added references to redux-persist:
import
{
persistStore
}
from
'redux-persist'
;
import
{
PersistGate
}
from
'redux-persist/es/integration/react'
;
const
persistor
=
persistStore
(
store
);
export
default
class
App
extends
Component
<
{}
>
{
render
()
{
return
<
Provider
store
=
{
store
}
>
<
PersistGate
persistor
=
{
persistor
}
>
<
AppContainer
/>
<
/PersistGate>
<
/Provider>
}
}
When the app is terminated and restarted, any state changes should be maintained in AsyncStorage.
redux-persist
is an excellent example of how the Redux design philosophy enables plugging in
libraries by extending the core Redux architecture based on your applicationâs use case.
See Also
As your app grows, you will undoubtedly want to selectively persist portions of your application.
redux-persist
provides mechanisms for handling state changes, whitelisting, and blacklisting of
reducers and parameters. Consult the documentation for more information.
Another excellent library is redux-offline
,
which depends on redux-persist
and provides additional hooks for handling poor network
connectivity scenarios.
4.4 Using the Filesystem
There are a lot of common use cases for working with an applicationâs filesystem: dealing with binary files, downloading assets from the web, or like in Recipe 4.1, because you want to manage photos inside your app.
Weâre going to extend the project started in Recipe 4.1 by adding
listing, viewing, and deleting functionality to the same application with the
react-native-fs
package.
Problem
How do you tackle some of the common challenges when dealing with the filesystem, such as how to write, delete, list, and view files?
Solution
Our solution involves refactoring App.js from Recipe 4.1 into
a <CameraContainer />
component. Our updated App.js file can toggle between a
camera view (cameraContainer.js) and a list view (listContainer.js). Figure 4-6 demonstrates the addition of a button group for toggling pages in the App.js file.
Begin by installing the
react-native-fs
package:
$>
npm install react-native-fs --save$>
react-native link react-native-fs
Letâs move the existing App.js file to a new cameraContainer.js file:
// cameraContainer.js
import
React
,
{
Component
}
from
'react'
;
import
{
Alert
,
StyleSheet
,
TouchableHighlight
,
View
,
Text
}
from
'react-native'
;
import
SimpleCamera
from
'./simpleCamera'
;
import
Permissions
from
'react-native-permissions'
;
export
default
class
CameraContainer
extends
Component
<
{}
>
{
constructor
(
props
)
{
super
(
props
);
this
.
state
=
{
cameraPermission
:
null
};
}
componentDidMount
()
{
this
.
determinePermission
();
}
async
determinePermission
(){
const
cameraPermission
=
await
Permissions
.
check
(
'camera'
)
this
.
setState
({
cameraPermission
});
}
async
requestCamera
()
{
const
cameraPermission
=
await
Permissions
.
request
(
'camera'
);
this
.
setState
({
cameraPermission
});
}
photoTaken
=
({
path
})
=>
{
Alert
.
alert
(
`Photo Path:
${
path
}
`
)
}
requestPermission
=
()
=>
{
this
.
requestCamera
();
}
renderDenied
()
{
return
<
View
>
<
Text
style
=
{
styles
.
textHeading
}
>
Looks
like
you
do
not
want
to
take
any
photos
.
<
/Text>
<
Text
style
=
{
styles
.
textHeading
}
>
Please
enable
camera
functionality
in
your
application
settings
<
/Text>
<
/View>
}
renderCameraRequest
()
{
return
<
View
>
<
Text
style
=
{
styles
.
textHeading
}
>
Let
Pastry
Cookbook
share
your
dishes
with
the
world
!
<
/Text>
<
TouchableHighlight
style
=
{
styles
.
button
}
onPress
=
{
this
.
requestPermission
}
>
<
Text
style
=
{
styles
.
buttonText
}
>
Enable
Camera
<
/Text>
<
/TouchableHighlight>
<
/View>
}
render
()
{
const
{
cameraPermission
}
=
this
.
state
;
return
<
View
style
=
{
styles
.
container
}
>
{
cameraPermission
===
"undetermined"
&&
this
.
renderCameraRequest
()
}
{
cameraPermission
===
"authorized"
&&
<
SimpleCamera
onPhoto
=
{
this
.
photoTaken
}
/>
}
{
cameraPermission
===
"denied"
&&
this
.
renderDenied
()
}
<
/View>
}
}
const
styles
=
StyleSheet
.
create
({
container
:
{
flex
:
1
,
paddingTop
:
30
,
backgroundColor
:
'#000'
,
},
buttonRow
:
{
flexDirection
:
'row'
,
position
:
'absolute'
,
bottom
:
25
,
right
:
0
,
left
:
0
,
justifyContent
:
'center'
},
button
:
{
padding
:
20
,
borderWidth
:
3
,
borderColor
:
'#FFF'
,
borderRadius
:
20
,
backgroundColor
:
'#2445A2'
,
margin
:
15
},
buttonText
:
{
color
:
'#FFF'
,
fontWeight
:
'bold'
,
textAlign
:
'center'
,
},
textHeading
:
{
color
:
'#44CAE5'
,
fontSize
:
24
,
padding
:
20
,
fontWeight
:
'bold'
,
textAlign
:
'center'
,
}
});
App.js will now set the page state between the list
and camera
states:
// App.js
import
React
,
{
Component
}
from
'react'
;
import
{
StyleSheet
,
TouchableHighlight
,
View
,
Text
}
from
'react-native'
;
import
CameraContainer
from
'./cameraContainer'
;
import
ListContainer
from
'./listContainer'
;
export
default
class
App
extends
Component
<
{}
>
{
constructor
(
props
)
{
super
(
props
);
this
.
state
=
{
page
:
"list"
}
}
render
()
{
const
{
page
}
=
this
.
state
;
return
<
View
style
=
{
styles
.
container
}
>
{
page
===
"list"
&&
<
ListContainer
style
=
{
styles
.
page
}
/>
}
{
page
===
"camera"
&&
<
CameraContainer
style
=
{
styles
.
page
}
/>
}
<
View
style
=
{
styles
.
buttonGroup
}
>
<
TouchableHighlight
onPress
=
{
()
=>
{
this
.
setState
(
{
page
:
'list'
}
)
}
}
style
=
{[
styles
.
button
,
(
page
===
"list"
&&
styles
.
activeButton
)
]}
>
<
Text
style
=
{[
styles
.
buttonText
,
(
page
===
"list"
&&
styles
.
activeButtonText
)
]}
>
List
<
/Text>
<
/TouchableHighlight>
<
TouchableHighlight
onPress
=
{
()
=>
{
this
.
setState
(
{
page
:
'camera'
}
)
}
}
style
=
{[
styles
.
button
,
(
page
===
"camera"
&&
styles
.
activeButton
)
]}
>
<
Text
style
=
{[
styles
.
buttonText
,
(
page
===
"camera"
&&
styles
.
activeButtonText
)
]}
>
Camera
<
/Text>
<
/TouchableHighlight>
<
/View>
<
/View>
}
}
const
styles
=
StyleSheet
.
create
({
container
:
{
flex
:
1
,
paddingTop
:
30
,
backgroundColor
:
'#FFF'
,
},
page
:
{
flex
:
1
,
},
buttonGroup
:
{
flexDirection
:
'row'
,
},
activeButton
:
{
backgroundColor
:
'#343678'
,
},
activeButtonText
:
{
color
:
'#FFF'
},
button
:
{
borderWidth
:
1
,
borderColor
:
'#242668'
,
flex
:
1
,
height
:
50
,
justifyContent
:
'center'
,
},
buttonText
:
{
fontWeight
:
'bold'
,
textAlign
:
'center'
,
color
:
'#242668'
,
}
});
The new <ListContainer />
component will begin by scanning the documents directory and populating
a local this.state
variable on componentDidMount()
:
// listContainer.js
import
React
,
{
Component
}
from
'react'
;
import
{
FlatList
,
StyleSheet
,
Image
,
TouchableHighlight
,
View
,
Text
}
from
'react-native'
;
import
{
unlink
,
readDir
,
DocumentDirectoryPath
}
from
'react-native-fs'
;
export
default
class
ListContainer
extends
Component
<
{}
>
{
constructor
(
props
)
{
super
(
props
);
this
.
state
=
{
photos
:
[]
}
}
componentDidMount
()
{
this
.
refreshPhotoList
();
}
async
deletePhoto
(
path
){
await
unlink
(
path
)
this
.
refreshPhotoList
();
}
async
refreshPhotoList
()
{
const
allFiles
=
await
readDir
(
DocumentDirectoryPath
);
const
photos
=
allFiles
.
filter
(
(
file
)
=>
{
return
file
.
path
.
split
(
'.'
)[
1
]
===
"jpg"
})
this
.
setState
({
photos
});
}
renderRow
(
file
)
{
return
<
View
style
=
{
styles
.
row
}
>
<
Image
style
=
{{
width
:
100
,
height
:
100
}}
resizeMode
=
'cover'
source
=
{{
uri
:
file
.
path
}}
/>
<
Text
numberOfLines
=
{
2
}
style
=
{
styles
.
rowText
}
>
{
file
.
name
}
<
/Text>
<
TouchableHighlight
style
=
{
styles
.
deleteButton
}
onPress
=
{()
=>
this
.
deletePhoto
(
file
.
path
)}
>
<
Text
style
=
{
styles
.
deleteButtonText
}
>
Delete
<
/Text>
<
/TouchableHighlight>
<
/View>
}
render
()
{
return
<
View
style
=
{
styles
.
container
}
>
<
Text
style
=
{
styles
.
titleText
}
>
My
Dishes
<
/Text>
<
FlatList
keyExtractor
=
{
({
name
})
=>
name
}
data
=
{
this
.
state
.
photos
}
renderItem
=
{
({
item
})
=>
this
.
renderRow
(
item
)
}
/>
<
/View>
}
};
const
styles
=
StyleSheet
.
create
({
container
:
{
flex
:
1
,
paddingTop
:
30
,
backgroundColor
:
'#FFF'
,
},
row
:
{
flexDirection
:
'row'
,
margin
:
5
,
},
rowText
:
{
fontSize
:
12
,
flex
:
1
,
paddingLeft
:
10
,
paddingTop
:
40
},
titleText
:
{
fontSize
:
16
,
textAlign
:
'center'
,
fontWeight
:
'bold'
,
height
:
20
,
},
deleteButton
:
{
backgroundColor
:
'#A22'
,
justifyContent
:
'center'
,
margin
:
20
,
width
:
80
,
borderRadius
:
5
,
},
deleteButtonText
:
{
color
:
'#FFF'
,
textAlign
:
'center'
,
justifyContent
:
'center'
,
}
});
We render the photos using a <FlatList />
component (discussed further in Recipe 4.2).
Notice that refreshPhotoList
is called asynchronously: all calls to the filesystem are blocking calls and therefore do not happen synchronously. By relying on Reactâs
this.state
variable, we can trigger a render on setState()
, whenever it happens next. DocumentDirectoryPath
is a global variable that
react-native-fs
resolves based on the platform and the application. Any
absolute path manipulations (such as reading a directory with readDir
) will
require using this constant.
See Also
This example only scratches the surface of whatâs possible. Use
react-native-fs
in combination with
react-native-zip-archive
to ZIP files before sending them. react-native-fs
can also provide
large data storage with redux-persist
on the Android platform thanks to
projects like
redux-persist-filesystem-storage
.
Get React Native Cookbook now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.