So I have a viewHolder with a checkbox
here is my viewModel
#Bindable
var itemIsSelected: Boolean = isSelected
set(value) {
if (field != value) {
field = value
notifyPropertyChanged(BR.itemIsSelected) // this doesn't work
notifyChange() // this one works
}
}
here is my viewHolder class
inner class SpecialityItemViewHolder(val binding: ItemSpecialityFilterBinding): RecyclerView.ViewHolder(binding.root) {
fun bind(specialityItemViewModel: SpecialityItemViewModel) {
binding.viewModel = specialityItemViewModel
binding.executePendingBindings()
this.itemView.setOnClickListener {
binding.viewModel?.let {
it.itemIsSelected = !it.itemIsSelected // this doesn't trigger ui changes
}
}
}
}
xml
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable
name="viewModel"
type="packagename.ItemViewModel" />
</data>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="#dimen/vertical_margin_small"
android:paddingBottom="#dimen/vertical_margin_small"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<Checkbox
android:id="#+id/checkbox"
android:layout_width="25dp"
android:layout_height="25dp"
android:checked="#={viewModel.itemIsSelected}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</android.support.constraint.ConstraintLayout>
</layout>
so what happens is that the setting is working properly as in that when i press the checkbox it sets the backing field to the corresponding value
but when i set the backing field (notice code in bind function) it doesn't trigger ui change I know that calling binding.executePendingBindings() would solve the problem but my understanding is that notifyPropertyChanged(BR.itemIsSelected) should not need executePendingBindings call, Actually if i call notifyChange instead everything works properly ( but I presume there is performance issue here as it notifies change for all properties instead )
Your ViewModel class have to extend BaseObservable class and using kotlin you have to use #get:Bindable annotation. If you don't want to use BaseObservable as a parent class then use ObservableField<Boolean>(). You find more information in https://developer.android.com/topic/libraries/data-binding/observability#kotlin
the view-model needs Kotlin annotations, else the annotation processor will ignore it:
class ViewModel : BaseObservable() {
#get:Bindable
var isSelected: Boolean
set(value) {
if (isSelected != value) {
isSelected = value
notifyPropertyChanged(BR.isSelected)
}
}
}
it.isSelected is easier to read than it.itemIsSelected.
Related
I have a basic Activity with a View and a ViewModel. I am using DataBinding library.
I need to update the visibility of a TextView from the ViewModel.
Here is how I setup my Activity :
class HomeActivity : AppCompatActivity() {
// Helper classes
private var binding : ActivityHomeBinding? = null
private var viewModel = HomeViewModel()
// ...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_home)
binding?.viewModel = viewModel
// ...
viewModel.setNoContact()
}
// ...
}
Here is how my HomeViewModel looks like :
class HomeViewModel {
val contactsListVisibility = ObservableInt(View.VISIBLE)
val loadingVisibility = ObservableInt(View.VISIBLE)
val noContactVisibility = ObservableInt(View.VISIBLE)
// ...
fun setNoContact() {
Log.d(TAG, "Current no contacts visibility = ${noContactVisibility.get()}")
contactsListVisibility.set(View.GONE)
loadingVisibility.set(View.GONE)
noContactVisibility.set(View.VISIBLE)
Log.d(TAG, "Should set no contacts visibility to ${noContactVisibility.get()}")
}
companion object {
private const val TAG = "HomeViewModel"
}
And here is my View :
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.example.ft_hangouts.home_activity.HomeViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:background="#color/cardview_dark_background"
tools:context=".home_activity.HomeActivity">
<!-- ... -->
<TextView
android:id="#+id/no_contact"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/no_contact"
android:textSize="30sp"
android:visibility="#{viewModel.noContactVisibility}"
android:textColor="#color/white"
style="#style/BasicTextStyle"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- ... -->
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
The path to my ViewModel in the xml layout is correct. The link is done, when I Ctrl + click it, I am brought to my HomeViewModel.
However, the "No Contact" TextView is never showing up.
Here is the result I get :
And here is the result expected :
When looking at the logs, I have :
D/HomeViewModel: Current no contacts visibility = 8
Should set no contacts visibility to 0
So there is no reason the TextView is not displayed. I am forgetting something ?
Thanks.
I have tried adding dummy data to test this. Assuming you have an ArrayList of contacts like this..
val contactList: ArrayList<Int> = arrayListOf()
and I have added dummy data for contacts so you can test yourself. Usually you will get this data from a service or contact app.
HomeViewModel
fun setDummyContact(isAddYes: Boolean){
if(isAddYes){
contactList.add(7673443334)
contactList.add(2233444512)
contactList.add(1233455623)
}
}
You can set data from your HomeActivity like this...
viewModel.setDummyContact(false) // for no contact data
**OR** viewModel.setDummyContact(true) // add contacts -
You can simply do the following to handle Visibility of TextView based on your contactList data.
Add import of View to your datatype
<data>
<import type="android.view.View"/>
<variable
name="viewModel"
type="com.example.ft_hangouts.home_activity.HomeViewModel" />
</data>
and then add this to your No Contact TextView
android:visibility="#{viewModel.contactList.size() > 0 ? View.GONE : View.VISIBLE}"
So you don't need to add any function or Boolean value to handle visibility. It is more cleaner and better solution because you don't need to additional variable to handle visibility of the textView. You can do the same for any other View you would like to show or hide.
I watched this awesome talk by Florina Muntenescu on KontlinConf 2018 where she talked about how they reshaped their app architecture.
One part of the talk was how they expose a UiModel (not ViewModel) via LiveData from the ViewModel. (watch here)
She made a example similar to this:
class MyViewModel constructor(...) : ViewModel() {
private val _uiModel = MutableLiveData<UiModel>()
val uiModel: LiveData<UiModel>
get() = _uiModel
}
A view declaration for the ViewModel above could be:
<layout>
<data>
<variable
name="viewModel"
type="com.demo.ui.MyViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout>
<EditText
android:id="#+id/text"
android:text="#={viewModel.uiModel.text}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
She didn't talked about (or I missed it) how they react to property changes within the UiModel itself. How can I execute a function everytime text changes?
When having the text in separate LiveData property within the ViewModel I could use MediatorLiveData for this like:
myMediatorLiveData.addSource(text){
// do something when text changed
}
But when using the approach above the UiModel does not change instead the values of it are changed. So this here doesn't work:
myMediatorLiveData.addSource(uiModel){
// do something when text inside uiModel changed
}
So my question is how can I react on changes inside a UiModel in the ViewModel with this approach?
Thanks for advice,
Chris
I want to summarize my research regarding the topic above.
As #CommonsWars said in the comments above you can implement the field in the UiModel as ObservableFields. But after some hands on I currently prefer the approach described here.
This led me to the following code:
ViewModel:
class MyViewModel constructor(...) : ViewModel() {
val uiModel = liveData{
val UiModel uiModel = UiModel() // get the model from where ever you want
emit(uiModel)
}
fun doSomething(){
uiModel.value!!.username = "abc"
}
}
UiModel
class UiModel : BaseObservable() {
#get:Bindable // puts '#Bindable' on the getter
var text = ""
set(value) {
field = value
notifyPropertyChanged(BR.text) // trigger binding
}
val isValid: Boolean
#Bindable("text") get() { // declare 'text' as a dependency
return !text.isBlank()
}
}
Layout
<layout>
<data>
<variable
name="viewModel"
type="com.demo.ui.MyViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout>
<EditText
android:id="#+id/text"
android:text="#={viewModel.uiModel.text}" />
<Button
android:id="#+id/sent_btn"
android:enabled="#{viewModel.uiModel.isValid}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
I've chosen this approach because of the way we change properties of the UiModel from within a ViewModel.
We can you can set/get the username in the ViewModel by:
fun doSomething(){
uiModel.value!!.username = "abc"
}
fun doSomething(){
uiModel.value!!.username
}
When you implementing it by ObservableFields you have to set/get the username by:
fun doSomething(){
uiModel.value!!.username.set("abc")
}
fun doSomething(){
uiModel.value!!.username.get()
}
You can pick the approach which suites best for your needs!
Hope this helps someone.
Chris
I have a view and viewmodel which has 2 functionalities -
1) Clicking a button in view and getting data.
2) A Spinner where you can select an item and ask the viewmodel to get data for that item as a query parameter.
I already implemented the first point like this -
MyView code -
viewModel.onGetDataClicked.observe(this, Observer {
//....
})
My ViewModel code -
private val viewState = MyViewState()
val onGetDataClicked =
Transformations.map(dataDomain.getData(MyAction.GetDataAction)) {
when (it) {
....
}
}
MyAction code -
sealed class MyAction : Action {
object GetDataAction : MyAction()
}
My question is how do I pass the spinner value from view to the viewmodel? Since in viewmodel I have a val onGetDataClicked and not a function.
First you should get the item value in the view itself, after that pass the item value to the required method in the ViewModel and from the ViewModel to the Repository(where you are querying the data from).
// in view
viewModel.onGetDataClicked(item:DataType).observe(this, Observer {
//....
})
//in viewmodel
private val viewState = MyViewState()
val onGetDataClicked:(item:DataType) =
Transformations.map(dataDomain.getData(MyAction.GetDataAction)) {
//you have item here, pass it where you require
when (it) {
....
}
}
Hi you can use the selectedItemPosition attribute from the view and pass position to viewModel, accordingly you can map the item using the position.
layout.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data class="FeedbackBinding">
<variable
name="vm"
type="com.ec.service.ServiceViewModel" />
</data>
<androidx.appcompat.widget.AppCompatSpinner
android:id="#+id/unitAET"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:selectedItemPosition="#={vm.selectedUnitPosition}"
app:entries="#{vm.unitNames}"
app:layout_constraintEnd_toEndOf="#+id/textView10"
app:layout_constraintStart_toStartOf="#+id/textView10"
app:layout_constraintTop_toBottomOf="#+id/textView10" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
the selectedUnitPosition is a MutableLeveData
fragment.kt
In your fragment initialise the vm (viewModel)
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
withViewModel<ServiceViewModel>(factory) {
binding.vm = this
}
}
I made a View that I want to reuse across many pages. It contains feedback elements for the user such as a ProgressBar, TextView etc.
Due to high amount of items within, binding all those turns out like this:
<layout ... >
<data>
<variable
name="screenObserver"
type="my.namespace.ScreenStateObserver" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout ... >
<my.namespace.view.ScreenStateView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:loading="#{screenObserver.isProgressVisible}"
app:errorText="#{screenObserver.errorTxt}"
app:buttonText="#{screenObserver.errorBtnTxt}"
app:errorVisible="#{screenObserver.isTextVisible}"
app:buttonVisible="#{screenObserver.isButtonVisible}"
app:onButtonClick="#{() -> screenObserver.onErrorResolve()}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
I find copy/pasting the whole XML block messy and error-prone. Is there any way to make this simpler ?
ScreenStateObserver is just a interface that I implement in my ViewModel and bind as follows:
override fun onCreateView(...): View? {
val factory = InjectorUtils.provideViewModelFactory()
viewmodel = ViewModelProviders.of(this, factory).get(MyViewModel::class.java)
binding = MyFragmentBinding.inflate(inflater, container, false).apply {
screenObserver = viewmodel
}
}
class AtoZViewModel() : ViewModel(), ScreenStateObserver { ... }
interface ScreenStateObserver {
val isProgressVisible : MutableLiveData<Boolean>
val isTextVisible : MutableLiveData<Boolean>
val isButtonVisible : MutableLiveData<Boolean>
// [..]
}
Thanks !
Here is my suggestion to reduce code.
First declare a class like this
interface ScreenState {
class Loading : ScreenState
class Error(val errorMessage: String, val errorButtonText: String) : ScreenState
}
and inside you CustomView it will be
internal class ScreenStateView {
fun setState(state: ScreenState) {
if (state is ScreenState.Loading) {
// show loading
} else {
// hide loading
}
if (state is ScreenState.Error) {
//show {state.errorMessage} and {state.errorButtonText}
} else {
// hide error
}
}
}
using in xml
<my.namespace.view.ScreenStateView
...
app:state="#{screenObserver.screenState}"
...
app:onButtonClick="#{() -> screenObserver.onErrorResolve()}" /> // for onButtonClick I think it still better if we keep like this
Hope it help
You can use <include> in data binding layouts. Included layout file can have its own data and variables that you can access from the main binding class as well.
You have to create a layout file(such as layout_state_view.xml that contains your view and data variables relevant to your view:
<layout>
<data>
<variable
name="screenObserver"
type="my.namespace.ScreenStateObserver" />
</data>
<my.namespace.view.ScreenStateView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:loading="#{screenObserver.isProgressVisible}"
app:errorText="#{screenObserver.errorTxt}"
app:buttonText="#{screenObserver.errorBtnTxt}"
app:errorVisible="#{screenObserver.isTextVisible}"
app:buttonVisible="#{screenObserver.isButtonVisible}"
app:onButtonClick="#{() -> screenObserver.onErrorResolve()}" />
</layout>
Now you can include this in your root layout file:
<layout>
<data>
...
</data>
<LinearLayout //Can be any layout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
layout:="#layout/layout_state_view">
</LinearLayout>
</layout>
Now when you are using binding class, if you root layout file was R.layout.mainActivity then it would look like this:
binding.layoutStateView.setScreenObserver(...)
You can also make a variable in root layout and pass that variable to child layout by using bind tag as mentioned on documentation but since you are looking to reduce code, it would be unnecessary.
Note: Since you only have a single view, you might be tempted to use <merge> tag. Databinding's layout tag does not support merge as a direct child.
Documentation Reference:
https://developer.android.com/topic/libraries/data-binding/expressions#includes
My solution to reduce code is first define class for ScreenStateView(different properties of ScreenStateView in this class) then use it as much times as you needed
I am working on a simple Android App using Kotlin. I want to use databindings, but I cannot get them work with an EditText
This is my XML Layout
<?xml version="1.0" encoding="utf-8"?>
<layout
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">
<data>
<import type="de.knerd.applicationmanager.models.AgencyModel"/>
<variable name="agency" type="AgencyModel"/>
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
tools:showIn="#layout/activity_add_agency"
tools:context="de.knerd.applicationmanager.activities.AddAgencyActivity"
android:orientation="vertical"
android:layout_margin="16dp">
<android.support.design.widget.TextInputLayout android:layout_height="match_parent"
android:layout_width="match_parent">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPersonName"
android:ems="10"
android:id="#+id/name"
android:labelFor="#+id/name"
android:hint="#string/agency_name"
android:text="#={agency.name}"/>
</android.support.design.widget.TextInputLayout>
</RelativeLayout>
</layout>
This is my Model class
package de.knerd.applicationmanager.models
import android.databinding.BaseObservable
import android.databinding.Bindable
import com.j256.ormlite.dao.ForeignCollection
import com.j256.ormlite.field.DatabaseField
import com.j256.ormlite.field.ForeignCollectionField
import com.j256.ormlite.table.DatabaseTable
import de.knerd.applicationmanager.BR
#DatabaseTable(tableName = "agency")
class AgencyModel : BaseObservable() {
#DatabaseField(unique = true, canBeNull = false)
#get:Bindable
#set:Bindable
var name: String? = null
set(value) {
field = value
notifyPropertyChanged(BR.name)
}
#DatabaseField(generatedId = true)
#get:Bindable
var id: Int = 0
#ForeignCollectionField(eager = false)
#get:Bindable
var agents: ForeignCollection<AgentModel>? = null
}
And this is the part that binds the model to the view
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView<ContentAddAgencyBinding>(this, R.layout.content_add_agency)
binding.agency = AgencyModel()
setContentView(R.layout.activity_add_agency)
val toolbar = findViewById(R.id.toolbar) as Toolbar
setSupportActionBar(toolbar)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
}
When I try to access the properties of model that is in the binding I get a null value. The Code I use is this
private fun save(): Boolean {
try {
val agency = binding.agency
Log.d("Name", agency.name)
return true
} catch (ex: Exception) {
return false
}
}
I found the Problem, it was caused by me binding to the wrong layout. The correct Code is as follows.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView<ActivityAddAgencyBinding>(this, R.layout.activity_add_agency)
binding.agency = AgencyModel()
val toolbar = findViewById(R.id.toolbar) as Toolbar
setSupportActionBar(toolbar)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
}
In both cases I had to bind to the activity layout
You are not using Binding the way it was meant to be Knerd. Two-Way binding is intended so that you can access objects or fields from code without ever having to reference the UI code. By you doing the binding.agency you are no different then findViewById() and you are defeating the purpose of binding.
Fix your binding problems first. Use BaseObservable for your object model and it should stay up to date with whatever values change on the screen.
Then when you save it should be nothing more then accessing the local variable.
private fun save(): Boolean {
try {
Log.d("Name", localObservableAgency.name)
return true
} catch (ex: Exception) {
return false
}
}
//not sure what this method is for, guessing you were try catching null. Better way would be to just do a null check on the object as catching exception by accessing null value is heavy and doesn't add value. Also Kotlin is null safe language, so you are either losing your value on the binding maybe by setting it to null to begin with or something is coded weirdly here as you have to intentionally make an object nullable if you keep a local copy. However, you weren't keeping a local copy you were just newing one up inline and accessing via binding later as if it was your repository of variables haha.
So long story short, is use data binding the way it was intended to be used and you won't have a problem. That is probably why someone down voted you is my guess.