WORKING EFFECTIVELY WITH VIEWMODELS AND TDD
ANDRIY MATKIVSKIY
Senior Mobile Engineer at Valtech
Topics
●
●
●
●
HOW MANY ARCHITECTURES DO YOU KNOW?
● MVC
● HMVC (Hierarchical model–view–controller)
● MVA (MODEL–VIEW–ADAPTER)
● MVP
● MVVM
● MVI
They all give us the ability to decouple
development process into smaller pieces
which can be distributed between team
members
They all lack integration with Android lifecycle system
How it works ?
● Lifecycle components
● LiveData<T>
● ViewModel
How it works ?
Lifecycle components
interface LifeCycle (Observer pattern)
+ addObserver()
+ removeObserver()
+ getCurrentState()
interface LifecycleObserver
interface LifecycleOwner
+ getLifecycle()
class LifecycleRegistry : LifeCycle
+ Does all the magic required to handle this
How it works ?
Lifecycle components
How it works ?
val lifecycleOwner = object : LifecycleOwner {
val lifecycleRegistry = LifecycleRegistry(this)
override fun getLifecycle() = lifecycleRegistry
}
How it works ?
val lifecycleOwner = object : LifecycleOwner {
val lifecycleRegistry = LifecycleRegistry(this)
override fun getLifecycle() = lifecycleRegistry
}
lifecycleOwner.lifecycle.addObserver(
object : LifecycleObserver {
@OnLifecycleEvent(Event.ON_ANY)
fun onAny(source: LifecycleOwner, event: Event) {
// Handle incoming events
}
}
)
How it works ?
val lifecycleOwner = object : LifecycleOwner {
val lifecycleRegistry = LifecycleRegistry(this)
override fun getLifecycle() = lifecycleRegistry
}
lifecycleOwner.lifecycle.addObserver(
object : LifecycleObserver {
@OnLifecycleEvent(Event.ON_ANY)
fun onAny(source: LifecycleOwner, event: Event) {
// Handle incoming events
}
}
)
lifecycleOwner.lifecycleRegistry.handleLifecycleEvent(Event.ON_RESUME)
How it works ?
val lifecycleOwner = object : LifecycleOwner {
val lifecycleRegistry = LifecycleRegistry(this)
override fun getLifecycle() = lifecycleRegistry
}
lifecycleOwner.lifecycle.addObserver(
object : LifecycleObserver {
@OnLifecycleEvent(Event.ON_ANY)
fun onAny(source: LifecycleOwner, event: Event) {
// Handle incoming events
}
}
)
lifecycleOwner.lifecycleRegistry.handleLifecycleEvent(Event.ON_RESUME)
How it works ?
LiveData<T>
class LiveData<T> (Observer pattern) on steroids (Lifecycle aware)
● Handles Observer state for us (via listening to LifecycleOwner)
liveData.observe(LifecycleOwner(), Observer {})
- Ensures Observer is not called when related LifecycleOwner is at least in
STARTED state
- Remove Observer if when related LifecycleOwner reaches DESTROYED
state
- Ensures that Observer receives last value when it is active again
(LifecycleOwner is back in STARTED state)
How it works ?
LiveData<T>
class LiveData<T> (Observer pattern) on steroids (Lifecycle aware)
● Handles threading for us
How it works ?
ViewModel
public abstract class ViewModel {
protected void onCleared() {}
}
That’s all :)
Tips and tricks of working with ViewModel and LiveData
Single emitted events through LiveData
- Show one time message (toast, snackbar,
dialog)
- Send one time actions to the view (close
activity, navigation events for fragments)
- Open other activities (by intent)
- Any type of activity that should be triggered
once and not re-triggered after rotation
Show me the code
data class Event<out T>(val content: T) {
private var consumed = false
fun consume(consumer: (T) -> Unit) {
if (not(consumed)) {
consumer(content)
}
consumed = true
}
fun not(condition: Boolean) = !condition
}
Show me the code TODO: Remove
data class Event<out T>(val content: T) {
private var consumed = false
fun consume(consumer: (T) -> Unit) {
if (not(consumed)) {
consumer(content)
}
consumed = true
}
fun not(condition: Boolean) = !condition
}
Show me the code TODO: Remove
data class Event<out T>(val content: T) {
private var consumed = false
fun consume(consumer: (T) -> Unit) {
if (not(consumed)) {
consumer(content)
}
consumed = true
}
fun not(condition: Boolean) = !condition
}
Show me the code TODO: Remove
data class Event<out T>(val content: T) {
private var consumed = false
fun consume(consumer: (T) -> Unit) {
if (not(consumed)) {
consumer(content)
}
consumed = true
}
fun not(condition: Boolean) = !condition
}
Usage (from ViewModel)
class MainViewModelExample : ViewModel() {
val events = MutableLiveData<Event<ViewModelEvent>>()
fun sendEvent() {
events.value = Event(ViewModelEvent.ShowToast("Hello"))
}
open class ViewModelEvent {
data class ShowToast(val message: String) : ViewModelEvent()
}
}
Usage (from ViewModel)
class MainViewModelExample : ViewModel() {
val events = MutableLiveData<Event<ViewModelEvent>>()
fun sendEvent() {
events.value = Event(ViewModelEvent.ShowToast("Hello"))
}
open class ViewModelEvent {
data class ShowToast(val message: String) : ViewModelEvent()
}
}
Usage (from ViewModel)
class MainViewModelExample : ViewModel() {
val events = MutableLiveData<Event<ViewModelEvent>>()
fun sendEvent() {
events.value = Event(ViewModelEvent.ShowToast("Hello"))
}
open class ViewModelEvent {
data class ShowToast(val message: String) : ViewModelEvent()
}
}
Usage (from ViewModel)
class MainViewModelExample : ViewModel() {
val events = MutableLiveData<Event<ViewModelEvent>>()
fun sendEvent() {
events.value = Event(ViewModelEvent.ShowToast("Hello"))
}
open class ViewModelEvent {
data class ShowToast(val message: String) : ViewModelEvent()
}
}
Usage (from View)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val model = ViewModelProviders.of(this)
.get(MainViewModelExample::class.java)
observeEvents(model.events) {
when (it) {
is ViewModelEvent.ShowToast -> showToast(it.message)
}
}
Sharing data between fragments (or views)
Imagine we have to fragments that need to
communicate somehow?
Sharing data between fragments (or views)
class SpeakersFragment : Fragment() {
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val model = ViewModelProviders.of(requireActivity())
.get(SharedViewModel::class.java)
// For example, on button click
model.onSpeakerSelected(speaker)
}
}
Sharing data between fragments (or views)
class SpeakerDetailsFragment : Fragment() {
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
// Get the same instance of ViewModel as in SpeakersFragment
val model = ViewModelProviders.of(requireActivity())
.get(SharedViewModel::class.java)
observe(model.selectedSpeaker) {
// Render speaker details on screen
}
}
}
Sharing data between fragments (or views)
class SharedViewModel : ViewModel() {
val selectedSpeaker = MutableLiveData<Speaker>()
fun onSpeakerSelected(speaker: Speaker) {
selectedSpeaker.value = speaker
}
}
Be careful with fragments (as usual)
class AnotherUserFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val model = ..
model.users.observe(this, Observer {
// Handle users
// This can be triggered multiple times during the switch of
fragments in activity
})
}
}
Be careful with fragments (as usual)
class AnotherUserFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val model = ..
model.users.observe(viewLifecycleOwner, Observer {
// Handle users
})
}
}
Using ViewModels in Views
class SpeakerFragment : Fragment() {
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val model = viewModel<SpeakerViewModel>(viewModelFactory) {
observe(speakers, ::handlerSpeakers)
observeEvents(events, ::handleEvent)
}
}
}
- Android-CleanArchitecture-Kotlin with Dagger way
Using ViewModels in Views
class SpeakerFragment : Fragment() {
private val model: SpeakerViewModel by sharedViewModel()
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
observe(model.speakers, ::handlerSpeakers)
observeEvents(model.events, ::handleEvent)
}
}
- With Koin DI framework
Rx Support in ViewModels
class SpeakerViewModel(private val useCase: FetchSpeakerUseCase)
:ViewModel() {
private val compositeDisposable = CompositeDisposable()
fun loadSpeaker() {
compositeDisposable.add(
useCase.call("speaker_id").subscribe {
// Handle result
}
)
}
override fun onCleared() {
compositeDisposable.clear()
}
}
Problem
Rx Support in ViewModels
abstract class BaseViewModel : ViewModel() {
private val compositeDisposable = CompositeDisposable()
fun addDisposable(block: () -> Disposable) {
compositeDisposable.add(block())
}
public override fun onCleared() {
compositeDisposable.clear()
}
}
How we can fix this?
Rx Support in ViewModels
class SpeakerViewModel(private val useCase: FetchSpeakerUseCase) :
BaseViewModel() {
fun loadSpeaker() {
addDisposable {
useCase.call("speaker_id").subscribe {
// Handle result
}
}
}
}
TDD with ViewModels
General architecture
General architecture
General architecture
This gives a independent development tasks for separate feature
Testing ViewModels
Handling LiveData values with TestObserver
class TestObserver<T> : Observer<T> {
override fun onChanged(value: T) {
// Handle values coming from LiveData
}
}
Testing ViewModels
Handling LiveData values with TestObserver
class TestObserver<T> : Observer<T> {
val observedValues = mutableListOf<T>()
override fun onChanged(value: T) {
observedValues.add(value)
}
}
Testing ViewModels
Handling LiveData values with TestObserver
class TestObserver<T> : Observer<T> {
val observedValues = mutableListOf<T>()
override fun onChanged(value: T) {
observedValues.add(value)
}
}
fun <T> LiveData<T>.testObserver() = TestObserver<T>().also {
observeForever(it)
}
Testing ViewModels
Sample test
class SharedViewModelTest {
@get:Rule val rule = InstantTaskExecutorRule()
}
class SharedViewModelTest {
@get:Rule val rule = InstantTaskExecutorRule()
val viewModel = SharedViewModel()
@Test fun `test view model send correct speaker`() {
}
}
Testing ViewModels
Sample test
Testing ViewModels
Sample test
class SharedViewModelTest {
@get:Rule val rule = InstantTaskExecutorRule()
val viewModel = SharedViewModel()
@Test fun `test view model send correct data`() {
val speaker = Speaker("Andriy")
val speakerObserver = viewModel.selectedSpeaker.testObserver()
}
}
Testing ViewModels
Sample test
class SharedViewModelTest {
@get:Rule val rule = InstantTaskExecutorRule()
val viewModel = SharedViewModel()
@Test fun `test view model send correct data`() {
val speaker = Speaker("Andriy")
val speakerObserver = viewModel.selectedSpeaker.testObserver()
viewModel.onSpeakerSelected(speaker)
}
}
Testing ViewModels
Sample test
class SharedViewModelTest {
@get:Rule val rule = InstantTaskExecutorRule()
val viewModel = SharedViewModel()
@Test fun `test view model send correct data`() {
val speaker = Speaker("Andriy")
val speakerObserver = viewModel.selectedSpeaker.testObserver()
viewModel.onSpeakerSelected(speaker)
speakerObserver.observedValues.shouldContainSame(speaker)
}
}
Testing ViewModels
Sample test
class SharedViewModelTest {
@get:Rule val rule = InstantTaskExecutorRule()
val viewModel = SharedViewModel()
@Test fun `test view model send correct data`() {
val speaker = Speaker("Andriy")
val speakerEventsObserver = viewModel.speakerEvents.testObserver()
viewModel.onSpeakerSelected(speaker)
speakerEventsObserver.observedValues.shouldContainSame(
Event(ViewModelEvent.OpenSpeakerUrl("https://andriy))
)
}
}
Testing ViewModels
Let’s add Kotlin magic
class TestObserver<T> : Observer<T> {
val observedValues = mutableListOf<T>()
fun <Event> shouldContainEvents(vararg events: Event) {
val wrapped = events.map { Event(it) }
observedValues.shouldContainSame(wrapped)
}
}
Testing ViewModels
Let’s add Kotlin magic
class TestObserver<T> : Observer<T> {
val observedValues = mutableListOf<T>()
fun <Event> shouldContainEvents(vararg events: Event) {
val wrapped = events.map { Event(it) }
observedValues.shouldContainSame(wrapped)
}
fun <T> shouldContainValues(vararg values: T) {
observedValues.shouldContainSame(values.asList())
}
}
Testing ViewModels
Sample test
class SharedViewModelTest {
@get:Rule val rule = InstantTaskExecutorRule()
val viewModel = SharedViewModel()
@Test fun `test view model send correct data`() {
val speaker = Speaker("Andriy")
val speakerEventsObserver = viewModel.speakerEvents.testObserver()
viewModel.onSpeakerSelected(speaker)
speakerEventsObserver.shouldContainEvents(
ViewModelEvent.OpenSpeakerUrl("https://andriy")
)
}
}
Testing ViewModels
Sample test
class SharedViewModelTest {
@get:Rule val rule = InstantTaskExecutorRule()
val useCase = mockk<FetchSpeakerUseCase>()
val viewModel = SharedViewModel(useCase)
@Test fun `test view model send correct data`() {
val speakerName = "Andriy"
val speakerObserver = viewModel.speaker.testObserver()
every { useCase.call(speakerName) }
.returns(Speaker("Andriy"))
viewModel.onSpeakerSelected(speakerName)
speakerObserver.shouldContainValues(Speaker("Andriy"))
}
}

Working effectively with ViewModels and TDD - UA Mobile 2019

  • 1.
    WORKING EFFECTIVELY WITHVIEWMODELS AND TDD ANDRIY MATKIVSKIY Senior Mobile Engineer at Valtech
  • 2.
  • 3.
    HOW MANY ARCHITECTURESDO YOU KNOW? ● MVC ● HMVC (Hierarchical model–view–controller) ● MVA (MODEL–VIEW–ADAPTER) ● MVP ● MVVM ● MVI
  • 9.
    They all giveus the ability to decouple development process into smaller pieces which can be distributed between team members
  • 10.
    They all lackintegration with Android lifecycle system
  • 11.
    How it works? ● Lifecycle components ● LiveData<T> ● ViewModel
  • 12.
    How it works? Lifecycle components interface LifeCycle (Observer pattern) + addObserver() + removeObserver() + getCurrentState() interface LifecycleObserver interface LifecycleOwner + getLifecycle() class LifecycleRegistry : LifeCycle + Does all the magic required to handle this
  • 13.
    How it works? Lifecycle components
  • 14.
    How it works? val lifecycleOwner = object : LifecycleOwner { val lifecycleRegistry = LifecycleRegistry(this) override fun getLifecycle() = lifecycleRegistry }
  • 15.
    How it works? val lifecycleOwner = object : LifecycleOwner { val lifecycleRegistry = LifecycleRegistry(this) override fun getLifecycle() = lifecycleRegistry } lifecycleOwner.lifecycle.addObserver( object : LifecycleObserver { @OnLifecycleEvent(Event.ON_ANY) fun onAny(source: LifecycleOwner, event: Event) { // Handle incoming events } } )
  • 16.
    How it works? val lifecycleOwner = object : LifecycleOwner { val lifecycleRegistry = LifecycleRegistry(this) override fun getLifecycle() = lifecycleRegistry } lifecycleOwner.lifecycle.addObserver( object : LifecycleObserver { @OnLifecycleEvent(Event.ON_ANY) fun onAny(source: LifecycleOwner, event: Event) { // Handle incoming events } } ) lifecycleOwner.lifecycleRegistry.handleLifecycleEvent(Event.ON_RESUME)
  • 17.
    How it works? val lifecycleOwner = object : LifecycleOwner { val lifecycleRegistry = LifecycleRegistry(this) override fun getLifecycle() = lifecycleRegistry } lifecycleOwner.lifecycle.addObserver( object : LifecycleObserver { @OnLifecycleEvent(Event.ON_ANY) fun onAny(source: LifecycleOwner, event: Event) { // Handle incoming events } } ) lifecycleOwner.lifecycleRegistry.handleLifecycleEvent(Event.ON_RESUME)
  • 18.
    How it works? LiveData<T> class LiveData<T> (Observer pattern) on steroids (Lifecycle aware) ● Handles Observer state for us (via listening to LifecycleOwner) liveData.observe(LifecycleOwner(), Observer {}) - Ensures Observer is not called when related LifecycleOwner is at least in STARTED state - Remove Observer if when related LifecycleOwner reaches DESTROYED state - Ensures that Observer receives last value when it is active again (LifecycleOwner is back in STARTED state)
  • 19.
    How it works? LiveData<T> class LiveData<T> (Observer pattern) on steroids (Lifecycle aware) ● Handles threading for us
  • 20.
    How it works? ViewModel public abstract class ViewModel { protected void onCleared() {} } That’s all :)
  • 21.
    Tips and tricksof working with ViewModel and LiveData
  • 22.
    Single emitted eventsthrough LiveData - Show one time message (toast, snackbar, dialog) - Send one time actions to the view (close activity, navigation events for fragments) - Open other activities (by intent) - Any type of activity that should be triggered once and not re-triggered after rotation
  • 23.
    Show me thecode data class Event<out T>(val content: T) { private var consumed = false fun consume(consumer: (T) -> Unit) { if (not(consumed)) { consumer(content) } consumed = true } fun not(condition: Boolean) = !condition }
  • 24.
    Show me thecode TODO: Remove data class Event<out T>(val content: T) { private var consumed = false fun consume(consumer: (T) -> Unit) { if (not(consumed)) { consumer(content) } consumed = true } fun not(condition: Boolean) = !condition }
  • 25.
    Show me thecode TODO: Remove data class Event<out T>(val content: T) { private var consumed = false fun consume(consumer: (T) -> Unit) { if (not(consumed)) { consumer(content) } consumed = true } fun not(condition: Boolean) = !condition }
  • 26.
    Show me thecode TODO: Remove data class Event<out T>(val content: T) { private var consumed = false fun consume(consumer: (T) -> Unit) { if (not(consumed)) { consumer(content) } consumed = true } fun not(condition: Boolean) = !condition }
  • 27.
    Usage (from ViewModel) classMainViewModelExample : ViewModel() { val events = MutableLiveData<Event<ViewModelEvent>>() fun sendEvent() { events.value = Event(ViewModelEvent.ShowToast("Hello")) } open class ViewModelEvent { data class ShowToast(val message: String) : ViewModelEvent() } }
  • 28.
    Usage (from ViewModel) classMainViewModelExample : ViewModel() { val events = MutableLiveData<Event<ViewModelEvent>>() fun sendEvent() { events.value = Event(ViewModelEvent.ShowToast("Hello")) } open class ViewModelEvent { data class ShowToast(val message: String) : ViewModelEvent() } }
  • 29.
    Usage (from ViewModel) classMainViewModelExample : ViewModel() { val events = MutableLiveData<Event<ViewModelEvent>>() fun sendEvent() { events.value = Event(ViewModelEvent.ShowToast("Hello")) } open class ViewModelEvent { data class ShowToast(val message: String) : ViewModelEvent() } }
  • 30.
    Usage (from ViewModel) classMainViewModelExample : ViewModel() { val events = MutableLiveData<Event<ViewModelEvent>>() fun sendEvent() { events.value = Event(ViewModelEvent.ShowToast("Hello")) } open class ViewModelEvent { data class ShowToast(val message: String) : ViewModelEvent() } }
  • 31.
    Usage (from View) overridefun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val model = ViewModelProviders.of(this) .get(MainViewModelExample::class.java) observeEvents(model.events) { when (it) { is ViewModelEvent.ShowToast -> showToast(it.message) } }
  • 32.
    Sharing data betweenfragments (or views) Imagine we have to fragments that need to communicate somehow?
  • 33.
    Sharing data betweenfragments (or views) class SpeakersFragment : Fragment() { override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) val model = ViewModelProviders.of(requireActivity()) .get(SharedViewModel::class.java) // For example, on button click model.onSpeakerSelected(speaker) } }
  • 34.
    Sharing data betweenfragments (or views) class SpeakerDetailsFragment : Fragment() { override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) // Get the same instance of ViewModel as in SpeakersFragment val model = ViewModelProviders.of(requireActivity()) .get(SharedViewModel::class.java) observe(model.selectedSpeaker) { // Render speaker details on screen } } }
  • 35.
    Sharing data betweenfragments (or views) class SharedViewModel : ViewModel() { val selectedSpeaker = MutableLiveData<Speaker>() fun onSpeakerSelected(speaker: Speaker) { selectedSpeaker.value = speaker } }
  • 36.
    Be careful withfragments (as usual) class AnotherUserFragment : Fragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val model = .. model.users.observe(this, Observer { // Handle users // This can be triggered multiple times during the switch of fragments in activity }) } }
  • 37.
    Be careful withfragments (as usual) class AnotherUserFragment : Fragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val model = .. model.users.observe(viewLifecycleOwner, Observer { // Handle users }) } }
  • 38.
    Using ViewModels inViews class SpeakerFragment : Fragment() { @Inject lateinit var viewModelFactory: ViewModelProvider.Factory override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) val model = viewModel<SpeakerViewModel>(viewModelFactory) { observe(speakers, ::handlerSpeakers) observeEvents(events, ::handleEvent) } } } - Android-CleanArchitecture-Kotlin with Dagger way
  • 39.
    Using ViewModels inViews class SpeakerFragment : Fragment() { private val model: SpeakerViewModel by sharedViewModel() override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) observe(model.speakers, ::handlerSpeakers) observeEvents(model.events, ::handleEvent) } } - With Koin DI framework
  • 40.
    Rx Support inViewModels class SpeakerViewModel(private val useCase: FetchSpeakerUseCase) :ViewModel() { private val compositeDisposable = CompositeDisposable() fun loadSpeaker() { compositeDisposable.add( useCase.call("speaker_id").subscribe { // Handle result } ) } override fun onCleared() { compositeDisposable.clear() } } Problem
  • 41.
    Rx Support inViewModels abstract class BaseViewModel : ViewModel() { private val compositeDisposable = CompositeDisposable() fun addDisposable(block: () -> Disposable) { compositeDisposable.add(block()) } public override fun onCleared() { compositeDisposable.clear() } } How we can fix this?
  • 42.
    Rx Support inViewModels class SpeakerViewModel(private val useCase: FetchSpeakerUseCase) : BaseViewModel() { fun loadSpeaker() { addDisposable { useCase.call("speaker_id").subscribe { // Handle result } } } }
  • 43.
  • 44.
  • 45.
  • 46.
    General architecture This givesa independent development tasks for separate feature
  • 47.
    Testing ViewModels Handling LiveDatavalues with TestObserver class TestObserver<T> : Observer<T> { override fun onChanged(value: T) { // Handle values coming from LiveData } }
  • 48.
    Testing ViewModels Handling LiveDatavalues with TestObserver class TestObserver<T> : Observer<T> { val observedValues = mutableListOf<T>() override fun onChanged(value: T) { observedValues.add(value) } }
  • 49.
    Testing ViewModels Handling LiveDatavalues with TestObserver class TestObserver<T> : Observer<T> { val observedValues = mutableListOf<T>() override fun onChanged(value: T) { observedValues.add(value) } } fun <T> LiveData<T>.testObserver() = TestObserver<T>().also { observeForever(it) }
  • 50.
    Testing ViewModels Sample test classSharedViewModelTest { @get:Rule val rule = InstantTaskExecutorRule() }
  • 51.
    class SharedViewModelTest { @get:Ruleval rule = InstantTaskExecutorRule() val viewModel = SharedViewModel() @Test fun `test view model send correct speaker`() { } } Testing ViewModels Sample test
  • 52.
    Testing ViewModels Sample test classSharedViewModelTest { @get:Rule val rule = InstantTaskExecutorRule() val viewModel = SharedViewModel() @Test fun `test view model send correct data`() { val speaker = Speaker("Andriy") val speakerObserver = viewModel.selectedSpeaker.testObserver() } }
  • 53.
    Testing ViewModels Sample test classSharedViewModelTest { @get:Rule val rule = InstantTaskExecutorRule() val viewModel = SharedViewModel() @Test fun `test view model send correct data`() { val speaker = Speaker("Andriy") val speakerObserver = viewModel.selectedSpeaker.testObserver() viewModel.onSpeakerSelected(speaker) } }
  • 54.
    Testing ViewModels Sample test classSharedViewModelTest { @get:Rule val rule = InstantTaskExecutorRule() val viewModel = SharedViewModel() @Test fun `test view model send correct data`() { val speaker = Speaker("Andriy") val speakerObserver = viewModel.selectedSpeaker.testObserver() viewModel.onSpeakerSelected(speaker) speakerObserver.observedValues.shouldContainSame(speaker) } }
  • 55.
    Testing ViewModels Sample test classSharedViewModelTest { @get:Rule val rule = InstantTaskExecutorRule() val viewModel = SharedViewModel() @Test fun `test view model send correct data`() { val speaker = Speaker("Andriy") val speakerEventsObserver = viewModel.speakerEvents.testObserver() viewModel.onSpeakerSelected(speaker) speakerEventsObserver.observedValues.shouldContainSame( Event(ViewModelEvent.OpenSpeakerUrl("https://andriy)) ) } }
  • 56.
    Testing ViewModels Let’s addKotlin magic class TestObserver<T> : Observer<T> { val observedValues = mutableListOf<T>() fun <Event> shouldContainEvents(vararg events: Event) { val wrapped = events.map { Event(it) } observedValues.shouldContainSame(wrapped) } }
  • 57.
    Testing ViewModels Let’s addKotlin magic class TestObserver<T> : Observer<T> { val observedValues = mutableListOf<T>() fun <Event> shouldContainEvents(vararg events: Event) { val wrapped = events.map { Event(it) } observedValues.shouldContainSame(wrapped) } fun <T> shouldContainValues(vararg values: T) { observedValues.shouldContainSame(values.asList()) } }
  • 58.
    Testing ViewModels Sample test classSharedViewModelTest { @get:Rule val rule = InstantTaskExecutorRule() val viewModel = SharedViewModel() @Test fun `test view model send correct data`() { val speaker = Speaker("Andriy") val speakerEventsObserver = viewModel.speakerEvents.testObserver() viewModel.onSpeakerSelected(speaker) speakerEventsObserver.shouldContainEvents( ViewModelEvent.OpenSpeakerUrl("https://andriy") ) } }
  • 59.
    Testing ViewModels Sample test classSharedViewModelTest { @get:Rule val rule = InstantTaskExecutorRule() val useCase = mockk<FetchSpeakerUseCase>() val viewModel = SharedViewModel(useCase) @Test fun `test view model send correct data`() { val speakerName = "Andriy" val speakerObserver = viewModel.speaker.testObserver() every { useCase.call(speakerName) } .returns(Speaker("Andriy")) viewModel.onSpeakerSelected(speakerName) speakerObserver.shouldContainValues(Speaker("Andriy")) } }