Android MVP pattern
3월에 있었던 Droid Knight 행사에서 참석자들을 대상으로 했었던 재미있는 설문입니다.
많은 분들이 MVP 패턴을 쓰고 있네요.
구조가 간단하다면 직관성이 더 높은 MVC 패턴이 더 나을 수 있지만, 구조가 복잡해지기 쉽고 Activity에 대한 종속성이 강한 안드로이드에서는 MVC 패턴을 이용하기에 여러 단점이 있을 수 있습니다.
이에 대한 자세한 내용은 아래 링크를 참조해 주시기 바랍니다.
MVP is a user interface architectural pattern engineered to facilitate automated unit testing and improve the separation of concerns in presentation logic
자동화된 단위 테스트를 용이하게 하고 presentation 로직에서 SOC(관심의 분리)를 개선하도록 설계된 사용자 인터페이스 아키텍처 패턴입니다.
** SOC : 복잡성을 줄여준다는 개념입니다.
** https://arload.wordpress.com/2009/04/01/soc_n_complexity/
MVP 패턴의 동작을 보면 아래와 같습니다.
1. View에서 사용자 이벤트 수신
2. View에서 Presenter 이벤트 호출
3. Presenter에서 Model에 데이터 요청
4. Model에서 Presenter로 데이터 전달
5. Presenter에서 전달받은 데이터를 기반으로 View 업데이트
6. View에서 화면 업데이트
어려운 로직은 아닙니다. 그냥 Presenter가 View와 Model 사이 매개체 역할을 하는 로직입니다.
구글에서는 안드로이드 MVP 패턴 기본 구조 샘플들을 github를 통해 제공하고 있습니다.
보시면 샘플들이 많은데 저는 Dagger 2를 이용한 MVP 패턴을 구현하려 합니다.
먼저 Contract interface를 정의합니다.
Contract interface는 View와 Presenter 간의 이벤트들을 선언합니다.
Contract.View interface
Presenter에서 View를 업데이트하기 위한 이벤트들을 선언합니다. [5. Update view]
Contrack.Presenter interface
View에서 호출할 Presenter 이벤트들을 선언합니다. [2. Invoke event]
/**
* This specifies the contract between the view and the presenter.
*/
public interface AddEditTaskContract {
interface View extends BaseView <Presenter> {
void showEmptyTaskError();
void showTasksList();
void setTitle(String title);
void setDescription(String description);
boolean isActive();
}
interface Presenter extends BasePresenter {
void saveTask(String title, String description);
void populateTask();
}
}
Activity나 Fragment에서 Contract.View interface를 implement 합니다.
즉, Activity나 Fragement에서는 사용자 이벤트를 처리하거나, 화면을 표시하는 일만 하면 됩니다.
그리고 Contract.Presenter presenter 변수를 통해 Presenter의 이벤트를 호출할 수 있습니다.
presenter 변수는 @Inject annotation을 통해 의존성을 요청하고, Dagger 2를 통해 주입받게 됩니다.
Dagger 2에 대한 내용은 뒤에 다루겠습니다.
public class MainActivity extends AppCompatActivity {
@Inject
Contract.Presenter presenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Create the presenter by Dagger
DaggerComponent.builder()
. addModule(new Module(this))
. build()
. inject(this);
presenter.populateTask(); // Call Contract.Presenter event
}
@Override
void showEmptyTaskError() {
Toast.makeText(this, "error", Toast.LENGTH_SHORT). show();
}
@Override
void showTasksList() {
//
}
@Override
void setTitle() {
//
}
@Override
void setDes() {
//
}
}
별도의 Presenter class에서 Contract.Presenter interface를 implement 합니다.
Presenter에서는 View와 Model 사이의 매개체 역할을 하게 됩니다.
즉, 주요 동작 로직들은 Presenter에서 수행하게 됩니다.
Contract.View view 변수를 통해 View의 UI들을 업데이트할 수 있습니다.
view 변수는 생성자에서 @Inject annotation을 통해 의존성을 요청하고, Dagger 2를 통해 주입받게 됩니다. Dagger 2에 대한 내용은 뒤에 다루겠습니다.
final class MainPresenter implements Contract.Presenter {
@NonNull
private final Contract.View view;
@Inject
MainPresenter(@NonNull Contract.View view) {
this.view = view;
}
@Override
public void saveTask(String title, String description) {
view.setTitle(title);
view.setDes(description);
}
@Override
public void populateTask() {
//
}
}
의존성 주입(Dependency Inject) 개념은 이전 글들에 정리되어 있습니다.
MVP 패턴에서는 View에 필요한 Presenter 인스턴스 변수와, Presenter에 필요한 View 인스턴스 변수를 Dagger 2를 이용해 쉽게 주입시킬 수 있습니다.
android {
...
android {
configurations.all {
resolutionStrategy.force 'com.google.code.findbugs:jsr305:1.3.9'
}
}
}
dependencies {
...
// dagger
compile 'com.google.dagger:dagger:2.11-rc2'
annotationProcessor 'com.google.dagger:dagger-compiler:2.11-rc2'
provided 'javax.annotation:jsr250-api:1.0'
// dagger android
compile 'com.google.dagger:dagger-android:2.11-rc2'
compile 'com.google.dagger:dagger-android-support:2.11-rc2'
annotationProcessor 'com.google.dagger:dagger-android-processor:2.11-rc2'
}
앞서 봤듯이, @Inject annotation을 변수나 생성자에 선언하여 의존성을 Dagger 2에 요청할 수 있습니다.
public class MainActivity extends AppCompatActivity {
@Inject
Contract.Presenter presenter;
final class MainPresenter implements Contract.Presenter {
...
@Inject
MainPresenter(@NonNull Contract.View view) {
this.view = view;
}
먼저 Contract.View 인스턴스 변수를 제공하기 위해 @Provide 메서드와 @Module 클래스를 생성합니다.
@Module
public class Module {
private final Contract.View view;
public Module (Contract.View view) {
this.view = view;
}
@Provides
Contract.View provideView() {
return view;
}
}
@Inject와 @Module 사이의 의존성을 연결하기 위해서는 @Component anotation을 이용해야 합니다.
@Component(modules = Module.class)
public interface Component {
void inject(MainActivity mainActivity);
}
@Module 클래스, @Component 인터페이스까지 생성하면 Compoent에 "Dagger" 가 추가된 "DaggerComponent" 클래스가 자동으로 생성됩니다.
import dagger.internal.Preconditions;
import javax.annotation.Generated;
import javax.inject.Provider;
@Generated(
value = "dagger.internal.codegen.ComponentProcessor",
comments = "https://google.github.io/dagger"
)
public final class DaggerComponent implements Component {
....
자동으로 생성된 DaggerComponent를 통해 Contract.View, Contract.Presenter 인스턴스 변수(의존성)들을 주입할 수 있습니다.
public class MainActivity extends AppCompatActivity {
@Inject
Contract.Presenter presenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Create the presenter by Dagger
DaggerComponent.builder()
. addModule(new Module(this))
. build()
. inject(this);
presenter.populateTask(); // Call Contract.Presenter event
}
자세한 내용은 아래 링크를 참조해 주시기 바랍니다.
여기저기 찾아보면 MVP 패턴을 구현하는 방식도 다양합니다.
저는 제가 이해하기 제일 쉬운 방법으로 구현했는데 다른 사람들은 잘 모르겠네요.