Životný cyklus komponentu

životný cyklus komponentov reprezentovaných triedami a funkciami, povolenia aplikácie v OS Android, identifikácia platformy

Záznam z prednášky

Introduction

  • (slide) stále pracujeme na baterke

    • baterka svieti
    • Dokonca vieme aj v prípade, že zariadenie baterkou nedisponuje, zobraziť správu a aplikáciu vypnúť. To však robíme neskoro - až vtedy, keď príde požiadavka na zasvietenie/zhasnutie.
    • Dnes vykonáme túto kontrolu rovno pri spustení aplikácie.

Component Lifecycle

  • (slide) Keďže je rámec React Native založený na rámci React, jeho komponenty sa riadia životným cyklom komponentov React-u. Tento životný cyklus je reprezentovaný niekoľkými metódami, ktoré je možné v našej implementácii prepísať (override).

  • (slide) Životný cyklus je možné ilustrovať nasledovným diagramom, ktorý nám poslúži ako istý ťahák:

    React Component Lifecycle [@y13]
  • Aby sme mu lepšie rozumeli, potrebujeme porozumieť nasledovným termínom:

    • mounting - proces pripojenia komponentu do DOM-u
    • updating - proces aktualizácie vlastností komponentu
    • unmounting - proces odpojenia komponentu z DOM-u
  • Ak však budeme hovoriť o mobilných aplikáciách, nebudeme hovoriť o pripájaní komponentov do DOM-u, ale obecne do pohľadu, z ktorého je vytváraný komponent.

Component Lifecycle in Class Components

  • Aby sme životnému cyklu komponentu lepšie rozumeli, budeme sa najprv na komponent pozerať ako na triedu, kde je možné do jednotlivých fáz životného cyklu komponentu vstúpiť vytvorením potrebných metód. Tieto metódy, ako aj ich následnosť, je vidieť na predchádzajúcom obrázku. Ich význam je nasledovný:

    • constructor() - Konštruktor komponentu je volaný ešte predtým, ako je komponent pripojený. Obyčajne sa konštruktor používa z týchto dôvodov:

      • na inicializáciu lokálneho stavu priradením objektu do this.state.
      • na prihlásenie metód v prípade vzniku udalosti
    • componentDidMount() - Metóda je volaná okamžite po pripojení do DOM-u/pohľadu. Používa sa na inicializáciu, pri ktorej sa vyžaduje existencia komponentov v DOM-e/pohľade. V tejto metóde môžete:

      • získať údaje zo vzdialenej služby
      • môžete sa v nej prihlásiť na odber rozličných udalostí (v tom prípade sa z nich nezabudnite odhlásiť v metóde componentWillUnmount())
    • componentDidUpdate() - Metóda je zavolaná okamžite po aktualizácii komponentu. Nie je však zavolaná po prvom renderovaní (volaní render()). Táto metóda sa používa v prípadoch, ktoré súvisia s aktualizovaním komponentu.

    • componentWillUnmount() - Je zavolaná tesne predtým, ako je komponent odpojený a odstránený. Funkcia sa používa na činnosti súvisiace s cleanup-om pri odstraňovaní komponentu, ako rušenie časovačov, ukončenie sieťových operácií alebo odhlásenie sa z odberu udalostí, ku ktorým sa komponent prihlásil v metóde componentDidMount().

  • Metód, ktoré sa používajú v procese životného cyklu komponentu je síce viac, ale tie ostatné sa používajú v špeciálnych prípadoch. Metódy, ktoré tu boli opísané, sú používané najčastejšie.

Clock as Class Component Example

  • Aby sme lepšie porozumeli tomu, ako životný cyklus komponentu funguje, ukážeme si ho na jednoduchom príklade komponentu reprezentujúcom hodiny. A tento komponent vytvoríme ako triedu.

Príprava komponentu

  • Začneme tým, že vytvoríme triedu reprezentujúcu komponent a vytvoríme metódu render(), ktorá vráti jeho vzhľad. Tým pádom budeme mať fungujúci kód, ktorý budeme len ďalej rozširovať. A každú fázu životného cyklu budeme logovať pomocou console.info(), aby sme videli, ako a kedy sa jednotlivé metódy spúšťajú.

    import React, { Component } from "react";
    import {StyleSheet, View, Text} from "react-native";
    
    export default class App extends Component {
        render() {
            console.info(">> render()");
    
            return (
                <View style={styles.container}>
                    <Text style={styles.clock}>
                        12:34:56
                    </Text>
                </View>
            );
        }
     }
    
    const styles = StyleSheet.create({
        container: {
            flex: 1,
            backgroundColor: "#fff",
            alignItems: "center",
            justifyContent: "center",
        },
        clock: {
            fontSize: 40,
        },
    });
  • Ak kód preložíme a spustíme na zariadení, uvidíme v aplikácii zatiaľ statický čas 12:34:56 a v termináli, kde je spustené Metro uvidíme správu

    >> render()

Inicializácia hodín

  • Komponent budeme inicializovať v konštruktore - teda v metóde constructor(). V konštruktore vytvoríme stavovú premennú now, ktorá bude držať informáciu o aktuálnom čase.

    constructor(props) {
       super(props);
    
       console.log();
       console.info(">> constructor");
       this.state = {
          now: new Date(),
       };
    }
  • Následne aktualizujeme funkciu render(), kde necháme vypísať obsah stavovej premennej now:

    {this.state.now.toLocaleTimeString()}
  • Ak teraz aplikáciu spustíme, uvidíme na displeji aktuálny čas, ktorý sa zatiaľ neaktualizuje. Aktuálny čas sa získa vždy vtedy, keď dôjde k spusteniu komponentu a teda zavolaniu konštruktora. Komponent zanikne vtedy, keď ho ručne zrušíme. Nespráva sa teda podobne ako aktivita v OS Android, ktorá sa ukončí aj pri otočení zariadenia.

Vytvorenie časovača

  • Správne hodiny tikajú a nebude tomu ináč ani v tomto prípade. Za tým účelom vytvoríme funkciu tick(), ktorá sa bude spúšťať každú sekundu a bude aktualizovať čas - teda stavovú premennú now komponentu. Jej implementácia bude vyzerať nasledovne:

    tick() {
       console.info(">> tick()");
    
       this.setState({
          now: new Date(),
       });
    }
  • Funkciu tick() budeme volať v časovači, ktorý sa bude spúšťať každú sekundu. V principe máme dve miesta, kde časovač môžeme vytvoriť:

    1. konštruktor komponentu, alebo
    2. metóda componentDidMount()
  • V prípade našej aplikácie je to jedno, ale urobíme to v metóde componentDidMount() (pretože aktuálne iné využitie pre ňu nemáme :-).

  • Samozrejme - uložíme si identifikátor časovača, aby sme ho vedeli nakoniec zrušiť (aby nebežal zbytočne vtedy, keď už komponent zrušíme). Uložíme ho teda do členskej premennej timerId.

  • Výsledná metóda bude vyzerať takto:

    componentDidMount() {
       console.info(">> componentDidMount()");
    
       this.timerId = setInterval(() => this.tick(), 1000 * 1);
    }  
  • Ak teraz spustíme appku, hodiny konečne začnú fungovať vďaka časovaču a vďaka tomu, že dochádza k pravidelnému jednosekundovému aktualizovaniu stavu - aktuálnemu času.

Aktualizácia po vykreslení

  • Metóda componentDidUpdate() sa zavolá automaticky po aktualizovaní vzhľadu komponentu - po jeho vykreslení. V našom prípade nemáme do tejto metóde extra čo vložiť, tak jej volanie len zalogujeme:

    componentDidUpdate() {
        console.info(">> componentDidUpdate()");
    }  

Odstránenie časovača

  • Ak sa pozrieme do logov tentokrát, uvidíme, že výpis volania funkcie tick() prebieha určite častejšie ako každú 1 sekundu. To je spôsobené tým, že došlo k znovuvytvoreniu komponentu s vytvorením nového časovača, ale bez odstránenia toho starého. Zakaždým, keď urobíme zmenu tentokrát, dôjde k vytvoreniu vždy nového časovača. V kontexte aplikácie sme však nikdy neodstránili časovač pri rušení komponentu. A presne na to je určená metóda componentWillUnmount().

  • Metóda componentWillUnmount() sa volá ako posledná pred odstránením komponentu z pohľadu. Jej implementácia bude vyzerať nasledovne:

    componentWillUnmount() {
        console.info(">> componentWillUnmount()");
        clearInterval(this.timerId);
    }    

Výsledný kód

  • Výsledný kód komponentu, ktorý bude reprezentovať hodiny, bude vyzerať nasledovne:

    import React, { Component } from "react";
    import { StyleSheet, View, Text } from "react-native";
    
    export default class App extends Component {
        constructor(props) {
            super(props);
    
            console.info();
            console.info(">> constructor()");
            this.state = {
                now: new Date(),
            };
        }
    
        render() {
            console.info(">> render()");
    
            return (
                <View style={styles.container}>
                    <Text style={styles.clock}>
                        {this.state.now.toLocaleTimeString()}
                    </Text>
                </View>
            );
        }
    
        componentDidMount() {
           console.info(">> componentDidMount()");
    
           this.timerId = setInterval(() => this.tick(), 1000 * 1);
        }
    
        tick() {
            console.info(">> tick()");
            this.setState({
                now: new Date(),
            });
        }
    
        componentDidUpdate() {
            console.info(">> componentDidUpdate()");
        }
    
        componentWillUnmount() {
            console.info(">> componentWillUnmount()");
            clearInterval(this.timerId);
        }
    }
    
    const styles = StyleSheet.create({
        container: {
            flex: 1,
            backgroundColor: "#fff",
            alignItems: "center",
            justifyContent: "center",
        },
        clock: {
            fontSize: 40,
        },
    });

Component Lifecycle in Functional Components

  • (slide) Situácia v prípade reprezentácie komponentu pomocou funkcie je však iná. Tu totiž všetky uvedené funkcie nahradíme pomocou jedného hook-u s názvom useEffect()

    import { useEffect } from "react";
    
    useEffect(function() {
        // code to run
    });
  • V závislosti od zápisu tohto hook-u dôjde k použitiu, ako sme videli v prípade komponentov reprezentovaných ako triedy. Na jednotlivé možnosti sa pozrieme bližšie.

Run Once

  • (slide) Tento spôsob použitia je podobný použitiu metódy componentDidMount()

  • Funkcia dostane v tomto prípade prázdny zoznam ako druhý parameter:

    useEffect(function(){
      // code to run
    }, []);

Run on Props Change

  • (slide) Tento spôsob použitia je podobný použitiu metódy componentDidUpdate()

  • Komponent dostane v tomto prípade ako parameter props. Tie sa stanú parametrom funkcie useEffect(). Samozrejme - nemusí sa jednať len o jeden z nich, ale je možné ich vymenovať viac.

  • Použitie hook-u je nasledovné:

    function Component({someProp}){
        useEffect(function(){
            // code to run
        }, [someProp]);
    }

Run on State Change

  • (slide) Tento spôsob použitia je podobný použitiu metódy componentDidUpdate()

  • Parametrom hook-u je v tomto prípade premenná reprezentujúca stav. Pri jej zmene je zavolaný kód funkcie. Samozrejme - nemusí sa jednať len o jeden z nich, ale je možné ich vymenovať viac.

  • Použitie hook-u je nasledovné:

    function Component(){
        const [state, setState] = useState();
        useEffect(function(){
            // code to run
        }, [state]);
    }

Run After Every Render

  • (slide) Tento spôsob použitia je podobný použitiu metódy componentDidUpdate()

  • V tomto prípade sa hook spustí po každom aktualizovaní, resp. vykreslení komponentu.

  • Použitie hook-u je nasledovné:

    useEffect(function(){
        // code to run
    });

Run on Unmount

  • (slide) Tento spôsob použitia je podobný použitiu metódy componentWillUnmount()

  • V tomto prípade hook vráti funkciu. Jej kód sa spustí vtedy, keď dôjde k zrušeniu komponentu.

  • Použitie hook-u je nasledovné:

    useEffect(function(){
        return function(){
            // code to run
        };
    });

Clock as Functional Component Example

  • Výsledná implementácia komponentu hodín, ktoré budú reprezentované pomocou funkcie, je nasledovná:

    import React, { useState, useEffect } from "react";
    import { StyleSheet, View, Text } from "react-native";
    
    export default function App() {
        const [now, setNow] = useState(new Date());
    
        useEffect(function () {
            console.log(">> componentDidMount()");
            const intervalId = setInterval(function () {
                console.log(">> tick");
                setNow(new Date());
            }, 1000 * 1);
    
            return function () {
                console.log(">> componentWillUnmount()");
                clearInterval(intervalId);
            };
        }, []);
    
        useEffect(
            function () {
                console.log(">> componentDidUpdate()");
            },
            [now]
        );
    
        return (
            <View style={styles.container}>
                <Text style={styles.clock}>
                    {now.toLocaleTimeString()}
                </Text>
            </View>
        );
    }
    
    const styles = StyleSheet.create({
        container: {
            flex: 1,
            backgroundColor: "#fff",
            alignItems: "center",
            justifyContent: "center",
        },
        clock: {
            fontSize: 40,
        },
    });

Checking the Presence of Flashlight

  • V našom prípade budeme teda potrebovať zabezpečiť, aby sa prítomnosť blesku overila pri spustení aplikácie - pri zavedení (mount) komponentu. Takže potrebujeme, aby sa konkrétny kód vykonal len raz po spustení. Použijeme teda hook useEffect() v roli metódy componentDidMount():

    useEffect(function () {
        Torch.switchState(isOn).catch(function (e) {
            Alert.alert(
                "Missing Flashlight",
                "No camera available. Go and buy a device " +
                "with some (or two) and come back later.",
                [
                    {
                        text: "Quit",
                        onPress: function () {
                            RNExitApp.exitApp();
                        },
                    },
                ]
            );
        });
    }, []);
  • Ostatný kód z funkcie toggle(), ktorý bol doteraz zodpovedný za prepínanie stavu baterky, môžeme vyhodiť a funkcia môže zostať v pôvodnom tvare:

    const toggleState = async function () {
        ding.play();
        await Torch.switchState(!isOn);
        setIsOn(!isOn);
    };
  • Po spustení aplikácie v emulátore sa nám hneď zobrazí dialógové okno, kde po kliknutí na tlačidlo Quit sa aplikácia vypne. Ak však aplikáciu spustíme na reálnom zariadení, ktoré je bleskom vybavené, bude aplikácia pracovať správne.

Android Permissions

  • (slide) Je tu však ešte jeden problém, ktorý je potrebné vyriešiť na platforme Android. S príchodom verzie 7 sa totiž zmenili možnosti používania práv/povolení aplikáciami. Do tejto verzie sa povolenia pre aplikáciu povoľovali len raz a to pri inštalácii. Aby ste aplikáciu mohli nainštalovať, museli ste povoliť všetko. Ináč ste si aplikáciu nainštalovať nemohli.

  • Od verzie 7 sa však tento prístup zmenil a aplikáciu nainštalujete bez toho, aby ste čokoľvek povoľovali. Android totiž umožňuje zapínať a vypínať povolenia aplikácie selektívne počas jej behu. To pre nás znamená, že síce test na prítomnosť blesku nám zbehne v poriadku, ale nemôžeme si byť istý, či je prístup k blesku pre aplikáciu povolený na úrovni operačného systému.

  • Preto musíme aplikáciu aktualizovať a pri každom prístupe k blesku vo funkcii toggle() najprv overiť, či na platforme Android príslušné povolenia máme alebo nie.

Overenie platformy

  • (slides) Síce sa v rámci predmetu zameriavame na vývoj aplikácií pre systém Android, ukážeme si, ako je možné v prípade potreby overiť platformu, na ktorej je aplikácia spustená.

  • Za týmto účelom obsahuje rámec React Native triedu s názvom Platform. Táto trieda obsahuje vlastnosti, pomocou ktorých je možné o cieľovej platforme získať niekoľko informácií. Ak nás zaujíma len to, či je aplikácia spustená na platforme Android alebo iOS, môžeme použiť vlastnosť Platform.OS. V nej sa nachádza reťazec buď android alebo ios v závislosti od toho, na akej platforme je aplikácia spustená.

  • Kostra kódu s overením platformy môže vyzerať nasledovne:

    const toggleState = async function () {
        if(Platform.OS === 'android'){
            console.info('>> checking permissions first')
        }
    
        ding.play();
        await Torch.switchState(!isOn);
        setIsOn(!isOn);
    };

Checking Permissions with Module react-native-torch

  • Modul react-native-torch, ktorý používame na svietenie baterkou, túto kontrolu už obsahuje, takže sa inšpirujeme ukážkou kódu, ktorú má na stránke a jemne ju upravíme pre naše použitie:

    const toggleState = async function () {
        let cameraAllowed = true;
    
        if (Platform.OS === "android") {
            cameraAllowed = await Torch.requestCameraPermission(
                "Camera Permissions",
                "We require camera permissions to use the " +
                "torch on the back of your phone."
            );
        }
    
        if (cameraAllowed) {
            ding.play();
            await Torch.switchState(!isOn);
            setIsOn(!isOn);
        }
    };
  • Ak teraz aplikáciu spustíme prvýkrát a klikneme buď na obrázok alebo na tlačidlo, systém Android si od nás vypýta explicitne povolenie pre prístup ku kamere. Ak aplikácii povolenie nedáme, pri ďalšom kliknutí sa nás bude pýtať znova. Ak naopak povolenie aplikácii udelíme, bude aplikácia pekne svietiť.

  • Overiť, poprípade zmeniť povolenia aplikácie môžeme aj ručne v systéme Android. To môžeme zabezpečiť cez Settings > Applications > Torch > Permissions.

Checking Permissions Manualy

  • V tomto prípade sme mali k dispozícii rovno volanie, ktoré poskytovalo API daného modulu. Čo však v prípade, že takúto možnosť nemáme?

  • (slide) React Native vo svojom API obsahuje objekt PermissionsAndroid, pomocou ktorého je možné overiť ktorékoľvek povolenie systému Android. Používa na to funkciu .request(), ktorej povinným parametrom je požadované povolenie. Toto povolenie je dostupné v tomto objekte ako konštanta cez PermissionsAndroid.PERMISSIONS. Napríklad povolenie pre prístup ku kamere je dostupné ako konštanta PermissionsAndroid.PERMISSIONS.CAMERA. Komplentý zoznam povolení je možné nájsť v dokumentácii.

  • Funkcia vráti výsledok, ktorý môže byť buď

    • PermissionsAndroid.RESULTS.GRANTED - povolené
    • PermissionsAndroid.RESULTS.DENIED - zakázané
    • PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN - zakázané a už sa viac nepýtať
  • Upravíme teda implementáciu funkcie toggleState() nasledovne:

    const toggleState = async function () {
        let cameraAllowed = true;
    
        if (Platform.OS === "android") {
            const granted = await PermissionsAndroid.request(
                PermissionsAndroid.PERMISSIONS.CAMERA
            );
    
            cameraAllowed = 
                (granted === PermissionsAndroid.RESULTS.GRANTED);
        }
    
        if (cameraAllowed) {
            ding.play();
            await Torch.switchState(!isOn);
            setIsOn(!isOn);
        }
    };
  • Druhým nepovinným parametrom funkcie .request() je tzv. rationale. V prípade, že bude uvedený, tak predtým, ako sa zobrazí samotný systémový dialóg s povolením/zakázaním príslušného povolenia, sa zobrazí pomocný dialóg s dodatočnými informáciami napr. s vysvetlením použitia daného povolenia:

    const toggleState = async function () {
        let cameraAllowed = true;
    
        if (Platform.OS === "android") {
            const granted = await PermissionsAndroid.request(
                PermissionsAndroid.PERMISSIONS.CAMERA,
                {
                    title: "Torch Needs Camera Permission",
                    message:
                        "Torch app uses camera flashlight as " +
                        "torch. To make Torch work, you need " +
                        "to allow Camera Permission.",
                    buttonNeutral: "Ask Me Later",
                    buttonNegative: "Cancel",
                    buttonPositive: "OK",
                }
            );
    
            cameraAllowed = 
                granted === PermissionsAndroid.RESULTS.GRANTED;
        }
    
        if (cameraAllowed) {
            ding.play();
            await Torch.switchState(!isOn);
            setIsOn(!isOn);
        }
    };

Conclusion

  • Dnes sme sa teda pozreli na to, ako vieme ovplyvniť správanie komponentu vzhľadom na jeho životný cyklus a ako na platforme Android zabezpečiť potrebné prístupové práva v prípade špeciálnej funkcionality.

  • Nabudúce sa pozrieme na to, ako aplikácie prekladať do iných jazykov a lepšie zorganizujeme projekt.