Pull to refresh

Вариант реализации MVP + DI(Dagger 2) в Android

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

Для начала, вспомним, что такое 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.
Tags:
Hubs:
+10
Comments 6
Comments Comments 6

Articles