Short answer: Use react-native init, install react-navigation for navigation, redux + react-redux for state, axios for HTTP. Shared code is ~85% - platform differences are mainly in push notifications (different APIs), native modules, and styling edge cases. Budget 20% extra time for Android-specific fixes.
Project Setup (React Native 0.30, 2016)
npm install -g react-native-cli
react-native init MyApp --version 0.30.0
cd MyApp
npm install --save redux react-redux
npm install --save react-navigation # Replaced Navigator
npm install --save axios
npm install --save react-native-vector-icons
react-native run-ios
react-native run-android
App Structure
src/
├── App.js ← Root component with navigation
├── store/
│ ├── index.js ← Redux store
│ └── reducers/
│ ├── auth.js
│ └── orders.js
├── screens/
│ ├── LoginScreen.js
│ ├── HomeScreen.js
│ └── OrderDetailScreen.js
├── components/
│ ├── Button.js
│ └── OrderCard.js
├── services/
│ └── api.js ← API calls
└── utils/
└── platform.js ← Platform-specific code
Navigation (react-navigation replaced Navigator in 2016)
// src/App.js
import React from 'react';
import { NavigationContainer } from 'react-navigation';
import { StackNavigator } from 'react-navigation';
import { Provider } from 'react-redux';
import store from './store';
import LoginScreen from './screens/LoginScreen';
import HomeScreen from './screens/HomeScreen';
import OrderDetail from './screens/OrderDetailScreen';
const AppNavigator = StackNavigator({
Login: {
screen: LoginScreen,
navigationOptions: { header: null } // No header on login screen
},
Home: {
screen: HomeScreen,
navigationOptions: { title: 'My Orders' }
},
OrderDetail: {
screen: OrderDetail,
navigationOptions: ({ navigation }) => ({
title: 'Order #' + navigation.state.params.orderId
})
},
}, {
initialRouteName: 'Login',
headerMode: 'screen',
});
export default () => (
<Provider store={store}>
<AppNavigator />
</Provider>
);
Redux Store
// src/store/index.js
import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunk from 'redux-thunk'; // Middleware for async actions
import auth from './reducers/auth';
import orders from './reducers/orders';
const rootReducer = combineReducers({ auth, orders });
const store = createStore(rootReducer, applyMiddleware(thunk));
export default store;
// src/store/reducers/orders.js
const initialState = {
list: [],
loading: false,
error: null,
};
export default function ordersReducer(state = initialState, action) {
switch (action.type) {
case 'ORDERS_FETCH_START':
return { ...state, loading: true, error: null };
case 'ORDERS_FETCH_SUCCESS':
return { ...state, loading: false, list: action.payload };
case 'ORDERS_FETCH_ERROR':
return { ...state, loading: false, error: action.payload };
default:
return state;
}
}
// Action creator (thunk)
export function fetchOrders() {
return async (dispatch, getState) => {
dispatch({ type: 'ORDERS_FETCH_START' });
try {
const { auth: { token } } = getState();
const response = await api.get('/orders', { headers: { Authorization: 'Bearer ' + token } });
dispatch({ type: 'ORDERS_FETCH_SUCCESS', payload: response.data.orders });
} catch (error) {
dispatch({ type: 'ORDERS_FETCH_ERROR', payload: error.message });
}
};
}
API Service
// src/services/api.js
import axios from 'axios';
import store from '../store';
const api = axios.create({
baseURL: 'https://api.myapp.com/v1',
timeout: 10000,
});
// Interceptor: add auth token to every request
api.interceptors.request.use(config => {
const { auth: { token } } = store.getState();
if (token) {
config.headers.Authorization = 'Bearer ' + token;
}
return config;
});
// Interceptor: handle 401 globally
api.interceptors.response.use(
response => response,
error => {
if (error.response && error.response.status === 401) {
store.dispatch({ type: 'AUTH_LOGOUT' });
// Navigation to login is handled by the App root component
}
return Promise.reject(error);
}
);
export default api;
HomeScreen with List
// src/screens/HomeScreen.js
import React, { Component } from 'react';
import { View, Text, FlatList, StyleSheet, TouchableOpacity, RefreshControl } from 'react-native';
import { connect } from 'react-redux';
import { fetchOrders } from '../store/reducers/orders';
// Note: FlatList replaced ListView in RN 0.28 - much better performance
class HomeScreen extends Component {
componentDidMount() {
this.props.fetchOrders();
}
renderOrder = ({ item }) => (
<TouchableOpacity
style={styles.orderCard}
onPress={() => this.props.navigation.navigate('OrderDetail', { orderId: item.id })}
>
<Text style={styles.orderId}>Order #{item.id}</Text>
<Text style={styles.status}>{item.status}</Text>
<Text style={styles.total}>${item.total}</Text>
</TouchableOpacity>
);
render() {
const { list, loading } = this.props;
return (
<View style={styles.container}>
<FlatList
data={list}
keyExtractor={item => String(item.id)}
renderItem={this.renderOrder}
refreshControl={
<RefreshControl
refreshing={loading}
onRefresh={this.props.fetchOrders}
/>
}
ListEmptyComponent={
!loading && <Text style={styles.empty}>No orders yet</Text>
}
/>
</View>
);
}
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#f5f5f5' },
orderCard: { backgroundColor: '#fff', padding: 16, marginBottom: 1 },
orderId: { fontSize: 16, fontWeight: 'bold' },
status: { fontSize: 14, color: '#666', marginTop: 4 },
total: { fontSize: 14, color: '#007AFF', marginTop: 4 },
empty: { textAlign: 'center', marginTop: 48, color: '#999' },
});
const mapStateToProps = state => ({ list: state.orders.list, loading: state.orders.loading });
const mapDispatchToProps = { fetchOrders };
export default connect(mapStateToProps, mapDispatchToProps)(HomeScreen);
Platform-Specific Code
// src/utils/platform.js
import { Platform, StyleSheet } from 'react-native';
// Shadow styles differ between iOS and Android
export const shadow = Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
android: {
elevation: 3, // Android uses elevation, not shadow
},
});
// Platform-specific file: Button.ios.js / Button.android.js
// React Native will automatically pick the right one:
// import Button from './Button'
// → loads Button.ios.js on iOS, Button.android.js on Android
Push Notifications in 2016
// iOS: PushNotificationIOS (built into RN)
// Android: in 2016, react-native-gcm-android or react-native-push-notification
import PushNotificationIOS from '@react-native-community/push-notification-ios';
import { Platform } from 'react-native';
if (Platform.OS === 'ios') {
PushNotificationIOS.requestPermissions(['alert', 'sound', 'badge']);
PushNotificationIOS.addEventListener('notification', notification => {
console.log('Received:', notification);
});
}
// Android: GCM → FCM transition was happening in 2016
// FCM SDK released May 2016
iOS and Android Build
# iOS: open Xcode
open ios/MyApp.xcodeproj
# Android: generate keystore for release
keytool -genkey -v -keystore my-release-key.keystore \
-alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000
# Android: assemble release
cd android && ./gradlew assembleRelease
# APK: android/app/build/outputs/apk/app-release.apk
Code Reuse in Practice (2016 Reality)
On our delivery app (iOS + Android, 2016):
| Category | Shared | iOS-only | Android-only |
|---|---|---|---|
| Business logic | 100% | 0% | 0% |
| API calls | 100% | 0% | 0% |
| State management | 100% | 0% | 0% |
| UI components | 82% | 10% | 8% |
| Push notifications | 0% | 50% | 50% |
Overall: ~85% code reuse. The 15% difference was mostly push notifications, platform-specific navigation styling, and a few camera/permissions APIs.
Build time: 8 weeks instead of the 13 we'd estimated for separate native apps.