Code my UI – React Native

Code my UI with React native

Oy oy !

Petit challenge de code en ce début d’année avec ma technologie préférée du moment : React Native.

En ce début d’année je suis tombé sur ce super prototype d’écran et j’ai eu envie d’en faire une version avec React Native.

Dans cet article je vais essayer de me rapprocher le plus possible de cet écran et de vous montrer la facilité du développement. Let’s go !

 

Let’s Code !

Je commence donc par initialiser le projet :

react-native init codemyui

Ensuite dans le package.json j’ajoute les dépendances dont j’ai besoin :

"dependencies": {
		"react": "15.4.2",
		"react-native": "^0.40.0",
		"react-native-linear-gradient": "^1.5.15",
		"react-native-vector-icons": "^2.0.2"
	},

react-native-linear-gradient va me fournir le moyen rapide de faire mes dégradés tandis que react-native-vector-icons me fourni des font icons.

Une fois mon répertoire de travail créé je commence avec la structure de ma page :

import React, {Component} from 'react';
import {View, Text, StyleSheet} from 'react-native';

// Styles
const styles = StyleSheet.create({

});

export default class BookAFlight extends Component {
    render() {
        return (
            <View >
                <Header>
                    {/* Will contains flight search information */}
                </Header>
                <FlightList>
                    {/* Will contains flight results */}
                </FlightList>
                <ProceedAction>
                    {/* Will contains proceed button */}
                </ProceedAction>
            </View>
        )
    }
}

Je passe ensuite aux composants.

Composants

<Header>

Le mockup nous présente plusieurs informations à afficher :

  • Ville et aéroport de départ
  • Ville et aéroport d’arrivée

J’utilise flexbox pour aligner mes éléments. Le dégradé et l’image sont placés de façon « absolute » dans la page.

Le code (avec les valeurs en dur) :

import React, {Component} from 'react';
import {View, Text, StyleSheet, Dimensions, Image} from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
var FontAwesome = require('react-native-vector-icons/FontAwesome');

// App
import Row from './Row';

// Styles
const {width} = Dimensions.get('window');
const headerHeight = 150;
const styles = StyleSheet.create({
    container: {
      height: headerHeight,
    },
    info: {
        paddingHorizontal: 40
    },
    gradient: {
        position: 'absolute',
        top: -18,
        width,
        height: headerHeight + 18,
        paddingTop: 40
    },
    text: {
        backgroundColor: 'transparent',
        color: 'white'
    },
    labelFromTo: {
        fontSize: 15,
        fontWeight: '100'
    },
    textFromTo: {
        fontSize: 24,
        fontWeight: 'bold'
    },
    image: {
        position: 'absolute',
        top: -18,
        height: headerHeight + 18
    }
});

export default class Header extends Component {
    render() {
        return (
            <View style={styles.container}>
                <Image source={{uri: 'seattle', isStatic: true, width, height: headerHeight}} style={styles.image}/>
                <LinearGradient colors={['#2196F3CC', '#EC6EADCC']}
                                style={styles.gradient}
                                start={{x: 0.0, y: 0.25}}
                                end={{x: 1, y: .45}}
                >
                    <View style={styles.info}>
                        <Row justifyContent="space-between">
                            <Text style={[styles.text]}>FLYING FROM</Text>
                            <Text style={[styles.text]}>FLYING TO</Text>
                        </Row>
                        <Row justifyContent="space-between">
                            <Text style={[styles.text, styles.textFromTo]}>CHICAGO</Text>
                            <Text style={[styles.text, {alignItems: 'flex-end'}]}>
                                <FontAwesome name="fighter-jet" size={20}/>
                            </Text>
                            <Text style={[styles.text, styles.textFromTo]}>SEATTLE</Text>
                        </Row>
                        <Row justifyContent="space-between">
                            <Text style={[styles.text]}>All Airports</Text>
                            <Text style={[styles.text]}>Tacoma Intl.</Text>
                        </Row>
                    </View>
                </LinearGradient>
            </View>
        )
    }
}

Et le résultat :

Simulator Screen Shot 24 Jan 2017 20.03.26

Je garde pour plus tard la gestion des polices et je passe au composant ProceedAction.

 

<ProceedAction>

Dans ce composant je vais avoir besoin de 2 données :

  • Le prix du vol sélectionné
  • L’id du vol

Pour l’UI, on retrouve du dégradé, du positionnement et de l’accès aux propriétés.

import React, {Component} from 'react';
import {View, Text, StyleSheet, Dimensions} from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
var MaterialIcons = require('react-native-vector-icons/MaterialIcons');

// Styles
const {width, height} = Dimensions.get('window');
const styles = StyleSheet.create({
    container: {
        width,
        alignItems: 'center',
        position: 'absolute',
        top: height - 60
    },
    gradient: {
        width: width - 60,
        height: 45,
        justifyContent: 'center',
        alignItems: 'center',
        borderRadius: 40,
        flexDirection: 'row',
        shadowOffset: {
            width: 3, height: 3
        },
        shadowColor: '#333', shadowOpacity: 0.35, shadowRadius: 3, elevation: 5
    },
    text: {
        backgroundColor: 'transparent',
        color: 'white',
        fontWeight: 'bold',
    },
    icon: {
        alignSelf: 'center',
        backgroundColor: 'transparent'
    }
});

export default class ProceedAction extends Component {
    render() {
        return (
            <View style={styles.container}>
                <LinearGradient colors={['#2196F3CC', '#5C258DCC']}
                                style={styles.gradient}
                                start={{x: 0.0, y: 0.25}}
                                end={{x: 1, y: .45}}
                >
                    <MaterialIcons name="payment" color="white" size={20} style={styles.icon}/>
                    <Text style={styles.text}>
                         PROCEED {this.props.price||0}$
                    </Text>
                </LinearGradient>
            </View>
        )
    }
}

Et le résultat temporaire :

Simulator Screen Shot 24 Jan 2017 20.49.31

Je m’attaque maintenant à la partie plus compliquée : la liste des vols.

<FlightList>

Avant de créer ce composant, il va me falloir un Service qui me retourne les données.

export default class FlightService {
    static all() {
        return Promise.resolve([
            {
                id: 0,
                fromTime: '12:00 am',
                toTime: '02:36 pm',
                fromAirport: 'Midway',
                toAirport: 'Tacoma Intl.',
                price: 341,
                miscs: ['4h34m Nonstop', 'WiFi, Video', 'Boieng 737-900'],
                logo: 'airline-logo'
            },
           /* .... */
        ]);
    }
}

 

Maintenant que le service est créé je vais l’utiliser dans mon composant :

import React, {Component} from 'react';
import {View, Text, StyleSheet} from 'react-native';

// App
import FlightService from "../services/FlightService";

// Styles
const styles = StyleSheet.create({})

export default class FlightList extends Component {
    constructor() {
        super();
        this.state = {
            flights: []
        }
    }
    componentDidMount() {
        FlightService.all().then(flights => this.setState({flights}))
    }
    render() {
        return (
            <View>

            </View>
        )
    }
}

Je passe maintenant à l’UI en commençant par définir la position de mon composant. Puis je rajoute l’ombre et le borderRadius.

import React, {Component} from 'react';
import {View, Text, StyleSheet, Dimensions} from 'react-native';

// App
import FlightService from "../services/FlightService";

// Styles
const {width, height} = Dimensions.get('window');
const styles = StyleSheet.create({
    container: {
        backgroundColor: 'white',
        marginHorizontal: 10,
        borderRadius: 7,
        height, width: width - 20,
        position:'absolute',
        top: 175,
        shadowOffset: {
            width: 1, height: 1
        },
        shadowColor: '#333', shadowOpacity: 0.35, shadowRadius: 7, elevation: 5
    }
})

export default class FlightList extends Component {
    constructor() {
        super();
        this.state = {
            flights: []
        }
    }
    componentDidMount() {
        FlightService.all().then(flights => this.setState({flights}))
    }
    render() {
        return (
            <View style={styles.container}>

            </View>
        )
    }
}

Et pour l’instant mon application ressemble à ça :

Simulator Screen Shot 24 Jan 2017 21.12.40

Je passe donc à l’implémentation de mon composant, version « gros grain ». Je vais créer la partie « Tabs » puis les cartes de résultats.

import React, {Component} from 'react';
import {View, Text, StyleSheet, Dimensions} from 'react-native';

// App
import FlightService from "../services/FlightService";
import ResultListTabs from './ResultListTabs';
import ResultList from './ResultList';
import Tab from './Tab';

// Styles
const {width, height} = Dimensions.get('window');
const styles = StyleSheet.create({
    container: {
        backgroundColor: 'white',
        marginHorizontal: 10,
        borderRadius: 7,
        height, width: width - 20,
        position:'absolute',
        top: 175,
        shadowOffset: {
            width: 1, height: 1
        },
        shadowColor: '#333', shadowOpacity: 0.35, shadowRadius: 7, elevation: 5
    }
})

export default class FlightList extends Component {
    constructor() {
        super();
        this.state = {
            flights: []
        }
    }
    componentDidMount() {
        FlightService.all().then(flights => this.setState({flights}))
    }
    render() {
        return (
            <View style={styles.container}>
                <ResultListTabs>
                    <Tab icon="flight" text="219$" action={this._foo.bind(this)}/>
                    <Tab icon="directions-railway" text="102$" action={this._foo.bind(this)}/>
                    <Tab icon="directions-bus" text="75$" action={this._foo.bind(this)}/>
                </ResultListTabs>
                <ResultList
                    flights={this.state.flights}
                    onSelectItem={flight => this._selectFlight(flight)}
                />
            </View>
        )
    }
    _foo() {
        // NOOP
    }
    _selectFlight(flight) {
        // NOOP
    }
}

<ResultListTabs> et <Tab>

ResultListTabs va se charger de gérer la logique des tabs. Je pourrais bien sur m’appuyer sur un package existant de la communauté, mais le but de cet article est aussi de vous montrer la facilité d’écriture des composants avec React Native.

Tabs quant à lui se chargera de la mise en page.

import React, {Component} from 'react';
import {View, Text, StyleSheet, TouchableOpacity} from 'react-native';

// App
import Row from './Row';

// Styles
const styles = StyleSheet.create({
    button: {
        height: 40,
        flex: 1
    }
});

export default class ResultListTabs extends Component {
    constructor() {
        super();
        this.state = {
            selectedTabIdx: 0
        }
    }
    render() {
        return (
            <Row justifyContent="space-between">
                {this.props.children.map(
                    (tab, idx) => {
                        let tabWithProps = React.cloneElement(
                            tab, {selected: this.state.selectedTabIdx === idx}
                        );
                        return (
                            <TouchableOpacity
                                key={idx}
                                onPress={this._selectTab.bind(this, tab, idx)}
                                style={styles.button}
                            >
                                {tabWithProps}
                            </TouchableOpacity>
                        )
                    }
                )}
            </Row>
        )
    }
    _selectTab(tab, idx) {
        this.setState({selectedTabIdx : idx});
        tab.props.action();
    }
}
ResultListTabs

Note : Ce composant se charge de garder l’onglet sélectionné. Il a donc besoin de garder dans son état une variable « selectedTabIdx » et de passer la propriété selected au composant <Tab>. Cependant l’attribut « props » d’un composant est immutable, et j’ai donc besoin de cloner le composant avec React.cloneElement.

import React, {Component} from 'react';
import {View, Text, StyleSheet} from 'react-native';
var MaterialIcons = require('react-native-vector-icons/MaterialIcons');

// Styles
const styles = StyleSheet.create({
    container: {
        flexDirection: 'row',
        backgroundColor: 'transparent',
        justifyContent: 'center',
        alignItems: 'center',
        height: 40,
        borderBottomWidth: 1,
        borderBottomColor: '#CCC'
    },
    icon: {
        color: '#CCC'
    },
    text: {
        color: '#CCC',
        fontWeight: 'bold',
        fontSize: 18
    },
    selectedContainer: {
        borderBottomWidth: 2,
        borderBottomColor: '#6A1B9A'
    },
    selectedText: {
        color: '#6A1B9A'
    }
});

export default class Tab extends Component {
    render() {
        var selectedStyle = this.props.selected ? styles.selectedContainer : {};
        var selectedTextStyle = this.props.selected ? styles.selectedText : {};
        return (
            <View style={[styles.container, selectedStyle]}>
                <MaterialIcons name={this.props.icon} size={20} style={[styles.icon, selectedTextStyle]}/>
                <Text style={[styles.text, selectedTextStyle]}>{this.props.text}</Text>
            </View>
        )
    }
}
Tab

Et le rendu à cette étape :

Simulator Screen Shot 25 Jan 2017 20.15.01

Je me rapproche de mon UI cible et je termine par la liste des résultats.

<ResultList>

La liste des résultats va itérer sur la liste des vols fournie par le service. Pour chaque vol nous allons afficher une ligne dans une <ScrollView>. A chaque action sur une ligne nous allons forcer le layout à générer une animation grâce à :

LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);

Enfin, je vais cabler la logique en faisant descendre l’event ‘onSelectFlight’ dans mon arbre de composant.

Note : J’aurais pu (et surement du) utiliser le composant ListView, mais pour les besoins de cet article j’ai préféré itérer directement sur mon array.

Pour l’UI cela nous donne

render() {
        return (
            <ScrollView style={{paddingHorizontal: 11}}>
                {
                    this.props.flights.map(
                        (flight, idx) => {
                            let selected = this.state.selectedFlightIdx === idx;
                            let style = selected ? styles.selected : styles.normal;
                            let textStyle = selected ? styles.textSelected : styles.text;
                            let textBold =  selected ? styles.textSelectedBold : styles.textBold;

                            return (
                                <TouchableOpacity key={idx} onPress={this._select.bind(this, idx)} style={style}>
                                    {selected ? <LinearGradient colors={['#B993D666', '#8CA6DB66']}
                                                    style={styles.gradient}
                                                    start={{x: 0.0, y: 0.25}}
                                                    end={{x: 1, y: .45}}
                                    ></LinearGradient> : null}
                                    <View style={{flexDirection: 'row', paddingTop: 11, paddingLeft: 7}}>
                                        <View style={{flex: 1}}>
                                            <Text style={[textStyle, {fontSize: 24}]}>
                                                {selected ?
                                                    <FontAwesome name="check-circle"
                                                                 size={15}
                                                                 color={'#6A1B9A'}
                                                             /> :
                                                    '.'}
                                            </Text>
                                        </View>
                                        <View style={{flex: 10, paddingHorizontal: 11, flexDirection: 'row', justifyContent: 'space-between'}}>
                                            <View>
                                                <Text style={[textStyle, {fontSize: 15}, textBold]}>
                                                    {flight.fromTime}
                                                </Text>
                                                <Text style={textStyle}>
                                                    {flight.fromAirport}
                                                </Text>
                                            </View>
                                            <View style={{justifyContent: 'center'}}>
                                                <FontAwesome name="fighter-jet" size={15} color={selected ? '#6A1B9A' : '#CCC'}/>
                                            </View>
                                            <View>
                                                <Text style={[textStyle, {fontSize: 15}, textBold]}>
                                                    {flight.toTime}
                                                </Text>
                                                <Text style={textStyle}>
                                                    {flight.toAirport}
                                                </Text>
                                            </View>
                                        </View>
                                        <View style={{flex: 4, justifyContent: 'center', alignItems: 'flex-end', paddingRight: 7}}>
                                            <Text style={[textStyle, {color: '#AAA'}, textBold, {fontSize: 18}]}>
                                                {flight.price}$
                                            </Text>
                                        </View>
                                    </View>
                                    {selected ?
                                        <View style={{flexDirection: 'row', paddingHorizontal: 14, marginTop: 14}}>
                                            <View style={{flex: 1}}>
                                                {
                                                    flight.miscs.map((text, key) => <Text key={key} style={{color: '#6A1B9AAA'}}>{text}</Text>)
                                                }
                                            </View>
                                            <View style={{flex: 1}}>
                                                <Image source={{uri: 'airline', isStatic: true, width: 160, height: 55}} />
                                            </View>
                                        </View> :
                                        null
                                    }

                                </TouchableOpacity>
                            );
                        }
                    )
                }
            </ScrollView>
        )
    }

Avec le « cablage », on obtient le résultat suivant :

Jan-29-2017 13-21-57

 

et sur Android (émulateur Genymotion):

Screen Shot 2017-01-30 at 11.35.44

Améliorations

Voilà c’est tout pour cet article, cependant je ne suis pas 100% ISO avec le prototype du départ. Les améliorations possibles sont donc :

  • Utiliser la ListView pour détecter facilement le scroll dans la liste et agir sur le composants ProceedActions et la taille du header.
  • Créer à la main l’animation lors du clic sur un vol.

Un bon exercice si vous voulez creuser plus dans React Native.

Questions

Vous avez des questions ou des remarques ? N’hésitez pas à m’en faire part dans les commentaires 😉

Vous voulez en savoir plus sur React Native ? Je vous propose ma formation personnalisée pour React Native.

Le code est disponible sur Github.

 

A plus sur le blog de We Are One,

Florian

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *