[Android] 안드로이드 개발 레벨업 교과서 정리 #7 다양한 설계 기법 - MVP

출처 : 안드로이드 개발 레벨업 교과서 142~151p


1. 패키지가 나뉜 것을 확인하자

MVP에는 model, view, presenter와 contract라는 패키지가 있다.

contract는 계약, 약속이라는 의미이고 view와 presenter가 구현해야 할 인터페이스가 정의되어있다. 구조는 아래와 같이 되어있다.


기본적으로 View에서 Spinner로 아이템을 선택하는 이벤트가 presenter로 통지된다.

다음으로 그 선택에 따라 presenter가 model에 접근해서 데이터를 가져오거나, 가져온 데이터를 뷰에 반영한다. presenter와의 통신은 contract 패키지에서 정의한 인터페이스로 이뤄진다.

contract에 있는 인터페이스는 단어 뜻 그대로 계약서 라고 이해하면 될 듯 싶다. View와 Presenter가 계약서만 가지고 이벤트나 데이터를 주고받는다.

추가적으로 리사이클러뷰 어댑터는 View와 Model의 역할을 가지고있지만 View에 더 가깝다고 한다. 근데 어떤게 맞는지 잘 모르겠다.

참고하기 : Adapter, 누구냐 넌? — Data? View?


2. MVP로 프로젝트를 구현하자

마찬가지로 액티비티를 살펴보면, 기본 구현(링크)과는 달리 Presenter에 대한 인터페이스로서 RepositoryListContract.View를 구현했다. 이것으로 Presenter가 View에 접근할 때는 액티비티 자체가 아니라 이 인터페이스를 통해 조작할 수 있다.

1
2
3
4
5
6
7
8
/**
* 리포지토리 목록을 표시하는 Activity
* MVP 의 View 역할을 가진다
*/
class RepositoryListActivity : AppCompatActivity(), RepositoryAdapter.OnRepositoryItemClickListener,
RepositoryListContract.View {
...
}

RepositoryListContract.View에는 Presenter가 View를 조작하는 데에 필요한 메서드가 선언되어있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 각자의 역할이 가진 Contract(계약)를 정의해 둘 인터페이스
*/
interface RepositoryListContract {
/**
* MVP 의 View 가 구현할 인터페이스
* Presenter 가 View 를 조작할 때 이용한다
*/
interface View {
val selectedLanguage: String
fun showProgress()
fun hideProgress()
fun showRepositories(repositories: GitHubService.Repositories)
fun showError()
fun startDetailActivity(fullRepositoryName: String)
}

// 생략. UserActions
}


RepositoryActivity 를 다시 보자.

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
class RepositoryListActivity : AppCompatActivity(), RepositoryAdapter.OnRepositoryItemClickListener,
RepositoryListContract.View {
private var languageSpinner: Spinner? = null

private var repositoryAdapter: RepositoryAdapter? = null

private var repositoryListPresenter: RepositoryListContract.UserActions? = null

override val selectedLanguage: String
get() = languageSpinner!!.selectedItem as String

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_repository_list)

// View 를 설정
setupViews()

// ① Presenter 의 인스턴스를 생성
val gitHubService = (application as NewGitHubReposApplication).gitHubService
repositoryListPresenter = RepositoryListPresenter(this, gitHubService)
}

private fun setupViews() {

// 생략

// Spinner
languageSpinner = findViewById<View>(R.id.language_spinner) as Spinner
val adapter = ArrayAdapter<String>(this, android.R.layout.simple_spinner_item)
adapter.addAll("java", "kotlin", "objective-c", "swift", "groovy", "python", "ruby", "c")
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
languageSpinner!!.adapter = adapter
languageSpinner!!.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
// 스피너의 선택 내용이 바뀌면 호출된다
val language = languageSpinner!!.getItemAtPosition(position) as String
// ② Presenter 에 프로그래밍 언어를 선택했다고 알린다
repositoryListPresenter!!.selectLanguage(language)
}

override fun onNothingSelected(parent: AdapterView<*>) {

}
}
}

/**
* RecyclerView 에서 클릭됐다
* @see RepositoryAdapter.OnRepositoryItemClickListener.onRepositoryItemClickListener
*/
override fun onRepositoryItemClick(item: GitHubService.RepositoryItem) {
repositoryListPresenter!!.selectRepositoryItem(item)
}

// =====RepositoryListContract.View 구현=====
// 이곳에서 Presenter 로부터 지시를 받아 View 의 변경 등을 한다

// 생략

override fun showRepositories(repositories: GitHubService.Repositories) {
// ③ 리포지토리 목록을 Adapter 에 설정한다
repositoryAdapter!!.setItemsAndRefresh(repositories.items)
}
}

①에서 onCreate()로 Presenter의 인스턴스를 생성한다.

다음으로 ②에서는 Spinner로 아이템이 선택 됐을 때 Presenter에 알리기위해 selectLanguage() 메서드를 호출한다. 여기서 Presenter는 선택된 프로그래밍 언어의 저장소 목록을 Model로부터 가져온다.

목록을 가져온 후 ③에서 Presenter가 View의 showRepositories() 메서드를 호출하고, 파라미터로 전달된 데이터를 Adapter에 설정하면 데이터가 표시된다.


이로써 액티비티에서는 View 표시와 Presenter 접근만 하도록 구현하였다. API 접근 구현도 액티비티 안에서 사라졌다. 이렇게 액티비티는 뷰 표시에만 전념할 수 있게되었다.

(그런데 override도 많고 다시 주고받아서 뭘 하는지 잘 이해가 안된다.)


다음으로 RepositoryListPresenter는 View가 통지하는 이벤트를 받기위해 RepositoryListContract.UserActions를 구현한다.

1
2
3
4
class DetailPresenter(private val detailView: DetailContract.View, private val gitHubService: GitHubService) :
DetailContract.UserActions {
...
}

다시한번 회고하면, RepositoryListContract는 RepositoryListActivity와 RepositoryListPresenter 사이를 매개하는 계약서이다. (Presenter의 역할은 데이터 요청, 액티비티 전환 이벤트 전달)

1
2
3
4
5
6
7
8
9
10
11
12
interface RepositoryListContract {
// 생략. View

/**
* MVP 의 Presenter 가 구현할 인터페이스
* View 를 클릭했을 때 등 View 가 Presenter 에 알릴 때 이용한다
*/
interface UserActions {
fun selectLanguage(language: String)
fun selectRepositoryItem(item: GitHubService.RepositoryItem)
}
}


RepositoryListPresenter.kt

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class RepositoryListPresenter(
private val repositoryListView: RepositoryListContract.View,
private val gitHubService: GitHubService
)// ① RepositoryListContract.View 로써 멤버 변수에 저장한다
: RepositoryListContract.UserActions {

override fun selectLanguage(language: String) {
loadRepositories()
}

override fun selectRepositoryItem(item: GitHubService.RepositoryItem) {
repositoryListView.startDetailActivity(item.full_name)
}

/**
* 지난 일주일간 만들어진 라이브러리의 인기순으로 가져온다
*/
private fun loadRepositories() {
// ② 로딩 중이므로 진행바를 표시한다
repositoryListView.showProgress()

// 일주일 전 날짜 문자열 지금이 2016-10-27이면 2016-10-20 이라는 문자열을 얻는다
val calendar = Calendar.getInstance()
calendar.add(Calendar.DAY_OF_MONTH, -7)
val text = DateFormat.format("yyyy-MM-dd", calendar).toString()

// Retrofit 을 이용해 서버에 액세스한다

// 지난 일주일간 만들어지고 언어가 language 인 것을 쿼리로 전달한다
val observable =
gitHubService.listRepos("language:" + repositoryListView.selectedLanguage + " " + "created:>" + text)
// 입출력(IO)용 스레드로 통신해 메인스레드로 결과를 받아오게 한다
observable.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Subscriber<GitHubService.Repositories>() {
override fun onNext(repositories: GitHubService.Repositories) {
// ③ 로딩을 마쳤으므로 진행바 표시를 하지 않는다
repositoryListView.hideProgress()
// ④ 가져온 아이템을 표시하기 위해, RecyclerView 에 아이템을 설정하고 갱신한다
repositoryListView.showRepositories(repositories)
}

override fun onError(e: Throwable) {
// 통신에 실패하면 호출된다
// 여기서는 스낵바를 표시한다(아래에 표시되는 바)
repositoryListView.showError()
}

override fun onCompleted() {
// 아무것도 하지 않는다
}
})
}

}

Presenter는 View의 구현에 대해 자세한 내용을 알 필요 없이 자신의 역할인 사용자의 액션을 처리하고 모델에 접근하는 데에만 전념할 수 있다.


3. 고찰과 깨달음

이로써 액티비티에 구현을 가득 채우지 않고, 뷰와 프레젠터의 역할을 나눌 수 있었다. 또한 인터페이스를 통해 서로 접근할 수 있도록 구현했으므로 테스트가 쉬워졌다. (테스트를 안해봐서 잘 와닿지 않는다)

하지만 이번 구현에서는 데이터와 상태를 뷰에 반영하는 부분에서 한줄로 구현된 메서드를 많이 만들어야해서 복잡했다. MVVM에서는 이런 부분은 어떻게 구현하게 될까?

Share