An Android Jetpack ViewModel LiveData tutorial using Kotlin Android 01.08.2019

Overview of components

Jetpack is a suite of libraries to help developers follow best practices, reduce boilerplate code, and write code that works consistently across Android versions and devices so that developers can focus on the code.

Android Jetpack brings new approaches and components. Most valuable of them are

  • ViewModel. It helps us to store, manage and prepare UI related data in a lifecycle conscious way. The main benefit of the ViewModel is that it is created the first time and if the Activity, Fragment or any other LifecycleOwner is destroyed or recreated by the system, the next time it starts it will receive the same ViewModel created before.
  • LiveData. LiveData is an observable class and implements the observable design pattern, which allows us to subscribe entities that will be notified when the data contained on the LiveData class change.

ViewModel

First of all add lifecycle-extensions to the build.gradle file

dependencies {
    ...
    implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
    implementation "androidx.activity:activity-ktx:1.1.0"
    implementation "androidx.fragment:fragment-ktx:1.2.2"
}

MoneyViewModel class is inherited from the ViewModel Android architecture component.

class MoneyViewModel: ViewModel() {
    private val dollarRate = 0.74f
    private var dollarText = ""
    private var result: Float? = 0f

    fun setValue(value: String) {
        this.dollarText = value
        result = dollarText.toFloat() * dollarRate
    }

    fun getResult(): Float? {
        return result
    }
}

Next step is add MoneyViewModel to the Activity via by viewModels() delegate.

class VMActivity : AppCompatActivity() {
    val vm: MoneyViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_vm)

        tv.text = vm.getResult().toString()

        btn.setOnClickListener {
            if (et.text.isNotEmpty()) {
                vm.setValue(et.text.toString())
                tv.text = vm.getResult().toString()
            } else {
                tv.text = "No value"
            }
        }
    }
}

Following is content of activity_vm.xml layout

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    <TextView
            android:text=""
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" 
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent" 
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent" android:id="@+id/tv"/>
    <Button
            android:text="Get"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintStart_toStartOf="parent" 
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tv"
            android:id="@+id/btn" 
            android:layout_marginRight="8dp" 
            android:layout_marginEnd="8dp"
            android:layout_marginLeft="8dp" 
            android:layout_marginStart="8dp" 
            android:layout_marginTop="32dp"
            android:visibility="visible"/>
    <EditText
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:inputType="number"
            android:ems="10"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent" 
            android:id="@+id/et"
            app:layout_constraintBottom_toTopOf="@+id/tv" 
            android:layout_marginBottom="32dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>

LiveData

LiveData is an observable data holder. This allows the components in your app to be able to observe LiveData objects for changes without creating explicit and tough dependency between them. UI view can receive a notification whenever the underlying LiveData value changes.

Edit the MoneyViewModel.kt file and wrap the result variable in a MutableLiveData instance

class MoneyViewModel: ViewModel() {
    private val dollarRate = 0.74f
    private var dollarText = ""
    private var result: MutableLiveData<Float> = MutableLiveData<Float>()
    fun setValue(value: String) {
        this.dollarText = value
        result.value = dollarText.toFloat() * dollarRate
    }

    fun getResult(): MutableLiveData<Float> {
        return result
    }
}

The next step is to update MoneyActivity.kt file and add the observer to the result LiveData object.

class MoneyActivity : AppCompatActivity() {
    val vm: MoneyViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_vm)

        val resultObserver: Observer<Float> = Observer {
            tv.text = it.toString()
        }
        vm.getResult().observe(this, resultObserver)

        btn.setOnClickListener {
            if (et.text.isNotEmpty()) {
                vm.setValue(et.text.toString())
            } else {
                tv.text = "No value"
            }
        }
    }
}

Passing data between Activity and Fragment

Following example shows one of the ways how to easily interact between Activity and Fragment. There is a ViewModel in the Activity and a Fragment want to publish some changes

class MovieViewModel {
    fun getView(): SomeView { ... }
    fun fetchData() { ... }
}

class MyActivity: Activity() {
    private val vm by viewModels<MovieViewModel>()
    override fun onResume() {
        updateView(vm.getView())
    }
}

class SomeFragment: Fragment() {
    private val vm by activityViewModels<MovieViewModel>()
    fun onClick() {
        vm.fetchData()
    }
}