MVC в Android: примеры внедрения паттерна в Java-приложения

Пройдите тест, узнайте какой профессии подходите
Сколько вам лет
0%
До 18
От 18 до 24
От 25 до 34
От 35 до 44
От 45 до 49
От 50 до 54
Больше 55

Для кого эта статья:

  • Разработчики, желающие улучшить свои навыки в 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 позволяет:

  1. Отделить бизнес-логику от UI, что упрощает модульное тестирование.
  2. Обеспечить возможность независимого развития компонентов.
  3. Повысить читаемость кода за счет четкого разделения ответственности.
  4. Упростить поддержку и расширение функционала приложения.

Алексей Соколов, 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:

Java
Скопировать код
// Базовый интерфейс для 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 и работать автономно. 📊

Начнем с создания простого класса данных:

Java
Скопировать код
// Класс данных
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;
}

// Геттеры и сеттеры опущены для краткости
}

Далее создадим источники данных — локальный и удаленный:

Java
Скопировать код
// Интерфейс для источника данных
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());
}
});
}

// Остальные методы опущены для краткости
}

Теперь создадим репозиторий, который будет абстрагировать источники данных от остальной части приложения:

Java
Скопировать код
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, который определит контракт между представлением и контроллером:

Java
Скопировать код
// Интерфейс представления для списка пользователей
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:

Java
Скопировать код
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 для отображения деталей пользователя:

Java
Скопировать код
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. 🔄

Создадим интерфейс контроллера для работы с пользователями:

Java
Скопировать код
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);
}

Теперь реализуем этот интерфейс:

Java
Скопировать код
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

Для тестирования такой архитектуры можно использовать следующий подход:

Java
Скопировать код
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-разработки. Главное — следовать принципам разделения ответственности, которые лежат в основе всех этих подходов.

Загрузка...