Android Mvvm 고찰 (1)

장준규·2023년 12월 9일

MVVM 고찰

목록 보기
1/2
post-thumbnail

Mvvm 패턴 목표

Model - View - ViewModel 으로 비즈니스 로직과 프레젠테이션 로직을 ui로 부터 분리하는것을 목표로 한다.
위의 목표가 달성 되었을때, TDD 및 유지보수 등 이점이 생긴다.
권장사항

View

사용자에게 보여지는 부분으로 실질적인 UI 부분이다
ViewModel을 지속적으로 관찰 하여 UI를 갱신한다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.fragments.NewsFragment">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rvNews"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_margin="20dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
 private fun setUpObserver() {
        newsViewModel.imageDocuments.observe(this) { _imageDocuments ->
            (binding.rvNews.adapter as NewsAdapter).apply {
                addItems(_imageDocuments)
            }
        }

        newsViewModel.networkErrorMsg.observe(this) {
            Snackbar.make(_binding!!.root, it, Snackbar.LENGTH_SHORT).show()
        }
    }

ViewModel

View에 노출되는 전반적인 데이터이다.
이벤트를 처리하고 비즈니스 로직을 적용 할때, Model(Repositroy)로 위임하는 역할을 한다.

    private val _imageDocuments = MutableLiveData<List<ImageDocumentDto>>()
    val imageDocuments: LiveData<List<ImageDocumentDto>> get() = _imageDocuments

    fun reqImageSearch(wordSearch: String?) {

        CoroutineScope(Dispatchers.IO).launch {
            when (val result = searchRepository.getImageSearch(page = 1, size = 20, query = wordSearch, sort = null)) {
                is ResponseWrapper.Success -> {
                    result.body.documents?.let { _documents ->
                        _imageDocuments.postValue(_documents)
                    }
                }
                is ResponseWrapper.StatusError -> {
                    _networkErrorMsg.postValue(result.ErrorMeta.errorType + "\n" + result.ErrorMeta.message)
                }
                is ResponseWrapper.ServerError -> {
                    _networkErrorMsg.postValue(result.e.toString())
                }
            }
        }
    }

Model(Repositroy)

DB(Room) 및 api 통신에 대한 비즈니스 로직에 해당한다

class SearchRepository(private val searchService: SearchService = SearchService.service) : NetworkRepository() {

    suspend fun getWebSearch(
        page: Int,
        size: Int,
        sort: String?,
        query: String?
    ) = apiCall {
        searchService.getWebSearch(page = page, size = size, sort = sort, query = query)
    }
}

***

View 구성

Single Activity Architecture 나온 이유

2018년 구글 I/O에서 언급된 Single Activity는
Mutli Activity 기반의 화면구성이 아닌, Single Activity의 Framgnet를 이용한 화면 구성하는 구조로 navigation과 함께 소개되었다.

  • 액티비티를 전환하는데 소모하는 리소스가
    프래그먼트를 전환하는데 소모하는 리소스보다
    상대적으로 크기 때문에 navigatin과 함께 사용된다.
  • Activity 간 데이터 전달을 Intent로 하는 반면에,
    safeArgs로 직관적이며 좀더 편리하게 사용이 가능하다.
  • navigatin graph로 관리되기 때문에 flow를 눈으로 확인 할수 있다.

등 위와 같은 이유로 Single Activity Architecture 를 사용하여 View를 구성한다.


위와 같이 화면을 구성한다고 했을때,

nav_main.xml 으로 구성을 한다.

nav_detail은 상세 화면으로 이동될때 사용된다.

ViewModel 관리

1. ViewModelStore과 ViewModelStoreOwner

open class ViewModelStore {

    private val map = mutableMapOf<String, ViewModel>()
}

ViewModelStore.kt에서 HashMap형태로 ViewModel은 관리가 된다.

interface ViewModelStoreOwner {
    val viewModelStore: ViewModelStore
}

ViewModelStoreOwner.kt 인터페이스를 통해 ViewModeStore가 관리가 되며,
해당 인터페이스는
Activiy : ComponentActivity.kt
Fragment : Framgent.kt
에서 구현하고 있기 때문에, ViewModel 생성시 Owner ( Activity / Fragment ) 따라 ViewModel Scope가 정해진다.

2. SharedViewModel

ViewModel을 인스턴스화할 때는 ViewModelStoreOwner 인터페이스를 구현하는 객체를 전달합니다. 이는 탐색 대상, 탐색 그래프, 활동, 프래그먼트 또는 인터페이스를 구현하는 다른 유형일 수 있습니다. 그러면 ViewModel의 범위가 ViewModelStoreOwner의 수명 주기로 지정됩니다.
참조 : Android Developer ViewModel

위의 문맥을 통해 아래와 같은 의문점이 생겼다.

  • Fragment간 ViewModel을 공유할 수 있지 않을까?
  • navigation Onwer로 넘기게되면, navigation이 종료가되면 ViewModel Scope에 따라 정리가 되지 않을까?



위의 의문점은

탐색 그래프로 범위가 지정된 ViewModel
탐색 그래프도 ViewModel 스토어 소유자입니다. Navigation Fragment 또는 Navigation Compose를 사용하는 경우 navGraphViewModels(graphId) 뷰 확장 함수를 사용하여 탐색 그래프로 범위가 지정된 ViewModel 인스턴스를 가져올 수 있습니다.
참조 : Android Developer ViewModel Scoping Api

해소가 된다.

  1. 탐색 그래프로 범위가 지정이 되어져 있기 때문에,
    News 탭에서, Search / Recenet의 ViewModel을 가지고 올수도 있고,
    아래 코드와 같이 같은 레벨 혹은 이미 생성된 ViewModel을 Provider를 통해 가지고 올 수 있다.
class NewsFragment : Fragment() {
	...
private fun getRecentViewModel() : RecentViewModel{
        return ViewModelProvider(findNavController().getViewModelStoreOwner(R.id.nav_main))[RecentViewModel::class.java]
    }

    private fun getSearchViewModel() : SearchViewModel{
        return  ViewModelProvider(findNavController().getViewModelStoreOwner(R.id.nav_main))[SearchViewModel::class.java]
    }   
    ...
}


  1. nav_detail은 DetailContentFragment, DetailPhotoFragment 로 구성되어져 있다.



Owner가 Fragment로 설정된 경우

//DetailContentFragment.kt
private val detailContentViewModel by lazy {
	ViewModelProvider(this@DetailContentFragment)[DetailContentViewModel::class.java]
}

//DetailPhotoFragment.kt
private val detailPhotoViewModel by lazy {
	ViewModelProvider(this@DetailPhotoFragment)[DetailPhotoViewModel::class.java]
}


Owner가 Fragment로 설정되면 ViewModelScope도 Fragment 생명주기를 따라간다.
이와 같기 때문에, 데이터 유지를 해야되는 경우에는 적절하지 않다.

Owner가 navigation으로 설정된 경우

//DetailContentFragment.kt
private val detailContentViewModel by lazy {
	ViewModelProvider(findNavController().getViewModelStoreOwner(R.id.nav_detail))[DetailContentViewModel::class.java]
}

//DetailPhotoFragment.kt
private val detailPhotoViewModel by lazy {
	ViewModelProvider(findNavController().getViewModelStoreOwner(R.id.nav_detail))[DetailPhotoViewModel::class.java]
}



위와 같이 nav_detail을 Owner로 설정하면,
해당 navigation이 종료시 ViewModelScope가 종료되므로 메모리 해제됨을 Profile을 통하여 확인할수 있다.
이와 같기 때문에, 데이터 유지를 해야되는경우 적절하다고 생각한다.

profile
안드로이드 개발자 입니다

0개의 댓글