Components and State

komponenty, stav a vlastnosti komponentov, základné komponenty

Záznam z prednášky

Project Torch Overview

  • (slide) Na minulej prednáške sme si predstavili rámec na vývoj natívnych mobilných aplikácií React Native a začali sme vyvíjať aplikáciu Torch. Dnes budeme vo vývoji tejto aplikácie pokračovať a popri tom sa budeme zoznamovať s vlastnosťami tohto rámca.

  • (slide) Táto aplikácia bude jednoduchou baterkou na svietenie, kde na svetlo využijeme blesk zadnej kamery/fotoaparátu.

Expo SDK 39 is Now Available

  • (slide) Keďže sme toho naposledy veľa nestihli a medzičasom bola vydaná nová verzia Expa s podporou SDK 39, ktorá podporuje React Native 0.63, začneme opäť rýchlym vytvorením aplikácie Torch pomocou nástroja expo-cli. Aktualizácia je potrebná, pretože v našom projekte budeme používať komponenty, ktoré v SDK 38 ešte nie sú podporované.

  • Projekt teda vytvoríme znova:

    $ expo-cli init torch
    # select blank
    $ cd torch
    $ code .
    $ npm start

React Native vs React

  • (slide) Ak chceme zvládnuť prácu v rámci React Native, nemôžeme zabudnúť na rámec React, na ktorom je React Native postavený, a na ktorom je závislý. Obecne môžeme povedať, že rámec React Native sa používa na tvorbu (nie len) mobilných aplikácií pomocou React-u a schopností/možností natívnej platformy. Túto závislosť si môžete všimnúť aj v súbore App.js, kde sa React importuje riadkom:

    import React from 'react';
    ReactJS vs React Native (zdroj)
  • Pre zvládnutie rámca React Native je teda dobré poznať aspoň základy rámca React.

  • (slide) Takže - čo je React? React je knižnica pre JavaScript na tvorbu používateľských rozhraní. Tie sa vytvárajú pomocou malých izolovaných častí, ktoré sa nazývajú komponenty.

  • Komponenty je možné navzájom prepájať, čím je možné vytvárať komplexné používateľské rozhrania. Toto prepojenie je možné vizualizovať pomocou stromu, v ktorom existuje jeden koreňový komponent a každý ďalší komponent sa stáva jeho samostatnou vetvou. Každá ďalšia vetva môže mať samozrejme ďalšie podvetvy. Príklad takéhoto stromu komponentov (z angl. component tree) sa nachádza na nasledujúcom obrázku:

Components

  • Komponenty sú základných stavebným prvkom týchto rámcov. Pozrime sa teda zblízka na to, čo je vlastne komponent.

  • V našej aplikácii máme zatiaľ jediný komponent, ktorý je súčasne aj koreňovým komponentom aplikácie. Tento komponent sa nachádza v súbore App.js a vyzerá takto:

    export default function App() {
      return (
        <View style={styles.container}>
          <Text>Open up App.js to start working on your app!</Text>
          <StatusBar style="auto" />
        </View>
      );
    }
  • Komponent je teda definovaný ako funkcia. Čokoľvek, čo táto funkcia vráti, je vykreslené ako React element, ktorý definuje, čo bude viditeľné na obrazovke zariadenia. Definujeme vlastne pohľad (view) komponentu.

  • Obecne je každý komponent definovaný svojim stavom a vlastnosťami. O nich si však porozprávame trochu neskôr.

Views

  • Pri vývoji aplikácií pre Android alebo iOS sú pohľady (views) záladným stavebným blokom pre tvorbu používateľského rozhrania. Obecne sa jedná o elementy obdĺžnikového tvaru, pomocou ktorých vieme zobrazovať text, obrázky, vedia reagovať na vstup od používateľa.

  • Pohľad (view) je vlastne všetko, čo je možné zobraziť na obrazovke zariadenia. Niektoré pohľady môžu obsahovať aj iné pohľady.

  • (slide) Rámec React Native obsahuje generické (core) komponenty (resp. pohľady), pomocou ktorých je možné vyskladať ľubovoľné používateľské rozhranie pre všetky podporované platformy. Následne pri spustení rovnakého kódu na konkrétnej platforme sú tieto pohľady nahradené natívnymi komponentami (resp. pohľadmi). Túto situáciu ilustruje nasledujúci obrázok.

    Different Views in Android and iOS, but similar application [@y1]
  • (slide) React Native má mnoho Core komponentov. Ich kompletný zoznam nájdete v dokumentácii API. Najčastejšie však budete pracovať s komponentami, ktoré sú uvedené v nasledujúcej tabuľke.

    Prehľad najčastejších komponentov React Native [@y1]
    React Native UI Component Android View iOS View Web Analog Description
    <View> <ViewGroup> <UIView> A non-scrollling <div> A container that supports layout with flexbox, style, some touch handling, and accessibility controls
    <Text> <TextView> <UITextView> <p> Displays, styles, and nests strings of text and even handles touch events
    <Image> <ImageView> <UIImageView> <img> Displays different types of images
    <ScrollView> <ScrollView> <UIScrollView> <div> A generic scrolling container that can contain multiple components and views
    <TextInput> <EditText> <UITextField> <input type="text"> Allows the user to enter text

JSX

  • (slide) Keď sa však pozrieme bližšie na to, čo funkcia v skutočnosti vracia, nie je to ani reťazec a nie je to ani HTML. React totiž používa syntax JSX (JavaScript XML), čo je XML/HTML rozšírenie pre kód jazyka JavaScript. React nie je na technológii JSX závislý, ale jeho použitím je tvorba komponentov jednoduchšia, prehľadnejšia a hlavne prirodzenejšia. Za syntaxou JSX stojí rovnako spoločnosť Facebook.

  • Príklad jednoduchého Hello, world! komponentu môže vyzerať takto:

    function HelloWorld() {
        return <Text>Hello, world!</Text>;
    }
  • Aby React (a vlastne JavaScript) rozumel JSX, je potrebné použiť transpiler Babel, ktorý dokáže preložiť tento značkovací jazyk do jazyka JavaScript. Ten okrem toho pozná aj všetky novinky, ktoré priniesol ES6 (ECMAScript 2015).

Button Component

  • Do UI našej aplikácie potrebujeme pridať tlačidlo, pomocou ktorého budeme baterku ovládať. Na to použijeme komponent Button, ktorý sa podľa potreby vyrenderuje na každej platforme.

  • Komponent bude vyzerať nasledovne:

    <Button
      onPress={onPressButton}
      title="Turn On"
    />
  • Ako je možné vidieť, komponent tlačidla má dva povinné atribúty:

    • title - text, ktorý sa na tlačidle zobrazí
    • onPress - funkcia, ktorá bude zavolaná po stlačení tlačidla.
  • Z komponentu App môžeme vypustiť komponent <StatusBar> a pridáme do neho komponent <Button>:

    export default function App() {
        return (
            <View style={styles.container}>
                <Text>
                    Open up App.js to start working on your app!
                </Text>
                <Button
                    onPress={function () {
                        console.log("Button was pressed");
                    }}
                    title="Turn On"
                />
            </View>
        );
    }
  • Ak tlačidlo stlačíme, v konzole Metro Bundler-a alebo v nástroji Visual Studio Code uvidíme vypísaný text.

Component State

  • Stlačením tlačidla chceme, aby sa baterka

    • rozsvietila, ak je zhasnutá, alebo
    • zhasla, ak je rozsvietená.
  • Našu aplikáciu by sme teda mohli nazvať aj stavovým strojom, pretože v ktoromkoľvek momente vieme povedať, že sa bude nachádzať v jednom dvoch stavov:

    • zhasnutá, a
    • rozsvietená.
  • V rámci React Native môze mať každý komponent definovaný aj svoj vlastný stav, ktorý si bude následne pamätať. Stav sa môže v čase meniť napr. na základe interakcie používateľa (stlačením tlačidla na baterke).

Definig State in React Native

  • (slide) Pridať stav do komponentu je možné pomocou volani hook-u useState() nasledovne:

    const [<getter>, <setter>] = useState(<initialValue>);

    kde:

    • getter reprezentuje premennú, ktorá bude stav komponentu reprezentovať,
    • setter reprezentuje názov funkcie, pomocou ktorej bude možné zmeniť stav, a
    • initialValue je hodnota, ktorá reprezentuje počiatočný stav komponentu.

Refactoring

  • V našom prípade môžeme nazvať stavovú premennú isOn, ktorá bude inicializovaná na hodnotu false (nesvieti). Funkcia na zmenu stavu sa bude volať setIsOn:

    const [isOn, setIsOn] = useState(false);
  • Kód komponentu bude teda vyzerať nasledovne:

    export default function App() {
        const [isOn, setIsOn] = useState(false);
    
        return (
            <View style={styles.container}>
                <Text>
                    Open up App.js to start working on your app!
                </Text>
                <Button
                    onPress={function () {
                        console.log("Button was pressed");
                    }}
                    title="Turn On"
                />
            </View>
        );
    }
  • Stav sa bude meniť pri stlačení tlačidla. Upravíme teda anonymnú funkciu v komponente Button, ktorá sa zavolá pri stlačení tlačidla:

    return (
        <View style={styles.container}>
            <Text>
                Open up App.js to start working on your app!
            </Text>
            <Button
                onPress={function () {
                    setIsOn(!isOn);
                    console.log(isOn);
                }}
                title="Turn On"
            />
        </View>
    );
  • Zmenu stavu môžeme teraz vidieť vypísanú v konzole, kde sa bude stlačením tlačidla striedať hodnota false a true.

  • Na zmenu stavu môžeme zareagovať aj priamo v aplikácií zmenou textu v komponente Text a zmenou názvu tlačidla v komponente Button:

    export default function App() {
        const [isOn, setIsOn] = useState(false);
    
        return (
            <View style={styles.container}>
                <Text>{isOn === true ? "On" : "Off"}</Text>
                <Button
                    onPress={function () {
                        setIsOn(!isOn);
                        console.log(isOn);
                    }}
                    title={isOn === true ? "Turn Off" : "Turn On"}
                />
            </View>
        );
    }

Image Component

  • (slide) Jediné, čo nám aktuálne chýba do používateľského rozhrania baterky, je obrázok reprezentujúci stav baterky. Ten vytvoríme pomocou komponentu Image.

  • Komponent Image je možné použiť na zobrazenie obrázkov z rôznych zdrojov, ako

    • obrázky umiestnené v internete/sieti,
    • statické obrázky,
    • dočasné lokálne obrázky, a
    • obrázky z lokálneho disku.
  • Pre rozličné typy obrázkov je možné použiť rozličné atribúty. Preto komponent Image nemá povinné props, ako tomu bolo v prípade komponentu Button.

  • Pre naše potreby budeme pracovať s obrázkami, ktoré budú statickou súčasťou aplikácie. Tieto obrázky, ktoré reprezentujú zasvietenú a zhasnutú žiarovku, uložíme do priečinku assets/

    Zapnutá baterka Vypnutá baterka

  • Ak ich budeme chcieť použiť v komponente Image, použijeme property source, kde sa na príslušný obrázok odkážeme pomocou volania require(). Upravíme teda pohľad komponentu App pridaním komponentu Image medzi text a tlačidlo s obrázkom zhasnutej žiarovky:

    <Image source={require("./assets/bulb_off.png")}></Image>
  • Tu je však potrebné si uvedomiť niekoľko vecí:

    • výsledkom volania funkcie require() je len číslo zdroja, ktoré bundler zabalil do projektu
    • funkcia require() sa volá pri balení aplikácie a teda nemôže obsahovať dynamicky konštruované reťazce
  • Upravíme teda kód komponentu tak, aby sa potrebný obrázok vybral mimo komponentu:

    export default function App() {
        const [isOn, setIsOn] = useState(false);
    
        var image = isOn
            ? require("./assets/bulb_on.png")
            : require("./assets/bulb_off.png");
    
        console.log(image);
    
        return (
            <View style={styles.container}>
                <Text>{isOn === true ? "On" : "Off"}</Text>
                <Image source={image}></Image>
                <Button
                    onPress={function () {
                        console.log(`>> before: ${isOn}`);
                        setIsOn(function (state) {
                            state = !state;
                            console.log(`>> after: ${state}`);
                            return state;
                        });
                    }}
                    title={isOn === true ? "Turn Off" : "Turn On"}
                />
            </View>
        );
    }

Pressable Image

  • Jediné, čo nám aktuálne chýba v našej aplikácii, je možnosť zmeniť jej stav stlačením obrázku. Ak sa však pozrieme na vlastnosti komponentu Image, nenájdeme v zozname nič, čo by pripomínalo možnosť ošetriť stlačenie podobne, ako tomu bolo v prípade obrázka. V tomto prípade treba postupovať ináč.

  • (slide) Na použitie obrázku ako tlačidla použijeme komponent Pressable. Ten funguje ako istý kontajner, do ktorého zabalíme komponent, ktorý má byť stlačiteľným.

  • Do komponentu Pressable teda zabalíme komponent Image:

    <Pressable
        onPress={function () {
            console.log("Image was pressed.");
        }}
    >
        <Image source={image} />
    </Pressable>
  • Keďže po stlačení obrázka očakávame rovnaké správanie ako po stlačení tlačidla, vytvoríme samostatnú funkciu, ktorú budeme volať v prípade stlačenia oboch komponentov. Upravený komponent App bude vyzerať takto:

    export default function App() {
        const [isOn, setIsOn] = useState(false);
    
        var image = isOn
            ? require("./assets/bulb_on.png")
            : require("./assets/bulb_off.png");
    
        const onPressed = function () {
            console.log(`>> before: ${isOn}`);
            setIsOn(function (state) {
                state = !state;
                console.log(`>> after: ${state}`);
                return state;
            });
        };
    
        return (
            <View style={styles.container}>
                <Text>{isOn === true ? "On" : "Off"}</Text>
                <Pressable onPress={onPressed}>
                    <Image source={image} />
                </Pressable>
                <Button
                    onPress={onPressed}
                    title={isOn === true ? "Turn Off" : "Turn On"}
                />
            </View>
        );
    }

Complete Solution

  • Po drobnom refaktorovaní bude výsledné riešenie vyzerať nasledovne:
import React, { useState } from "react";
import { 
    StyleSheet, View, Button, Image, Pressable 
} from "react-native";

export default function App() {
    const [isOn, setIsOn] = useState(false);

    var image = isOn
        ? require("./assets/bulb_on.png")
        : require("./assets/bulb_off.png");

    const toggleState = function () {
        setIsOn(!isOn);
    };

    return (
        <View style={styles.container}>
            <Pressable onPress={toggleState}>
                <Image source={image} />
            </Pressable>
            <Button
                onPress={toggleState}
                title={isOn === true ? "Turn Off" : "Turn On"}
            />
        </View>
    );
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: "#fff",
        alignItems: "center",
        justifyContent: "center",
    },
});

Conclusion

  • Dnes sme teda vytvorili kostru používateľského rozhrania aplikácie baterka. Popri tom sme sa zoznámili s niektorými komponentmi rámca React Native a predstavili sme si ich vlastnosti.
  • Nabudúce budeme pokračovať a pokúsime sa rozsvietiť blesk fotoaparátu vo vašom zariadení. Samozrejme len v prípade, že ho zariadenie obsahuje vo svojej výbave.