MVP 패턴 구현해보기

sumi Yoo·2022년 9월 22일
0
post-thumbnail

MVP 구조

MVP의 구조는 다음과 같이 생겼습니다.

Model, View, Presenter 간의 상호 의존성을 떨어트리기 위한 용도임과 동시에 Test Code 작성을 위한 최적의 구조 중 하나입니다.

기존 안드로이드 코드 작성 시에는 View에 모든 코드가 포함되어 있다거나, 중복 코드를 Util 형태로 사용하는 경우도 많았습니다.

그에 비해 MVP는 각각의 독립된 코드의 구현이 가능한 형태입니다.

MVP를 통해서 Test 코드 작성하는 것도 좋고, View를 완전히 분리시킬 수 있다는 점도 좋습니다.

MVP란?

  • Model : Data와 관련된 처리를 담당
    Data의 전반적인 부분을 model에서 담당하고, 네트워크, 로컬 데이터 등을 포함
  • View : 사용자의 실질적인 이벤트가 발생하고, 이를 처리 담당자인 Presenter로 전달
    완전한 View의 형태를 가지도록 설계합니다. 계산을 하거나, 데이터를 가져오는 등의 행위는 Presenter에서 처리하도록
  • Presenter : View에서 전달받은 이벤트를 처리하고, 이를 다시 View에 전달
    View와는 무관한 Data등을 가지고, 이를 가공하고, View에 다시 전달하는 역할

전체적인 흐름

  1. View : View에서 터치 이벤트 발생
  2. View -> Presenter : Presenter에 이벤트 전달
  3. Presenter : 이벤트의 형태에 따라 캐시 데이터를 가져오거나, Model에 요청
  4. Presenter -> Model : Presenter에서 데이터를 요청받음
  5. Model : 데이터를 로컬 또는 서버에서 가져온다
  6. Model -> Presenter : Model로부터 데이터를 통보받는다
  7. Presenter : 전달받은 데이터를 가공
  8. Presenter -> View : 가공한 데이터를 View에 전달
  9. View -> Presenter로 전달받은 데이터를 View에 갱신

상황에 따라서 Presenter는 Model을 사용할 수도 하지 않을 수도 있지만 기본 형태는 위와 같습니다.

구현해보기

  • Model: Plane
  • Presenter: MainPresenter
  • View: MainActivity, CanvasView

MainContract

View와 Presenter에 대한 interface를 정의합니다.

interface MainContract {

    interface View {
        fun setFocusIndex(index: Int?)
        fun setDragBox(box: Box?)
        fun setBoxList(boxList: List<Box>)
        fun updateFocusInfo(box: Box?)
        fun updateBoostCanvas()
    }

    interface Presenter {
        var view: View
        var plane: Plane

        fun addRectangle(type: Type, source: Bitmap? = null)
        fun loadBoxes()
        fun changeBackground()
        fun onFocusRectangle(x: Float, y: Float)
        fun onDragRectangle(x: Float, y: Float)
        fun onDropRectangle(x: Float, y: Float)
        fun changeSeekBar(progress: Int)
    }
}

MainPresenter

class MainPresenter(override var view: MainContract.View, private var factory: BoxFactory) : MainContract.Presenter, UpdateListener {

    override lateinit var plane: Plane

    override fun loadBoxes() {
        view.setBoxList(plane.getBoxList())
        view.updateBoostCanvas()
        view.setFocusIndex(plane.getFocusIndex())
        view.setDragBox(plane.getDragBox())
        val focusIndex = plane.getFocusIndex()
        if (focusIndex != null) {
            view.updateFocusInfo( plane.getBox(focusIndex))
        } else {
            view.updateFocusInfo(null)
        }
    }

    override fun addRectangle(type: Type, source: Bitmap?) {
        plane.addBox(factory.createBox(type, source))
    }

    override fun onFocusRectangle(x: Float, y: Float) {
        val boxList = plane.getBoxList()
        var focusIndex: Int? = null
        for(i in boxList.lastIndex downTo 0) {
            val it = boxList[i]
            if (x.toInt() in (it.point.x..it.point.x + it.size.width) && y.toInt() in (it.point.y..it.point.y + it.size.height)) {
                focusIndex = i
                break
            }
        }
        plane.setFocusIndex(focusIndex)
    }

    override fun onDragRectangle(x: Float, y: Float) {
        val focusIndex = plane.getFocusIndex()
        if (focusIndex != null) {
            val moveRect = plane.getBox(focusIndex).clone()
            moveRect.point = Point(x.toInt() - (moveRect.size.width.div(2)), y.toInt() - (moveRect.size.height.div(2)))
            plane.setDragBox(moveRect)
        }
    }

    override fun onDropRectangle(x: Float, y: Float) {
        if (plane.getDragBox() == null) {
            return
        }
        val focusIndex = plane.getFocusIndex()
        if (focusIndex != null) {
            val moveRect = plane.getBox(focusIndex)
            plane.removeBox(focusIndex)
            moveRect.point = Point(x.toInt() - (moveRect.size.width.div(2)), y.toInt() - (moveRect.size.height.div(2)))
            plane.addBox(moveRect)
            plane.setFocusIndex(plane.getBoxCount() - 1)
            plane.setDragBox(null)
        }
    }

    override fun changeBackground() {
        val background = Color.rgb(Random().nextInt(256), Random().nextInt(256), Random().nextInt(256))
        val focusIndex = plane.getFocusIndex()
        if (focusIndex != null) {
            val focusBox = plane.getBox(focusIndex)
            if (focusBox is Image) return
            val focusRectangle = (focusBox as Rectangle)
            focusRectangle.background = background
            plane.setBox(focusIndex, focusRectangle)
        }
    }

    override fun changeSeekBar(progress: Int) {
        val focusIndex = plane.getFocusIndex()
        if (focusIndex != null) {
            val focusBox = plane.getBox(focusIndex)
            focusBox.alpha = progress.div(10) * 25
            plane.setBox(focusIndex, focusBox)
        }
    }

    override fun onUpdate() {
        loadBoxes()
    }
}

Plane

class Plane(private var listener: UpdateListener) {

    private val boxList = mutableListOf<Box>()
    private var focusIndex: Int? = null
    private var dragBox:Box? = null

    fun addBox(box: Box) {
        boxList.add(box)
        listener.onUpdate()
    }

    fun removeBox(index: Int) {
        boxList.removeAt(index)
    }

    fun setBox(index: Int, box: Box) {
        boxList[index] = box
        listener.onUpdate()
    }

    fun setDragBox(box: Box?) {
        dragBox = box
        listener.onUpdate()
    }

    fun setFocusIndex(index: Int?) {
        focusIndex = index
        listener.onUpdate()
    }

    fun getDragBox() = dragBox

    fun getFocusIndex() = focusIndex

    fun getBoxList() = boxList

    fun getBoxCount(): Int {
        return boxList.size
    }

    fun getBox(index: Int): Box {
        return boxList[index]
    }
}

MainActivity

class MainActivity : AppCompatActivity(), MainContract.View {

    private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
    private lateinit var presenter: MainPresenter
    private lateinit var boostCanvas: BoostCanvasView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
        .
        .
        .
    }

    override fun updateBoostCanvas() {
        boostCanvas.invalidate()
    }

    override fun setFocusIndex(index: Int?) {
        boostCanvas.focusIndex = index
    }

    override fun setDragBox(box: Box?) {
        boostCanvas.dragBox = box
    }

    override fun setBoxList(boxList: List<Box>) {
        boostCanvas.boxList = boxList
    }

    override fun updateFocusInfo(box: Box?) {
        if (box == null) {
            binding.apply {
                btnBackground.text = getString(R.string.empty)
                sbAlpha.progress = 0
            }
            return
        }
        if (box.type == Type.IMAGE) {
            binding.apply {
                btnBackground.text = getString(R.string.empty)
                sbAlpha.progress = (box.alpha.div(25)).times(10)
            }
            return
        }
        (box as Rectangle).background.let {
            val red = Integer.toHexString(it.red)
            val green = Integer.toHexString(it.green)
            val blue = Integer.toHexString(it.blue)
            binding.apply {
                btnBackground.text = String.format(getString(R.string.btn_background), red, green, blue)
                sbAlpha.progress = (box.alpha.div(25)).times(10)
            }
        }
    }
}

MainActivity 안에 CanvasView(커스텀뷰)를 포함하고 있다. CanvasView에서 이벤트 리스너를 받아도 되지만, 모든 이벤트 리스너는 MainActivity 에서만 받을 수 있게 구현했다.

MainActivity 에서 이벤트를 받게되면 Presenter의 함수를 호출하게 되고 이 함수 내에서 Plane의 데이터를 가져와 로직에 맞게 가공하고 필요에 따라 다시 Plane에 데이터를 저장한다.

가공하는 모든 로직은 Presenter에서 하고 Plane은 단순히 데이터의 get과 set만 해주도록 구현했다.

여기서 MVP 패턴과는 별개로 Plane의 속성이 바뀌면 Presenter에게 속성이 바뀌었다고 알려주는 로직에 Observer 패턴을 이용했다.

이 패턴 적용 방법은 Observer 패턴 구현해보기 에서 보면 된다.

My Private Repository
참고자료

0개의 댓글

관련 채용 정보