[Android] MVVM 개봉하기(2)

양현진·2022년 3월 3일
0

Oh My Android

목록 보기
8/22
post-thumbnail

전 포스팅 MVVM 핥아보기(1)에 이어 이번엔 직접 코드를 가져와 좀 더 현장감있게 알아보려 한다. 실제 이번 캠핑앱에 쓰였던 코드 중 일부분을 가져와봤다.

View (fragment.xml)

<?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같은 것들도 들어가니 입맛대로 사용하면 된다.

View (fragment.kt, BaseFragment 상속)


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패턴이랑 다를게 없다는 생각이 든다.

ViewModel


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를 시켜주면 된다.

Model (Repository)


package com.example.camping.data

class Repository(private val service: Service) {

    // Retrofit
    fun getList(contentId : String): Single<ArrayList<String>> = service.getList(contentId)

}

Model (Remote - Retrofit)


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을 구현하는 방법을 훑어보았당!

profile
Android Developer

0개의 댓글