React Native i Redux – przykładowa aplikacja

 In Mobile, React Native, Technicznie

Facebook, Instagram, Skype, Uber, Pinterest… Co łączy wszystkie te aplikacje poza tym, że królują na listach najpopularniejszych aplikacji mobilnych? Wszystkie są napisane z wykorzystaniem React Native. Wydaje się to być wystarczającą rekomendacją do zainteresowania się tą technologią oraz przeczytania tego wpisu.

LEARN ONCE, WRITE EVERYWHERE!

Naucz się raz, pisz wszędzie – jest to główna idea przyświecająca twórcom tej zdobywającej coraz większą popularność biblioteki. Programiści facebooka – bo o nich mowa – na oficjalnej stronie projektu wymieniają 4 główne założenia React Native, których streszczenie znajduje się poniżej:

  • Budowanie mobilnych aplikacji na podstawie JavaScript oraz Reacta.
    • Logikę aplikacji można napisać wyłącznie używając języka JavaScript.
    • React Native pozwala budować, podobnie jak w React’cie,  interfejsy użytkownika w oparciu o komponenty.
  • Aplikacja stworzona przy pomocy React Native jest “prawdziwą” aplikacją mobilną. Nie mamy tu do czynienia z aplikacjami typu “mobile web app”, “HTML5 app” czy aplikacjami hybrydowymi.
  • Tworzenie aplikacji przy wykorzystaniu React Native jest szybsze, dzięki braku konieczności ponownego kompilowania kodu. Aby wprowadzić zmiany w widoku wystarczy przeładować aplikację. Dodatkowo dzięki funkcji hot reloading istnieje możliwość aktualizowania widoku bez konieczności odświeżania przy zachowaniu bieżącego stanu aplikacji.
  • W razie konieczności zoptymalizowania niektórych aspektów aplikacji możliwe jest tworzenie komponentów wykorzystując natywny kod (Objective-C, Java, Swift).

REDUX

Jedną z ważniejszych bibliotek użytych w poradniku zdecydowanie jest Redux. Służy ona do zarządzania stanem całej aplikacji w jednym obiekcie nazywanym Storem. Podstawowe koncepty dotyczące Reduxa:

  • Cały stan aplikacji przechowywany jest w drzewie w jednym Storze.
  • Stan aplikacji jest tylko do odczytu. Wszystkie zmiany odbywają się poprzez akcje.
  • Aby zdefiniować jak akcja wpłynie na stan aplikacji, należy napisać reduktor (reducer).

Tworzenie Stora

Pierwszym krokiem przy dodawaniu Redux’a do aplikacji jest inicjalizacja Stora poprzez funkcję:

createStore(reducer , [initialState], [enhancer])

gdzie:

  • reducer – w przypadku kilku reducerów – RootReducer,
  • initialState – początkowy stan aplikacji,
  • enhancers – dodatki, dzięki którym możemy skorzystać z dodatkowych funkcjonalności Stora oraz narzędzi debugujących.

Akcje

Do zmiany stanu aplikacji wymagane są dwie czynności:

  • Musimy wyemitować akcję.
  • Musi istnieć reducer obsługujący wywołaną akcję i określający zmianę stanu.

Akcje (actions) są to JavaScriptowe obiekty. Użycie akcji to jedyny sposób, by zmienić stan aplikacji. Akcja musi posiadać typ (type). Opcjonalnie mogą posiadać dodatkowe właściwości, zazwyczaj zwane payloadem.

export const testAction = () => ({
  type: 'EXAMPLE_ACTION',
});

Typy akcji

Dobrą praktyką, aby zredukować prawdopodobieństwo napisania akcji o takim samym, istniejącym już typie lub zmniejszyć ryzyko literówek, jest tworzenie typów akcji (action types).

export const EXAMPLE_ACTION = 'EXAMPLE_ACTION';

export const testAction = () => ({
  type: EXAMPLE_ACTION,
});

Typy akcji powinny być zdefiniowane jako stałe łańcuchowe. Najprościej mówiąc definiują typ akcji.

Reduktor

Reduktor (reducer) to JS’owa funkcja, która przyjmuje dwa parametry: stan aplikacji oraz akcję i na tej podstawie generuje nowy, zaktualizowany stan.

Pisząc reduktory należy pamiętać o zasadzie pure function – te same parametry wejściowe powinny zwracać zawsze ten sam wynik.

import { EXAMPLE_ACTION } from ... 

switch (action.type) {
   case EXAMPLE_ACTION:
     return {
       ...state,
       isWorking: true,
     };
};

W reduktorze importujemy wcześniej zdefiniowany typ akcji.

INTEGRACJA REACT Z REDUXEM

Ostatnim krokiem do wykorzystywania Redux’a i jego możliwości jest integracja go z React’em poprzez komponent Provider z biblioteki ‘react-redux’.

<Provider store={store}>
  <App />
</Provider>

Do komponentu przekazywany jest wcześniej utworzony Store.

Aby móc odczytywać i zmieniać stan aplikacji z poziomu komponentu należy go połączyć z Redux’em funkcją connect() dostępną w ramach wcześniej wspomnianej biblioteki ‘react-redux’.

connect(mapStateToProps, mapDispatchToProps)(Component)

gdzie:

  • mapStateToProps – jest funkcją która na wejściu przyjmuje aktualny stan aplikacji i zwraca obiekt, który później można wykorzystać jako propsy.
  • mapDispatchToProps – przyjmuje dispatch() jako parametr i zwraca callback props, które możemy wstrzyknąć do komponentu.

TWORZENIE PROJEKTU

W tym artykule położono nacisk na stworzenie pełnoprawnej, działającej aplikacji. Skrócono opis konfiguracji środowiska oraz tworzenia samego projektu do minimum, aby skupić się bardziej na tworzonym kodzie. Punktem wyjściowym w pisanym poradniku jest nowy projekt stworzony za pomocą  wcześniej zainstalowanego ,przy pomocy npm, narzędzia linii poleceń create-react-native-app. Narzędzie to pozwala skrócić czas potrzebny na zbudowanie aplikacji do minimum i od razu przejść do tworzenia kodu. Jeśli chcielibyście dowiedzieć się co ukryte jest pod wspomnianą komendą zajrzyjcie koniecznie do tego wpisuSzczegółowy opis jak skonfigurować środowisko i wystartować swoją aplikację dostępny jest na stronie projektu.

STRUKTURA PROJEKTU

Na potrzeby wpisu tworzona jest aplikacja jednoekranowa – kuchenny timer. Jej głównym zadaniem jest odliczanie czasu do końca ugotowania jajka w zależności od wybranego sposobu (na miękko, pół twardo, twardo).

Struktura projektu została stworzona tak, aby można było w łatwy i uporządkowany sposób dodawać nowe funkcjonalności, ekrany. Jest to struktura skalowalna, niekoniecznie najlepsza pod małe, jednoekranowe projekty.

Opis folderów:

  • .expo – tu znajdują się pliki konfiguracyjne wygenerowane przy tworzeniu aplikacji z wykorzystaniem expo CLI,
  • assets – pliki multimedialne, czcionki,
  • components – reactowe komponenty podzielne na podfoldery. Jeden podfolder dla jednego ekranu. Komponenty odpowiedzialne są za wyświetlanie elementów na ekranie,
  • config –  dodatkowe pliki konfiguracyjne, plik ze stałymi,
  • containers – reduxowe kontenery. Podobnie jak w komponentach – jeden kontener dla jednego ekranu. Podłączone są do Redux’a oraz zawiera się w nich logika prezentacji,
  • modules – reduxowe moduły zawierające akcje, typy akcji i reduktory podzielone według logiki aplikacji,
  • App.js – plik startowy aplikacji stworzonej przy pomocy expo,
  • inne pliki konfiguracyjne.

WIDOKI

Tworzenie widoków w aplikacji React Native odbywa się przy wykorzystaniu JSX, czyli rozszerzenia języka JavaScript, wyglądającego jak XML. Przykładowy kod zapisany w tym języku wygląda następująco:

<Text style={styles.text}>Wybierz sposób ugotowania jajka:</Text>

gdzie:

  • Text – wskazuję na komponent z biblioteki React Native, którego używamy,
  • style – jest właściwością, a jakże, stylu,
  • Wybierz… – jest treścią która ma się wyświetlić.

Tworzenie widoków w React Native jest bardzo zbliżone do komponowania stron internetowych. Z tym że zamiast znaczników div, p używamy View, Text itd. Kolejną różnicą jest sposób stylowania. Zamiast używać webowych klas, używamy właściwości style, w której przekazujemy obiekt, bądź tablicę składającą się z kilku obiektów, np.

<Ionicons style={[styles.title, styles.icon]} name="ios-egg-outline" />

Można również stosować stylowanie w linii (inline styling), ale nie jest to zalecane ze względu na czytelność kodu.

Style tworzymy poprzez metodę create() klasy StyleSheet.

export const styles = StyleSheet.create({
title: {
  fontSize: 60,
  paddingVertical: 30,
},

{...}

});

Dostępne właściwości stylów nasuwają od razu skojarzenie do Kaskadowych Arkuszy Stylów (CSS). Różnica polega w zapisie nazwy stylu oraz na niedostępności niektórych właściwości.

Każdy komponent ma inny zestaw dostępnych sposobów zmiany wyglądu. Wszystkie dostępne style komponentów oraz opis każdego z nich znajdziecie na świetnym repozytorium użytkownika Vahe Hovhannisyan

Opisywana aplikacja zawiera tylko jeden ekran, którego strukturę można podzielić na 4 segmenty:

  • Tytuł,
  • Przyciski wyboru sposobu ugotowania jajka,
  • Przycisk startu/stopu,
  • Indykator pozostałego czasu.

Kontener widoku, gdzie określamy styl ekranu (kolor tła, rozkład elementów, itp.)

<View style={styles.container}>
  {...}
</View>

Sekcja tytułu jest niczym innym jak czterema elementami składającymi się na logo aplikacji. Są to dwie ikony oraz dwa teksty ułożone w jednym rzędzie.

<View style={styles.row}>
  <Text style={styles.title}>EGG C</Text>
  <Ionicons style={[styles.title, styles.icon]} name="ios-egg-outline" />
  <Ionicons style={styles.title} name="ios-egg-outline" />
  <Text style={styles.title}>K</Text>
</View>

Aby uzyskać trzy przyciski o tym samym wyglądzie i zastosowaniu użyto funkcji map(). Aby umożliwić interakcję użytkownika z aplikacją poprzez dotyk należy skorzystać z  wrappera TouchableOpacity posiadającego między innymi funkcję onPress().

<Text style={styles.text}>Wybierz sposób ugotowania jajka:</Text>
  <View style={{ flexDirection: 'row' }}>
    {BOIL_TYPES.map(t => (
      <TouchableOpacity
        key={t.name}
        disabled={isOn}
        style={[styles.typeButton, boilType.name === t.name && styles.typeButtonSelected, isOn && styles.disabled]}
        onPress={() => this.handleSelectBoilType(t)}
      >
        <Text style={styles.text}>{t.name}</Text>
      </TouchableOpacity>
      ))}
  </View>

Do przycisku startu/stopu przypisano jedną funkcję, która w zależności od stanu aplikacji wywołuje inną akcję. Również etykieta się zmienia wraz ze stanem aplikacji.

<TouchableOpacity onPress={this.handleStartStopPress} style={styles.button}>
  <Text style={styles.text}>{isOn ? 'Stop' : 'Start'}</Text>
</TouchableOpacity>

Wyświetlanie statusu ugotowania jajka  zrealizowano za pomocą renderowania warunkowego (conditional rendering) z wykorzystaniem operatora &&.

{!eggBoiled && <Text style={styles.text}>Pozostały czas:</Text>}
{!eggBoiled && <Text style={styles.timer}>{`${minutes}:${seconds}`}</Text>}
{eggBoiled && <Text style={[styles.text]}> Twoje jajko jest gotowe! </Text>}

Pełny kod widoków i styli przykładowej aplikacji dostępny jest w załączonym repozytorium.

LOGIKA APLIKACJI

Na wstępie trzeba zaznaczyć, że Redux w naszej aplikacji jest tzw. overkillem, ale używamy go na potrzeby poradnika. Stworzono tylko 1 reduktor i 5 akcji.

Proces tworzenia potrzebnych elementów do korzystania z Reduxa omówione zostało w jednym z poprzednich rozdziałów.

W drzewie stanu przechowujemy następujące informacje:

  • isOn – czy timer jest aktualnie włączony,
  • boilType – sposób gotowania jajka,
  • timeLeft – pozostała ilość czasu,
  • startedTime – czas rozpoczęcia,
  • eggBoiled – definiuje czy czas dobiegł do końca.

Logiki związanej z Redux’em w poradniku jest bardzo niewiele.

Tak naprawdę użytkownik może wyemitować 3 akcje:  startTimer(), selectBoilType() oraz stopTimer() jeśli chcemy zatrzymać timer przed upływem czasu, inne akcje zależne są od stanu np.

timerTick() emitowana jest co sekundę po ruszeniu czasu (zależna jest od stanu isOn).

Prześledźmy przykładowe zachowanie aplikacji na podstawie akcji selectBoilType().

Klikając w przycisk symbolizujący sposób ugotowania jajka emitowana jest akcja z argumentem określającym wybrany sposób.

export const selectBoilType = type => ({
  type: actionTypes.SELECT_BOIL_TYPE,
  payload: type,
});

Akcja ta zwraca obiekt z dwoma właściwościami:

  • type – wcześniej zdefinowana nazwa akcji,
    export const SELECT_BOIL_TYPE = `${rootElement}/SELECT_BOIL_TYPE`;
  • payload – dane przekazywane do reduktora, w tym wypadku obiekt definiujący sposób gotowania jajka.

Następnie wywoływany jest reduktor, który zmienia na podstawie typu i payload’u zmienia stan aplikacji.

export default function(state = initialState, action) {
  switch (action.type) {
    case actionTypes.SELECT_BOIL_TYPE:
      return {
        ...state,
        boilType: action.payload,
        timeLeft: action.payload.timeInSeconds,
        eggBoiled: false,
      };
  }
}

Reduktor zmienił stan aplikacji który wpłynął na wyświetlany widok, np. zmienił styl przycisku odpowiadającemu wybranemu sposobu ugotowania jajka.

PODSUMOWANIE

Tworzenie aplikacji w React Native wydaje się być dosyć łatwe, intuicyjne i przyjemne, szczególnie programistom mającym za sobą już pierwsze projekty w React’cie. Polecenie create-react-native-app pozwala praktycznie od razu zacząć przygodę z tą nową technologią. Duża społeczność, wsparcie Facebooka, mnogość bibliotek również przemawiają za tą technologią. Przedstawiona aplikacja można traktować jako punkt wyjściowy do większej aplikacji ze względu na zastosowaną strukturę. Możecie spodziewać się kolejnych wpisów na blogu dotyczącej React Native oraz przedstawionej aplikacji, ponieważ w planach dodanie jest kolejnych funkcjonalności.

Odnośniki

Autorzy

Recent Posts