Only update RecyclerView when a change in the dataset occurs - android

I have a dataset of restaurants and a recycler view where they are displayed. Depending on a few options, they should or not be visible: opening time, food type, etc.
Right now every time the activity with the recycler view is opened I run adapter.updateDataset() which internally goes through the whole dataset, creates a subset based on all the possible filters, and then does notifyDataSetChanged().
How can I make it so that I only need to run adapter.updateDataset() when a change actually occurs? Since these changes occur in a different context from the RecyclerView activity, I can't just call the function there. What alternative do I have, to improve performance?

You should probably use a list of LiveData objects either from room or a network resource and bind it your viewmodel. Then you will be observing the changes in your fragment/activity. When the change occurs, update the adapters data list and do not forget to use DiffUtil in order to update only changed items. A good example is in google sample codes on room database usage.
In your Room Dao query it should be like:
#Query("SELECT * FROM products")
LiveData<List<ProductEntity>> loadProducts();
Then in your viewmodel:
public class ProductListViewModel extends AndroidViewModel {
// MediatorLiveData can observe other LiveData objects and react on their emissions.
private final MediatorLiveData<List<ProductEntity>> observableProducts;
public ProductListViewModel(#NonNull Application application) {
super(application);
observableProducts = new MediatorLiveData<>();
// set by default null, until we get data from the database.
observableProducts.setValue(null);
LiveData<List<ProductEntity>> products = ((YourBaseApp) application).getRepository()
.loadProducts();
observableProducts.addSource(products, observableProducts::setValue);
}
public static class Factory extends ViewModelProvider.NewInstanceFactory {
#NonNull
private final Application mApplication;
public Factory(#NonNull Application application) {
mApplication = application;
}
#Override
public <T extends ViewModel> T create(Class<T> modelClass) {
//noinspection unchecked
return (T) new ProductListViewModel(mApplication);
}
}
public LiveData<List<ProductEntity>> getProductList() {
return observableProducts;
}
}
Then in your activity/fragment onCreate you may call such a sample function and start observing your data:
#Nullable
#Override
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
// Binding is of type ProductListLayoutBinding
// you need to declare it on tope of your fragment
binding = DataBindingUtil.inflate(inflater, R.layout.product_list_layout, container, false);
// your other stuff if needed..
productAdapter = new ProductAdapter(/*...Your parameters if any*/);
binding.yourRecylerViewId.setAdapter(productAdapter);
return binding.getRoot();
}
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
//...
//...
// do your normal stuff above
ProductListViewModel.Factory factory = new ProductListViewModel.Factory(
YourBaseApp.getInstance());
final ProductListViewModel viewModel =
new ViewModelProvider(this, factory).get(ProductListViewModel.class);
subscribeUi(viewModel);
}
private void subscribeUi(ProductListViewModel viewModel) {
// Update the list when the data changes
viewModel.getProductList().observe(this, new Observer<List<ProductEntity>>() {
#Override
public void onChanged(#Nullable List<ProductEntity> myProducts) {
if (myProducts != null) {
if (myProducts.size() == 0) {
binding.setIsLoading(true);
} else {
binding.setIsLoading(false);
productAdapter.setProductList(myProducts);
}
} else {
binding.setIsLoading(true);
}
binding.executePendingBindings();
}
});
}
Finally on your adapter:
public void setProductList(final List<? extends Product> inProductList) {
if (productList == null) {
productList = inproductList;
notifyItemRangeInserted(0, productList.size());
} else {
DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() {
#Override
public int getOldListSize() {
return productList.size();
}
#Override
public int getNewListSize() {
return inproductList.size();
}
#Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return productList.get(oldItemPosition).getId() == inproductList.get(newItemPosition).getId();
}
#Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
productList newProduct = inproductList.get(newItemPosition);
productList oldProduct = productList.get(oldItemPosition);
return newProduct.getId() == oldProduct.getId()
&& Objects.equals(newProduct.getDefinition(), oldProduct.getDefinition())
//... compare other properties
//...
;
}
});
productList = inproductList;
result.dispatchUpdatesTo(this);
}
}
Hope , this helps.

Related

Flowable<List<T>> changes to empty List on Fragment switching back

I'm trying to populate a RecyclerView by following the nexts steps:
Download data from server and getting a SoapObject (yah, old server)
Transform the data to Flowable<List<MyItem>> (in Repository) in order to subscribe to it (in ViewModel) through LiveDataStreams.fromPublisher(flowableObj)
Set the resulted list into a MediatorLiveData object.
Observe the MediatorLiveData object in the Fragment's onViewCreated method.
So, when I click an on item from the list, it navigates (through Navigation Component) to a new Fragment, but, once I go back through the phone's back button, the list becomes empty and consequently the observer is notified and updates the list (shows nothing cause is empty).
I don't know why, the list gets empty and therefore the RecyclerView. Any help? -- code below:
Generic Fragment
public abstract class ListFragment<T> extends Fragment {
protected ListViewModel mViewModel;
#Override
public void onViewCreated(#NonNull View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mNavController = Navigation.findNavController(getDataBindingObject().getRoot());
showItemsList();
setUpFilters();
}
protected void showItemsList() {
mViewModel.getList().observe(getViewLifecycleOwner(), listObserver);
mViewModel.getItemSelected().observe(getViewLifecycleOwner(), onListItemSelected());
}
protected final Observer<List<T>> listObserver = new Observer<List<T>>() {
#Override
public void onChanged(List<T> list) {
mViewModel.setListAdapter(list);
}
};
MyItem Fragment's code:
#Override
public View onCreateView(#NotNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
mViewModel = new ViewModelProvider(this, new ViewModelFactory()).get(MyItemViewModel.class);
mDataBinding = ...
mDataBinding.setLifecycleOwner(this); //geViewLifecycleOwner()
mDataBinding.setViewModel(mViewModel);
return mDataBinding.getRoot();
}
#Override
public void onViewCreated(#NonNull #NotNull View view, #Nullable #org.jetbrains.annotations.Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mNavController = NavHostFragment.findNavController(this);
}
Generic ViewModel:
public abstract class ListViewModel<T, K> extends MyViewModel {
protected ListRepository<T> mRepository;
protected MediatorLiveData<List<T>> list;
protected MutableLiveData<K> mListAdapter;
public ListViewModel() {
super();
}
public LiveData<List<T>> getList() {
if (list == null) {
LiveData<List<T>> lD = LiveDataReactiveStreams.fromPublisher(mRepository.getList());
list = new MediatorLiveData<>();
list.addSource(lD, li -> {
this.list.postValue(li);
list.remove(lD); //removing this line does not work either
});
}
return list;
}
public LiveData<K> getListAdapter() {
if (mListAdapter == null)
mListAdapter = new MutableLiveData<>();
return mListAdapter;
}
public abstract void setListAdapter(List<T> list);
MyItemViewModel:
public class MyItemViewModel extends ListViewModel<MyItem, MyItemAdapter> {
protected MyItemRepository mHistoryRepository;
public MyItemViewModel(MyItemRepository repository) {
super();
mRepository = repository;
}
#Override
public void setListAdapter(List<MyItem> list) {
if (getListAdapter().getValue() == null) {
MyItemAdapter adapter = new MyItemAdapter(list);
adapter.setListener(onListItemSelectedListener);
mListAdapter.setValue(adapter);
} else
mListAdapter.getValue().updateList(list);
}
Generic Repository
public abstract class ListRepository<T> {
protected Flowable<List<T>> list;
protected abstract Flowable<List<T>> getItemsList(int orderByField);
public Flowable<List<T>> getList() {
if (list == null)
list = getItemsList();
return list;
}
MyItemRepository:
public class MyItemRepository extends ListRepository<MyItem> {
protected static volatile MyItemRepository instance;
protected final MyItemLocalDS mLocalDataSource;
protected final MyItemRemoteDS mRemoteDataSource;
public MyItemRepository(MyItemRemoteDS remoteDataSource,
MyItemLocalDS localDataSource) {
this.mRemoteDataSource = remoteDataSource;
this.mLocalDataSource = localDataSource;
}
public static MyItemRepository getInstance(MyRemoteDS remoteDataSource,
MyLocalDS localDataSource) {
if (instance == null)
instance = new MyItemRepository(remoteDataSource, localDataSource);
return instance;
}
#Override
protected Flowable<List<MyItem>> getItemsList() {
list = mRemoteDataSource.download(...)
.map(soapObject -> parseItemsList(soapObject))
.map(wsResult -> transformItemsList(wsResult));
return list.subscribeOn(Schedulers.io());
}

MVVM. Set data in ViewModel

It is necessary to put the data in LiveData to send to the callback. In this method:
public void setData(List<Data> data) {
this.currentData.setValue((Data) data);
}
according to the documentation setValue is called by MutableLiveData, I replaced the LiveData in ViewModel with MutableLiveData, but anyway, when I open the required fragment, the application crashes
java.lang.ClassCastException: androidx.room.RoomTrackingLiveData cannot be cast to androidx.lifecycle.MutableLiveData
at avocado.droid.ptitsami.room.DataViewModel.<init>(DataViewModel.java:24)
at avocado.droid.ptitsami.room.DataViewModel$ModelFactory.create(DataViewModel.java:54)
at androidx.lifecycle.ViewModelProvider$FactoryWrapper.create(ViewModelProvider.java:268)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.java:179)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.java:147)
at avocado.droid.ptitsami.fragment.DataFragment.onCreateView(DataFragment.java:57)
How to fix it?
ViewModel
public class DataViewModel extends AndroidViewModel {
MutableLiveData<Data> currentData;
DataRepository repository;
public DataViewModel(#NonNull Application application, final int verseId) {
super(application);
int verseId1 = verseId;
repository = new DataRepository(application);
currentData = (MutableLiveData<Data>) repository.getById(verseId);
}
public LiveData<Data> getById() {
return currentData;
}
public void setData(List<Data> data) {
this.currentData.setValue((Data) data);
}
public static class ModelFactory extends ViewModelProvider.NewInstanceFactory {
#NonNull
private final Application application;
private final int dataId;
private final DataRepository repository;
public ModelFactory(#NonNull Application application, int id) {
super();
this.application = application;
this.dataId = id;
repository = new DataRepository(application);
}
#NonNull
#Override
public <T extends ViewModel> T create(#NonNull Class<T> modelClass) {
if (modelClass == DataViewModel.class) {
return (T) new DataViewModel(application, dataId);
}
return null;
}
}
Fragment
public class DataFragment extends Fragment {
private int dataId;
private static final String KEY_DATA_ID = "KEY_DATA_ID";
public TextView tvTitle;
public DataFragment() {
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootViewRead = inflater.inflate(R.layout.fragment_data, container, false);
Toolbar toolbar = rootViewRead.findViewById(R.id.toolbar);
AppCompatActivity activity = (AppCompatActivity) getActivity();
if (activity != null) {
activity.setSupportActionBar(toolbar);
}
setHasOptionsMenu(true);
tvTitle = (TextView) rootViewRead.findViewById(R.id.text);
DataViewModel.ModelFactory factory = new DataViewModel.ModelFactory(
getActivity().getApplication(), getArguments().getInt(KEY_DATA_ID));
final DataViewModel model = ViewModelProviders.of(this, factory)
.get(DataViewModel.class);
model.getById().observe(this, new Observer<Data>() {
#Override
public void onChanged(Data data) {
model.setData((List<Data>) data);
}
});
return rootViewRead;
}
public static DataFragment forData(int dataId) {
DataFragment fragment = new DataFragment();
Bundle args = new Bundle();
args.putInt(KEY_DATA_ID, dataId);
fragment.setArguments(args);
return fragment;
}
repository
public class DataRepository {
private DatabaseCopier db;
DataRepository(Application application) {
db = DatabaseCopier.getInstance(application);
}
LiveData<Data> getById(int id) {
return db.getDatabase().dataDao().getById(id);
}
Try adding source to currentData and change currentData from MutableLivaData to MediatorLiveData.
LiveData<Data> data = repository.getById(verseId);
currentData.addSource(data, observer);
There are multiple things odd here..
In your ViewModel you have a getter and a setter for the LiveData:
public LiveData<Data> getById() {
return currentData;
}
public void setData(List<Data> data) {
this.currentData.setValue((Data) data);
}
And in your observer of the LiveData you call the setter of the LiveData??
model.getById().observe(this, new Observer<Data>() {
#Override
public void onChanged(Data data) {
model.setData((List<Data>) data);
}
});
That does not make sense! When the observe method is called, the model already has this data set! So you do not need to call setData. Without the main issue, this will create an endless loop!
Now to your main Issue:
androidx.room.RoomTrackingLiveData cannot be cast to androidx.lifecycle.MutableLiveData
Room data can only be loaded to LiveData. The reason is because Room always keeps a link to it and automatically updates it, once the content of the database changed! But therefore YOU cannot change the content of the LiveData!
So please explain why
It is necessary to put the data in LiveData to send to the callback.
You have to change:
//MutableLiveData<Data> currentData;
LiveData<Data> currentData;
and
model.getById().observe(this, new Observer<Data>() {
#Override
public void onChanged(Data data) {
//model.setData((List<Data>) data); <- this creates an endless loop
// do here what you want to do with the content of the data
// if you need to pass it to the viewmodel, do it, but do not call `setValue`
}
});
For specific usecases a MediatorLiveData might be reasonable, but for this you would have to explain in detail why above doesn't do the job for you.

I can't figure out why my list view is not updated as I expect

I have a Fragment containing a RecyclerView for a list of items (groups) from a database. The user can navigate to a separate page to create a new group. In that action the group gets inserted into the database via a repository with asynchronous task. During that, the user is returned to the page holding the list, so the onResume method of the Fragment is called. When the insert record database operation is completed, the Repository posts the new list in the LiveData. The adapter for the group list is updated, but the view is not. I don't know why. I thought I was following the MVVM pattern properly.
Here is logger output, showing that the insert operation completes after onResume. The GroupListAdapter method to update the list is called, as expected. In this case "GroupListAdapter: SetGroups 2" meaning 2 groups, after having 1 before the user creates the 2nd.
HomeFragment onAttach
HomeFragment onCreate
HomeFragment onCreateView
GroupsFragment onAttach
GroupsFragment onCreate
GroupsFragment initData
GroupListViewModel created
GroupsFragment onCreateView
GroupsFragment: Do group list view
GroupsFragment list changed
GroupListAdapter: setGroups null
GroupListAdapter: setGroups inserted
GroupsFragment: Do group list view
GroupsFragment onResume
GroupsFragment: Do group list view
Repository: on group list change.
GroupsFragment list changed
GroupListAdapter: setGroups 1
GroupListAdapter: setGroups inserted
GroupsFragment: Do group list view
GroupsFragment list changed
GroupListAdapter: setGroups 0
GroupListAdapter dispatch updates
GroupListAdapter: setGroups 1 <<<<<<<<<< Initially 1 item in the group
GroupListAdapter dispatch updates
GroupsFragment: Do group list view
Finish create group activity. Result: Intent { (has extras) }
MainActivity onActivityResult for request 6, result: -1
Handle result of create group activity.
GroupListViewModel created
GroupListViewModel insert group
Created insert group task.
Async insert group.
Group inserted. <<<<<<<<<<<<<<<<<<<<<<< insertion completes before fragment's onResume
GroupsFragment onResume
GroupsFragment: Do group list view
Repository: on group list change.
GroupsFragment list changed
GroupListAdapter: setGroups 2 <<<<<<<<<<<<< The adapter knows there are 2 items in the list
GroupListAdapter dispatch updates
GroupsFragment: Do group list view >>>>>>>>>>>>>>>> Fragment should update, but does not.
Following are classes involves.
The Fragment:
public class GroupsFragment extends Fragment
{
private Context m_context = null;
private RecyclerView rv_groups;
private GroupListAdapter adapter = null;
private TextView tv_noGroups;
private GroupListViewModel m_groupViewModel;
public GroupsFragment ()
{} // Required empty public constructor
#Override
public void onAttach (Context context)
{
Logger.get().fine("GroupsFragment onAttach");
super.onAttach(context);
m_context = context;
}
#Override
public void onCreate (#Nullable Bundle savedInstanceState)
{
Logger.get().fine("GroupsFragment onCreate");
super.onCreate(savedInstanceState);
initData();
}
private void initData ()
{
Logger.get().fine("GroupsFragment initData");
m_groupViewModel = ViewModelProviders.of(this).get(GroupListViewModel.class);
if (m_groupViewModel.getAllGroups() == null)
Logger.get().severe("null group list live data in viewmodel");
m_groupViewModel.getAllGroups().observe(this, groups -> {
Logger.get().fine("GroupsFragment list changed");
adapter.setGroups(groups); // FIXME: Shouldn't this cause view update?
doListView(); // FIXME: Shouldn't need this?
});
}
#Override
public View onCreateView (#NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState)
{
Logger.get().fine("GroupsFragment onCreateView");
View view = inflater.inflate (R.layout.fragment_groups, container, false);
adapter = new GroupListAdapter(m_context);
initListView(view);
FloatingActionButton fab_add;
fab_add = view.findViewById(R.id.fab_add);
fab_add.setOnClickListener (v -> {
// FIXME: getActivity may return null if fragment is associated with Context, not Activity
getActivity().startActivityForResult(new Intent(getContext(), CreateGroupActivity.class), Activities.CREATE_GROUP);
});
setHasOptionsMenu (true);
return view;
}
protected void initListView (View view)
{
rv_groups = view.findViewById(R.id.rv_groups);
rv_groups.setAdapter(adapter);
tv_noGroups = view.findViewById(R.id.tv_noGroups);
RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(m_context);
rv_groups.setLayoutManager(mLayoutManager);
rv_groups.setItemAnimator(new DefaultItemAnimator());
rv_groups.addItemDecoration(new DividerItemDecoration(m_context, DividerItemDecoration.VERTICAL));
doListView();
}
/** Indicate on the UI that there are no groups to display. */
private void showNoGroups ()
{
rv_groups.setVisibility(View.GONE);
tv_noGroups.setVisibility(View.VISIBLE);
tv_noGroups.setText(getResources().getString(R.string.string_noGroups));
}
private void doListView ()
{
Logger.get().fine("GroupsFragment: Do group list view");
if (m_groupViewModel == null || m_groupViewModel.countGroups() == 0)
{
showNoGroups();
}
else
{
rv_groups.setVisibility(View.VISIBLE);
tv_noGroups.setVisibility(View.GONE);
}
}
#Override
public void onResume ()
{
Logger.get().fine("GroupsFragment onResume");
super.onResume();
doListView();
}
}
The ViewModel:
public class GroupListViewModel extends AndroidViewModel
{
private final Repository m_repository;
private final MediatorLiveData<List<GroupEntity>> m_groups;
public GroupListViewModel (#NonNull Application application)
{
super(application);
m_groups = new MediatorLiveData<>();
// set null until we get data from the database.
m_groups.setValue(null);
m_repository = ((MyApp)application).getRepository();
LiveData<List<GroupEntity>> groups = m_repository.getAllGroups();
// observe the changes of the groups from the database and forward them
m_groups.addSource(groups, m_groups::setValue);
Logger.get().finer("GroupListViewModel created");
}
public int countGroups ()
{
if (m_groups.getValue() == null)
return 0;
return m_groups.getValue().size();
}
public LiveData<List<GroupEntity>> getAllGroups ()
{ return m_groups; }
public List<GroupEntity> searchGroups (String query)
{ return m_repository.searchGroups(query); }
public void insert (GroupEntity group)
{
Logger.get().finer("GroupListViewModel insert group");
m_repository.insertGroup(group);
}
public void update (GroupEntity group)
{
Logger.get().finer("GroupListViewModel update group");
m_repository.updateGroup(group);
}
}
The Adapter:
public class GroupListAdapter extends RecyclerView.Adapter<GroupListAdapter.GroupViewHolder>
{
private List<? extends Group> m_groupList;
private Context context;
public GroupListAdapter (Context context)
{
this.context = context;
}
class GroupViewHolder extends RecyclerView.ViewHolder
{
TextView name;
LinearLayout lay_group;
GroupViewHolder (View itemView)
{
super(itemView);
name = itemView.findViewById(R.id.tv_name);
lay_group = itemView.findViewById(R.id.lay_groups);
}
}
#Override
#NonNull
public GroupViewHolder onCreateViewHolder (#NonNull ViewGroup parent, int viewType)
{
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_groups, parent, false);
return new GroupViewHolder(view);
}
#Override
public void onBindViewHolder (#NonNull final GroupViewHolder holder, int position)
{
final Group group = m_groupList.get(position);
holder.name.setText(group.getName());
// Set listener to show group details when group in list is clicked
holder.lay_group.setOnClickListener(v -> {
Intent intent = new Intent(context, GroupDetailActivity.class);
Logger.get().fine("Start group details activity for id " + group.getId());
intent.putExtra(GroupEntity.GROUP_ID_KEY, group.getId());
context.startActivity(intent);
});
}
public void setGroups (#Nullable final List<? extends Group> groups)
{
Logger.get().fine("GroupListAdapter: setGroups " + (groups == null ? "null" : groups.size()));
if (m_groupList == null)
{
m_groupList = groups;
notifyItemRangeInserted(0, groups == null ? 0 : groups.size());
Logger.get().fine("GroupListAdapter: setGroups inserted");
return;
}
DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback()
{
#Override
public int getOldListSize ()
{ return m_groupList.size(); }
#Override
public int getNewListSize ()
{ return groups.size(); }
#Override
public boolean areItemsTheSame (int oldItemPosition, int newItemPosition)
{ return m_groupList.get(oldItemPosition).getId() == groups.get(newItemPosition).getId(); }
#Override
public boolean areContentsTheSame (int oldItemPosition, int newItemPosition)
{
Group newGroup = groups.get(newItemPosition);
Group oldGroup = m_groupList.get(oldItemPosition);
return newGroup.getId() == oldGroup.getId()
&& (CommonUtils.equalStrings(newGroup.getName(), oldGroup.getName()));
}
});
m_groupList = groups;
Logger.get().finer("GroupListAdapter dispatch updates");
result.dispatchUpdatesTo(this);
}
#Override
public int getItemCount()
{
// Must allow for groups not completed loading yet
if (m_groupList == null)
return 0;
return m_groupList.size();
}
#Override
public long getItemId (int position)
{ return m_groupList.get(position).getId(); } // Note online BasicSample example does not check for null list here.
}
Repository:
public class Repository
{
private static Repository sInstance;
private final Database m_database;
private GroupDAO m_groupDAO;
private MediatorLiveData<List<GroupEntity>> m_observableGroups;
//private LiveData<List<GroupEntity>> m_groups;
public static Repository getInstance (final Database database)
{
if (sInstance == null)
{
synchronized (Repository.class)
{
if (sInstance == null)
sInstance = new Repository(database);
}
}
return sInstance;
}
private Repository (final Database database)
{
m_database = database;
load();
}
public Repository (Application application)
{
this(Database.getDatabase(application));
}
/**
* Get all data access objects.
*/
private void getDAO ()
{
m_groupDAO = m_database.groupDAO();
}
/**
* Get objects from the database, to store in this repository.
*/
private void load ()
{
getDAO();
Logger.get().info("Load objects to repository");
m_observableGroups = new MediatorLiveData<>();
m_observableGroups.addSource(m_database.groupDAO().loadAllSync(),
new Observer<List<GroupEntity>>()
{
#Override
public void onChanged (List<GroupEntity> groupEntities)
{
Logger.get().info("Repository: on group list change.");
if (m_database.getDatabaseCreated().getValue() != null)
m_observableGroups.postValue(groupEntities);
}
}
);
}
public LiveData<List<GroupEntity>> getAllGroups ()
{ return m_observableGroups; }
/*private LiveData<GroupEntity> loadGroup (final int id)
{ return m_database.groupDAO().loadById(id); }*/
// TODO which getGroup? return LiveData<GroupEntity> ?
/*public GroupEntity getGroup (int id)
{
Logger.get().finer("Repository: getGroup.");
for (GroupEntity group : getAllGroups().getValue())
{
if (group.getId() == id)
return group;
}
return null;
}*/
public GroupEntity getGroup (int id)
{
Logger.get().finer("Repository: getGroup.");
return m_database.groupDAO().loadById(id);
}
public LiveData<GroupEntity> getGroupSync (int id)
{
Logger.get().finer("Repository: getGroup.");
return m_database.groupDAO().loadByIdSync(id);
}
public void insertGroup (GroupEntity group)
{ new insertGroupTask(m_groupDAO).execute(group); }
private static class insertGroupTask extends AsyncTask<GroupEntity, Void, Void>
{
private GroupDAO mAsyncTaskDao;
insertGroupTask(GroupDAO dao) {
Logger.get().info("Created insert group task.");
mAsyncTaskDao = dao;
}
#Override
protected Void doInBackground (final GroupEntity... params)
{
Logger.get().info("Async insert group.");
mAsyncTaskDao.insert(params[0]);
Logger.get().fine("Group inserted.");
return null;
}
}
}
Group:
public interface Group
{
...
}
Group Entity
#Entity(tableName = "groups")
public class GroupEntity implements Group
{
...
}
No mistake in the java. A GUI newbie mistake in the layout definition meant the groups would be displayed one per page. Changed TextView layout_height to "wrap_content".

Lag in displaying data from Oberver on LiveData

I'm using a ViewModel that populates a RecyclerView.Adapter, and loads data from LiveData from my Room database. The problem is that my display is always blank, and checking with the Dao (for debug, on the main thread) shows me that the data is retreived just fine. (ergo there is data in the DB).
The problem is that the Observer on my LiveData always returns a null (or no data) and I end up having to refresh the fragment at least once (by moving away and moving back) to see anything - even the meagre one record I put in the Database for testing.
Restarting the app or fragment means a blank screen and a few refreshes before I see anything which is strange because, well, the data is already there.
I'm out of ideas on how to get this to show me data in more or less real time. Can anyone help?
Sharing the DAO, ViewModel and Fragment code here.
Fragment
... import libs and set up variables ...
private HouseCallAdapter houseCallAdapter;
private RecyclerView recyclerView;
private TextView emptyView;
RevivDatabase revivDatabase;
private LiveData<List<HouseCall>> liveHousecalls;
private List<HouseCall> houseCalls;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View view = inflater.inflate(R.layout.fragment_reviv_housecall_request_list, container, false);
Bundle arguments = getArguments();
String action = arguments.getString("data");
revivDatabase = RevivDatabase.getDatabase(getActivity().getApplicationContext());
emptyView = view.findViewById(R.id.txtNoData);
recyclerView = view.findViewById(R.id.hcrecyclerView);
viewModel = ((Reviv) getActivity()).getViewModel();
if(liveHousecalls == null) {
liveHousecalls = new MutableLiveData<List<HouseCall>>();
}
houseCallAdapter = new HouseCallAdapter(getContext(), apikey, false, false);
liveHousecalls = viewModel.getOpenHousecalls();
// this is to test if there is actually any data retreived
// calling on main thread. Lose this code later.
houseCalls = revivDatabase.revivDao().getHousecallsByStatus(action);
break;
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity().getApplicationContext(), LinearLayoutManager.VERTICAL, false));
houseCallAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
#Override
public void onChanged() {
super.onChanged();
checkEmpty();
}
#Override
public void onItemRangeInserted(int positionStart, int itemCount) {
super.onItemRangeInserted(positionStart, itemCount);
checkEmpty();
}
#Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
super.onItemRangeRemoved(positionStart, itemCount);
checkEmpty();
}
void checkEmpty() {
//emptyView.setText (R.string.no_data_available);
emptyView.setVisibility(houseCallAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
recyclerView.setVisibility (houseCallAdapter.getItemCount() == 0 ? View.GONE : View.VISIBLE);
}
});
houseCallAdapter.setData(houseCalls);
houseCallAdapter.notifyDataSetChanged();
liveHousecalls.observe(getActivity(), new Observer<List<HouseCall>>() {
#Override
public void onChanged(#Nullable List<HouseCall> houseCalls) {
if(houseCalls != null) {
houseCallAdapter.setData(houseCalls);
houseCallAdapter.notifyDataSetChanged();
}
}
});
recyclerView.setItemAnimator (new DefaultItemAnimator());
recyclerView.setAdapter(houseCallAdapter);
emptyView.setVisibility(houseCallAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
return view;
}
ViewModel
private LiveData<List<HouseCall>> housecallList;
private LiveData<List<HouseCall>> openHousecalls, confirmedHousecalls, closedHousecalls, missedHousecalls, userCancelledHousecalls, respCancelledHousecalls;
private LiveData<List<Incident>> incidentList, openIncidents;
private LiveData<List<Incident>> closedIncidents, usercancelIncidents, respcancelIncidents;
private LiveData<Incident> liveIncident;
private RevivDatabase database;
Context context;
/////////////////////////////////////////////////////////
// CONSTRUCTOR
/////////////////////////////////////////////////////////
public RevivViewModel(Application application) {
super(application);
//////////////////////////////////////////////////////////////////////////
// //
// DANGER WILL ROBINSON //
// Storing context in ViewModel is Not A Good Idea (TM) //
context = application.getApplicationContext(); //
// //
//////////////////////////////////////////////////////////////////////////
database = RevivDatabase.getDatabase(application);
}
/////////////////////////////////////////////////////////
// GETTERS AND SETTERS
/////////////////////////////////////////////////////////
// Housecalls
public LiveData<List<HouseCall>> getHousecallList() {
if (housecallList == null) {
housecallList = new MutableLiveData<>();
loadHousecalls();
}
return housecallList;
}
public LiveData<List<HouseCall>> getOpenHousecalls() {
if (openHousecalls == null) {
openHousecalls = new MutableLiveData<>();
loadOpenHousecalls();
}
return openHousecalls;
}
/////////////////////////////////////////////////////////
// TRIGGER REFRESH FROM VIEWMODEL
/////////////////////////////////////////////////////////
// TBD
/////////////////////////////////////////////////////////
// EXTERNAL CALLS - REFRESH FROM DB
/////////////////////////////////////////////////////////
// Methods to accept/cancel incidents and housecalls
public void loadHousecalls(){
class OneShotTask implements Runnable {
OneShotTask() {
}
public void run() {
housecallList = database.revivDao().getAllLiveHousecalls();
//housecallList.postValue(hc);
}
}
Thread t = new Thread(new OneShotTask());
t.start();
}
public void loadOpenHousecalls(){
class OneShotTask implements Runnable {
OneShotTask() {
}
public void run() {
openHousecalls = database.revivDao().getLiveHousecallsByStatus("open");
}
}
Thread t = new Thread(new OneShotTask());
t.start();
}
}
DAOInterface
public interface RevivDaoInterface {
// Housecalls
... numerous insert, delete and update calls ...
#Query("SELECT * FROM housecalls WHERE housecallid = :housecallid")
public HouseCall getHousecallById(String housecallid);
#Query("SELECT * FROM housecalls WHERE status = :status")
public List<HouseCall> getHousecallsByStatus(String status);
#Update(onConflict = OnConflictStrategy.IGNORE)
void updateHousecall(HouseCall houseCall);
#Query("SELECT * FROM housecalls WHERE status = \'open\'")
public LiveData<List<HouseCall>> getOpenHousecalls();
#Query("SELECT * FROM housecalls WHERE status = :status")
public LiveData<List<HouseCall>> getLiveHousecallsByStatus(String status);
#Query("SELECT * FROM housecalls")
public List<HouseCall> getAllHousecalls();
}
DAO
imports
#Dao
public abstract class RevivDao implements RevivDaoInterface {
#Transaction
public void upsert(HouseCall houseCall){
try {
this.insert(houseCall);
} catch (SQLiteConstraintException exception) {
this.update(houseCall);
Log.e(TAG, "upsert: ", exception);
}
}
#Transaction
public void upsert(List<HouseCall> houseCall){
for(HouseCall hc : houseCall) {
try {
this.insert(hc);
} catch (SQLiteConstraintException exception) {
this.update(hc);
Log.e(TAG, "upsert: ", exception);
}
}
}
}
Thanks to #pskink, I figured a way around the lag in updating the data into my ViewModel.
To solve this issue, I had to implement PagedListAdapter.
In the build.gradle (Module) file
implementation 'android.arch.paging:runtime:1.0.1'
In the DAO
#Query("SELECT * from housecalls where status = :status")
public abstract DataSource.Factory<Integer, HouseCall> getHousecallPagesByStatus(String status);
In the ViewModel
//declare a LiveData of PagedList
LiveData<PagedList<HouseCall>> openhousecallPages;
// Define Configuration for Paged List
Config pagedListConfig = (new PagedList.Config.Builder()).setEnablePlaceholders(true)
.setPrefetchDistance(10)
.setPageSize(20).build();
// Function to access the data
public LiveData<PagedList<HouseCall>> getOpenhousecallPages(){
openhousecallPages = new LivePagedListBuilder<>(database.revivDao().getHousecallPagesByStatus("open"),
pagedListConfig).build();
return openhousecallPages;
}
Set up the PagedListAdapter
package packagename;
import static android.content.ContentValues.TAG;
public class HouseCallPagedAdapter extends PagedListAdapter<HouseCall, HouseCallViewHolder>{
protected HouseCallPagedAdapter(#NonNull DiffUtil.ItemCallback<HouseCall> diffCallback) {
super(diffCallback);
}
public HouseCallPagedAdapter(#NonNull DiffUtil.ItemCallback diffcallback){
super(diffcallback);
}
#NonNull
#Override
public HouseCallViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int i) {
return new HouseCallViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.card_housecall_request, parent, false),parent.getContext());
}
#Override
public void onBindViewHolder(#NonNull in.portmanteau.reviv.Adapters.HouseCallViewHolder holder, int i) {
holder.bindTo(getItem(i));
}
}
Define a ViewHolder with the following structure
public class HouseCallViewHolder extends RecyclerView.ViewHolder{
// declare values, elements, etc
public HouseCallViewHolder(View itemView, Context mContext) {
super(itemView);
// set up UI elements
}
void bindTo(final HouseCall houseCall){
this.houseCall = houseCall;
//populate values, set onClickListeners, etc.
}
}
Finally, use the Adapter in your Activity/Fragment!
// Implement an DiffUtil.ItemCallback
private DiffUtil.ItemCallback<HouseCall> diffCallback = new DiffUtil.ItemCallback<HouseCall>() {
#Override
public boolean areItemsTheSame(#NonNull HouseCall houseCall, #NonNull HouseCall newhouseCall) {
return houseCall.getHousecallid().equalsIgnoreCase(newhouseCall.getHousecallid()) ;
}
#Override
public boolean areContentsTheSame(#NonNull HouseCall houseCall, #NonNull HouseCall newhouseCall) {
return houseCall.isTheSame(newhouseCall);
}
};
HouseCallPagedAdapter houseCallPagedAdapter = new HouseCallPagedAdapter(diffCallback);
viewModel.getOpenhousecallPages().observe(this, houseCallPagedAdapter::submitList);
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity().getApplicationContext(), LinearLayoutManager.VERTICAL, false));
recyclerView.setItemAnimator (new DefaultItemAnimator());
recyclerView.setAdapter(houseCallPagedAdapter);

RxJava Room with Flowable: Returning new rows only

We decided to use Flowable of RxJava with Room persistence library. We have tables in which content is added by services on indefinite intervals(there are 2-3 update inserts every few seconds) and that table's rows are subscribed in BaseAdapter for making changes in view on live basis.
The problem is that when ever there is any update/insert operation, we get whole list again making the base adaptor regenerate the view. There can be delete operations as well making the length of rows no use to us.
I want to ask if there is any other operator which supports live data and call onNext on new data, and provide whole list on delete operations.
are subscribed in BaseAdapter
Use RecyclerView with fine-grained notify* calls like notifyItemInserted instead of a ListView
The problem is that when ever there is any update/insert operation, we get whole list again
That is completely expected behavior with both LiveData<List<T>> and Flowable<List<T>>.
making the base adaptor regenerate the view.
That's because you aren't using DiffUtil or you aren't using RecyclerView's new addition, ListAdapter (which handles the diffing internally and automatically)
provide whole list on delete operations.
It actually already provides the whole list (without the deleted items, of course).
The solution from AAC side is to use DataSource.Factory<Integer, T> instead of Flowable<List<T>>/LiveData<List<T>> so that you can create a LiveData<PagedList<T>> via a LivePagedListBuilder which you can set to your PagedListAdapter. That way, it only fetches a given page size instead of the whole list, and handles diffing.
EDIT:
#Entity(tableName = Task.TABLE_NAME)
public class Task {
public static DiffUtil.ItemCallback<Task> DIFF_CALLBACK = new DiffUtil.ItemCallback<Task>() {
#Override
public boolean areItemsTheSame(#NonNull Task oldItem, #NonNull Task newItem) {
return oldItem.id == newItem.id;
}
#Override
public boolean areContentsTheSame(#NonNull Task oldItem, #NonNull Task newItem) {
return oldItem.equals(newItem);
}
};
public static final String TABLE_NAME = "TASK";
public static final String COLUMN_ID = "task_id";
public static final String COLUMN_TEXT = "task_text";
public static final String COLUMN_DATE = "task_date";
and
#Dao
public interface TaskDao {
#Query("SELECT * FROM " + Task.TABLE_NAME + " ORDER BY " + Task.COLUMN_DATE + " ASC ")
DataSource.Factory<Integer, Task> tasksSortedByDate();
// ...
}
and
public class TaskViewModel
extends ViewModel {
private final TaskDao taskDao;
private LiveData<PagedList<Task>> liveResults;
public TaskViewModel(TaskDao taskDao) {
this.taskDao = taskDao;
liveResults = new LivePagedListBuilder<>(taskDao.tasksSortedByDate(),
new PagedList.Config.Builder() //
.setPageSize(20) //
.setPrefetchDistance(20) //
.setEnablePlaceholders(true) //
.build())
.setInitialLoadKey(0)
.build();
}
public LiveData<PagedList<Task>> getTasks() {
return liveResults;
}
}
and
public class TaskFragment
extends Fragment {
RecyclerView recyclerView;
// ...
#Override
public void onViewCreated(View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
recyclerView = view.findViewById(R.id.recycler_view);
TaskViewModel viewModel = ViewModelProviders.of(this).get(TaskViewModel.class);
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false));
final TaskAdapter taskAdapter = new TaskAdapter();
recyclerView.setAdapter(taskAdapter);
viewModel.getTasks().observe(this, pagedList -> {
//noinspection Convert2MethodRef
taskAdapter.submitList(pagedList);
});
}
#Override
protected void onDestroyView() {
super.onDestroyView();
viewModel.getTasks().removeObservers(this);
}
}
and
public class TaskAdapter
extends PagedListAdapter<Task, TaskAdapter.ViewHolder> {
public TaskAdapter() {
super(Task.DIFF_CALLBACK);
}
No, With room, there aren't. If you are using Rx with Room, you can use Diff util
or you can use List Adapter
There's also a variation called SortedListAdapter
Providing a sample implementation from developer android
#Dao
interface UserDao {
#Query("SELECT * FROM user ORDER BY lastName ASC")
public abstract LiveData<List<User>> usersByLastName();
}
class MyViewModel extends ViewModel {
public final LiveData<List<User>> usersList;
public MyViewModel(UserDao userDao) {
usersList = userDao.usersByLastName();
}
}
class MyActivity extends AppCompatActivity {
#Override
public void onCreate(Bundle savedState) {
super.onCreate(savedState);
MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
RecyclerView recyclerView = findViewById(R.id.user_list);
UserAdapter<User> adapter = new UserAdapter();
viewModel.usersList.observe(this, list -> adapter.submitList(list));
recyclerView.setAdapter(adapter);
}
}
class UserAdapter extends ListAdapter<User, UserViewHolder> {
public UserAdapter() {
super(User.DIFF_CALLBACK);
}
#Override
public void onBindViewHolder(UserViewHolder holder, int position) {
holder.bindTo(getItem(position));
}
public static final DiffUtil.ItemCallback<User> DIFF_CALLBACK =
new DiffUtil.ItemCallback<User>() {
#Override
public boolean areItemsTheSame(
#NonNull User oldUser, #NonNull User newUser) {
// User properties may have changed if reloaded from the DB, but ID is fixed
return oldUser.getId() == newUser.getId();
}
#Override
public boolean areContentsTheSame(
#NonNull User oldUser, #NonNull User newUser) {
// NOTE: if you use equals, your object must properly override Object#equals()
// Incorrectly returning false here will result in too many animations.
return oldUser.equals(newUser);
}
}
}
Or, you can create a RXBus implementation, and when you are inserting data into database, publish an event with the added/deleted data. Subscribe to it, and you can get what you want.

Categories

Resources