Двустороннее связывание данных, RecyclerView, ViewModel, Room, LiveData, Oh My - PullRequest
0 голосов
/ 16 января 2019

Новичок в разработке для Android, и я пытаюсь обернуть голову вокруг двустороннего связывания данных в сочетании с RecyclerView, ViewModel, Room и LiveData. Я получаю односторонние привязки, но не могу понять двусторонние.

Проще говоря, я бы хотел иметь возможность нажать на переключатель id / switch_enabled и обновить Db, чтобы отразить это (затем я планирую использовать это для обновления других членов класса / Db). Я думаю, что мне нужна помощь с набором (значением) в моей ViewModel и обновлением правильного элемента 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 голосов
/ 22 мая 2019

Я также ищу хороший способ сохранения изменений, внесенных в элемент RecyclerView, с использованием двусторонней привязки, и лучшее решение, которое я нашел на данный момент, было в этом репо , который в основном состоит в добавлении переменная макета обратного вызова .

Я надеюсь, что это поможет, и хотел бы знать, есть ли лучшие решения там.

0 голосов
/ 09 августа 2019

Я пришел к такому же выводу, что наилучшим способом является предоставление модели представления для привязки макета, содержащей элемент, отображаемый в представлении переработчика. Я создал решение общего назначения для этого сценария.

Адаптер можно увидеть ниже и поддерживает несколько типов макетов.

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

Затем мы представляем в модели представления diffables следующим образом.

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

Теперь вы можете использовать модель представления long с элементом в макете, используя привязку данных.

0 голосов
/ 15 марта 2019

Я только что столкнулся с той же проблемой установки двухсторонней привязки данных в архитектуре MVVM со списком ViewModel и RecyclerView. Я решил, что в этой ситуации было невозможно или не стоит усилий, чтобы заставить работать двухстороннее связывание, потому что вы напрямую не используете viewmodel в макете элемента повторного просмотра (используемая переменная макета имеет тип Person, а не Ваша модель представления).

Я бы предложил добавить вашу модель представления в качестве переменной макета, затем использовать android:onClick="@{() -> viewmodel.onSwitchClicked()}" и реализовать этот метод в вашей модели представления.

Проверьте подробности в моем проекте здесь: https://github.com/linucksrox/ReminderList

...