Кросс-платформенное программирование под современные мобильные Windows платформы

от автора

Актуальность

В мире мобильных операционных систем, первые два места сейчас разделяют Android и iOS. По разным метрикам и оценкам разных компаний мы можем отнести первое место то к одной операционной системе, то к другой, но в том, что они лидируют, сомнений нет. Но как на любой олимпиаде, у нас есть еще бронза. Попробуем определиться с ней. Symbian, лидирующая по всем показателям еще пару лет назад, постепенно с рынка ушла. Blackberry – это, в основном, бизнес-пользователи, и, в основном, в Америке; в остальном мире она не так распространена. Тенденции показывают, что третье место сейчас достается Windows Phone. И вот тут у каждого разработчика встает вопрос, будь он частный разработчик, или будь он компания:

Ответ «Да!», и сейчас расскажу почему. Меня зовут Вадим Балашов, я разработчик почты для Windows 8 и для Windows Phone, и я занимаюсь разработкой под мобильные Windows начиная с PocketPC 2003.

Доля продаж Windows Phone растёт. В 2012 году она росла быстрее, чем в 2011. Особенно если посмотреть данные за последние несколько месяцев, когда вышла Windows Phone 8, там рост был ещё более динамичным. Более того, по данным компании IDC, которая оценивает количество проданных девайсов, в некоторых странах Windows Phone перешагнула отметку в 10% по продажам, и доля продаж уже в 26 странах обогнала долю продаж Blackberry, а в 7 странах даже обогнала iOS. По прогнозам той же IDC, суммарная доля продаж Windows Phone на данный момент, это 3,2%, а через 4 года она будет 11,4%. Нет сомнений в том, что платформа набирает обороты.

Теперь посмотрим на Windows 8. Рынок desktop-систем абсолютно другой, здесь немного другие оценки по количеству пользователей. Но, даже через 8 месяцев после официального запуска Windows 8, то есть на начало июля, она уже занимает 5,10%, это уже в 1.5 раза больше, чем самая популярная Mac OS X 10.8.

При этом доля Windows 8 продолжает расти не сбавляя темпов:

Введение

В октябре 2010 была представлена Windows Phone 7. Она, фактически, стала первым продуктом, который реализовал идеологию Metro, первые идеи которой чуть раньше появились в Zune Player и в X-Box, но там она была еще не до конца сформулирована. Windows Phone 7 уже вышла полностью с гайдами «как нужно делать», «как нужно оформлять» и «что делать не нужно». Поэтому именно она задала тренд. Следующей, исторически, вышла Windows 8, которая, в силу специфики desktop систем, имеет свои особенности: это большой экран, немного другие задачи, возложенные на приложения. И последним подтянулся Windows Phone 8, который, естественно, полностью вобрал в себя API Windows Phone 7 для обратной совместимости старых приложений, но при этом частично вобрал в себя API из ядра Windows RT, лежащего в основе Store Apps.

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

Преимущества единого проекта очевидны: это единая реализация бизнес-логики, это единый функционал, это единый user-experience при разном пользовательском интерфейсе. То есть, будь то телефон, будь то планшет, пользователь, делая одни и те же действия, получает одни и те же результаты и, естественно, остается доволен. С продуктовой точки зрения – это меньшее суммарное время разработки. То есть, все 3 платформы охватываются разом. Конечно, время больше, чем на любую из трех платформ отдельно, но суммарное время меньше. Из недостатков: бОльшая сложность в начале проекта — необходимо сразу развернуть всю архитектуру, о которой речь пойдет ниже; в течение поддержки и разработки проекта у вас чуть более сложная архитектура. Соответственно, проект сложнее поддерживать, сложнее вводить в курс дела новых разработчиков. Так же иногда приходится идти на компромиссные решения по функционалу. То есть, если какая-то платформа что-то не поддерживает совсем, то нужно либо самостоятельно реализовать недостающий функционал, если это возможно, либо изменить поведение программы так, чтобы оно укладывалось в рамки ограничений всех платформ.

MVVM

Теперь немного об MVVM-патерне. Коллегами с платформы iOS мне был уже дважды задан вопрос: «Почему не сервис-локатор?». MVVM – это, наверное, следующий шаг после идеологии «сервис локатор», где данные и методы обработки данных расположены вместе. Здесь они разделены.

Model (модель) – это только данные, т.е. простейшие классы, у которых есть наборы полей, хранящих данные. Так же, возможно, какие-то простейшие обработки полей, или поля, вычисляемые на основе других полей, например, поле FullName, которое само конкатенирует свойства FirstName и LastName.
View – это то, как данные выглядят в интерфейсе, то есть, какими их видят пользователи.
ViewModel – это то, что связывает View и Model, т.е., фактически, обработка данных и представление в UI. ViewModel подготавливает данные для их правильного отображения в UI и изменяет модели, как результат действий пользователя.

Расширим схему, добавим сюда Data Handler — это метод, который будет вести подготовку данных. LowLevel – это функции самого низкого уровня, которые невозможно сделать кроссплатформенными. В данной статье примерами таких функций будут функции работы с диском и с сетью.
Рассмотрим работу этой схемы на примере работы почтового приложения. Возьмем направление против часовой стрелки, и рассмотрим случай получения письма:

  • низкоуровневая функция работы с сетью получает массив байт по сети. Фактически, это JSON, то есть текстовая строка, но никакой дополнительной обработки строки не происходит;
  • полученная строка передается в DataHandler. В Data Handler происходит парсинг JSON, инициализация объектов. Так же возможна первичная обработка данных, например разворачивание HTML сущностей;
  • далее созданная Model помещается в какой-либо контейнер в памяти, где и находится на протяжении жизненного цикла программы;
  • когда пользователь выбирает письмо, во ViewModel происходит дополнительная обработка, например, тело письма, которое приходит в виде div-тегов, заворачиваем в html, c тэгами Head и Body; добавляем Java-Script для взаимодействия с интерфесом; переопределяем необходимые для отображения в компактном варианте стили. Когда данные подготовлены, ViewModel уведомляет UI, что данные можно отобразить;
  • UI обращается ко View, где описана компоновка, то есть расположение элементов тела письма, заголовков, расположение получатели и вложений. Так же во View описаны стили, то есть, каких размеры и начертания шрифтов будут использоваться, каких цветов будут подписи и элементы. Анимации объектов тоже описываются во View, т.е. определяется как элемент появится на экране, со смещением (Slide) или с растворением (Fade).

Допустим, пользователю письмо очень понравилось, он хочет пометить его флагом. Идём в обратную сторону:

  • пользователь нажимает на флаг в UI; во View описано, какое событие должно быть активировано;
  • во ViewModel срабатывает команда о том, что нужно изменить объект;
  • состояние Model меняется, то есть там выставляется флажок;
  • Data Handler формирует запрос на установку флага на сервере;
  • LowLevel функция непосредственно передает запрос на сервер.

Так как мы говорим о кроссплатформенной разработке и платформозависимых компонентах, то первый явный зависимый компонент, это View. Очевидно, что на планшетных устройствах и на телефонах наша программа должна выглядеть по-разному. Второй зависимый элемент – это низкоуровневые функции. Специфика платформ нас вынуждает использовать разные функции. Например, работа с локальным хранилищем и передача данных по сети реализована по-разному.

Платформонезависимые компоненты, соответственно – это Model, ViewModel и Data Handler.

Model, т.е., письмо – что на телефоне, что на планшете, одинаково. Оно с сервера приходит в одинаковом виде, у нас есть определенный набор полей, с которыми будет вестись работа.

ViewModel – это обработка данных письма и подготовка к его отображению. Если есть необходимость завернуть тело письма в html – это необходимо делать на всех платформах, пусть немного по-разному. Так же реакция на поведение пользователя должна быть единой для всех платформ для создания единого user experience.

Data Handlers – это подготовка данных из вида, в котором они передаются по сети или хранятся на диске, в модели, с которым происходит работа в программе. Например, если нам приходит JSON от сервера, то он приходит на всех платформах. Нам нужно его распарсить и создать модель. Это всегда происходит одинаково.

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

В левой части представлены платформозависимые проекты. Это проекты для Windows Phone 7, для Windows Phone 8 и для Windows 8. В правой части рисунка представлены платформонезависимые проекты: ViewModels реализующие единую логику для всех программ, Models хранящие данные в едином виде и Data Handlers обрабатывающие данные единым образом. Фактически это 6 проектов в одном Solution. LowLevel функции реализуется в каждом отдельном проекте по-своему, т.е. имеются 3 разные реализации. Дальше Data Handler работают с моделями, модели с view-моделями, и view-модели должны работать с view, то есть с интерфейсом. Соответственно, поскольку интерфейс в трех проектах разный, ViewModels работают каждый с конкретной реализацией View в каждом проекте.

Реализация кроссплатформенного MVVM

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

Так как в приложениях чаще всего не одна ViewModel, то реализовывать этот интерфейс в каждой ViewModel нецелесообразно и лучше определить единый класс ViewModelBase.
Частный случай – это списки. Для списков удобно использовать ObservableCollection, который автоматически уведомляет UI об изменении состава элементов списка.

Преимущество этого контейнера в том, что вам не нужно отдельно уведомлять UI о том, что что-то поменялось. Недостаток состоит в том, что если вы делаете большое количество операций, то View у вас обновляется слишком часто и это может сказаться на производительности приложения.
Теперь обратная ситуация – это когда из View необходимо получить команду во ViewModel, как в случае с пометкой флагом.

Такая команда должна быть унаследована от интерфейса ICommand. Здесь та же самая ситуация – мы создаем базовую команду CommandBase и дальше работаем с ней, т.к. в любой программе команд еще больше, чем ViewModel’ей.

Теперь об обработчиках данных. Обработчики данных, фактически, занимаются тем, что преобразуют сырые данные, которые нам пришли по сети в модели. И обратная ситуация, когда она преобразуют изменения или команды, пришедшие из ViewModel’ей, как в случае установки флага, в команды серверу.

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

Низкоуровневые функции – это те самые платформозависимые функции, которые невозможно сделать платформонезависимыми. Их нужно сделать как можно меньше. Они должны нести в себе минимум функционала, который действительно нельзя сделать кроссплатформенным. Например, для работы с диском/сетью низкоуровневые объекты должны предоставлять две функции SaveBytes/SendBytes и LoadBytes/ReceiveBytes. Любой другой функционал (проверка целостности, первичная обработка и т.п.) должен быть перенесен в Data Handler.
Иными словами, чтобы выделить LowLevel-функцию из общего функционала, необходимо понять, что точно не может быть оставлено в Data Handler’е. Этот минимум и будет Low Level функцией.

Платформозависимые компоненты

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

Библиотека портируемых классов содержит классы HttpRequest и HttpWebRequest. Этих классов достаточно для того, чтобы полностью реализовать работу с сервером по протоколу Http. Однако они не имеют полную функциональность, которую можно было бы использовать. Например, в API для Windows 8 есть класс HttpClient, который поддерживает сжатие трафика, и плюс позволяет очень удобно работать с POST-запросами. То есть, фактически, в класс передается объект, а тот формирует POST-запрос. В случае с HttpRequest POST-запрос необходимо сформировать в соответствии с RFC, практически, вручную.

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

Локальное хранилище. Windows Phone пошел по пути iPhone-а и все программы имеют доступ только к своему IsolatedStorage. То есть одна программа не имеет доступа к данным другой программы. Работа с файлами происходит с помощью класса IsolatedStorageFile.
Windows 8 в силу того, что это ветвь, которая вышла от desktop-ной операционной системы, не ограничивает доступ к диску так жестко, как в телефоне. Для работы с диском есть класс ApplicationData.

Интересная ситуация складывается с Windows Phone 8. Наследуя часть API из Windows RT, Windows Phone 8 имеет как доступ к хранилищу с помощью IsolatedStorageFile так и с помощью ApplicationData. Где хранить данные – зависит, исключительно, от вас. Если вы разрабатываете под все 3 платформы, то мой вам совет – использовать для Windows Phone-ов одинаковое хранилище, то есть IsolatedStorageFile. Если вы Windows Phone 7 в силу каких-то особенностей не рассматриваете как платформу, для которой вы будете разрабытывать приложение, то используйте ApplicationData, которая есть для Windows 8 и для Windows Phone 8.

Отдельная ситуация, когда хранить необходимо настройки. В общем виде хранение настроек никак не отличается от хранения любых других данных, т.к. настройки хранятся в том же самом дисковом хранилище. Однако для настроек системой предоставляются обертки, позволяющие без лишних усилий хранить словари ключ-значение. Ситуация схожа с дисковым хранилищем: для Windows Phone есть IsolatedStorageSettings.ApplicationSettings, и ApplicationData.Current.LocalSettings для Windows 8. И снова интересная ситуация с Windows Phone 8: API содержит оба класса для работы с настройками. Но, на практике, если мы пытаться сохранить настройки в LocalSettings, будет сгенерировано исключение NotImplementedException. То есть, у нас ссылка на класс настроек есть, но на практике за этой ссылкой ничего не стоит.

Я бы рекомендовал работу с настройками делать самостоятельно: делайте свой словарь настроек, сериализуйте его тем сериализатором, который вам нравится и сохраняйте на локальный диск.
Теперь немного о диспетчере потоков. Low-level-функции, что в Windows Phone, что в Windows 8 реализованы так, что даже если вы посылаете запрос в сеть или на диск в UI-потоке, то ответ вам в любом случае придет в фоновом потоке. Это сделано для того, чтобы долгие операции не блокировали UI. Это удобно тем, что разработчику не нужно заботиться о создании фоновых потоков, в которых делать обращение к сети. При этом, отобразить данные (которые были получены и уже каким-то образом обработаны) в интерфейсе пользователя, можно только в UI-потоке. И вот тут возникает вопрос, «В какой момент переходить из фонового потока в UI-поток?».

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

Проблема состоит в том, что dispatcher специфичен для платформы. То есть Windows 8 содержит dispatcher и Windows Phone содержит dispatcher. И суть их работы именно в том, что они исполняют делегаты переданные из background потоков в UI-потоке. Но при этом реализованы они по-разному: они расположены в разных NameSpace’ах, обращение идёт к методам с разными названиями, они принимают разные параметры.

Рассмотрим моменты, в которые мы можем сделать диспетчеризацию? Можно сделать неявную диспетчеризацию сразу в LowLevel функции, которая так же платформозависима: обернуть результат обработки данных в диспетчер и передать в Data Handler уже в UI-потоке. Очень удобно тем, что у нас Data Handler, Model, ViewModel — никто не будет знать ничего о потоках, dispatcher-ах и так далее, все просто и прозрачно.

Быстро в плане проектирования, но медленно в плане пользования. Если у вас, вдруг, данных из сети пришло достаточно много, то пока произойдет сначала их первичная обработка в Data Handler, а потом дополнительная возможная обработка во ViewModel, все это время UI-поток будет занят, а приложение будет выглядеть зависшим. Если в этот момент в приложении выполнялись какие-то анимации, они замирают, потом продолжаются после обработки. С точки зрения пользователя приложение выглядит зависшим.

В случае явной диспетчеризации, необходимо иметь доступ к диспетчеру из любой ViewModel. Удобнее всего это сделать в некотором ViewModelLocator, то есть месте, которое объединяет все модели, и к этому локатору все модели могут иметь доступ. Таким образом, первичная обработка данных, формирование моделей и подготовка данных к отображению могут производиться в фоновом потоке. Интерфейс пользователя при этом отзывчив, прогресс бар бегает, spinner крутится, пользователь понимает, что программа занята, но не зависла. Ровно в тот момент, когда у нас данные уже готовы, то есть это еще до выхода из ViewModel’и, необходимо передать управление в UI поток и сообщить интерфейсу о том, что данные обновились, их можно отобразить.

Cамый простой способ получить кроссплатформеннй доступ к диспечеру, это сделать во ViewModelLocator свойство типа action, например, DoDispatched и дальше при запуске приложения, это свойство проинициализировать.

Для Windows Phone 7 и 8 оно инициализируется следующим образом:

DoDispatched = action => Dispatcher.BeginInvoke(action);

В случае для Windows 8 мы делаем все примерно то же самое, только, в соответствии со спицификой вызова, дополнительно указываем приоритет:

DoDispatched = action => Dispatcher.RunAsync(priority, action.Invoke);

Из различия вызовов явно видно, что диспетчер сильно платфорозависим, однако заворачивая его в action ViewModel’и могут не заботиться их разнице. ViewModel просто вызывает DoDispatched, передает внутрь делегат того, что нужно исполнить UI-потоке и все:

DoDispatched(() =>
   {
      …
   }

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

ссылка на оригинал статьи http://habrahabr.ru/company/mailru/blog/185218/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *