ABONAMENTE VIDEO REDACȚIA
RO
EN
×
▼ LISTĂ EDIȚII ▼
Numărul 87
Abonament PDF

De la React la React Native, un exemplu practic

Vasile Boris
Team lead @ Garmin
PROGRAMARE

Experiența mea cu React Native nu a fost prietenoasă la început, am avut câteva provocări și bătăi de cap și doresc să împărtășesc cu voi modul cum le-am rezolvat.

Proiectul original de unde mi-a venit idea

Am început să lucrez cu JavaScript în 2017, așa că am căutat modalități de învățare. Am creat o aplicație care îmi monitoriza progresul la cititul de cărți unde am folosit React, Redux & Saga și Spring Boot. Inițial mă gândeam să o îmbunătățesc și să-i fac deploy undeva în cloud dar mi s-a părut o bătaie de cap prea mare pentru o singură persoană. Implementarea pe mobile părea mai simplă și alegerea firească a fost React Native.

Opțiunile și pregătirea

Am urmat secțiunea Getting Started din documentația oficială:

În naivitatea mea am crezut că voi putea reutiliza ușor ceva cod Java și am ales opțiunea a doua.

React Native CLI Quickstart

Am instalat Android Studio, watchman și am configurat variabilele de mediu ANDROID_HOME:

export ANDROID_HOME=$HOME/Android/Sdk
export PATH=$ANDROID_HOME/platform-tools:$PATH
export PATH=$ANDROID_HOME/emulator:$PATH
export PATH=$ANDROID_HOME/tools/bin:$PATH
export PATH=$ANDROID_HOME/tools:$PATH

Am generat proiectul și m-am pierdut în detalii. Modelul ales era mult prea complex pentru un începător: React Native, Android, Gradle, React Native + Android. Dacă aveam noroc și îmi mergea totul din start era ideal, dar dacă mă loveam de vreo problemă, nici nu aș fi știut de unde să încep. I-am crezut pe cuvânt că e mai potrivit pentru cei care știu mai multe despre React Native și Android.

Expo CLI Quickstart

Am ales șablonul "blank - minimal dependencies to run and an empty root component" și am găsit un mediu familiar din nou.

Părți comune

Recomand de la început să știi cum:

emulator -list-avdsemulator -avd Pixel_2_XL_API_26 ;

Atenție la:

#export PATH=$ANDROID_HOME/tools/bin:$PATH
#export PATH=$ANDROID_HOME/tools:$PATH

Soluția implementată cu React Native și Expo

Am dorit să păstrez cât pot de mult din proiectul original așa că pașii pe care i-am urmat au fost:

Configurare i18next

Am decis să folosesc fișiere de traducere json, formatul implicit folosit de i18next, să îmi fac viața mai ușoară:

{  
   "app-title": "My Reads",
}

Logica de încărcare a fișierelor de traducere a fost adăugată în Localizer.js:

const Localizer = {
    init: function(callback) {
        i18next.init({
            lng: „en”,
            fallbackLng: „en”,
            interpolation: {
                prefix: „{„,
                suffix: „}”
            },
            resources: {
                en: {
                    translation: messages
                }
            },
            debug: true
        }, () => callback());
    }
};

de care depinde punctul de intrare în aplicație:

export default class App extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            isI18nInitialized: false,
            books: []
        }
    }

    render() {
     const { isI18nInitialized, books } = this.state;

     if(!isI18nInitialized) {
          return null;
     }
     return (
       <View style={styles.container}>
       <Text>{localizer.localize(
        ‚books-search-text’)}</Text>
        books.map( book => (<Text>{book.title}
         </Text>))}
        </View>
       );
    }

    componentDidMount() {
        localizer.init(() => {
            fetchBooks().then((response) => {
               this.setState({
                   isI18nInitialized: true,
                   books: response.data
               })
            });
        });
    }
}

În acest prin pas, implementarea din metoda render conține câteva exemple ca să fiu sigur că logica de traducere funcționează.

Am folosit babel-root-slash-import ca să pot importa cu căi absolute pe care l-am înlocuit ulterior cu babel-plugin-module-resolver.

Migrare presentational components

Următorul pas a fost să migrez presentational components, primul a fost cel care afișează mesajele de eroare:

import React from ‚react’;
import { Text, StyleSheet } from ‚react-native’;
import appStyles from ‚/styles/AppStyles’;

const styles = StyleSheet.create({
    message: {
        color: ‚white’,
        backgroundColor: ‚red’
    },
});

const MessageComponent = props => {
    const { message } = props;
    return message && (
        <Text style={[appStyles.text, styles.message]}>{message}</Text>
    );
};

export default MessageComponent;

și să îl randez de câteva ori în App.js:

render() {
    const { isI18nInitialized, inputValue } = 
       this.state;

    return isI18nInitialized && (
        <View style={appStyles.vertical}>
           <MessageComponent key={1} 
              message=”This is message 1”/>
            <MessageComponent key={2} 
              message=”This is message 2”/>
            <MessageComponent key={3} 
              message=”This is message 3”/>
            <Text 
              style={[appStyles.text]}>
               {localizer.localize(‚
               books-search-text’)}</Text>
            <MessageComponent key={4} 
              message=”This is message 4”/>
            <MessageComponent key={5} 
              message=”This is message 5”/>
            <MessageComponent key={6} 
              message=”This is message 6”/>
        </View>
    );
}

După ce am făcut deploy la aplicație am observat că acoperă status barul telefonului.

Rezolvarea a fost să folosesc o margine în cazul în care platforma era Android:

import { StyleSheet, Platform } from ‚react-native’;
import { Constants } from ‚expo’;

const appStyles = StyleSheet.create({
  app: {
  marginTop: ‚android’ === Platform.OS ? 
    Constants.statusBarHeight : 0
    }
});

export default appStyles;
render() {
  const { isI18nInitialized, inputValue } = 
    this.state;

  return isI18nInitialized && (
  <View style={[appStyles.app, appStyles.vertical]}>
    <MessageComponent key={1} 
     message=”This is message 1”/>
    <MessageComponent key={2} 
      message=”This is message 2”/>
    <MessageComponent key={3} 
      message=”This is message 3”/>

    <Text style={[appStyles.text]}>{
     localizer.localize(‚books-search-text’)}</Text>
    <MessageComponent key={4} 
      message=”This is message 4”/>
    <MessageComponent key={5} 
      message=”This is message 5”/>
    <MessageComponent key={6} 
      message=”This is message 6”/>
     </View>
    );
}

Am continuat cu flexbox layout care mi-a dat următoarea durere de cap. Eu am înțeles că trebuie să folosesc flex: 1 pentru fiecare componentă care are nevoie să își aranjeze componentele copil cu flebox, așa că am definit următoarele stiluri:

import { StyleSheet, Platform } from ‚react-native’;
import { Constants } from ‚expo’;
import appColors from „./AppColors”;
import appSizes from „./AppSizes”;

const appStyles = StyleSheet.create({
    container: {
        flex: 1,
    },

    horizontal: {
        flexDirection: ‚row’,
        flexWrap: ‚wrap’,
        justifyContent: ‚center’,
    },

    vertical: {
        flexDirection: ‚column’,
        flexWrap: ‚nowrap’,
        justifyContent: ‚flex-start’,
    },

    resultSingle: {
        width: appSizes.resultWidth(),
        margin: appSizes.margin,
    }
});

export default appStyles;

pe care le-am folosit ulterior după nevoie, ca de exemplu în această componentă:

import React from ‚react’;
import {
    View,
    Text
} from ‚react-native’;
import localizer from ‚utils/Localizer’;
import BookFigureComponent from
  ‚./BookFigureComponent’;
import appStyles from ‚styles/AppStyles’;

function ReadonlyBookComponent(props) {
    const { book } = props;
    if(!book) {
        return null;
    }
    return (
      <View style={[appStyles.resultSingle, 
          appStyles.container, appStyles.vertical]}>
        <BookFigureComponent book={book} 
          size=”large”/>
        <Text>{localizer.localize(‚book-by-label’)} 
          {book.authors.join(‚, ‚)}</Text>
        <Text>{book.pages} 
         {localizer.localize(‚book-pages-label’)}
        </Text>
      </View>
    );
}

export default ReadonlyBookComponent;

și apoi în componenta de start:

render() {
    const { isLocalizerInitialized } = this.state;

    let bookRNIA = {
        title: „React Native In Action”,
        image: 
          „https://images.manning.com/720/960/resize/book/2/8a23d37-c21c-491a-a5a9-498b6b54fe6d/Dabit-React-HI.png”,
        authors: [„Sir John Whitmore”],
        pages: 320
    };
    return isLocalizerInitialized && (
        <View style={[appStyles.app, 
         appStyles.container, appStyles.horizontal]}>
        <ReadonlyBookComponent book={{...bookRNIA, 
     title: `${bookRNIA.title} 11`}}/>
<ReadonlyBookComponent 
book={{...bookRNIA, title: `${bookRNIA.title} 11`}}/>
<ReadonlyBookComponent book={{...bookRNIA, 
  title: `${bookRNIA.title} 11`}}/>
<ReadonlyBookComponent book={{...bookRNIA, 
  title: `${bookRNIA.title} 11`}}/>
</View>
  );
}

Rezultatul nu a fost cel la care mă așteptam. După câteva dimineți de căutări și încercări repetate, am observat că în cazul meu rezolvarea a fost să folosesc flex: 1 doar în componenta de start:

function ReadonlyBookComponent(props) {
    const { book } = props;
    if(!book) {
        return null;
    }
    return (

  <View style={[appStyles.resultSingle, 
      appStyles.vertical]}>
    <BookFigureComponent book={book} size=”large”/>
    <Text>{localizer.localize(‚book-by-label’)} 
      {book.authors.join(‚, ‚)}</Text>
    <Text>{book.pages} {localizer.localize(‚
      book-pages-label’)}</Text>
   </View>
  );
}

și rezultatul a fost cel așteptat.

Migrare cod Redux

După ce am migrat câteva componente de React și mi-am făcut o părere de cum funcționează lucrurile în React Native, am vrut să refolosesc și alte structuri din proiectul anterior, cum ar fi Redux, care a vrut să-mi dea și el o lecție.

Evident a fost greșeala mea, am modificat package.json, ca să am ultimele versiuni la toate dependințele. Rezolvarea a fost să fiu atent la documentația de React Native și să folosesc versiunile cerute:

„react”: „16.5.0”,
„react-native”: 
 „https://github.com/expo/react-native/archive/sdk-32.0.2.tar.gz”,
„react-redux”: „^6.0.1”,
„redux”: „^4.0.1”,

Am observat, de asemenea, că ^6.0.1 e versiunea maximă pentru react-redux care merge cu redux ^4.0.1.

Migrare container components

După rezolvarea anterioară, nu m-am mai lovit de alte probleme majore, excepție făcând bugurile proprii.

Înlocuire apeluri API cu local storage

Ultimul pas a fost să înlocuiesc apelurile de API cu local storage, mai precis să folosesc AsyncStorage. În documentație e menționat că e deprecated și recomandarea e să se folosească React Native Async Storage, un pachet întreținut de React Native Community. Problema e că nu funcționează cu Expo așa că am rămas la AsyncStorage. Mai jos e un extras din logica de salvare a cărților, suficient pentru o primă impresie:

import {AsyncStorage} from ‚react-native’;
import uuid from ‚uuid’;
import {
    buildError,
    getReason
} from ‚utils/Error’;
import { buildResponse } from ‚utils/Response’;

const BOOKS_KEY = ‚MyReads:Books’;

export const fetchBookFromStore = uuid => {
    return new Promise((resolve, reject) => {
        AsyncStorage.getItem(BOOKS_KEY)
            .then(rawBooks => {
                if(!rawBooks) {
                    rawBooks = ‚{}’;
                }
                const books = JSON.parse(rawBooks);

                if(!books[uuid]) {
                    reject(buildError(404));
                    return;
                }

                resolve(buildResponse(books[uuid]));
            })
            .catch(error => {
                reject(error);
            });
    });
};

export const addBookInStore = book => {
    return new Promise((resolve, reject) => {
        AsyncStorage.getItem(BOOKS_KEY)
            .then(rawBooks => {
                if(!rawBooks) {
                    rawBooks = ‚{}’;
                }
                const books = JSON.parse(rawBooks);

                const savedBook = {...book};
                savedBook.uuid = uuid.v1();
                books[savedBook.uuid] = savedBook;
                AsyncStorage.setItem(BOOKS_KEY, 
                  JSON.stringify(books))
                    .then( () => {
                        resolve(buildResponse(
                          savedBook));
                    })
                    .catch(error => {
                        reject(error);
                    });
            })
            .catch(error => {
                reject(error);
            });
    });
};

Concluzii

Am avut atâtea bătăi de cap din cauza lipsei mele de experiență cu React Native. După ce mi-am rezolvat problemele de infrastructură, nu m-am mai lovit de alte probleme majore. Cred că React Native și Expo se pot folosi fără probleme, cel puțin pentru aplicațiile care nu au nevoie de funcționalități specifice ale platformei pe care rulează.

Resurse:

Proiect original:

Proiectul cu React Native:

Sponsori

  • comply advantage
  • ntt data
  • 3PillarGlobal
  • Betfair
  • Telenav
  • Accenture
  • Siemens
  • Bosch
  • FlowTraders
  • MHP
  • Connatix
  • UIPatj
  • MetroSystems
  • Globant
  • Colors in projects

Vasile Boris a mai scris