Eduardo Medina
MVVM Pattern
Model View ViewModel
MVC Model View Controller
MVP Model View Presenter
Clean Architecture
MVVM Model View ViewModel
Architecture Patterns & Architectures
You should use some architecture pattern...
Kotlin MVVM Pattern
Why Kotlin?
Kotlin-First
Google goes Kotlin-First
Google I/O 19 �Kotlin-first
Kotlin v1.3
Kotlin v1.2
Google I/O 17
Support Kotlin
Kotlin 1.0
Jetbrains
Kotlin
- 2019
- 2018
- 2017
- 2017
- 2016
- 2011
MVVM
VIEW
MODEL
VIEW
MODEL
MVVM - Model View ViewModel
Callbacks
Property changed events
UI events
Android Jetpack
Architecture
MVVM
VIEW
MODEL
VIEW
MODEL
Model
LiveData
request data
Callbacks
Repository
Data source
Entity
interface OperationCallback {
fun onSuccess(obj:Any?)
fun onError(obj:Any?)
}
interface MuseumDataSource {
fun retrieveMuseums(callback: OperationCallback)
fun cancel()
}
class MuseumRepository:MuseumDataSource {
override fun retrieveMuseums(callback: OperationCallback) {
… }
override fun cancel() {
… }
}
Model
MVVM
VIEW
MODEL
VIEW
MODEL
ViewModel
ViewModel provides data for UI components and survive configuration changes.
class ViewModelFactory(private val repository:MuseumDataSource):ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return MuseumViewModel(repository) as T
}
}
class MuseumViewModel(private val repository: MuseumDataSource):ViewModel() {
private val _isViewLoading=MutableLiveData<Boolean>()
val isViewLoading:LiveData<Boolean> = _isViewLoading
…
_isViewLoading.postValue(false)
}
private fun setupViewModel(){
viewModel = ViewModelProviders.of(this,ViewModelFactory(Injection.providerRepository())).get(MuseumViewModel::class.java)
viewModel.isViewLoading.observe(this,isViewLoadingObserver)
}
ViewModel
MVVM
VIEW
MODEL
VIEW
MODEL
View
Observers
UI events
LiveData
Property changed events
LiveData & Lifecycle
LiveData & Lifecycle
private fun setupViewModel(){
viewModel = ViewModelProviders.of(this,ViewModelFactory(Injection.providerRepository())).get(MuseumViewModel::class.java)
viewModel.museums.observe(this,renderMuseums)
viewModel.isViewLoading.observe(this,isViewLoadingObserver)
viewModel.onMessageError.observe(this,onMessageErrorObserver)
}
private val renderMuseums= Observer<List<Museum>> {
layoutError.visibility=View.GONE
layoutEmpty.visibility=View.GONE
adapter.update(it)
}
private val isViewLoadingObserver= Observer<Boolean> {
val visibility=if(it)View.VISIBLE else View.GONE
progressBar.visibility= visibility
}
private val onMessageErrorObserver= Observer<Any> {
layoutError.visibility=View.VISIBLE
layoutEmpty.visibility=View.GONE
textViewError.text= "Error $it"
}
override fun onResume() {
super.onResume()
viewModel.loadMuseums()
}
View
Demo
https://github.com/emedinaa/kotlin-mvvm
Architecture
Flow
View behavior
View actions |
retrieveMuseums() |
showLoading() |
hideLoading() |
errorMessage() |
emptyList() |
renderMuseums() |
View actions |
retrieveMuseums() |
showLoading() |
hideLoading() |
errorMessage() |
emptyList() |
renderMuseums() |
View actions |
retrieveMuseums() |
showLoading() |
hideLoading() |
errorMessage() |
emptyList() |
renderMuseums() |
View actions |
retrieveMuseums() |
showLoading() |
hideLoading() |
errorMessage() |
emptyList() |
renderMuseums() |
Only view actions |
retrieveMuseums() |
showLoading() |
hideLoading() |
errorMessage() |
emptyList() |
renderMuseums() |
Property changed events
UI events
Request data
Callbacks
Observers
LiveData
Data source
View actions | ViewModel |
showLoading() | isViewLoading�<Boolean> |
hideLoading() | isViewLoading <Boolean> |
errorMessage() | onErrorMessage�<Any> |
emptyList() | isEmptyList <Boolean> |
renderMuseums() | museums List<Museum>> |
View actions | ViewModel | Observers(View) |
showLoading() | isViewLoading | isViewLoadingObserver |
hideLoading() | isViewLoading | isViewLoadingObserver |
errorMessage() | onErrorMessage | onMessageErrorObserver |
emptyList() | isEmptyList | emptyListObserver |
renderMuseums() | museums | renderMuseums |
Unit Tests
Unit tests
JUnit v4.12 + Mockito v2.27.0 + ArchTesting v1.1.1
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
...
testImplementation "junit:junit:$junitVersion"
testImplementation "org.mockito:mockito-core:$mockitoVersion"
testImplementation "android.arch.core:core-testing:$archTestingVersion"
androidTestImplementation "androidx.test:runner:$runnerVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
androidTestImplementation "org.mockito:mockito-core:$mockitoVersion"
implementation "androidx.lifecycle:lifecycle-extensions:$archLifecycleVersion"
kapt "androidx.lifecycle:lifecycle-compiler:$archLifecycleVersion"
…
Dependencies
class MVVMUnitTest {
@Mock private lateinit var repository: MuseumDataSource
@Mock private lateinit var context: Application
@Captor
private lateinit var operationCallbackCaptor: ArgumentCaptor<OperationCallback>
private lateinit var viewModel:MuseumViewModel
private lateinit var isViewLoadingObserver:Observer<Boolean>
private lateinit var onMessageErrorObserver:Observer<Any>
private lateinit var emptyListObserver:Observer<Boolean>
private lateinit var onRenderMuseumsObserver:Observer<List<Museum>>
private lateinit var museumEmptyList:List<Museum>
private lateinit var museumList:List<Museum>
…
MVVMUnitTest - @Mock / @Captor
class MVVMUnitTest {
private lateinit var viewModel:MuseumViewModel
@get:Rule
val rule = InstantTaskExecutorRule()
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
`when`<Context>(context.applicationContext).thenReturn(context)
viewModel= MuseumViewModel(repository)
mockData()
setupObservers()
}
…
MVVMUnitTest - ViewModel & Model
private fun setupObservers(){
isViewLoadingObserver= mock(Observer::class.java) as Observer<Boolean>
onMessageErrorObserver= mock(Observer::class.java) as Observer<Any>
emptyListObserver= mock(Observer::class.java) as Observer<Boolean>
onRenderMuseumsObserver= mock(Observer::class.java)as Observer<List<Museum>>
}
private fun mockData(){
museumEmptyList= emptyList()
val mockList:MutableList<Museum> = mutableListOf()
mockList.add(Museum(0,"Museo Nacional de Arqueología, Antropología e Historia del Perú",""))
mockList.add(Museum(1,"Museo de Sitio Pachacamac",""))
mockList.add(Museum(2,"Casa Museo José Carlos Mariátegui",""))
museumList= mockList.toList()
}
MVVMUnitTest - Observers & Mock data
@Test
fun museumListRepositoryAndViewModel(){
with(viewModel){
loadMuseums()
isViewLoading.observeForever(isViewLoadingObserver)
museums.observeForever(onRenderMuseumsObserver)
}
verify(repository, times(1)).retrieveMuseums(capture(operationCallbackCaptor))
operationCallbackCaptor.value.onSuccess(museumList)
Assert.assertNotNull(viewModel.isViewLoading.value)
Assert.assertTrue(viewModel.museums.value?.size==3)
}
MVVMUnitTest - @Test
@Test
fun museumFailRepositoryAndViewModel() {
with(viewModel){
loadMuseums()
isViewLoading.observeForever(isViewLoadingObserver)
onMessageError.observeForever(onMessageErrorObserver)
}
verify(repository, times(1)).retrieveMuseums(capture(operationCallbackCaptor))
operationCallbackCaptor.value.onError("Ocurrió un error")
Assert.assertNotNull(viewModel.isViewLoading.value)
Assert.assertNotNull(viewModel.onMessageError.value)
}
MVVMUnitTest - @Test
Summary
Summary
https://github.com/emedinaa/kotlin-mvvm
Next ->
VIEW
MODEL
VIEW
MODEL
Next
Show me �the code </>
VIEW
MODEL
VIEW
MODEL
https://github.com/emedinaa/kotlin-mvvm/tree/koin
https://github.com/emedinaa/kotlin-mvvm/tree/coroutines
Kotlin mvvm samples
https://github.com/emedinaa/kotlin-mvvm
Eduardo Medina
Thank you
@eduardomedina | emedinaa@gmail.com | github/emedinaa |
Android
Dev�Perú
https://www.facebook.com/groups/androidpe/