Přejít k navigační liště

Zdroják » JavaScript » Vyvíjíme hybridní aplikace v Ionicu: První mobilní aplikace

Vyvíjíme hybridní aplikace v Ionicu: První mobilní aplikace

Dnešní třetí díl pojmeme čistě prakticky. Vytvoříme si jednoduchou mobilní aplikaci, která bude zobrazovat filmy na základě vyhledávací fráze (bude to takové malé ČSFD). Jako databázi filmů použijeme předdefinované pole objektů. V příštím díle našeho seriálu pak tuto aplikaci napojíme na webové API.

Přirovnání k ČSFD je trochu přehnané a doufám, že jste se předchozího odstavce příliš nevyděsili. Ve skutečnosti je to jen snaha o nalákání čtenářů k přečtení článku.

Aplikace samotná bude docela triviální a ukážeme si na ní základní práci s Ionicem. Pokud už máte nějaké zkušenosti s AngularJS, nemáte se čeho bát. Pokud ne, článek by měl být pochopitelný i pro lidi, kteří s Angularem zatím nepracovali. Pokud bude něco nejasného, doporučuji podívat se na podrobnější článek o AngularJS například zde na Zdrojáku nebo napsat do komentářů. Co je pro pochopení důležité, je znát základy práce s JavaScriptem a návrhový vzor Model-View-Controller.

Abychom měli lepší představu, jak bude výsledná aplikace vypadat, tak si ukážeme na začátku video z iOS simulátoru:

 

Výsledný projekt je umístěn v repozitáři na Githubu.

Angular, no fuj!

Proč bychom měli dělat Angular, když existuje React, který je mnohem více cool? Odpověď je docela jednoduchá – jediná rozumná možnost, jak používat React v mobilních aplikacích, je React Native, a ten zatím není úplně připravený na produkční nasazení. Navíc jde o úplně jiný přístup, než jaký dělá Ionic. React Native je kompilován do nativního kódu, zatímco Ionic běží ve WebView. Pro hybridní aplikace sice existují Reactí alternativy jako Reapp nebo Touchstone.js, ale žádná z nich není natolik pokročilá, aby překonala Ionic svoji komunitou, podporou meziplatformních odlišností nebo službami okolo.

Další věc, ve které se liší pohledy na vývoj aplikací mezi Ionicem a React Native, je ten, že Ionic se snaží sjednotit multiplatformní vývoj a jeho cílem je řešit automaticky odlišnosti mezi jednotlivými platformami. React Native je sjednocen na úrovni kódu, ale některé na platformě závislé věci je nutné řešit pro každou platformu zvlášť. Myslím, že do budoucna bude React Native velmi zajímavý, stejně tak jako Ionic2.

Ionic před pár dny vydal verzi 1.2, která kromě opravy mnoha chyb přináší i několik velmi zajímavých věcí – podporu platformy Windows 10 mobile, mobilního webu anebo defaultně použitý native scroll. Kromě toho se také Ionic soustředí na sjednocování syntaxe s Ionic2, který je zatím ve velmi rané fázi, stejně jako Angular2, na kterém je postaven. O Ionicu2 se budoucích článcích ještě určitě mnohokrát zmíním a možná i víc než to.

Inicializace projektu

Z prvního dílu našeho seriálu už umíme založit nový projekt pomocí příkazu: ionic start movies blank.

Záměrně si dnes založíme úplně prázdný projekt (to zajišťuje to slovíčko blank) a pojmenujeme ho movies. Vysvětlíme si na něm základní principy funkcionality v Ionic frameworku hezky polopatě. Jako první věc si v terminálu si spustíme auto-reload server pro sledování projektu v prohlížeči pomocí příkazu ionic serve. Měl by se nám automaticky otevřít prohlížeč s náhledem našeho projektu. V minulém díle jsme si také ukázali, jak přizpůsobit prohlížeč Chrome pro zobrazování mobilních aplikací, respektive mobilních webů. To se nám teď bude hodit.

Kdyby cokoliv nebylo jasné nebo něco nefungovalo, neváhejte to napsat do komentářů nebo na Twitter.

Routování

Při návrhu aplikace potřebujeme jako první vyřešit stavy aplikace. Jak je už vidět na videu, budeme chtít udělat 2 stavy aplikace:

  • Seznam filmů s vyhledávacím políčkem
  • Detail filmu s podrobnějšími informacemi

Mírně zmodifikujeme soubor www/js/app.js tak, abychom si přiřadili vytvoření angularovského modulu do proměnné app:

var app = angular.module('starter', ['ionic'])

.run(function($ionicPlatform) {
  $ionicPlatform.ready(function() {
    // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
    // for form inputs)
    if(window.cordova && window.cordova.plugins.Keyboard) {
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
    }
    if(window.StatusBar) {
      StatusBar.styleDefault();
    }
  });
})

Díky tomu budeme moci přistupovat k modulu starter i mimo tento soubor.

Pro definování stavů aplikace si potom vytvoříme samostatný soubor www/js/routes.js, který bude vypadat následovně:

app.config(function ($stateProvider, $urlRouterProvider) {
    $stateProvider
        .state('movies', {
            url: '/movies',
            templateUrl: "views/movies/movies.html",
            controller: 'MoviesCtrl'
        })
        .state('movie-detail', {
            url: '/movies/:id',
            templateUrl: "views/movies/movie-detail.html",
            controller: 'MovieDetailCtrl'
        });
    $urlRouterProvider.otherwise('/movies');
});

Je vhodné zvolit si nějakou konvenci, kterou budeme používat při tvorbě struktury projektu. Já jsem zvolil stejné řešení, jako jsem popisoval v minulém díle seriálu, protože se to bude do budoucna u větších projektů hodit.

Do funkce app.config si předáváme $stateProvider a $urlRouterProvider, pomocí nichž jsme schopni nadefinovat si vlastní stavy aplikace.

U každého stavu si zvolíme jeho jméno (v těchto případech to budou movies a movie-detail) a pak také URL, pomocí níž se do stavu dostaneme (parametr url), cestu k šabloně, kterou bude stav používat (parametr templateUrl), nakonec také název použitého controlleru (parametr controller).

V případě stavu movie-detail je nutné definovat v URL proměnnou, která bude identifikovat film, který chceme zobrazovat. Právě to řeší :id v samotné URL. Výsledná URL tvaru /movies/:id může vypadat tedy například takto:

  • /movies/123
  • /movies/kon-tiki
  • /movies/123-kon-tiki

To, jak budeme s danou proměnnou nakládat, je naprosto v naší moci. Většinou se však vyplatí používat identifikátor pouze jako číslo. V oblasti mobilních aplikací nás totiž netrápí, že URL adresa není „hezká“. Takzvané „pretty URL“ se používají při vývoji webových stránek, kdy je důležité, aby vyhledávací robot mohl získat informace o obsahu už z URL, a také, aby bylo toto URL čitelnější pro návštěvníky. V mobilních aplikaci ale není URL vidět, takže nám na tomto faktoru nezáleží.

Pomocí  $urlRouterProvider.otherwise('/movies') definuji stav, do kterého má aplikace jít, pokud současný stav aplikace (respektive její URL) neodpovídá žádnému stavu, který je definován. V tomto případě se jednoduše přesměrujeme na URL se seznamem filmů. Takže pokud v aplikaci načtu například URL /best-movies, která neexistuje, tak se budu přesměrován na /movies.

Vytvoření souborů k projektu

Nyní nás čeká vytvoření několika nových složek a souborů pro controllery a view:

  • /www/views/
  • /www/views/movies/
  • /www/views/movies/movies.html
  • /www/views/movies/movies.js
  • /www/views/movies/movie-detail.html
  • /www/views/movies/movie-detail.js

Rovnou si také dopředu vytvoříme složku a soubor pro modely:

  • /www/models/
  • /www/models/movies-service.js

Nově vytvořené javascriptové soubory (včetně souboru se stavy aplikace) je potřeba načíst ve stránce, a proto je vložíme jako klasický Javascript do index.html.

...
<head>
...
<script src="js/app.js"></script>
<script src="js/routes.js"></script>
<script src="views/movies/movies.js"></script>
<script src="views/movies/movie-detail.js"></script>
<script src="models/movies-service.js"></script>
</head>
...

O HTML soubory se starat nemusíme, jejich načítání řeší $stateProvider v www/js/routes.js.

Protože chceme mít index.html pouze jako šablonu se základním layoutem, do které se budou vkládat jednotlivé šablony odjinud, musíme ještě modifikovat samotnou základní HTML strukturu stránky:

...
<body ng-app="starter">
  <ion-nav-bar class="bar-positive">
      <ion-nav-back-button></ion-nav-back-button>
  </ion-nav-bar>
  <ion-nav-view animation="slide-left-right"></ion-nav-view>
</body>
...

Vytváření souborů, jejich propojování a inicializace je v případě větších projektů trochu nudná a časově náročná práce. Tentokrát si ji ukazujeme proto, abychom pochopili, jak aplikace funguje, ale do budoucnosti si ukážeme, jak generovat a propojovat tyto soubory rychleji, pomocí generátoru.

Zobrazení seznamu filmů a vyhledávání

Vytvoření modelu pro získávání dat

Model nám bude poskytovat data z improvizované databáze a později i z webového API. Prozatím budeme za databázi považovat pole objektů, které máme staticky uložené v proměnné moviesDb. Jednotlivé objekty (pro jednoduchost jich uvádím pouze 6) v databázi obsahují následující záznamy (struktura je přejata z omdbapi.com):

  • Title: Titulek
  • Year: Rok vydání
  • imdbID: Unikátní identifikátor na serveru imdb.com
  • Type: Typ videa, může označovat, že jde o film nebo třeba o seriál
  • Poster: URL plakátu k filmu

Do souboru www/models/movies-service.js vložíme následující kód a postupně si jej vysvětlíme:

/**
 * @description Service for getting and handling movies data from DB/API
 */
app.factory('MoviesService', function () {
    return new (function () {
        var service = this;
        service.data = {};

        /**
         * @description Improvised movie database
         */
        var moviesDb = [
            {"Title":"Inglourious Basterds","Year":"2009","imdbID":"tt0361748","Type":"movie","Poster":"http://ia.media-imdb.com/images/M/MV5BMjIzMDI4MTUzOV5BMl5BanBnXkFtZTcwNDY3NjA3Mg@@._V1_SX300.jpg"},
            {"Title":"Pulp Fiction","Year":"1994","imdbID":"tt0110912","Type":"movie","Poster":"http://ia.media-imdb.com/images/M/MV5BMTkxMTA5OTAzMl5BMl5BanBnXkFtZTgwNjA5MDc3NjE@._V1_SX300.jpg"},
            {"Title":"Gladiator","Year":"2000","imdbID":"tt0172495","Type":"movie","Poster":"http://ia.media-imdb.com/images/M/MV5BMTgwMzQzNTQ1Ml5BMl5BanBnXkFtZTgwMDY2NTYxMTE@._V1_SX300.jpg"},
            {"Title":"Batman Begins","Year":"2005","imdbID":"tt0372784","Type":"movie","Poster":"http://ia.media-imdb.com/images/M/MV5BNTM3OTc0MzM2OV5BMl5BanBnXkFtZTYwNzUwMTI3._V1_SX300.jpg"},
            {"Title":"The Shawshank Redemption","Year":"1994","imdbID":"tt0111161","Type":"movie","Poster":"http://ia.media-imdb.com/images/M/MV5BODU4MjU4NjIwNl5BMl5BanBnXkFtZTgwMDU2MjEyMDE@._V1_SX300.jpg"},
            {"Title":"Lion of the Desert","Year":"1981","imdbID":"tt0081059","Type":"movie","Poster":"http://ia.media-imdb.com/images/M/MV5BNzgxMzM0MzAxNF5BMl5BanBnXkFtZTcwOTM5NjIzMQ@@._V1_SX300.jpg"}
        ];

        /**
         * @description Get list of movies from DB
         */
        service.searchMoviesFromDatabase = function (searchString) {
            service.data.movies = [];
            angular.forEach(moviesDb, function(movie, key){
                if(movie.Title.toLowerCase().indexOf(searchString) > -1) service.data.movies.push(movie);
            })
        }
        /**
         * @description Get movie data by imdbID
         */
        service.getMovieById = function (imdbID) {
            var selectedMovie = {};
            angular.forEach(service.data.movies, function(movie){
                if(movie.imdbID == imdbID) {
                    selectedMovie = movie;
                }
            })
            return selectedMovie;
        };
    })();
});

Pomocí metody searchMoviesFromDatabase můžeme jednoduchým způsobem vyhledávat v databázi. Hloupým způsobem projdeme všechny řádky pole naší databáze pomocí angular.foreach, převedeme titulky na malá písmena a vyhledáme shody. Objekty, které mají shodu v titulku s naší vyhledávanou frází, potom přidáváme do pole výsledků service.data.movies. Není to příliš sofistikované řešení, ale pro účely naší první práce s modelem to bohatě postačí.

Metoda getMovieById slouží pro výběr konkrétního filmu, na základě parametru imdbID, který slouží pro identifikaci i na imdb.com. V našem případě metoda jen jednoduše projde pole service.data.movies a v případě shody zadaného imdbID vrátí výsledek.

Controller pro seznam filmů

V našem případě bude controller reagovat pouze na to, když uživatel klikne na vyhledávací tlačítko. Na základě toho controller získá požadovaná data z modelu a zobrazí je uživateli v šabloně.

V souboru www/views/movies/movies.js si vytvoříme následující strukturu:

/**
 * Controller for movie list view
 */
app.controller('MoviesCtrl', function ($scope, MoviesService) {
    $scope.data = MoviesService.data; //Sync data from service to controller

    /**
     * @description Search movies corresponding with searchString
     */
    $scope.search = function(searchString) {
        MoviesService.searchMoviesFromDatabase(searchString);
    }
})

Na 4. řádku si do controlleru MoviesCtrl předáváme námi vytvořený model MovieService. V MovieService máme objekt data, kde jsem si v modelu vytvořil  pole movies, kam ukládám všechny nalezené filmy ve formátu JSON. Ve finále budu mít toto pole synchronizované v $scope.data.movies.

Vytvoření HTML šablony (view) pro zobrazení seznamu

Jak by měl vypadat seznam filmů, přibližuje následující screenshot:

movies-list

V souboru views/movies/movies.html vytvoříme následující základní strukturu:

<ion-view title="Seznam filmů" id="movies">
    <ion-content>
       

    </ion-content>
</ion-view>

Zde vidíme tag <ion-view>, pomocí kterého určujeme obsah, který má být vložen do tagu <ion-nav-view>, který se nachází v souboru index.html<ion-view> má atribut title, pomocí kterého můžeme nastavit nadpis každého view v aplikaci. Kromě toho jsem zde také přidal atribut id pro jednoznačnou identifikaci view při následném stylování.

Tag <ion-content> určuje viditelnou hlavní obsahovou část ve View. Kromě <ion-content> zde lze ještě použít i další tagy, určující další konkrétní oblasti ve View. Patří mezi ně například <ion-nav-buttons>, pomocí kterého můžeme nadefinovat tlačítka v hlavičce view a podobně. S těmito tagy se seznámíme později.

Zobrazení dat ve view

View, které chceme vytvořit bychom mohli rozdělit na dvě části. Mělo by obsahovat vyhledávací pole s tlačítkemseznam vylistovaných položek s případnými chybovými hláškami v případě, kdy se nepodařilo nic najít. Do tagu <ion-content> (stále v souboru views/movies/movies.html) vložíme následující části kódu, které si ještě podrobněji vysvětlíme.

<form ng-submit="search(searchString)">
    <div class="bar item-input-inset bar-search">
        <label class="item-input-wrapper">
            <i class="icon ion-ios-search"></i>
            <input type="search" ng-model="searchString" placeholder="Vyhledat">
        </label>
        <button class="button button-clear" type="submit">
            <i class="icon ion-ios-search"></i>
        </button>
    </div>
</form>

Jako první budeme řešit formulář. V ukázce kódu vidíme tag <form>, který ohraničuje vyhledávací pole s tlačítkem lupy a pomocí atributu ng-submit se určuje, co se má s formulářem dělat po kliknutí na odesílací tlačítko. Odesílací tlačítko je pak definováno pomocí atributu type="submit". Ve formuláři je také vyhledávací pole <input type="search">, ve kterém je důležitý atribut ng-model, pomocí něhož je provedeno mapování do proměnné s názvem searchString. Ikonka lupy je definována pomocí elementu: <i class="icon ion-ios-search"></i> (Kromě ion-ios-search můžeme použít mnoho jiných tříd pro konkrétní ikonky – jejich seznam lze najít na ionicons.com)

Třídy, které elementy obsahují, pro nás nejsou příliš důležité. Pouze využívají třídy z Ionic frameworku a zajišťují, že výsledek bude vypadat dobře. Třídy rozhodně není nutné si nějak zapamatovávat, protože se dají vždy nalézt na stránce o Ionic komponentách (v některém z příších dílů se podíváme na tyto komponenty konkrétněji).

Druhou věcí, co budeme řešit je seznam filmů. Do ukázky za konec předchozího kódu (za ukončovací tag <form>) vložíme následující kód:

<ul class="list">
    <li ng-if="!data.movies" class="item text-center">Zadejte vyhledávací frázi</li>
    <li ng-if="data.movies.length == 0" class="item text-center">Nic nenalezeno</li>
    <li class="item" ng-repeat="movie in data.movies" ui-sref="movie-detail({'id': movie.imdbID})">{{movie.Title}}</li>
</ul>

Uděláme jednoduchý položkový seznam, který je díky class="list" a class="item správně nastylovaný. Tyto třídy lze opět bez problému vyčíst na stránce o Ionic komponentách. První tag <li> mi řeší případy, kdy neexistuje proměnná data.movies , a to znamená, že jsme ještě nezačali vyhledávat. Druhý tag <li> řeší případ, kdy proměnná data.movies obsahuje pole, ale je prázdné. To znamená, že databáze vrátila nulový počet výsledků. Třetí tag <li> pak slouží pro samotný výpis položek. Pomocí atributu ng-repeat jsem definoval, že se má projít každý prvek pole data.movies a jednotlivé objekty z pole mají být uložené při průchodu cyklem v proměnné movie. Jedná se vlastně o klasický foreach, který je akorát vypisován v HTML šabloně.

Zároveň uvnitř tagu vypisuji titulek (Title) z objektu movie. Pomocí dvojitých složených závorek mohu vypisovat proměnné, které se nacházejí ve $scope. Takže například {{movies}} by vypsalo celý objekt, který se v controlleru nachází v $scope.movies. V tomto případě vypsání titulku v ng-repeat cyklu pomocí {{movie.Title}} vypisovalo něco podobného, co by v Javasciptu ukazoval následující kód v konzoli (V Chrome se konzole spustí pomocí menu Zobrazit -> Vývojář -> Konzole JavaScriptu):

angular.forEach($scope.data.movies, function(movie){
    console.log($scope.movie.Title);
})

Pomocí atributu ui-sref="movie-detail({'id': movie.imdbID})" nastavuji na elementu vytvoření odkazu do stavu se jménem movie-detail (viz www/js/routes.js), a to včetně poslání atributu id ve formě JSON.

Zobrazení detailu filmu

V seznamu filmů jsme již přidali parametr ui-sref, pomocí kterého vytváříme odkaz na stránku s detailem filmu. V detailu filmu budou dostupné další informace o filmu, jako například rok vydání a obrázek.

Controller views/movies/movie-detail.js bude velmi jednoduchý:

/**
 * Controller for movie detail
 */
app.controller('MovieDetailCtrl', function ($scope, MoviesService, $stateParams) {
    if($stateParams){
        $scope.movie = MoviesService.getMovieById($stateParams.id);
    }
})

Opět si do tohoto controlleru předáváme náš model MoviesService a kromě toho i nově $stateParams. $stateParams potřebujeme kvůli tomu, že chceme získat parametr z URL, který jsme si předali z šablony views/movies/movies.html.

Pomocí metody MoviesService.getMovieById, kterou jsme si definovali v modelu, potom získáme objekt, jehož imdbID se shoduje s parametrem v naší URL. Tento objekt si ukládáme do $scope.movie, která je viditelná i z kontextu naší šablony.

Šablona pro detail filmu

movies-detail

Samotná šablona nezahrnuje žádné velké novinky, pouze zobrazíme data z objektu movie:

<ion-view title="{{movie.Title}}" id="movie-detail">
    <ion-content>
        <ul class="list">
            <li class="item item-text-wrap">
                <img ng-src="{{movie.Poster}}" width="100%" alt="">
            </li>
            <li class="item">
                Rok vydání: {{movie.Year}}
            </li>
        </ul>
    </ion-content>
</ion-view>

Spuštění aplikace

Nyní už je vše hotové a jediné, co nám chybí, je přidat platformu, na které chceme aplikaci provozovat a spustit ji. Pro iOS to provedeme v konzoli pomocí příkazů:

ionic platform add ios
ionic run ios

A pro Android:

ionic platform add android
ionic run android

V naší testovací databázi je jen několik filmů, ve kterých se dá vyhledávat, proto doporučuji zadat do vyhledávání nějaké písmeno/slovo z názvu některého z nich, například „batman“.

Na závěr

To bylo z dnešního vyčerpávajícího dílu vše. Omlouvám se všem čtenářům za dlouhé čekání na tento díl seriálu, ale byl jsem teď zaměstnán digitálně-nomádskou dovolenou na Tenerife. Jak jsem už naznačil na začátku, v příštím díle se podíváme na to, jak napojit tento projekt na reálnou filmovou databázi a kromě toho si ukážeme jak vytvořit pro aplikaci splash screen, ikonku a mnoho dalších vychytávek. Těšte se!

Komentáře

Subscribe
Upozornit na
guest
13 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
janchalupa

Ahoj/dobrý den, chápu to správně, že by takováto aplikace měla bežet i na Windows 10 (Mobile)? Děkuji

Pablo74

Zatím s tím asi nikdo nemá zkušenost, nová verze je tu krátce, nicméně vývojáři Ionicu tvrdí:

Ionic 1.2 marks the first release with a new supported platform. Today, Ionic 1.2 supports Windows 10 and Edge, which runs on the new Windows Phone 10 platform.

Vypadá to jako ANO :-)

Honza

Ahoj, zkoušel jsem tutoriál. Ale bohužel v prohlížeči se mi nezobrazuje žádný obsah. Screenshot zde. Nikde se neobjevuje žádná chyba. Díky za rady :)

Pablo74

Skoro to vypadá, že neběží služba Ionicu. V příkazovým řádku ve složce (adresáři) projektu je třeba zadat příkaz

ionic serve -l

Honza

Dík za tip, ale nepomohlo to https://ctrlv.cz/gYj6

Honza

Už to funguje. Díky :)

Ondra

Zdravím, zkouším a stejný problém jako kolega, co nenapasl, čím to bylo :) Spouštím, žádná chyba, ale prázdná stránka. Jen modrý pruh nahoře, jinak nic. Nevím jak dál a co zkoušet.

vSkiper

Velice pravdepodobne mas chybu v routeru. Formalne mas vsechno v poradku (zadna chyba), ale fakticky ti ui-router nerozumi vraci false a nic se nestane. Pro develop fazi si do .run statementu Ionic/Angular aplikace pridej tenhle kouzelny clanek a console ti pomerne presne rekne co delas spatne $rootScope.$on("$stateChangeError", console.log.bind(console));

Marek

Dekuji Vam, paradni prace, kdy se muzeme tesit na dalsi dil?

Marian Benčat

Zdravím,

hádám, že to tak děláte jen v rámci tutoriálu, ale data bych pro konkrétní view nepřiřazovat (negetoval např, pres promise v controlleru), jednak ty data pak maji tendenci doskakovat do viewčka – vypadá to blbě a na pomalejších strojích způsobuje sekání animace mezi view. Proto se ty data tahají pomocí resolve – které nastává před transitionem a samozřejmě čeká na resolve u promise.

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.