- Provide feedback throughout your users’ journey
- Network conditions
- Responsive UI layout rendering
- Edge case scenarios using monkey testing
- Error and exception handling
Provide feedback throughout your users’ journey
The “happy” path is a well-known concept in software development where no exceptions or errors occur. As developers, it’s our duty to make sure we design solutions that keep users on the happy path. If they end up on “unhappy” paths, well, we end up with unhappy users.
Designing solutions that abstract UI states can help create deterministic app flows and more easily keep users within deterministic “happy paths”
Changes in UI state happen regularly, especially with network requests.
Usually, we see this pattern:
requestState = {
data,
isLoading,
error,
}
Tip: you can improve this by moving the request state to a deterministic set of strings:
requestState = {
data,
state: 'LOADING' | 'COMPLETE' | 'ERROR'
}
This may seem like a small change, but we’ve removed the complexity involved in handling the loading and error states separately.
Here’s an example of how we can use requestState, for a download progress indicator:
const DownloadProgressIndicator = ({requestState}) => {
switch (requestState.status) {
case RequestStatus.LOADING:
return (
<ProgressCircle
value={uploadItem.progress}
/>
);
case RequestStatus.ERROR:
return (
<Icon
color={colors.danger}
name='error'
/>
);
case RequestStatus.COMPLETE:
return (
<Icon
color={colors.green}
name='success'
/>
);
}
};
By managing this state via a set of strings, we create a sequence of events with logic that can be managed and a simple experience for our users.
It’s important to note that requestState creates a foundation for providing feedback, as this will give users the necessary guidance for users to complete a task.
Network Conditions
Network conditions will vary; not all users will be connected to wi-fi, so we should test for those scenarios.
Here are a few tips when the network is unstable:
Debugging using Network Link Condition
An easy way to test is using Xcode’s “Network Link Condition” feature.
Here are instructions on how to enable the feature for iOS physical devices.
With this enabled, you can discover issues related to loading states and race conditions that could impact the user experience.
An optimal configuration is setting 2G Network - average; this makes the app’s requests slow enough to notice odd behaviors while loading.
Network Status Feedback
When a network connection is unstable or lost, it’s best to present the user with an alert so they understand immediately why the app is not behaving normally. You can accomplish this by using libraries like react-native-netinfo and listening to network changes.
Executing async fetch requests
Finally, fetch requests should be executed outside a component; this makes it easy to manage when unmounted. Executing asynchronous fetch requests when a component has unmounted may cause its state to change, triggering memory leaks. Ideally, fetch requests should be abstracted with libraries such as Redux or MobX. By doing this, you’ll be able to easily handle errors and store in a global state.
Responsive UI Layout Rendering
Next up, let’s look at the interface. Users expect well-functioning UIs – slow navigation or interactions can cause users to dislike your app. In Android, one major reason why apps may feel sluggish is due to overdraw.
What is overdraw?
According to the Google developer docs:
“Overdraw refers to the system’s drawing a pixel on the screen multiple times in a single frame of rendering. For example, if we have a bunch of stacked UI cards, each card hides a portion of the one below it.”
However, the system still needs to draw even the hidden portions of the cards in the stack. This is because stacked cards are rendered according to the painter’s algorithm: that is, in back-to-front order. This sequence of rendering allows the system to apply proper alpha blending to translucent objects such as shadows.
This can be enabled by going to Settings > Developer Options on your Android device and selecting Debug GPU overdraw from the HARDWARE ACCELERATED RENDERING section
Here’s an example from the Google Developer docs. Tip: by reducing the amount of overdraw you are saving GPU computation, making your app less resource-intensive and creating a snappy experience.
It’s worth noting that these optimizations will also transfer over to iOS. Fortunately, when it comes to handling operations on the UI thread, React Native works very well out of the box.
Edge case scenarios using monkey testing
When developing apps, we can only account for so many variations of user interactions; when bugs are reported, we sometimes get surprised by how they were triggered.
One way to catch these one-off interactions is to use monkey testing, which is a form of stress testing that adds confidence to the robustness of our apps. Android has a built-in solution for performing monkey tests with adb.
Enabling Screen Pinning
Tip: before running monkey tests on Android, enable Screen Pinning – this will prevent it from navigating outside the app.
This can be enabled by going to Settings > Security > Advanced > Screen Pinning and select “On.”
Running Monkey Test
Here’s an example running a monkey test on the Google Play Store app from an emulator using this command:
adb shell monkey -v 4000
You can customize input events by providing different arguments to the adb command. A comprehensive list of arguments can be found in Google’s UI/Application Exerciser Monkey documentation.
The default arguments, however, capture a wide range of scenarios. This has helped me in the past to capture a few undefined object property errors.
Disabling Screen Pinning
When done running your monkey tests, you can disable Screen Pinning using this adb command:
adb shell am task lock stop `dumpsys activity | grep -A2 '(dumpsys
activity recents)'| grep '#'| cut -d ' ' " \
"-f 7| cut -c 2-`
Monkey testing is a great way to discover bugs that are hard to reproduce. By identifying these one-off bugs you can increase the stability of your application.
Error and Exception Handling
It’s important when users end up in a situation where an error or an exception occurred that we give them feedback and next steps to remedy the situation. With React Native applications if you have unhandled JavaScript errors, you can end up with a white screen like this:
Here are a few tips to display a better message:
JavaScript Errors
Using libraries such as react-native-error-boundary you can show a message to the user explaining that something went wrong. By doing so, you give users a quick solution to get back to their task, thus making the errors more forgiving.
Native Exceptions
Native exceptions in React Native give a much harsher experience than the JavaScript counterpart. These commonly occur from imported Native modules. If not handled, it will kill the app entirely and leave the user at the home screen.
The library react-native-exception-handler is used to capture these exceptions and present a message that something went wrong and give users the ability to restart.
Unfortunately, iOS has a limitation with restarting apps; users must do it manually.
Lastly, it’s important to pair these solutions with reporting tools like Sentry to capture the exceptions and address them as hotfixes so they do not affect others.
Conclusion
Above are a few ways to make sure your app is ready for production and will deliver an exceptional user experience. Providing feedback throughout their journey when things go wrong is especially important so the user can continue on to accomplish their objective.