Activities
Fragments
Views
Memory
Remote APIs
Database
View
ViewModel
<data>
<variable
name="viewModel"
type="io.greenerpastures.example.MyViewModel" />
</data>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.name}"
android:visibility="@{viewModel.isNameVisible}"
tools:text="Min bonus" />
observes
View
ViewModel
fun onCreate(savedInstanceState: Bundle?) {
...
viewModel.viewState
.observe(this, Observer<ViewState> { data ->
findViewById<TextView>(R.id.name).text = data.name
findViewById<ImageView>(R.id.image).drawable = data.image
})
}
observes
View
ViewModel
fun onStart() {
...
viewModel.viewState
.subscribe { data ->
findViewById<TextView>(R.id.name).text = data.name
findViewById<ImageView>(R.id.image).drawable = data.image
}
}
observes
fun onStart() {
...
viewModel.viewState
.subscribe { data ->
findViewById<TextView>(R.id.name).text = data.name
findViewById<ImageView>(R.id.image).drawable = data.image
}
.disposeOnStop()
}
class BaseActivity() : Activity() {
private var compositeDisposable: CompositeDisposable = CompositeDisposable()
protected fun Disposable.disposeOnStop() {
compositeDisposable.add(this)
}
override fun onStop() {
super.onStop()
compositeDisposable.dispose()
}
...
}
class MainViewModel : ViewModel() {
var isVisible: ObservableBoolean = ObservableBoolean(true)
...
}
override fun onStart() {
super.onStart()
binding.addOnRebindCallback(object : OnRebindCallback<ViewDataBinding>() {
override fun onPreBind(binding: ViewDataBinding): Boolean {
TransitionManager.beginDelayedTransition(binding.root as ViewGroup)
return super.onPreBind(binding)
}
})
}
override fun onStart() {
super.onStart()
binding.addOnRebindCallback(object : OnRebindCallback<ViewDataBinding>() {
override fun onPreBind(binding: ViewDataBinding): Boolean {
return super.onPreBind(binding)
}
})
}
@BindingAdapter("animatedVisibility")
fun setAnimatedVisibility(target: View, isVisible: Boolean) {
TransitionManager.beginDelayedTransition(target.rootView as ViewGroup)
target.visibility = if (isVisible) View.VISIBLE else View.GONE
// Or run any other animation using any other mechanism
}
viewModel.isVisible.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
TransitionManager.beginDelayedTransition(binding.root as ViewGroup)
binding.view.visibility = if (viewModel.isVisible.get()) View.VISIBLE else View.GONE
// Or run any other animation using any other mechanism
}
// NOTE: Requires cleaning up the OnPropertyChangeCallback via
// viewModel.isVisible.removeOnPropertyChangedCallback()
viewModel.isVisible.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
TransitionManager.beginDelayedTransition(binding.root as ViewGroup)
binding.view.visibility = if (viewModel.isVisible.get()) View.VISIBLE else View.GONE
// Or run any other animation using any other mechanism
}
/**
* The base ViewModel class for an item to be shown in a list.
*/
interface ItemViewModel {
}
/**
* A [RecyclerView.ViewHolder] implementation that handles binding of an [ItemViewModel] with a
* [ViewDataBinding].
*/
open class SimpleViewModelItemViewHolder<in VM : ItemViewModel, out DB : ViewDataBinding>
constructor(protected val binding: DB,
private val viewModelBindingVarId: Int) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: VM) {
this.binding.setVariable(viewModelBindingVarId, item)
this.binding.executePendingBindings()
}
}
/**
* The base ViewModel class for an item to be shown in a list.
*/
interface ItemViewModel {
}
/**
* The base ViewModel class for an item to be shown in a list.
*/
interface ItemViewModel {
}
/**
* A [RecyclerView.ViewHolder] implementation that handles binding of an [ItemViewModel] with a
* [ViewDataBinding].
*/
open class SimpleViewModelItemViewHolder<in VM : ItemViewModel, out DB : ViewDataBinding>
constructor(protected val binding: DB,
private val viewModelBindingVarId: Int) : RecyclerView.ViewHolder(binding.root) {
}
abstract class BaseViewModelItemAdapter<IVM : ItemViewModel, out B : ViewDataBinding>
: RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
SimpleViewModelItemViewHolder<IVM, B>(
inflateView(LayoutInflater.from(parent.context), parent, viewType),
itemViewModelBindingVarId
)
abstract fun inflateView(inflater: LayoutInflater, parent: ViewGroup, viewType: Int): B
abstract val itemViewModelBindingVarId: Int
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) =
(holder as SimpleViewModelItemViewHolder<IVM, B>).bind(getItem(position))
abstract fun getItem(position: Int): IVM
}
abstract class BaseViewModelItemAdapter<IVM : ItemViewModel, out B : ViewDataBinding>
: RecyclerView.Adapter<RecyclerView.ViewHolder>() {
}
abstract class BaseViewModelItemAdapter<IVM : ItemViewModel, out B : ViewDataBinding>
: RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
SimpleViewModelItemViewHolder<IVM, B>(
inflateView(LayoutInflater.from(parent.context), parent, viewType),
itemViewModelBindingVarId
)
abstract fun inflateView(inflater: LayoutInflater, parent: ViewGroup, viewType: Int): B
abstract val itemViewModelBindingVarId: Int
}
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="ro.greenerpastures.devfest2018.ProductItemViewModel" />
</data>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{viewModel.name}"
android:textSize="18sp"
tools:text="Main Text" />
</layout>
class ProductItemViewModel(private val product: Product) : ItemViewModel {
val name: String
get() = product.name
}
class ProductAdapter : BaseViewModelItemAdapter<ProductItemViewModel, ProductItemBinding>() {
var items: List<ProductItemViewModel> = listOf()
set(value) {
field = value
notifyDataSetChanged() // DiffUtil should be used for a better experience
}
override fun getItem(position: Int): ProductItemViewModel = items[position]
override fun getItemCount(): Int = items.size
override val itemViewModelBindingVarId: Int
get() = BR.viewModel
override fun inflateView(inflater: LayoutInflater, parent: ViewGroup, viewType: Int) =
ProductItemBinding.inflate(inflater, parent, false)
}
class ProductItemViewModel(private val product: Product) : ItemViewModel {
val name: String
get() = product.name
}
class ProductAdapter : BaseViewModelItemAdapter<ProductItemViewModel, ProductItemBinding>() {
var items: List<ProductItemViewModel> = listOf()
set(value) {
field = value
notifyDataSetChanged() // DiffUtil should be used for a better experience
}
override fun getItem(position: Int): ProductItemViewModel = items[position]
override fun getItemCount(): Int = items.size
}
class ProductItemViewModel(private val product: Product) : ItemViewModel {
val name: String
get() = product.name
}
class ProductAdapter : BaseViewModelItemAdapter<ProductItemViewModel, ProductItemBinding>() {
}
class ProductItemViewModel(private val product: Product) : ItemViewModel {
val name: String
get() = product.name
}
class ProductListViewModel : ViewModel() {
// Expose the list of items as a list of item view models
val items = MutableLiveData<List<ProductItemViewModel>>()
...
}
class ProductListActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Setup ViewModel & RecyclerView
val viewModel: ProductListViewModel = ...
val recyclerView: RecyclerView = ...
// Setup adapter
val adapter = ProductAdapter()
recyclerView.adapter = adapter
// Make sure we update the data in the adapter when data in the ViewModel gets updated
viewModel.items.observe(this, Observer { updatedItems: List<ProductItemViewModel> ->
adapter.items = updatedItems
})
}
}
class ProductListViewModel : ViewModel() {
// Expose the list of items as a list of item view models
val items = MutableLiveData<List<ProductItemViewModel>>()
...
}
class ProductListActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Setup ViewModel & RecyclerView
val viewModel: ProductListViewModel = ...
val recyclerView: RecyclerView = ...
// Setup adapter
val adapter = ProductAdapter()
recyclerView.adapter = adapter
}
}
class ProductListViewModel : ViewModel() {
// Expose the list of items as a list of item view models
val items = MutableLiveData<List<ProductItemViewModel>>()
...
}
class ProductListActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Setup ViewModel & RecyclerView
val viewModel: ProductListViewModel = ...
val recyclerView: RecyclerView = ...
}
}
class ProductListViewModel : ViewModel() {
// Expose the list of items as a list of item view models
val items = MutableLiveData<List<ProductItemViewModel>>()
...
}
Category 1
Product 1.1
Product 1.2
Category 2
Product 2.1
Product 2.2
Product 2.3
class ProductAdapter : BaseViewModelItemAdapter<ItemViewModel, ViewDataBinding>() {
...
override fun getItemViewType(position: Int): Int =
when (getItem(position)) {
is CategoryItemViewModel -> VIEW_TYPE_CATEGORY
is ProductItemViewModel -> VIEW_TYPE_PRODUCT
else -> throw UnsupportedOperationException("Invalid view model type")
}
override fun inflateView(inflater: LayoutInflater, parent: ViewGroup, viewType: Int) =
when (viewType) {
VIEW_TYPE_PRODUCT -> ProductItemBinding.inflate(inflater, parent, false)
VIEW_TYPE_CATEGORY -> CategoryItemBinding.inflate(inflater, parent, false)
else -> throw UnsupportedOperationException("Invalid view type")
}
}
class ProductAdapter : BaseViewModelItemAdapter<ItemViewModel, ViewDataBinding>() {
...
override fun getItemViewType(position: Int): Int =
when (getItem(position)) {
is CategoryItemViewModel -> VIEW_TYPE_CATEGORY
is ProductItemViewModel -> VIEW_TYPE_PRODUCT
else -> throw UnsupportedOperationException("Invalid view model type")
}
}
class ProductAdapter : BaseViewModelItemAdapter<ItemViewModel, ViewDataBinding>() {
}
class ProductListViewModel : ViewModel() {
// Expose the list of items as a mixt list of item view models,
// either CategoryItemViewModel or ProductItemViewModel
val items = MutableLiveData<List<ItemViewModel>>()
...
}
Navigation must be driven by View Models
Context is required for Navigation
View Models mustn't reference Context
View
ViewModel
observes
View
Navigate back
event
observes
event
once!
class MainViewModel : ViewModel() {
val events: PublishSubject<Event> = PublishSubject.create()
enum class Event {
NAV_TO_DETAILS
}
}
class MainActivity : Activity() {
override fun onStart() {
super.onStart()
viewModel.events
.subscribe {
when (it) {
MainViewModel.Event.NAV_TO_DETAILS -> startActivity(..)
}
}
.disposeOnStop()
}
}
class MainViewModel : ViewModel() {
private val navigateToDetails = SingleLiveEvent<Boolean>()
}
class MainActivity : Activity() {
override fun onStart() {
super.onStart()
viewModel.navigateToDetails.observe(this, Observer {
startActivity(DetailsActivity...)
})
}
}
View
ViewModel
observes
View
event
observes
event
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set
}
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set
/** Fetches the content and prevents its use again. */
fun consumeContentIfNotHandled(): T? =
if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
/** Fetches the content, even if it's already been handled. */
fun peekContent(): T = content
}
class MainViewModel : ViewModel() {
val events: PublishSubject<Event> = PublishSubject.create()
enum class Event {
NAV_TO_DETAILS
}
}
class MainActivity : Activity() {
override fun onStart() {
super.onStart()
viewModel.events
.subscribe {
when (it.consumeContentIfNotHandled()) {
MainViewModel2.Event.NAV_TO_DETAILS -> startActivity(..)
}
}
.disposeOnStop()
}
}
View
ViewModel
Command Handler
class MainViewModel() {
var commandHandler: Commands? = null
interface Commands {
fun navigateToDetails()
fun showConfirmationDialog(): Single<Boolean>
}
fun onButtonClick() {
commandHandler?.showConfirmationDialog()
?.subscribe { isConfirmed ->
if(isConfirmed)
commandHandler?.navigateToDetails()
}
}
}
class MainViewModel() {
var commandHandler: Commands? = null
interface Commands {
fun navigateToDetails()
fun showConfirmationDialog(): Single<Boolean>
}
}
abstract class BaseActivity() {
abstract lateinit var viewModel: ViewModel
abstract val commandHandler: Any
override fun onStart() {
super.onStart()
viewModel.commandHandler = commandHandler
}
override fun onStop() {
super.onStart()
viewModel.commandHandler = null
}
}
abstract class BaseActivity() {
abstract lateinit var viewModel: ViewModel
abstract val commandHandler: Any
override fun onStart() {
super.onStart()
viewModel.commandHandler = commandHandler
}
}
abstract class BaseActivity() {
abstract lateinit var viewModel: ViewModel
abstract val commandHandler: Any
}