Upgrade to Pro — share decks privately, control downloads, hide ads and more …

React-Native in a Content-Driven World

React-Native in a Content-Driven World

We want to be more agile, we want to continuously deliver a better native experience - and React-Native enables us to do so! I will talk about our on-going process of implementing a react-native powered, content-driven feature in our existing iOS / Android Apps at Runtastic. I will cover some topics like our Redux workflow, native navigation, app-updates without Apphub and more!

Patrick Stapfer

December 16, 2015
Tweet

More Decks by Patrick Stapfer

Other Decks in Programming

Transcript

  1. content kənˈtɛnt/ Goals Achievements User Interactions => Available => Addressable

    => Browsable => Sharable => Extendable Goals Achievements User Interactions Activities Body-Measurements Leaderboards Achievements User Interactions Goals ... User generated content (content) => UX
  2. + Same web-codebase + Easy to integrate + Fast iteration

    - No native experience - No specific UI for each platform ~ (Currently only available for iOS) U can’t touch this (properly) We want to improve this! Embedded webframe
  3. Our Technical Goals... Support multiple platforms Iterate fast without breaking

    stuff Content Share knowledge & code-style Runtastic Apps Content
  4. Apps runtastic.com Content Module Macro Services Backend JSON API RTCore.js

    Data Use-Cases LIB Architecture ... Components Native Code RN-Stuff get newest bundle
  5. import React from 'react-native'; import { Provider } from 'react-redux/native';

    import { fromJS } from 'immutable'; import configureStore from '../../store/configureStore'; import * as userActionCreators from '../../actions/userActionCreators'; import NewsFeedNavigator from '../NewsFeedNavigator'; const store = configureStore(fromJS({})); class NewsFeedApp extends React.Component { componentWillMount() { store.dispatch(userActionCreators.setCurrentUser(0, 'first', 'last', null)); } render() { return (<Provider store={store}> {() => <NewsFeedNavigator {...this.props} />}</Provider>); } } export default NewsFeedApp; No difference to common react.js … but wait! It is not rendering DOM! React can render to different platforms AppRegistry is taking care of that. Neat. But how do I run it? It is using react-native components
  6. react-native packager.sh cli.js native components + js-bindings bridge / magic

    content-module App-Code init view and bridge bundle (android) bundle (ios) first deploy / install app => {app, react-native, bundle.js} update bundle.js (production) Newsfeed,... Native-Code bundle (dev) hot-changes (dev) JS-Libs (RTCore) GET /index.bundle?platform=ios
  7. Native (Java/C++) [1] Under the hood of react-native (mkonicek) JavaScriptCore

    (JS-VM) RTMapViewManager RTMapView Bridge-Communication [[ ‘RTMapView’], {...}]
  8. //src/native_components/RTMapView.android.js import { requireNativeComponent, PropTypes } from 'react-native'; const mapViewComponent

    = { name: 'RTMapView', propTypes: { encodedTrace: PropTypes.string, }, }; const RTMapView = requireNativeComponent('RTMapView', mapViewComponent); export default RTMapView; Defines the React-Component in JS-Land Maps the bridge-details to the component
  9. public class RuntasticMapViewManager extends SimpleViewManager<MapView> implements OnMapReadyCallback { public static

    final String REACT_CLASS = "RTMapView"; private ReactContext reactContext; private MapView mapView; private GoogleMap googleMap; private String encodedTrace; @Override public String getName() { return REACT_CLASS; } @ReactProp(name = "encodedTrace") public void setEncodedTrace(MapView view, @Nullable String encodedTrace) { this.encodedTrace = encodedTrace; updateTrace(); } //Do the gmaps render logic } Needs to match the name Needs to match the propType!
  10. RuntasticReactPackage extends ReactPackage @Override public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) { List<ViewManager>

    modules = new ArrayList<>(); modules.add(new RuntasticToolbarManager()); modules.add(new RuntasticMapViewManager()); return modules; } RuntasticReactManager extends ActivityProvider ReactInstanceManager.Builder builder = ReactInstanceManager.builder(); builder.setApplication(application) .setBundleAssetName("") .setJSMainModuleName("index.android") .addPackage(new MainReactPackage()) .addPackage(new RuntasticReactPackage(this)) .setUseDeveloperSupport(devModeEnabled) .setInitialLifecycleState(createInBackground ? LifecycleState.BEFORE_RESUME : LifecycleState.RESUMED);
  11. React-Native + Redux = ❤ Action-Creator Store Reducer RTCore.js state

    dispatch(action) async-code Components new state (state, action) action interaction thunk(dispatch, getState) emit changes action
  12. (Async) action-creators import newsFeedRepo from '../RTCoreRepository'; export function fetchSocialFeed() {

    return (dispatch, getState) => { dispatch({ type: actions.FETCH_SOCIAL_FEED }); // TODO use getSocialFeed const promises = newsFeedRepo.getProfileFeed('1'); promises.forEach(p => p.then( feed => dispatch(fetchSocialFeedSuccess(feed.posts)), error => dispatch(fetchSocialFeedFailure(error)) )); }; } function fetchSocialFeedSuccess(posts) { return { type: actions.FETCH_SOCIAL_FEED_SUCCESS, posts, lastUpdate: Date.now() }; } function fetchSocialFeedFailure(error) { return { type: actions.FETCH_SOCIAL_FEED_FAILURE, error }; }
  13. Redux connect //./src/containers/NewsFeedSocialScreen.js class NewsFeedSocialScreen extends React.Component {} function mapStateToProps(state)

    { return { isFetching: state.getIn(['newsFeed', 'socialFeed', 'isFetching']), error: state.getIn([‘newsFeed’, 'socialFeed', 'error']), lastUpdate: state.getIn(['newsFeed', 'socialFeed', 'lastUpdate']), posts: state.getIn(['newsFeed', 'socialFeed', 'posts']).toJS() }; } export default connect(mapStateToProps)(NewsFeedSocialScreen);
  14. Platform specific render-methods _renderList() { return ( <View style={styles.root}> {this._renderToolbarAndroid()}

    <NewsFeedList posts={this.props.posts} onPostPressed={this._onPostPressed} onProfilePressed={this._onProfilePressed} onLikePostPressed={this._onLikePostPressed} /> </View> ); } const IS_ANDROID = Platform.OS === 'android'; _renderToolbarAndroid() { if (IS_ANDROID && this.props.needsToolbar) { return ( <RTToolbarAndroid title='Post Detail' onNavIconPressed={} /> ); } } We actually prefer separate files, whenever possible
  15. Bundle Update Process On App start BundleHelper Content-Module-Version: 1.0 Backend

    New Bundle? (Re)load Bridge (Re)load App-View Locale Storage GET /bundle?filter[latest.version]=~1. 0 Yes Stored bundle available? Show Fallback Error View http-failure... CodePush bundle-file
  16. 3 - 7 days {Iterate, ship} fast without breaking stuff

    14 days (Feature-) Team Sprint Release new bundle.js App-Store Review 0 days