Android passing query parameter to viewmodel - android

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
}
}

Related

Using switch in recyclerview with databinding

Here is what I am trying to do:
When the switch is toggled in the recyclerview
Update the item (alarm) which was toggled to the switch state (true or false) using my viewModel from the fragment (Passing the ID of the clicked item (alarm) to my fragment and update its status from the switch)
I, personally dont fancy the idea of having a reference of the viewModel in my adapter..but is that the way out..:(?
I am having difficulty implementing this with databinding.
recycler_view_item.xml
<data>
<variable
name="alarm"
type="com.package.appname.Alarm" />
<variable
name="fragment"
type="com.package.appname.ui.fragments.AlarmFragment" />
.................
<com.google.android.material.switchmaterial.SwitchMaterial
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:checked="#{alarm.status}"
android:onCheckedChanged="#{(button, bool)->
fragment.updateAlarmStatus(bool)}"
android:layout_marginEnd="#dimen/rcv_lyt_margin_spacing"/>
</data>
RecyclerViewAdapter.kt
class AlarmAdapter(private val onClickListener: OnAlarmClickListener) : ListAdapter<Alarm, AlarmAdapter.AlarmViewHolder>(AlarmDiffCallback()) {
class AlarmViewHolder private constructor(private val binding:
RcvLytAlarmsBinding) : RecyclerView.ViewHolder(binding.root){
fun bind(alarm: Alarm?){
binding.alarm = alarm
binding.executePendingBindings()
}
}
AlarmFragment.kt
fun updateAlarmStatus(status: Boolean){
//Here I want to use my viewmodel to update the selected alarm like
vieModel.updateAlarmStatus(alarmClicked)
Timber.d(status.toString()) <--- This code is not been called, IDK why
}

multiple views in binding adapter

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.

Visibility from an ObservableInt in a ViewModel not showing up

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.

Custom click event with android data binding

i want to set a certain action (like preventing multiple click) on every click event in data binding , in other phrase when a user click on each view, first do a specific action and after that do action relevant to clicked view(different for each view). How can I do this?
description: i implement MVVM and use databinding
This is what I do in this situation.
First: add onclick in your xml that call method on view model and pass it view
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"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="model"
type="....ViewModel" />
</data>
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:onClick="#{(v)-> model.onClick(v)}"/>
</layout>
Second: adding prevent double click with kotlin extensions
Kotlin:
fun View.preventDoubleClick() {
isClickable = false
Handler().postDelayed({ isClickable = true },500L)
}
Third:
Kotlin:
fun onClick(view: View?){
view?.preventDoubleClick()
}
now you have access to your view that clicked in view model.
remember make your view nullable. this help you when for example you want add unit test for your method you can just send view null.
First: create a mutableLiveData of type boolean in your SomeViewModel class with initial value to true
val data = MutableLiveData<Boolean>(true)
next in your xml
<data>
<variable
name="viewModel"
type="..SomeViewModel" />
</data>
<View
android:enabled = "#{viewModel.data}" // if working with button
android:clickable = "#{viewModel.data}" // for views which dont have enable tag
android:onClick="#{() -> viewModel.disableButtonAndPerformRequiredAction()}"/>
// In viewmodel
fun disableButtonAndPerformRequiredAction() {
data.value = false // it will disable the click for the view
// Perform other tasks
// post executing required task set
data.value = true // it will again enable the click for the view
}
So, today(2022) I had the same use case in one of my projects and i was able to figure out a way to implement custom click listeners for android views using data binding and custom adapters.
The use case is :
Click event should not be triggered twice or to prevent accidental clicks from the user
I created a file called ViewExtensions.kt and added the following code
class DebouncingOnClickListener(
private val intervalMillis: Long,
private val doClick: (() -> Unit)
) : View.OnClickListener {
override fun onClick(v: View) {
if (enabled) {
enabled = false
v.postDelayed(ENABLE_AGAIN, intervalMillis)
doClick()
}
}
companion object {
#JvmStatic
var enabled = true
private val ENABLE_AGAIN =
Runnable { enabled = true }
}
}
#BindingAdapter("singleClick")
fun View.setSingleClick(doClick: () -> Unit) =
setOnClickListener(
DebouncingOnClickListener(
intervalMillis = 5000, //5ms delay for click event
doClick = doClick
)
)
The debouncing click is used to defer the click for the given time, and in the xml called the click event like below
<androidx.appcompat.widget.AppCompatButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Click Me"
app:singleClick="#{()->fragment.clicked()}" />
Now I'm able to listen for click events on both fragment and in the viewmodel and the click is deferred for the given amount of time.
Hence the user cannot click the view accidentally multiple times.
References:
https://proandroiddev.com/ensure-single-click-on-android-butterknife-did-it-right-48ef56153c78

two way DataBinding not working properly in viewholder

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.

Categories

Resources