Ad

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 referenceSystem.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";
    }
}
Ad

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.

Ad
source: stackoverflow.com
Ad