ABONAMENTE VIDEO REDACȚIA
RO
EN
NOU
Numărul 150
Numărul 149 Numărul 148 Numărul 147 Numărul 146 Numărul 145 Numărul 144 Numărul 143 Numărul 142 Numărul 141 Numărul 140 Numărul 139 Numărul 138 Numărul 137 Numărul 136 Numărul 135 Numărul 134 Numărul 133 Numărul 132 Numărul 131 Numărul 130 Numărul 129 Numărul 128 Numărul 127 Numărul 126 Numărul 125 Numărul 124 Numărul 123 Numărul 122 Numărul 121 Numărul 120 Numărul 119 Numărul 118 Numărul 117 Numărul 116 Numărul 115 Numărul 114 Numărul 113 Numărul 112 Numărul 111 Numărul 110 Numărul 109 Numărul 108 Numărul 107 Numărul 106 Numărul 105 Numărul 104 Numărul 103 Numărul 102 Numărul 101 Numărul 100 Numărul 99 Numărul 98 Numărul 97 Numărul 96 Numărul 95 Numărul 94 Numărul 93 Numărul 92 Numărul 91 Numărul 90 Numărul 89 Numărul 88 Numărul 87 Numărul 86 Numărul 85 Numărul 84 Numărul 83 Numărul 82 Numărul 81 Numărul 80 Numărul 79 Numărul 78 Numărul 77 Numărul 76 Numărul 75 Numărul 74 Numărul 73 Numărul 72 Numărul 71 Numărul 70 Numărul 69 Numărul 68 Numărul 67 Numărul 66 Numărul 65 Numărul 64 Numărul 63 Numărul 62 Numărul 61 Numărul 60 Numărul 59 Numărul 58 Numărul 57 Numărul 56 Numărul 55 Numărul 54 Numărul 53 Numărul 52 Numărul 51 Numărul 50 Numărul 49 Numărul 48 Numărul 47 Numărul 46 Numărul 45 Numărul 44 Numărul 43 Numărul 42 Numărul 41 Numărul 40 Numărul 39 Numărul 38 Numărul 37 Numărul 36 Numărul 35 Numărul 34 Numărul 33 Numărul 32 Numărul 31 Numărul 30 Numărul 29 Numărul 28 Numărul 27 Numărul 26 Numărul 25 Numărul 24 Numărul 23 Numărul 22 Numărul 21 Numărul 20 Numărul 19 Numărul 18 Numărul 17 Numărul 16 Numărul 15 Numărul 14 Numărul 13 Numărul 12 Numărul 11 Numărul 10 Numărul 9 Numărul 8 Numărul 7 Numărul 6 Numărul 5 Numărul 4 Numărul 3 Numărul 2 Numărul 1
×
▼ LISTĂ EDIȚII ▼
Numărul 87
Abonament PDF

De la React la React Native, un exemplu practic

Vasile Boris
Programator, team lead, trainer & coach



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:

NUMĂRUL 149 - Development with AI

Sponsori

  • Accenture
  • BT Code Crafters
  • Accesa
  • Bosch
  • Betfair
  • MHP
  • BoatyardX
  • .msg systems
  • P3 group
  • Ing Hubs
  • Cognizant Softvision
  • Colors in projects