Stav komponentu a asset-y

stav a vlastnosti komponentu, obrázky, zvuky, komponent ako trieda, inštalácia modulov tretích strán, štruktúra projektu

Záznam z prednášky

Project Torch Overview

  • (slide) Naposledy sme začali s vývojom aplikácie, ktorá z nášho telefónu spraví baterku. Dnes budeme pokračovať vo vývoji, vytvoríme používateľské rozhranie a aplikácii pridáme stav.

O komponentoch

  • (slide) Vráťme sa ešte späť ku komponentom. Komponenty sú základným stavebným prvkom rámca React Native a sú implementované ako JavaScript-ové funkcie, ktoré vracajú React Native elementy definujúce vzhľad. Každý komponent je definovaný svojim stavom a je možné mu pri volaní nastaviť jeho vlastnosti.

  • (slide) Pre názornosť si môžeme základné vlastnosti komponentu predstaviť na základnom komponente Button.

    <Button
        onPress={onPressButton}
        title="Turn On"
        color="#841584"
    />

Vlastnosti komponentov

  • Vlastnosti komponentu (označované ako properties alebo skrátene ako props) sú do komponentu vložené pri jeho vytváraní a sú ďalej nemenné. Niektoré vlastnosti sú povinné (v prípade tlačidla sú to vlastnosti onPress a title). Z pohľadu zápisu sa jedná o atribúty XML elementov (v tomto prípade JSX). Zoznam vlastnsotí každého komponentu nájdete v jeho dokumentácii. V prípade komponentu Button ho nájdete tu.

Stav komponentu

  • Stav komponentu je naopak meniteľný a vďaka stavu sa dá na komponent pozerať ako na stavový stroj. V prípade tlačidla vieme hovoriť o dvoch stavoch:

    • tlačidlo je stlačené, a
    • tlačidlo nie je stlačené.
  • V prípade vytváranej aplikácie chceme, aby sa stlačením tlačidla baterka

    • rozsvietila, ak je zhasnutá, alebo
    • zhasla, ak je rozsvietená.
  • O našej aplikácii teda vieme povedať, že sa bude nachádzať v jednom dvoch stavov:

    • zhasnutá, a
    • rozsvietená.
  • Každý React Native komponent môze mať definovaný 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).

Definovanie stavu komponentu

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

    import {useState} from 'react';
    
    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(). Hook useState() bude teda vyzerať nasledovne:

    const [isOn, setIsOn] = useState(false);
  • Kód komponentu aplikácie bude následne vyzerať nasledovne:

    export default function App() {
        const [isOn, setIsOn] = useState(false);
    
        return (
            <View style={styles.container}>
                <Text>Hello, world!</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, ktorá sa zavolá po stlačení, aby zmenila aktuálny stav komponentu:

    return (
        <View style={styles.container}>
            <Text>Hello, world!</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.

  • (slide) 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. Ak chceme vypísať obsah ľubovoľnej premennej alebo obecne výraz pomocou JSX, uzavrieme ho do kučeravých zátvoriek. Týmto spôsobom môžeme nechať vypísať ľubovoľný platný JavaScript-ový výraz. V našom prípade môžeme teda upravíme obsah komponentu Text a vlastnosť title komponentu 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>
        );
    }

Komponent pre obrázok

  • (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, ktorý importujeme z react-native.

  • 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é vlastnosti, ako tomu bolo v prípade komponentu Button.

  • Obrázky, ktoré budú reprezentovať stav aplikácie, budú jej statickou súčasťou. V projekte preto vytvoríme samostatný priečinok assets/, kde tieto obrázky uložíme. Obrázky, ktoré použijeme, sú zo sady Tango Icons, ktoré sú vydané pod licenciou Creative Commons.

    Zapnutá baterka Vypnutá baterka

  • Ak ich budeme chcieť použiť v komponente Image, použijeme vlastnosť 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
  • Obrázok vyberieme na základe stavu komponentu a ternárneho operátora a výsledok uložíme do premennej image. Jeho hodnotu následne použijeme ako hodnotu vlastnsoti source komponentu Image:

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

Stlačiteľný obrázok

  • 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, ktorý je pred použitím treba importovať z react-native. Ten funguje ako istý kontajner, do ktorého zabalíme komponent, ktorý má byť stlačiteľným.

  • Do komponentu Pressable 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, vytiahneme funkciu z komponentu Button von, nazveme ju toggleState() a použijeme ju ako callback v oboch komponentoch vo vlastnosti onPress.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 toggleState = function () {
            setIsOn(function (state) {
                state = !state;
                return state;
            });
        };
    
        return (
            <View style={styles.container}>
                <Text>{isOn === true ? "On" : "Off"}</Text>
                <Pressable onPress={toggleState}>
                    <Image source={image} />
                </Pressable>
                <Button
                    onPress={toggleState}
                    title={
                        isOn === true ? "Turn Off" : "Turn On"
                    }
                />
            </View>
        );
    }

Kompletné riešenie

  • Po drobnom refaktorovaní, kedy vyčistíme funkciu toggleState() a odstránime komponent Text, 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);
    
        let 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",
        },
    });

Komponent ako trieda

  • (slide) Pred uvedením hook-ov vo verzii React 16.8, sa komponenty vytvárali ako triedy. Triedy boli potomkom obecného komponentu Component a mali metódy, ktoré definovali ich správanie. V tejto odbočke si ukážeme, ako by komponent aplikácie vyzeral, ak by sme ho implementovali ako triedu.

  • Výpis výsledného kódu sa nachádza nižšie.

    import React, { Component } from "react";
    import {
        StyleSheet,
        View,
        Button,
        Image,
        Pressable,
    } from "react-native";
    
    export default class App extends Component {
        constructor(props) {
            super(props);
            this.state = {
                isOn: false,
            };
        }
    
        toggleState() {
            const { isOn } = this.state;
            console.log(">> state change...");
            this.setState({ isOn: !isOn });
        }
    
        render() {
            const image = this.state.isOn
                ? require("./assets/bulb_on.png")
                : require("./assets/bulb_off.png");
    
            return (
                <View style={styles.container}>
                    <Pressable
                        onPress={this.toggleState.bind(this)}
                    >
                        <Image source={image} />
                    </Pressable>
                    <Button
                        onPress={this.toggleState.bind(this)}
                        title={
                            this.state.isOn === true
                                ? "Turn Off"
                                : "Turn On"
                        }
                    />
                </View>
            );
        }
    }
    
    const styles = StyleSheet.create({
        container: {
            flex: 1,
            backgroundColor: '#fff',
            alignItems: 'center',
            justifyContent: 'center',
        },
    });

Prehratie zvuku pri kliknutí

  • (slide) Pre dosiahnutie väčšieho efektu zabezpečíme, aby sa pri stlačení tlačidla prehral zvuk zapnutia/vypnutia baterky. Ukážeme si teda, ako je možné v aplikácii prehrávať zvuky.

Inštalácia modulov tretích strán

  • (slide) Rámec React Native nemá priamu podporu pre prehrávanie zvukov. Ak teda chceme v našej aplikácii prehrávať zvuky, musím podporu pre ich prehrávanie doinštalovať.

  • Ako odrazový mostík môžeme použiť projekt Awesome React Native. Jedná sa o web, ktorý ponúka prehľad najpopulárnejších modulov pre rámec React Native rozdelený do niekoľkých kategórií.

  • Samozrejme je stále možné použiť aj obecný vyhľadávač www.npmjs.com pre vyhľadávanie modulov pre node. Pri vyhľadávaní preto netreba zabudnúť pripísať aj “react native”.

  • (slide) Ak sa teda pozrieme do zoznamu Awesome do kategórie Media, nájdeme na prvých priečkach modul react-native-sound, ktorý použijeme. Do projektu ho nainštalujeme príkazom:

    $ yarn add react-native-sound

Pridanie zvuku do projektu

  • Zvuk môžeme do projektu pridať dvoma spôsobmi:

    1. Súbor zvuku nahráme priamo do priečinku zdrojov pre platormu Android. Konkrétne sa jedná o priečinok android/app/src/main/res/raw/. Je dôležité, aby sa súbor skladal len z malých písmen a znaku podčiarkovník “_”. V tomto prípade je možné sa na súbor odkazovať priamo menom bez uvádzania cesty k nemu.

    2. Súbor uložíme do priečinku projektu a v kóde sa na neho budeme odkazovať buď pomocou príkazu import alebo funkcie require() podobne, ako sme to urobili v prípade obrázkov.

  • Podobne, ako v prípade obrázkov, použijeme na prácu so zvukovými súbormi funkciu require().

Vytvorenie objektu Sound

  • Najprv v projekte naimportujeme modul react-native-sound:

    import Sound from 'react-native-sound';
  • Najprv vytvoríme objekt zvuku vytvorením objektu Sound týmto zápisom:

    const ding = new Sound(require('./assets/flashlight.wav'));
  • Tým, že sme použili funkciu require() na načítanie zdroja, nemusíme pri vytváraní používať žiadne ďalšie parametre, ako napr. callback pre ošetrenie prípadu, kedy k načítaniu nedôjde z dôvodu neexistencie zdroja. V prípade, že zdroj neexistuje, dôjde k chybe už pri zostavovaní balíku a nie až pri spustení aplikácie.

Prehratie zvuku

  • Vytvorený objekt zvuku môžeme prehrať metódou .play(). Zvuk budeme prehrávať pri stlačení tlačidla, takže aktualizujeme funkciu toggle(). Najprv však prehráme zvuk a až potom zmeníme stav aplikácie. Tým čiastočne potlačíme prípadné spozdenie prehrávania zvuku.

    const toggleState = function () {
        ding.play();
        setIsOn(!isOn);
    };
  • Modul react-native-sound má samozrejme mnoho ďalších možností, ako napr. nastavenie hlasitosti prehrávaného zvuku, prehrávanie zvuku na ľavej/pravej strane pri stereu, opakované prehrávanie a ďalšie. Všetky možnosti nájdete na stránke projektu.

Project Structure

  • (slide) React a React Native sú rámce, ktoré nám nijakým spôsobom nepredpisujú štruktúru projektu alebo aplikácie. Aktuálne je náš projekt jednoduchý, ale s novými funkciami, obrazovkami, komponentmi, prekladmi sa počet súborov, z ktorých sa skladá, rapídne zväčší. Ako teda organizovať súbory tak, aby sme sa dokázali v projekte jednoducho orientovať aj napriek veľkému počtu súborov?

  • Existuje niekoľko spôsobov, ako organizovať štruktúru projektu. Spoločné majú hlavne logické zoskupovanie súborov podľa ich typu alebo funkcionality (tzv. type vs feature). Ukážka jedného z týchto prístupov sa nachádza napr. v projekte React Native Easy Starter.

  • Nebudeme rozoberať a upravovať štruktúru nášho projektu pre veci, ktoré nemáme, takže urobíme niečo, čo majú všetky štruktúry spoločné: súbory, ktoré tvoria náš projekt a nie sú automaticky vygenerované nástrojmi rámca, umiestnime do osobitného priečinku s názvom src/.

  • Vytvoríme teda priečinok src/ a presunieme do neho všetko, čo sme vytvorili včetne priečinku assets/:

    src/
    ├── App.js
    └── assets/
        ├── bulb_off.png
        ├── bulb_on.png
        └── flashlight.wav
  • Prostredie Visual Studio Code urobí potrebné úpravy v súboroch projektu za nás, takže projekt by mal fungovať bez prípadných zásahov. Môžeme však z priečinku src/ vytvoriť rovno modul pridaním súboru index.js, v ktorom exportujeme komponent App zo súboru App.js:

    import App from './App';
    
    export default App;
  • Pre istotu môžeme overiť ešte súbor index.js v koreňovom priečinku projektu a v prípade potreby upraviť import:

    import App from './src';

    A okrem toho aj odkazy na ikony v súbore app.json.

  • Okrem uvedeného môžeme aktualizovať aj súbor .gitignore. Môžeme použiť verziu priamo od Facebook-u, ktorá sa dá rovno stiahnuť z GitHub-u projektu.

Záver

  • Nabudúce sa konečne pokúsime rozsvietiť blesk na fotoaparáte.