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)
}
}
})
Related
I have a ListView and trying to use two-way data binding to set the selectedItemPosition in a ViewModel using Two-Way Attributes
But the problem is it doesn't work, the selected item doesn't set in the Value of the liveData, I tried to observe it and the value never changes when i select an item in the listView
data binding in XML:
<ListView
android:id="#+id/listView"
android:layout_width="match_parent"
android:layout_height="#dimen/_150sdp"
android:nestedScrollingEnabled="true"
tools:listheader="#tools:sample/lorem"
tools:visibility="visible"
android:choiceMode="singleChoice"
android:selectedItemPosition="#={viewModel.chosenPosition}" />
in the ViewModel:
val chosenPosition = MutableLiveData<Int>()
in the Fragment:
binding.viewModel = viewModel
binding.lifecycleOwner = viewLifecycleOwner
binding.teamsListView.adapter = ArrayAdapter(
context,
R.layout.list_item_choice, teamsNames
)
viewModel.chosenPosition.observe(viewLifecycleOwner) {
Timber.d("chosen position = $it") //never triggers when I select an item in the ListView
}
Problem:
android:selectedItemPosition is triggered whenever an item is selected (this doesn't implicitly include that the item is clicked/checked.
Using android:selectedItemPosition as a two-way data binding in a ListView doesn't actually automatically triggered when an item is selected, and therefore the LiveData doesn't get triggered.
You can see that when you create a normal ListView without any data binding; when you click an item, this won't trigger the selection, notice the below when an item is clicked (nothing get highlighted with a different color):
Solution:
In order to solve that for the sake of data binding, you need to explicitly do select the that item whenever it is clicked by registering OnItemClickListener to the ListView:
binding.listView.setOnItemClickListener { _, _, position, _ ->
if (position >= 0) { // avoid -1 position calls
binding.listview.requestFocusFromTouch()
binding.listview.setItemChecked(position, true)
binding.listview.setSelection(position)
}
}
This way the live data will be set to the current selected position:
Notice when an item is selected, it's now highlighted with a light grey that is because the selection is enabled:
Make sure that you have not forgetten to set your binding lifecycleOwner in your fragment
binding.lifecycleOwner = this
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.
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 an app structured in MVVM. I have different fragments within the same activity. Each fragment has its own ViewModel and all data are retrieved from a REST API.
In FragmentA, there is a RecyclerView that lists X class instances. I want to set OnClickListener on the RecyclerView and I want to pass related X object to FragmentB when an item clicked in the RecyclerView. How can I achieve this?
How I imagine it is the following.
The Fragment passes a listener object to the adapter, which in turn passes it to the ViewHolders
Here is a quick sketch of how it should look like
class Fragment {
val listener = object: CustomAdapter.CustomViewHolderListener() {
override fun onCustomItemClicked(x: Object) {}
}
fun onViewCreated() {
val adapter = CustomAdapter(listener)
}
}
---------------
class CustomAdapter(private val listener: CustomViewHolderListener) {
val listOfXObject = emptyList() // this is where you save your x objects
interface CustomViewHolderListener{
fun onCustomItemClicked(x : Object)
}
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
holder.itemView.setOnClickListener {
listener.onCustomItemClicked(listOfXObject[position])
}
}
}
Here are some articles that might help you get the general gist of the things.
They don't answer your question directly though
Hope it is helpful
link 1 link 2
if you're using data binding you need to pass your view(which is Fragment in your case) into the layout via adapter class and you need to import your view in layout file to be able to call view's method
android:onClick="#{() -> view.onXXXClick(item)}"
pass your current model class which is item into this new method and then create onXXXClick method in your view and do whatever you wish.
if you will be doing view related operations such as navigation from one fragment to another or starting a service you should create above function in your view, if you're doing network or db related operations it should be in your ViewModel
you can check out my GitHub repository to understand better.
I have two variables inside my layout file :
<data>
<variable name="createExpenseViewModel" type="com.lionosur.dailyexpenses.viewModels.MainViewModel"/>
<variable name="createExpenseConverter" type="com.lionosur.dailyexpenses.converters.createExpenseActivityConverter.Companion"/>
</data>
My view model has an method to return the live data :
fun getAllExpenseItems(): LiveData<List<Expense>> {
return expenseRepository.getAllExpenseItems()
}
I need to observe this data and populate an spinner,
class createExpenseActivityConverter {
// contains all the static methods to convert the data for the ui
companion object {
fun getExpenseCategoryListFromSource(list:List<Source>):ArrayList<String> {
val categoryItems = ArrayList<String>()
categoryItems.addAll(list.map { it.sourceName })
return categoryItems
}
}
}
to populate a spinner I need to supply an array list of string
<Spinner
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="#+id/expense_category"
android:entries="#{()-> createExpenseViewModel.getAllSourceItems(1) }"
app:layout_constraintStart_toStartOf="#+id/textView"
android:layout_marginTop="20dp"
app:layout_constraintTop_toBottomOf="#+id/textView" app:layout_constraintWidth_percent="0.7"
/>
in android:entries I need to convert the observed data to array list of string, how do I pass the #{()-> createExpenseViewModel.getAllSourceItems(1) } result in to another static method createExpenseViewConverter.getExpenseCategoryListFromSource(sourceList) which would return a array list of string.
in my activity i have setup binding like this
binding = DataBindingUtil.setContentView(this, R.layout.activity_create_expense)
val mainViewModel = DaggerExpenseComponent.builder()
.setContext(this)
.build()
.getExpenseViewModel()
binding.setLifecycleOwner(this)
binding.createExpenseViewModel = mainViewModel
You'll need to use below syntax for that :
android:entries="#{createExpenseConverter.getExpenseCategoryListFromSource(createExpenseViewModel.getAllSourceItems(1))}"
Here, what we've done is accessed your input from MainViewModel object createExpenseViewModel using getAllSourceItems() method;
And then passing it to another class createExpenseActivityConverter object createExpenseConverter using method getExpenseCategoryListFromSource() which returns you ArrayList<String> that your spinner requires.
Edit:
When you use LiveData in DataBinding, Data-binding Compiler takes care of refreshing data just like ObservableFields. All you need to do is provide your LifeCycleOwner to your databinding object.
For Example:
If your activity has ViewDataBinding let's say mActivityBinding using which you provide your ViewModel to set LiveData in xml binding, then after setting your ViewModel consider setting LifecycleOwner like below code :
//Some Activity having data-binding
... onCreate() method of activity
mActivityBinding.setViewModel(myViewModel);
mAcivityBinding.setLifecycleOwner(this); // Providing this line will help you observe LiveData changes from ViewModel in data-binding.
...
Refer here