На руках действующее приложение на Xamarin, и команда смотрит в будущее с тревогой: официальная поддержка закончилась 1 мая 2024 года, а магазины приложений с каждым годом требуют сборок всё более свежими SDK. Паниковать не стоит: кодовая база на C# никуда не девается, разметка почти не меняется, а преемник .NET MAUI несёт ту же архитектуру, доведённую до современного дотнета. По пути расскажем и о том, как был устроен этот мир, потому что на нём выросли тысячи приложений, живущих до сих пор. Разберём, как Xamarin устроен под капотом, почему он занял своё место в истории, чем жива его логика сегодня и как перевести проект на MAUI без переписывания с нуля.
Архитектура Xamarin делит проект на общую библиотеку и платформенные головы
Технология выросла из Mono, открытой среды исполнения .NET, основанной в 2011 году создателями этой среды, и в 2016 году вошла в состав Microsoft, которая открыла инструменты для всех желающих. В 2014 году вышел Xamarin.Forms, скрывший различия платформ за общей разметкой, в 2018 году кодовая база полностью открылась, а в 2022 году появился .NET MAUI, за которым в мае 2024 года закрылась дверь старого стека. Идея при этом не менялась: бизнес-логика, модели и работа с сетью пишутся один раз на C#, а вокруг общей библиотеки стоят платформенные головы, по одной на целевую систему.
На iOS среда Mono компилируется в машинный код с опережающей генерацией, как того требует Apple, на Android работает с динамической компиляцией, как привычно этой платформе. Обе головы обращаются к родным API через тонкие привязки: Xamarin.iOS даёт полный доступ к Cocoa Touch, Xamarin.Android к пространствам имён Android, и приложение остаётся нативальным с точки зрения системы, просто говорит на C#.
Точка входа платформенной головы занимает считанные строки и лишь инициализирует движок Forms, после чего управление переходит общей библиотеке:
[Activity(Label = "Shop", MainLauncher = true)]
public class MainActivity
: global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
Xamarin.Forms.Forms.Init(this, savedInstanceState);
LoadApplication(new App());
}
}
Аналогичная голова на iOS делает то же самое в AppDelegate, и вся специфика платформ остаётся за бортом логики приложения.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
</Project>
Общая библиотека целилась в стандарт .NET, поэтому в неё безболезненно ложились LINQ, асинхронность и любые пакеты NuGet. Головы подключали библиотеку и добавляли точку входа, а сборка каждого приложения шла своим инструментарием под конкретную операционную систему.
Xamarin Forms описывает экраны кроссплатформенной разметкой XAML
Писать интерфейс отдельно для каждой платформы утомительно, и в 2014 году появился Xamarin.Forms: слой, который описывает экраны один раз на XAML, а на устройство отрисовывает нативные управляющие элементы. Кнопка на Android выглядит как кнопка Android, на iOS как кнопка iOS, код при этом общий.
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
x:Class="Shop.CatalogPage">
<StackLayout Padding="16">
<Label Text="Каталог" FontSize="24" />
<ListView ItemsSource="{Binding Items}" />
<Button Text="Обновить" Command="{Binding RefreshCommand}" />
</StackLayout>
</ContentPage>
Разметка сводится к знакомому набору: страницы, компоновки вроде стеков и сеток, управляющие элементы от ярлыка до списка. Тонкая настройка внешнего вида конкретной платформы делается эффектами, когда нужно поправить отступ или тень, и визуализаторами, когда нужен собственный управляющий элемент с нуля. Общие ресурсы собираются в словаре приложения, и единый стиль кнопок задаётся один раз:
<Application.Resources>
<Color x:Key="Accent">#2c7be5</Color>
<Style TargetType="Button">
<Setter Property="BackgroundColor"
Value="{StaticResource Accent}" />
<Setter Property="TextColor" Value="White" />
</Style>
</Application.Resources>
Навигация строилась на стеке страниц: стек толкал следующий экран методом PushAsync, модальные окна открывались поверх, а оболочка Shell из поздних версий Forms собрала маршрутизацию по адресам в одном месте, приблизив мобильное приложение к привычкам веб-разработки. Такой баланс покрывал большинство корпоративных приложений, для которых.Forms и покупали, а правки разметки в горячей перезагрузке отражались на экране за секунды, не требуя пересборки.
Связывание в стиле MVVM соединяет свойства модели с интерфейсом
Паттерн MVVM в Xamarin.Forms скрепляет разметку и логику: у страницы есть контекст связывания, у контекста свойства и команды, изменения свойств оповещают интерфейс через событие. Пишется это на чистом C#, без сторонних каркасов, хотя сообщество охотно подключало и их.
public class CatalogViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private ObservableCollection<Item> items;
public ObservableCollection<Item> Items
{
get => items;
set
{
items = value;
OnPropertyChanged();
}
}
public ICommand RefreshCommand { get; }
public CatalogViewModel()
{
RefreshCommand = new Command(async () =>
{
Items = new ObservableCollection<Item>(
await rest.GetCatalogAsync());
});
}
private void OnPropertyChanged(
[CallerMemberName] string name = null) =>
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(name));
}
Асинхронность через async и await дышала здесь полной грудью: обработчик команды не блокирует поток интерфейса, список обновляется одним присваиванием, а коллекция следит за изменениями сама. Тестируется такая моделька без эмулятора, обычными тестами, что было сильной стороной подхода с первого дня.
Когда значение нужно показать не как есть, а привести к виду, в разметке работают конвертеры, маленькие классы с парой методов:
public class PriceConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
var price = (decimal)value;
return $"{price:0.00} руб.";
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture) =>
throw new NotSupportedException();
}
Конвертер подключается к связыванию по ключу, объявленному в ресурсах страницы, и превращает число в готовую строку с валютой, не трогая модель.
Доступ к платформенным API идёт через службы зависимостей и эффекты
Общая библиотека не видит платформенных деталей, а они ей нужны: пути файлов, датчики, уведомления, буфер обмена. Мост работает по классической схеме: интерфейс живёт в общем коде, реализация в голове платформы, а регистрация через атрибут сборки.
// общая библиотека
public interface IFileService
{
string GetLocalPath(string fileName);
}
// голова Android
[assembly: Dependency(typeof(FileService))]
namespace Shop.Droid
{
public class FileService : IFileService
{
public string GetLocalPath(string fileName) =>
Path.Combine(Environment.GetFolderPath(
Environment.SpecialFolder.Personal), fileName);
}
}
Общий код зовет службу одной строкой DependencyService.Get, получая реализацию, о которой он ничего не знает. Приём знакомый по любому инверсионному каркасу, и его аккуратно используют в проектах, где важно держать слой логики чистым от операционных систем.
Для рассылки событий между не связанными частями приложения служил MessagingCenter: страница подписывалась на сообщение по строковому ключу, служба отправляла его после завершения загрузки, и части ничего не знали друг о друге. Инструмент простой, в больших проектах его обвешивали типизированными обёртками, чтобы ключи не расползались по коду. Для сложных сценариев головы подключали родные библиотеки через привязки, и сообщество собрало тысячи готовых обёрток над платформенными SDK, от сканера штрихкодов до уведомлений.
Жизненный цикл поддержки Xamarin завершился в мае 2024 года
Последняя мажорная версия Xamarin.Forms осталась пятой, её обслуживание тянулось сервисными выпусками до весны 2024 года. Затем Microsoft зафиксировала дату: с 1 мая 2024 года инструменты переходят в разряд неподдерживаемых, а рекомендуемым путём объявлен .NET MAUI, вышедший в 2022 году и продолжающий ту же архитектурную линию в современном дотнете.
Что это значит на практике. Компилятор и эмуляторы продолжают работать, старые сборки не отвалятся с полки магазина сами по себе. Но с каждым обновлением требований магазинов приложения, собранные устаревшим инструментарием, начинают возвращаться с отказом, потому что магазины требуют свежих SDK целевых платформ. Ошибки в среде больше не чинятся, уязвимости среды не закрываются, и корпоративные политики безопасности одно за другим вычёркивают неподдерживаемые стеки из списка разрешённых.
Оставаться на Xamarin разумно ровно до ближайшего крупного обновления целевой платформы, и этот момент лучше планировать заранее, чем ловить его в очереди релизов. Окно в два года между появлением MAUI и финалом старого стека и было планом Microsoft: ничего резкого, только эволюция.
Миграция проекта на NET MAUI проходит по проторённому пути
Хорошая новость в том, что переезд механический, а не сжигательный. Разметка почти не меняется, C# остаётся, XAML остаётся, меняются пространства имён и детали инфраструктуры. Чек-лист переезда умещается в шесть пунктов:
- обновить целевые платформы в файле проекта на современные net10.0-android и net10.0-ios;
- заменить корневые пространства имён с Xamarin.Forms на Microsoft.Maui.Controls;
- перенести обращения к платформенным службам с DependencyService на встроенный контейнер внедрения зависимостей;
- переписать пользовательские визуализаторы на обработчики MAUI, которые легче и предсказуемее;
- прогнать ассистент обновления и погасить список различий, который покажет компиляция;
- проверить на живых устройствах жесты, переходы и жизненный цикл, которые изменились тонко.
<!-- файл проекта после переезда -->
<TargetFrameworks>
net10.0-android;net10.0-ios
</TargetFrameworks>
Ассистент обновления берёт на себя рутину: переименовывает ссылки, переставляет цели сборки и протоколирует, что не поддалось автоматически. Главная инженерная новизна, с которой столкнётся команда, это обработчики вместо визуализаторов: старый слой создавал собственную реализацию для каждого управляющего элемента, новый настраивает платформенный элемент напрямую, что убирает целую прослойку наследования и упрощает отладку. Опыт команд показывает, что проект среднего размера переезжает за считанные недели, и львиная доля времени уходит не на код, а на проверку и доводку мелких экранов.
Практические выводы для команд с действующими проектами
Если приложение живёт на Xamarin и стабильно работает, паника не помощник. Составьте план: зафиксируйте дату, когда очередное требование магазина заблокирует сборку, отведите окно на переезд и держите его, не позволяя релизам смести его в долгий ящик. Если проект большой и ветвистый, имеет смысл переезжать по экранам, запуская обе оболочки параллельно через общую библиотеку.
Если приложение только в планах, выбор очевиден: писать сразу на MAUI, где новая графика, лучший доступ к платформам и живое сообщество, в которое легко нанять людей. Опыт Xamarin здесь не обуза: инженер, знающий старый стек, осваивает новый за недели, потому что язык, разметка и паттерн остались прежними. Xamarin как материал изучения тоже не пропал: его разметка, MVVM и службы зависимостей переехали в MAUI почти без изменений, поэтому часы, вложенные в старую документацию, окупятся и на новом стеке.
Финальный вывод прост. Xamarin был мостом между миром дотнета и мобильными платформами, и мост сработал: миллионы строк C# на телефонах, целая школа разработчиков и преемник, который не заставил эту школу переучиваться. Технологии уходят, когда доводят идею до конца, и именно это произошло здесь.