С недавних пор заинтересовался данной темой и порылся в дебрях сети на эту тему. На англоязычных ресурсах есть раскиданная по разным местам информация. Прочитал все, что есть в рунете на эту тему. Ссылки приведу в конце статьи. В итоге, стал применять этот подход в своих приложениях.

Для начала, вспомним, что такое MVP. MVP — это паттерн, который позволяет разбивать приложение на три основных слоя (компонента):
  1. Модель (Model) — где сосредоточены данные;
  2. Представление (View) — интерфейс приложения (UI — элементы);
  3. Presenter — промежуточный компонент, который реализует связь между Моделью и Представлением.

image

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)


  1. gurinderu
    24.07.2015 11:56

    Спасибо за Dagger2. Очень интересная идея кодогенерации для реализации DI


  1. gshock
    24.07.2015 12:02

    А мне вот в Dagger 2 не понравилось то, что setupComponents() нужно реализовывать в каждой activity/fragment. Нельзя просто взять и вынести этот код в базовый класс activity/fragment. Он просто не будет работать и инъектить зависимости


    1. gurinderu
      24.07.2015 16:51

      Что-то я не помню, чтобы там нужно было реализовывать setupComponents


  1. EngineerSpock
    24.07.2015 22:44

    MVP — это паттерн, который позволяет разбивать приложение на три основных слоя (компонента):

    MVP (как и MVC, MVVM, MVPVM) — это всего лишь UI-паттерн, находящийся на границе приложения. Такой паттерн не может «разбивать приложение на три основных слоя». Эти паттерны не описывают архитектуру приложений (хоть и оказывают влияние), это всего лишь UI-boundary patterns. Я понимаю, что вы имели ввиду, но хотелось всё же указать на то, что так говорить некорректно.


    1. withoutuniverse
      26.07.2015 14:30
      +2

      Как раз говорить, как говорите вы, некорректно. Какие же это UI паттерны? Что значит граница приложения? Где можно почитать про то, что это всё вместе UI-паттерн?

      Мне всегда казалось, что это архитектурные паттерны.
      В MVC (к примеру) модель, UI и схема их взаимодействия разделены.
      Основная цель применения этой концепции состоит в разделении бизнес-логики (модели) от её визуализации (представления, вида).
      Т.е. они используются для грамотного разделения модели и UI, это очевидно.
      Разделять приложение на слои (я даже не говорю про разделение на уровни в рамках одного слоя) и нужно, если следовать данным архитектурным паттернам.


      1. atetc
        17.08.2015 00:32

        Antonio Leiva кстати тоже пишет, что это не архитектурные паттерны.