1 of 53

Eduardo Medina

MVVM Pattern

Model View ViewModel

2 of 53

3 of 53

4 of 53

MVC Model View Controller

MVP Model View Presenter

Clean Architecture

MVVM Model View ViewModel

Architecture Patterns & Architectures

5 of 53

  • Delegate responsibilities to other components. (BL, Data layer, View layer)
  • Consider the lifecycle of android components.
  • Convert your views to Passive Views.
  • Define a rule of dependencies.
  • Possibility to refactor and support changes.
  • Easier to create Unit tests.

You should use some architecture pattern...

6 of 53

Kotlin MVVM Pattern

Why Kotlin?

7 of 53

Kotlin-First

Google goes Kotlin-First

Google I/O 19Kotlin-first

Kotlin v1.3

Kotlin v1.2

Google I/O 17

Support Kotlin

Kotlin 1.0

Jetbrains

Kotlin

- 2019

- 2018

- 2017

- 2017

- 2016

- 2011

8 of 53

MVVM

VIEW

MODEL

VIEW

MODEL

9 of 53

MVVM - Model View ViewModel

Callbacks

Property changed events

UI events

10 of 53

Android Jetpack

11 of 53

Architecture

12 of 53

MVVM

VIEW

MODEL

VIEW

MODEL

13 of 53

Model

LiveData

request data

Callbacks

Repository

Data source

Entity

14 of 53

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

15 of 53

MVVM

VIEW

MODEL

VIEW

MODEL

16 of 53

ViewModel

ViewModel provides data for UI components and survive configuration changes.

17 of 53

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

18 of 53

MVVM

VIEW

MODEL

VIEW

MODEL

19 of 53

View

Observers

UI events

LiveData

Property changed events

20 of 53

LiveData & Lifecycle

21 of 53

LiveData & Lifecycle

22 of 53

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

23 of 53

Demo

24 of 53

https://github.com/emedinaa/kotlin-mvvm

25 of 53

Architecture

26 of 53

Flow

27 of 53

View behavior

28 of 53

View actions

retrieveMuseums()

showLoading()

hideLoading()

errorMessage()

emptyList()

renderMuseums()

29 of 53

View actions

retrieveMuseums()

showLoading()

hideLoading()

errorMessage()

emptyList()

renderMuseums()

30 of 53

View actions

retrieveMuseums()

showLoading()

hideLoading()

errorMessage()

emptyList()

renderMuseums()

31 of 53

View actions

retrieveMuseums()

showLoading()

hideLoading()

errorMessage()

emptyList()

renderMuseums()

32 of 53

Only view actions

retrieveMuseums()

showLoading()

hideLoading()

errorMessage()

emptyList()

renderMuseums()

33 of 53

Property changed events

UI events

Request data

Callbacks

Observers

LiveData

Data source

34 of 53

View actions

ViewModel

showLoading()

isViewLoading�<Boolean>

hideLoading()

isViewLoading

<Boolean>

errorMessage()

onErrorMessage�<Any>

emptyList()

isEmptyList

<Boolean>

renderMuseums()

museums List<Museum>>

35 of 53

View actions

ViewModel

Observers(View)

showLoading()

isViewLoading

isViewLoadingObserver

hideLoading()

isViewLoading

isViewLoadingObserver

errorMessage()

onErrorMessage

onMessageErrorObserver

emptyList()

isEmptyList

emptyListObserver

renderMuseums()

museums

renderMuseums

36 of 53

Unit Tests

37 of 53

  • Local tests
  • Instrumentation tests

Unit tests

JUnit v4.12 + Mockito v2.27.0 + ArchTesting v1.1.1

38 of 53

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

39 of 53

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

40 of 53

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

41 of 53

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

42 of 53

@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

43 of 53

@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

44 of 53

Summary

45 of 53

  • ViewModel provides data to UI components.
  • Use LiveData for communication between ViewModel and View.
  • Don’t use the activity reference on the ViewModel component.
  • You can use Mockito with Kotlin but with some considerations.
  • Use repository or data source interface for mock the layer data.
  • The LiveData components can be mocked.

Summary

46 of 53

https://github.com/emedinaa/kotlin-mvvm

47 of 53

Next ->

VIEW

MODEL

VIEW

MODEL

48 of 53

  • Sealed classes to replace callbacks
  • Manage asynchronous processes as synchronous processes with coroutines.
  • Integrate ViewModel component and retrofit library with coroutines.
  • Dependency injections ? Koin

Next

49 of 53

Show me �the code </>

VIEW

MODEL

VIEW

MODEL

50 of 53

  • Kotlin mvvm + tests https://github.com/emedinaa/kotlin-mvvm/tree/master
  • Kotlin mvvm + koin + tests

https://github.com/emedinaa/kotlin-mvvm/tree/koin

  • Kotlin mvvm + coroutines + sealed class + tests

https://github.com/emedinaa/kotlin-mvvm/tree/coroutines

Kotlin mvvm samples

51 of 53

https://github.com/emedinaa/kotlin-mvvm

52 of 53

Eduardo Medina

Thank you

@eduardomedina | emedinaa@gmail.com | github/emedinaa |

53 of 53

Android

Dev�Perú

https://www.facebook.com/groups/androidpe/