20

I am trying to learn ViewModel in android, in my first phase of learning I am trying to update UI (TextView) by using ViewModel and DataBinding. In ViewModel, I have an AsyncTask callback and it will invoke REST API call. I am getting the response from API call but the value in textview is not getting updated.

my ViewModel class:

public class ViewModelData extends ViewModel {

    private MutableLiveData<UserData> users;

    public LiveData<UserData> getUsers() {
        if (users == null) {
            users = new MutableLiveData<UserData>();
            loadUsers();
        }

        return users;
    }

    public void loadUsers() {
        ListTask listTask =new ListTask (taskHandler);
        listTask .execute();

    }

    public Handler taskHandler= new Handler() {
        @Override
        public void handleMessage(Message msg) {


            UserData  userData = (UserData) msg.obj;
        
            users.setValue(userData);
        }
    };
}

and my MainActivity class:

public class MainActivity extends AppCompatActivity implements LifecycleOwner {
    private LifecycleRegistry mLifecycleRegistry;
    private TextView fName;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        fName = (TextView)findViewById(R.id.text_name);
        mLifecycleRegistry = new LifecycleRegistry(this);
        mLifecycleRegistry.markState(Lifecycle.State.CREATED);
        ViewModelData model = ViewModelProviders.of(this).get(ViewModelData.class);
        model.getUsers().observe(this, new Observer<UserData>() {
            @Override
            public void onChanged(@Nullable UserData userData) {
                Log.d("data"," =  - - - - ="+userData.getFirstName());

            }
        });

    }

    @Override
    public Lifecycle getLifecycle() {
        return mLifecycleRegistry;
    }
}

and my data class:

public class UserData extends BaseObservable{
    private String firstName ;
@Bindable
    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
        notifyPropertyChanged(BR.firstName);
    }
}

and layout file

<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <import type="android.view.View" />
        <variable name="data" type="com.cgi.viewmodelexample.UserData"/>
    </data>
    <RelativeLayout
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/activity_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        tools:context="com.cgi.viewmodelexample.MainActivity">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{data.firstName}"
            android:id="@+id/text_name"/>
    </RelativeLayout>
</layout>
mahdi
  • 598
  • 5
  • 22
karthik selvaraj
  • 467
  • 5
  • 8
  • 21

3 Answers3

30

I suggest to follow next basic principles:

  • don't overload data objects by business or presentation logic
  • only view model required to obtain data in presentation layer
  • view model should expose only ready to use data to presentation layer
  • (optional) background task should expose LiveData to deliver data

Implementation notes:

  • firstName is read only on view
  • lastName is editable on view
  • loadUser() is not threadsafe
  • we have error message when call save() method until data is not loaded

Don't overload data objects by business or presentation logic

Suppose, we have UserData object with first and last name. So, getters it's (usually) all what we need:

public class UserData {

    private String firstName;
    private String lastName;

    public UserData(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }
}

Only view model required to obtain data in presentation

To follow this suggestion we should to use only view model in data binding layout:

<?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"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context="com.example.vmtestapplication.MainActivity">

    <data>

        <import type="android.view.View" />

        <!-- Only view model required -->
        <variable
            name="vm"
            type="com.example.vmtestapplication.UserDataViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:animateLayoutChanges="true"
        android:orientation="vertical">

        <!-- Primitive error message -->
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{vm.error}"
            android:visibility="@{vm.error == null ? View.GONE : View.VISIBLE}"/>

        <!-- Read only field (only `@`) -->
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{vm.firstName}" />

        <!-- Two-way data binding (`@=`) -->
        <EditText
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@={vm.lastName}" />

    </LinearLayout>
</layout>

Note: you can use a few view models in one layout, but not raw data

View model should expose only ready to use data to presentation

This mean, you shouldn't to expose complex data objects (UserData in our case) directly from view model. Preferable to expose privative types which view can use as-is. In example below we don't need to hold UserData object because it used only to loading grouped data. We, probably, need to create UserData to save it but it depends on your repository implementation.

public class UserDataViewModel extends ViewModel {

    private ListTask loadTask;

    private final MutableLiveData<String> firstName = new MediatorLiveData<>();
    private final MutableLiveData<String> lastName = new MediatorLiveData<>();
    private final MutableLiveData<String> error = new MutableLiveData<>();

    /**
     * Expose LiveData if you do not use two-way data binding
     */
    public LiveData<String> getFirstName() {
        return firstName;
    }

    /**
     * Expose MutableLiveData to use two-way data binding
     */
    public MutableLiveData<String> getLastName() {
        return lastName;
    }

    public LiveData<String> getError() {
        return error;
    }

    @MainThread
    public void loadUser(String userId) {
        // cancel previous running task
        cancelLoadTask();
        loadTask = new ListTask();
        Observer<UserData> observer = new Observer<UserData>() {
            @Override
            public void onChanged(@Nullable UserData userData) {
                // transform and deliver data to observers
                firstName.setValue(userData == null? null : userData.getFirstName());
                lastName.setValue(userData == null? null : userData.getLastName());
                // remove subscription on complete
                loadTask.getUserData().removeObserver(this);
            }
        };
        // it can be replaced to observe() if LifeCycleOwner is passed as argument
        loadTask.getUserData().observeForever(observer);
        // start loading task
        loadTask.execute(userId);
    }

    public void save() {
        // clear previous error message
        error.setValue(null);
        String fName = firstName.getValue(), lName = lastName.getValue();
        // validate data (in background)
        if (fName == null || lName == null) {
            error.setValue("Opps! Data is invalid");
            return;
        }
        // create and save object
        UserData newData = new UserData(fName, lName);
        // ...
    }

    @Override
    protected void onCleared() {
        super.onCleared();
        cancelLoadTask();
    }

    private void cancelLoadTask() {
        if (loadTask != null)
            loadTask.cancel(true);
        loadTask = null;
    }
}

Background task should expose LiveData to deliver data

public class ListTask extends AsyncTask<String, Void, UserData> {

    private final MutableLiveData<UserData> data= new MediatorLiveData<>();

    public LiveData<UserData> getUserData() {
        return data;
    }

    @Override
    protected void onPostExecute(UserData userData) {
        data.setValue(userData);
    }

    @Override
    protected UserData doInBackground(String[] userId) {
        // some id validations
        return loadRemoiteUser(userId[0]);
    }
}

MainActivity.java

public class MainActivity extends AppCompatActivity {

    private UserDataViewModel viewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // get view model
        viewModel = ViewModelProviders.of(this).get(UserDataViewModel.class);
        // create binding
        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        // set view model to data binding
        binding.setVm(viewModel);
        // don't forget to set LifecycleOwner to data binding
        binding.setLifecycleOwner(this);

        // start user loading (if necessary)
        viewModel.loadUser("user_id");
        // ...
    }
}

PS: try to use RxJava library instead of AsyncTask to perform background work.

Sergei Bubenshchikov
  • 5,275
  • 3
  • 33
  • 60
  • binding.setLifecycleOwner(this); i am not able to use this Method, is there any alternative method is available and and i am getting data Binding error too while doing rebuild (its thorws DataBinding is exist.). – karthik selvaraj Oct 30 '18 at 13:02
  • @karthikselvaraj which version of Android Studio and gradle plugin do you use? Can you post original error message or full stack trace? – Sergei Bubenshchikov Oct 31 '18 at 03:56
  • 1
    Wow, the comment `Expose MutableLiveData to use two-way data binding` was really helpful!! Thanks! – Tanasis Nov 28 '18 at 20:59
  • What would be the best recommendation for an app that has user authentication and then showing bookings by the user. So Login/Signup pages will have to interact with UserRepo and Dashboard page has to interact with UserRepo and BookingsRepo, also Profile page just need to deals with UserRepo? – Suyash Dixit Jan 25 '20 at 06:46
  • Also, I have seen posts where they have something like fun onLoginClick(view: View){} inside viewmodel and then bind it with XML. But somewhere else I read that View should never be passed to ViewModel. I am confused! – Suyash Dixit Jan 25 '20 at 06:51
  • @SuyashDixit with `onLoginClick(view: View){}` you can use [method reference binding](https://developer.android.com/topic/libraries/data-binding/expressions#method_references) instead of [listener binding](https://developer.android.com/topic/libraries/data-binding/expressions#listener_bindings). It makes your binding code shorter but no more. You still shouldn't store links of view inside of view model. Moreover, this view argument usually ignored – Sergei Bubenshchikov Jan 26 '20 at 07:28
0

You'll require to notify observer when you set value like this :

public class UserData extends BaseObservable{
private String firstName ;
@Bindable
public String getFirstName() {
    return firstName;
}

public void setFirstName(String firstName) {
    this.firstName = firstName;
    notifyPropertyChanged(BR.firstName) // call like this
}
}
Jeel Vankhede
  • 11,592
  • 2
  • 28
  • 58
  • 1
    because of data binding, you've set **UserData** as variable in xml. so any changes related to **UserData** will reflect to **UI through DataBinding**. – Jeel Vankhede Oct 19 '18 at 10:23
0

If you want binding layout to work then you have to set your view in binding way. Also set data in binding class.

public class MainActivity extends AppCompatActivity implements LifecycleOwner {
    ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        ...
        ViewModelData model = ViewModelProviders.of(this).get(ViewModelData.class);
        ...
        binding.setData(model.getUsers());
    }
}
Khemraj Sharma
  • 57,232
  • 27
  • 203
  • 212
  • can you explain little bit more ? i have changed in layout file and bind the viewModel in mainclass (like what you mentioned in code) even though i didn't get this. – karthik selvaraj Oct 22 '18 at 13:31
  • Are you setting `notifyPropertyChanged` in `setFirstName()` – Khemraj Sharma Oct 22 '18 at 13:33
  • yes, i have used " notifyPropertyChanged(BR.firstName);" – karthik selvaraj Oct 22 '18 at 13:38
  • I could only see above issue in your code, now it should work as expected. How are you setting data when response come? – Khemraj Sharma Oct 22 '18 at 15:16
  • i will receive the data in handler and by using setvalue will set data(all this things done in ViewModel class). now i am getting an compile time error while doing binding "binding.setData(model.getUsers());" . i tried smae what you metioned in your code. – karthik selvaraj Oct 23 '18 at 06:18