MVP의 구조는 다음과 같이 생겼습니다.
Model, View, Presenter 간의 상호 의존성을 떨어트리기 위한 용도임과 동시에 Test Code 작성을 위한 최적의 구조 중 하나입니다.
기존 안드로이드 코드 작성 시에는 View에 모든 코드가 포함되어 있다거나, 중복 코드를 Util 형태로 사용하는 경우도 많았습니다.
그에 비해 MVP는 각각의 독립된 코드의 구현이 가능한 형태입니다.
MVP를 통해서 Test 코드 작성하는 것도 좋고, View를 완전히 분리시킬 수 있다는 점도 좋습니다.
상황에 따라서 Presenter는 Model을 사용할 수도 하지 않을 수도 있지만 기본 형태는 위와 같습니다.
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)
}
}
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()
}
}
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]
}
}
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 패턴 구현해보기 에서 보면 된다.