안드로이드 MVP 패턴 적용하기

안드로이드 개발 패턴들의 기본적인 개념은 어느 정도 알게 되었지만, 실전에 그것을 적용하기는 쉽지 않았다. 느낌을 말하자면 뷰와 프레젠터의 인터페이스를 만들고 서로 핑퐁하는 모양새랄까. MVP 중에서도 방법이 여러가지이기도 하고 아무튼 장벽이 좀 있었다. 하지만 구글과 스승님의 도움을 받아 이번 프로젝트에서 문자 메세지를 다루는 부분에 MVP 패턴(구글 아키텍처를 따름)을 사용할 수 있었고, 그 부분을 공유하고자 한다. 코드는 코틀린으로 작성되어 있다.

SMS와 MMS 메세지를 읽고 저장하는 것에 대한 내용은 아래 링크를 참조하시라.

안드로이드 SMS와 MMS 읽어 저장하기 (1)

안드로이드 SMS와 MMS 읽어 저장하기 (2)

그리고 글에 들어가기에 앞서 안드로이드 MVP 패턴을 적용하기 위해 참고한 링크들을 첨부한다.

Android Testing Codelab Android MVP 무작정 따라하기

본인이 진행하고 있는 프로젝트의 폴더 구조는 다음과 같다.

패키지가 여러 개 있지만 이번 MVP 패턴 적용하기에서 다룰 것은 message 패키지에 있는 파일들 중 MessageContract, MessagePresenter와, MainActivity, BasePresenter, BaseView 파일로 모델(데이터 관련 로직만 있음)보다는 뷰와 프레젠터에 중점을 둔다. 먼저 Base 프레젠터와 뷰에 대해 다뤄보자.

1. BasePresenter.kt, BaseView.kt

이름에서도 짐작할 수 있듯, 두 파일은 각각 프레젠터와 뷰의 기본적인 구조를 가지는 인터페이스이다. 코드는 다음과 같다.

interface BasePresenter {
    fun start()
}
interface BaseView<T> {
    var presenter: T
}

이보다 코드가 간단할 수는 없을 것이다. 곧 알게 되겠지만 이 둘은 모든 프레젠터의 인터페이스와 모든 뷰의 인터페이스의 인터페이스이기 때문이다. 인터페이스의 인터페이스라니 이 얼마나 추상적인가

2. MessageContract.kt

사용할 뷰와 프레젠터의 인터페이스를 정의한 Contract 클래스이다. 각각 1에서 만들어둔 베이스 뷰와 베이스 프레젠터를 상속받고, 필요한 메소드를 정의한다.

interface MessageContract {
    interface View : BaseView<Presenter> {
        fun showPermissionMessage(permissionListener: PermissionListener)

        fun showToastMessage(string: String)

        fun finishActivity()
    }

    interface Presenter : BasePresenter {
        fun readAndSaveMessageList()

        fun performTaskOrFinishByPermission()

        fun cancelMessageAsyncTask()
    }
}

3. MessagePresenter.kt

해당 프레젠터는 안드로이드 메세지 퍼미션을 체크하여 뷰에서 사용자가 퍼미션을 수락할 때, 모델이 데이터베이스 동기화를 할 수 있도록 만든다.

class MessagePresenter(
        private val context: Context,
        private val messagePermissionView: MessageContract.View,
        private val progressbar: ProgressBar) : MessageContract.Presenter {
    private lateinit var smsReader: SmsReader // SMS를 읽어 저장하는 클래스의 객체
    private lateinit var mmsReader: MmsReader // MMS를 읽어 저장하는 클래스의 객체
    private lateinit var recordManager: RecordDBManager // 메세지의 카테고리를 저장하는 클래스의 객체
    init {
        messagePermissionView.presenter = this // 콜백 구현을 위해 뷰 프레젠터에 현재 프레젠터 할당
    }

    override fun start() {
        performTaskOrFinishByPermission()
    }

    override fun performTaskOrFinishByPermission() {
        val permissionListener = object : PermissionListener {
            override fun onPermissionGranted() { // 퍼미션 허용 시 AsyncTask 실행
                MessageAsyncTask().execute()
            }

            override fun onPermissionDenied(deniedPermissions: ArrayList<String>) {
                messagePermissionView.finishActivity() // 비허용시 뷰에서 액티비티 종료
            }
        }
        messagePermissionView.showPermissionMessage(permissionListener)
    }

    override fun cancelMessageAsyncTask() {
        MessageAsyncTask().cancel(true)
    }

    override fun readAndSaveMessageList() {
        smsReader.gatherMessages(context)
        mmsReader.gatherMessages(context)
    }

    @SuppressLint("StaticFieldLeak") // static이 아니라 누수가 발생하는 문제로 아래에 해결방안 첨부
    inner class MessageAsyncTask : AsyncTask<Unit, Unit, Unit>() { // 백그라운드에서 메세지 내역 저장을 수행
        override fun onPreExecute() {
            super.onPreExecute()
            messagePermissionView
                    .showToastMessage(context.getString(R.string.start_message_synchronization))
            progressbar.visibility = View.VISIBLE
        }

        override fun doInBackground(vararg p0: Unit?) {
            var realm: Realm? = null
            publishProgress()
            Thread.sleep(2000)
            try {
                realm = Realm.getDefaultInstance()
                smsReader = SmsReader(realm)
                mmsReader = MmsReader(realm)
                recordManager = RecordDBManager(realm)
                readAndSaveMessageList()
                recordManager.categorizeMessages(context)
            } finally {
                if (realm != null) {
                    realm.close()
                }
            }
        }

        override fun onPostExecute(result: Unit?) { // 동기화 완료 시 토스트 메세지 뷰에서 출력
            super.onPostExecute(result)
            messagePermissionView
                    .showToastMessage(context.getString(R.string.end_message_synchronization))
            progressbar.visibility = View.INVISIBLE
        }
    }
}

4. MainActivity.kt

뷰를 담당하는 액티비티이다. MessageContract.View를 상속 받아 함수를 구현하고 있으며, MessageContract.Presenter를 참조해 뷰에서 발생하는 이벤트를 전달한다. 아래는 불필요한 내용을 제거한 코드이다.

class MainActivity : AppCompatActivity(), MessageContract.View {
    override lateinit var presenter: MessageContract.Presenter
    ...

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        ...
        initialize()
        presenter.start()
        ...
    }

    ...

    override fun showToastMessage(string: String) { // 토스트 메세지를 띄우는 함수
        Toast.makeText(this, string, Toast.LENGTH_SHORT).show()
    }

    override fun finishActivity() {
        finish()
    }

    override fun onPause() {
        super.onPause()
        presenter.cancelMessageAsyncTask()// 뷰가 종료되었을 시 프레젠터에게 알림
    }

    private fun initialize() {
        Realm.init(this)
        progressbar = main_message_prograssbar // AsyncTask의 onPreExecute에서 보여줄 프로그래스바
        presenter = MessagePresenter(applicationContext, this, progressbar) // 프레젠터 오브젝트 생성
        ...
    }

    override fun showPermissionMessage(permissionListener: PermissionListener) { // 퍼미션을 수락 및 거절하는 이벤트 오버라이드
        TedPermission.with(this)
                .setPermissionListener(permissionListener)
                .setRationaleTitle(getString(R.string.read_sms_request_title))
                .setRationaleMessage(getString(R.string.read_sms_request_detail))
                .setDeniedTitle(getString(R.string.denied_read_sms_title))
                .setDeniedMessage(getString(R.string.denied_read_sms_detail))
                .setGotoSettingButtonText(getString(R.string.move_setting))
                .setPermissions(android.Manifest.permission.READ_SMS)
                .check()
    }

    ...
}

이상으로 MVP 패턴의 구현에 대해 알아보았다. 분명 부족한 부분도 있을 것이지만, 실제로 구현을 해보니 이론상으로 알던 것보다 더 잘 이해된다. MVP나 MMVM를 배우고자 하는 분들께는 꼭 해당 패턴을 적용해보는 것을 추천한다.