New to Android development and I’m trying to wrap my head around two-way data binding in conjunction with RecyclerView, ViewModel, Room and LiveData. I grok one-way bindings, but can’t figure out two-way.
Simply, I’d like to be able to tap the id/switch_enabled Switch and update the Db to reflect this (I then plan to leverage this to update other members in the class/Db). I think I need some help with set(value) on my ViewModel and getting the correct RecyclerView item updated in the Db, but I’m uncertain how to do this or if this is the right or best way to do this.
Thank you.
Class:
data class Person (@ColumnInfo(name = "first_name") val firstName: String,
@ColumnInfo(name = "last_name") val lastName: String,
//...
val enabled: Boolean = true
){
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}
Layout detail for RecyclerView:
<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="p" type="com.example.data.Person" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraintLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/first_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{p.firstName}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="John" />
<TextView
android:id="@+id/last_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:text="@{' ' + p.lastName}"
app:layout_constraintStart_toEndOf="@id/first_name"
app:layout_constraintTop_toTopOf="parent"
tools:text=" Doubtfire" />
<Switch
android:id="@+id/switch_enabled"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="@={p.enabled}"
app:layout_constraintBaseline_toBaselineOf="@id/last_name"
app:layout_constraintEnd_toEndOf="parent" />
<!--...-->
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
ViewModel:
class MainViewModel(private val repository: DataRepository) : ViewModel() {
private val _people: LiveData<List<Person>>
// @Bindable?
// @get:Bindable?
var people: LiveData<List<Person>>
@Bindable
get() = _people
set(value) {
//Find out which member of the class is being changed and update the Db?
Log.d(TAG, "Value for set is $value!")
}
init {
_people = repository.livePeople()
}
}
Fragment:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val binding = FragmentPeopleBinding.inflate(inflater, container, false)
val context = context ?: return binding.root
val factory = Utilities.provideMainViewModelFactory(context)
viewModel = ViewModelProviders.of(requireActivity(), factory).get(MainViewModel::class.java)
val adapter = PeopleViewAdapter()
viewModel.people.observe(this, Observer<List<Person>> {
adapter.submitList(it)
})
binding.apply {
vm = viewModel
setLifecycleOwner(this@PeopleFragment)
executePendingBindings()
rvPeopleDetails.adapter = adapter
}
return binding.root
}
List Adapter:
class PeopleViewAdapter: ListAdapter<Person, PeopleViewAdapter.ViewHolder>(PeopleDiffCallback()) {
class PeopleDiffCallback : DiffUtil.ItemCallback<Person>() {
override fun areItemsTheSame(oldItem: Person, newItem: Person): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Person, newItem: Person): Boolean {
return oldItem.number == newItem.number
}
}
class ViewHolder(val binding: FragmentPeopleDetailBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(person: Person) {
binding.p = person
}
}
@NonNull
override fun onCreateViewHolder(@NonNull parent: ViewGroup, viewType: Int): ViewHolder =
ViewHolder(FragmentPeopleDetailBinding.inflate(LayoutInflater.from(parent.context), parent, false))
@NonNull
override fun onBindViewHolder(@NonNull holder: ViewHolder, position: Int) {
holder.apply {
bind(getItem(position))
}
}
}
I just ran into the same problem of setting up two way data binding within an MVVM architecture with a ViewModel and RecyclerView list. I determined that it was either not possible or not worth the effort to get two way binding working in this situation because you aren't directly using the viewmodel in the recyclerview item layout (the layout variable you're using is of type Person, not your viewmodel).
What I would suggest is actually adding your viewmodel as a layout variable, then using android:onClick="@{() -> viewmodel.onSwitchClicked()}"
and implementing that method within your viewmodel.
Check out the details in my project here: https://github.com/linucksrox/ReminderList
I came to the same conclusion that the best way is to provide the view model to the layout binding hosting the item that is displayed in the recycler view. I have created a general purpose solution for this scenario.
The adapter can be seen below and supports multiple type of layouts as well.
public abstract class ViewModelBaseAdapter<T extends Diffable, VM extends ViewModel>
extends ListAdapter<T, DoubleItemViewHolder<T, VM>> {
private final int itemVariableId;
private final int viewModelVariableId;
/**
* Constructor
*
* @param diffCallback the comparison strategy between items in {@code this} adapter
* @param variableId the variable in the data binding layout to set with the items
*/
public ViewModelBaseAdapter(int itemVariableId, int viewModelVariableId) {
super(new DiffUtil.ItemCallback<T>() {
@Override
public boolean areItemsTheSame(@NonNull Diffable oldItem,
@NonNull Diffable newItem) {
return oldItem.isSame(newItem);
}
@Override
public boolean areContentsTheSame(@NonNull Diffable oldItem,
@NonNull Diffable newItem) {
return oldItem.isContentSame(newItem);
}
});
this.itemVariableId = itemVariableId;
this.viewModelVariableId = viewModelVariableId;
}
@NonNull
@Override
public DoubleItemViewHolder<T, VM> onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ViewDataBinding binding = DataBindingUtil.inflate(
LayoutInflater.from(parent.getContext()), viewType, parent, false);
return new DoubleItemViewHolder<>(binding, itemVariableId, viewModelVariableId);
}
@Override
public void onBindViewHolder(@NonNull DoubleItemViewHolder<T, VM> holder, int position) {
holder.bind(getItem(position), getItemViewModel(position));
}
@Override
public abstract int getItemViewType(int position);
/**
* Provides the {@code ViewModel} to be bound together with the item at
* a specified position.
*
* @param position the position of the item
* @return the view model
*/
public abstract VM getItemViewModel(int position);
}
The interface and ViewHolder is defined as follows.
public interface Diffable {
boolean isSame(Diffable other);
boolean isContentSame(Diffable other);
}
public final class DoubleItemViewHolder<V1, V2> extends RecyclerView.ViewHolder {
private final ViewDataBinding binding;
private final int firstVariableId;
private final int secondVariableId;
/**
* Constructor
*
* @param binding the binding to use
* @param firstVariableId the first variable set on the binding
* @param secondVariableId the second variable set on the binding
*/
public DoubleItemViewHolder(ViewDataBinding binding,
int firstVariableId,
int secondVariableId) {
super(binding.getRoot());
this.binding = Objects.requireNonNull(binding);
this.firstVariableId = firstVariableId;
this.secondVariableId = secondVariableId;
}
/**
* Sets the data binding variables to the provided items
* and calls {@link ViewDataBinding#executePendingBindings()}.
*
* @param firstItem the first item to bind
* @param secondItem the second item to bind
* @throws NullPointerException if {@code firstItem} or {@code secondItem} is {@code null}
*/
public void bind(@NonNull V1 firstItem, @NonNull V2 secondItem) {
Objects.requireNonNull(firstItem);
Objects.requireNonNull(secondItem);
binding.setVariable(firstVariableId, firstItem);
binding.setVariable(secondVariableId, secondItem);
binding.executePendingBindings();
}
}
Now that the "boiler plate" has been set up it becomes simple to use.
The purpose of the example is to provide a complete answer including set up for anyone wishing to use this approach, it can be generalised very simply.
First the models are defined.
public class AppleModel implements Diffable {
// implementation...
}
public class DogModel implements Diffable {
// implementation...
}
Then we expose diffables in the view model like so.
private final MutableLiveData<List<Diffable>> diffables = new MutableLiveData<>();
public LiveData<List<Diffable>> getDiffables() {
return diffables;
}
And implement the adapter by overriding the ViewModelBaseAdapter.
public class ModelAdapter
extends ViewModelBaseAdapter<Diffable, MyViewModel> {
private final MyViewModel myViewModel;
public SalesmanHistoryAdapter(MyViewModel myViewModel) {
super(BR.item, BR.vm);
myViewModel = myViewModel;
}
@Override
public int getItemViewType(int position) {
final Diffable item = getItem(position);
if (item instanceof AppleModel) {
return R.layout.item_apple_model;
}
if (item instanceof DogModel) {
return R.layout.item_dog_model;
}
throw new IllegalArgumentException("Adapter does not support " + item.toString());
}
@Override
public MyViewModel getItemViewModel(int position) {
// You can provide different viewmodels if you like here.
return myViewModel;
}
}
Then you attach those items and the adapter to the recycler view in the layout.
<variable
name="adapter"
type="ModelAdapter" />
<variable
name="vm"
type="MerchantLogViewModel" />
<androidx.recyclerview.widget.RecyclerView
list_adapter="@{adapter}"
list_adapter_items="@{vm.diffables}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
They are attached using this binding adapter.
@BindingAdapter(value = {
"list_adapter",
"list_adapter_items"
})
public static <T> void setRecyclerViewListAdapterItems(RecyclerView view,
@NonNull ListAdapter<T, ?> adapter,
@Nullable final List<T> items) {
Objects.requireNonNull(adapter);
if (view.getAdapter() == null) {
view.setAdapter(adapter);
Timber.w("%s has no adapter attached so the supplied adapter was added.",
view.getClass().getSimpleName());
}
if (items == null || items.isEmpty()) {
adapter.submitList(new ArrayList<>());
Timber.w("Only cleared adapter because items is null");
return;
}
adapter.submitList(items);
Timber.i("list_adapter_items added %s.", items.toString());
}
Where your item layouts (only showing for the DogModel here but the same goes for AppleModel).
<?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="item"
type="DogModel" />
<variable
name="vm"
type="MyViewModel" />
</data>
<!-- Add rest below -->
Now you can use the view model a long with the item in the layout using data binding.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With