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
- 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)
- When I remove the line
android:visibility="@{viewModel.loaded ? View.VISIBLE : View.GONE}"
from theRecyclerView
their is no issue and theRecyclerView
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