본문 바로가기

코딩/안드로이드

Android Kotlin MVVM패턴으로 간단한 검색 앱 만들기 - 1. BaseView, BaseViewModel을 작성하여 MVVM의 토대 만들기

MVVM 패턴과 Kotlin으로 간단한 앱을 만드는 것에 대해 글을 써보려고 합니다.


저도 정리를 좀 하고, 다른 분들도 도움이 좀 되셨으면 하는 의미에서..




일단 어떤 앱을 만들고자 하냐면,


간단하게 카카오 Api로 이미지 검색을 해서 그 내용을 화면에 리사이클러뷰로 그리드하게 쭉 뿌려주는 앱을 샘플로 만들어 볼 예정입니다.




이렇게요!





그 과정에서


MVVM패턴을 위한 Livedata,


통신을 위한 Retrofit + RxJava,


이미지를 뿌려주기 위한 Picasso


의존성주입을 위한 Koin,


그리고 com.android.support를 대채하는 AndroidX


를 활용해 볼 예정입니다.



AndroidX에 관해서는 

https://thdev.tech/google%20io/2018/05/12/Android-New-Package-AndroidX/

이걸 한번 읽어보셔도 좋을듯합니다



기본적으로 모든 소스 코드는 


https://github.com/5seunghoon/Kotlin-MVVM-Sample


에 있습니다.





그럼 그 첫번째로 BaseActivity와 BaseViewModel을 만들며 MVVM의 틀을 짜보도록 하겠슴다




자.. 먼저, MVVM이란?



MVP패턴에서 Presenter와 VIew, 그리고 Model간의 양방향 의존성이 너무 깊어서, 그 점을 개선한 패턴입니다.


View는 ViewModel의 참조를 가지고 있지만, ViewModel은 View의 참조를 가지고 있지 않고,

ViewModel도 Model의 참조를 가지고 있지만, Model은 ViewModel의 참조를 가지고 있지 않습니다.


그럼  ViewModel은 StartActivity나 Snackbar 등, 어떻게 View의 함수를 호출할 수 있을까요?


바로 View가 ViewModel을 Binding하고 있으면 됩니다!


그럼 ViewModel은 단순히 값을 바꾸기만 하고, View는 그 값이 바뀌는걸 관찰하면 되죠.



https://kaidroid.me/post/android-mvvm-viewmodel-livedata-databinding/


자세한 설명은 링크로 대채하겠슴다 (한국어)


여튼 MVVM으로 구현을 하면 단방향 의존만 가지게 되어 아주 바람직한 개발을 할 수 있게 됩니다.





그럼, BaseActivity와 BaseViewModel을 어떻게 활용하여 MVVM을 구현하는지에 대해 적어보겠습니다.






Gradle 설정


먼저 Gradle을 쭉 설정 해보죠


일단 AndroidX로 MIGRATION을 합시다


어떻게요?



안드로이드 스튜디오에서 이걸 눌려주면 끝납니다! (안드로이드 스튜디오 3.2 이상)



그러면 build.gradle에 있는 com.support.android..가 

implementation 'androidx.appcompat:appcompat:1.1.0-alpha01'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha3'

이런식으로 바뀌게 되죠!


결과적으로




dependencies {
// kotlin
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

// androidx
implementation 'androidx.appcompat:appcompat:1.1.0-alpha01'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha3'
implementation 'androidx.core:core-ktx:1.1.0-alpha03'
implementation "androidx.lifecycle:lifecycle-extensions:2.1.0-alpha01"
implementation "androidx.cardview:cardview:1.0.0"
implementation "androidx.recyclerview:recyclerview:1.1.0-alpha01"
implementation "androidx.legacy:legacy-support-v4:1.0.0"
implementation "com.google.android.material:material:1.1.0-alpha02"

// rxjava
implementation "io.reactivex.rxjava2:rxjava:2.2.0"
implementation "io.reactivex.rxjava2:rxandroid:2.0.2"

// koin
implementation "org.koin:koin-androidx-scope:1.0.2"
implementation "org.koin:koin-androidx-viewmodel:1.0.2"

// picasso
implementation "com.squareup.picasso:picasso:2.5.2"

// retrofit
implementation "com.squareup.retrofit2:retrofit:2.5.0"
implementation "com.squareup.retrofit2:converter-gson:2.5.0"
implementation "com.squareup.retrofit2:adapter-rxjava2:2.5.0"
implementation "com.squareup.retrofit2:retrofit-mock:2.4.0"


.

.
.


}


이렇게 하면 됩니다.


RxJava, Koin, Picasso, Retrofit에 관한 설명은 그걸 쓸때 차차 하도록 하겠습니다 ㅎ



또 기본적으로 DataBinding을 쓰기 때문에


app.build.gradle에서


android {
compileSdkVersion 28
defaultConfig {
.
.
.
}
buildTypes {
release {
.
.
.
}
}
dataBinding {
enabled = true
}
}

이렇게 데이터바인딩 true를 해줘야 합니다.






BaseViewModel



자, 그럼 본격적으로 MVVM패턴을 만들어 봅시다.


우리가 할 일은 액티비티(뷰)가 참조할 뷰모델을 만들고, 그러한 뷰모델을 뷰가 참조하고, 데이터 바인딩을 수행하게 하는 것입니다.


먼저 뷰모델이 상속 받을 BaseViewModel을 만들어 보죠



open class BaseKotlinViewModel : ViewModel() {

/**
* RxJava 의 observing을 위한 부분.
* addDisposable을 이용하여 추가하기만 하면 된다
*/
private val compositeDisposable = CompositeDisposable()

fun addDisposable(disposable: Disposable) {
compositeDisposable.add(disposable)
}

override fun onCleared() {
compositeDisposable.clear()
super.onCleared()
}
}


기본적으로 뷰모델은 android.lifecycle.ViewModel을 상속받으면 끝입니다만, 


그 외에 할 일이 많기 때문에 android.lifecycle.ViewModel을 상속 받는 BaseViewModel(여기서는 BaseKotlinViewModel)을 만들겠슴다


다른 뷰모델은 이제 이 베이스뷰모델을 상속받게 됩니다.



차근차근 설명해 보겠습니다.





/**
* RxJava 의 observing을 위한 부분.
* addDisposable을 이용하여 추가하기만 하면 된다
*/
private val compositeDisposable = CompositeDisposable()

fun addDisposable(disposable: Disposable) {
compositeDisposable.add(disposable)
}

override fun onCleared() {
compositeDisposable.clear()
super.onCleared()
}


이 부분을 먼저 설명해 보겠습니다.


Model에서 들어오는 Single<>과 같은 RxJava 객체들의 Observing을 위한 부분입니다.


기본적으로 RxJava의 Observable들은 compositeDisposable에 추가를 해주고, 뷰모델이 없어질 때 추가했던 것들을 지워줘야합니다.


이 부분은 그러한 동작을 수행하는 코드로서, Observable들을 옵저빙할때 addDisposable()을 쓰게 됩니다.


또한 ViewModel은 View와의 생명주기를 공유하기 때문에 View가 부서질 때 ViewModel의 onCleared()가 호출되게 되며, 그에 따라 옵저버블들이 전부 클리어 되게 됩니다.



대략 어떻게 사용하냐면


addDisposable(model.requestToServer(senderInfo)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
// 성공적인 응답
}, {
// 에러
}))


와 같이 씁니다.


자세한것은 RxJava편에서 설명하였습니다 : https://deque.tistory.com/110?category=984011






BaseView



이제 BaseView(여기서는 BaseKotlinActivity)를 만들어 보겠슴다


/**
* BaseKotlinActivity<ActivitySbsMainBinding>
* 와 같이 상속 받을 때, ActivitySbsMainBinding 과 같은 파일이 자동생성되지 않는다면
* 1. 해당 엑티비티의 레이아웃이 <layout></layout> 으로 감싸져 있는지 확인
* 2. 다시 빌드 수행 or 클린 빌드 후 다시 빌드 수행
* 3. 이름 확인 : sbs_main_activity => ActivitySbsMainBinding
*/
abstract class BaseKotlinActivity<T : ViewDataBinding, R : BaseKotlinViewModel> : AppCompatActivity() {

lateinit var viewDataBinding: T

/**
* setContentView로 호출할 Layout의 리소스 Id.
* ex) R.layout.activity_sbs_main
*/
abstract val layoutResourceId: Int

/**
* viewModel 로 쓰일 변수.
*/
abstract val viewModel: R

/**
* 레이아웃을 띄운 직후 호출.
* 뷰나 액티비티의 속성 등을 초기화.
* ex) 리사이클러뷰, 툴바, 드로어뷰..
*/
abstract fun initStartView()

/**
* 두번째로 호출.
* 데이터 바인딩 및 rxjava 설정.
* ex) rxjava observe, databinding observe..
*/
abstract fun initDataBinding()

/**
* 바인딩 이후에 할 일을 여기에 구현.
* 그 외에 설정할 것이 있으면 이곳에서 설정.
* 클릭 리스너도 이곳에서 설정.
*/
abstract fun initAfterBinding()

private var isSetBackButtonValid = false

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

viewDataBinding = DataBindingUtil.setContentView(this, layoutResourceId)

initStartView()
initDataBinding()
initAfterBinding()
}


}


모든 액티비티는 이 베이스 액티비티를 구현하게 됩니다.


일단 먼저


lateinit var viewDataBinding: T

이 부분에 관해 설명하겠습니다.


lateinit는 나중에 초기화 하겠다.. 라는 것이죠


나중에 뭐를? ViewDataBinding을!


ViewDataBinding은 액티비티의 layout을 빌드하면 자동 생성되는 클래스입니다.


근데 그냥 빌드하면 바로 생성되는건 아니고, layout에 간단한 한가지 작업이 필요합니다.


주석에도 나와있듯이, 


<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".view.MainActivity">
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>



레이아웃이 이런식으로 되어있으면 됩니다.


일반적인 레이아웃을 만들되, 그 레이아웃 전체를 <layout></layout>으로 감싸면 되죠


그리고 빌드를 하면 ViewDataBinding클래스가 자동생성됩니다.


자동생성될때의 네이밍규칙도 간단합니다.


activity_main.xml 이라면 MainActivityBinding 으로 자동생성됩니다.




자 두번째, 

/**
* setContentView로 호출할 Layout의 리소스 Id.
* ex) R.layout.activity_main
*/
abstract val layoutResourceId: Int

이 부분에 관해 설명하겠습니다.


onCreate에서 레이아웃 리소스를 설정해줘야하는데, BaseKotlinActivity에서 onCreate를 override해버리니 


BaseKotlinActivity를 구현한 액티비티는 레이아웃 리소스를 설정해 줄대가 없죠?


그렇다고 그걸 위해 함수 하나 만들어서 인자로 넣어주게 하자니 코틀린스럽지가 않고..


그래서 abstract 변수를 만들어주고, BaseKotlinAcitivy를 구현하는 액티비티에서는


override val layoutResourceId: Int
get() = R.layout.activity_main

이런식으로, 코틀린스럽게 코딩할 수 있게 하였습니다.



그리고 BaseKotlinActivity에서는

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

viewDataBinding = DataBindingUtil.setContentView(this, layoutResourceId)

.

.



이렇게, 레이아웃 리소스를 설정해주게 됩니다.


일반적인 setContentView와는 약간 다른 모습인데, 데이터바인딩을 할 때는 저런식으로 하면 됩니다.





세번째로 뷰모델 변수입니다.

/**
* viewModel 로 쓰일 변수.
*/
abstract val viewModel: R

액티비티가 BaseKotlinActivity를 구현할 때, ViewDataBinding클래스 뿐만 아니라 뷰모델 클래스도 제네릭으로 주게 됩니다.


그냥 액티비티에서 뷰모델을 선언하면 되는거아니냐! 라고 할 수 있는데, 굳이 이렇게 베이스 뷰에서도 뷰모델을 참조 할 수 있게 한 이유는 스낵바 옵저빙을 위해서입니다.


이 부분은 차차 설명을 하겠습니다.




마지막으로 액티비티에서 구현해줄 세가지 함수인


initStartView()
initDataBinding()
initAfterBinding()

이 남았는데,


이 부분은 그냥 주석을 보시면 이해가 되실겁니다.


그냥 onCreate에서 수행할 함수를 abstract하게 만들어서 넣어둔건데, 세개나 만들기 귀찮으신 분들은 그냥 하나만 만들어도 충분합니다.




추가로 


private fun snackbarObserving() {
viewModel.observeSnackbarMessage(this) {
Snackbar.make(findViewById(android.R.id.content), it, Snackbar.LENGTH_LONG).show()
}
viewModel.observeSnackbarMessageStr(this){
Snackbar.make(findViewById(android.R.id.content), it, Snackbar.LENGTH_LONG).show()
}
}

이 부분이 남았는데, 이 부분 역시 스낵바와 관련된거라, 마지막에 글을 한번 쓰겠습니다.




일단 여기까지 베이스 뷰, 베이스 뷰모델을 만들었는데, 이제 이걸 상속해서 메인 액티비티와 메인 뷰 모델을 만들어 보겠습니다.






MainActivity

class MainActivity : BaseKotlinActivity<ActivityMainBinding, MainViewModel>() {
override val layoutResourceId: Int
get() = R.layout.activity_main
override val viewModel: MainViewModel by viewModel()

override fun initStartView() {

}

override fun initDataBinding() {

}

override fun initAfterBinding() {

}

}


할게 없습니다.


제네릭으로 ViewDataBinding과 ViewModel을 줍니다.


그리고 layoutResourceId를 위에서 처럼 '코틀린스럽게' 설정 해주고,


뷰 모델을 설정해 줍니다....인데!


by viewModel()


이게 뭐죠?


라고 하시는 분을 위해 다음 게시물에 Koin 관련하여 작성하였으니 그것을 참고해주시기 바랍니다.(https://deque.tistory.com/109?category=984011)


Koin을 통해 DI를 수행하기 귀찮으신분들은 그냥 

override val viewModel: MainViewModel = MainViewModel()

이렇게 해주셔도 됩니다.




MainViewModel


이제 뷰모델을 만들어보겠습니다....는 할게 없습니다.

class MainViewModel(private val model:DataModel): BaseKotlinViewModel() {

}


끝!


추가로, 뷰모델을 생성할 때 파라미터로 model을 줍니다.


모델은 아~주 간단하게 이렇게 되어있습니다.


interface DataModel {
fun getData()
}

class DataModelImpl: DataModel{
override fun getData() {
return
}
}





다음장엔 의존성주입(DI)를 수행하는 Koin라이브러리에 대해 알아보겠습니다.