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 패턴에 예제는 이곳에서 확인하실 수 있습니다.
감사합니다.