MVC в Android: примеры внедрения паттерна в Java-приложения
Для кого эта статья:
- Разработчики, желающие улучшить свои навыки в Android-разработке
- Студенты и новички в программировании, изучающие архитектурные шаблоны
Практикующие разработчики, ищущие конкретные примеры реализации MVC в реальных проектах
Архитектурные шаблоны — не роскошь, а необходимость для создания масштабируемых Android-приложений. MVC (Model-View-Controller) — один из фундаментальных паттернов, который до сих пор занимает важное место в арсенале разработчика. Но между теоретическим пониманием MVC и его практической реализацией часто лежит пропасть. В этой статье я предлагаю мост через эту пропасть: конкретные примеры кода на Java для Android, демонстрирующие, как разделить ответственность между компонентами вашего приложения и избежать создания монолитного, неподдерживаемого кода. 🚀
Хотите структурировать свои Android-приложения профессионально? Курс Java-разработки от Skypro погружает вас в мир чистой архитектуры на практике. Вы освоите не только MVC, но и другие современные паттерны проектирования под руководством практикующих разработчиков. Каждый модуль курса включает реальные проекты, где вы применяете изученные концепции сразу. Инвестиция в свои навыки структурирования кода окупается моментально при первом же техническом интервью!
Основы MVC: структурированный подход для Android
MVC представляет собой архитектурный паттерн, разделяющий приложение на три взаимосвязанных компонента: Model (модель), View (представление) и Controller (контроллер). В контексте Android-разработки каждый из этих компонентов имеет специфические обязанности.
- Model — отвечает за данные и бизнес-логику. Это независимый от пользовательского интерфейса компонент, содержащий методы для работы с данными, их обработки и бизнес-правил.
- View — отвечает за отображение данных пользователю. В Android это обычно Activity, Fragment или custom View-компоненты.
- Controller — связующее звено между Model и View, обрабатывающее пользовательский ввод и обновляющее Model или View соответственно.
В отличие от более современных паттернов, таких как MVP или MVVM, в классическом MVC контроллер напрямую взаимодействует как с моделью, так и с представлением. Это создает некоторые сложности в контексте Android, поскольку Activity или Fragment часто выполняют роли как View, так и Controller.
| Компонент | Ответственность | Реализация в Android |
|---|---|---|
| Model | Бизнес-логика, работа с данными | POJO-классы, Repository, DAO |
| View | Отображение UI, передача действий пользователя | Activity, Fragment, Custom Views |
| Controller | Обработка пользовательских действий, обновление Model и View | Отдельный класс-контроллер или часть Activity/Fragment |
Преимущество MVC в Android заключается в его относительной простоте и интуитивности. При корректной реализации MVC позволяет:
- Отделить бизнес-логику от UI, что упрощает модульное тестирование.
- Обеспечить возможность независимого развития компонентов.
- Повысить читаемость кода за счет четкого разделения ответственности.
- Упростить поддержку и расширение функционала приложения.
Алексей Соколов, Senior Android Developer
На одном из проектов я столкнулся с монолитным кодом, где бизнес-логика, сетевые запросы и обработка UI были смешаны в одной Activity размером более 2000 строк. Рефакторинг такого кода — настоящий кошмар.
Я убедил команду внедрить MVC. Мы выделили модель для работы с API и локальной базой данных, создали интерфейс для View и контроллер, связывающий их. Вначале казалось, что мы усложняем простые вещи, но когда потребовалось добавить офлайн-режим, преимущества стали очевидны. Модель была обновлена независимо от UI, контроллер получил новую логику работы с кешем, а интерфейс View остался прежним.
Размер отдельных компонентов редко превышал 300 строк, код стал тестируемым, а изменения — локальными и безболезненными. Теперь я внедряю MVC на начальном этапе даже в небольших проектах — это страховка от будущих проблем с расширением функциональности.

Настройка проекта для реализации MVC на Android с Java
Реализация MVC начинается с правильной организации проекта. Рекомендую структурировать пакеты в соответствии с компонентами MVC, что сделает код более понятным и поддерживаемым. 📂
Типичная структура проекта для MVC может выглядеть следующим образом:
com.example.app
├── model
│ ├── data
│ │ ├── User.java
│ │ └── ...
│ ├── repository
│ │ ├── UserRepository.java
│ │ └── ...
│ └── source
│ ├── local
│ │ └── DatabaseHelper.java
│ └── remote
│ └── ApiService.java
├── controller
│ ├── UserController.java
│ └── ...
└── view
├── activities
│ ├── MainActivity.java
│ └── ...
├── fragments
│ └── ...
└── interfaces
├── UserView.java
└── ...
После создания структуры проекта необходимо настроить зависимости. Вот список библиотек, которые часто используются в MVC-проектах на Android:
- Retrofit — для сетевых запросов в Model-слое
- Room — для работы с локальной базой данных
- Gson/Jackson — для парсинга JSON
- EventBus/RxJava — для обмена событиями между компонентами
- JUnit/Mockito — для модульного тестирования
Добавьте эти зависимости в build.gradle файл вашего модуля:
dependencies {
// Retrofit для сетевых запросов
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// Room для локальной БД
implementation 'androidx.room:room-runtime:2.4.3'
annotationProcessor 'androidx.room:room-compiler:2.4.3'
// Gson для работы с JSON
implementation 'com.google.code.gson:gson:2.9.0'
// EventBus для коммуникации компонентов
implementation 'org.greenrobot:eventbus:3.2.0'
// Для тестирования
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.mockito:mockito-core:4.0.0'
}
Теперь создадим базовые интерфейсы для взаимодействия между компонентами MVC:
// Базовый интерфейс для View
public interface BaseView {
void showError(String message);
void showLoading(boolean isLoading);
}
// Базовый интерфейс для Controller
public interface BaseController {
void onDestroy();
}
Эти базовые интерфейсы помогут создать стандартизированный подход к взаимодействию между компонентами MVC в вашем приложении.
| Проблема при реализации MVC | Решение |
|---|---|
| Смешивание ролей View и Controller в Activity | Выделить интерфейс для View, реализуемый Activity, и отдельный класс Controller |
| Сложная коммуникация между компонентами | Использовать EventBus или интерфейсы обратного вызова |
| Сохранение состояния при повороте экрана | Хранить данные в Model и/или использовать ViewModel из Architecture Components |
| Утечки памяти | Отвязывать контроллер от представления в методе onDestroy() |
Разработка компонента Model в Android MVC архитектуре
Model — сердце MVC-архитектуры, отвечающее за бизнес-логику и данные приложения. Хорошо спроектированный Model-слой должен быть полностью независим от UI и работать автономно. 📊
Начнем с создания простого класса данных:
// Класс данных
public class User {
private long id;
private String name;
private String email;
private String profileImageUrl;
// Конструкторы, геттеры и сеттеры
public User(long id, String name, String email, String profileImageUrl) {
this.id = id;
this.name = name;
this.email = email;
this.profileImageUrl = profileImageUrl;
}
// Геттеры и сеттеры опущены для краткости
}
Далее создадим источники данных — локальный и удаленный:
// Интерфейс для источника данных
public interface UserDataSource {
interface LoadUsersCallback {
void onUsersLoaded(List<User> users);
void onError(String error);
}
void getUsers(LoadUsersCallback callback);
void getUser(long userId, LoadUserCallback callback);
void saveUser(User user, SaveUserCallback callback);
void deleteUser(long userId, DeleteUserCallback callback);
// Другие callback-интерфейсы опущены для краткости
}
// Реализация локального источника данных
public class UserLocalDataSource implements UserDataSource {
private final AppDatabase database;
public UserLocalDataSource(AppDatabase database) {
this.database = database;
}
@Override
public void getUsers(final LoadUsersCallback callback) {
new AsyncTask<Void, Void, List<User>>() {
@Override
protected List<User> doInBackground(Void... voids) {
return database.userDao().getAll();
}
@Override
protected void onPostExecute(List<User> users) {
callback.onUsersLoaded(users);
}
}.execute();
}
// Остальные методы опущены для краткости
}
// Реализация удаленного источника данных
public class UserRemoteDataSource implements UserDataSource {
private final ApiService apiService;
public UserRemoteDataSource(ApiService apiService) {
this.apiService = apiService;
}
@Override
public void getUsers(final LoadUsersCallback callback) {
apiService.getUsers().enqueue(new Callback<List<User>>() {
@Override
public void onResponse(Call<List<User>> call, Response<List<User>> response) {
if (response.isSuccessful()) {
callback.onUsersLoaded(response.body());
} else {
callback.onError("Error: " + response.code());
}
}
@Override
public void onFailure(Call<List<User>> call, Throwable t) {
callback.onError(t.getMessage());
}
});
}
// Остальные методы опущены для краткости
}
Теперь создадим репозиторий, который будет абстрагировать источники данных от остальной части приложения:
public class UserRepository implements UserDataSource {
private final UserDataSource remoteDataSource;
private final UserDataSource localDataSource;
// Кеш данных в памяти
private Map<Long, User> cachedUsers = new LinkedHashMap<>();
private boolean cacheIsDirty = false;
public UserRepository(UserDataSource remoteDataSource, UserDataSource localDataSource) {
this.remoteDataSource = remoteDataSource;
this.localDataSource = localDataSource;
}
@Override
public void getUsers(final LoadUsersCallback callback) {
// Возвращаем кешированные данные, если они доступны и свежие
if (!cachedUsers.isEmpty() && !cacheIsDirty) {
callback.onUsersLoaded(new ArrayList<>(cachedUsers.values()));
return;
}
// Если кеш грязный, загружаем данные из сети
if (cacheIsDirty) {
getFromRemoteDataSource(callback);
} else {
// Иначе пробуем загрузить из локального хранилища
localDataSource.getUsers(new LoadUsersCallback() {
@Override
public void onUsersLoaded(List<User> users) {
refreshCache(users);
callback.onUsersLoaded(new ArrayList<>(cachedUsers.values()));
}
@Override
public void onError(String error) {
getFromRemoteDataSource(callback);
}
});
}
}
private void getFromRemoteDataSource(final LoadUsersCallback callback) {
remoteDataSource.getUsers(new LoadUsersCallback() {
@Override
public void onUsersLoaded(List<User> users) {
refreshCache(users);
refreshLocalDataSource(users);
callback.onUsersLoaded(new ArrayList<>(cachedUsers.values()));
}
@Override
public void onError(String error) {
callback.onError(error);
}
});
}
private void refreshCache(List<User> users) {
cachedUsers.clear();
for (User user : users) {
cachedUsers.put(user.getId(), user);
}
cacheIsDirty = false;
}
private void refreshLocalDataSource(List<User> users) {
// Сохраняем данные в локальное хранилище
// Код опущен для краткости
}
// Остальные методы опущены для краткости
}
Этот репозиторий реализует шаблон проектирования Repository и использует стратегию кеширования для оптимизации доступа к данным. Он абстрагирует контроллеры от конкретных источников данных, делая систему более гибкой и тестируемой.
Михаил Петров, Lead Android Developer
Когда я занимался разработкой приложения для службы доставки, мы столкнулись с проблемой нестабильного интернет-соединения. Пользователи жаловались, что приложение "зависает" при плохом сигнале сети.
Изначально мы реализовали простую загрузку данных с сервера прямо в Activity, но это вызывало блокировку UI и множество проблем при переходах между экранами. Мы решили применить MVC с грамотно реализованным Model-слоем.
Мы создали систему, где Model-слой предоставлял данные из кеша, пока загружал свежую информацию с сервера. Контроллер подписывался на обновления модели и передавал данные в View по мере их поступления. Обработку ошибок мы также вынесли в Model, реализовав автоматические повторные попытки и политики кеширования.
Результат превзошел ожидания. Пользователи заметили, что приложение стало "моментально отзываться", даже когда интернет был медленным. А нам, разработчикам, стало проще поддерживать и расширять функционал, поскольку вся логика обработки данных была изолирована в Model-слое.
Создание компонента View для эффективного отображения
View в MVC отвечает за отображение данных пользователю и передачу пользовательских действий контроллеру. В Android это обычно Activity, Fragment или custom View-классы. 🖥️
Для начала создадим интерфейс View, который определит контракт между представлением и контроллером:
// Интерфейс представления для списка пользователей
public interface UserListView extends BaseView {
void displayUsers(List<User> users);
void showUserDetails(long userId);
void showNoUsers();
}
// Интерфейс представления для деталей пользователя
public interface UserDetailView extends BaseView {
void displayUserDetails(User user);
void enableEditing(boolean enable);
void showUserSaved();
void showUserDeleted();
}
Теперь реализуем Activity, которая будет реализовывать интерфейс UserListView:
public class UserListActivity extends AppCompatActivity implements UserListView {
private RecyclerView recyclerView;
private UserAdapter adapter;
private ProgressBar progressBar;
private TextView emptyView;
private UserController controller;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_user_list);
// Инициализация UI-компонентов
recyclerView = findViewById(R.id.recyclerView);
progressBar = findViewById(R.id.progressBar);
emptyView = findViewById(R.id.emptyView);
// Настройка RecyclerView
recyclerView.setLayoutManager(new LinearLayoutManager(this));
adapter = new UserAdapter(new ArrayList<>(), this::onUserClick);
recyclerView.setAdapter(adapter);
// Создание источников данных и репозитория
AppDatabase database = AppDatabase.getInstance(this);
ApiService apiService = RetrofitClient.getInstance().create(ApiService.class);
UserDataSource localDataSource = new UserLocalDataSource(database);
UserDataSource remoteDataSource = new UserRemoteDataSource(apiService);
UserRepository repository = new UserRepository(remoteDataSource, localDataSource);
// Создание контроллера
controller = new UserControllerImpl(this, repository);
// Загрузка данных
controller.loadUsers();
}
@Override
protected void onDestroy() {
super.onDestroy();
controller.onDestroy();
}
@Override
public void displayUsers(List<User> users) {
recyclerView.setVisibility(View.VISIBLE);
emptyView.setVisibility(View.GONE);
adapter.updateUsers(users);
}
@Override
public void showNoUsers() {
recyclerView.setVisibility(View.GONE);
emptyView.setVisibility(View.VISIBLE);
}
@Override
public void showUserDetails(long userId) {
Intent intent = new Intent(this, UserDetailActivity.class);
intent.putExtra("USER_ID", userId);
startActivity(intent);
}
@Override
public void showLoading(boolean isLoading) {
progressBar.setVisibility(isLoading ? View.VISIBLE : View.GONE);
}
@Override
public void showError(String message) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
}
private void onUserClick(User user) {
controller.onUserSelected(user.getId());
}
// Адаптер для RecyclerView
private static class UserAdapter extends RecyclerView.Adapter<UserAdapter.ViewHolder> {
private List<User> users;
private final OnUserClickListener clickListener;
interface OnUserClickListener {
void onUserClick(User user);
}
UserAdapter(List<User> users, OnUserClickListener clickListener) {
this.users = users;
this.clickListener = clickListener;
}
void updateUsers(List<User> users) {
this.users = users;
notifyDataSetChanged();
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_user, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
User user = users.get(position);
holder.nameTextView.setText(user.getName());
holder.emailTextView.setText(user.getEmail());
// Загрузка изображения профиля (использование библиотеки Picasso или Glide)
// Код опущен для краткости
holder.itemView.setOnClickListener(v -> clickListener.onUserClick(user));
}
@Override
public int getItemCount() {
return users.size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
ImageView profileImageView;
TextView nameTextView;
TextView emailTextView;
ViewHolder(@NonNull View itemView) {
super(itemView);
profileImageView = itemView.findViewById(R.id.profileImageView);
nameTextView = itemView.findViewById(R.id.nameTextView);
emailTextView = itemView.findViewById(R.id.emailTextView);
}
}
}
}
Аналогично реализуем Activity для отображения деталей пользователя:
public class UserDetailActivity extends AppCompatActivity implements UserDetailView {
private EditText nameEditText;
private EditText emailEditText;
private Button editButton;
private Button saveButton;
private Button deleteButton;
private ProgressBar progressBar;
private ImageView profileImageView;
private UserController controller;
private boolean editMode = false;
private long userId;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_user_detail);
// Инициализация UI-компонентов
nameEditText = findViewById(R.id.nameEditText);
emailEditText = findViewById(R.id.emailEditText);
editButton = findViewById(R.id.editButton);
saveButton = findViewById(R.id.saveButton);
deleteButton = findViewById(R.id.deleteButton);
progressBar = findViewById(R.id.progressBar);
profileImageView = findViewById(R.id.profileImageView);
// Получение ID пользователя из Intent
userId = getIntent().getLongExtra("USER_ID", -1);
if (userId == -1) {
showError("Invalid user ID");
finish();
return;
}
// Создание источников данных и репозитория
AppDatabase database = AppDatabase.getInstance(this);
ApiService apiService = RetrofitClient.getInstance().create(ApiService.class);
UserDataSource localDataSource = new UserLocalDataSource(database);
UserDataSource remoteDataSource = new UserRemoteDataSource(apiService);
UserRepository repository = new UserRepository(remoteDataSource, localDataSource);
// Создание контроллера
controller = new UserControllerImpl(this, repository);
// Настройка обработчиков нажатий
editButton.setOnClickListener(v -> controller.onEditClicked());
saveButton.setOnClickListener(v -> {
String name = nameEditText.getText().toString();
String email = emailEditText.getText().toString();
controller.saveUser(new User(userId, name, email, null));
});
deleteButton.setOnClickListener(v -> controller.deleteUser(userId));
// Загрузка данных пользователя
controller.loadUser(userId);
}
@Override
protected void onDestroy() {
super.onDestroy();
controller.onDestroy();
}
@Override
public void displayUserDetails(User user) {
nameEditText.setText(user.getName());
emailEditText.setText(user.getEmail());
// Загрузка изображения профиля (с использованием библиотеки Picasso или Glide)
// Код опущен для краткости
}
@Override
public void enableEditing(boolean enable) {
editMode = enable;
nameEditText.setEnabled(enable);
emailEditText.setEnabled(enable);
editButton.setVisibility(enable ? View.GONE : View.VISIBLE);
saveButton.setVisibility(enable ? View.VISIBLE : View.GONE);
deleteButton.setVisibility(enable ? View.VISIBLE : View.GONE);
}
@Override
public void showUserSaved() {
Toast.makeText(this, "User saved successfully", Toast.LENGTH_SHORT).show();
enableEditing(false);
}
@Override
public void showUserDeleted() {
Toast.makeText(this, "User deleted successfully", Toast.LENGTH_SHORT).show();
finish();
}
@Override
public void showLoading(boolean isLoading) {
progressBar.setVisibility(isLoading ? View.VISIBLE : View.GONE);
}
@Override
public void showError(String message) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
}
}
Реализация Controller и интеграция всех компонентов MVC
Controller в MVC отвечает за связь между Model и View, обрабатывая пользовательский ввод и обновляя состояние приложения. В Android контроллер обычно реализуется как отдельный класс, который получает ссылки на Model и View. 🔄
Создадим интерфейс контроллера для работы с пользователями:
public interface UserController extends BaseController {
void loadUsers();
void onUserSelected(long userId);
void loadUser(long userId);
void onEditClicked();
void saveUser(User user);
void deleteUser(long userId);
}
Теперь реализуем этот интерфейс:
public class UserControllerImpl implements UserController {
private UserListView listView;
private UserDetailView detailView;
private final UserRepository repository;
// Конструктор для списка пользователей
public UserControllerImpl(UserListView view, UserRepository repository) {
this.listView = view;
this.repository = repository;
}
// Конструктор для деталей пользователя
public UserControllerImpl(UserDetailView view, UserRepository repository) {
this.detailView = view;
this.repository = repository;
}
@Override
public void loadUsers() {
if (listView != null) {
listView.showLoading(true);
repository.getUsers(new UserDataSource.LoadUsersCallback() {
@Override
public void onUsersLoaded(List<User> users) {
listView.showLoading(false);
if (users.isEmpty()) {
listView.showNoUsers();
} else {
listView.displayUsers(users);
}
}
@Override
public void onError(String error) {
listView.showLoading(false);
listView.showError(error);
}
});
}
}
@Override
public void onUserSelected(long userId) {
if (listView != null) {
listView.showUserDetails(userId);
}
}
@Override
public void loadUser(long userId) {
if (detailView != null) {
detailView.showLoading(true);
repository.getUser(userId, new UserDataSource.LoadUserCallback() {
@Override
public void onUserLoaded(User user) {
detailView.showLoading(false);
detailView.displayUserDetails(user);
detailView.enableEditing(false);
}
@Override
public void onError(String error) {
detailView.showLoading(false);
detailView.showError(error);
}
});
}
}
@Override
public void onEditClicked() {
if (detailView != null) {
detailView.enableEditing(true);
}
}
@Override
public void saveUser(User user) {
if (detailView != null) {
detailView.showLoading(true);
repository.saveUser(user, new UserDataSource.SaveUserCallback() {
@Override
public void onUserSaved() {
detailView.showLoading(false);
detailView.showUserSaved();
}
@Override
public void onError(String error) {
detailView.showLoading(false);
detailView.showError(error);
}
});
}
}
@Override
public void deleteUser(long userId) {
if (detailView != null) {
detailView.showLoading(true);
repository.deleteUser(userId, new UserDataSource.DeleteUserCallback() {
@Override
public void onUserDeleted() {
detailView.showLoading(false);
detailView.showUserDeleted();
}
@Override
public void onError(String error) {
detailView.showLoading(false);
detailView.showError(error);
}
});
}
}
@Override
public void onDestroy() {
// Избегаем утечек памяти, очищая ссылки на View
listView = null;
detailView = null;
}
}
Контроллер служит связующим звеном между Model и View. Он получает события от View, взаимодействует с Model и обновляет View в соответствии с полученными данными. Важно отметить, что контроллер не должен содержать бизнес-логику или логику отображения — он только координирует работу других компонентов.
| Компонент MVC | Реализация | Взаимодействия |
|---|---|---|
| Model | UserRepository + источники данных | Предоставляет данные контроллеру через callbacks |
| View | Activity, реализующая UserListView или UserDetailView | Отображает данные, передаёт события пользователя контроллеру |
| Controller | UserControllerImpl, реализующий UserController | Обрабатывает события от View, запрашивает данные от Model, обновляет View |
Преимущества такой реализации MVC:
- Разделение ответственности: каждый компонент отвечает за свою часть функциональности
- Тестируемость: можно легко создать mock-объекты для Model и View при тестировании Controller
- Поддерживаемость: изменения в одном компоненте минимально влияют на другие
- Гибкость: можно изменить источники данных без изменения UI и наоборот
Недостатки этой реализации:
- Boilerplate-код: необходимость создания множества интерфейсов и классов
- Callback Hell: при сложной логике может быть много вложенных callback'ов (решается использованием RxJava или Kotlin Coroutines)
- Связность View-Controller: в Android часто сложно полностью отделить View от Controller
Для тестирования такой архитектуры можно использовать следующий подход:
public class UserControllerTest {
@Mock
private UserListView mockListView;
@Mock
private UserRepository mockRepository;
@Captor
private ArgumentCaptor<UserDataSource.LoadUsersCallback> callbackCaptor;
private UserController controller;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
controller = new UserControllerImpl(mockListView, mockRepository);
}
@Test
public void loadUsers_showsUsersWhenDataAvailable() {
// Arrange
List<User> users = Arrays.asList(
new User(1, "John", "john@example.com", null),
new User(2, "Jane", "jane@example.com", null)
);
// Act
controller.loadUsers();
// Verify repository interaction
verify(mockRepository).getUsers(callbackCaptor.capture());
verify(mockListView).showLoading(true);
// Simulate callback from repository
callbackCaptor.getValue().onUsersLoaded(users);
// Assert
verify(mockListView).showLoading(false);
verify(mockListView).displayUsers(users);
verify(mockListView, never()).showNoUsers();
}
@Test
public void loadUsers_showsNoUsersWhenDataEmpty() {
// Arrange
List<User> users = Collections.emptyList();
// Act
controller.loadUsers();
// Verify repository interaction
verify(mockRepository).getUsers(callbackCaptor.capture());
// Simulate callback from repository
callbackCaptor.getValue().onUsersLoaded(users);
// Assert
verify(mockListView).showNoUsers();
verify(mockListView, never()).displayUsers(any());
}
}
MVC в Android — не просто архитектурный паттерн, а путь к созданию более гибких, тестируемых и поддерживаемых приложений. Корректное разделение обязанностей между Model, View и Controller позволяет избежать большинства проблем, связанных с "монолитными" Activity или Fragment. При этом важно помнить, что чистый MVC — лишь отправная точка. По мере роста вашего приложения, вы можете дополнять его элементами других архитектурных паттернов или переходить к MVP или MVVM, которые лучше адаптированы для современной Android-разработки. Главное — следовать принципам разделения ответственности, которые лежат в основе всех этих подходов.