The document discusses working effectively with ViewModels and test-driven development (TDD) in Android. It provides an overview of ViewModels and how they integrate with the Android lifecycle system using Lifecycle components and LiveData. It also covers tips for using ViewModels such as handling single emitted events, sharing data between fragments, RxJava support, and testing ViewModels with TestObserver and mock objects.
3. HOW MANY ARCHITECTURES DO YOU KNOW?
● MVC
● HMVC (Hierarchical model–view–controller)
● MVA (MODEL–VIEW–ADAPTER)
● MVP
● MVVM
● MVI
4.
5.
6.
7.
8.
9. They all give us the ability to decouple
development process into smaller pieces
which can be distributed between team
members
10. They all lack integration 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
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 :)
22. 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
23. 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
}
24. 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
}
25. 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
}
26. 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
}
27. 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()
}
}
28. 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()
}
}
29. 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()
}
}
30. 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()
}
}
31. 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)
}
}
32. Sharing data between fragments (or views)
Imagine we have to fragments that need to
communicate somehow?
33. 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)
}
}
34. 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
}
}
}
35. Sharing data between fragments (or views)
class SharedViewModel : ViewModel() {
val selectedSpeaker = MutableLiveData<Speaker>()
fun onSpeakerSelected(speaker: Speaker) {
selectedSpeaker.value = speaker
}
}
36. 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
})
}
}
37. 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
})
}
}
38. 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
39. 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
40. 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
41. 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?
42. Rx Support in ViewModels
class SpeakerViewModel(private val useCase: FetchSpeakerUseCase) :
BaseViewModel() {
fun loadSpeaker() {
addDisposable {
useCase.call("speaker_id").subscribe {
// Handle result
}
}
}
}
51. class SharedViewModelTest {
@get:Rule val rule = InstantTaskExecutorRule()
val viewModel = SharedViewModel()
@Test fun `test view model send correct speaker`() {
}
}
Testing ViewModels
Sample test
52. 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()
}
}
53. 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)
}
}
54. 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)
}
}
55. 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))
)
}
}
56. 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)
}
}
57. 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())
}
}
58. 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")
)
}
}
59. 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"))
}
}