image

Всем привет! Не так давно в одном из комментариев я пообещал написать вводную статью для быстрого знакомства с возможностями Ionic Framework (далее IF). Стараюсь сдерживать свои обещания. Для начала мне хотелось бы выложить список ссылок, которыми часто пользуюсь при разработке с помощью IF:




Что будем писать? Небольшое приложение, которое будет брать данные о погоде с openweathermap.org для г. Москва и отображать их. Разработка будет вестить в ОС Linux Mint для целевой платформы Android. Для компиляции под IOS необходимо иметь в наличии ноутбук или ПК фирмы Apple. Предполагается что читатель знаком с ЯП JavaScript и основами AngularJS (ну или быстро во всем разбирается).


Введение


IF — это open source SDK для разработки гибридных мобильных приложений с использованием всей мощи HTML5, CSS3 и JavaScript, прекрасный пример симбиоза Cordova и AngularJS. Приложения, созданные с его помощью, можно посмотреть на showcase.ionicframework.com. Порог вхождения невысок, но для создания серьезного приложения необходимы продвинутые знания AngularJS и особенностей работы Cordova для целевой платформы. Так давайте установим его скорее!

Установка


Этот этап достаточно прост если у вас уже есть NodeJS. Для этого наберите в консоли:

sudo npm i -g cordova ionic

У вас установлен и настроен Android SDK? Если нет, то вам поможет эта инструкция. Для разработчиков Windows есть вводное видео по установке. Также существует Vagrant сборка со всем предустановленым ПО. Будем считать что проблем у вас не возникло (а если возникли, то на форуме точно есть описание решения похожих проблем, в основном это установка и настройка Android SDK, но есть и траблы с плагинами. Также можно посмотреть на SO, там достаточно много информации по теме). Настало время созидать.

Создание проекта


Тут тоже все достаточно просто. IF предлагает на выбор 3 шаблона для приложения — blank (пустой), tabs (на основе табов) и sidemenu (с боковым меню). Достаточно интуитивно, не находите? Все что нужно, это набрать в консоли:

ionic start MSKWeather tabs

Тем самым мы говорим IF создать проект с именем MSKWeather и шаблоном tabs. Не забудьте перейти в вашу папку с проектами и выполнить эту команду там. После создания можно перейти в папку с заготовкой:

cd MSKWeather

Я сознательно использую непустой проект дабы сэкономить свое и ваше время и показать возможности фреймворка, ведь гораздо проще и быстрее выкинуть ненужное и изменять под себя чем программировать с нуля. Для быстрого старта сойдет и такой подход. Тем не менее я надеюсь, что к концу статьи у вас наступит понимание архитектуры фреймворка и вы с легкостью начнете с blank шаблона, а затем поблагодарите меня за мою лень. Итак, разберем структуру проекта IF.

Структура проекта


В корне проекта есть файл config.xml, отвечающий за основные настройки нашего приложения. К нему (а также к другим файлам) мы вернемся чуть позже, а пока перейдем в папку www (кстати я использую Sublime и уже открыл папку проекта в нем) и увидим заготовку нашего приложения. Бросается в глаза наличие index.html — это и есть связующее звено для всех компонентов будущего приложения (внезапно!). Рассмотрим что нам предоставляет фреймворк из коробки:

<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">

Крутая кодировка и запрет на масштабирование приложения пользователем. Уже подключенные стили и скрипты, а также привязка AngularJS в теге body, но самое главное:

<ion-nav-bar class="bar-stable">
  <ion-nav-back-button>
  </ion-nav-back-button>
</ion-nav-bar>
<ion-nav-view></ion-nav-view>

Есть заготовка для панели табов с кнопкой «Назад», и представление в которое будут рендериться (отрисовываться) наши шаблоны. Давайте теперь заглянем в папку templates. Интересует нас файл tabs.html в котором есть список табов для открытия внутри приложения. Давайте удалим 2 ненужных таба:

<!-- Chats Tab -->
<ion-tab title="Chats" icon-off="ion-ios-chatboxes-outline" icon-on="ion-ios-chatboxes" href="#/tab/chats">
  <ion-nav-view name="tab-chats"></ion-nav-view>
</ion-tab>
<!-- Account Tab -->
<ion-tab title="Account" icon-off="ion-ios-gear-outline" icon-on="ion-ios-gear" href="#/tab/account">
  <ion-nav-view name="tab-account"></ion-nav-view>
</ion-tab>

А в оставшемся заменим иконку, и в результате содержимое файла у вас будет следующим:

<ion-tabs class="tabs-icon-top tabs-color-active-positive">
  <ion-tab title="Status" icon-off="ion-cloud" icon-on="ion-cloud" href="#/tab/dash">
    <ion-nav-view name="tab-dash"></ion-nav-view>
  </ion-tab>
</ion-tabs>

Самое время посмотреть что мы натворили, для этого в корневой папке проекта запустите команду:

ionic serve

Если все удачно, вас перекинет в браузер на адрес http://localhost:8100/#/tab/dash где запустится live-reload web-версия будущего приложения.

Вот это да!


Как видно, изменения подхватились и у нас остался один таб с иконкой облака. Теперь нам нужно удалить tab-account.html и tab-chats.html (ибо уже не нужны), а также переименовать chat-detail.html в city-detail.html, а tab-dash.html в tab-city.html соответственно. Это будут наши шаблоны для выбора города и его детализации соответственно. А еще давайте изменим имя представления и url для доступа. Файл tabs.html теперь должен выглядеть так:

<ion-tabs class="tabs-icon-top tabs-color-active-positive">
  <ion-tab title="Status" icon-off="ion-cloud" icon-on="ion-cloud" href="#/tab/city">
    <ion-nav-view name="tab-city"></ion-nav-view>
  </ion-tab>
</ion-tabs>

Настал черед поиграться с маршрутизацией в нашем приложении.

Работа с маршрутами


Изменения, которые мы сделали с шаблонами, необходимо учесть в конфигурации маршрутов и состояний IF. Для этого в папке www/js найдите файл app.js, в нем и хранится вся эта информация.

angular.module('starter', ['ionic', 'starter.controllers', 'starter.services'])

Здесь мы наблюдаем создание главного модуля приложения и подключение к нему модуля IF, модуля с контроллерами и модуля обеспечения данными (может быть я и коряво это назвал). Нас интересует по большей части секция приложения config которая принимает 2 объекта $stateProvider и $urlRouterProvider (менеджер состояний и менеджер путей соответственно). Давайте удалим лишнее, и изменим параметры путей. Файл app.js нужно привести к такому виду:

app.js
angular.module('starter', ['ionic', 'starter.controllers', 'starter.services'])
.run(function($ionicPlatform) {
  $ionicPlatform.ready(function() {
    if (window.cordova && window.cordova.plugins && window.cordova.plugins.Keyboard) {
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
    }
    if (window.StatusBar) {
      StatusBar.styleLightContent();
    }
  });
})
.config(function($stateProvider, $urlRouterProvider) {
  $stateProvider
  .state('tab', {
    url: "/tab",
    abstract: true,
    templateUrl: "templates/tabs.html"
  })
  .state('tab.city', {  
    url: '/city',
    views: {
      'tab-city': {
        templateUrl: 'templates/tab-city.html',
        controller: 'CityCtrl'
      }
    }
  })
  .state('tab.city-detail', {
    url: '/city/:id',
    views: {
      'tab-city': {
        templateUrl: 'templates/city-detail.html',
        controller: 'CityDetailCtrl'
      }
    }
  })
  $urlRouterProvider.otherwise('/tab/city');
});


Внимательный читатель заметит что же мы изменили, а именно — настроили $stateProvider под наши маршруты, контроллеры и шаблоны. Настройка состояния тривиальна — «наименование состояния», объект настройки, в котором указывается url доступа, view с именем представления (имя которое мы поменяли у таба на прошлом шаге), путь к шаблону, и имя контроллера. На самом деле, существует много способов настройки состояния, вплоть до того, что вместо имени контроллера можно подставить саму функцию контроллера, но мы это затрагивать не будем (кстати, советую посмотреть презентазию Ionic от Andrew Joslin'a на ngEurope). Обратите внимание, что имя представления у состояний tab.city и tab.city-detail одно и то же, чтобы загружать шаблон в одно и то же (единственное) настроенное у нас представление. $urlRouterProvider.otherwise('/tab/city') предоставляет маршрутизацию по умолчанию если никакого пути не представлено (попросту редиректит приложение на tab/city). Если мы посмотрим как применились правки в браузере, то визуально ничего не должно измениться, кроме, естественно, адресной строки. Если все верно, то самое время перейти к вкусностям, а именно — к получению данных и их последующему отображению.

Получение данных


Теперь нам необходимо открыть файл controllers.js дабы провести дальнейшую настройку приложения. Как вы помните, в маршрутизаторе мы обозначили привязку состояний к контроллерам CityCtrl и CityDetailCtrl, значит в этом файле нужно их объявить:

angular.module('starter.controllers', [])
.controller('CityCtrl', function($scope) {
})
.controller('CityDetailCtrl', function($scope) {
});

А теперь откройте файл services.js и приведите его к такому виду:

services.js
angular.module('starter.services', [])
.factory('Cities', function() {
  var cities = [{
    id: 524901,
    name: 'Москва',
    desc: 'Столица нашей Родины',
    emblem: 'http://upload.wikimedia.org/wikipedia/commons/d/da/Coat_of_Arms_of_Moscow.png'
  }];
  return {
    all: function() {
      return cities;
    },
    get: function(id) {
      for (var i = 0; i < cities.length; i++) {
        if (cities[i].id === parseInt(id)) {
          return cities[i];
        }
      }
      return null;
    }
  };
});


В принципе он нам больше не понадобится, так что можно его сохранить и закрыть. В дальнейшем вся магия будет внутри controllers.js.
Вышеприведенным кодом мы создали фабрику данных, чтобы использовать ее в нашем приложении. Подключается эта фабрика очень просто, достаточно прописать ее имя 'Cities' в зависимостях контроллера, чтобы затем использовать ее методы. Напишите:

.controller('CityCtrl', function($scope, Cities) {
	$scope.cities = Cities.all();
})

Так мы получим массив городов и сохраним его в области видимости контроллера. Давайте пропишем в зависимостях у CityDetailCtrl то что будем использовать внутри него, а именно $http (для получения данных с помощью AJAX), $stateParams (для получения параметра из адресной сроки) и $ionicPopup (для сообщения об ошибке). А также пропишем запрос на получение погоды для выбранного города. В результате у нас должен получиться вот такой замечательный контроллер:

CityDetailCtrl
.controller('CityDetailCtrl', function($scope, $http, $stateParams, $ionicPopup) {
	$scope.data = {};
	$scope.id = $stateParams.id;
	$scope.showAlert = function(title, text) {
		$ionicPopup.alert({
			title: title,
			template: text
		});
	};
	$scope.refresh = function() {
		$http.get('http://api.openweathermap.org/data/2.5/forecast/daily?id='+$scope.id)
		.success(function(data, status, headers, config){
			$scope.data = data;
			$scope.$broadcast('scroll.refreshComplete');
		})
		.error(function(data, status, headers, config){
			$scope.showAlert(status, data);
			$scope.$broadcast('scroll.refreshComplete');
		});
	};
	$scope.refresh();
})


Ну вот, у нас практически все готово! Осталось подредактировать наши шаблоны и получить готовое приложение =)

Редактирование шаблонов


Откроем для начала tab-city.html. Помнится, в контроллере мы получили от фабрики в скоуп контроллера список всех городов. Давайте реализуем их списком с аватарками. Для этого пропишите в файле следующую структуру:

<ion-view view-title="Города">
  <ion-content class="padding">
    <ion-list>
      <a class="item item-avatar" ng-repeat="city in cities" href="#/tab/city/{{city.id}}">
        <img ng-src="{{city.emblem}}">
        <h2>{{city.name}}</h2>
        <p>{{city.desc}}</p>
      </a>
    </ion-list>
  </ion-content>
</ion-view>

Сохраним файл и посмотрим в браузере:

Россия - священная наша держава!


Замечательно! Теперь перейдем к city-detail.html. Данные запрашиваются за неделю, поэтому нам здесь тоже понадобится список. Чтобы понять что именно отображать, необходимо перейти по адресу api.openweathermap.org/data/2.5/forecast/daily?id=524901, и посмотреть структуру ответа сервера (мне нравится сервис jsoneditoronline.org для просмотра и форматирования).

Ответ сервера
{
  "cod": "200",
  "message": 0.0165,
  "city": {
    "id": 524901,
    "name": "Moscow",
    "coord": {
      "lon": 37.615555,
      "lat": 55.75222
    },
    "country": "RU",
    "population": 1000000
  },
  "cnt": 7,
  "list": [
    {
      "dt": 1428915600,
      "temp": {
        "day": 283.84,
        "min": 278.31,
        "max": 284.09,
        "night": 278.31,
        "eve": 283.05,
        "morn": 281.75
      },
      "pressure": 1010.15,
      "humidity": 74,
      "weather": [
        {
          "id": 802,
          "main": "Clouds",
          "description": "scattered clouds",
          "icon": "03d"
        }
      ],
      "speed": 5.02,
      "deg": 243,
      "clouds": 32
    },
    {
      "dt": 1429002000,
      "temp": {
        "day": 279.6,
        "min": 275.65,
        "max": 280.3,
        "night": 275.65,
        "eve": 279.3,
        "morn": 278.04
      },
      "pressure": 994.7,
      "humidity": 89,
      "weather": [
        {
          "id": 500,
          "main": "Rain",
          "description": "light rain",
          "icon": "10d"
        }
      ],
      "speed": 5.01,
      "deg": 200,
      "clouds": 64,
      "rain": 1.86
    },
    {
      "dt": 1429088400,
      "temp": {
        "day": 277.79,
        "min": 273.76,
        "max": 278.35,
        "night": 277.3,
        "eve": 278.35,
        "morn": 273.76
      },
      "pressure": 998.51,
      "humidity": 73,
      "weather": [
        {
          "id": 500,
          "main": "Rain",
          "description": "light rain",
          "icon": "10d"
        }
      ],
      "speed": 6.53,
      "deg": 221,
      "clouds": 76,
      "rain": 0.53
    },
    {
      "dt": 1429174800,
      "temp": {
        "day": 282.85,
        "min": 276.5,
        "max": 282.85,
        "night": 276.5,
        "eve": 278.69,
        "morn": 279.93
      },
      "pressure": 991.07,
      "humidity": 0,
      "weather": [
        {
          "id": 501,
          "main": "Rain",
          "description": "moderate rain",
          "icon": "10d"
        }
      ],
      "speed": 5.36,
      "deg": 287,
      "clouds": 47,
      "rain": 4.22
    },
    {
      "dt": 1429261200,
      "temp": {
        "day": 280.71,
        "min": 274.5,
        "max": 280.71,
        "night": 274.5,
        "eve": 277.19,
        "morn": 280.17
      },
      "pressure": 995.12,
      "humidity": 0,
      "weather": [
        {
          "id": 501,
          "main": "Rain",
          "description": "moderate rain",
          "icon": "10d"
        }
      ],
      "speed": 5.32,
      "deg": 285,
      "clouds": 65,
      "rain": 3.23
    },
    {
      "dt": 1429347600,
      "temp": {
        "day": 281.27,
        "min": 276.59,
        "max": 281.27,
        "night": 276.59,
        "eve": 278.67,
        "morn": 279.17
      },
      "pressure": 1002.53,
      "humidity": 0,
      "weather": [
        {
          "id": 500,
          "main": "Rain",
          "description": "light rain",
          "icon": "10d"
        }
      ],
      "speed": 9.51,
      "deg": 359,
      "clouds": 67,
      "rain": 0.62
    },
    {
      "dt": 1429434000,
      "temp": {
        "day": 282.04,
        "min": 278.38,
        "max": 282.04,
        "night": 279.1,
        "eve": 280.23,
        "morn": 278.38
      },
      "pressure": 1006.22,
      "humidity": 0,
      "weather": [
        {
          "id": 500,
          "main": "Rain",
          "description": "light rain",
          "icon": "10d"
        }
      ],
      "speed": 5.5,
      "deg": 311,
      "clouds": 88,
      "rain": 0.97
    }
  ]
}


Что-ж, для этих данных необходимо создать простенький шаблон, пример которого приведен ниже:

city-detail.html
<ion-view view-title="{{data.city.name}}">
  <ion-content class="padding">
    <h2>
      Город: {{data.city.name}}
    </h2>
    <p>
      Страна: {{data.city.country}}
    </p>
    <p>
      Население: {{data.city.population}} чел.
    </p>
    <p>
      Широта: {{data.city.coord.lon}}
      Долгота: {{data.city.coord.lat}}
    </p>
    <ion-list>
      <a class="item item-avatar" ng-repeat="day in data.list" href="#">
        <img ng-src="http://openweathermap.org/img/w/{{day.weather[0].icon}}.png">
        <h2>Температура: {{(day.temp.day - 273.15).toFixed(2)}} C</h2>
        <p>На дату: <span ng-bind="day.dt*1000 | date: 'dd.MM.yyyy'"></span></p>
      </a>
    </ion-list>
  </ion-content>
</ion-view>


Некогда объяснять — сохраняем, запускаем и смотрим!

Погодка радует)



А как сделать, чтобы аватарка стала не круглой? Откройте файл css/style.css и напишите:

.item-avatar>img:first-child {
	border-radius: 5%;
}

Это позволит убрать стандартный border-radius аж в 50% (для аватарки может и в самый раз, но для герба некрасиво). В этом файлике, как вы уже догадались, можно прописывать свои стили для компонентов (а также вы можете внаглую переписать стили самого IF).

Архив с кодом я конечно-же приложу к статье, так что у вас будет возможность всячески поиграться с полями. Как видите, процесс разработки прост, понятен и приятен (серъезно, надеюсь не нужно объяснять что куда в этом шаблоне?). Ну и напоследок…

Обновление данных (pull-to-refresh)


Нравится мне эта вкусняшка. Что нужно? Всего лишь добавить

<ion-refresher pulling-text="Тянем-потянем" on-refresh="refresh()">
</ion-refresher>

в предыдущий файл в самое начало тега ion-content. А также прописать

$scope.$broadcast('scroll.refreshComplete');

в обработчики success и error у $http.get запроса. Попробуйте это сделать и потянуть страничку вниз.

Компиляция и публикация приложения


На этом этапе мы можем скомпилировать и отладить наше приложение на устройствах и эмуляторах. Для просмотра приложения сразу в IOS и Android запустите:

ionic serve --lab

и увидите следующее:

It's alive!


Чтобы скомпилировать приложение и запустить в эмуляторе, нужно выполнить:

ionic platform add android (ну или ios)
ionic build android (ios)
ionic emulate android (ios)

В случае техники Apple вам нужно будет поставить ios-sym. Для деплоя смотрите инструкции к вашей целевой платформе. Например для Android необходимо будет сгенерировать ключ и подписать приложение, что неплохо описывается здесь. Как сгенерировать иконки и сплэши к платформам описано здесь

Архив с проектом

Заключение


Надеюсь у меня получилось донести основные концепции IF, а также сподвигнуть вас попробовать этот фреймворк для своих проектов. Замечания и предложения принимаются строго в ЛС, статья будет редактироваться и дополняться на основе ваших отзывов (планирую из этого поста сделать сборник best practices). Всем спасибо за внимание и удачного кодинга!
Продолжать писать про IF?

Проголосовало 45 человек. Воздержалось 4 человека.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Комментарии (12)


  1. mrded
    13.04.2015 19:50
    +3

    Если интересно посмотреть, как работает ionic на телефоне (не в эмуляторе), то можете потыкать приложение про которое я ко дню святого Патрика писал на geektimes: geektimes.ru/post/247360

    Также буду рад ответить на вопросы, касательные разработки на ionic.


  1. menvil
    13.04.2015 20:41
    +2

    По-моему отличный мануал для быстрого старта.


    1. m0sk1t Автор
      13.04.2015 20:43
      +1

      Спасибо, как раз для этого и задумывался топик)


  1. sanitar
    13.04.2015 23:26
    +1

    Какой оптимальный способ в ионике хранить локальные данные? Локалсторедж ведет себя абсолютно случайным образом, сбрасывается когда ему вздумается. А sqlite перебор для пары переменных.

    Кстати, пултурефреш на длинных листах лагает просто неприлично.


    1. xGromMx
      14.04.2015 08:33
      +1

      можете попробовать developer.mozilla.org/ru/docs/IndexedDB


    1. m0sk1t Автор
      14.04.2015 10:06
      +1

      Хранил в localStorage, он у меня не сбрасывался при обновлении приложения и при перезагрузке телефона. Можно попробовать indexedDB как предложили выше. Или вот еще способ. А тут все собрано воедино с примерами.


      1. sanitar
        14.04.2015 10:49
        +1

        Воу, на айфонах у вас тоже локалсторедж оставался даже после обновлений? Я год назад тыкался, была беда совсем.


        1. zapolnoch
          14.04.2015 11:10
          -1

          При обновлении приложения, localStorage сохраняется, а если очистить браузер (куки, формы, историю, данные), то пропадает.


          1. m0sk1t Автор
            14.04.2015 11:26

            Вы сейчас про какой браузер? InAppBrowser? Зачем очищать, если вам эти данные необходимы для корректной работы приложения?


        1. m0sk1t Автор
          14.04.2015 11:24

          На айфонах не тестил к сожалению… Но на андроид все нормально, данные в сохранности, так что думаю на IOS тоже должно быть хорошо. Понимаете, фреймворк достаточно молодой, предложения по апдейту на новую версию мне приходили пару раз в неделю во время запуска ionic serve, на гитхабе коммитов по 50-100 штук ежедневно на почту падали, баги фиксились, работа кипела. Надеюсь все не зря)


          1. RouR
            14.04.2015 12:07
            +1

            У меня приложение и под айфоном и под андроидом — всё нормально с локал стораджем.


    1. m0sk1t Автор
      14.04.2015 13:05
      +1

      Для длинных списков попробуйте collection-repeat. Его фишка в том что рендерится только видимая часть списка нежели бы мы использовали ng-repeat.