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.
Related
I am trying to make a RecyclerView of a list of Tasks with done and undone states and I want to create a beautiful transition between those states.
So I created an animated-selector to make the transition between the states, but the transitions is not working when I use it inside the RecyclerView, when I test it on a Button/ImageView outside the RecyclerView it works as it should.
Screen with the RecyclerView
aslv_status.xml
<?xml version="1.0" encoding="utf-8"?>
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="#+id/done"
android:drawable="#drawable/ic_done"
android:state_selected="true" />
<item
android:id="#+id/undone"
android:drawable="#drawable/ic_undone" />
<transition
android:drawable="#drawable/avd_done_to_undone"
android:fromId="#id/done"
android:toId="#id/undone" />
<transition
android:drawable="#drawable/avd_undone_to_undone"
android:fromId="#id/undone"
android:toId="#id/done" />
</animated-selector>
I toggle the attribute isDone of the Task in the adapter item onClickListener and seted the animated-selector as the android:src of the AppCompatImageView of my recyclerView adapter item:
item_task.xml
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="#{() -> viewModel.toggleTaskState(task)}" >
...
<androidx.appcompat.widget.AppCompatImageView
android:id="#+id/imageview_status_image"
android:layout_width="35dp"
android:layout_height="35dp"
android:src="#drawable/aslv_status"
.../>
...
TaskViewModel.kt
fun toggleTaskState(task: Task) = viewModelScope.launch {
task.isDone = !task.isDone
taskRepository.updateTask(task)
}
On TaskAdapter on init I call notifyDataSetChanged() when the liveData allTasks change, and on TaskViewHolder binding I updated the AppCompatImageView selected state.
TaskAdapter.kt
private var allTasks = viewModel.allTasks
init {
viewModel.allTasks.observe(lifecycle, {
notifyDataSetChanged()
})
}
...
class TaskViewHolder(private var binding: ItemTaskBinding, private val viewModel: TaskViewModel)
: RecyclerView.ViewHolder(binding.root) {
fun bind(task: Task) {
binding.task = task
binding.viewModel = viewModel
binding.imageviewStatusImage.isSelected = task.isDone
binding.executePendingBindings()
}
}
I think the problem is the notifyDataSetChanged() called when I change the state of the Task object, because I imagine the RecyclerView updates the layout abruptly and doesn't show the transition state animation, but if I don't call the notifyDataSetChanged() the RecyclerView won't change the list items state.
Responding in case someone goes through the same situation as me.
In the notifyDataSetChanged() method documentation:
RecyclerView will attempt to synthesize visible structural change
events for adapters that report that they have {#link #hasStableIds()
stable IDs} when this method is used. This can help for the purposes
of animation and visual object persistence but individual item views
will still need to be rebound and relaid out.
So I setted hasStableIds() to my Adapter and it worked!
val taskAdapter = TaskAdapter(this, taskViewModel)
taskAdapter.setHasStableIds(true)
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
I need to pass an id of clicked object from AutoCompleteTextView to ViewModel. Here I have a binding adapter to set a spinner with objects for AutoCompleteTextView.
#BindingAdapter("bindAutocomplete")
fun bindAutocomplete(textView: AutoCompleteTextView, cities: List<City>?){
cities?.let {
val adapter = ArrayAdapter<City>(
textView.context,
R.layout.support_simple_spinner_dropdown_item,
it)
textView.setAdapter(adapter)
}
}
My question is: where should I place my OnItemClickListener, in this adapter above or in the Fragment class like in code below?
The problem for first way is that I dont know how to access my ViewModel from Binding Adapter. And for the second, if I put this listener in the Fragment class isn`t it breaks a pattern, because initializations of the Biniding Adapter and of the OnItemClickListener are not synchronized?
So I need to pass a city.id to some method in my ViewModel.
binding.autoCompleteTextView.setOnItemClickListener { parent, view, position, id ->
val city = parent.adapter.getItem(position) as City
binding.viewModel.getWeatherProperties(city.id)
}
You can use two way data binding like below to pass data to viewmodel..
<AutoCompleteTextView
android:id="#+id/autoCompleteTextView"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginLeft="92dp"
android:layout_marginTop="144dp"
android:text="#={viewmodel.rememberMe}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
So, I solved this task just by using an Observer, I am not sure if it is even possible to use a Two-way binding with AutoCompleteTextView here. Probably its because of SetOnItemClickListener has no built-in support for two-way data binding.
viewModel.autocompleteArray.observe(viewLifecycleOwner, Observer {
it?.let {
val adapter = ArrayAdapter<City>(
binding.autoCompleteTextView.context,
R.layout.support_simple_spinner_dropdown_item,
it)
binding.autoCompleteTextView.setAdapter(adapter)
binding.autoCompleteTextView.setOnItemClickListener { parent, view,
position, id ->
val s = parent.adapter.getItem(position) as City
viewModel.getWeatherProperties(s.id)
}
}
})
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
}
}