뷰모델을 쓰다보면 파라미터가 필요한경우가 있다
asset폴더에 접근해서 데이터를 불러오려면 context가 필요한데
viewmodel에서 직접 context를 불러오면 View의 UI와 직접적으로 연결되기때문에 MVVM패턴이 깨지게 되고
그렇다고 View에서 직접 데이터를 불러오면 데이터를 받아오는 로직이 UI에서 실행되기때문에 MVVM패턴이 깨지게 된다
그외에 비즈니스로직을 수행할때 데이터를이 변하고 리턴값을 받아야하는데 그걸 뷰모델에서 수행하게 되면 뷰모델의 값이 직접적으로 UI에 적용되기 때문에 MVVM패턴이 깨진다
그걸 해결하기 위해서는 데이터를 변경하는 로직을 수행할 다른 클래스가 필요했고
그 클래스들을 뷰모델에 파라미터로 전달하기 위해서 뷰모델 팩토리가 필요했다
class SignUpViewModelFactory(private val userRepository: UserRepository, private val useCase: SignUpUseCase) : ViewModelProvider.Factory{
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return SignUpViewModel(userRepository, useCase) as T
throw IllegalArgumentException("unKnown ViewModel class")
}
}
기본적이 구성은 이렇게 되어있다
전달할 Repository 클래스와 UseCase클래스를 뷰모델에 파라미터로 전달해준다
그러면 액티비티에서는
private val userRepository by lazy {
UserRepository()
}
private val signUpUseCase by lazy {
SignUpUseCase()
}
private val viewModel by lazy {
ViewModelProvider(
this@SignUpActivity,
SignUpViewModelFactory(userRepository, signUpUseCase)
)[SignUpViewModel::class.java]
}
이렇게 Repo와 UC를 팩토리에넣으면서 뷰모델을 불러주고
뷰모델은
class SignUpViewModel(private val userRepository: UserRepository, private val useCase: SignUpUseCase): ViewModel() {
이런식으로 선언해서 받아주면 Repository와 UseCase의 로직들을 수행시키고 그값을 갱신할 수 있게된다
Repository는 데이터의 접근을 담당한다
이때 데이터는 로컬 DB, 웹 서버 등 어디에서 오든 상관 없다
Repository는 이런 데이터의 출처를 숨기고, 일관된 데이터 접근을 제공한다
class PostRepository {
var totalPost: MutableList<Post> = mutableListOf()
var detailPost: Post? = null
fun setList(){
totalPost = PostData.totalPost
}
/**
* TODO 데이터에서 특정 포스트 제거
*
* @param position 포스트 위치
*/
fun deletePost(position: Int) {
PostData.totalPost.removeAt(position)
totalPost = PostData.totalPost
}
}
이번에 개인적인 과제를 하면서 만든 Repository인데 데이터를 직접적으로 관여하고 갱신하게 된다
일반적으로 Repository는 데이터의 CRUD(Create, Read, Update, Delete) 작업을 담당하도록 하는 것이 좋은데 그렇다면 비즈니스 로직은 어디서 수행하는게 좋을까?
class SignUpUseCase {
fun setError(type: EditType, text: String): SignUpErrorMessage? {
val error: SignUpErrorMessage? = when {
text.isEmpty() -> when (type) {
EditType.NAME -> SignUpErrorMessage.NAME
EditType.ID, EditType.EMAIL_ID -> SignUpErrorMessage.ID
EditType.EMAIL -> SignUpErrorMessage.EMAIL
EditType.PASSWORD -> SignUpErrorMessage.PASSWORDLEGTH
else -> null
}
type == EditType.NAME && text.includeSpecialCharacters() -> SignUpErrorMessage.NAMESPECIAL
type in listOf(EditType.ID, EditType.EMAIL_ID) && text.includeSpecialCharacters() -> SignUpErrorMessage.IDSPECIAL
else -> when (type) {
EditType.ID -> when {
text.length !in 2..8 -> SignUpErrorMessage.IDLEGTH
UserList.userList.any { it.id == text } -> SignUpErrorMessage.IDUSE
else -> null
}
EditType.EMAIL -> if (!text.validEmailServiceProvider()) SignUpErrorMessage.EMAILSPECIAL else null
EditType.PASSWORD -> when {
text.length !in 10..16 -> SignUpErrorMessage.PASSWORDLEGTH
!text.includeSpecialCharacters() -> SignUpErrorMessage.PASSWORDSPECIAL
!text.includeUpperCase() -> SignUpErrorMessage.UPPERONE
else -> null
}
else -> null
}
}
return error
}
fun setServiceIndex(service: String?, emails: Array<String>): Int {
return if (emails.any{ it == service}) {
emails.indexOf(emails.find { it == service })
}else emails.lastIndex
}
fun checkPassword(password: String, confirmPassword: String): SignUpErrorMessage? {
return if (password == confirmPassword) null
else SignUpErrorMessage.PASSWORDNOCOINCIDE
}
}
UseCase는 비즈니스 로직을 수행한다
이는 앱의 핵심 기능을 구현한 것으로, ViewModel 혹은 Repository에서 직접적으로 데이터를 다루는 것이 아니라, UseCase를 통해 데이터를 처리하고 결과를 받아 사용한다
리턴값이 있는 이런 비즈니스 로직을 UseCase에서 수행한 모습이다
여기서 나온 리턴값으로 뷰모델의 라이브 데이터를 갱신하고
메인 액티비티에서는 옵저빙해서 라이브 데이터의 변화를 감지하고 UI를 갱신한다
비즈니스 로직은 UseCase 또는 Interactor 등의 별도 클래스에서 처리하는 것이 보통인데
간단한 로직은 UseCase 복잡한 로직은 Interactor에서 주로 수행한다고 한다
이렇게 각각의 역할을 분명하게 나눔으로써 코드의 유지보수성을 높일 수 있다
예를 들어, 데이터의 출처가 변경되더라도 Repository의 내부 구현만 수정하면 되고, 비즈니스 로직이 변경되더라도 UseCase만 수정하면 된다.
또한 ViewModel에서는 Repository와 UseCase를 파라미터로 받아 사용한다
이는 Dependency Injection의 원칙을 따르는 것으로, 이를 통해 ViewModel은 Repository와 UseCase의 구현에 의존하지 않게 된다
이를 통해 테스트의 용이성을 높일 수 있다.