Hun's Blog

[Android] 디자인패턴 4 - MVP패턴 Google todo-mvp 본문

Android

[Android] 디자인패턴 4 - MVP패턴 Google todo-mvp

jhk-im 2020. 3. 22. 10:11

Google todo-mvp 예제

 

참고 

https://github.com/android/architecture-samples/tree/todo-mvp

 

todo-mvp 전체를 분석하기에는 양이 너무 많기 때문에 참고하여 간단하게 MainActivity와 프래그먼트를 구현한 예제를 통해서 mvp를 알아보도록 하자. 

 

 

기능은 제외하고 todo-mvp에서 구현한 mvp의 껍데기만 남겨놓았다. (실제 todo-mvp와는 조금 다르다) 

메인 액티비티에서 영상, 기록, test 3가지의 프래그먼트를 보여주고 있다. 

Navigation Drawer를 사용하여 왼쪽에 네비게이션 뷰를 구현하였고 그안에있는 내정보, About, Notice를 클릭했을때 각각 구분하여 토스트 메시지를 띄워주는 상태이다. 현재 상태에서 mvp를 어떤식으로 구현하였는지 살펴보고 기능을 하나씩 추가해보면서 분석해보도록 하겠다. 

 

 

클래스 구조 

이미지 1 todo-mvp 구조 

 

mvp형태는 그대로 가지고 있으면서 Contract, BasePresenter, BaseView 인터페이스가 추가되었다. 프래그먼트가 어떠한 방식으로 액티비티에 표시되는지 자세히 보도록 하자. 

 

 

이미지 2 todo-mvp UML

 

프래그먼트가 표시되는 과정을 간략하게 표현하면 이미지 2와 같다. 번호 순서대로 하나하나 씩 분석해 보았다. 

 

 

1. BaseView<T> 인터페이스

1
2
3
public interface BaseView<T> {
    void setPresenter(T presenter);
}
 
 

- 특정하지 않은 형태를 제네릭으로 지정하는 인터페이스이다. 

* T는 Contract에서 생성할 Presenter가 지정된다. 

- Presenter를 입력하는 setPresenter()메소드가 있다. 

 

2. BasePresenter 인터페이스

1
2
3
public interface BasePresenter {
    void start();
}
 
 

- start() 메소드가 있다.

 

형태로만 봐서는 2개의 인터페이스가 어떻게 사용되는지 아직 알 수 없다. 

 

 

 

3. Contract 인터페이스

1
2
3
4
5
6
7
public interface VideoContract {
    interface View extends BaseView<Presenter> {
    }
    interface Presenter extends BasePresenter{
        void loadMain(boolean forceUpdate);
    }
}
 
 

- View 내부 인터페이스가 있다.

   - BaseView를 implements하고 제네릭으로 Presenter 내부 인터페이스를 지정한다. 

- Presenter 내부 인터페이스가 있다. 

   - BasePresenter를 implements 한다. 

   - loadMain(boolean forceUpdate) 메소드가 있다. 

 

todo-mvp에서 contract를 사용한 이유는 View와 Presenter를 하나의 인터페이스에 각각 정의하여 한눈에 보도록하여 가독성을 높이는 것과 이해를 돕기위한 이유도 있다. 

또한 BaseView<T>와 BasePresenter에 대해 생각해 볼 수 있는 것은 액티비티, 프래그먼트 등의 View에서는 어떠한 경우라도 setPresenter()를 반드시 구현하도록 하겠다는 것이며 

비즈니스 로직이 구현되는 Presenter 클래스에서는 반드시 start() 메소드가 구현되도록 하겠다는 것이다. 

두 메소드의 정확한 사용용도에 대해서는 뒤에서 다시한번 짚어보도록 하자. 

 

 

 

4. Presenter 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class VideoPresenter implements VideoContract.Presenter {
    private final VideoContract.View mMainView;
    @SuppressLint("RestrictedApi")
    public VideoPresenter(@NonNull VideoContract.View mainView){
        mMainView = checkNotNull(mainView, "mainView cannot be null");
        mMainView.setPresenter(this);
    }
    @Override
    public void start() {
        loadMain(false);
    }
    @Override
    public void loadMain(boolean forceUpdate) {}
}
 
 

- Contract.Presenter 를 implements 한다.  

  - loadMain() 메소드가 오버라이딩 되어 구현된다.

  - start() 메소드가 오버라이딩 되어 구현된다. + loadMain() 메소드에 false를 입력한다.  

- Contract.View가 멤버변수로 존재한다.

- Contract.View를 입력받는 Presenter 생성자있다

  - 입력받은 View를 멤버변수와 매치시킨다. 

  - 매치시킨 View의 setPresenter()에 현재 Presenter 클래스를 입력한다.   

 

Contract.Presenter를 상속받아서 start()와 loadMain()이 오버라이딩 되어 start()에서 loadMain()에 false를 입력한다.  loadMain은 forceUpdate -> 강제 업데이트 여부를 boolean 값으로 입력받는다는 의미인데 여기까지만 봐서는 존재하는 이유가 정확히 판별이 되지 않는다. 뒤에가서 정확히 짚어보도록 하자. 

 

액티비티,프래그먼트 등의 View에서 Presenter인터페이스를 상속받은 Presenter 클래스에 구현된 비즈니스 로직을 활용한다.  Presenter인터페이스와 클래스는 각각의 View 별로 나뉘어 구현되며 해당 View에서 비즈니스 로직을 활용하기 위해 Presenter 객체를 생성할 때 View를 입력받게 되며 해당 View의 Presenter 객체가 현재의 Presenter 클래스임을 지정하는 sertPresenter() 메소드를 활용하여 지정함으로써 View와 Presenter를 연결하는 작업을 마무리하게 된다. 

 

이제 여기에 비즈니스 로직이 추가된다면 Contract.Presenter에 메소드를 추가하고 오버라이딩 하여 구현하면 된다. 

구현된 비즈니스 로직을 활용해 UI가 갱신되어야 한다면 그 부분은 Contract.View에 메소드를 추가하고 오버라이딩하여 액티비티나 프래그먼트 클래스에서 구현하면 될 것이다.

 

 

5. MainActivity 클래스 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class FragmentBtnOnClickListener implements  Button.OnClickListener {
    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.btn_video_frag :
                // Create the presenter
                videoFragment = VideoFragment.newInstance();
                ActivityUtils.replaceFragmentToActivity(
                        getSupportFragmentManager(), videoFragment, R.id.contentFrame);
                VideoPresenter mVideoPresenter = new VideoPresenter(videoFragment);
                break;
            case R.id.btn_record_frag :
                // Create the presenter
                recordFragment = RecordFragment.newInstance();
                ActivityUtils.replaceFragmentToActivity(
                        getSupportFragmentManager(), recordFragment, R.id.contentFrame);
                RecordPresenter mRecordPresenter = new RecordPresenter(recordFragment);
                break;
            case R.id.btn_sns_frag :
                // Create the presenter
                testFragment = TestFragment.newInstance();
                ActivityUtils.replaceFragmentToActivity(
                        getSupportFragmentManager(), testFragment, R.id.contentFrame);
                TestPresenter mTestPresenter = new TestPresenter(testFragment);
        }
    }
}
 
 

*중요한 부분만 자세히 살펴보자. 

FragmentBtnOnClickListener 내부 클래스에 switch / case  3가지 프래그먼트(영상,기록,Test)를 띄우는 각각의 버튼이 구현되어있다. 여기서 특정 프래그먼트가 선택 될때 3가지 작업을 한다.

 

1. 해당 프래그먼트의 newInstance() 메소드로 프래그먼트 객체를 생성한다.

2. 프래그먼트가 띄워지는 부분은 ActivityUtils 클래스에 따로 메소드가 구현되어 있기 때문에 ActivityUtils클래스로 메소드를 실행시킨다. -> 프래그먼트가 활성화된다. 

3. 해당 프래그먼트의 기능이 구현되어 있는 Presenter 객체를 생성하고 프래그먼트를 입력한다.  

 

 

 

6. ActivityUtils 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ActivityUtils {
    public static void addFragmentToActivity (@NonNull FragmentManager fragmentManager,
                                              @NonNull Fragment fragment, int frameId) {
        FragmentTransaction transaction = fragmentManager.beginTransaction();
        transaction.add(frameId, fragment);
        transaction.commit();
    }
    public static void replaceFragmentToActivity (@NonNull FragmentManager fragmentManager,
                                                  @NonNull Fragment fragment, int frameId){
        FragmentTransaction transaction = fragmentManager.beginTransaction();
        transaction.replace(frameId,fragment);
        transaction.commit();
    }
}
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4ftext-decoration:none">Colored by Color Scripter
 

- addFragmentToActivity : 맨 처음 프래그먼트를 표시할 때 사용하는 메소드 

- replaceFragmentToActivity : 기존 프래그먼트를 삭제하고 새로운 프래그먼트를 생성하는 replace() 메소드가 사용되는 메소드 

 

 

 

7. Fragment 클래스 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class VideoFragment extends Fragment implements VideoContract.View {
    private VideoContract.Presenter mPresenter;
    public VideoFragment(){}
    public static VideoFragment newInstance() {
        return new VideoFragment();
    }
    @SuppressLint("RestrictedApi")
    @Override
    public void setPresenter(@NonNull VideoContract.Presenter presenter) {
        mPresenter = checkNotNull(presenter);
    }
    @Override
    public void onResume() {
        super.onResume();
        mPresenter.start();
    }
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        View root = inflater.inflate(R.layout.main_video_frag, container, false);
        return root;
    }
}
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4ftext-decoration:none">Colored by Color Scripter
 

- Contract.View를 implements 한다. 

  - setPresenter() 가 구현되어 있으며 해당 View에서 사용될 Presenter를 매치시킨다. 

- onResume() 일때 Presnter.start()메소드가 실행된다. 

- onCreateView() fragment.xml을 매치시킨다. 

 

Presenter 클래스와 비교하면 정반대로 구현된 모습을 확인할 수 있다. 이 과정을 통해 View와 Presenter가 매치되고 각각의 영역에서 View 로직과 비즈니스 로직이 구현될 것이라는걸 확인할 수 있다. 

곳에서 위에서 마무리하지 못한 start() 메소드가 onResume일때 실행된다. 완벽하지는 않지만 현재 View가 실행되고 해당 뷰와 매치된 Presenter가 실행되었다는 상태를 구분한다는 느낌을 받았다. 

 

Contract가 사용되어 내부 인터페이스로 Presenter와 View가 구분되는것을 살펴보았다. 

실제 todo-mvp 프로젝트에서는 Room 데이터베이스를 활용하여 Task를 추가/삭제/수정 하는 부분까지 구현되어있다. 디자인패턴에 관련된 내용이 끝나면 로컬 데이터베이스에 관련하여 정리할 예정이니 그때 위 예제를 활용해서 todo-mvp에 대해 공부해보도록 하자.