Pull to refresh
136.6
2ГИС
Главные по городской навигации

Новый 2ГИС под Windows Phone: архитектура и стек технологий

Reading time 9 min
Views 13K
картинка для привлечения внимания

Шел 2013 год. За доллар давали 30 рублей, а я устроился в компанию 2ГИС разрабатывать под Windows Phone. Мне удалось поучаствовать в запуске почти готового к тому времени приложения 2ГИС, которое в скором времени стало доступно нашим пользователям в Marketplace.

Была у этого приложения одна досадная особенность: оно работало на нашем WebAPI, и, соответственно, требовало подключения к Интернету. Поэтому почти сразу возникла необходимость научить 2ГИС под WP работать офлайн. А заодно решить другие насущные проблемы.

Причины всё переписать


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

Быстрая доставка данных


Предположим, 2ГИС узнал какую-нибудь новую информацию о вашем городе. Для того чтобы поделиться этой информацией с пользователями, мы проделывали такие вот действия.

  1. Учили приложение отображать новые данные.
  2. Все тестировали.
  3. Публиковали новую версию приложения в сторе.
  4. Выпускали обновление данных для города.

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

Быстрое появление новых фич


Допустим, мы придумали новый алгоритм поиска, который ищет лучше и быстрее. Хотелось, чтобы этот алгоритм появился в продукте без существенных трудозатрат со стороны команды Windows Phone.

Офлайн


Как я уже говорил, для прежней версии приложения нужен был Интернет.

картинка с нехорошим отзывом

Некоторых наших пользователей это слегка расстраивало, и поэтому новый 2ГИС для WP обязательно должен был работать офлайн.

Архитектура нового 2ГИС под Windows Phone


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

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

архитектура

Кроссплатформенное ядро


Верхний блок на картинке — это кроссплатформенное ядро, основа всего, точка входа во все наши алгоритмы и сервисы. Наш офлайновый бэкенд, которому мы задаем вопросы и получаем ответы. Эта штука обеспечивает нам работу без подключения к сети. Мы называем его кроссплатформенным, потому что можем собирать его не только под WP, но и под Windows, OS X, Linux, iOS и Android.

В соответствии с требованием быстро добавлять новые фичи, все новое, что появляется в 2ГИС, появляется в ядре и автоматически попадает в новый 2ГИС для WP (как и во все продукты, которые это ядро используют). Кроссплатформенное ядро разрабатывается на C++ очень умными ребятами из специальной команды по разработке кроссплатформенного ядра. Они молодцы, но больше я о них ничего писать не буду, потому что статья не про них, а про про Windows Phone.

UI


Самый нижний блок на диаграмме — это UI, фронтэнд, та часть, с которой работает пользователь. Она разрабатывается с помощью нативных для платформы инструментов (C# и XAML), чтобы максимально легко и качественно обеспечить пользователю привычный опыт взаимодействия с его любимой платформой. На момент написания этой статьи разработчикам было доступно несколько типов приложений под WP: Silverlight Application, Windows Phone (не Silverlight!) Application, Universal Application. Но когда мы только начинали работать над новым 2ГИС, был только Silverlight, поэтому неудивительно, что мы выбрали его.

Промежуточный слой


Для того чтобы подружить C++ и C#, научить ядро общаться с приложением, а приложение с ядром, необходим некий промежуточный слой в виде Windows Runtime Component, написанный на C++/CX.

Трехмерная карта на OpenGL


Продолжая разговор об офлайне, нельзя не сказать о нашей карте, которая тоже работает без подключения к интернету. Карта в 2ГИС — это довольно узнаваемый элемент. Она трехмерная, кроссплатформенная (тоже собирается под разные ОС) и написана на OpenGL.

К сожалению, OpenGL не поддерживается на WP, поэтому нам приходится использовать Angle для трансляции OpenGL вызовов в DirectX. Хочу отметить, что с интеграцией карты мы натерпелись много разного, но в итоге удалось-таки ее запустить на WP. Конечно, использование Angle накладывает свой отпечаток на производительность, но мы стараемся свести это влияние к минимуму.

Инструменты


Как в любом другом приложении, написанном на C#/XAML, все внутреннее устройство фронтэнда 2ГИС для WP подчиняется паттерну MVVM.

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

  1. Открываем любимый браузер. Например, Internet Exporer.
  2. Заходим на сайт caliburnmicro.com.
  3. Качаем Caliburn.Micro, устанавливаем и радуемся.

Если серьезно, то в 2013 году популярных MVVM фрэймворков, работающих на WP8 было не очень много. Фактически выбор стоял между Prism, MVVM Light и Caliburn.Micro. Prism слишком монструозный, подходящий больше для больших энтерпрайз приложений, MVVM Light, напротив, слишком уж light, и хотелось чего-то большего. А вот Caliburn.Micro нам пришелся по душе по следующим причинам.

Поддержка навигации

Обычно навигация между страницами в silverlight приложении выглядит примерно так:

NavigationService.Navigate(new Uri("/GroupPage.xaml?name=Administrators", UriKind.Relative));

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

var name = NavigationContext.QueryString["name"];

Caliburn.Micro позволяет ту же самую задачу решить следующим образом.

NavigationService.UriFor<GroupPageViewModel>()
         .WithParam(x => x.Name, "Administrators")
         .Navigate(); 

Код выглядит красиво, присутствует контроль типов, а параметр Name сразу записывается в соответствующее свойство ViewModel при навигации.

Поддержка сохранения состояния

Caliburm.Micro предлагает интересную инфраструктуру для сохранения состояния приложения. Например, для того чтобы определить стратегию сохранения состояния для GroupPageViewModel, достаточно определить вот такой класс.

public class GroupPageViewModelStorage : StorageHandler<GroupPageViewModel>
{
    public override void Configure()
    {
        Property(x => x.Name)
            .InPhoneState()
            .RestoreAfterActivation();
    }
}

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

Встроенная поддержка IoC контейнера

Сердцем Caliburn.Micro является встроенный IoC контейнер, с помощью которого реализуется паттерн dependency injection. При навигации между страницами ViewModel’и получают все необходимые сервисы с помощью constructor/property injection, и это чрезвычайно удобно. Всячески рекомендую.

Для WP — поддержка Pivot

Caliburn.Micro предоставляет инфраструктуру, при которой каждая страница контрола Pivot может быть отдельной View со своей отдельной ViewModel. Это очень удобно, позволяет декомпозировать логику и относительно просто наладить ленивую загрузку данных для вкладок Pivot.

Специальные методы во вьюмоделях, привязанные к жизненному циклу страницы

Класс Screen — базовый класс для большинства ваших ViewModel’ей имеет очень удобные методы OnInitialize, OnActivate, OnDeactivate и т.п., которые вызываются фрэймворком в момент создания экземпляра ViewModel’и при навигации на соответствующую страницу или при уходе с нее. Вы можете переопределять эти методы в своих ViewModel’ях и выполнять там какой-нибудь полезный код.

Открытый исходный код

Caliburn.Micro имеет открытый исходный код. Если вам чего-то не хватает во фрэймворке, всегда можно это дописать самому.

Низкий порог вхождения

Начать пользоваться Caliburn.Micro легко. Кроме того, он довольно легковесный, весь код Caliburn.Micro вполне реально изучить за день-два.

Сохранение состояния приложения


Стоит сказать, что мы несколько доработали механизм сохранения состояния приложения, используемый в Caliburn.Micro. По умолчанию Caliburn использует стандартные механизмы XML-сериализации, используемые в WP. Мы добавили поддержку бинарной сериализации с помощью SharpSerializer. Получилось удобно, быстро, и можно сериализовать практически все что угодно.

Быстрая доставка данных


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

Ответ в том, что вместе с новыми данными мы должны поставить и новый UI для их отображения. В нашем случае это XAML-шаблоны. Обычно XAML-шаблоны живут внутри самого приложения и поставляются вместе с ним, но мы хотим распространять XAML-ресурсы отдельно и совершенно независимо от самого приложения.

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

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

{
  "data": "Windows Phone"
}

Тут ничего особенного — это просто json.

Также мы распространяем (тоже отдельно от приложения) XAML-шаблон, необходимый для отображения этих данных.

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">

    <DataTemplate x:Key="TestIcon">
        <Path Width="42"
              Height="41"
              Stretch="Fill"
              Fill="{StaticResource PhoneForegroundBrush}"
              Data="F1 M 17,23L 34,20.7738L 34,37L 17,37L 17,23 Z M 34,55.2262L 17,53L 17,39L 34,39L 34,55.2262 Z M 59,17.5L 59,37L 36,37L 36,20.5119L 59,17.5 Z M 59,58.5L 36,55.4881L 36,39L 59,39L 59,58.5 Z " />
    </DataTemplate>

    <DataTemplate x:Key="EntryPoint">
        <StackPanel>
            <ContentPresenter ContentTemplate="{StaticResource TestIcon}"
                              Margin="0,0,0,12" />
            <TextBlock Text="{Binding [data]}" />
        </StackPanel>
    </DataTemplate>

</ResourceDictionary>

Здесь стоит отметить несколько моментов.

  • Этот шаблон на стороне приложения мы будем загружать в виде строки и парсить с помощью XamlReader.Load(). Из этого естественным образом вытекает то, что не любой XAML можно так использовать. Правильный XAML не должен содержать никакого code behind, никаких ссылок на x:Class, подписок на события и т.п.
  • Под предыдущее требование отлично подходят DataTemplates, поэтому мы будем использовать их.
  • Мы используем ResourceDictionary как контейнер для нескольких шаблонов.
  • Обратите внимание, как мы распространяем иконку TestIcon, — тоже в виде DataTemplate. А отображаем ее с помощью ContentPresenter.
  • Свойство Text текстового блока привязано к некоторому свойству data посредством привязки к индексатору некоторой ViewModel’и (о которой после). Обратите внимание, что в нашем json’е есть элемент с точно таким же именем, и это неспроста.

Допустим, уже в самом приложении у нас есть вот такая View.

<phone:PhoneApplicationPage x:Class="DynamicXaml.MainPage"
                            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                            xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
                            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                            xmlns:local="clr-namespace:DynamicXaml"
                            mc:Ignorable="d">
    
    <phone:PhoneApplicationPage.DataContext>
        <local:MainPageViewModel />
    </phone:PhoneApplicationPage.DataContext>

    <ContentControl Content="{Binding DynamicData}"
                    ContentTemplate="{StaticResource EntryPoint}" />

</phone:PhoneApplicationPage>

В этой View элемент ContentControl является точкой входа для отображения динамических данных. Здесь есть важное соглашение: ContentControl знает, что должен отобразить шаблон с ключом EntryPoint, и именно такой шаблон есть в нашем XAML’е, который мы распространяем отдельно. Собственно, имя ключа — это единственное, что приложение знает о шаблоне, который будет показывать.

Соответственно, для View определена ViewModel, которая реализует некоторую тестовую магию по загрузке динамического содержимого.

public class MainPageViewModel
{
    public MainPageViewModel()
    {
        // Загружаем с секретных серверов 2ГИС данные о городе.
        string json = LoadDynamicData();
        DynamicData = (JObject)JsonConvert.DeserializeObject(json);

        // Загружаем шаблон для отображения данных.
        string xaml = LoadDynamicXaml();

        // Парсим xaml.
        var resources = (ResourceDictionary)XamlReader.Load(xaml);

        // Добавляем шаблоны в ресурсы приложения.
        foreach (DictionaryEntry entry in resources)
        {
            Application.Current.Resources.Add(entry.Key, entry.Value);
        }
    }

    public JObject DynamicData { get; private set; }
}

Поясню несколько моментов.

  • В примере я использовал Json.Net для того, чтобы распарсить json и получить из него некоторый объект, пригодный для привязки данных. На самом деле для этих целей мы используем DynamicDataContext, но в данном примере для простоты используется JObject.
  • У JObject есть индексатор, принимающий строку — имя json-элемента, и возвращающий значение этого элемента.
  • Именно поэтому в шаблоне текст текстового блока привязывается к индексатору [data], который возвращает значение элемента data из json. Так обычный json становится вьюмоделью для нашего шаблона.

Если собрать этот пример и запустить его на своем любимом телефоне под управлением Windows Phone, можно увидеть такую картинку.

результат

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

Итоги


Мы сделали настоящий 2ГИС для WP: у нас есть трехмерная карта, подробный справочник организаций, кроссплатформенное ядро, а xaml-шаблоны мы распространяем независимо от приложения. И все это работает без подключения к Интернету. Если вы по каким-то причинам все еще не установили новый 2ГИС на ваши смартфоны с Windows Phone, самое время это сделать.
Tags:
Hubs:
+21
Comments 41
Comments Comments 41

Articles

Information

Website
2gis.ru
Registered
Founded
Employees
1,001–5,000 employees
Location
Россия