Model - View - ViewModel 으로 비즈니스 로직과 프레젠테이션 로직을 ui로 부터 분리하는것을 목표로 한다.
위의 목표가 달성 되었을때, TDD 및 유지보수 등 이점이 생긴다.
권장사항
사용자에게 보여지는 부분으로 실질적인 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()
}
}
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())
}
}
}
}
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)
}
}
2018년 구글 I/O에서 언급된 Single Activity는
Mutli Activity 기반의 화면구성이 아닌, Single Activity의 Framgnet를 이용한 화면 구성하는 구조로 navigation과 함께 소개되었다.
등 위와 같은 이유로 Single Activity Architecture 를 사용하여 View를 구성한다.

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

nav_main.xml 으로 구성을 한다.
nav_detail은 상세 화면으로 이동될때 사용된다.
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가 정해진다.

ViewModel을 인스턴스화할 때는 ViewModelStoreOwner 인터페이스를 구현하는 객체를 전달합니다. 이는 탐색 대상, 탐색 그래프, 활동, 프래그먼트 또는 인터페이스를 구현하는 다른 유형일 수 있습니다. 그러면 ViewModel의 범위가 ViewModelStoreOwner의 수명 주기로 지정됩니다.
참조 : Android Developer ViewModel
위의 문맥을 통해 아래와 같은 의문점이 생겼다.
탐색 그래프로 범위가 지정된 ViewModel
탐색 그래프도 ViewModel 스토어 소유자입니다. Navigation Fragment 또는 Navigation Compose를 사용하는 경우 navGraphViewModels(graphId) 뷰 확장 함수를 사용하여 탐색 그래프로 범위가 지정된 ViewModel 인스턴스를 가져올 수 있습니다.
참조 : Android Developer ViewModel Scoping Api
해소가 된다.
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]
}
...
}

//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 생명주기를 따라간다.
이와 같기 때문에, 데이터 유지를 해야되는 경우에는 적절하지 않다.
//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을 통하여 확인할수 있다.
이와 같기 때문에, 데이터 유지를 해야되는경우 적절하다고 생각한다.