Efficient and Testable MVVM pattern
김범준
레이니스트 / 안드로이드 개발
레이니스트에서 뱅크샐러드 안드로이드 어플리케이션을 개발하고 있는 5년차 개발자 입니다. Reactive, 함수형 프로그래밍에 관심이 많으며 효율적이고 가독성 있는 코드를 짜는 것을 항상 목표로 부단히 노력중입니다.
42. BaseViewModel
abstract class BaseViewModel : ViewModel(){
protected val compositeDisposable : CompositeDisposable = CompositeDisposable()
override fun onCleared() {
super.onCleared()
compositeDisposable.clear()
}
}
43. SearchViewModel - input, output, state
interface SearchViewModelInPuts : Input {
fun name(name: String)
fun clickSearchButton()
}
interface SearchViewModelOutPuts : Output {
fun state(): LiveData<SearchViewState>
fun goResultActivity(): LiveData<String>
}
data class SearchViewState(
val enableSearchButton: Boolean
)
44. SearchViewModel - properties
private val name = PublishSubject.create<String>()
private val clickSearchButton = PublishSubject.create<Parameter>()
val input = object : SearchViewModelInPuts {
override fun name(name: String) =
this@SearchViewModel.name.onNext(name)
override fun clickSearchButton() =
this@SearchViewModel.clickSearchButton.onNext(Parameter.CLICK)
}
private val state = MutableLiveData<SearchViewState>()
private val goResultActivity = MutableLiveData<String>()
val output = object : SearchViewModelOutPuts {
override fun state() = state
override fun goResultActivity() = goResultActivity
}
56. MainViewModel - constructor
class MainViewModel(
searchedUserName: String,
private val getUserData: GetUserData,
logger: Logger
) : BaseViewModel()
57. MainViewModel - input, output, state
interface MainViewModelInputs : Input {
fun clickUser(user: User)
fun clickHomeButton()
}
interface MainViewModelOutPuts : Output {
fun state(): LiveData<MainViewState>
fun refreshListData(): LiveData<Pair<User, List<Repo>>>
fun showErrorToast(): LiveData<String>
fun goProfileActivity(): LiveData<String>
fun finish(): LiveData<Unit>
}
data class MainViewState(
val showLoading: Boolean,
val title: String
)
58. MainViewModel - properties
private val clickUser = PublishSubject.create<User>()
private val clickHomeButton = PublishSubject.create<Parameter>()
val input: MainViewModelInputs = object : MainViewModelInputs {
override fun clickUser(user: User) = clickUser.onNext(user)
override fun clickHomeButton() = clickHomeButton.onNext(Parameter.CLICK)
}
private val state = MutableLiveData<MainViewState>()
private val refreshListData = MutableLiveData<Pair<User, List<Repo>>>()
private val showErrorToast = MutableLiveData<String>()
private val goProfileActivity = MutableLiveData<String>()
private val finish = MutableLiveData<Unit>()
val output = object : MainViewModelOutPuts {
override fun state() = state
override fun refreshListData() = refreshListData
override fun showErrorToast() = showErrorToast
override fun goProfileActivity() = goProfileActivity
override fun finish() = finish
}
76. DI for Test
val testModule = module {
single(override = true) {
TestSchedulerProvider() as SchedulersProvider
}
single(override = true) {
TestDummyGithubBrowserService() as GithubBrowserService
}
}
val test_module = listOf(myModule, testModule)
77. for Spek + LiveData
beforeEachTest {
ArchTaskExecutor.getInstance().setDelegate(object : TaskExecutor() {
override fun executeOnDiskIO(runnable: Runnable) {
runnable.run()
}
override fun isMainThread(): Boolean {
return true
}
override fun postToMainThread(runnable: Runnable) {
runnable.run()
}
})
}
afterEachTest {
ArchTaskExecutor.getInstance().setDelegate(null)
}
78. MainViewModelSpec
object MainViewModelSpec : KoinSpek({
beforeEachTest {…}
afterEachTest {…}
lateinit var userName: String
val viewModel: MainViewModel by inject { parametersOf(userName) }
val getUserData: GetUserData by inject()
Feature("MainViewModel spec") {…}
})
84. Dagger2 vs Koin
- Heavy vs light
- Dependency Injection vs ServiceLocator
- CompileTime vs RunTime
https://twitter.com/jakewharton/status/908419644742098944