Android Architecture Components: bind to ViewModel
Asked Answered
B

2

7

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

Brittbritta answered 28/11, 2017 at 18:57 Comment(1)
Good Question.. i have a same confusion...Kanya
O
6

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);
        }
    });
}
Opinicus answered 8/12, 2017 at 0:44 Comment(5)
thank you for the answer, I'll try it asap. marked as accepted until I try. I've another question: that will cause will give the viewmodel direct control over the view, that shouldn't happen right?Brittbritta
Not really. ViewModel just desribes logic, while View observes all that's needed. And once View is not exists no NPE is thown. Did I understood your question correct? Oh, I'd done mistake in code, It should not contain setList() call, but just update LiveData inside ViewModel, and activity observes live data and calls setList methodOpinicus
more or less yes. still have to try to understand everything better :)Brittbritta
Where does onResponse come from? It would be good if you can elaborate this function.Fromm
@Fromm this method is questioners methodOpinicus
W
0

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.

Wakefield answered 5/12, 2017 at 13:41 Comment(2)
Bu that doesn't use data binding, am I wrong? Actually, the question is: how do I use data binding with architecture components?Brittbritta
If you want to use data binding, maybe you can use ObservableFeild instead of LiveData, but I don't see how combining them would be of value, since they do basically the same thing. LiveData has the advantage of being life cycle aware, I guessWakefield

© 2022 - 2024 — McMap. All rights reserved.