Создание кроссплатформенного приложения в Titanium

Что это

Если вы читаете эту статью, вы, вероятно, знаете, что такое титан. Если вы этого не сделаете. Проще говоря, Titanium - это фреймворк, который позволяет вам разрабатывать мобильное приложение, которое будет работать как на Android, так и на iOS (и Windows phone), повторно используя один и тот же код или 90% его.

Самое приятное, что язык программирования - JavaScript, а все мы javascript.

Trimethyl - это набор инструментов и библиотек, который пытается сократить этот 10% разрыв между ними, за исключением того, что упрощает использование некоторых API, сокращая время реализации.

Установка

Установить Titanium просто: либо установите IDE, либо выполните следующий код в своем терминале.

npm i -g titanium \
 alloy \
 tishadow \
 tisdk \
 tn \
 gittio

Установить Триметил так же просто

npm i -g trimethyl

Это возможно, потому что триметил, или, как мы его называем, T, обладает мощным кли.

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

npm i -g tishadow tn

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

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

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

Моя рабочая область будет ~ / projects / trimethyl_example

mkdir -p ~/projects/trimethyl_example
cd ~/projects/trimethyl_example

Теперь я собираюсь создать проект Титаниум. Это просто, просто следуйте подсказкам в cli.

Если вы используете IDE, просто создайте новый проект и выберите Alloy. Очевидно, пропустите следующий шаг и перейдите к триметилу.

ti create

Запрос действительно прост: он запрашивает тип проекта, который в данном случае равен «1», платформы, для которых я хочу приложение, и «все» работает. хорошо для этого примера, идентификатор приложения и название проекта, вы можете оставить URL пустым, это не обязательно, и поскольку я уже в рабочий каталог, мы можем просто ввести «.».

Этого было бы достаточно для меня, чтобы начать разработку приложения, используя то, что у меня есть. Те из вас, кто использовал Titanium с первых дней, будут готовы к работе. Но поскольку MVC - довольно эффективный шаблон программирования, я собираюсь добавить его, установив Alloy.

Третий шаг - добавление сплава

Обратите внимание, что ti create создал внутри нашего каталога папку с «названием проекта». Поэтому для простоты я перейду в этот новый каталог.

alloy new .

Это было достаточно просто, правда? Теперь вы уже можете создать приложение, и оно будет отлично работать.

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

trimethyl install

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

Я думаю мне нужно следующее

  • Bootstrap
  • App
  • Auth потому что я хочу показать вам, как легко управлять аутентификацией с помощью Facebook, поэтому я также добавлю Auth.Facebook
  • Flow
  • Router
  • GA что очевидно является Google Analytics,
  • HTTP
  • Logger Я объясню это ниже
  • Notifications
  • UIUtil Не уверен, что буду использовать, но все равно добавлю
  • WebAlloy

Я также добавлю несколько других утилит, например Q Underscore.String REST Alloy Adapter, и на этом пока все.

Когда вы нажимаете Enter, cli спросит вас, хотите ли вы включить некоторые титановые модули, необходимые для работы библиотек, например ti.safaridialog

Для каждого титанового модуля вы можете ответить A, чтобы добавить его в tiapp.xml файл, i, чтобы установить его через gittio, s, чтобы пропустить установку, e, чтобы выйти из приглашения и h, чтобы получить помощь

Logger работает как Ti.API в том смысле, что я могу вызывать Logger.warn() или Logger.info() так же, как и Ti.API.warn(), но проблема с последним состоит в том, что если вы попытаетесь сбросить в него модель Backbone, она будет работать в разработке (TiShadow ), но это приведет к сбою приложения в рабочей среде без объяснения причин. Поэтому мы обернули Ti.API Logger добавлением некоторых проверок, чтобы убедиться, что если журнал остается в рабочем состоянии, он не убивает приложение.

Мы закончили создание проекта

Давай построим что-нибудь

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

Чтобы помочь с будущим тестированием, я создам файл с именем tn.json и добавлю к нему следующее:

{
 "sim": ["build", "-T", "simulator", "-p", "ios", "--device-id", "SIMULATOR_ID"]
}

Замените SIMULATOR_ID идентификатором симулятора, в котором вы хотите запустить приложение. В моем случае это iPhone SE под управлением iOS 10.3.1. Чтобы получить идентификатор симулятора, запустите instruments -s devices , чтобы получить список всех доступных симуляторов iOS.

Затем, чтобы запустить его, введите tn sim

Как следует из этой статьи, Sublime Text - один из лучших текстовых редакторов с точки зрения управления памятью, поэтому я начну писать код.

Точкой входа для приложений сплава является сплав.js, который используется для установки глобальных переменных, а затем сплав автоматически запускает индекс, определенный контроллером index.js и представление index.xml

Нужна идея перед написанием кода

Итак, я думаю о создании приложения, которое требует, чтобы пользователь входил в систему с помощью Facebook, и после входа в систему я хочу показать им список данных из Интернета, и, очевидно, после этого, когда пользователь нажимает на элемент списка, я ' Я покажу страницу с одним элементом. Думаю, я собираюсь использовать IMDB API и создать что-нибудь на его основе. Я просто включу уведомления, чтобы отправлять людям случайные уведомления.

Я создам прокси для IMDB API, чтобы я мог кэшировать ответ и не делать слишком много запросов к серверу IMDB. Я буду использовать прокси-сервер для регистрации идентификатора устройства моих пользователей, чтобы я мог отправлять им уведомления в будущем.

Предположим, мой прокси-сервер IMDB будет предоставлять следующие конечные точки API, тогда

  • /фильмы
  • / фильмы / идентификатор
  • / уведомления / подписка
  • / уведомления / отписаться
  • / уведомления / активировать
  • / уведомления / деактивировать

Библиотека уведомлений Trimethyl по умолчанию использует 4 конечные точки. Разница между подпиской и активацией заключается в том, что при активации пользователь остается подписанным на сервер, но сервер не отправляет никаких уведомлений на устройство.

Настройка проекта для использования моих библиотек Trimethyl

На этом этапе я открываю проект с возвышенным текстом $ subl ., а затем открываю сплав.js. Чтобы использовать инструмент subl, предоставляемый Sublime Text, проверьте этот поток переполнения стека.

Я добавлю глобальную функцию T, которая поможет мне решить проблему с библиотеками Trimethyl.

var T = function(name) { return require('T/' + name); };
T('trimethyl'); // Requiring base trimethyl

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

var Auth = null;
var HTTP = null;
var App = T('app');
var Event = T('event');
var Flow = T('flow');
var Logger = T('logger');
var Notifications = T('notifications');
var Q = T('ext/q');
var Router = T('router');
var Util = T('util');
var UIUtil = T('uiutil');
var WebAlloy = T('weballoy');
var GA = T('ga');

WebAlloy копирует Alloy MVC, но для веб-просмотров. Это означает, что вы можете создать компонент на основе WebView, имеющий index.html в качестве представления, index.jslocal в качестве контроллера (javascript) и index.css для стиля представления. Прочтите документацию, чтобы увидеть, насколько он действительно мощный.

Конечно, я воспользуюсь moment.js. Я собираюсь добавить логику запуска в Core, поэтому я включу их (пока пустые файлы). Один из самых важных - маршруты, поэтому я использую routes.js, я вернусь к нему позже.

var Moment = require('alloy/moment');
var Core = require('core');
// Routes to navigate the app
require('routes');
// Finally, registering notifications.
Notifications.onReceived = function(e) {
    // Handle notifications
};

На данный момент нам не хватает core.js, поэтому давайте создадим его. Он должен быть создан в ./app/lib/core.js

Я также добавлю еще один файл с именем routes в ./app/lib/routes.js, о котором я упоминал выше.

Чтобы лучше объяснить, для чего используется core.js, я добавлю к нему пару функций.

exports.notification = null;
exports.canConsumeUINotifications = false;
exports.canConsumeQueuedNotification = function() {
   if (exports.notification == null) return false;
};
exports.consumeQueuedNotification = function() {
  if (exports.notification == null) return;
  var notif = _.clone(exports.notification);
  exports.notification = null;
  Logger.debug(">>> Notification: ", notif);
  // App events are defined in events.
 
  if (notif.inBackground == false && notif.data.alert != null) {
    Event.trigger('ui.message', notif.data.alert);
  
  } else if(notif.data.data && notif.data.data.route != null) {
    // Using Trimethyl router
    Router.go(notif.data.data.route);
  
  } else if (notif.data.data && notif.data.data.external_link != null) {
    
    Util.openHTTPLink(notif.data.data.external_link);
  }
  Event.trigger("notifications.new", notif);
};

Здесь мы определяем canConsumeQueuedNotification и consumeQueuedNotification, причем первое возвращает истину, если есть новые уведомления, а второе действует в соответствии с полезной нагрузкой уведомления.

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

Не забудьте добавить var Util = T('util'); и var Event = T('event') в сплав .js

В Util есть некоторые служебные функции, например, использованная выше openHTTPLink, которая выполняет все необходимые проверки, а затем открывает ссылку, используя модуль safaridialog, если он доступен, а если нет, ссылка будет открыта с помощью сафари на iOS и веб-браузера по умолчанию на Android.

Позже я объясню, где буду обрабатывать события.

Конфигурация

Я настроил проект, я добавил недостающие файлы, такие как routes.js (хотя все еще пустые), core.js, но прежде чем я начну кодировать, я собираюсь установить некоторые параметры конфигурации в ./app/config.json, который является файлом конфигурации, который Alloy автоматически включается в проект.

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

"env:development": {
  "T":{
   "notifications":{
    "driver": "http"
   },
   "http":{
    "base": "http://imdb.api.progress44.com",
    "log": false
   },
   "ga": {
    "ua": "UA-0000-00",
    "log": false
   },
   "fb":{
    "permissions":["email","user_likes"]
   },
   "auth":{
    "facebook":{
     "loginUrl": "/login/fb"
    }
   }
  }
}

На данный момент все одинаково для сред разработки, тестирования и производства. Как видите, библиотеки легко настраиваются. Чтобы лучше понять, как настроить конкретную библиотеку, которую я хочу использовать, я обычно перехожу к ее файлу, например ./app/lib/T/http.js, и прямо в заголовке файла есть объект exports.config с некоторыми значениями по умолчанию, которые можно перезаписать с помощью config.js

Маршрутизация

Trimethyl Router очень хорошо работает вместе с Flow, но его можно использовать для самых разных целей.

С помощью объекта Router вы можете эмулировать концепцию маршрутизации, принятую во многих веб-фреймворках, таких как Laravel, Symphony или Expressjs. По сути, с этой парадигмой вы связываете строку (имя маршрута) с функцией обратного вызова, которая (большую часть времени) открывает окно в вашем приложении.
Документы https://github.com/trimethyl/trimethyl/wiki/Router

Я просто объясню его использование на примере ниже. Я отредактирую lib/routes.js вот так

Router.on('/login', function() {
    Flow.open("index", {}); 
});

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

Flow - это утилита, полезная для управления стеком окон. Я собираюсь инициализировать его, установив главное окно навигации в index.js

Flow использует концепцию навигации через окна. После определения базового навигатора он автоматически отслеживает все открытые / закрытые окна (отслеживая их через модуль Trimethyl.GA).
Документы https://github.com/trimethyl/trimethyl/wiki/Flow

Flow.setNavigationController($.index, true);

И содержание index.xml

<Alloy>
 <NavigationWindow module="T/uifactory">
  <Window id="mainWindow">
   <!-- some content -->
  </Window>
 </NavigationWindow>
</Alloy>

Компонент окна навигации будет расширен из UIFactory, который я только что добавил с помощью команды trimethyl add uifactory && trimethyl install

Мне также нужно добавить var UIFactory = T('uifactory');, если мне нужно использовать библиотеку из кода.

Чтобы получить этот результат, мне пришлось учесть некоторые соображения (это также объясняется в сообщении о фиксации). Чтобы использовать вход через Facebook, мне нужно установить fbproxy на корневой контроллер пользовательского интерфейса. Я установил index.xml в пустое окно и добавил в контроллер следующее:

var args = arguments[0] || {};
//////////
// Open //
//////////
function auto() {
    Router.go("/movies");
}
if (OS_ANDROID) {
    UI.RootActivity = $.getView();
    UI.RootActivity.fbProxy = FB.createActivityWorker({ lifecycleContainer: UI.RootActivity });
    UI.RootActivity.addEventListener('open', auto);
    UI.RootActivity.open();
} else {
    auto();
}

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

Теперь же изменился и routes.js. Находясь вне навигационного потока, я не могу использовать Flow.open, но мне нужно использовать Flow.openDirect, поэтому обновленный файл routes.js будет

Router.on('/login', function() {
    // index isn't the login window anymore
    Flow.openDirect("login", {});
});
Router.on('/movies', function() {
    Flow.openDirect("movie", {});
});

Войти с Facebook

Чтобы это работало, сначала мне нужно зарегистрировать приложение на https://developers.facebook.com/

iOS

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

Консоль разработчика показывает мне набор служб, которые я могу настроить. Я собираюсь войти в Facebook

Щелчок по настройке показывает мне список платформ, на которых я хочу использовать эту функцию. Сначала я установлю iOS, а потом займусь Android.

Я перейду к Bundle ID, потому что с титаном мне не нужно добавлять Facebook SDK, я могу просто использовать модуль.

Далее мастер Facebook показывает мне, как обновить мой info.plist, но в контексте Titanium info.plist включен в tiapp.xml

Мой тег ios tiapp.xml теперь выглядит так

<?xml version="1.0" encoding="UTF-8"?>
<ti:app xmlns:ti="http://ti.appcelerator.org">
    <ios>
    <enable-launch-screen-storyboard>false</enable-launch-screen-storyboard>
    <use-app-thinning>true</use-app-thinning>
    <plist>
      <dict>
        <key>UISupportedInterfaceOrientations~iphone</key>
        <array>
          <string>UIInterfaceOrientationPortrait</string>
        </array>
        <key>UISupportedInterfaceOrientations~ipad</key>
        <array>
          <string>UIInterfaceOrientationPortrait</string>
          <string>UIInterfaceOrientationPortraitUpsideDown</string>
          <string>UIInterfaceOrientationLandscapeLeft</string>
          <string>UIInterfaceOrientationLandscapeRight</string>
        </array>
        <key>UIRequiresPersistentWiFi</key>
        <false/>
        <key>UIPrerenderedIcon</key>
        <false/>
        <key>UIStatusBarHidden</key>
        <false/>
        <key>UIStatusBarStyle</key>
        <string>UIStatusBarStyleDefault</string>
        <key>CFBundleURLTypes</key>
        <array>
          <dict>
          <key>CFBundleURLSchemes</key>
          <array>
            <string>fb1061219234012962</string>
          </array>
          </dict>
        </array>
        <key>FacebookAppID</key>
        <string>1061219234012962</string>
        <key>FacebookDisplayName</key>
        <string>Trimethyl Test</string>
        <key>LSApplicationQueriesSchemes</key>
        <array>
          <string>fbapi</string>
          <string>fb-messenger-api</string>
          <string>fbauth2</string>
          <string>fbshareextension</string>
        </array>
      </dict>
      <key>NSAppTransportSecurity</key>
      <dict>
        <key>NSExceptionDomains</key>
        <dict>
            <key>facebook.com</key>
                <dict>
                    <key>NSIncludesSubdomains</key> 
                    <true/>        
                    <key>NSExceptionRequiresForwardSecrecy</key> 
                    <false/>
                </dict>
            <key>fbcdn.net</key>
                <dict>
                    <key>NSIncludesSubdomains</key> 
                    <true/>
                    <key>NSExceptionRequiresForwardSecrecy</key>  
                    <false/>
                </dict>
            <key>akamaihd.net</key>
                <dict>
                    <key>NSIncludesSubdomains</key> 
                    <true/>
                    <key>NSExceptionRequiresForwardSecrecy</key> 
                    <false/>
                </dict>
        </dict>
    </dict>
    </plist>
  </ios>
</ti:app>

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

Либо создайте для менее сложной платформы, либо ту, с которой вам удобнее, сначала (в моем случае iOS), а затем, когда у меня будет стабильная версия, начните исправлять ее для других платформ или создайте кросс-платформу для каждой функции. с самого начала. И вот как я это сделаю.

Android

Перво-наперво, я вернусь в консоль разработчика и включу новую платформу, выбрав Настройки ›Основные в меню левой панели и прокрутив вниз.

Здесь очень мало вещей, которые можно добавить. Название пакета Google Play совпадает с идентификатором пакета для iOS. Имя класса установлено, как показано ниже (часть, выделенная жирным шрифтом, - это имя класса, и мне нужно будет использовать имя пакета в качестве префикса)

<android xmlns:android="http://schemas.android.com/apk/res/android">
    <manifest android:installLocation="auto" android:versionCode="1" android:versionName="1.0.0">
    <application android:hardwareAccelerated="true" android:theme="@style/Theme.AppCompat" android:largeHeap="true">
        <activity android:name=".TrimethylTestActivity" android:screenOrientation="portrait" android:configChanges="keyboardHidden|orientation|screenSize">
          <intent-filter>
            <action android:name="android.intent.action.MAIN"/>
            <category android:name="android.intent.category.LAUNCHER"/>
          </intent-filter>
        </activity>
      </application>
    </manifest>
 </android>

Имя моего класса будет com.caffeina.trimethyltest.TrimethylTestActivity
Если на этом этапе возникнут проблемы, проверьте AndroidManifest.xml в разделе build / android на предмет правильного имени класса.

Если ваши appname и android: name различаются, вы увидите два значка приложения на телефоне, и оба запустят его.

Мне также нужно добавить пару действий и метаданные в узел приложения файла tiapp.xml.

<activity 
  android:name="com.facebook.Activity" 
  android:theme="@android:style/Theme.Translucent.NoTitleBar" 
  android:label="Trimethyl Test"
/>
<activity 
  android:name="com.facebook.FacebookActivity" 
  android:theme="@android:style/Theme.Translucent.NoTitleBar" 
  android:label="Trimethyl Test" 
android:configChanges="keyboard|keyboardHidden|screenSize|orientation"
/>
<meta-data android:name="com.facebook.sdk.ApplicationId" android:value="@string/facebook_app_id"/>

И ссылки на метаданные string/facebook_app_id, которые я должен добавить в /app/platform/res/values/strings.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_id">1061219234012962</string>
</resources>

Теперь мне нужно сделать хеширование клавиш. Что такое хеш-код ключа Android?

keytool -exportcert -alias androiddebugkey -keystore "~/Library/Application\ Support/Titanium/mobilesdk/osx/6.0.4.CAFFEINA-1/dev_keystore" | openssl sha1 -binary | openssl base64

Это результирующий хэш-ключ rUv + KchXF0iiWn2shxp3igTr + PE =

Поскольку на Android это довольно просто, я сейчас просто создам productionkeystore и хэш.

keytool -genkey -keystore "./certs/keystore" -storepass "PASSWORD" -keypass "PASSWORD" -alias "TRIMETHYL" -keyalg RSA -keysize 2048 -validity 10000 -dname "CN=TRIMETHYL, OU=Mobile, O=Caffeina, L=Parma, S=Parma, C=IT"

Приведенная выше команда сгенерирует файл с именем хранилище ключей в каталоге приложения в разделе сертификаты с паролем хранилища и паролем ключей, установленным на ПАРОЛЬ. В этом случае я устанавливаю псевдоним хранилища ключей на TRIMETHYL, но обычно я устанавливаю его равным имени приложения. То же самое и с именем сертификата (CN).
Вместо этого приведенная ниже команда выведет производственный хэш, который будет скопирован в Facebook. Очевидно, что псевдоним, путь к хранилищу ключей и пароль должны быть такими же, как указано выше.

keytool -exportcert -alias "TRYMETHIL" -keystore "./certs/keystore" -storepass "PASSWORD" | openssl sha1 -binary | openssl base64

Последний шаг - установить fbproxy в качестве основного действия, но я уже сделал это в контроллере index.js выше.

Отметьте этот коммит для полного tiapp.xml

UI

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

Я создам свой логин контроллер и его представление

Выше показан мой вид входа в систему, контроллер пока пуст.

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

$.loginButton.addEventListener("click", function() {
 Auth.login({ driver: 'facebook' });
});

Это ровно 3 строки кода

Auth инициализирует модуль Facebook, запрашивает авторизацию у пользователя, и после получения access_token он автоматически отправляется на сервер на конечной точке login/fb, которую можно очень легко настроить.
К сожалению, я еще не работал с моим API, поэтому он возвращает ошибку, и вход в систему не выполняется. Но вход в Facebook работает.

Обработка ответа

Достаточно справедливо, но как мне сказать, что делать, если вход в систему прошел успешно или как обрабатывать ошибки?

Auth запускает несколько глобальных событий. Теперь я добавлю файл events.js /lib/events.js для управления глобальными событиями. Я добавлю к нему несколько обработчиков для аутентификации. Мне он понадобится в сплава.js прямо под require('routes');

Триггеры Auth для событий: success error logout Я добавлю обработчик для первых двух.

function authHandler(e) {
 Logger.debug('Auth: ', e);
 Flow.close();
 Router.go("/movies");
}
Auth.event('success', authHandler);
Auth.event('error', authHandler);

Я использую одну и ту же функцию для успеха и ошибки, потому что мой API входа не работает, но я все же хочу иметь возможность использовать приложение. Вот почему Router.go("/movies");

"Попробуй сам."

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

(Может, напишу еще одну статью о разработке простого сервиса REST API 😀)

Я создал простую службу API, чтобы предоставить несколько маршрутов, необходимых для моего приложения.

Модулю уведомлений нужны эти четыре конечные точки.

  • /notifications/subscribe
  • /notifications/unsubscribe
  • /notifications/mute
  • /notifications/unmute

Они означают, соответственно, подписать текущее устройство на получение push-уведомлений, отменить подписку, оставить устройство подписанным, но сервер не должен отправлять какие-либо уведомления и вернуться в состояние подписки.
Их можно перезаписать в файле config.json в T ›уведомлениях› http

Чтобы включить аутентификацию, вам необходимо предоставить эти API на своем сервере.

  • /login
  • /login/fb - вызывается автоматически при входе в Facebook
  • /logout

Не нужно объяснять, что они делают. Очевидно, вы можете переопределить их в conf.json
В моем серверном приложении все вышеперечисленное возвращает {success: true}

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

  • /movies - Obvioulsy возвращает коллекцию фильмов
  • /movies/:id - возвращает модель фильма, идентифицированную id
  • /tmdb/configuration - содержит важные настройки для создания URL изображений.

Чтобы уважать этот выбор, мне нужны модели Alloy configuration и movie. Начну с киномодели.

Модели (и коллекции) Alloy являются расширением моделей Backbone.js. Они работают точно так же. Trimethyl автоматически включает несколько реализаций синхронизации, поэтому модели будут напрямую связаны со службой REST.

exports.definition = {
  config: {
    adapter: {
      type: 'api',
      name: 'movies'
    }
  },
  extendModel: function(Model) {
    _.extend(Model.prototype, {
    
    });
    return Model;
  },
  extendCollection: function(Collection) {
    _.extend(Collection.prototype, {
    
    });
  }
};

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

exports.definition = {
  config: {
    adapter: {
      type: 'api',
      name: 'tmdb/configuration'
    }
  },
extendModel: function(Model) {
    _.extend(Model.prototype, {
    
    });
    return Model;
  },
extendCollection: function(Collection) {
    _.extend(Collection.prototype, {
    
    });
  }
};

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

Я написал следующий метод

getImageBase: function(size, type) {
    var img = this.get('images');
    if (!img || img.length == 0) return;
    
    // getting sizes for the image type
    // and filtering them by the closest 
    // or return the latest in the list 
    return img.secure_base_url + _(img[type + "_sizes"]).filter(function(s) {
     // return true if the absolute value of the 
     // size - requested size is less than or equal to 20
     return Math.abs( parseInt( s.substr(1) ) - size ) <= 30;
    }).join() || img[type + "_sizes"][img[type + "_sizes"].length - 1];
   }

Посмотрите этот файл, чтобы увидеть, как все это сочетается. Чтобы лучше понять, посмотрите, что API возвращает для этого вызова.

Я также добавил служебную программу в core.js (lib / core.js) для получения конфигурации из API.

Узнайте больше о Q и обещаниях здесь.

var configuration = null;
exports.getConfiguration = function() {
 return Q.promise(function(resolve, reject) {
  
  if (configuration == null) {
   configuration = Alloy.createModel("configuration");
   
   configuration.fetch({
    success: function() {
     resolve(configuration);
    },
    error: reject
   });
  
  } else {
   
    return resolve(configuration);
  
  }
 });
};

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

Router.on('/movies', function() {
 Core.getConfiguration()
 .then(function(conf) {
   
   Flow.openDirect("movie", {tmdb: conf});
 
 });
});

Итак, теперь, когда откроется представление «Фильм», у меня будет args.tmdb моя конфигурация. Я могу получить коллекцию фильмов из API в контроллере представления.

var args = arguments[0] || {};
var Movies = null; // New line
Flow.setNavigationController($.movie, true);
// Init
function init() {
 Movies = Alloy.createCollection("movie");
 Movies.fetch({
  success: function() {
    $.list.setConfiguration(args.tmdb);
    $.list.setCollection(Movies.filter(function(movie) {
      return movie.get('poster_path');
    }));
  },
  error: function(err) {
   Logger.error(err);
  }
 });
}
init();

Я добавил переменную Movies и блок init, который извлекает коллекцию. Чтобы показать список фильмов, я использую ListView и связываю коллекцию с помощью утилиты Trimethyl. Но в моем movie.xml все содержимое окна требуется от другого контроллера в «контроллерах / фильмах / списке», поэтому здесь я должен реализовать функции setCollection и setConfiguration для передачи коллекции из контроллера movie.js в фильмы / list.js контроллер.

Мое представление list.xml теперь содержит компонент ListView с одним шаблоном.

В файле list.js две упомянутые выше функции очень просты, они только сохраняют данные в аргументах переменной. Теперь мне нужно добавить еще две функции. Я собираюсь вызвать первую функцию populate, и она, как следует из названия, добавит данные в ListView. Вот код для этого.

function populate() {
    UIUtil.populateListViewFromCollection(Movies, {
        datasetCb: getTemplateObject
    }, $.moviesList);
}

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

function getTemplateObject(model) {
    return {
        poster: {
            image: Config.getImageBase(200, "poster") + model.get("poster_path")
        }
    };
};

Чтобы попробовать это до сих пор, проверьте этот коммит. Вот результат.

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

function listItemClicked(e) {
 if (e.itemId) Router.go("/movie/" + e.itemId);
};

Функция, которую я хочу вызывать при срабатывании события касания, - это функция, указанная выше, которая просто скажет маршрутизатору перейти к фильму с определенным id

Чтобы функция активировалась, мне нужно добавить ее в список. В элементе шаблона элемента с именем фильм у меня есть подвид с идентификаторами столбцы, и я собираюсь добавить слушателя, как показано ниже.

<ItemTemplate name="movie">
    <View id="columns" onClick="listItemClicked">
        <ImageView bindId="poster" id="poster"/>
...

И теперь я могу считать просмотр списка фильмов завершенным. Переходим к следующей задаче - просмотру одного фильма.

Завершение приложения

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

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

Вот результат, который мне нужен.

Я хочу, чтобы плакат фильма занимал большую часть экрана, а затем, прокручивая его вниз, я хочу показать некоторую информацию о фильме. Поскольку я уверен, что мне придется прокручивать представление, потому что там будет много контента, я буду использовать ScrollView

Как вы можете заметить из моего кода, я широко использую свойство layout. Таким образом, мне не нужно устанавливать фиксированные позиции для элементов просмотра, и все можно адаптировать. (Может даже работать в ландшафтном режиме 🤥)

Код для самого представления очень маленький.

<Alloy>
 <Window title="Movie">
  <ScrollView id="container">
   <View id="coverContainer">
    <ImageView id="cover"/>
   </View>
   <View id="info">
    <Label id="title" />
   </View>
  </ScrollView>
 </Window>
</Alloy>

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

Но сначала давайте вернемся к routes.js и обновим его, чтобы он извлекал содержимое перед открытием просмотра фильма.

Router.on(/\/movies\/([^\/]+)\/?/, function(id) {
  Core.getConfiguration()
  .then(function(conf) {
    return Q.promise(function(resolve, reject) {
      var movie = Alloy.createModel("movie", {id: id});
      movie.fetch({
        success: function(m) {
          resolve([conf, movie]);
        },
        error: reject
      })
    });
  })
  .then(function(data) {
    Flow.open("movie/single", {
      id: id,
      tmdb: data[0],
      movie: data[1]
    });
  })
  .catch(function() {
    alert("There has been an error while loading the movie or the configuration");
  });
});

Я также добавил резервную функцию для перехвата исключений. Он покажет предупреждение, если что-то пойдет не так

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

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

// init
function init() {
  $.setConfiguration(args.tmdb);
  $.setModel(args.movie);
}
init();

Если эти аргументы пусты, оба метода setModel и setConfiguration попытаются снова загрузить их с сервера.

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

Метод setConfiguration просто сохраняет аргумент в переменной, глобальной для контроллера. setModel вместо этого делает дополнительную вещь. Он вызывает populate, который заполняет представление информацией.

Ниже метода populate.

function populate() {
  $.cover.image = Config.getImageBase(500, "poster") + Movie.get("poster_path");
  $.title.text = Movie.get('original_title');
  $.info.add(createRow("Original Language", [ Movie.get('original_language') ]));
  $.info.add(createRow("Release Date", [ Movie.get('release_date') ]));
  $.info.add(createRow("Popularity", [ Movie.get('popularity') ]));
  $.info.add(createRow("Genre", pickName(Movie.get('genres')) ));
  $.info.add(createRow("Production Company", pickName(Movie.get('production_companies')) ));
  $.info.add(createRow("Production Countries", pickName(Movie.get('production_countries')) ));
  $.info.add(createRow("Plot",[ Movie.get('overview')] ));
}

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

Вот коммит с кодом для этого раздела.

Это будет последний шаг в разработке приложения.

Последний шаг, публикация приложения

Контрольный список перед сборкой для производства.

  • Номер версии (и Apple, и Google не позволяют загружать одну и ту же сборку дважды)
  • Правильный код Google Analytics
  • Правильная конечная точка API для производственной среды
  • ATS (или еще лучше, если вы используете HTTPS с включенным закреплением SSL)
  • Может быть, на всякий случай проверить приложение на разных размерах экрана, может быть.
  • Конечно, проверьте значок приложения и заставку
  • Убедитесь, что ресурсы существуют для всех разрешений экрана (2x, 3x, hdpi, xxhdpi и т. Д.)
  • Убедитесь, что в корневом каталоге приложения есть файл DefaultIcon, чтобы Titanium мог генерировать значки, если они отсутствуют.
  • Объедините функциональную ветку в разработку, проверьте все, проверьте свой код, объедините разработку в мастер, дважды проверьте, что вы ничего не забыли, исправьте конфликты слияния, отправьте в мастер, отметьте версию. В конце концов настройте CI для создания и выпуска приложения.
  • Строим для производства.

Не так быстро

Перед созданием приложения я должен кое-что удостовериться. Для Android мне нужно производственное хранилище ключей (я уже объяснял, как его получить выше). Для iOS процесс немного дольше. Предполагая, что у меня есть учетная запись разработчик Apple, я должен сделать следующее.

(Чтобы протестировать приложение на устройстве во время разработки, мне пришлось бы повторить тот же процесс снова)

  • Мне нужно зайти на developer.apple.com и войти в систему

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

  • Я нажимаю на значок плюса в правом верхнем углу и затем шаг за шагом следую указаниям мастера. Все очень просто.

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

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

Наконец-то я могу создать приложение для производства: D

tn appstore -output-dir “dist” -no-prompt -log-level warn
tn playstore -output-dir “dist” -no-prompt -log-level warn

Приведенные выше команды выведут два созданных приложения (ipa, apk) в папку dist приложения.

Чтобы опубликовать его, мне нужно перейти в Консоль разработчика Android и iTunes connect. Публикацию в этой статье рассматривать не будем.

Еще несколько преимуществ использования триметила

Google Analytics автоматически отслеживает просмотры экрана, неперехваченные исключения и события
Sentry, автоматическая регистрация исключений
Полная интеграция с HTTPS / SSL, SSL-закрепление

Просмотр - это XML-файл, представляющий макет элементов пользовательского интерфейса.
Контроллер - это файл javascript, код которого соответствует представлению.
Стиль - это файл tss, содержащий все стили Представление.
Каждый элемент пользовательского интерфейса можно назвать представлением.

Вы можете скачать код приложения на github https://github.com/progress44/trimethyl-guide

Если у вас есть вопросы, прокомментируйте или откройте вопросы на github.

Спасибо, что дочитали до этого места, и ждите новых интересных статей :)