27 марта 2018

Наш первый опыт разработки на Xamarin

Полгода назад мы завершили разработку внутрикорпоративного мобильного приложения “Company Staff”. Company Staff (далее – CS) – это приложение для сотрудников нашей компании, помогающее нам искать коллег, получать информацию о них (дата рождения, вишлисты, достижения и т.д.), узнавать последние новости компании и многое другое.

Xamarin development Noveo

Все началось с того, что у нашей .NET-команды между завершенным проектом и запланированным стартом нового выдалось свободное время, которое решили потратить на изучение новой для нас технологии – Xamarin. И чтобы не тратить время зря, мы хотели сразу опробовать эту технологию на приложении, которое было бы полезно Noveo. Долго думать не пришлось – мы вспомнили о Company Staff. Это приложение уже существовало в виде Web- и iOS-версий и, соответственно, готового Web API. Также существовало Android-приложение, которое просто импортировало контакты в телефон. Более того, наши дизайнеры уже потрудились и приготовили замечательный дизайн для нового Android-приложения, так что выбор был очевиден.

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

Xamarin

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

Xamarin – это фреймворк для кросс-платформенной разработки мобильных приложений, основанный на open-source реализации платформы .Net – Mono. С помощью Xamarin можно разрабатывать приложения для Apple-, Android- и Windows-девайсов, используя один язык – C#, и одно IDE – Visual Studio или Xamarin Studio. Также данная платформа предоставляет полный доступ к SDK, единую среду выполнения и даже родные механизмы создания UI. Приложения на Xamarin практически не уступают в производительности и внешнему виду нативным.

Многие до сих пор полагают, что кроссплатформенная разработка должна подчиняться фразе “write once, run everywhere”, что подразумевает, что один код должен работать на нескольких платформах без изменений. Однако такой подход обычно приводит к приложениям с ограниченной функциональностью (многие фичи, присущие той или иной платформе, недоступны) и универсальным UI, который не вписывается ни в одну из мобильных платформ.

Xamarin, в свою очередь, нельзя отнести к категории “write once, run everywhere”, хотя бы потому, что одна из его важнейших функций – возможность реализовать нативный UI для каждой платформы отдельно. При этом большая часть кода, которая включает в себя бизнес-логику, слой данных, доступ к ним и др., остаётся общей для всех платформ.

Как это работает?

Исходники на C# становятся нативными приложениями совершенно различными путями на каждой платформе. Главное различие заключается в предварительной компиляции:

  • Для iOS используется Ahead-of-Time (AOT) компиляция. Xamarin.iOS-приложение компилируется прямо в машинный код (ARM Assembly language). Поскольку Apple запрещает динамическую генерацию кода, накладывается ряд ограничений (см. Xamarin.iOS Limitations).
  • Для Android все немного сложнее. Для выполнения приложений в Android используется виртуальная Java-машина Dalvik. Нативные Java-приложения компилируются в промежуточный байт-код, который интерпретируется Dalvik’ом в команды процессора во время исполнения программы (Just-in-time (JIT) компиляция). Xamarin в свою очередь компилирует C#-код в промежуточный байт-код для виртуальной машины Mono, которая упаковывается в приложение. При запуске Xamarin-приложения две виртуальные машины Mono и Dalvik работают параллельно, обмениваясь данными через JNI.
  • Для Windows все как обычно. C# компилируется в IL и выполняется встроенной средой выполнения. Инструменты Xamarin’a не нужны. Также можно отметить, что для приложений Universal Windows Platform (UWP) остаётся функция .NET Native, которая ведет себя аналогично AOT-компиляции для iOS.

Результатом скомпилированного Xamarin-приложения являются .app-файл для iOS, .apk для Android и .appxbundle для UWP. Эти файлы неотличимы от пакетов приложений, созданных стандартными для платформ IDE, и развертываются точно таким же образом.

Xamarin.Forms

Первый же вопрос, который мы себе задали, – зачем для нашего приложения нужен Xamarin? Ведь по большому счету все, что должно уметь приложение, – это отобразить данные, которые приходят с сервера. По предварительным оценкам, только 25-30% приложения общие для всех платформ, а всё остальное – UI, который нужно реализовывать отдельно для каждой платформы.

Хотя целью Company Staff первоначально было обучение новой технологии, чтобы максимально проникнуться платформой и в дальнейшем заниматься серьёзными коммерческими проектами, мы решили отнестись к “тренингу” как к продукту. Тем более, что он действительно имел практическое применение в реалиях нашей компании. И поэтому нужно было сначала ответить на вопрос целесообразности применения Xamarin.

К счастью, разработчики Xamarin позаботились о нас и предоставили  Xamarin Forms API.

Xamarin.Forms – это “надстройка” над Xamarin.iOS и Xamarin.Android, которая позволяет создавать кросс-платформенный пользовательский интерфейс. Такой интерфейс можно создать как с помощью XAML, так и кодом С#. Устроено это довольно просто: верстка реализуется с помощью Xamarin.Forms-компонентов, которые транслируются в нативные компоненты с помощью специальных классов-рендереров на каждой целевой платформе. Кроме того, важным достоинством является полноценная поддержка MVVM (Model-View-ViewModel) – наличие привязки данных (data binding) и управление ими. Таким образом, в идеальном случае разработка на Xamarin.Forms немногим отличается от разработки приложений UWP/WindowsPhone/WPF.

Первые шаги

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

Для Xamarin Forms есть 3 архитектурных подхода для общего кросс-платформенного кода:

  • Shared Asset Projects (SAP) – довольно простой подход. Такой проект не компилируется в отдельную сборку, а “встраивается” в проекты, в которых используется. Такой подход позволяет использовать платформо-зависимый код наряду с общим, используя директивы компилятора #if.
  • Portable Class Libraries (PCL) – кроссплатформенная библиотека. В настройках такой библиотеки необходимо указать целевые платформы, в которых она будет использоваться: Xamarin development NoveoВ результате будут доступны только те функции, которые находятся в пересечении указанных платформ. PCL не позволит использовать какой-либо платформо-зависимый функционал, таким образом гарантируя, что PCL код запустится на всех указанных платформах.
  • .NET Standard Libraries  – более новая технология, упрощенная версия PCL, но с более широким функционалом. На момент разработки нашего приложения .NETStandard еще не появился в общем доступе, и мы, естественно, не могли рассматривать его. Хотя стоит отметить, что сегодня мы бы выбрали именно этот подход, поскольку он более гибок и прогрессивен.

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

Верхняя структура проекта проста, определилась сразу и почти не изменилась до релиза проекта:

Xamarin development Noveo

Xamarin development Noveo

— CompanyStaff – PCL библиотека Forms, которая содержит общие для всех платформ модели (Models), вью-модели (ViewModels), локализацию, общие настройки приложений, бизнес-логику и, собственно, сами Xamarin.Forms вью (Views);

— CompanyStaff.Droid, CompanyStaff.iOS, CompanyStaff.UWP – по большому счету это и есть нативный Xamarin. Здесь содержится реализация нативного функционала приложения, и именно эти проекты затем собираются в пакеты приложений под указанные платформы. Код данных проектов – это C#-обертки над нативными классами. В нашем случае классы этих проектов можно разделить на две категории:

1) Рендереры – классы, отвечающие за отображение визуальных компонент. Они содержат ссылки на PCL-объекты и выставляют свои свойства согласно этим объектам. В дальнейшем рендереры разворачиваются в нативные контролы.

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

Для начала нужно создать класс в PCL, унаследованный от Entry.

/// <summary>
/// Необходим для кастомных рендереров для того, 
/// чтобы они ссылались именно на данный тип.
/// </summary>
public class NoHelperEntry : Entry
{
    // Поскольку новый класс унаследован от Entry, нет необходимости 
    // реализовывать дополнительные поля и методы. 
    // Entry содержит в себе всё, необходимое для рендереров.
    // При желании, конечно, можно добавить дополнительные проперти, 
    // события и всё остальное, чтобы передать больше необходимой 
    // информации между общим и платформо-зависимым кодом.
}

Затем просто добавляем новый контрол в код пользовательского интерфейса (в нашем случае интерфейс описан в XAML):

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:controls="clr-namespace:CS.Controls;assembly=CS"
             x:Class="CompanyStaff.Views.TestPage">

    <controls:NoHelperEntry x:Name="NoHelperEntry" />

 </ContentPage>

Это все, что нужно в PCL для нашего простого примера. Переходим к реализации рендереров. Для этого необходимо всего три вещи:

  • Создать унаследованный от EntryRenderer класс, который отвечает за отображение нативного элемента.
  • Переопределить метод OnElementChanged, в котором необходимо описать логику кастомизации контрола. Этот метод вызывается при создании соответствующего контрола Forms.
  • Добавить атрибут ExportRenderer на класс рендерера, чтобы “зарегистрировать” наш класс в Xamarin.Forms для рендеринга определенного контрола.

Вот так выглядит код рендереров для нашего поля ввода:

iOS:

[assembly: ExportRenderer(typeof(NoHelperEntry), typeof(NoHelperEntryRenderer))]
namespace CompanyStaff.iOS.Renderers
{
    public class NoHelperEntryRenderer : EntryRenderer
    {
        protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
        {
            base.OnElementChanged(e);

            //Control - "нативный" UIKit.UITextField
            if (Control != null)
            {
                Control.SpellCheckingType = UITextSpellCheckingType.No;
                Control.AutocorrectionType = UITextAutocorrectionType.No;
                Control.AutocapitalizationType = UITextAutocapitalizationType.None;
            }
        }
    }
}

Android:

[assembly: ExportRenderer(typeof(NoHelperEntry), typeof(NoHelperEntryRenderer))]
namespace CompanyStaff.Droid.Renderers
{
    public class NoHelperEntryRenderer : EntryRenderer
    {
        protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
        {
            base.OnElementChanged(e);

            //Control – “нативный” EntryEditText
            if (Control != null)
            {
                Control.SetRawInputType(InputTypes.TextFlagNoSuggestions);
            }
        }
    }
}

UWP:

[assembly: ExportRenderer(typeof(NoHelperEntry), typeof(NoHelperEntryRenderer))]
namespace CompanyStaff.UWP.Renderers
{
    public class NoHelperEntryRenderer : EntryRenderer
    {
        protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
        {
            base.OnElementChanged(e);

            //Control – почти нативный FormsTextBox
            if (Control != null)
            {
                Control.IsSpellCheckEnabled = false;
                Control.IsTextPredictionEnabled = false;
            }
        }
    }
}

2) Реализация DependencyService’ов – реализация функционала, не связанного с UI, позволяющая работать с данным функционалом из PCL на уровне абстракций.

Одной из важных функций в CS является возможность добавления контактов сотрудника в телефонную книгу устройства. Поскольку реализация данного функционала сильно “платформо-зависима”, а доступ к нему нам необходим из PCL кода, то наиболее удобный способ – использование DependencyService’ов.

Для начала нужен интерфейс в PCL для операций добавления контактов. В нашем приложении он выглядит так:

public interface IContacts
{
    /// <summary>
    /// Добавление одного контакта
    /// </summary>
    /// <param name="contact">Экземпляр модели контакта</param>        
    void AddContact(ContactData contact);

    /// <summary>
    /// Добавление списка контактов в бэкраунд потоке.
    /// </summary>
    /// <param name="employees">Список контактов</param>
    /// <param name="p">Колбэк завершения операции</param>
    void AddContactsInBackground(List<ContactData> employees, Action p);
}

Затем нужно реализовать данный интерфейс на каждой платформе. Общая схема одинакова (за исключением .NET Native компиляции UWP проектов). Пример для Android:

//Каждая имплементация интерфейса должна быть зарегестрирована на уровне пространства имён в DependencyService матадата-атрибутом:
[assembly: Xamarin.Forms.Dependency(typeof(ContactsExt))]
namespace CompanyStaff.Droid.Extensions.Contacts
{
    public class ContactsExt : IContacts
    {
        public async void AddContact(ContactData contact)
        {
            ...
            //нативная реалиазция
            ...          
        }

        public async void AddContactsInBackground(List<ContactData> contacts, Action callback)
        {
            ...
            //нативная реалиазция
            ... 
        }
    }
}

Далее из PCL кода достаточно вызвать фабричный метод Get<T>() у статического класса DependencyService для выполнения описанных операций:

var contacts = DependencyService.Get<IContacts>();
contacts.AddContact(new ContactData() {...});

В CS с помощью DependencyService’ов реализована работа с контактами, ориентацией девайса, социальными сетями, локализацией, защищенным хранилищем данных и др.

CompanyStaff.Dependencies – сюда мы вынесли интерфейсы DependencyService’ов.

— CompanyStaff.ServicesContract и CompanyStaff.Services – интерфейсы и реализация работы с web-сервисом соответственно.

— Bootstrapper – регистрация сервисов в IoC контейнере для Dependency Injection.

Затем нужно было выбрать фреймворк, который упростит работу с MVVM и легко встроится в описанную выше архитектуру. От данного фреймворка нам требовался следующий функционал:

  • базовые классы для работы с MVVM (ViewModelBase, Commands, Events);
  • Dependency Injection, в первую очередь для внедрения сервисов во вью-модели;
  • реализация межстраничной навигации.

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

Существует несколько наиболее популярных фреймворков для данных целей:

  • MVVMLight;
  • Caliburn.Micro;
  • MVVMCross;
  • Prism.

Каждый из этих фреймворков портирован для Xamarin.Forms. Со всеми, кроме последнего, мы имели опыт работы раньше на других платформах – UWP, WindowsPhone, WPF.

MVVMCross и Prism – большие и мощные фреймворки с большой поддержкой коммьюнити. Поскольку и Xamarin, и Xamarin.Forms быстро развиваются, количество форков и звездочек на github’e на данный момент и в дальнейшем стало определяющим фактором при выборе сторонних инструментов.

Мы решили одновременно попробовать оба фреймворка и выбрать более удобный. С MVVMCross сразу возникли проблемы c инициализацией на UWP. Prism, в свою очередь, завелся сразу и без каких-либо трудностей. Долго не разбираясь, оставили Prism. Тем более, что мы давно хотели его «пощупать».

В итоге – мы остались довольны выбором Prism, в первую очередь – благодаря удобной реализации навигации MVVM-путём, хоть и пришлось повозиться с навигацией на уровне Xamarin.Forms.

О том, как мы преодолели трудности, связанные с навигацией и не только, читайте во второй части!

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

Читайте в нашем блоге

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: