Reputation: 1971
I'm a bit confused about how data binding should work when using the new Architecture Components.
let's say I have a simple Activity with a list, a ProgressBar and a TextView. the Activity should be responsible for controlling the state of all the views, but the ViewModel should hold the data and the logic. For example, my Activity now looks like this:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
listViewModel = ViewModelProviders.of(this).get(ListViewModel.class);
binding.setViewModel(listViewModel);
list = findViewById(R.id.games_list);
listViewModel.getList().observeForever(new Observer<List<Game>>() {
@Override
public void onChanged(@Nullable List<Game> items) {
setUpList(items);
}
});
listViewModel.loadGames();
}
private void setUpList(List<Game> items){
list.setLayoutManager(new LinearLayoutManager(this));
GameAdapter adapter = new GameAdapter();
adapter.setList(items);
list.setAdapter(adapter);
}
and the ViewModel it's only responsible for loading the data and notify the Activity when the list is ready so it can prepare the Adapter and show the data:
public int progressVisibility = View.VISIBLE;
private MutableLiveData<List<Game>> list;
public void loadGames(){
Retrofit retrofit = GamesAPI.create();
GameService service = retrofit.create(GameService.class);
Call<GamesResponse> call = service.fetchGames();
call.enqueue(this);
}
@Override
public void onResponse(Call<GamesResponse> call, Response<GamesResponse> response) {
if(response.body().response.equals("success")){
setList(response.body().data);
}
}
@Override
public void onFailure(Call<GamesResponse> call, Throwable t) {
}
public MutableLiveData<List<Game>> getList() {
if(list == null)
list = new MutableLiveData<>();
if(list.getValue() == null)
list.setValue(new ArrayList<Game>());
return list;
}
public void setList(List<Game> list) {
this.list.postValue(list);
}
My question is: which is the correct way to show/hide the list, progressbar and error text?
should I add an Integer for each View in the ViewModel making it control the views and using it like:
<TextView
android:id="@+id/main_list_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.error}"
android:visibility="@{viewModel.errorVisibility}" />
or should the ViewModel instantiate a LiveData object for each property:
private MutableLiveData<Integer> progressVisibility = new MutableLiveData<>();
private MutableLiveData<Integer> listVisibility = new MutableLiveData<>();
private MutableLiveData<Integer> errorVisibility = new MutableLiveData<>();
update their value when needed and make the Activity observe their value?
viewModel.getProgressVisibility().observeForever(new Observer<Integer>() {
@Override
public void onChanged(@Nullable Integer visibility) {
progress.setVisibility(visibility);
}
});
viewModel.getListVisibility().observeForever(new Observer<Integer>() {
@Override
public void onChanged(@Nullable Integer visibility) {
list.setVisibility(visibility);
}
});
viewModel.getErrorVisibility().observeForever(new Observer<Integer>() {
@Override
public void onChanged(@Nullable Integer visibility) {
error.setVisibility(visibility);
}
});
I'm really struggling to understand that. If someone can clarify that, it would be great.
Thanks
Upvotes: 7
Views: 5039
Reputation: 326
Here are simple steps:
public class MainViewModel extends ViewModel {
MutableLiveData<ArrayList<Game>> gamesLiveData = new MutableLiveData<>();
// ObservableBoolean or ObservableField are classes from
// databinding library (android.databinding.ObservableBoolean)
public ObservableBoolean progressVisibile = new ObservableBoolean();
public ObservableBoolean listVisibile = new ObservableBoolean();
public ObservableBoolean errorVisibile = new ObservableBoolean();
public ObservableField<String> error = new ObservableField<String>();
// ...
// For example we want to change list and progress visibility
// We should just change ObservableBoolean property
// databinding knows how to bind view to changed of field
public void loadGames(){
GamesAPI.create().create(GameService.class)
.fetchGames().enqueue(this);
listVisibile.set(false);
progressVisibile.set(true);
}
@Override
public void onResponse(Call<GamesResponse> call, Response<GamesResponse> response) {
if(response.body().response.equals("success")){
gamesLiveData.setValue(response.body().data);
listVisibile.set(true);
progressVisibile.set(false);
}
}
}
And then
<data>
<import type="android.view.View"/>
<variable
name="viewModel"
type="MainViewModel"/>
</data>
...
<ProgressBar
android:layout_width="32dp"
android:layout_height="32dp"
android:visibility="@{viewModel.progressVisibile ? View.VISIBLE : View.GONE}"/>
<ListView
android:layout_width="32dp"
android:layout_height="32dp"
android:visibility="@{viewModel.listVisibile ? View.VISIBLE : View.GONE}"/>
<TextView
android:id="@+id/main_list_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.error}"
android:visibility="@{viewModel.errorVisibile ? View.VISIBLE : View.GONE}"/>
Also notice that it's your choice to make view observe
ObservableBoolean : false / true
// or
ObservableInt : View.VISIBLE / View.INVISIBLE / View.GONE
but ObservableBoolean is better for ViewModel testing.
Also you should observe LiveData considering lifecycle:
@Override
protected void onCreate(Bundle savedInstanceState) {
...
listViewModel.getList().observe((LifecycleOwner) this, new Observer<List<Game>>() {
@Override
public void onChanged(@Nullable List<Game> items) {
setUpList(items);
}
});
}
Upvotes: 6
Reputation: 3263
Here are simple steps to achieve your point.
First, have your ViewModel
expose a LiveData
object, and you can start the LiveData
with an empty value.
private MutableLiveData<List<Game>> list = new MutableLiveData<>();
public MutableLiveData<List<Game>> getList() {
return list;
}
Second, have your view (activity/fragment) observe that LiveData
and change UI accordingly.
listViewModel = ViewModelProviders.of(this).get(ListViewModel.class);
listViewModel.data.observe(this, new Observer<List<Game>>() {
@Override
public void onChanged(@Nullable final List<Game> games) {
setUpList(games);
}
});
Here it is important that you use the observe(LifecycleOwner, Observer)
variant so that your observer do NOT receive events after that LifecycleOwner
is no longer active, basically, that means that when your activity of fragment is no longer active, you won't leak that listener.
Third, as a result of data becoming available you need to update your LiveData
object.
@Override
public void onResponse(Call<GamesResponse> call, Response<GamesResponse> response) {
if(response.body().response.equals("success")){
List<Game> newGames = response.body().data; // Assuming this is a list
list.setValue(newGames); // Update your LiveData object by calling setValue
}
}
By calling setValue()
on your LiveData
, this will cause onChanged
on your view's listener to be called and your UI should be updated automatically.
Upvotes: 0