Для начала, вспомним, что такое MVP. MVP — это паттерн, который позволяет разбивать приложение на три основных слоя (компонента):
- Модель (Model) — где сосредоточены данные;
- Представление (View) — интерфейс приложения (UI — элементы);
- Presenter — промежуточный компонент, который реализует связь между Моделью и Представлением.
MVP паттерн — это наследник более известного шаблона — MVC. Применение такого подхода позволяет отделить в приложении логику от интерфейса.
То, как сейчас реализована структура в приложении Android, с «натяжкой» можно назвать чем-то похожим на MVP. Т.е. модель — это какие-либо данные, представление — это UI-элементы в наших layout-xml файлах, а к presenter можно отнести Activity и Fragment. Но, это не так и не позволяет полностью отделить логику от данных и представления этих данных.
Теперь, давайте попробуем применить MVP в нашем Android приложении.
Для того, чтобы реализовать добавление ссылки на presenter во view применим еще один паттерн — Dependency Injection (DI). Для этого воспользуемся полезной библиотекой от Google – Dagger 2. В этой статье я не буду приводить описание библиотеки и ее компоненты, а также принципы ее работы. Подразумевается, что это нам уже известно.
Создадим тестовое приложение, которое будет выводить список конференций с сайта www.ted.com. Оно будет содержать одну главную activity и три фрагмента: фрагмент со списком, фрагмент с детализацией и фрагмент просмотра конференции.
Создание графа для DI вынесем в класс наследник Application:
public class TalksTEDApp extends Application {
private ITalksTEDAppComponent appComponent;
public static TalksTEDApp get(Context context) {
return (TalksTEDApp) context.getApplicationContext();
}
@Override
public void onCreate() {
super.onCreate();
buildGraphAndInject();
}
public ITalksTEDAppComponent getAppComponent() {
return appComponent;
}
public void buildGraphAndInject() {
appComponent = DaggerITalksTEDAppComponent.builder()
.talksTEDAppModule(new TalksTEDAppModule(this))
.build();
appComponent.inject(this);
}
}
В базовом абстрактном классе BaseActivity определим абстрактный метод setupComponent, который будет определять компонент для каждой активити в нашем приложении.
public abstract class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setupComponent(TalksTEDApp.get(this).getAppComponent());
}
protected abstract void setupComponent(ITalksTEDAppComponent appComponent);
}
Для того, чтобы мы могли применить DI и для наших фрагментов, нам нужно:
1. Создать интерфейс IHasComponent, который будет имплементирован в каждой нашей активити:
public interface IHasComponent <T> {
T getComponent();
}
2. Создать базовый абстрактный класс для фрагментов:
public abstract class BaseFragment extends Fragment {
@SuppressWarnings("unchecked")
protected <T> T getComponent(Class<T> componentType) {
return componentType.cast(((IHasComponent<T>)getActivity()).getComponent());
}
}
3. Создать интерфейс, кторый будет реализован в каждом Presenter для наших фрагментов:
public interface BaseFragmentPresenter<T> {
void init(T view);
}
Далее, создадим Module и Component классы для нашего Application:
@Module
public class TalksTEDAppModule {
private final TalksTEDApp app;
public TalksTEDAppModule(TalksTEDApp app) {
this.app = app;
}
@Provides
@Singleton
public Application provideApplication() {
return app;
}
}
@Singleton
@Component(
modules = {
TalksTEDAppModule.class
}
)
public interface ITalksTEDAppComponent {
void inject(TalksTEDApp app);
}
Определим для наших активити компонентов отдельный скоуп как:
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface ActivityScope {
}
После этого мы можем написать класс активити для нашего приложения. В данном примере это одна главная активити. В коде приведены интересные нам методы, а полный код вы можете посмотреть на GitHub (ссылка на проект в конце статьи.)
public class MainActivity extends BaseActivity implements IMainActivityView, IHasComponent<IMainActivityComponent> {
@Inject
MainActivityPresenterImpl presenter;
private IMainActivityComponent mainActivityComponent;
...
@Override
protected void setupComponent(ITalksTEDAppComponent appComponent) {
mainActivityComponent = DaggerIMainActivityComponent.builder()
.iTalksTEDAppComponent(appComponent)
.mainActivityModule(new MainActivityModule(this))
.build();
mainActivityComponent.inject(this);
}
@Override
public IMainActivityComponent getComponent() {
return mainActivityComponent;
}
...
}
Следующий шаг — это реализация наших фрагментов (приведу лишь код методов, которые нам интересны в плане реализации DI):
public class ListFragment extends BaseFragment implements IListFragmentView {
@Inject
ListFragmentPresenterImpl presenter;
protected SpiceManager spiceManager = new SpiceManager(TalkTEDService.class);
private Activity activity;
private ListView listView;
private TalkListAdapter talkListAdapter;
private View rootView;
public ListFragment() {
}
...
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
this.getComponent(IMainActivityComponent.class).inject(this);
}
@Override
public void onResume() {
super.onResume();
presenter.init(this);
presenter.onResume(spiceManager);
}
...
}
И последнее — это пример реализации наших классов presenter.
Для активити:
public class MainActivityPresenterImpl implements IMainActivityPresenter {
private IMainActivityView view;
@Inject
public MainActivityPresenterImpl(IMainActivityView view) {
this.view = view;
}
@Override
public void onBackPressed() {
view.popFragmentFromStack();
}
}
Для фрагмента:
public class ListFragmentPresenterImpl implements IListFragmentPresenter {
int offset = 0;
private static final String URL_LIST_TALKS_API = "https://api.ted.com/v1/talks.json?api-key=umdz5qctsk4g9nmqnp5btsmf&limit=30";
private IListFragmentView view;
int totalTalks;
private SpiceManager spiceManager;
@Inject
public ListFragmentPresenterImpl() {
}
@Override
public void init(IListFragmentView view) {
this.view=view;
}
...
}
Эта статья не претендует на полное описание MVP и Dependency Injection, это еще один пример, как их можно применить в структуре Android приложения. Вначале может показаться, что нагромождение лишних классов и интерфейсов уменьшает читаемость кода, но это не так. После применения MVP на практике становится проще ориентироваться в приложении, код проще расширять.
Мы все больше программируем на уровне интерфейсов, а не реализации, компоненты приложения слабо связаны, что уменьшает количество ошибок при изменениях. А с использованием Dependency Injection код становится более лаконичным и читаемым.
Отмечу, что в данный момент в Dagger 2 есть один недостаток. Мы не можем делать в модуле override (это реализовано в Dagger от Square). Это создает проблемы при написании тестов. Надеюсь, что в последующих обновлениях библиотеки этот недочет будет исправлен. Кому интересна эта тема — есть пост на StackOverflow здесь и здесь.
Приведу ссылки на статьи, которые довели меня до такой жизни:
habrahabr.ru/post/202866
habrahabr.ru/post/252903
antonioleiva.com/mvp-android
antonioleiva.com/dependency-injection-android-dagger-part-1
antonioleiva.com/dagger-android-part-2
antonioleiva.com/dagger-3
Android MVP — Community
Полный код тестового проекта, который рассмотрен в статье, вы можете посмотреть и скачать на GitHub.
Комментарии (6)
gshock
24.07.2015 12:02А мне вот в Dagger 2 не понравилось то, что setupComponents() нужно реализовывать в каждой activity/fragment. Нельзя просто взять и вынести этот код в базовый класс activity/fragment. Он просто не будет работать и инъектить зависимости
EngineerSpock
24.07.2015 22:44MVP — это паттерн, который позволяет разбивать приложение на три основных слоя (компонента):
MVP (как и MVC, MVVM, MVPVM) — это всего лишь UI-паттерн, находящийся на границе приложения. Такой паттерн не может «разбивать приложение на три основных слоя». Эти паттерны не описывают архитектуру приложений (хоть и оказывают влияние), это всего лишь UI-boundary patterns. Я понимаю, что вы имели ввиду, но хотелось всё же указать на то, что так говорить некорректно.withoutuniverse
26.07.2015 14:30+2Как раз говорить, как говорите вы, некорректно. Какие же это UI паттерны? Что значит граница приложения? Где можно почитать про то, что это всё вместе UI-паттерн?
Мне всегда казалось, что это архитектурные паттерны.
В MVC (к примеру) модель, UI и схема их взаимодействия разделены.
Основная цель применения этой концепции состоит в разделении бизнес-логики (модели) от её визуализации (представления, вида).
Т.е. они используются для грамотного разделения модели и UI, это очевидно.
Разделять приложение на слои (я даже не говорю про разделение на уровни в рамках одного слоя) и нужно, если следовать данным архитектурным паттернам.
gurinderu
Спасибо за Dagger2. Очень интересная идея кодогенерации для реализации DI