Как вы реализуете архитектуру с раздельным UI для iOS и Android, сохраняя общий бизнес-слой?

Реализация архитектуры с разделённым пользовательским интерфейсом (UI) для iOS и Android при сохранении общего бизнес-слоя в Xamarin требует чёткого разграничения логики приложения и интерфейсной части. Такая архитектура особенно востребована в случаях, когда необходимо обеспечить нативный пользовательский опыт на каждой платформе, но при этом сохранить единую базу бизнес-логики, сервисов, моделей и API-взаимодействий.

Ниже описан подход, включающий практические приёмы, структуру проекта и инструменты для достижения этой цели.

1. Общая архитектурная модель

Обычно используется следующая архитектура:

  • **Бизнес-слой (shared logic):
    **

    • Расположен в общем проекте (.NET Standard, net6.0, net8.0)

    • Содержит модели, ViewModel-ы, сервисы, репозитории, API-клиенты

    • Не имеет прямой зависимости от UI

  • **UI-слой:
    **

    • Отдельно реализован в проектах Xamarin.Android и Xamarin.iOS

    • Использует платформенно-специфичные разметки, контролы и взаимодействие

    • Привязан к общему бизнес-слою через ViewModel и интерфейсы

2. Структура проекта

/MyApp.Solution

├── MyApp.Core/ # Общий бизнес-слой (ViewModels, Models, Services)
 └── ViewModels/
 └── Services/
 └── Models/

├── MyApp.Android/ # Платформенный UI-проект
 └── Views/ # Android Layouts, Activities, Fragments
 └── MainActivity.cs

├── MyApp.iOS/ # Платформенный UI-проект
 └── ViewControllers/ # Storyboards, UIKit, Swift-like UIs
 └── AppDelegate.cs

В MyApp.Core нет ни одной зависимости от Xamarin.Forms или Xamarin.*, только System.* и сторонние кросс-платформенные библиотеки.

3. Использование ViewModel'ов из общего слоя

Каждая платформа подключает ViewModel и подписывается на свойства или команды:

// Android Activity
public class LoginActivity : AppCompatActivity
{
private LoginViewModel \_viewModel;
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
SetContentView(Resource.Layout.Login);
\_viewModel = new LoginViewModel(new AuthService());
var loginButton = FindViewById<Button>(Resource.Id.btnLogin);
loginButton.Click += async (s, e) =>
{
\_viewModel.Username = "input_user";
\_viewModel.Password = "input_pass";
await \_viewModel.LoginCommand.ExecuteAsync(null);
};
}
}

4. Варианты UI-реализации

A. Android

  • Используется Android XML Layout + AppCompatActivity/Fragment

  • Подключение через SetContentView(Resource.Layout...)

  • Обработка событий вручную: button.Click += ...

  • Привязка к ViewModel через ручной биндинг (или сторонние биндинг-фреймворки)

B. iOS

  • Используется UIViewController + Storyboard или XIB

  • Ссылки на элементы через IBOutlet или FindViewById

  • Подключение к ViewModel — через DI или вручную в контроллере

5. Подходы к биндингу и MVVM

В Xamarin.Native (без Forms) отсутствует встроенный механизм привязки данных. Возможны три подхода:

1. Ручной биндинг

Вы подписываетесь на PropertyChanged и вручную обновляете UI:

\_viewModel.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(\_viewModel.IsLoading))
{
progressBar.Visibility = \_viewModel.IsLoading ? ViewStates.Visible : ViewStates.Gone;
}
};

2. Использование сторонних MVVM-фреймворков

Можно использовать:

  • MvvmCross — мощный MVVM-фреймворк с платформенной интеграцией

  • ReactiveUI — реактивный подход с Rx и расширенными возможностями

  • FreshMVVM (для Forms) — менее применим для чисто нативной архитектуры

Фреймворки предоставляют:

  • Автоматический биндинг между View и ViewModel

  • Инъекцию зависимостей

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

6. Организация навигации

В Android:

Навигация между экранами реализуется через Intent:

var intent = new Intent(this, typeof(HomeActivity));
StartActivity(intent);

В iOS:

Навигация — через PushViewController, PresentViewController:

var homeVC = new HomeViewController();
NavigationController.PushViewController(homeVC, true);

Навигация абстрагируется через сервис:

public interface INavigationService
{
Task NavigateToAsync(string route);
}

Каждая платформа реализует этот интерфейс по-своему и передаёт в ViewModel.

7. Платформенные сервисы через Dependency Injection

Для доступа к нативным возможностям, например камере или геолокации, используется интерфейсный слой:

// Общий интерфейс
public interface ICameraService
{
Task<Stream> CapturePhotoAsync();
}

В Android:

public class AndroidCameraService : ICameraService { ... }

В iOS:

public class IOSCameraService : ICameraService { ... }

Регистрируются через DI-контейнер и передаются в ViewModel:

var vm = new PhotoViewModel(new AndroidCameraService());

8. Unit-тесты для ViewModel

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

9. Работа с ресурсами и локализацией

  • Ресурсы (строки, иконки, цвета) хранятся отдельно в каждой платформе:

    • Android → Resources/values/strings.xml

    • iOS → Localizable.strings

  • Общее ядро может запрашивать ресурсы через IResourceService, реализованный на платформе.

10. Примеры из реальных проектов

Примеры ситуаций, где применяется разделённый UI:

  • Банковское приложение с кастомной навигацией и нативной анимацией на iOS

  • Корпоративное приложение с различным UI для Android-терминалов и iPad

  • Медицинское ПО с различной UX-моделью: табы на Android, Flyout на iOS

11. Сборка и деплой

Каждая платформа собирается отдельно:

  • Android → .apk/.aab

  • iOS → .ipa

Общий слой компилируется как .dll и подключается в обеих платформах.

Такая архитектура с разделением UI между платформами и общим логическим слоем обеспечивает гибкость, масштабируемость и максимальное соответствие пользовательским ожиданиям. Она требует чёткой структуры, хорошей инверсии зависимостей и дисциплины в разработке, но даёт высокий контроль над UX.