MVVM LiveData и DataBinding
Я работаю над компонентами архитектуры и сейчас изучаю LiveData и привязку данных. Я могу связать LiveData с макетом для целочисленных значений, но я не могу найти для списка пользователей.
Это рабочий пример для LiveData с привязкой данных
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable
name="viewModel"
type="com.example.tutorial3livedataanddatabinding.viewmodel.CounterViewModel" />
</data>
<android.support.constraint.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{String.valueOf(viewModel.counter)}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
</layout>
Класс ViewModel и активность
public class CounterViewModel extends ViewModel {
private MutableLiveData<Integer> counter = new MutableLiveData<>();
public MutableLiveData<Integer> getCounter() {
return counter;
}
public void setCounter(MutableLiveData<Integer> counter) {
this.counter = counter;
}
public void counterValue(int val) {
this.counter.setValue(val);
}
}
public class MainActivity extends AppCompatActivity {
private int mCounter = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
/*
LifeCycleOwner should be set for LiveData changes to be propagated to UI for this binding
*/
binding.setLifecycleOwner(this);
final CounterViewModel counterViewModel = ViewModelProviders.of(this).get(CounterViewModel.class);
binding.setViewModel(counterViewModel);
increaseCounter(counterViewModel);
}
private void increaseCounter(final CounterViewModel counterViewModel) {
final Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
mCounter ++;
counterViewModel.counterValue(mCounter);
handler.postDelayed(this, 1000);
}
}, 1000);
}
}
Для примера выше связывание данных с данными работает нормально. Счетчик обновлений, но, в отличие от документа, он не прекращает подсчет, пока приложение остановлено или LiveData
Примечание. Объекты LiveData отправляют обновления только тогда, когда активна активность или LifecycleOwner. Если вы переходите в другое приложение, сообщения журнала приостанавливаются, пока вы не вернетесь. Объекты LiveData считают подписки активными только тогда, когда их соответствующий владелец жизненного цикла либо НАЧИНАЕТСЯ, либо ВОЗОБНОВЛЯЕТСЯ.
Мой реальный вопрос заключается в том, как реализовать связывание данных с LiveData<List<User>>
учебный класс.
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable
name="viewModel"
type="com.example.tutorial3livedataanddatabinding2.viewmodel.MyViewModel"/>
</data>
<android.support.constraint.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.tutorial3livedataanddatabinding2.MainActivity">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.getUserRecords()}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.349" />
<Button
android:id="@+id/button_shufle"
android:layout_width="88dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="20dp"
android:onClick="@{() -> viewModel.shuffleUsers()}"
android:text="Shuffle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
<Button
android:id="@+id/button_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:onClick="@{() -> viewModel.addUser()}"
android:text="Add"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button_shufle" />
</android.support.constraint.ConstraintLayout>
</layout>
android: text = "@ {viewModel.getUserRecords () привязывает данные пользователя как String при запуске, но не обновляет их при добавлении нового пользователя или перетасовке списка, если устройство не поворачивается.
public class MyViewModel extends ViewModel {
private MutableLiveData<List<User>> users;
public LiveData<List<User>> getUsers() {
if (users == null) {
users = new MutableLiveData<>();
loadUsers();
}
return users;
}
private void loadUsers() {
// do async operation to fetch users
List<User> userList = new ArrayList<>();
User user = new User();
user.setFirstName("Jack");
user.setLastName("Daniels");
userList.add(user);
user = new User();
user.setFirstName("Johnny");
user.setLastName("Walker");
userList.add(user);
user = new User();
user.setFirstName("James");
user.setLastName("Jameson");
userList.add(user);
user = new User();
user.setFirstName("Arthur");
user.setLastName("Guinness");
userList.add(user);
users.setValue(userList);
}
public void shuffleUsers() {
if (users != null && users.getValue() != null && users.getValue().size() > 0) {
Collections.shuffle(users.getValue());
// Needed to update Live Data observers
users.setValue(users.getValue());
}
}
public void addUser() {
if (users != null && users.getValue() != null) {
User user = new User();
user.setFirstName("Winston");
user.setLastName("Whiskey");
users.getValue().add(user);
// Needed to update Live Data observers
users.setValue(users.getValue());
}
}
public String getUserRecords() {
if (users != null && users.getValue() != null && users.getValue().size() > 0) {
StringBuilder sb = new StringBuilder();
for (User user : users.getValue()) {
sb.append( user.getFirstName() + " " + user.getLastName() + "\n");
}
return sb.toString();
}
return "empty list";
}
}
Деятельность
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MyViewModel myViewModel = ViewModelProviders.of(MainActivity.this).get(MyViewModel.class);
myViewModel.getUsers();
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.setViewModel(myViewModel);
// Required for binding with LiveData
binding.setLifecycleOwner(this);
}
}
РЕДАКТИРОВАТЬ: я редактировал класс MyViewModel после ответа Blackbelt Теперь я получаю Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.arch.lifecycle.LiveData.observeForever(android.arch.lifecycle.Observer)' on a null object reference
System.out.println()
внутри apply()
не вызывается, другие 2 запускаются и не возвращают ноль для users
а также users.getValue()
import android.arch.core.util.Function;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData;
import android.arch.lifecycle.Transformations;
import android.arch.lifecycle.ViewModel;
import com.example.tutorial3livedataanddatabinding2.model.User;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class MyViewModel extends ViewModel {
private MutableLiveData<List<User>> users;
public LiveData<String> userStringLiveData = Transformations.map(users, new Function<List<User>, String>() {
@Override
public String apply(List<User> input) {
System.out.println("userStringLiveData users: " + users);
return getUserRecords();
}
});
public LiveData<List<User>> getUsers() {
if (users == null) {
users = new MutableLiveData<>();
loadUsers();
}
return users;
}
private void loadUsers() {
// do async operation to fetch users
List<User> userList = new ArrayList<>();
User user = new User();
user.setFirstName("Jack");
user.setLastName("Daniels");
userList.add(user);
user = new User();
user.setFirstName("Johnny");
user.setLastName("Walker");
userList.add(user);
user = new User();
user.setFirstName("James");
user.setLastName("Jameson");
userList.add(user);
user = new User();
user.setFirstName("Arthur");
user.setLastName("Guinness");
userList.add(user);
users.setValue(userList);
System.out.println("MyViewModel users " + users);
System.out.println("MyViewModel users List" + users.getValue());
}
public void shuffleUsers() {
if (users != null && users.getValue() != null && users.getValue().size() > 0) {
Collections.shuffle(users.getValue());
// Needed to update Live Data observers
users.setValue(users.getValue());
}
}
public void addUser() {
if (users != null && users.getValue() != null) {
User user = new User();
user.setFirstName("Winston");
user.setLastName("Whiskey");
users.getValue().add(user);
// Needed to update Live Data observers
users.setValue(users.getValue());
}
}
public String getUserRecords() {
if (users != null && users.getValue() != null && users.getValue().size() > 0) {
StringBuilder sb = new StringBuilder();
for (User user : users.getValue()) {
sb.append(user.getFirstName() + " " + user.getLastName() + "\n");
}
return sb.toString();
}
return "empty list";
}
}
2 ответа
Похоже, что пользователи будут нулевыми в момент создания объекта, когда пользователь впервые оценивает userStringLiveData. Таким образом, Transformations.map будет передано значение NULL в качестве первого параметра, а не LiveData, с которым вы хотите, чтобы он работал позже.
Вот моя рекомендация:
public class r_RealTulipCoin {
private LiveData<List<User>> users;
public LiveData<String> userStringLiveData;
public void onCreate() {
getUsersLiveDataFromRoomOrSomething();
prepareUserStringLiveData();
}
private void prepareUserStringLiveData() {
userStringLiveData = Transformations.map(users, new android.arch.core.util.Function<List<User>, String>() {
@Override
public String apply(List<User> input) {
System.out.println("userStringLiveData users: " + users);
return getUserRecordsAsSingleStringFromThisParticularList(input);
}
});
}
private String getUserRecordsAsSingleStringFromThisParticularList(List<User> a_list_of_users) {
return "i concatenated them or something";
}
private class User {
String a_name_or_something;
}
}
Это макет, но я надеюсь, что он иллюстрирует мою точку зрения. Вы заметите, что я сделал пару вещей... Я проверяю, что пользователи на самом деле не равны NULL, прежде чем я попытаюсь передать его в Transformations.map. Я использую входной параметр, который обеспечивает карта. Я ценю, что вы уже знаете, к чему это относится, поэтому ваш код использует поле пользователя, но размышления о том, откуда он берется и что пытается сделать класс Transformations, могут вам помочь.
В вашем случае я бы порекомендовал вам сделать это:
public LiveData<List<User>> getUsers() {
if (users == null) {
users = new MutableLiveData<>();
// this is the first time the users pointer is not null
loadUsers();
prepareUserStringLiveData();
}
return users;
}
Последний вариант вместо этого - вернуться к исходному коду и использовать getUsers() вместо пользователей при первом создании Transform:
public LiveData<String> userStringLiveData = Transformations.map(getUsers(), blah...)
Это не самое удачное решение, но опять же - оно иллюстрирует, почему я продолжаю настаивать на том, что вы фактически передаете null в Transformations.map.
В android:text="@{viewModel.getUserRecords()}
getUserRecords()
возвращает String
не LiveData
объект. То, что вы должны это привязать к LiveData<String>
объект вместо
Например
public LiveData<String> userStringLiveData = Transformations.map(getUsers(), new Function<List<User>, String>() {
@Override
public String apply(List<User> input) {
return getUserRecords();
}
});
и в вашем макете android:text="@{viewModel.userStringLiveData}
, Таким образом, когда вы публикуете новое значение для пользователей, будет вызвана карта и обновлен пользовательский интерфейс.