Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletter Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds
Arrow up icon
GO TO TOP
Mastering React Native

You're reading from   Mastering React Native Learn Once, Write Anywhere

Arrow left icon
Product type Paperback
Published in Jan 2017
Publisher Packt
ISBN-13 9781785885785
Length 496 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Authors (2):
Arrow left icon
Eric Masiello Eric Masiello
Author Profile Icon Eric Masiello
Eric Masiello
Jacob Friedmann Jacob Friedmann
Author Profile Icon Jacob Friedmann
Jacob Friedmann
Arrow right icon
View More author details
Toc

Table of Contents (13) Chapters Close

Preface 1. Building a Foundation in React FREE CHAPTER 2. Saying HelloWorld in React Native 3. Styling and Layout in React Native 4. Starting our Project with React Native Components 5. Flux and Redux 6. Integrating with the NYT API and Redux 7. Navigation and Advanced APIs 8. Animation and Gestures in React Native 9. Refactoring for Android 10. Using and Writing Native Modules 11. Preparing for Production 12. React Native Tools and Resources

Adding Android support to RNNYT

With all the necessary tools installed, we can actually begin to experiment with some code in an Android emulator. Open up our RNNYT project and make sure you have your Android emulator running.

From your project's root directory, launch RNNYT in the Android emulator by running the command:

react-native run-android

With any luck, you'll see the Welcome to React Native screen.

Tip

Running adb devices will display a list of all attached Android devices. This list will include emulators and any physical hardware connected, with developer options enabled.

Before we dive into any refactoring, let's briefly orient ourselves to the Android emulator. The keyboard shortcut Command + R won't work for us anymore. If you want to refresh the screen, you'll need to either double tap the R key on your keyboard or launch the menu. There're a few ways to do this. The most obvious way is to click the Menu button visible under the section labeled Hardware Buttons. If keyboard shortcuts are more your thing, you can do the same by pressing Command + M on your keyboard. Refer to the following screenshot:

Adding Android support to RNNYT

From there, you can trigger the remote debugger, hot reloading, manual reloading, and other debugging tools.

Branching platform logic

As you've most likely noticed by now, React Native allows you to optionally add a .ios.js or .android.js extension to any file name. If you omit the platform-specific extension, React Native assumes that the file is universal and will be used by either platform. This is probably best explained with some examples. Imagine our project includes the following files:

  • Home.js
  • MyComponent.js
  • MyComponent.android.js

Among other things, Home.js includes the following code:

    import MyComponent from './MyComponent';

If the code is run on Android, it will use MyComponent.android.js. However, when the code runs on iOS, the packager is unable to find MyComponent.ios.js and will fall back to MyComponent.js.

Now look at our next example:

  • Home.js
  • MyComponent.js
  • MyComponent.ios.js
  • MyComponent.android.js

Here, iOS would ignore MyComponent.js and utilize MyComponent.ios.js. And, once again, Android would use MyComponent.android.js. Admittedly, this is a contrived example since MyComponent.js would effectively go unused. However, it illustrates the point that platform-specific files will always be favored over non-platform-specific files. If your code is meant to be universal, just use the traditional .js extension. There is one exception to all of this. The root project files index.ios.js and index.android.js must maintain their platform extensions. Even if the contents of each of these files are identical, you cannot create a universal index.js file.

This capability illustrates one of the ways you can tailor your code to a specific platform. But what if you don't want to branch an entire file? What if you only need to branch a small piece of logic within a file? Thankfully, React Native supports that too. In this case, both platforms can use the same JavaScript file, but utilize the Platform API to branch the code:

import { Platform } from 'react-native'; 
 
if (Platform.OS === 'android') { 
  // Do something specific for Android 
} else if (Platform.OS === 'ios') { 
  // Handle iOS 
} 

The Platform API also includes a few other useful methods and properties. Platform.Version exposes the underlying Android version. Sadly, this only works for Android. Platform.Version on iOS will simply return undefined. Finally, Platform.select is a method you can use to toggle platform-specific code. Here's an example:

const backgroundStyle = Platform.select({ 
  ios: { 
    backgroundColor: 'green' 
  }, 
  android: { 
    backgroundColor: 'red' 
  } 
}); 
 
const styles = StyleSheet.create({ 
  container: { 
    flex: 1, 
    padding: 20, 
    ...backgroundStyle 
  } 
}); 

Here, Platform.select expects an object with keys that match the platform. Those keys can map to any value. In the previous code sample, the keys map to an object that defines platform-specific background colors. It then uses the spread operator to merge the background color with the rest of the styles inside the container.

Refactoring RNNYT for Android

Thankfully, we've already moved as much code as possible outside of index.ios.js. Therefore, we can duplicate the contents of index.ios.js into index.android.js. Take a look at the following code snippet:

import { 
  AppRegistry 
} from 'react-native'; 
import App from './src/App'; 
 
AppRegistry.registerComponent('RNNYT', () => App); 

If you happen to refresh your app in the Android emulator, you'll probably see the error Requiring unknown module "../components/HomeScreen". Given our discussion of platform-specific file extensions, this shouldn't come as any surprise. The HomeScreen component is housed inside HomeScreen.ios.js. Let's create a different version of HomeScreen that's customized for Android inside HomeScreen.android.js. Just to recap, HomeScreen is responsible for toggling between our three primary components-- NewsFeedContainer, SearchContainer, and BookmarksContainer. We accomplished this with the iOS specific component TabBarIOS. For Android, we'll need to do something else. But, before we get into that, let's just get a basic component running that simply renders the NewsFeedContainer. Even though this component doesn't need to be a class-based component yet, we'll set it up as such since we'll be adding methods to it shortly. Take a look at the following code snippet:

// HomeScreen.android.js 
import React, { Component, PropTypes } from 'react'; 
import NewsFeedContainer from '../containers/NewsFeedContainer'; 
 
export default class HomeScreen extends Component { 
  render() { 
    return <NewsFeedContainer />; 
  } 
} 
 
HomeScreen.propTypes = { 
  selectedTab: PropTypes.string, 
  tab: PropTypes.func.isRequired 
}; 

Aside from some styling issues within the NewsFeed, this simplified version of HomeScreen gets us back in the game. The list of news items will render and allow us to press on them to see the detailed article. However, we added an onLongPress event handler inside of NewsItem, which uses the ActionSheetIOS API. For Android, we'll use the ToastAndroid and Vibration APIs to inform users they've bookmarked a news article.

Import ToastAndroid, Platform, and Vibration at the top of NewsItem.js:

import { 
  View, 
  TouchableOpacity, 
  StyleSheet, 
  ActionSheetIOS, 
  ToastAndroid, 
  Platform, 
  Vibration 
} from 'react-native'; 

Then, inside the onLongPress method, we'll utilize Platform.select to branch the logic between Android and iOS solutions:

onLongPress() { 
  const platformMsgFn = Platform.select({ 
    android: () => { 
      ToastAndroid.show( 
        `"${this.props.title}" has been bookmarked!`, 
        ToastAndroid.LONG 
      ); 
      Vibration.vibrate(); 
      this.props.onBookmark(); 
    }, 
    ios: () => ( 
      ActionSheetIOS.showActionSheetWithOptions({ 
        options: ['Bookmark', 'Cancel'], 
        cancelButtonIndex: 1, 
        title: this.props.title 
      }, (buttonIndex) => { 
        if (buttonIndex === 0) { 
          this.props.onBookmark(); 
        } 
      }) 
    ) 
  }); 
 
  platformMsgFn(); 
} 

I've refactored this code using Platform.select to return an anonymous function based on the platform. The resulting function is stored as platformMsgFn and is then executed. In the case of Android, the anonymous function executes ToastAndroid.show, passing it a message and a constant specifying how long the toast should remain visible (this can be either ToastAndroid.SHORT or ToastAndroid.LONG). Next, we call Vibration.vibrate and save our bookmark using the onBookmark action creator.

Fixing Android vibration

If you run the app inside the Android emulator and attempt to long press one of the news articles, you'll be greeted with a very unfriendly error Requires VIBRATE permissions. What's that about? you ask. Well, Android requires that we give it explicit permissions to utilize vibration notifications. Thankfully, this is pretty simple. Open up android/app/src/main/AndroidManifest.xml. Within the manifest tag, add the following:

<uses-permission android:name="android.permission.VIBRATE"/> 

We'll need to rebuild the app in order for the permission to take effect. From your Terminal, simply rerun:

react-native run-android

Now, if you try to bookmark an article, you'll see everything works as expected (the vibration itself will only work with actual hardware). Refer to the following screenshot:

Fixing Android vibration

With that behind us, let's get back to our Android HomeScreen component. React Native offers two components for Android that solve our navigation dilemma. One option is ToolbarAndroid. This component locks a fixed header at the top of your app that can contain a logo, a navigation icon (for example, a hamburger menu), a title, and a subtitle. This would work; however, I've found this component doesn't offer many options for styling. Instead, we're going to use the fairly vanilla DrawerLayoutAndroid. This component adds an off-screen drawer that can be toggled by swiping your finger (or, in the case of the emulator, your mouse cursor) to the right from the left edge of the device. The drawer typically contains navigation items, but could also display a logo or any other information.

Using DrawerLayoutAndroid

What I like about DrawerLayoutAndroid is that there isn't a lot to it. As stated earlier, it's just a component that can slide in from off-screen to display our navigation options. Beyond that, we control all the logic to display the selected view. Thankfully, this won't be too challenging. To begin, add these imports to HomeScreen.android.js:

import { 
  DrawerLayoutAndroid, 
  View, 
  StyleSheet 
} from 'react-native'; 
import SearchContainer from '../containers/SearchContainer'; 
import BookmarksContainer from '../containers/BookmarksContainer'; 
import AppText from './AppText'; 
import * as globalStyles from '../styles/global'; 

Let's also configure some styles that we'll need shortly:

const styles = StyleSheet.create({ 
  container: { 
    backgroundColor: globalStyles.BG_COLOR, 
    flex: 1 
  }, 
  drawer: { 
    backgroundColor: globalStyles.BG_COLOR, 
    flex: 1, 
    padding: 10 
  }, 
  drawerItem: { 
    fontSize: 20, 
    marginBottom: 5 
  }, 
  menuButton: { 
    marginHorizontal: 10, 
    marginTop: 10, 
    color: globalStyles.LINK_COLOR 
  } 
}); 

Next, add this navigation configuration before the HomeScreen component:

const navConfig = { 
  order: ['newsFeed', 'search', 'bookmarks'], 
  newsFeed: { 
    title: 'News', 
    view: <NewsFeedContainer />, 
    tab: 'newsFeed' 
  }, 
  search: { 
    title: 'Search', 
    view: <SearchContainer />, 
    tab: 'search' 
  }, 
  bookmarks: { 
    title: 'Bookmarks', 
    view: <BookmarksContainer />, 
    tab: 'bookmarks' 
  } 
}; 

This configuration will be used to dynamically set the active view.

Within the render method, we're going to wrap the active container element with DrawerLayoutAndroid. DrawerLayoutAndroid accepts several props. We're just going to focus on a few key ones. renderNavigationView is a prop that expects a function that returns the drawer's view contents. The drawerWidth prop sets the width of the drawer, and drawerPosition configures the drawer to slide in from either the left or right side. Finally, drawerBackgroundColor acts as an overlay sitting between the open drawer and the remainder of the application. We'll start by filling in the completed render method and then we'll add in the missing pieces afterward:

render() { 
  return ( 
    <DrawerLayoutAndroid 
      drawerWidth={310} 
      drawerPosition={DrawerLayoutAndroid.positions.Left} 
      drawerBackgroundColor="rgba(0,0,0,0.5)" 
      renderNavigationView={this.renderDrawer} 
    > 
      <View style={styles.container}> 
        <AppText 
          style={styles.menuButton} 
          onPress={this.showNav} 
        >Menu</AppText> 
        {navConfig[this.props.selectedTab].view} 
      </View> 
    </DrawerLayoutAndroid> 
  ); 
} 

Inside DrawerLayoutAndroid, we've added an AppText element. At the moment, it has an onPress handler pointing to a yet-to-be-defined method. Once it's implemented, it will provide an alternative means of opening the drawer for users unfamiliar with Android's drawer swipe gesture. The renderNavigationView prop is calling out to a method we'll define next:

constructor(props) { 
  super(props); 
  this.renderDrawer = this.renderDrawer.bind(this); 
} 
 
renderDrawer() { 
  return ( 
    <View style={styles.drawer}> 
      {navConfig.order.map(key => ( 
        <AppText 
          key={key} 
          style={styles.drawerItem} 
          onPress={() => this.props.tab(navConfig[key].tab)} 
        > 
          {navConfig[key].title} 
        </AppText> 
      ))} 
    </View> 
  ); 
} 

renderDrawer does exactly that it returns a View that contains our three navigation options, News, Search, and Bookmarks. Pressing on any of them will call the tab action creator. Once the active tab is updated in the Redux state, the correct container will render inside the DrawerLayoutAndroid:

{navConfig[this.props.selectedTab].view} 

This gets us a lot closer to where we'd like to be. However, we still have a few problems. For one, the Menu button doesn't do anything. Also, you need to manually swipe away the drawer after selecting News, Search, or Bookmarks. Thankfully, DrawerLayoutAndroid has openDrawer and closeDrawer methods. We just need some way of hooking them into our DrawerLayoutAndroid element so we can execute them. This is one of those cases where we'll want to use a React ref (reference). If you've used ref in React before, you may have seen something like <Component ref="myRefName" />. Here, the ref is set to a string and can be referenced in other parts of the component with this.refs.myRefName. However, the string value approach is considered legacy. So, we'll use the ref callback value. Add the following ref callback:

<DrawerLayoutAndroid 
  ref={(c) => { this.drawer = c; }} 
  //... 

Once the DrawerLayoutAndroid element has mounted, the ref callback will execute, saving the component c to this.drawer. Next, update the onPress handler inside renderDrawer to close the drawer after the user selects an option:

onPress={() => { 
  this.props.tab(navConfig[key].tab); 
  this.drawer.closeDrawer(); 
}} 

Now we can write that showNav method we've been neglecting, allowing us to open the drawer by clicking the Menu button:

showNav() { 
  this.drawer.openDrawer(); 
} 

Since showNav references this, we'll need to bind it in the constructor:

this.showNav = this.showNav.bind(this); 

Customizing Android styling

Functionally, our app is working well on Android. Visually, however, it could use some tuning. When we defined many of our global styles back in earlier chapters, we were only taking iOS into account. We'll need to tweak a few things to make the design work nicely on both platforms.

Let's start by fixing our global styles. Through my own experimentation, I've found that, when it comes to styling view container elements, Android tends to work better with margin and iOS with padding. With that said, we're going to create two new style files in the src/styles directory platform.android.js and platform.ios.js.

We'll start with platform.ios.js. Inside this file, we'll place all the styles that are specific to iOS:

export default { 
  PAGE_CONTAINER_STYLE: { 
    paddingTop: 20, 
    paddingHorizontal: 10, 
    marginBottom: 48 
  }, 
  TEXT_STYLE: { 
    fontFamily: 'Helvetica Neue' 
  } 
}; 

Now we'll do the same for Android inside platform.android.js:

export default { 
  PAGE_CONTAINER_STYLE: { 
    marginTop: 10, 
    marginHorizontal: 10 
  }, 
  TEXT_STYLE: { 
    fontFamily: 'sans-serif' 
  } 
}; 

Next, open up global.js and import the platform module at the top, then destructure the values into PAGE_CONTAINER_STYLE and TEXT_STYLE:

import platformStyles from './platform'; 
 
const { PAGE_CONTAINER_STYLE, TEXT_STYLE } = platformStyles; 

Thanks to our use of platform file extensions, our code will automatically use the correct styles. Now we just need to apply them. To do this, update COMMON_STYLES:

export const COMMON_STYLES = StyleSheet.create({ 
  pageContainer: { 
    backgroundColor: BG_COLOR, 
    flex: 1, 
    ...PAGE_CONTAINER_STYLE 
  }, 
  text: { 
    color: TEXT_COLOR, 
    ...TEXT_STYLE 
  } 
});  

Take a look at the following screenshot:

Customizing Android styling

With everything looking nice on our primary news feed, let's switch over to the Search component. I feel there's a bit too much margin above the search box on Android. Plus, there's this unsightly dark line that Android adds by default to TextInputs. Let's touch up the Search component by importing Platform at the top of Search.js, and update the search style's marginTop to the following:

marginTop: Platform.OS === 'ios' ? 10 : 0, 

While we're at it, let's update the input style so Android adds some paddingBottom:

paddingBottom: Platform.OS === 'android' ? 8 : 0 

TextInput has several props that are platform specific allowing us to tune the styling and user experience. In order to remove the underline visible on Android, set the underlineColorAndroid prop to transparent. We can also change the Android keyboard's return key text by setting the returnKeyLabel prop. Thankfully, iOS will just ignore these so we don't need to add any platform branching logic. On the iOS side, we can change the keyboard's default theme to dark, using the keyboardAppearance prop, to match our app's aesthetic:

<TextInput 
  style={styles.input} 
  onChangeText={this.searchNews} 
  value={this.state.searchText} 
  placeholder="Search" 
  placeholderTextColor={globalStyles.MUTED_COLOR} 
  underlineColorAndroid="transparent" 
  returnKeyLabel="Search" 
  keyboardAppearance="dark" 
/> 

In the process of fixing up our global styles, we actually introduced a bug into our IntroScreen. If you visit the IntroScreen now on Android, you'll find an unsightly white border around the top, left, and right sides. This was introduced from the margin values we applied globally. There's also one other minor issue. We call the iOS-only setBarStyle method of StatusBar. We can fix the latter issue by once again importing Platform from React Native and then wrapping our call to setBarStyle with a simple if statement:

if (Platform.OS === 'ios') { 
  StatusBar.setBarStyle('light-content'); 
} 

And, finally, since neither platform actually needs a margin applied to the IntroScreen, we'll override the global pageContainer styles to reset the affected margin properties:

container: { 
  marginTop: 0, 
  marginBottom: 0, 
  marginHorizontal: 0, 
  justifyContent: 'center', 
  alignItems: 'center' 
} 

Enabling LayoutAnimation

There's one final edit we need to make to our Onboarding component. If you click through the onboarding experience, you'll notice everything animates as expected until you reach the final transition. Pressing Done is intended to trigger an animation using LayoutAnimation (as opposed to using the Animated API). As is, LayoutAnimation doesn't throw any errors, but it also doesn't animate whatsoever. It just appears.

In order to use LayoutAnimation on Android, we need to explicitly opt into experimental layout animation by using the UIManager. UIManager is yet another API we'll import from the React Native library.

Inside of Onboarding.js, import the UIManager:

import { 
  StyleSheet, 
  View, 
  LayoutAnimation, 
  Animated, 
  PanResponder, 
  UIManager 
} from 'react-native'; 

You'll need to exercise caution since the setLayoutAnimationEnabledExperimental method isn't available on iOS. That said, we need to first verify if the method is present. If so, we'll call it inside the Onboarding constructor:

constructor(props) { 
  super(props); 
  this.moveNext = this.moveNext.bind(this); 
  this.movePrevious = this.movePrevious.bind(this); 
  this.transitionToNextPanel = this.transitionToNextPanel.bind(this); 
  this.moveFinal = this.moveFinal.bind(this); 
  this.state = { 
    currentIndex: 0, 
    isDone: false, 
    pan: new Animated.Value(0) 
  }; 
  if (UIManager.setLayoutAnimationEnabledExperimental) { 
    UIManager.setLayoutAnimationEnabledExperimental(true); 
  } 
} 

With this change in place, the Onboarding component will behave exactly as it does on iOS.

lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image