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

Pushing React Native Outside the Comfort Zone

Pushing React Native Outside the Comfort Zone

Raúl Gómez Acuña

September 14, 2018
Tweet

More Decks by Raúl Gómez Acuña

Other Decks in Technology

Transcript

  1. View Slide

  2. Thanks to our sponsors!

    View Slide

  3. ME
    Seriously
    Another talk?

    View Slide

  4. (Future) Frontend Software engineer at
    @rgommezz

    View Slide

  5. getDerivedStateFromProps()
    obtenerElEstadoDerivadoDeLasPropiedades()
    getSnapshotBeforeUpdate()
    obtenerLaInstantaneaAntesDeLaActualizacion()

    View Slide

  6. PUSHING REACT NATIVE
    OUTSIDE THE COMFORT
    ZONE

    View Slide

  7. PUSHING REACT SPANISH
    DEVELOPERS INSIDE THE
    COMFORT ZONE

    View Slide

  8. BY CREATING A BABEL PLUGIN
    THAT TRANSLATES REACT API
    FROM SPANISH TO ENGLISH

    View Slide

  9. Origin

    View Slide

  10. View Slide

  11. View Slide

  12. • Score rendering and motion
    • Support for mixing different audio sources
    • Seeking by interacting with the screen
    • Seeking by interacting with the progress bar (Spotify)
    • Timer component
    • Replay button

    View Slide

  13. The backbone

    View Slide

  14. View Slide

  15. Approaches

    View Slide

  16. NetInfo
    BackHandler
    AppState
    COMPONENTS APIs
    Native UI
    Component
    Native Module

    View Slide

  17. Native Module

    View Slide

  18. class Playback extends React.Component {
    componentDidMount() {
    MediaPlayer.init({ src: this.props.src }).then(() => {
    if (this.props.isPlaying) {
    MediaPlayer.play();
    }
    });
    }
    import * as React from 'react';
    import { NativeModules } from 'react-native';
    const MediaPlayer = NativeModules.MediaPlayer;
    type Props = {
    isPlaying: boolean,
    src: string,
    };

    View Slide

  19. class Playback extends React.Component {
    componentDidMount() {
    MediaPlayer.init({ src: this.props.src })
    .then(() => {
    // Song loaded into memory
    if (this.props.isPlaying) MediaPlayer.play();
    });
    }
    componentDidUpdate(prevProps: Props) {
    if (prevProps.isPlaying && !this.props.isPlaying) {
    MediaPlayer.pause();
    } else if (!prevProps.isPlaying && this.props.isPlaying) {
    MediaPlayer.play();
    }
    }
    render() {
    return null;
    }
    }
    };

    View Slide

  20. Sending progress to JS

    View Slide

  21. import {
    View,
    Button,
    NativeModules,
    NativeEventEmitter,
    Platform,
    DeviceEventEmitter,
    Score,
    } from 'react-native';
    import Playback from ‘./PlayerNative';
    const { MediaPlayer } = NativeModules;
    const mediaPlayerTimeProgressEmitter = Platform.select({
    ios: new NativeEventEmitter(MediaPlayer),
    android: DeviceEventEmitter,
    });
    type State = {
    isPlaying: boolean,
    progress: number,

    View Slide

  22. type State = {
    isPlaying: boolean,
    progress: number,
    };
    class Player extends React.Component {
    subscription: *;
    state: State = {
    isPlaying: false,
    progress: 0, // progress of the song in ms
    };
    componentDidMount() {
    this.subscription = mediaPlayerTimeProgressEmitter.addListener(
    'TimeProgress',
    progress =>
    this.setState({
    progress,
    const mediaPlayerTimeProgressEmitter = Platform.select({
    ios: new NativeEventEmitter(MediaPlayer),
    android: DeviceEventEmitter,
    });

    View Slide

  23. isPlaying: false,
    progress: 0,
    };
    componentDidMount() {
    // We are re-rendering 60 times per second with the new progress
    this.subscription = mediaPlayerTimeProgressEmitter.addListener(
    'TimeProgress',
    (progress: number) =>
    this.setState({
    progress,
    }),
    );
    }
    componentWillUnmount() {
    this.subscription.remove();
    }
    render() {
    const { isPlaying } = this.state;
    // Calculating the left margin based on the score width,
    // and total duration of the song

    View Slide

  24. Score renderer

    View Slide

  25. Can we render an SVG with an
    component?

    View Slide

  26. Can we render an SVG with an
    component?

    View Slide

  27. View Slide

  28. View Slide

  29. Can we render an SVG with
    component?

    View Slide

  30. source={{
    uri: svgUri,
    }}
    />

    View Slide

  31. source={{
    uri: `https://svgwrapper.net/?url=${src}`,
    }}
    />

    View Slide

  32. render() {
    const { isPlaying } = this.state;
    // Calculates the margin left based on the time progress,
    // width of the score and duration of the song
    const scoreLeftMargin = this.calculateLeftMargin();
    return (



    title={isPlaying ? ‘PAUSE' : 'PLAY'}
    onPress={() =>
    this.setState({
    isPlaying: !isPlaying,
    })}
    />

    );
    }
    }
    componentWillUnmount() {
    this.subscription.remove();
    }

    View Slide

  33. View Slide

  34. View Slide

  35. Drawbacks
    • No way to follow the tempo
    • 60 bridge passes per second: frames skipped
    • Styles not set optimally
    • Animations run on JS thread
    • Not easy to scale

    View Slide

  36. Native UI Component

    View Slide

  37. ScrollView

    View Slide

  38. onScroll={Animated.event([
    {
    nativeEvent:
    {
    contentOffset: { y: this.scrollY }
    }
    },
    ], { useNativeDriver: true })}
    scrollEventThrottle={16}
    >
    const bigTitleOpacity =
    this.scrollY.interpolate({
    inputRange: [0, 50],
    outputRange: [1, 0],
    extrapolate: 'clamp',
    });

    View Slide

  39. The mental model

    View Slide


  40. View Slide

  41. ref={(ref: any) => {
    this.playback = ref;
    }}
    playing={this.state.isPlaying}
    tracks={this.state.tracksComposition}
    onProgress={Animated.event(
    [
    {
    nativeEvent: {
    progress: this.progress,
    },
    },
    ])}
    />

    View Slide

  42. return (

    style={[
    styles.container,
    { transform: [{ translateX: marginLeft }] },
    ]}
    >


    style={[
    styles.tempoBar,
    { transform: [{ translateX: barOffset }] },
    ]}
    />

    );

    View Slide

  43. Benefits
    Can we go even further?
    (x1, t1) (x2, t2)
    • Easy to follow the tempo
    • Optimally setting styles with Animated Views

    View Slide

  44. Using native driver

    View Slide

  45. ref={(ref: any) => {
    this.playback = ref;
    }}
    playing={this.state.isPlaying}
    tracks={this.state.tracksComposition}
    onProgress={Animated.event(
    [
    {
    nativeEvent: {
    progress: this.state.progress,
    },
    },
    ],
    {
    useNativeDriver: true,
    },
    )}
    />

    View Slide

  46. Player demo

    View Slide

  47. Diving deeper into
    interpolations
    The power of interpolation

    View Slide

  48. Seeking

    View Slide

  49. View Slide

  50. Pan Responder?
    React-native-gesture-handler?

    View Slide

  51. Playback
    Progress
    Animated.Value
    Score Position Red bar position
    Animated.event
    Animated.interpolate Animated.interpolate

    View Slide

  52. Playback
    Progress
    Animated.Value
    Pan Responder
    Score Position Red bar position
    Animated.interpolate Animated.interpolate
    onPanResponderRelease
    this.playback.seekTo
    onPanResponderMove
    this.progress.setValue

    View Slide

  53. Pan Responder lifecycle

    View Slide

  54. Negotiation
    onStartShouldSetPanResponder: () => !this.props.isPlaying,
    onStartShouldSetPanResponderCapture: () => !this.props.isPlaying,
    onMoveShouldSetResponderCapture: () => !this.props.isPlaying,
    onMoveShouldSetPanResponderCapture: () => !this.props.isPlaying,

    View Slide

  55. Touch move
    onPanResponderMove: (evt, gestureState) => {
    // Setting the new animated value based on the accumulated
    // distance of the gesture since the touch started, dx.
    // Duration and score width are constants
    const nextValue = -(gestureState.dx /
    (this.state.width / this.props.duration));
    this.props.progress.setValue(nextValue + this.progressAccValue);
    },

    View Slide

  56. Touch release
    onPanResponderRelease: (evt, gestureState) => {
    this.props.progress.stopAnimation(lastTimeStamp => {
    this.playback.seekTo(lastTimeStamp);
    });
    },

    View Slide

  57. Seeking demo

    View Slide

  58. Time Progress Component

    View Slide





  59. View Slide

  60. View Slide

  61. Android first

    View Slide





  62. View Slide

  63. Performance demands

    View Slide

  64. style={styles.absoluteBottom}
    progress={this.state.progress}
    duration={song.duration}
    onSlidingStart={this.handleSlidingStart}
    onSlidingComplete={this.handleSlidingComplete}
    />

    View Slide

  65. Animated ad-hoc listeners

    View Slide

  66. /**
    * Controlling the progress bar from the player.
    * We'll update the local state 1 time per second
    * when the value is controlled by the playback
    */
    componentDidMount() {
    let prevValue = 0;
    this._listenerId = this.props.progress.addListener((e: *) => {
    // Math.round() will always round down to the lesser integer
    const nextTimeInSeconds = Math.floor(e.value / 1000);
    if (!this.state.isSliding && prevValue !== nextTimeInSeconds) {
    this.setState({ value: nextTimeInSeconds });
    }
    prevValue = nextTimeInSeconds;
    });
    }

    View Slide

  67. render() {
    const { duration, style } = this.props;
    const { value } = this.state;
    const maxValue = Math.round(duration / 1000);
    return (

    minimumValue={0}
    maximumValue={maxValue}
    step={1}
    value={value}
    />
    duration={duration}
    value={value}
    />

    );
    }

    View Slide

  68. Time progress demo

    View Slide

  69. Spotify example

    View Slide

  70. View Slide

  71. Seeking by sliding

    View Slide


  72. minimumValue={0}
    maximumValue={maxValue}
    step={1}
    value={value}
    onSlidingStart={this.handleSlidingStart}
    onValueChange={this.handleValueChange}
    onSlidingComplete={this.handleSlidingComplete}
    />

    View Slide

  73. Slide start
    handleSlidingStart = (value: number) => {
    this.setState(
    {
    value,
    isSliding: true,
    },
    );
    };

    View Slide

  74. Slide move
    handleValueChange = (value: number) => {
    this.setState({
    value,
    });
    };

    View Slide

  75. Slide release
    handleSlidingComplete = (tStamp: number) => {
    // Convert to ms
    this.playback.seekTo(tStamp * 1000);
    this.setState({
    value: tStamp,
    isSliding: false,
    });
    };

    View Slide

  76. View Slide

  77. View Slide

  78. Slide release
    handleSlidingComplete = (tStamp: number) => {
    // Convert to ms
    this.playback.seekTo(tStamp * 1000);
    this.setState({
    value: tStamp,
    isSliding: false,
    });
    };

    View Slide

  79. JS Thread Native Thread
    this.playback.seekTo()
    seekTo() nSources times
    postFrameCallback()
    onSeekComplete()
    UIManagerModule
    .dispatchEvent(‘onProgress’, ts);

    this.props.progress.addListener(…)

    View Slide

  80. Slide release
    handleSlidingComplete = (tStamp: number) => {
    this.playback.seekTo(tStamp * 1000);
    this.setState({
    value: tStamp,
    });
    this._timeout = setTimeout(
    () => {
    this.setState({
    isSliding: false,
    });
    },
    1500,
    );
    };

    View Slide

  81. Seeking by tapping

    View Slide


  82. onPressIn={this.tapSliderHandler}
    >
    minimumValue={0}
    maximumValue={maxValue}
    step={1}
    value={value}
    onValueChange={this.handleValueChange}
    onSlidingStart={this.handleSlidingStart}
    onSlidingComplete={this.handleSlidingComplete}
    />


    width
    xTap
    =
    duration
    ?

    View Slide

  83. Benefits
    • Updates isolated on leaf component
    • Updates via setState are happening 1 time per second.
    • Avoiding unnecessary re-renders on other parts of the
    screens

    View Slide

  84. Progress bar seeking demo

    View Slide

  85. Replay

    View Slide

  86. this.playback.seekTo(0)

    View Slide

  87. Takeaways

    View Slide

  88. Takeaways
    • Is React Native ready for the challenge? YES!
    • Native Modules and Native UI Components are the game
    changers
    • Keep the ScrollView in mind as a reference model:
    Practical hacks for delightful interactions
    • Think. Research. Plan. Write. Validate. Fail. Modify

    View Slide

  89. Gracias! :)
    @rgommezz

    View Slide