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 ={onPressButton} onPress="Turn On" title="#841584" color/>
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
atitle
). 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 komponentuButton
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, ainitialValue
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 hodnotufalse
(nesvieti). Funkcia na zmenu stavu sa bude volaťsetIsOn()
. HookuseState()
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 ={function () { onPressconsole.log("Button was pressed"); }}="Turn On" title/> </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 ={function () { onPresssetIsOn(!isOn); console.log(isOn); }}="Turn On" title/> </View> ; )
Zmenu stavu môžeme teraz vidieť vypísanú v konzole, kde sa bude stlačením tlačidla striedať hodnota
false
atrue
.(slide) Na zmenu stavu môžeme zareagovať aj priamo v aplikácií zmenou textu v komponente
Text
a zmenou názvu tlačidla v komponenteButton
. 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 komponentuText
a vlastnosťtitle
komponentuButton
:export default function App() { const [isOn, setIsOn] = useState(false); return ( <View style={styles.container}> <Text>{isOn === true ? "On" : "Off"}</Text> <Button ={function () { onPresssetIsOn(!isOn); console.log(isOn); }}={ title=== true ? "Turn Off" : "Turn On" isOn }/> </View> ; ) }
Poznámka
Ak budete pozorne sledovať, všimnete si, že pri ošetrení stlačenia tlačidla nedôjde k aktualizovaniu hodnoty stavu okamžite. To je spôsobené tým, že funkcia
setIsOn()
pridá zmenu stavu ako úlohu do fronty, ktorá sa vyprázdňuje postupne. Ak chcete ale zareagovať okamžite, funkciasetIsOn()
môže ako parameter dostať funkciu, ktorá vráti nový stav. Aktualizovaný komponentButton
môže vyzerať nasledovne:<Button ={function () { onPressconsole.log(`>> before: ${isOn}`); setIsOn(function (state) { = !state; state console.log(`>> after: ${state}`); return state; ; }) }}={isOn === true ? "Turn Off" : "Turn On"} title/>
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 zreact-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 komponentuButton
.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.Ak ich budeme chcieť použiť v komponente
Image
, použijeme vlastnosťsource
, kde sa na príslušný obrázok odkážeme pomocou volaniarequire()
. Upravíme teda pohľad komponentuApp
pridaním komponentuImage
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
- výsledkom volania funkcie
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 vlastnsotisource
komponentuImage
: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 ={function () { onPresssetIsOn(function (state) { = !state; state return state; ; }) }}={ title=== true ? "Turn Off" : "Turn On" isOn }/> </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ť zreact-native
. Ten funguje ako istý kontajner, do ktorého zabalíme komponent, ktorý má byť stlačiteľným.Poznámka
Komponent
Pressable
je stále novinkou v rámci React Native a patrí medzi core komponenty. Pred jeho zavedením sa používali komponenty s prefixomTouchable*
, ako napr.TouchableHighlight
,TouchableOpacity
aTouchableWithoutFeedback
. KomponentPressable
ich má postupne nahradiť, takže sa dá očakávať, že uvedené komponenty postupne zmiznú z budúcich verzií rámca React Native a neodporúča sa ich používať.Do komponentu
Pressable
zabalíme komponentImage
:<Pressable ={function () { onPressconsole.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 jutoggleState()
a použijeme ju ako callback v oboch komponentoch vo vlastnostionPress
.Upravený komponentApp
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 ={toggleState} onPress={ title=== true ? "Turn Off" : "Turn On" isOn }/> </View> ; ) }
Kompletné riešenie
Po drobnom refaktorovaní, kedy vyčistíme funkciu
toggleState()
a odstránime komponentText
, bude výsledné riešenie vyzerať nasledovne:import React, { useState } from "react"; import { StyleSheet, , View, Button, Image, Pressablefrom "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 ={toggleState} onPress={isOn === true ? "Turn Off" : "Turn On"} title/> </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, Pressablefrom "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 ={this.toggleState.bind(this)} onPress> <Image source={image} /> </Pressable> <Button ={this.toggleState.bind(this)} onPress={ titlethis.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í.
Poznámka
Podobných projektov je samozrejme viac. Pod názvom Awesome nájdete aj iné zoznamy a to nie len pre rámec React Native. Veľmi často ich nájdete priamo na Github-e. Otázkou je vždy ich aktuálnosť.
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
Upozornenie
Odporúča sa tiež vyčistiť cache pre zostavenie Android aplikácie, čím sa dá predísť problémom pri zostavovaní. To zabezpečíme príkazom:
$ cd android $ ./gradlew cleanBuildCache
Pridanie zvuku do projektu
Zvuk môžeme do projektu pridať dvoma spôsobmi:
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.Súbor uložíme do priečinku projektu a v kóde sa na neho budeme odkazovať buď pomocou príkazu
import
alebo funkcierequire()
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 funkciutoggle()
. 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 () { .play(); dingsetIsOn(!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/
.Poznámka
V Expo projekte sa dá stretnúť s názvom tohto priečinku
app/
.Vytvoríme teda priečinok
src/
a presunieme do neho všetko, čo sme vytvorili včetne priečinkuassets/
: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úboruindex.js
, v ktorom exportujeme komponentApp
zo súboruApp.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
.Upozornenie
Pravdepodobne bude potrebné zmazať aj výsledné zostavenie Android projektu v priečinku
android/
:$ cd android $ ./gradlew clean $ cd ..
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.