1

I have a ViewModel that aims to control the state of an Activity or a Fragment. This ViewModel contains 4 ObservableBoolean that are used inside my layouts to tell which view has to be visible or not:

public class ContentLoaderViewModel extends BaseObservable {

    public static final int LOADING_LAYOUT = 0;
    public static final int CONTENT_LAYOUT = 1;
    public static final int NO_DATA_LAYOUT = 2;
    public static final int ERROR_LAYOUT = 3;

    public final ObservableBoolean loading = new ObservableBoolean();
    public final ObservableBoolean loaded = new ObservableBoolean();
    public final ObservableBoolean noDataFound = new ObservableBoolean();
    public final ObservableBoolean hasErrors = new ObservableBoolean();

    @Bindable
    public int displayedLayout;

    public ContentLoaderViewModel() {
        setDisplayedLayout(LOADING_LAYOUT);
    }

    public ContentLoaderViewModel(int displayedLayout) {
        setDisplayedLayout(displayedLayout);
   }

    public int getDisplayedLayout() {
        return displayedLayout;
    }

    public void setDisplayedLayout(int displayedLayout) {
        this.displayedLayout = displayedLayout;
        notifyPropertyChanged(BR.displayedLayout);

        loading.set(displayedLayout == LOADING_LAYOUT);
        loaded.set(displayedLayout == CONTENT_LAYOUT);
        noDataFound.set(displayedLayout == NO_DATA_LAYOUT);
        hasErrors.set(displayedLayout == ERROR_LAYOUT);
    }
}

I'm using it in one of my Fragment. This Fragment aims to give a simple interface to show a list. It is displaying a RecyclerView once an adapter has been created. The adapter is created asynchronously and then the RecyclerView is initialized. So at first the Fragment is in the state of "loading" and when it receives its adapter it change to the state of "content". Here is how it looks like:

XML

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

        <import type="android.view.View" />
        <variable
            name="viewModel"
            type="app.viewmodels.ContentLoaderViewModel" />
        <variable
            name="noDataText"
            type="String" />
        <variable
            name="bottomButtonText"
            type="String" />
        <variable
            name="showBottomButton"
            type="boolean" />

    </data>

    <android.support.constraint.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <Button
            android:text="@{bottomButtonText}"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:id="@+id/bottomButton"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            android:visibility="@{showBottomButton ? View.VISIBLE : View.GONE}"/>

        <ProgressBar
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:visibility="@{viewModel.loading ? View.VISIBLE : View.GONE}"
            tools:layout_constraintTop_creator="1"
            tools:layout_constraintRight_creator="1"
            tools:layout_constraintBottom_creator="1"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintRight_toRightOf="@+id/textView"
            tools:layout_constraintLeft_creator="1"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintLeft_toRightOf="@+id/textView"
            android:id="@+id/progressBar2" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{noDataText}"
            android:visibility="@{viewModel.noDataFound ? View.VISIBLE : View.GONE}"
            android:id="@+id/textView"
            tools:layout_constraintTop_creator="1"
            tools:layout_constraintRight_creator="1"
            app:layout_constraintRight_toRightOf="parent"
            android:layout_marginTop="16dp"
            tools:layout_constraintLeft_creator="1"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="@+id/progressBar2" />

        <android.support.v7.widget.RecyclerView
            android:id="@+id/list"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_marginBottom="8dp"
            android:visibility="@{viewModel.loaded ? View.VISIBLE : View.GONE}"
            app:layout_constraintBottom_toTopOf="@+id/bottomButton"
            app:layout_constraintHorizontal_bias="1.0"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0.0" />

        <include
            layout="@layout/error_layout"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_marginRight="8dp"
            app:layout_constraintRight_toRightOf="parent"
            android:layout_marginLeft="8dp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            android:layout_marginBottom="8dp"
            app:layout_constraintTop_toTopOf="parent"
            android:layout_marginTop="8dp"
            app:isVisible="@{viewModel.hasErrors}"/>


    </android.support.constraint.ConstraintLayout>

</layout>

JAVA

@FragmentWithArgs
public class BaseListFragment extends BaseFragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        inflateBinding(inflater, container);
        init(binding);
        return binding.getRoot();
    }

    protected int getLayoutId() {
        return R.layout.fragment_base_list;
    }

    protected void inflateBinding(LayoutInflater inflater, ViewGroup container) {
        binding = DataBindingUtil.inflate(inflater, getLayoutId(), container, false);
        onAttachToBinding(binding);
    }

    @Override
    protected void init(final FragmentBaseListBinding binding) {
        selectedItemPosition = -1;
        restoreState();
        initBinding(binding);
        refresh();
    }

    private void initBinding(FragmentBaseListBinding binding) {
        binding.setViewModel(new ContentLoaderViewModel());
        binding.setNoDataText(noDataText);
        binding.setBottomButtonText(bottomButtonText);
        binding.setShowBottomButton(showBottomButton);
    }

    public void refresh() {
        binding.getViewModel().setDisplayedLayout(ContentLoaderViewModel.LOADING_LAYOUT);
        Observable<RecyclerView.Adapter> observableAdapter = Observable.fromCallable(new Callable<RecyclerView.Adapter>() {
            @Override
            public RecyclerView.Adapter call() throws Exception {
                return hostAction.getAdapterAsync();
            }
        });

        Observer<RecyclerView.Adapter> observerAdapter = new Observer<RecyclerView.Adapter>() {
            @Override
            public void onCompleted() {

            }

            @Override
            public void onError(Throwable e) {
                e.printStackTrace();
                binding.getViewModel().setDisplayedLayout(ContentLoaderViewModel.ERROR_LAYOUT);
            }

            @Override
            public void onNext(RecyclerView.Adapter adapter) {
                initializeList(adapter);
            }
        };

        adapterSubscription = observableAdapter
                .subscribeOn((Schedulers.newThread()))
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(observerAdapter);
    }

    protected void initializeList(RecyclerView.Adapter adapter) {
        this.adapter = adapter;

        binding.list.setLayoutManager(hostAction.getLayoutManager());
        binding.list.setAdapter(getFinalAdapter());
        //Select first item by default
        if(selectedItemPosition >= 0 && selectedItemPosition < adapter.getItemCount())
            onItemClick(binding.list, null, selectedItemPosition, 0, null);

        //Set item decoration (simple line under each items)
        if(!hideDecorationItem)
            binding.list.addItemDecoration(new SimpleItemDecoration(getContext(), 0));

        binding.getViewModel().setDisplayedLayout(adapter.getItemCount() > 0
                ? ContentLoaderViewModel.CONTENT_LAYOUT
                : ContentLoaderViewModel.NO_DATA_LAYOUT
        );

        Log.d(TAG, "initializeList: " + "viewModel.displayLayout[" + binding.getViewModel().getDisplayedLayout() + "]," +
                " recyclerView.visibility[" + binding.list.getVisibility() + "], adapter.count[" + adapter.getItemCount() + "]");

        binding.executePendingBindings();
        binding.invalidateAll();

        Log.d(TAG, "initializeList: (after pendingBinding)" + "viewModel.displayLayout[" + binding.getViewModel().getDisplayedLayout() + "]," +
                " recyclerView.visibility[" + binding.list.getVisibility() + "], adapter.count[" + adapter.getItemCount() + "]");
    }

    protected RecyclerView.Adapter getFinalAdapter() {
        return getWrapAdapter(adapter);
    }

    @NonNull
    private WrapAdapter getWrapAdapter(RecyclerView.Adapter adapter) {
        WrapAdapter wrapAdapter = new WrapAdapter(adapter);
        wrapAdapter.setOnItemClickListener(binding.list, BaseListFragment.this);
        return wrapAdapter;
    }
}

And I'm pretty sure this comes of the Databinding because

  1. The 2 logs in initializeList() are showing that the adapter has items:

D/BaseListFragment: initializeList: viewModel.displayLayout[1], recyclerView.visibility[8], adapter.count[27]

D/BaseListFragment: initializeList: (after pendingBinding)viewModel.displayLayout[1], recyclerView.visibility[0], adapter.count[27]

(what is strange here is that after having executing pending binding and invalidating all, visibility is set to 0 -> View.VISIBLE but the RecyclerView is still not showing up)

  1. When I remove the line android:visibility="@{viewModel.loaded ? View.VISIBLE : View.GONE}" from the RecyclerView their is no issue and the RecyclerView is always visible.

Any idea of what is happening here?

UPDATE

WrapAdapter is a class that comes from an external library: https://github.com/eyeem/RecyclerViewTools/blob/master/library/src/main/java/com/eyeem/recyclerviewtools/adapter/WrapAdapter.java

MHogge
  • 5,408
  • 15
  • 61
  • 104
  • I tried to keep reference to the ViewModel instead of the binding but it does not seem to change anything. I changed the 1st line of `initBinding` into `binding.setViewModel(viewModel = new ContentLoaderViewModel());` and then changed everywhere 'binding.getViewModel()' by `viewModel` which is now a private variable of my `fragment`. Is it what you were suggesting? Plus, you can see in my update from where `WrapAdapter` comes from. And could you tell me what is the reason why it is better to keep reference of the `ViewModel` instead of the `DataBinding` object (just curiosity)? – MHogge Jun 13 '17 at 13:01

1 Answers1

-2

Changing visibility from GONE to INVISIBLE fix it for me. I just changed android:visibility="@{viewModel.loaded ? View.VISIBLE : View.GONE}" to android:visibility="@{viewModel.loaded ? View.VISIBLE : View.INVISIBLE}".

I do not need the RecyclerView to be GONE so by now it's ok but I'm still wondering what was the issue.

If someone understand the reason why GONE was not working but INVISIBLE is, please let a comment.

MHogge
  • 5,408
  • 15
  • 61
  • 104