지금까지는 프로그래밍 과정을 단순화하여 보여드리기 위해 Activity에서 생성한 한 개의 ViewModel을 모든 프래그먼트가 공유하는 n:1 형태로 앱을 만들었습니다.
MVVM에서 View와 ViewModel은 n:1 구조를 갖기 때문에 앱의 작동에는 문제가 없습니다만 관심사의 분리라는 관점에서 보면 바람직한 구조는 아닙니다.
사용하는 로직이나 변수가 겹치지 않는다면 View별로 ViewModel을 분리하여 사용하는 것이 적절한데요, Hilt를 도입하면서 의존성 주입이 간편해졌기 때문에 여기서는 View별로 ViewModel을 분리해 보도록 하겠습니다.
우선은 각 화면별로 ViewModel을 분리 작성하고 의존성으로 주입할 수 있게 @HiltViewModel을 붙여줍니다.
@HiltViewModel
class BookViewModel @Inject constructor(
) : ViewModel() { }
@HiltViewModel
class FavoriteViewModel @Inject constructor(
) : ViewModel() { }
@HiltViewModel
class SearchViewModel @Inject constructor(
) : ViewModel() { }
@HiltViewModel
class SettingsViewModel @Inject constructor(
) : ViewModel() { }
이제 각 Fragment에서는 by activityViewModels() 대신 by viewModels()로 ViewModel을 각각 초기화하도록 합니다. MainActivity의 bookSearchViewModel은 더 이상 필요없으니 삭제합니다.
class MainActivity : AppCompatActivity() {
- lateinit var bookSearchViewModel: BookSearchViewModel
그리고나면 Fragment에서 로직이 없는 부분이 빨간색으로 강조 표시되기 때문에 BookSearchViewModel에서 필요한 내용을 가져와 붙여넣습니다.
@HiltViewModel
class BookViewModel @Inject constructor(
private val bookSearchRepository: BookSearchRepository,
) : ViewModel() {
// Room
fun saveBook(book: Book) = viewModelScope.launch {
bookSearchRepository.insertBook(book)
}
}
@HiltViewModel
class FavoriteViewModel @Inject constructor(
private val bookSearchRepository: BookSearchRepository,
) : ViewModel() {
// Paging
val favoritePagingBooks: StateFlow<PagingData<Book>> =
bookSearchRepository.getFavoritePagingBooks()
.cachedIn(viewModelScope)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), PagingData.empty())
// Room
fun saveBook(book: Book) = viewModelScope.launch {
bookSearchRepository.insertBook(book)
}
fun deleteBook(book: Book) = viewModelScope.launch {
bookSearchRepository.deleteBook(book)
}
}
@HiltViewModel
class SearchViewModel @Inject constructor(
private val bookSearchRepository: BookSearchRepository,
private val savedStateHandle: SavedStateHandle,
) : ViewModel() {
private val _searchPagingResult = MutableStateFlow<PagingData<Book>>(PagingData.empty())
val searchPagingResult: StateFlow<PagingData<Book>> = _searchPagingResult.asStateFlow()
suspend fun searchBooksPaging(query: String) {
bookSearchRepository.searchBooksPaging(query, getSortMode())
.cachedIn(viewModelScope)
.collect {
_searchPagingResult.value = it
}
}
private suspend fun getSortMode() = withContext(Dispatchers.IO) {
bookSearchRepository.getSortMode().first()
}
// SavedState
var query = String()
set(value) {
field = value
savedStateHandle.set(SAVE_STATE_KEY, value)
}
init {
query = savedStateHandle.get<String>(SAVE_STATE_KEY) ?: ""
}
companion object {
private const val SAVE_STATE_KEY = "query"
}
}
@HiltViewModel
class SettingsViewModel @Inject constructor(
private val bookSearchRepository: BookSearchRepository,
private val workManager: WorkManager,
) : ViewModel() {
// DataStore
fun saveSortMode(value: String) = viewModelScope.launch {
bookSearchRepository.saveSortMode(value)
}
suspend fun getSortMode() = withContext(Dispatchers.IO) {
bookSearchRepository.getSortMode().first()
}
fun saveCacheDeleteMode(value: Boolean) = viewModelScope.launch {
bookSearchRepository.saveCacheDeleteMode(value)
}
suspend fun getCacheDeleteMode() = withContext(Dispatchers.IO) {
bookSearchRepository.getCacheDeleteMode().first()
}
// WorkManager
fun setWork() {
val constraints = Constraints.Builder()
.setRequiresCharging(true)
.setRequiresBatteryNotLow(true)
.build()
val workRequest = PeriodicWorkRequestBuilder<CacheDeleteWorker>(15, TimeUnit.MINUTES)
.setConstraints(constraints)
.build()
workManager.enqueueUniquePeriodicWork(
WORKER_KEY, ExistingPeriodicWorkPolicy.REPLACE, workRequest
)
}
fun deleteWork() = workManager.cancelUniqueWork(WORKER_KEY)
fun getWorkStatus(): LiveData<MutableList<WorkInfo>> =
workManager.getWorkInfosForUniqueWorkLiveData(WORKER_KEY)
companion object {
private const val WORKER_KEY = "cache_worker"
}
}
ViewModel 분리를 완료했습니다. 우선 BookFragment에서는 saveBook이 필요하기 때문에 복사를 했고, 복사과정에서 Repository가 필요하기 때문에 생성자로 받도록 했습니다.
그리고 FavoriteFragment에서는 Room을 다루는 부분이 필요하기 때문에 그 부분과 페이징으로 표시하는 부분을 추가하였구요.
SearchFragment에서는 검색을 수행하기 위해서 Paging 파트가 필요하고, query를 저장하기 위해서 SavedState 파트가 필요합니다. 그리고 Paging에서 SortMode가 필요하기 때문에 getSortMode를 붙여넣기 합니다. 의존객체로는 Repository와 savedStateHandle을 주입해 줍니다.
SettingsViewModel에서는 sortMode와 deleteMode를 다루어야 하기 때문에 DataStore 관련된 내용이랑 WorkManager 관련된 내용을 옮겨넣으면 됩니다. 의존객체로는 Repository와 workManager를 전달하면 되겠죠.
이렇게 함으로써 각 Fragment에 필요한 로직만이 모여지는 형태로 ViewModel이 분리되면서 각 ViewModel의 내용이 간결해지고 더 읽기 쉬워졌습니다. 그러면 BookSearchViewModel은 더 이상 필요없기 때문에 주석처리를 해 줍니다.
이렇게 해서 Hilt를 통해 관심사에 따라 ViewModel을 분리해 보았습니다.