MVVM LiveData And DataBinding
I'm working on architecture components and now i'm learning LiveData and data binding. I'm able to bind LiveData with layout for Integer values but i'm not able to find for List of Users.
This one is a working example for LiveData with data binding
<?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 class and Activity
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);
}
}
For the example above data binding with data works fine. Counter updates but unlike the document states it does not stop counting while app is stopped or LiveData
Note: LiveData objects only send updates when the activity, or the LifecycleOwner is active. If you navigate to a different app, the log messages pause until you return. LiveData objects only consider subscriptions as active when their respective lifecycle owner is either STARTED or RESUMED.
My real question is how to implement Data Binding with a LiveData<List<User>>
class.
<?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() binds user data as String at start but it does not update when new a new user is added or list is shuffled unless device is rotated.
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";
}
}
Activity
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);
}
}
EDIT: I edited MyViewModel class after Blackbelt's answer
Now i'm getting 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()
inside apply()
is not invoked, other 2 gets invoked and does not return null for users
and 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";
}
}
Answer
It looks a lot like users will be null at the moment of object creation, which is when userStringLiveData is first evaluated. So Transformations.map will be passed a null as its first parameter, rather than the LiveData you want it to operate on later.
Here's my recommendation:
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;
}
}
It's a mockup, but I hope it illustrates my point. You'll note I've done a couple of things... I make sure that users is actually non-null before I ever try to pass it into Transformations.map. I use the input parameter that map provides. I appreciate that you already know what it refers to which is why your code falls back on using the users field, but thinking about where it comes from and what the Transformations class is trying to do might help you.
In your case, I'd recommend you do this:
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;
}
One final option instead of that would be to go back to your original code and use getUsers() instead of users when you first construct the Transform:
public LiveData<String> userStringLiveData = Transformations.map(getUsers(), blah...)
It's not the neatest solution, but again - it illustrates why I keep insisting that you're actually passing null into Transformations.map.
Related Questions
- → should I choose reactjs+f7 or f7+vue.js?
- → Phonegap Android write to sd card
- → Local reference jquery script in nanohttpd (Android)
- → Click to navigate on mobile devices
- → How to allow api access to android or ios app only(laravel)?
- → Access the Camera and CameraRoll on Android using React Native?
- → React native change listening port
- → What is the default unit of style in React Native?
- → Google play market autocomplete icon
- → Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of `ListView`
- → Using Laravel with Genymotion
- → react native using like web-based ajax function
- → react native pdf View