jack_the_beast
jack_the_beast

Reputation: 1971

Android Architecture Components: bind to ViewModel

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

Answers (2)

Yurii Kot
Yurii Kot

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

elmorabea
elmorabea

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

Related Questions