MVP + Kotlin Example

Hanmolee
10 min readNov 23, 2018

--

MVP 패턴의 기본 구현 예제입니다.

회사 프로젝트에 테스트 코드를 도입하기 위해 가장 안정적인 패턴을 찾던 중

프로젝트와 알맞은 패턴으로 MVP를 적용하게 되었습니다.

앞서 작성한 Dagger2와 함께 Presenter를 주입하기 위한 예제를 만들기 전에 필요한 학습인 MVP 패턴에 대한 예제를 만들어 보았습니다.

MVP란?

Model-View-Presenter로 이루어진 상호 의존성을 떨어뜨려 결합도를 낮출 수 있는 디자인 패턴입니다.

Model :

DB, REST API 등 비즈니스 로직 및 데이터와 관련된 처리역할을 하는 부분입니다.

View :

Activity 와 Fragment 같이 View를 담당하는 사용자 인터페이스 역할을 하는 부분입니다.

Presenter :

View에서 이벤트를 받아서 데이터를 가공하여 View 에 전달하는 역할을 하는 부분입니다. View와 Model의 다리 역할을 해줍니다.

Model, View, Presenter가 각각 어떤 역할을 하는지 예제로 확인해 보겠습니다.

예제 시나리오는 간단합니다.

  • 사용자가 버튼을 누르면 Dog List의 이름과 나이가 보여야 한다.

이때 각자의 역할의 흐름을 살펴보면

1. 사용자가 버튼을 누른다.

→ View에서 클릭 이벤트가 발생

→ View에서 발생한 클릭 이벤트를 Presenter로 전달.

2. Dog List를 가져온다.

(이때 Dog List를 가져온다는 것을 DB 혹은 REST API를 호출한다고 생각하면 좀 더 이해하기 쉬울거 같습니다.)

→ Presenter가 Model에 데이터를 요청한다.

→ Model이 Presenter에 요청한 데이터를 전달한다. (서버,디비에서 가져온 데이터)

→ Presenter가 Model에서 전달 받은 데이터를 정제한다.

→ Presenter가 정제한 데이터를 View에 전달한다.

3. Dog List의 이름과 나이가 보여야 한다.

→ View가 Presenter에서 전달받은 데이터를 사용자에게 보여준다.

저 흐름대로 구현한 예제를 보겠습니다.

  • 이 예제는 View, Presenter가 구현해야할 인터페이스를 정의하는 Contract 인터페이스를 사용하였습니다.

먼저 MVP에 공통적으로 들어가기 때문에 재사용성이 높은 부분을 만들어 줍니다.

BaseView

interface BaseView {

fun showError(error : String)

}
  • showError : Error를 출력하는 부분이 공통적으로 쓰인다는 가정하에 함수를 생성.

BasePresenter

interface BasePresenter<T> {

fun takeView(view: T)
fun dropView()

}
  • takeView : View가 생성 혹은 bind 될 때를 Presenter에 전달
  • dropView : View가 제거되거나 unBind 될 때를 Presenter에 전달

BaseActivity

abstract class BaseActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initPresenter()
}

abstract fun initPresenter()

}
  • initPresenter : View와 상호작용할 Presenter를 주입하기 위해

Model역할을 하는 Dog, DogList를 구현해 줍니다.

dog (ex : data model)

data class Dog(
val name : String,
val age : Int
)

DogList (ex : GET Response)

object DogListData {

fun getDoglistData(): List<Dog> {
return listOf(
Dog("Maltese",3),
Dog("Golden Retriever",5),
Dog("Siberian Husky",2)
)
}
}

View와 Presenter가 구현해야할 인터페이스를 정의 하는 Contract를 구현한다.

Contract

interface SearchContract {

interface View : BaseView {
fun showLoading()
fun hideLoading()
fun showDogList(dogList : List<Dog>)
}

interface Presenter : BasePresenter<View> {
fun getDogList()
}

}
  • showLoading, hideLoading : 데이터를 받아서 정제 하는동안 보일 ProgressBar관리하는 함수
  • showDogList : 정제한 데이터를 Presenter에서 전달받을 함수
  • getDogList : Model로 부터 데이터를 받아오기(정제하기) 위한 함수

View와 1 : 1 관계를 유지할 Presenter를 구현한다.

class SearchPresenter : SearchContract.Presenter {

private var searchView : SearchContract.View? = null

override fun takeView(view: SearchContract.View) {
searchView = view
}

override fun getDogList() {
searchView?.showLoading()

Handler().postDelayed({
val dogList = DogListData.getDoglistData()
searchView?.showDogList(dogList)
searchView?.hideLoading()
}, 1000)
}

override fun dropView() {
searchView = null
}

}
  • takeView : View와 Presenter를 연결해준다. (searchView = view)
  • getDogList :

1. searchView?.showLoading() : 1초간 네트워크와 통신하는 척을 해주었다. (View에 프로그레스바를 보여주도록 요청한다.)

2. DogListData.getDoglistData() : Model에서 DogList를 전달 받는다. (정제는 해줄필요가 없었다… 억지로 넣었다가 코드가 지저분해져서 뺏다.)

3. searchView?.showDogList(dogList) : Model에서 전달받은 데이터를 View에게 전달한다.

4. searchView?.hideLoading() : 네트워크 통신이 끝났으니 View에 프로그레스바를 숨기도록 요청한다.

  • dropView : View가 제거된 것을 Presenter에 알려준다.

사용자 인터페이스를 처리하는 View를 구현한다.

SearchActivity

class SearchActivity : BaseActivity(), SearchContract.View {

private lateinit var searchPresenter: SearchPresenter

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

searchPresenter.takeView(this)

setButton()

}

private fun setButton() {
getDogListButton.setOnClickListener {
searchPresenter.getDogList()
}
}

@SuppressLint("SetTextI18n")
override fun showDogList(dogList : List<Dog>) {
firstDogText.text = "Name : ${dogList[0].name}, Age : ${dogList[0].age}"
secondDogText.text = "Name : ${dogList[1].name}, Age : ${dogList[1].age}"
thirdDogText.text = "Name : ${dogList[2].name}, Age : ${dogList[2].age}"
}

override fun onDestroy() {
super.onDestroy()
searchPresenter.dropView()
}


override fun initPresenter() {
searchPresenter = SearchPresenter()
}

override fun showError(error: String) {
Toast.makeText(this@SearchActivity, error, Toast.LENGTH_SHORT).show()
}

override fun showLoading() {
searchRefresh.visibility = View.VISIBLE
}

override fun hideLoading() {
searchRefresh.visibility = View.GONE
}

}
  • lateinit var searchPresenter: SearchPresenter : SearchActivity와 1:1 대응하는 SerachPresenter를 연결시켜주기 위한 초기화 지연 변수
  • override fun initPresenter() : BaseActivity에서 Activity가 생성이되면 해당 Activity에 Presenter를 초기화 시켜준다.
  • searchPresenter.takeView(this) : SearchContract.View를 상속받는 Activity가 생성이 되었다는 것을 Presenter에 알려준다.
  • setButton() : 버튼 이벤트가 발생하면 Presenter에 이벤트가 발생하였다고 알려줌과 동시에 Model로 부터 데이터를 가져오라는 것을 알려준다.
  • override fun showDogList(dogList : List<Dog>) : Model로 부터 받은 데이터를 Presenter에서 View로 전달해 주며 TextView를 통해서 사용자에게 보여준다.

MVP패턴을 사용하면 Presenter와 View를 연결시켜주는 부분을 의존성 주입을 활용할 수 있습니다.

- 의존성 주입에 관한 예제는 이곳에서 확인할 수 있습니다.

- 이 글의 MVP 패턴에 예제는 이곳에서 확인하실 수 있습니다.

감사합니다.

--

--