Xamarin + PCL + MVVM — как облегчить написание мобильных приложений под разные платформы

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

Прежде всего, что такое Xamarin?

Xamarin это коммерческий продукт, основанный на open-source проекте Mono, который позволяет использовать .Net framework, в том числе язык C#, для кроссплатформенных разработок.

Основные свойства:
  • C# для написания приложений под Android, iOs и Windows8. Поддержка LINQ и async/task
  • Native UI, Native Peformance — код компилируется под конкретную платформу и использует native UI контроли
  • Visual Studio и все его plugins, включая ReSharper, NUnit testing…
  • Xamarin Studio, которая похожа на Visual Studio, но доступна также и для Mac
  • Использование .Net библиотек
  • Использование других готовых native компонент, обвернутых в C#
  • Графический редактор для Андроида

image

MvvmCross

В этом примере я буду использовать модель MVVM, для тех кто с ней не знаком, то вкратце цитата из википедии:
Шаблон Model-View-ViewModel
Паттерн MVVM делится на три части:
Модель (Model), так же, как в классической MVC, Модель представляет собой фундаментальные данные, необходимые для работы приложения.
Вид/Представление (View) — это графический интерфейс, то есть окно, кнопки и.т.п. Вид является подписчиком на событие изменения значений свойств или команд, предоставляемых Моделью Вида. В случае, если в Модели Вида изменилось какое-либо свойство, то она оповещает всех подписчиков об этом, и Вид в свою очередь запрашивает обновленное значение свойства из Модели Вида. В случае, если пользователь воздействует на какой-либо элемент интерфейса, Вид вызывает соответствующую команду, предоставленную Моделью Вида.
Модель вида (ViewModel, что означает «Model of View»[1]) является с одной стороны абстракцией Вида, а с другой предоставляет обертку данных из Модели, которые подлежат связыванию. То есть она содержит Модель, которая преобразована к Виду, а также содержит в себе команды, которыми может пользоваться Вид, чтобы влиять на Модель.


Для кроссплатформенной имплементации этого шаблона, лучшей библиотекой (к тому же open source) является MvvmCross. Ее разработчик замечательно ее поддерживает и практически мгновенно откликается на любой вопрос.

Начнем

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

Установка

Прежде всего, нам необходимо зарегистрироваться и установить Xamarin Studio. Сам шаг довольно таки простой, никаких проблем я не заметил. Автоматически установятся Java, ADT, plugin для Visual Studio и библиотеки Mono.

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

Почти любое приложение будет содержать несколько проектов.
  • Один из них будет 'core', который будет кроссплатформенный и содержать как можно больше кода: view models, общение с сервером, протоколы, обьекты даты и т.д.
  • UI проект для каждой платформы. Этот код не может быть общим, так как в нем используются компоненты, специфичные для платформы


Создаем New Project в Visual Studio, выбираем Visual C#, Windows, Portable Class Library.
Создаем проект TipCalc.Core и solution TipCalc. Выбираем опцию Windows Phone 7.5 and higher.
Хочу заметить, что пока поддерживается только версия .Net 4.0, но это должно измениться буквально в этом месяце.
Картинки
image

image


Удаляем Class1.cs, он никому не нужен
Добавляем библиотеку MvvmCross через NuGet. Для примера мы выберем Hot Tuna Start Pack, который включает в себя несколько начальных файлов.
Важно убедиться, что установлена последняя версия NuGet Package Manager, как минимум 2.5

Картинкa
image

После успешной установки мы увидим два новых файла: App.cs, в котором производится начальная инициализация MvvmCross, и файл FirstViewModel.cs в папке ViewModels. С точки зрения правильного программирования, я должен переименовать имя класса FirstViewModel на TipViewModel, но для упрощения примера так и оставим.

FirstViewModel класс наследует от MvxViewModel, это класс библиотеки MvvmCross и позволяет нам в дальнейшем посылать сообщения UI, когда данные изменились. Этот класс имплементирует INotifyPropertyChanged и его аналогия в WPF это BaseViewModel.
Добавим в этот класс 3 свойства: SubTotal (сумма счета), Generosity (щедрость) и Tip (чаевые, которые мы оставим). И перерасчет чаевых, в зависимости от них:
Код всего класса
using Cirrious.MvvmCross.ViewModels;

namespace TipCalc.Core.ViewModels
{
    public class FirstViewModel 
		: MvxViewModel
    {
        public FirstViewModel()
        {
        }

        public override void Start()
        {
            _subTotal = 100;
            _generosity = 10;
            Recalcuate();
            base.Start();
        }

        private double _subTotal;

        public double SubTotal
        {
            get { return _subTotal; }
            set { _subTotal = value; RaisePropertyChanged(() => SubTotal); Recalcuate(); }
        }

        private int _generosity;

        public int Generosity
        {
            get { return _generosity; }
            set { _generosity = value; RaisePropertyChanged(() => Generosity); Recalcuate(); }
        }

        private double _tip;

        public double Tip
        {
            get { return _tip; }
            set { _tip = value; RaisePropertyChanged(() => Tip);}
        }

        private void Recalcuate()
        {
            Tip = SubTotal * ((double)Generosity) / 100.0;
        }    
    }
}



Компилируем, ура, библиотека 'core' готова.

Клиент на Андроиде

Теперь мы хотим увидеть нашу логику на живом экране/телефоне/планшете. Так будет выглядеть его эскиз:
image

В том же solution добавим новый проект, но его тип будет Android -> Android Application. Назовем его TipCalc.UI.Droid
Если все нормально установлено, то мы увидим такую картинку:
Картинкa
image

Удаляем Activity1.cs и Main.axml из проекта.
Добавляем точно также, через NuGet, тот же пакет MvvmCross — Hot Tuna Starter Pack. Мы увидим в проекте новые файлы:
  • SplashScreen.cs — начальный Activity для аппликации
  • Setup.cs — иницализация MvvmCross и привязки ее к 'core'
  • FirstView.cs — Activity для нашего FirstViewModel
  • FirstView.axml — и его layout

Добавим в reference проект TipCalc.Core.csproj
Картинкa
image

Теперь мы нарисуем сами контроли.
Для этого откроем FirstView.axml и отредактируем его или в графическом редакторе или в xml редакторе. Мы добавим туда LinearLayout, TextView, EditText, SeekBar.
Через MvxBind мы свяжем свойства контроля с свойством нашего ViewModel.
Должна получиться вот такая картинка:
Картинкa
image

Если мы напишем такой код:
FirstView.axml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:local="http://schemas.android.com/apk/res/TipCalc.UI.Droid"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="SubTotal" />
    <EditText
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        local:MvxBind="Text SubTotal" />
    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="Generosity" />
    <SeekBar
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:max="40"
        local:MvxBind="Progress Generosity" />
    <View
        android:layout_width="fill_parent"
        android:layout_height="1dp"
        android:background="#ffff00" />
    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="Tip to leave" />
    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        local:MvxBind="Text Tip" />
</LinearLayout>


Важно заметить как мы делаем связывания свойств контролей. Свойство Text элемента TextView связывается (двусторонне) с свойством Tip нашего класса FirstViewModel посредством строчки local:MvxBind=«Text Tip» внутри самого описания контроля. Во всем остальном файл axml абсолютно идентичен любому layout в Андроиде.

Сохраняем, компилируем и запускаем. Вуаля, мы меняем сумму счета или нашу щедрость и чаевые автоматически меняются.
image

Итоги

  • Мы установили Xamarin Studio
  • Мы построили кроссплатформенную библиотеку с общей логикой
  • Мы создали приложение для Андроида, которая общается с логикой посредством MvvmCross (в этом нет необходимости, но Mvvm это круто)

Что мы упустили

  • Как работает Xamarin
  • Что такое Mvvm, как это имплементировано в MvvmCross и какие дополнительные возможности мы можем получить
  • Мы не построили примеры для iOs и Windows8
  • И много много другого


Источники

Xamarin
MvvmCross project
MvvmCross tutorial
MvvmCross tutorial video
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 35

    +17
    «мобильных аппликаций»
    Сделайте меня развидеть это!
      +2
      исправил на «мобильное приложение». так лучше?
        0
        Umputun с вами не согласен :)
        • UFO just landed and posted this here
          –6
          Воот. Три комментария об ошибках автора.
          А куча какого-то хабрасброда меня ещё учить хотела :)
          Всё правильно делаете, писать надо в комментах, а не в личку.
          +8
          Приложение — аппликация
          Разговор — конверсация
          Производство — продакция
          Продвижение — промоуция

          ну вы поняли как много «правильных» слов новых может появиться =)
            +1
            спасибо, я сейчас исправлю. Первый раз пишу тематический пост на русском
              +2
              да это ерунда, на хабре всегда ворчат вслух ))
              А сама статья интересная. Интересно если ли что-то подобное на Python?
                0
                к сожалению, Apple запрещает использовать динамические языки типа python, только компилируемые.
                  +3
                  Это не так, примерно с 2010 года. Правило менялось несколько раз и в данный момент звучит так.

                  3.3.2 An Application may not download or install executable code. Interpreted code may only be used in an Application if all scripts, code and interpreters are packaged in the Application and not downloaded. The only exception to the foregoing is scripts and code downloaded and run by Apple’s built-in WebKit framework.

                  Т.е. если питоновские скрипты и интепретатор зашиты в клиент — то сколько угодно.
                  0
                    0
                    Тут какое-то противоречие. А как же ограничение динамических языков? Коим руби так же как и питон является.
                    upd — увидел в коменте выше, что ограничение уже не актуально.
                    Когда интересно сделают аналог рубмушн для питонистов?
              –1
              «экране/телефоне/таблете». Таблет — планшет. А то таблет звучит немного странно.
              А в целом, спасибо за статью!
                0
                спасибо, исправил
                0
                А расскажите, каким образом можно на нем вызывать нативные методы? Ну т.е. вот есть ряд платформозависимых плюшек которые нужно нам вызывать — на android java код, на ios objc и т.п. Ну в качестве пример fb sdk, или MAT sdk и т.п.

                Каким образом происходит прокидывание вызовов из C# в «нативные» методы для различных платформ?
                  +1
                  Приложение под iOS в итоге просто компилируется в нативный код, только добавляется небольшой рантайм .net. Сборка мусора, bcl и все что вы используете. Т.е. почти что трансляция кода из c# (MSIL) в obj-c.
                  Для доступа к сторонним либам, есть специальный тип проекта obj-c\java binding library который создает обертки над библиотекой и можно вызывать эти методы из c#
                    0
                    Создает автоматом как-то? И что с android? Т.е. есть у меня некоторое SDK — android и ios версия. Хочу чтобы один метод в C# в зависимости от платформы вызывал нужный метод на каждой платформы. Или мне придется сделать две обертки, и потом их еще обернуть в одну свою?

                    И обратный вопрос — как в платформо зависимом коде вызывать методы моей логики на C#?

                    Т.е. есть некоторое событие приходящие от встроенного SDK — callback, соотвественно в java коде хочеться вызвать мой C# метод и в objc его же. Для c++ методов это на ура делает, что мне придется в случае Xamarin делать?
                      +1
                      Хочу чтобы один метод в C# в зависимости от платформы вызывал нужный метод на каждой платформы — просто передаем ему интерфейс, который отдельно имплементирован на каждой платформе.

                      как в платформо зависимом коде вызывать методы моей логики на C# — лучше всего делать наоборот, вызывать java код из C#. Но если нет, то смотрите мой ответ ниже, можно использовать JNI:
                      Java Native Interface – The Java Native Interface (JNI) is a framework that allows non-Java code (such as C++ or C#) to call or be called by Java code running inside a JVM.
                        0
                        Можно уточнить плиз. Вот упрощенный пример — вот есть у меня Java класс ClassA с статическим методом A и ObjectC класс classA со статическим методом А. Я хочу иметь в своей бизнес-логике C# класс ClassA со статическим методом A — который в зависимости на какой платформе мы запускаемся вызывал нужный метод.

                        Мне нужно сделать враппер на C# вокруг явской реализации, враппер на C# вокруг iOS реализации, и далее уже в C# сделать ClassA которые в зависимости от платформы (явным образом указанной? или можно автоматизировать?) вызывает нужный метод нужного врапера?

                        — По поводу «лучше всего делать наоборот» — не совсем понятно — т.е. вот есть некий callback в java, предлагает там взводить некий флажок, а в бизнес-логике его опрашивать (в паралельном потоке?) и по нему понимать что callback случился?
                          +2
                          в принципе, тут у нас будет три проекта: core, iOs, Android. И все три написаны на C#
                          Теперь добавляем 2 новых проекта, один обвертывает Java class, второй обвертывает ObjectC class в классы C#.

                          теперь, если у нас бизнесс-логика написана в core, то мы передаем ему интерфейс с методом A, который он будет вызывать когда надо, в независимости от платформы. Сама имплементация этого интерфейса будет реализована в iOs/Android проектах, и они будут вызывать свой wrapper.

                          на второй отвечу чуть позже
                            0
                            Если честно, то я не нашел примеров запуска C# code из Java/Objective-C. Не совсем уверен, но мне кажется это практически невозможно
                            forums.xamarin.com/discussion/4904/possible-to-create-a-c-xamarin-library-add-and-call-it-from-java-app
                              0
                              Это возможно и это я сам делал. Мы создавали свою нативную ObjectiveC-библиотеку и из нее вызывали C#-код.
                              Как это делается на iOS:
                              1) Создается интерфейс на ObjectiveC (MonoObject.h):
                              @interface MonoOject : NSObject {
                              }
                              
                              @property(nonatomic) BOOL isAuthenticated;
                              
                              - (int) search: (NSString*)query;
                              @end
                              

                              2) Нативная библиотека компилируется и подключается в Binding Library проект.
                              3) В ApiDefinition.cs файле этого проекта прописывается следующий код:
                                  [BaseType(typeof(NSObject), Name="MonoObject")]
                                  [Model]
                                  interface MonoObject
                                  {
                                      [Export("isAuthenticated")]
                                      [Abstract]
                                      bool IsAuthenticated { get; }
                              
                                      [Export("search:")]
                                      [Abstract]
                                      int Search(string query);
                                  }
                              

                              4) Создается наследник-имлементация от класса MonoObject:
                              public class MonoObjectImpl : MonoObject
                              {
                                  public override bool IsAuthenticated
                                  {
                                      get 
                                      {
                                          return true;
                                      }
                                  }
                                  ...
                              }
                              

                              5) Объект класса MonoObjectImpl передается в нативный код и оттуда уже вызываются методы этого объекта.

                              Такой подход позволил нам создать нативную библиотеку со всем UI и взаимодействовать с ней. При этом вся бизнес логика осталась кросс-платформенной на C#.
                      +1
                      Если в кратце, то мы создаем проект типа Java Bindings Library или Xamarin.iOs Binding Project. В этом проекте добавляем .jar файл, компилируем и автоматически получем C# обвертку (wrapper) на все методы и классы, обьявленные в jar как public.

                      image

                      Подробнее можно почитать у них на сайте:
                      There are three possible ways to reuse Java libraries in a Xamarin.Android application:

                      Create a Java Bindings Library – With this technique, a Xamarin.Android project is used to create C# wrappers around the Java types. A Xamarin.Android application can then reference the C# wrappers created by this project, and then use the .jar file.
                      Java Native Interface – The Java Native Interface (JNI) is a framework that allows non-Java code (such as C++ or C#) to call or be called by Java code running inside a JVM.
                      Port the Code – This method involves taking the Java source code, and then converting it to C#. This can be done manually, or by using an automated tool such as Sharpen.
                      docs.xamarin.com/guides/android/advanced_topics/java_integration_overview
                        0
                        Ага, это понятно. Спасибо. А как быть в ios? Ну т.е. SDK они такие, они под все платформы — можно же сделать так, чтобы этот враппер в случае ios вызывал нужные ios методы?

                        Или мне нужно будет ios wrapper, а потом еще свой wrapper которые объединит эти врапперы?

                        Ну и если про обратные вызовы (вызов C# методов из «платформенного» кода) расскажите тоже спасибо.
                          0
                          Враппер для метода в общем случае это «тупой» DllImport. Если же хочется не метод дергать, а именно выполнять одинаковую операцию на разных платформах, то нужен собственный уровень абстракции. Из коробки такого SDK в Xamarin нет, его надо либо писать, либо под каждую платформу отдельно кодить GUI, либо как-то исхитрятся. К примеру, у библиотек OpenGL ES одинаковый интерфейс на Xamarin of iOS и Xamarin for Android = OpenTK, потому портирование с одной платформы на другую может уложиться в пол-дня.
                            0
                            С удовольствем бы почитал статью о том, как правильно выстроить работу в Xamarin серьезного проекта с нативными интерфейсами под разные платформы.
                            Сейчас пишу под iOS на monotouch, впереди заточка под iPad и в перспективе андроид. Насущные вопросы — форкать ли код? Как правильно организовать работу, если все делается в одном проекте?

                            PS Случайно запостил в ветку, считайте это комментарием к статье.
                              0
                              По моему опыту, если писать интерфейс под каждую платформу в без-форковом проекте, то через какое-то время уж очень много появляется #if/#endif конструкций, потому я для себя решил делать всю бизнес-логику в виде отдельной общей библиотеки, а под интерфейс создавать отдельные проекты с нативными особенностями. Библиотека замораживается и больше не правится и не отлаживается при портировании. Подход работал отлично для iOS, Android, MonoMac и честного .Net, пока не в списке целей не добавилась Windows 8 — там отсутствуют или переделаны неймспейсы System.IO, System.Graphics, System.Xml и System.Data, потому пришлось чуть усложнить.
                                +1
                                абсолютно согласен. особенно если разделять логику и интерфейс с помощью MVVM.
                                Еще одно очень замечательное изобретение в C# — partial class
                        +1
                        Можете поглядеть как я это делаю на «Маке»: github.com/bolknote/MacGreenerCS (как раз Замарин и Си#).
                        0
                        Разработка под IOS без мака также недоступна? Скачал xamarin плюс тестовый пример tasky, попытался открыть — Android-версия без проблем, а IOS пишет что «this project type is not supported by Xamarin Studio on Windows».
                        0
                        Добавляем библиотеку MvvmCross через NuGet. Для примера мы выберем Hot Tuna Start Pack, который включает в себя несколько начальных файлов.
                        Важно убедиться, что установлена последняя версия NuGet Package Manager, как минимум 2.5

                        Замечательно, имеем NuGet 2.6.40627.9000

                        Полностью следуем всем шагам, описанным в статье, получаем:

                        Attempting to resolve dependency 'MvvmCross.HotTuna.MvvmCrossLibraries (≥ 3.0.9)'.
                        Attempting to resolve dependency 'MvvmCross.HotTuna.CrossCore (≥ 3.0.9)'.
                        Attempting to resolve dependency 'MvvmCross.PortableSupport (≥ 3.0.9)'.
                        Installing 'MvvmCross.PortableSupport 3.0.9'.
                        Successfully installed 'MvvmCross.PortableSupport 3.0.9'.
                        Installing 'MvvmCross.HotTuna.CrossCore 3.0.9'.
                        Successfully installed 'MvvmCross.HotTuna.CrossCore 3.0.9'.
                        Installing 'MvvmCross.HotTuna.MvvmCrossLibraries 3.0.9'.
                        Successfully installed 'MvvmCross.HotTuna.MvvmCrossLibraries 3.0.9'.
                        Installing 'MvvmCross.HotTuna.StarterPack 3.0.9'.
                        Successfully installed 'MvvmCross.HotTuna.StarterPack 3.0.9'.
                        Adding 'MvvmCross.PortableSupport 3.0.9' to HelloWorlderCross.
                        Uninstalling 'MvvmCross.PortableSupport 3.0.9'.
                        Successfully uninstalled 'MvvmCross.PortableSupport 3.0.9'.
                        Install failed. Rolling back…
                        Could not install package 'MvvmCross.PortableSupport 3.0.9'. You are trying to install this package into a project that targets 'portable-win+net40+sl40+wp71', but the package does not contain any assembly references or content files that are compatible with that framework. For more information, contact the package author.

                        ЧЯДНТ?

                        Only users with full accounts can post comments. Log in, please.