Создайте приложение Weather с помощью TypeScript и NativeScript

14 января 2018

В этом уроке я покажу вам, как создать приложение для погоды в NativeScript с использованием языка TypeScript.

В предыдущей статье этой серии мы создали приложение для заметок, использующее простой JavaScript. На этот раз мы будем использовать TypeScript. Сначала давайте выясним, почему TypeScript является хорошим вариантом для создания приложений NativeScript.

1. Почему TypeScript?

TypeScript является первоклассным гражданином в NativeScript. Он используется основной командой NativeScript для создания самой структуры NativeScript. Вот несколько причин, по которым вы хотели бы использовать TypeScript для разработки приложений NativeScript:

TypeScript компилируется в JavaScript. Когда компилятор работает, он ловит любые ошибки, которые могут возникнуть в вашем коде, чтобы вы могли действовать немедленно, не дожидаясь завершения компилятора NativeScript. Это означает большую производительность для вас как разработчика. TypeScript позволяет использовать ES6-функции, такие как классы, модули, функции стрелок, литералы шаблонов и многое другое. Это означает, что в вашем распоряжении больше инструментов для организации и написания лучшего кода.

Если бы я плохо работал над тем, чтобы убедить вас, или вы хотите узнать больше о том, почему TypeScript хорошо подходит для разработки с помощью NativeScript, вы можете проверить, что Build Better NativeScript Apps имеет TypeScript.

2. Инструмент

Чтобы полностью использовать функции, предлагаемые TypeScript, я рекомендую использовать текстовый редактор кода Visual Studio. Он имеет функцию IntelliSense, которая обеспечивает интеллектуальное автоматическое завершение, когда вы пишете код TypeScript, он интегрируется с Git, а также имеет возможности отладки.

Лучше всего, есть также плагин NativeScript, который сделает вас более продуктивным при разработке приложений NativeScript. Одна из функций, которую я считаю полезной, - это интеграция эмулятора. Это позволяет запускать эмулятор непосредственно из текстового редактора и отлаживать приложение из самого текстового редактора. Код Visual Studio является бесплатным и доступен на всех основных платформах (Windows, Linux, OS X).

Однако, если вы не хотите оставлять удобство текстового редактора, вы также можете установить расширения, которые улучшат кодирование с помощью TypeScript. Для Atom есть плагин Atom-typescript. Для Sublime Text есть плагин TypeScript Sublime.

3. Обзор приложения

Приложение, которое мы собираемся создать, - это приложение для погоды. Он будет иметь следующие страницы:

Главная страница, показывающая текущую погоду, а также некоторую релевантную информацию, такую ​​как температура, давление воздуха и влажность. Страница прогноза, которая показывает пятидневный прогноз того, какая погода будет в течение следующих пяти дней.

Вот как будет выглядеть главная страница:

И вот страница прогноза:

Вы можете найти завершенный исходный код для этого приложения в своем репозитории GitHub.

4. OpenWeatherMap

Данные о погоде будут поступать из API OpenWeatherMap, и, как и любой другой API, вам необходимо подписать ключ API для его использования. Иди вперед и зарегистрируйся для учетной записи, я подожду. После входа в систему перейдите на страницу ключей API, скопируйте значение поля ключа и сохраните его в безопасном месте. Это понадобится вам позже, как только вы начнете создавать приложение.

5. Создание приложения

Теперь, когда вы знаете, как будет выглядеть приложение, пришло время начать его создавать. Начните с создания нового проекта NativeScript, который использует шаблон TypeScript:

tns create weatherApp --template typescript

После этого перейдите в папку приложения и создайте следующие папки и файлы. Для вашего удобства вы также можете загрузить или клонировать репозиторий GitHub и скопировать файлы из папки приложения.

- commo
    + constants.ts
    + navigation.ts
    + requestor.ts
    + utilities.ts
- fonts
- pages
    + forecast
        * forecast.css
        * forecast.ts
        * forecast.xml
        * forecast-view-model.ts
    + mai
        * main.css
        * main.ts
        * main.xml
        * main-view-model.ts
- stores
    + location.ts
- app.css
- app.ts

Мы будем работать только в каталоге приложения, поэтому каждый раз, когда я ссылаюсь на путь к файлу или папку, предположим, что каталог приложения является корневым.

Установка зависимостей

Для приложения требуется несколько зависимостей: модуль NationScript Geolocation и момент. Вы можете установить модуль геолокации с помощью следующей команды:

tns plugin add nativescript-geolocatio

И установите Moment с:

pm install moment

Модуль Geolocation используется для определения текущего местоположения пользователя. Moment позволяет легко форматировать временные метки Unix, которые мы будем получать от API позже.

Общие модули

Прежде чем мы рассмотрим код для каждой из страниц приложения, давайте сначала рассмотрим настраиваемые модули, которые будут использоваться во всем приложении.

Константы

Модуль констант (common constants.ts) содержит все константные значения, используемые во всем приложении: такие вещи, как базовый URL-адрес API OpenWeatherMap, ключ API, который вы получили ранее, пути к конечным точкам, которые мы будем использовать использовать коды символов значков погоды и направления ветра.

export const WEATHER_URL = 'http://api.openweathermap.org/data/2.5/';
export const WEATHER_APIKEY = 'YOUR OPENWEATHERMAP API KEY';
export const CURRENT_WEATHER_PATH = 'weather/';
export const WEATHER_FORECAST_PATH = 'forecast/daily/';
export const WEATHER_ICONS = {
  day: {
    'clear': 0xf00d,
    'clouds': 0xf002,
    'drizzle': 0xf009,
    'rain': 0xf008,
    'thunderstorm': 0x010,
    'snow': 0xf00a,
    'mist': 0xf0b6
  },
  night: {
    'clear': 0xf02e,
    'clouds': 0xf086,
    'drizzle': 0xf029,
    'rain': 0xf028,
    'thunderstorm': 0xf02d,
    'snow': 0xf02a,
    'mist': 0xf04a
  },
  neutral: {
    'temperature': 0xf055,
    'wind': 0xf050,
    'cloud': 0xf041,
    'pressure': 0xf079,
    'humidity': 0xf07a,
    'rain': 0xf019,
    'sunrise': 0xf046,
    'sunset': 0xf052
  }
};
export const WIND_DIRECTIONS = [
  "North", "North-northeast", "Northeast",
  "East-northeast", "East", "East-southeast", "Southeast",
  "South-southeast", "South", "South-southwest", "Southwest",
  "West-southwest", "West", "West-northwest", "Northwest", "North-northwest"
];

Утилиты

Модуль утилит включает в себя всевозможные служебные функции: такие вещи, как преобразование степеней в направления, определение описательного текста скорости ветра, преобразование Кельвина в Цельсию и преобразование кодов символов в символ. Вы увидите, как все эти функции будут использоваться позже на страницах.

import constants = require('./constants');
export function degreeToDirection(num) {
  var val= Math.floor((num / 22.5) + .5);
  return constants.WIND_DIRECTIONS[(val % 16)];
}
export function describeWindSpeed(speed) {
  if(speed < 0.3) {
    return 'calm';
  } else if(speed >= 0.3 &&speed < 1.6) {
    return 'light air';
  } else if (speed >= 1.6 &&speed < 3.4) {
    return 'light breeze';
  } else if (speed >= 3.4 &&speed < 5.5) {
    return 'gentle breeze';
  } else if (speed >= 5.5 &&speed < 8) {
    return 'moderate breeze';
  } else if(speed >= 8 &&speed < 10.8) {
    return 'fresh breeze';
  } else if(speed >= 10.8 &&speed < 13.9) {
    return 'strong breeze';
  } else if(speed >= 13.9 &&speed < 17.2) {
    return 'moderate gale';
  } else if (speed >= 17.2 &&speed < 20.8) {
    return 'gale';
  } else if (speed >= 20.8 &&speed < 24.5) {
    return 'strong gale';
  } else if (speed >= 24.5 &&speed < 28.5) {
    return 'storm';
  } else if (speed >= 28.5 &&speed < 32.7) {
    return 'violent storm';
  } else if (speed >= 32.7 &&speed < 42) {
    return 'hurricane force';
  }
  return 'super typhoon';
}
export function describeHumidity(humidity) {
  if (humidity >= 0 &&humidity <= 40) {
    return 'very dry';
  } else if (humidity >= 40 &&humidity <= 70) {
    return 'dry';
  } else if (humidity >= 85 &&humidity <= 95) {
    return 'humid';
  }
  return 'very humid';
}
export function describeTemperature(temp) {
  var celsius = convertKelvinToCelsius(temp);
  if (celsius >= 0 &&celsius < 7) {
    return 'very cold';
  } else if (celsius >= 8 &&celsius < 13) {
    return 'cold';
  } else if (celsius >= 13 &&celsius < 18) {
    return 'cool';
  } else if (celsius >= 18 &&celsius < 23) {
    return 'mild';
  } else if (celsius >= 23 &&celsius < 28) {
    return 'warm';
  } else if (celsius >= 28 &&celsius < 32) {
    return 'hot';
  }
  return 'very hot';
}
export function convertKelvinToCelsius(celsius) {
  return celsius - 273.15;
}
export function getTimeOfDay() {
  var hour = (new Date()).getHours();
  var time_of_day = 'night';
  if(hour >= 5 &&hour <= 18){
    time_of_day = 'day';
  }
  return time_of_day;
}
export function getIcons(icon_names) {
  var icons = icon_names.map((name) => {
    return {
      'name': name,
      'icon': String.fromCharCode(constants.WEATHER_ICONS.neutral[name])
    };
  });
  return icons;
}

Навигация

Модуль навигации - это специальный вспомогательный модуль, который позволяет нам легко перемещаться между всеми страницами приложения. Откройте общий файл navigation.ts и добавьте следующее:

import frame = require('ui/frame');
export function getStartPage() {
  return 'pages/main/main';
}
export function goToForecastPage() {
  frame.topmost().navigate('pages/forecast/forecast');
}
export function goToMainPage() {
  frame.topmost().goBack();
}

Это использует модуль Frame для перехода на другие страницы приложения. Метод getStartPage () просто возвращает местоположение главной страницы приложения. GoToForecastPage (), как следует из названия, позволяет пользователю перейти на страницу прогноза.

При навигации по NativeScript вам необходимо указать, где вы находитесь. Вот почему вам сначала нужно вызвать функцию topmost (), чтобы получить текущую или самую верхнюю страницу, а затем функцию navigate () перейти на другую страницу. Эта функция принимает путь к странице, куда вы хотите перейти.

Requestor

Модуль Requestor выполняет фактический запрос API OpenWeatherMap. Как упоминалось в статье «Введение в NativeScript», NativeScript использует виртуальную машину JavaScript для запуска кода JavaScript. Это означает, что мы также можем использовать функции, доступные в браузере.

Одной из таких функций является выборка, которая позволяет нам делать HTTP-запросы на удаленный сервер. Параметр - это URL-адрес, где вы хотите сделать запрос. Он возвращает обещание, поэтому мы используем then (), чтобы ждать сырой ответ. Обратите внимание на использование слова «raw»; функция fetch возвращает ответ с заголовками и другой низкоуровневой информацией, поэтому нам нужно вызвать функцию json (), чтобы получить фактические данные JSON. Это вернет другое обещание, поэтому мы будем использовать then () еще раз, чтобы получить фактический объект.

export function get(url){
  return fetch(
    url
  ).then(function(response){
    return response.json();
  }).then(function(json){
    return json;
  });
}

Кроме того, вы можете использовать модуль Http, который является более надежным способом создания HTTP-запросов в NativeScript.

Место хранения

Хранилище местоположений служит хранилищем для информации о местоположении. Это позволяет нам обновлять и получать текущее местоположение из любого файла, который импортирует этот модуль.

export var location;
export function saveLocation(loc) {
  location = loc;
}
export function getLocation() {
  return location;
}

Главная страница

Теперь пришло время взглянуть на код для каждой из страниц приложения. Но прежде чем мы это сделаем, сначала откройте файл точки входа (app.ts). Это использует навигационный модуль, чтобы получить стартовую страницу приложения:

import application = require("application");
import navigation = require('./common/navigation');
application.mainModule = navigation.getStartPage();
application.start();

Далее, давайте разложим основной файл main.xml.

Событие navigatingTo используется для выполнения аналогично названной функции в файле TypeScript каждый раз, когда пользователь переходит к этой конкретной странице. Класс CSS также динамически определяется из файла TypeScript.

<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" class="{{ background_class }}">
...
</Page>

Компонент ScrollView используется для обертывания всего, чтобы вертикальная полоса прокрутки автоматически генерировалась, когда содержимое выходит за пределы того, что может отображаться на экране.

И поскольку мы собираемся загрузить данные с удаленного сервера, компонент ActivityIndicator используется для отображения загрузочной анимации по умолчанию платформы. Это требует, чтобы вы предоставили атрибут busy, который принимает логическое значение, определяющее, запускать ли анимацию или нет. По умолчанию это значение равно true и обновляется только до значения false, как только приложение будет завершено, чтобы сделать запрос на сервер.

Атрибут видимости также используется, чтобы убедиться, что компонент не потребляет какое-либо пространство, пока оно не анимируется.

<StackLayout>
  <ScrollView>
    <ActivityIndicator busy="{{ is_loading }}" visibility="{{ is_loading ? 'visible' : 'collapsed' }}" />
    <StackLayout visibility="{{ !is_loading ? 'visible' : 'collapsed' }}">
      ...
    </StackLayout>
  </ScrollView>
</StackLayout>

Для основного контента мы имеем общий обзор текущей погоды вверху и подробностей ниже. Общий обзор показывает значок, представляющий текущую погоду, текущую температуру, описание погоды и место.

<Label text="{{ icon }}" class="icon" />
<Label text="{{ temperature }}" class="temperature" />
<Label text="{{ weather }}" class="weather" textWrap="true"/>
<Label text="{{ place }}" class="place" textWrap="true"/>

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

На скриншоте, который я показал вам ранее, вы видели, что он использует двухстолбцовый макет для обеих страниц. Вот почему мы используем GridLayout. Но, как видно из приведенного ниже кода, мы также используем GridLayout для первого столбца каждой строки.

Вы можете спросить, почему мы делаем это, а не просто создаем трехколоночный макет со значком в первом столбце, атрибутом погоды на втором и значением на третьем. Это совершенно понятно, и это сделает код более кратким.

Но проблема в том, что NativeScript версии 2.1 в настоящее время не разрешает процентные единицы для своего GridLayout. Это означает, что мы не можем использовать что-то вроде 10% для значка, в то время как остальные два столбца потребляют 45%.

Макет, который мы использовали ниже, работает вокруг этой проблемы, используя GridLayout для обертывания значка и атрибута погоды, причем значок потребляет 30 пикселей, а атрибут погоды потребляет объем пространства, необходимый для размещения текста. Обратите внимание на использование атрибута row и col на GridLayout.

<GridLayout columns="*,*" rows="auto,auto,auto,auto,auto,auto,auto" cssClass="details">
  <GridLayout columns="30,auto" rows="auto" row="0" col="0">
    <Label text="{{ wind_icon }}" class="small-icon" row="0" col="0" />
    <Label text="Wind" textWrap="true" row="0" col="1" class="label" />
  </GridLayout>
  <Label text="{{ wind }}" textWrap="true" row="0" col="1" />
  <GridLayout columns="30,auto" rows="auto" row="1" col="0">
    <Label text="{{ cloud_icon }}" class="small-icon" row="0" col="0" />
    <Label text="Cloudiness" textWrap="true" row="1" col="1" class="label" />
  </GridLayout>
  <Label text="{{ clouds }}" textWrap="true" row="1" col="1" />
  <GridLayout columns="30,auto" rows="auto" row="2" col="0">
    <Label text="{{ pressure_icon }}" class="small-icon" row="0" col="0" />
    <Label text="Pressure" textWrap="true" row="2" col="1" class="label" />
  </GridLayout>
  <Label text="{{ pressure }}" textWrap="true" row="2" col="1" />
  <GridLayout columns="30,auto" rows="auto" row="3" col="0">
    <Label text="{{ humidity_icon }}" class="small-icon" row="0" col="0" />
    <Label text="Humidity" textWrap="true" row="3" col="1" class="label" />
  </GridLayout>
  <Label text="{{ humidity }}" textWrap="true" row="3" col="1" />
  <GridLayout columns="30,auto" rows="auto" row="4" col="0">
    <Label text="{{ rain_icon }}" class="small-icon" row="0" col="0" />
    <Label text="Rain" textWrap="true" row="4" col="1" class="label" />
  </GridLayout>
  <Label text="{{ rain }}" textWrap="true" row="4" col="1" />
  <GridLayout columns="30,auto" rows="auto" row="5" col="0">
    <Label text="{{ sunrise_icon }}" class="small-icon" row="0" col="0" />
    <Label text="Sunrise" textWrap="true" row="5" col="1" class="label" />
  </GridLayout>
  <Label text="{{ sunrise }}" textWrap="true" row="5" col="1" />
  <GridLayout columns="30,auto" rows="auto" row="6" col="0">
    <Label text="{{ sunset_icon }}" class="small-icon" row="0" col="0" />
    <Label text="Sunset" textWrap="true" row="6" col="1" class="label" />
  </GridLayout>
  <Label text="{{ sunset }}" textWrap="true" row="6" col="1" />
</GridLayout>

Последней разметкой главной страницы является кнопка, которая ведет к странице прогноза:

<Button text="5 day Forecast" tap="goToForecastPage" />

Главная страница JavaScript

Откройте главный файл main.ts и добавьте следующий код:

import { EventData } from "data/observable";
import { Page } from "ui/page";
import { MainViewModel } from "./main-view-model";
import navigation = require('../../common/navigation');
export function navigatingTo(args: EventData) {
  var page = <Page>args.object;
  page.bindingContext = new MainViewModel();
}
export function goToForecastPage () {
  navigation.goToForecastPage();
}

В приведенном выше коде мы импортируем пару встроенных модулей NativeScript, основную модель представления и навигацию.

Объект EventData извлекается с помощью деструктурирования объекта, а ES6-функция доступна для использования с помощью TypeScript. EventData - это то, что мы передаем как аргумент функции navigatingTo, чтобы он имел доступ к любым данным, переданным любой страницей, которая перемещается на эту страницу.

Это свойство объекта, которое в основном является тем, что вызвало событие. В этом случае мы знаем, что он инициируется на компоненте страницы, и именно поэтому мы используемв качестве утверждения типа. После этого мы привязываем основную модель представления к странице. Это позволит нам добавить или обновить свойство в классе, и оно мгновенно отразится на странице.

В основной модели представления (страницы main main-view-model.ts) сначала импортируйте все модули, которые мы будем использовать:

import observable = require("data/observable");
import requestor = require("../../common/requestor");
import constants = require("../../common/constants");
import geolocation = require("nativescript-geolocation");
import moment = require('moment');
import utilities = require('../../common/utilities');
import locationStore = require('../../stores/locationStore');

Создайте модель представления, расширив модуль Observable, который делает все свойства этого класса наблюдаемыми. Это означает, что все ссылки каждого свойства в пользовательском интерфейсе будут обновляться каждый раз, когда он изменяется в этом классе.

export class MainViewModel extends observable.Observable {
  constructor() {
    ...
  }
}

Внутри конструктора проверьте, включена ли геолокация. Если он не включен, попробуйте включить его, вызвав функцию enableLocationRequest (). Это заставляет приложение запрашивать у пользователя возможность использования геолокации.

super(); //call the constructor method of the parent class
//check if geolocation is not enabled
if (!geolocation.isEnabled()) {
  geolocation.enableLocationRequest(); //try to enable geolocatio
}

Затем определите, день или ночь, и установите фон страницы в соответствии с результатом. Затем установите значки на странице.

var time_of_day = utilities.getTimeOfDay();
this.set('background_class', time_of_day);
this.setIcons();

После этого попытайтесь определить текущее местоположение. Обратите внимание: если пользователь не разрешил геолокацию, приложение не будет работать вообще, потому что погода зависит от местоположения пользователя. Приложение попытается определить местоположение пользователя за 10 секунд. Если это не удастся, то пользователю будет сообщено сообщение об ошибке.

var location = geolocation.getCurrentLocation({timeout: 10000}).
  then(
    (loc) => {
      if (loc) {
        ...
      }
    },
    (e) => {
      //failed to get locatio
      alert(e.message);
    }
);

Если запрос местоположения преуспел, мы сохраняем его с помощью locationStore. Это позволяет нам позже получить доступ к местоположению на других страницах, не запрашивая его снова.

locationStore.saveLocation(loc);

Для вашей справки, вот пример ответа, который вы можете получить при запросе местоположения в NativeScript. Вы можете проверить документацию по местоположению для NativeScript, чтобы узнать больше о каждом из свойств ниже.


   "latitude":51.50853,
   "longitude":-0.12574,
   "altitude":0,
   "horizontalAccuracy":37.5,
   "verticalAccuracy":37.5,
   "speed":0,
   "direction":0,
   "timestamp":"2016-08-08T02:25:45.252Z",
   "android":{ 
   }
}

Мы можем построить полный URL запроса API с использованием шаблонных литералов и сделать запрос с помощью модуля Requestor.

this.set('is_loading', true); //show the loader animatio
var url = `${constants.WEATHER_URL}${constants.CURRENT_WEATHER_PATH}?lat=${loc.latitude}&lon=${loc.longitude}&apikey=${constants.WEATHER_APIKEY}`;
requestor.get(url).then((res) => {
  ...
});

Как только ответ вернется, извлеките и отформатируйте его, а затем установите результирующее значение как свойство класса. И поскольку класс является наблюдаемым, это автоматически обновит пользовательский интерфейс приложения.

this.set('is_loading', false); //stop loader animatio
var weather = res.weather[0].main.toLowerCase();
var weather_description = res.weather[0].description;
var temperature = res.main.temp;
var icon = constants.WEATHER_ICONS[time_of_day][weather];
var rain = '0';
if(res.rain){
  rain = res.rain['3h'];
}
this.set('icon', String.fromCharCode(icon));
this.set('temperature', `${utilities.describeTemperature(Math.floor(temperature))} (${utilities.convertKelvinToCelsius(temperature).toFixed(2)} °C)`);
this.set('weather', weather_description);
this.set('place', `${res.name}, ${res.sys.country}`);
this.set('wind', `${utilities.describeWindSpeed(res.wind.speed)} ${res.wind.speed}m/s ${utilities.degreeToDirection(res.wind.deg)} (${res.wind.deg}°)`);
this.set('clouds', `${res.clouds.all}%`);
this.set('pressure', `${res.main.pressure} hpa`);
this.set('humidity', `${utilities.describeHumidity(res.main.humidity)} (${res.main.humidity}%)`);
this.set('rain', `${rain}%`);
this.set('sunrise', moment.unix(res.sys.sunrise).format('hh:mm a'));
this.set('sunset', moment.unix(res.sys.sunset).format('hh:mm a'));

Для справки, вот пример ответа, который может быть возвращен API:


   "coord":{ 
      "lon":-0.13,
      "lat":51.51
   },
   "weather":[ 
      { 
         "id":803,
         "main":"Clouds",
         "description":"broken clouds",
         "icon":"04d"
      }
   ],
   "base":"cmc stations",
   "main":{ 
      "temp":291.44,
      "pressure":1031.7,
      "humidity":82,
      "temp_min":290.37,
      "temp_max":292.25
   },
   "wind":{ 
      "speed":0.3,
      "deg":45,
      "gust":1
   },
   "rain":{ 
      "3h":0.075
   },
   "clouds":{ 
      "all":68
   },
   "dt":1470545747,
   "sys":{ 
      "type":3,
      "id":1462694692,
      "message":0.0043,
      "country":"GB",
      "sunrise":1470544455,
      "sunset":1470598626
   },
   "id":2643743,
   "name":"London",
   "cod":200
}

Вы можете найти подробную информацию о каждом свойстве в документации для текущих данных о погоде.

В заключение. есть функция setIcons (), которая устанавливает все значки, используемые на странице:

setIcons() {
  var icons = utilities.getIcons([
    'temperature', 'wind', 'cloud',
    'pressure', 'humidity', 'rain',
    'sunrise', 'sunset'
  ]);
  icons.forEach((item) => {
    this.set(`${item.name}_icon`, item.icon);
  });
}

Стили и значки на главной странице

Вот стили для главной страницы:

.temperature {
  font-size: 40px;
  font-weight: bold;
  text-align: center;
}
.weather {
  font-size: 30px;
  text-align: center;
}
.place {
  font-size: 20px;
  text-align: center;
}
.icon {
  font-family: 'weathericons';
  font-size: 100px;
  text-align: center;
}
.small-icon {
  font-family: 'weathericons';
  font-size: 18px;
  margin-right: 5px;
}
.details {
  margin-top: 20px;
  padding: 30px;
  font-size: 18px;
}
.label {
  font-weight: bold;
}
.details Label {
  padding: 5px 0;
}
Button {
  margin: 20px;
}

Обратите внимание на использование погодных условий в качестве семейства шрифтов для значков и классов небольших значков. Так мы используем шрифты значков в NativeScript. Если вы любите шрифты значков, такие как Font Awesome на своих веб-страницах, вы можете использовать их одинаково в приложениях NativeScript.

Сначала загрузите шрифт значка, который вы хотите использовать. Для этого приложения используется шрифт Weather Icons. Извлеките zip-архив и внутри извлеченной папки перейдите в каталог шрифтов. Скопируйте файл.ttf в каталог шрифтов вашего приложения и переименуйте его в файл weathericons.ttf. Имя файла - это то, что вы используете в качестве значения для семейства шрифтов каждый раз, когда хотите использовать этот значок шрифта. Помимо этого, вы также должны добавить размер шрифта, чтобы контролировать размер значков.

Страница прогноза

Теперь давайте посмотрим на разметку для страницы прогноза (страницы прогноза прогноза.xml). В заголовке есть кнопка назад, которая позволяет пользователю вернуться на главную страницу. Обратите внимание: вместо компонента общего назначения Button мы используем NavigationButton, который является эквивалентом NativeScript кнопки возврата iOS и кнопкой навигации Android.

<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" class="{{ background_class }}">
  <Page.actionBar>
    <ActionBar title="5-day Forecast" class="header">
      <NavigationButton text="Back" android.systemIcon="ic_menu_back" tap="goToMainPage" />
    </ActionBar>
  </Page.actionBar>
  ...
</Page>

Для основного содержимого вместо обычного ListView используется компонент Repeater. Оба компонента могут использоваться для создания списка, но ListView имеет больше колоколов и свистов. Например, он автоматически создает вертикальную полосу прокрутки, когда список переходит по размеру экрана. Кроме того, встроены функции бесконечной прокрутки.

В этом случае используется компонент Repeater, потому что нет никакой реальной необходимости в тех функциях, которые я только что упомянул. Все, что нам нужно, это список без костей.

<StackLayout>
  <ScrollView>
    <ActivityIndicator busy="{{ is_loading }}" visibility="{{ is_loading ? 'visible' : 'collapsed' }}" />
    <Repeater items="{{ forecast }}">
      <Repeater.itemTemplate>
       ...
      </Repeater.itemTemplate>
    </Repeater>
  </ScrollView>
</StackLayout>

Внутри каждого Repeater.itemTemplate находится GridLayout с двумя столбцами, один для общей информации о погоде и один для деталей.

Первый столбец - это StackLayout, содержащий дату, значок погоды и описание погоды. Вторая колонка также представляет собой StackLayout, содержащую четыре GridLayouts, которые будут содержать четыре атрибута погоды (температура, скорость ветра, облачность и давление воздуха).

Первый GridLayout имеет три столбца для размещения значка, дневной температуры и ночной температуры. Остальные три строки содержат только два столбца - для значка и значения атрибута погоды.

<GridLayout class="item" columns="*,*" rows="auto">
  <StackLayout class="day-weather" row="0" col="0">
    <Label text="{{ day }}" class="date" />
    <Label text="{{ icon }}" class="icon" />
    <Label text="{{ description }}" textWrap="true" />
  </StackLayout>
  <StackLayout class="details" row="0" col="1">
    <GridLayout columns="30,auto,auto" rows="auto" row="0" col="0">
      <Label text="{{ $parents['Page'].temperature_icon }}" class="small-icon" row="0" col="0" />
      <Label text="{{ temperature.day }}" class="temp day-temp" row="0" col="1" />
      <Label text="{{ temperature.night }}" class="temp night-temp" row="0" col="2" />
    </GridLayout>
    <GridLayout columns="30,auto" rows="auto" row="1" col="0">
      <Label text="{{ $parents['Page'].wind_icon }}" class="small-icon" row="0" col="0" />
      <Label text="{{ wind }}" row="0" col="1" />
    </GridLayout>
    <GridLayout columns="30,auto" rows="auto" row="2" col="0">
      <Label text="{{ $parents['Page'].cloud_icon }}" class="small-icon" row="0" col="0" />
      <Label text="{{ clouds }}" row="0" col="1" />
    </GridLayout>
    <GridLayout columns="30,auto" rows="auto" row="3" col="0">
      <Label text="{{ $parents['Page'].pressure_icon }}" class="small-icon" row="0" col="0" />
      <Label text="{{ pressure }}" row="0" col="1" />
    </GridLayout>
  </StackLayout>
</GridLayout>

Обратите внимание на использование $ parents ['Page']. При использовании компонента Repeater или ListView у вас не может быть доступа к данным за пределами массива, которые вы указали для списка, - если только вы явно не указали родительский компонент, в котором данные доступны. Вот где $ parent ['Page'] входит. $ Parents - это специальная переменная в NativeScript, которая позволяет вам получать доступ к данным, доступным для определенного компонента. В этом случае мы указали страницу для доступа к значкам для каждого атрибута погоды.

<GridLayout columns="30,auto" rows="auto" row="1" col="0">
  <Label text="{{ $parents['Page'].wind_icon }}" class="small-icon" row="0" col="0" />
  <Label text="{{ wind }}" row="0" col="1" />
</GridLayout>

Страница прогноза JavaScript

Код для страницы прогноза в значительной степени совпадает с кодом для главной страницы. Единственное отличие состоит в том, что навигационная функция предназначена для возврата на главную страницу, и мы используем ForecastViewModel в качестве модели представления.

import { EventData } from "data/observable";
import { Page } from "ui/page";
import { ForecastViewModel } from "./forecast-view-model";
import navigation = require('../../common/navigation');
export function navigatingTo(args: EventData) {
  var page = <Page>args.object;
  page.bindingContext = new ForecastViewModel();
}
export function goToMainPage() {
  navigation.goToMainPage();
}

Вот код для модели просмотра (прогноз прогноза-view-model.ts):

import observable = require("data/observable");
import requestor = require("../../common/requestor");
import constants = require("../../common/constants");
import moment = require('moment');
import utilities = require('../../common/utilities');
import locationStore = require('../../stores/locationStore');
export class ForecastViewModel extends observable.Observable {
  constructor() {
    super();
    var location = locationStore.getLocation();
    var url = `${constants.WEATHER_URL}${constants.WEATHER_FORECAST_PATH}?cnt=6&lat=${location.latitude}&lon=${location.longitude}&apikey=${constants.WEATHER_APIKEY}`;
    var time_of_day = utilities.getTimeOfDay();
    this.set('is_loading', true);
    this.set('background_class', time_of_day);
    this.setIcons();
    requestor.get(url).then((response) => {
      this.set('is_loading', false);
      var forecast = this.getForecast(response);
      this.set('forecast', forecast);
    });
  }
  private getForecast(response) {
    var forecast = [];
    var list = response.list.splice(1); //remove first item from array of forecasts
    //format and push all the necessary data into a new array
    list.forEach((item) => {
      forecast.push({
        day: moment.unix(item.dt).format('MMM DD (ddd)'),
        icon: String.fromCharCode(constants.WEATHER_ICONS['day'][item.weather[0].main.toLowerCase()]),
        temperature: {
          day: `${utilities.describeTemperature(item.temp.day)}`,
          night: `${utilities.describeTemperature(item.temp.night)}`
        },
        wind: `${item.speed}m/s`,
        clouds: `${item.clouds}%`,
        pressure: `${item.pressure} hpa`,
        description: item.weather[0].descriptio
      })
    });
    return forecast;
  }
  private setIcons() {
    var icons = utilities.getIcons(['temperature', 'wind', 'cloud', 'pressure']);
    icons.forEach((item) => {
      this.set(`${item.name}_icon`, item.icon);
    });
  }
}

Внутри конструктора мы получаем текущее местоположение из хранилища местоположений и создаем конечную точку URL для 16-дневного прогноза погоды. Но вместо 16 мы хотим всего пять дней, поэтому мы укажем 6 для count (cnt). Мы используем 6, потому что часовой пояс зависит от сервера, а не от указанного местоположения. Это означает, что есть вероятность, что API вернет погоду за предыдущий день или текущий день. Вот почему есть дополнительный 1 день, который служит дополнением.

var location = locationStore.getLocation();
var url = `${constants.WEATHER_URL}${constants.WEATHER_FORECAST_PATH}?cnt=6&lat=${location.latitude}&lon=${location.longitude}&apikey=${constants.WEATHER_APIKEY}`;

Затем сделайте запрос и обновите интерфейс с ответом API, вызвав функцию getForecast ():

requestor.get(url).then((response) => {
  this.set('is_loading', false);
  var forecast = this.getForecast(response);
  this.set('forecast', forecast);
});

Вот пример ответа, возвращаемого 16-дневной конечной точкой прогноза. Обратите внимание: чтобы сделать образец более кратким, я установил count в 1, поэтому свойство list содержит только один объект.


   "city":{ 
      "id":2643743,
      "name":"London",
      "coord":{ 
         "lon":-0.12574,
         "lat":51.50853
      },
      "country":"GB",
      "population":0
   },
   "cod":"200",
   "message":0.0347,
   "cnt":1,
   "list":[ 
      { 
         "dt":1470571200,
         "temp":{ 
            "day":24.69,
            "min":17.37,
            "max":24.69,
            "night":17.37,
            "eve":23.29,
            "morn":19.02
         },
         "pressure":1029.77,
         "humidity":0,
         "weather":[ 
            { 
               "id":500,
               "main":"Rain",
               "description":"light rain",
               "icon":"10d"
            }
         ],
         "speed":8.27,
         "deg":253,
         "clouds":0
      }
   ]
}

Стили страницы прогноза

Вот стили для страницы прогноза (страницы прогноза прогноза.css):

Page {
  font-size: 15px;
}
.item {
  padding: 20px 10px;
}
.day-weather {
  text-align: center;
}
.details {
  horizontal-align: left;
}
.date {
  font-size: 20px;
}
.icon {
  font-family: 'weathericons';
  font-size: 30px;
}
.temp {
  padding: 3px;
  text-align: center;
  font-size: 15px;
}
.day-temp {
  background-color: #d0c110;
}
.night-temp {
  background-color: #505050;
}
.small-icon {
  font-family: 'weathericons';
  margin-right: 5px;
  text-align: center;
}

Глобальные стили приложений

Откройте файл app.css и добавьте следующие стили:

.header {
  background-color: #333;
  color: #fff;
}
.day {
  background-color: #f48024;
  color: #fff;
}
.night {
  background-color: #924da3;
  color: #fff;
}

6. Изменение значка приложения по умолчанию

Вы можете изменить значок приложения по умолчанию, перейдя в папку App_Resources. Там вы можете увидеть папку Android и iOS. Для Android вы можете заменить файл изображения в каждой из следующих папок, чтобы изменить значок:

drawable-hdpi drawable-ldpi drawable-mdpi

Для iOS это изображения внутри папки Assets.xcassets AppIcon.appiconset, которую вы хочу заменить.

Если вы хотите легко создавать значки для Android и iOS, посмотрите MakeAppIcon. Просто выберите файл изображения, который будет использоваться в качестве значка, и он автоматически будет генерировать разные размеры для Android и iOS. Затем вы можете перенести их в папки, упомянутые выше. Просто убедитесь, что вы получили правильные размеры, и имена совпадают с изображениями, которые они заменяют.

7. Запуск приложения

Вы можете запустить приложение на своем устройстве или эмуляторе, как обычно, выполнив следующие команды tns:

tns run {platform}
tns livesync {platform} --watch

Единственное отличие теперь, когда мы используем TypeScript, заключается в том, что в начале каждой задачи есть дополнительный шаг, чтобы скомпилировать файлы TypeScript в JavaScript. Сильная проверка типа TypeScript обеспечивает защиту от сбоев некоторых ошибок до того, как NativeScript даже скомпилирует приложение.

Заключение

В этом уроке вы узнали, как создать приложение с NativeScript с использованием языка TypeScript. В частности, вы изучили следующие концепции:

Структурирование вашего приложения путем размещения связанных файлов в их собственной папке. Повторное использование кода с использованием модулей. Использование моделей просмотра для предоставления данных и функциональных возможностей для страниц приложения. Определение местоположения с помощью плагина геолокации. Выполнение HTTP-запросов. Использование значков шрифтов. Навигация между страницами.

Я оставлю вас с несколькими ресурсами, чтобы продолжить свое путешествие по разработке замечательных приложений с NativeScript:

Прочитайте руководство по публикации вашего приложения NativeScript в Google Play или в App Store. Подпишитесь на рассылку NativeScript и обновите информацию о том, что происходит в сообществе NativeScript. Проверьте Awesome NativeScript, набор потрясающих библиотек, инструментов, ресурсов и приложений NativeScript. Смотрите наш курс Envato Tuts + по началу работы с TypeScript.