Pull to refresh
VK
Building the Internet

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

VK corporate blog .NET *Development for Windows Phone *

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


В мире мобильных операционных систем первые два места сейчас разделяют 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-систем абсолютно другой, здесь немного другие оценки по количеству пользователей. Но даже через 7 месяцев после официального запуска Windows 8, то есть на начало июня, она уже занимает 5,10%; это уже больше, чем самая популярная Mac OS X 10.8.



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



Введение


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

Немного о терминологии. Когда речь будет идти о WP7, будут иметься в виду две версии платформы — 7.5 и 7.8. В некоторых случаях также отдельно будет идти речь о Windows Phone 8, в которой есть некоторые отличия от предыдущей версии.

Хуже дела обстоят с терминологией по Windows 8. В этой статье речь пойдет о ее тач-составляющей, которая сначала называлась Metro UI, потом — Modern UI. При этом планшетная версия операционной системы, где есть только Modern UI, называется Windows RT, а приложения называются Store Apps (приложения для Магазина Windows). Все это многообразие терминов появилось после выхода Windows 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 для всех трех систем. То есть, используя API из этого подмножества, можно разрабатывать код под три системы одновременно.

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

MVVM


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



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

View – это то, как данные выглядят в интерфейсе, т.е. какими их видят пользователи.

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



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

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

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

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

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

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

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



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

Теперь перейдем к чуть более расширенной схеме.



В левой части представлены платформозависимые проекты. Это проекты для Windows Phone 7, Windows Phone 8 и Windows 8. В правой части рисунка представлены платформонезависимые проекты: ViewModels, реализующие единую логику для всех программ, Models, хранящие данные в едином виде, и DataHandlers, обрабатывающие данные единым образом. Фактически это 6 проектов в одном Solution. LowLevel-функции реализуются в каждом отдельном проекте по-своему, т.е. имеются 3 разные реализации. Дальше DataHandlers работают с моделями, модели – с 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. Любой другой функционал (проверка целостности, первичная обработка и т.п.) должен быть перенесен в DataHandler.

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

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


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

Библиотека портируемых классов содержит классы 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. Где хранить данные – зависит исключительно от вас. Если вы разрабатываете под все три платформы, то мой вам совет – используйте для Windows Phone-ов одинаковое хранилище, то есть IsolatedStorageFile. Если вы Windows Phone 7 в силу каких-то особенностей не рассматриваете как платформу, для которой вы будете разрабатывать приложение, то используйте ApplicationData, который есть для Windows 8 и для Windows Phone 8.

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



Я бы рекомендовал работу с настройками делать самостоятельно: делайте свой словарь настроек, сериализуйте его тем сериализатором, который вам нравится, и сохраняйте на локальный диск.

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

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



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

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



Быстро в плане проектирования, но медленно в плане пользования. Если у вас вдруг данных из сети пришло достаточно много, то пока произойдет сначала их первичная обработка в DataHandler, а потом дополнительная возможная обработка во 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(() =>
   {
      …
   }

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

Надеюсь, что после прочтения этой статьи вы уже можете ответить на вопрос:

Tags:
Hubs:
Total votes 88: ↑56 and ↓32 +24
Views 25K
Comments Comments 16

Information

Founded
Location
Россия
Website
vk.com
Employees
5,001–10,000 employees
Registered
Representative
Миша Буданов