Model View ViewModel is an android architecture model. It separates data-presentation logic from business logic. It’s designed to store and manage UI-related data.
- Model - Application data logic. Plain Objects. API processing classes. Databases.
- View - Screen’s layout. Houses all widgets for displaying information.
- ViewModel - Business Logic. Object which describes the behaviour of view logic depending on the result of the model work.
We usually create one ViewModel for an activity or a fragment. Sometimes ViewModels can be shared by two or more Activities or Fragments. ViewModel are created when the activity is created, and they live until the activity is cleared from memory. Therefore ViewModel can only hold values which belong to the activity.
Implementation
We will have 3 packages: model
, view
, and viewmodel
.
Inside model
, we have data
classes such as this one
data class Country(val countryName: String?)
Inside view
, we move the MainActivity
class that already comes with the default project.
In viewmodel
we create a ListViewModel
class.
Dependencies
Add the following dependencies to your app’s build.gradle
.
implementation "android.arch.lifecycle:viewmodel:1.1.1"
implementation "android.arch.lifecycle:extensions:1.1.1"
ViewModel
LiveData is a variable type that anyone can subscribe to and can get this data in real time. When this variable is updated then all the subscribers to this variable will also get notified and they may be updated.
class ListViewModel: ViewModel() {
val countries = MutableLiveData<List<Country>>()
val countryLoadError = MutableLiveData<Boolean>()
val loading = MutableLiveData<Boolean>()
fun refresh() {
countries.value = ...
}
}
Following this model, anyone who subscribes to the variable countries
will get notified whenever this variable is updated.
View
This is how to use ViewModel from MainActivity.
class MainActivity : AppCompatActivity() {
lateinit var viewModel: ListViewModel
lateinit var adapter: ...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProviders.of(this).get(ListViewModel::class.java)
viewModel.refresh()
observeViewModel()
}
fun observeViewModel() {
viewModel.countries.observe(this, Observer { countries ->
// this will get called whenever 'countries.value = ...' is set
countries?.let { adapter.updateCountries(it) }
})
}
}
This way, the ViewModel
just needs to update its data and doesn’t know anything from the consumer Activity
, as it will be observing and called when the data changes.
Activity vs Fragments
When binding ViewModel, it has to be bound to an Activity. Not a Fragment
. This way all the Fragment
that are related to the same Activity
will share the same ViewModel status.
bind an Activity
lateinit var viewModel: ListViewModel
override fun onCreate(...) {
viewModel = ViewModelProviders.of(this).get(ListViewModel::class.java)
}
bind a Fragment
lateinit var viewModel: ListViewModel
override fun onCreate(...) {
viewModel = ViewModelProviders.of(requireActivity()).get(ListViewModel::class.java)
}
ViewModel Factory
Sometimes we need to pass constructor params to a ViewModel as a starting point. To do such thing, we need support from a ViewModel Factory.
We have the following base ViewModel class. We have set the constructor param we need.
class MainActivityViewModel(startingTotal : Int) : ViewModel() {
private var total = 0
init {
total = startingTotal
}
fun getTotal(): Int { return total }
fun setTotal(input: Int) { total += input }
}
In order to do this, we create the following factory. We override the create()
method and add the same constructor param.
class MainActivityViewModelFactory(private val startingTotal : Int) : ViewModelProvider.Factory {
override fun<T : ViewModel?> create(modelClass: Class<T>): T {
if(modelClass.isAssignableFrom(MainActivityViewModel::class.java)) {
return MainActivityViewModel(startingTotal) as T
}
throw IllegalArgumentException("Unknown View Model Class")
}
}
Then we use it as follows
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainActivityViewModel
private lateinit var viewModelFactory: MainActivityViewModelFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// programmatically set the value we need.
viewModelFactory = MainActivityViewModelFactory(5)
viewModel = ViewModelProvider(this, viewModelFactory).get(MainActivityViewModel::class.java)
}
}
Adapter
In order to display a list of elements into the view, we need an Adapter. This is a class that will take the data in the form the ViewModel gives and it will transform that list into something the view’s layouts can display.
This adapter takes a ViewHolder. We will create it as an internal class.
class CountryListAdapter(var countries: ArrayList<Country>) : RecyclerView.Adapter<CountryViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, p1: Int) = CountryViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_country,
parent,
false)
)
override fun getItemCount() = countries.size
override fun onBindViewHolder(holder: CountryViewHolder, position: Int) {
holder.bind(countries[position])
}
class CountryViewHolder(view: View): RecyclerView.ViewHolder(view) {
fun bind(country: Country)
}
}
}