I am using a bindingAdapter to set the background of a view based on the state.
But the bindingAdapter works only when the view reinflates or the visibility change. Is there any way to invoke the binding without reinflating or changing the visibility?
#BindingAdapter("bgClickable")
fun bgClickable(layout: ConstraintLayout, state: String) {
when (state) {
DISCONNECTED -> {
val outValue = TypedValue()
layout.context.theme
.resolveAttribute(android.R.attr.selectableItemBackground, outValue, true)
layout.setBackgroundResource(outValue.resourceId)
}
else -> {
layout.background = null
}
}
}
View
.
.
.
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/layout_location"
android:layout_width="wrap_content"
app:bgClickable="#{viewModel.getCurrentStatus()}"
android:layout_height="wrap_content"
android:onClick="#{()->viewModel.redirectToExplorer()}"
app:layout_constraintBottom_toTopOf="#+id/guideline3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.25">
<.../>
A common mistake with binding adapter is not to add a lifecycleOwner to the binding object and without it the changes to the underlying live data won't be notified to the binding object. so add the following in the fragment/activity class
binding.lifecycleOwner = this
Related
I have a button. When the button is clicked, the button and a textView are animated. The question is: how to get multiple views on the binding adapter? Is the way I did it correct?
<variable
name="variableTextViewDescription"
type="androidx.appcompat.widget.AppCompatTextView" />
fun bind(task: Task, viewModel: ToDoListViewModel) {
binding.task = task
binding.viewModel = viewModel
binding.variableTextViewDescription = binding.textViewDescription
binding.executePendingBindings()
}
#BindingAdapter(value = ["task", "textViewDescription"], requireAll = true)
fun ImageButton.setOnClickButtonMore(task: Task, textViewDescription: AppCompatTextView) {
if (task.isExpanded) {
toggleArrow(this, false, textViewDescription)
} else {
toggleArrow(this, true, textViewDescription)
}
this.setOnClickListener {
task.isExpanded = toggleArrow(it, task.isExpanded, textViewDescription)
}
}
<ImageButton
android:id="#+id/buttonMore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:task="#{task}"
app:textViewDescription="#{variableTextViewDescription}"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="#drawable/ic_baseline_keyboard_arrow_down_24"
tools:ignore="ContentDescription" />
I can propose a solution for you, that maybe different from adding multiple Views to the same Binding Adapter.
You can add a MutableLiveData when changed by Button click, it starts the animation.
So, we will have a single MutableLiveData added to 2 Binding Adapters (the button binding adapter and the ImageView binding adapter).
when the value of the MutableLiveData changed, both binding adapters will fire and in both adapters load your animation.
I am trying to show a popup menu for the items in my RecyclerView:
All the code samples on how to do this that I found online either use Java or when they in rare cases do use Kotlin, it's done without data binding.
Anyway what I'm trying to achieve is this:
<ImageButton
android:id="#+id/options"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:layout_marginEnd="16dp"
android:background="?attr/selectableItemBackground"
android:contentDescription="#string/desc_options"
android:onClickListener="#{(v) -> holder.test1.invoke()}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="#drawable/ic_more_vert" />
And in my RowHolder I have the following code:
class AgendaRowHolder(
private val binding: AgendaRowBinding,
val onRowClick: (AgendaModel) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
fun bind(model: AgendaModel) {
binding.model = model
binding.holder = this
binding.executePendingBindings()
}
fun test1() {
// do something here
}
}
However this will not compile at all and a workaround to solving this problem is to have a function as a property of the AgendaRowHolder class, but this approach doesn't work since I also have to pass the ImageView for the popup menu to know where to show up. So I used this alternative approach, namely adding an onClickListener in the bind() function:
fun bind(model: AgendaModel) {
binding.model = model
binding.holder = this
binding.executePendingBindings()
binding.options.setOnClickListener { showPopup(it) }
}
private fun showPopup(view : View) {
val popup = PopupMenu(view.context, view)
popup.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.delete -> {
AgendaRepository.delete(binding.model!!)
true
} else -> false
}
}
val inflater: MenuInflater = popup.menuInflater
inflater.inflate(R.menu.actions_agenda, popup.menu)
popup.show()
}
This approach "works" in that it will show a popup menu and allow me to delete items, but I have a new problem here in that when an item/row gets delete it doesn't show up until the view is recreated, right now I'm not observing my data so that might be the reason as to why, but what I'm wondering is what would be the best approach to achieve this? Note: another thing that I tried is adding an android:onClick attribute in the xml and trying to handle this in the MainActivity, but I'm not sure which approach to take here. Can the first one be achieved? Ideally, I'd have:
android:onClickListener="#{(v) -> holder.showPopup.invoke(model, v)}" and then that showPopup function in the ViewHolder.
One Activity, two Fragments that share a common ViewModel. I have verified that the view model reference is the same in each fragment.
In the layout XML for fragment one, there is a TextInputLayout. Fragment two updates the view model with a boolean value. The text input layout is observing this value and should call a BindingAdapter when the value is changed.
The binding adapter fires when the fragments are instantiated and their layouts are inflated, so I know that the view is observing this value. However, later on, when fragment two updates the value, the view in fragment one does not trigger the binding adapter.
This is in onCreateView() of fragment one:
registrationViewModel = activity?.run {
ViewModelProviders
.of(this, RegistrationViewModelFactory(prefs, dataFetcherService))
.get(RegistrationViewModel::class.java)
} ?: throw Exception("Invalid Activity")
and this is the view that is observing that view model:
<com.google.android.material.textfield.TextInputLayout
android:id="#+id/reg_auth_code_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
bind:errorState="#{registrationViewModel.registrationData.authorizationError}"
bind:errorMessage="#{#string/invalid_auth_code}">
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/reg_auth_code"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#{registrationViewModel.registrationData.authCode}"
android:hint="#string/enter_auth_code"
android:maxLines="1"
android:inputType="text"
android:imeOptions="actionDone"
app:autoSizeTextType="uniform"/>
</com.google.android.material.textfield.TextInputLayout>
As for fragment two, same code in onCreateView():
registrationViewModel = activity?.run {
ViewModelProviders
.of(this, RegistrationViewModelFactory(prefs, dataFetcherService))
.get(RegistrationViewModel::class.java)
} ?: throw Exception("Invalid Activity")
When a button is clicked, fragment two fires an activity in the view model:
private fun attemptNavigationToUserData() {
viewModelScope.launch {
isAuthorized = runBlocking { useCase.isAuthorized() }
registrationData.value?.authorizationError = !isAuthorized
}
}
And finally, here is the BindingAdapter:
#BindingAdapter("errorState", "errorMessage")
fun setErrorState(
textInputLayout: TextInputLayout?,
errorState: Boolean,
errorMessage: String) {
textInputLayout?.let {
it.isErrorEnabled = errorState
if (errorState) it.error = errorMessage
}
}
This all seems to be set up correctly, AFAIK. As I mentioned, the binding adapter fires when the views are initially inflated, but never again.
Why isn't my XML observing the view model? Or, why isn't the binding adapter firing upon update??
Thanks for any help.
The answer is most likely that you don't set the lifecycleOwner for your fragment binding object.
For further information see https://stackoverflow.com/a/56011798/1894338
Look at my answer here https://stackoverflow.com/a/66488334/9747826
setLifeCyclerOwner and setting the viewModel are the key.
You authorizationError should be a LiveData<>.So that the BindingAdapter mothod will called automatically when the LiveData's value(errorState) is updated.
If you want BindingAdapter method receive the automatic update, you should use LiveData in the dataBinding expression.
Such as:
subTitleText="#{removableItemsViewModel.removableItemsInfo}"
and the BindingAdapter method and the LiveData:
val removableItemsInfo: LiveData<Pair<Int, Long>>
-----------------------------------------------------
#BindingAdapter("subTitleText")
fun setSubTitleText(textView: TextView, pair: Pair<Int, Long>){
}
also,don't forget to invoke binding.setLifecycleOwner()
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
}
}
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.