双向数据绑定,RecyclerView,ViewModel,Room,LiveData,噢,我的

时间:2019-01-15 21:14:50

标签: android-recyclerview android-room android-databinding android-livedata android-viewmodel

Android开发的新手,我想将双向数据绑定与RecyclerView,ViewModel,Room和LiveData结合起来。我查看了单向绑定,但是找不到双向绑定。

简而言之,我希望能够点一下id / switch_enabled开关并更新Db以反映这一点(然后,我打算利用它来更新class / Db中的其他成员)。我想我需要在ViewModel的set(value)上获得一些帮助,并在Db中更新正确的RecyclerView项,但是我不确定如何执行此操作,或者这是否是正确或最佳的方法。

谢谢。

班级:

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
}

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

片段:

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
}

列表适配器:

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

3 个答案:

答案 0 :(得分:2)

我只是遇到了一个同样的问题,即在带有ViewModel和RecyclerView列表的MVVM体系结构中设置双向数据绑定。我确定在这种情况下不可能或者无法进行双向绑定工作是因为您没有在recyclerview项目布局中直接使用viewmodel(您使用的布局变量是Person类型,而不是您的视图模型)。

我建议实际上是将viewmodel作为布局变量添加,然后使用android:onClick="@{() -> viewmodel.onSwitchClicked()}"并在viewmodel中实现该方法。

在以下位置查看我的项目中的详细信息:https://github.com/linucksrox/ReminderList

答案 1 :(得分:0)

我还在寻找一种使用双向绑定来持久保存RecyclerView项上所做的更改的好方法,到目前为止,我发现的最佳解决方案是在此repo中,该解决方案主要是添加一个{{ 3}}。

我希望能对您有所帮助,并想知道是否有更好的解决方案。

答案 2 :(得分:0)

我得出一个相同的结论,最好的方法是将视图模型提供给布局宿主,该布局宿主托管在回收站视图中显示的项目。我已经为这种情况创建了通用解决方案。

适配器可以在下面看到,并且还支持多种布局。

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

接口和ViewHolder的定义如下。

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();
    }
}

现在已经设置好“锅炉板”,使用起来变得简单。

示例

该示例的目的是提供一个完整的答案,包括为希望使用此方法的任何人进行设置,可以很简单地将其概括。

首先定义模型。

public class AppleModel implements Diffable {
    // implementation...
}

public class DogModel implements Diffable {
    // implementation...
}

然后我们像这样暴露视图模型中的可变量。

private final MutableLiveData<List<Diffable>> diffables = new MutableLiveData<>();

public LiveData<List<Diffable>> getDiffables() {

    return diffables;
}

并通过重写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;
    }
}

然后将这些项目和适配器附加到布局中的回收者视图。

<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" />

它们是使用此绑定适配器连接的。

@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());
}

项目布局的位置(此处仅显示DogModel,而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 -->

现在,您可以使用数据绑定将视图模型与布局中的项目一起使用很长时间了。