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>
View binding doesn't support layout variables or layout expressions, so it can't be used to declare dynamic UI content straight from XML layout files. View binding doesn't support two-way data binding.
Data Binding allows you to effortlessly communicate across views and data sources. This pattern is important for many Android designs, including model view ViewModel (MVVM), which is currently one of the most common Android architecture patterns.
In short you can use ViewBinding to replace findviewbyid() effectively. If your project is however more complex and you need to add features like binding data to views, binding adapters e.t.c, use DataBinding.
You can still use MVVM without data binding but you need some way to notify the view about data changes, It can be LiveData (preferred way), Java Observable, Rx or even a custom implementation.
I suggest to follow next basic principles:
LiveData
to deliver dataImplementation notes:
firstName
is read only on viewlastName
is editable on viewloadUser()
is not threadsafe save()
method until data is not loadedSuppose, 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;
}
}
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
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;
}
}
LiveData
to deliver datapublic 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]);
}
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With