Разработка кроссплатформенного приложения с помощью Ionic Framework +19


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 человека.

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




К сожалению, не доступен сервер mySQL