전 포스팅 MVVM 핥아보기(1)에 이어 이번엔 직접 코드를 가져와 좀 더 현장감있게 알아보려 한다. 실제 이번 캠핑앱에 쓰였던 코드 중 일부분을 가져와봤다.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.example.camping.viewModel.DetailViewModel" />
</data>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- 다람쥐들의 낙원인 민들레동산캠핑장에서 귀여운 다람쥐들과 캠핑을! 부분 -->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:background="@color/white"
android:paddingStart="10dp"
android:paddingTop="10dp"
android:paddingEnd="10dp"
android:paddingBottom="10dp">
<!-- 알람 이미지 -->
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_notifications"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- 설명 텍스트 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:layout_marginBottom="30dp"
android:fontFamily="@font/paybooc_b"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:nullCheck="@{viewModel.detail.lineIntro}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</ScrollView>
</layout>
DataBinding을 적용한 xml파일이다. 일반 xml파일이랑 다른점은 최상위 layout이 leaner나 constraint가 아닌 layout이고,
variable과 그 속성으로 name과 type이 있다. 그리고 Textview에 그 name을 사용하는 @{viewModel.detail.lineIntro}가 있다.
1. layout
최상위 layout을 layout으로 감싸줘야 Databinding객체가 생성된다. 이름은 해당 xml이름을 따라가는데 activity_main.xml이면
MainActivityBinding으로 자동생성된다.
val binding: MainActivityBinding = DataBindingUtil.inflate(inflater, R.id.activity_main, container, false)
binding.lifecycleOwner = this
선언한 binding객체로 해당 xml파일의 view들을 읽고 쓸 수 있게된다.
그리고 databinding을 쓰게되면 lifecycleOwner를 줄 수 있게 되는데 이는 해당 activity의 lifecycle을 xml에도 주입하여 activity가 생성되고 파괴될 때 자동으로 관리되어 지다보니 lifecycleOwner를 알고 있는 livedata와 같이쓰게 되면 시너지가 펄떡 뛰게 된다.
2. variable, name, type
Kotlin의 var의 의미가 variable인데, '변할 수 있는' 뜻이다. 그래서 val이랑 다르게 반복 초기화가 가능하다. 이 말을 꺼낸 이유는 xml의 variable도 코드의 변수와 비슷한 느낌이기 때문이다. 한개가 아닌 여러개를 선언하여 xml내에서 원하는 variable을 사용할 수 있다.
name은 변수명, type은 dataType..그냥 똑같다고 보면 될 듯 하다. 보통 type에는 viewModel의 경로를 많이 넣곤 하는데 이 외에도 String이나 ArrayList같은 것들도 들어가니 입맛대로 사용하면 된다.
package com.example.camping.view
class DetailFragment : BaseFragment<FragmentDetailBinding, DetailViewModel>() {
// layout 설정
override val layoutResourceId: Int
get() = R.layout.fragment_detail
private val action: DetailFragmentArgs by navArgs()
private val data: Item
get() = action.data.item
// 전 Fragment에서 넘겨받은 해당 캠핑장 ID
private val contentId: Int
get() = data.contentId
private lateinit var repository: Repository
// Repository 설정
override fun setRepository() {
repository = Repository(Retrofit.Service)
}
// ViewModel 설정
override fun setViewModel() {
viewModel = ViewModelProvider(this, ViewModelFactory(repository))[DetailViewModel::class.java]
}
// DataBinding 설정
override fun viewInitialize() {
binding.viewModel = viewModel
// 서버에서 데이터 가져오기
// !!AtomicBoolean 설명
if (isInit.compareAndSet(true, false))
viewModel.getList()
}
// ViewModel에서 LiveData 관찰하고 뒤로가기 및 업로드 완료 이벤트 시 ProgressBar 사라짐
override fun viewEvent() {
// !!viewLifecycleOwner 설명
viewModel.fragmentCall.observe(viewLifecycleOwner, {
when (it.fragmentEventType) {
FragmentEventType.BACK_STACK -> backStack()
FragmentEventType.SUCCESS_LOAD -> onSuccessLoad()
FragmentEventType.Fail_LOAD -> onFailLoad()
else -> Log.d(ERROR, "viewEvent: Not contain FragmentEventType")
}
})
}
private fun onSuccessLoad() {
// ProgressBar 사라짐
binding.progressBar.visibility = View.GONE
}
private fun onFailLoad() {
with(binding) {
// ProgressBar 사라짐
progressBar.visibility = View.GONE
// 에러 메세지
txtError.visibility = View.Visible
}
}
}
이번 캠핑앱은 navigation을 사용하여 activity가 아닌 fragment를 사용했다. 위 xml쪽 글은 쓰다보니 activity로 수정해 버렸네,,
일반적인 Fragment class와 다르게 onCreateView()와 같은 생명주기 콜백 메서드들이 안보이는데, 이는 BaseFragment라는 부모 클래스를 상속받아서 그렇다. 포스팅에 이런 식으로 상속처리를 많이 하는 글이 보여 내 코딩 스타일에 맞게 바꿔 적용해봤는데 신세계다. 보일러 코드들이 쫙 없어지고 fragment별 집중하는 코드만 나오니 가독성이 더 좋아졌다.
위 코드에선 safeArgs, AtomicBoolean, ViewModel의 이벤트 처리, fragment의 lifecycleowner 등 할말이 너무 많으니 다음글에다 모아 적을 예정이다.
DataBinding과 ViewModel을 사용하기 위해선 위와 같이 binding과 viewModel을 초기화 시켜줘야한다.
viewModel을 초기화하는 법은 상당히 많은데, 그 중 Factory Pattern을 사용한 ViewModelProvider를 사용했다. 이는 일반적인 viewModel의 초기화 방식과는 다르게 생성자를 사용할 수 있어 Repository나 Retrofit같은 객체를 넘겨줄 수 있다.
그리고 MVVM의 특징인 Observer Pattern이 여기서 나온다. 물론 DataBinding에 lifecycleOwner를 주면 observing하는 일은 없지만 UI가 아닌 이벤트를 받을 목적으로는 위 코드와 같이 써야한다.
처음에는 viewModel의 이벤트를 인터페이스를 통한 콜백방식으로 해왔는데, 이 방식이 안좋다고 하여 이벤트도 livedata를 사용하는 식으로 바꿨다. 이유가 오래돼서 기억이 안 나는데, 대충 MVVM의 패턴을 훼방하는? 방식이라고 했던 것 같다.
하긴.. 콜백으로 이벤트 처리하면 MVP패턴이랑 다를게 없다는 생각이 든다.
package com.example.camping.viewModel
class DetailViewModel(private val repository: Repository) : BaseViewModel() {
private val _detail = MutableLiveData<Item>()
val detail: LiveData<Item>
get() = _detail
private fun getList(contentId: String) {
addDisposable(
repository.getList(contentId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(item ->
{
setList(item)
onSuccessLoad()
},
{
onFailLoad()
}
)
)
}
private fun setList(imageList: ArrayList<String>) {
imageList.forEach { image ->
val viewPagerFragment = ViewPagerFragment(null,null, image)
_fragments.add(viewPagerFragment)
}
_detail.postValue(item)
onSuccessLoad()
}
private fun onSuccessLoad() {
_fragmentCall.postValue(
FragmentCall.Builder(FragmentEventType.SUCCESS_LOAD)
.bool(isSelected)
.build()
)
}
private fun onFailLoad() {
_fragmentCall.postValue(
FragmentCall.Builder(FragmentEventType.FAIL_LOAD)
.bool(isSelected)
.build()
)
}
}
해당 viewModel도 BaseViewModel이 있어 onClear가 안 보인다.
fragment에서 구독하는 객체가 바로 livedata이다. 이는 캡슐화를 통해 viewModel외엔 읽기만 가능하게 해놨다. 캡슐화 안 하면 오줌(warning)생김주의. 보다시피 fragment와는 아무런 의존관계가 없어 유닛 테스트하기에 용이하다.
Observe하는 객체들은 꼭 해당 클래스가 파괴되면 구독을 해제해줘야 한다. 만약 구독해제를 안 하게 되면 클래스가 GC에 수거되어야 할 상황에도 활동하는 객체가 남아있어 그 유명한 메모리 누수가 발생하게 된다!
livedata는 view의 lifecycle을 알고 있어 자동으로 구독을 해제한다. 하지만 Rx의 경우 수동으로 구독해제를 해줘야 해서 viewModel의 onClear메서드가 실행될 때 dispose를 시켜주면 된다.
package com.example.camping.data
class Repository(private val service: Service) {
// Retrofit
fun getList(contentId : String): Single<ArrayList<String>> = service.getList(contentId)
}
package com.example.camping.data.retrofit
interface Service {
// 정보 목록 조회
@GET("List")
fun getImageList(@Query("contentId") contentId: String): Single<ArrayList<String>>
}
나름 Model부분이니 DB와 Server를 구현해줬다.
전 포스팅에서 말한것 처럼 viewModel은 DB에서 가져오든 Server에서 가져오든 알 필요 없이 Repository에서 가져오게 하는 방식이 Repository Pattern이다.
MVVM 디자인 패턴외에 다양한 디자인 패턴들(Factory, Observer, Repository, Builder)이 존재한다.
사실 정보처리기사 공부를 하면서 많이 봐왔던 녀석들인데 이딴거 왜 알아야하지 했던 기억이 나네..
이로써 MVVM을 구현하는 방법을 훑어보았당!