Как вы реализуете архитектуру с раздельным 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.