5

I am trying to show call log list using Room-Paging-LiveData-ViewModel. Without paging my code works perfectly. And I want to use paging also.

In my database I have total 25 call log record. The first 9 call log is showing in the list.

By debugging I found that while reading data in view model via Dao, it is returning list of size 25. But only first 9 of these are non null. All other entries in the list is null.

I am expecting the null data will refresh soon as this is a paged list. But the problem is the null are never getting refreshed with valid data.

And the observe method of view model is getting called only once, the first time only.

I think I am doing something wrong.

Here is the code below

The fragment

public class CallLogListFragment extends Fragment {
    private static final String TAG = "RecentCallsFragment";

    public static String getTAG() {
        return TAG;
    }

    public static Fragment newInstance() {
        return new CallLogListFragment();
    }

    public CallLogListFragment() {
    }

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        FragmentCallLogListBinding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_call_log_list, container, false);
        CallLogListAdapter adapter = new CallLogListAdapter();
        binding.list.setAdapter(adapter);
        CallLogListViewModel model = ViewModelProviders.of(this).get(CallLogListViewModel.class);
        model.getCallLogList().observe(this, adapter::refreshData);
        return binding.getRoot();
    }

}

The Adapter

public class CallLogListAdapter extends PagedListAdapter<CallLogItem, CallLogListAdapter.ViewHolder> {
    CallLogListAdapter() {
        super(DIFF_CALLBACK);
    }

    void refreshData(List<CallLogItem> data) {
        DiffUtil.DiffResult calculatedDiff = DiffUtil.calculateDiff(new CallLogListDiffUtilCallBack(this.data, data));
        this.data.clear();
        this.data.addAll(data);
        calculatedDiff.dispatchUpdatesTo(this);
    }

    private List<CallLogItem> data = new ArrayList<>();

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return new ViewHolder(DataBindingUtil.inflate(
                LayoutInflater.from(parent.getContext()),
                R.layout.call_log_list_single_item,
                parent, false
        ));
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        CallLogItem item = data.get(position);
        holder.binding.setCallLog(item);
    }

    @Override
    public int getItemCount() {
        return data.size();
    }

    class ViewHolder extends RecyclerView.ViewHolder {
        public CallLogListSingleItemBinding binding;
        public ViewHolder(@NonNull CallLogListSingleItemBinding binding) {
            super(binding.getRoot());
            this.binding = binding;
        }
    }

     private static DiffUtil.ItemCallback<CallLogItem> DIFF_CALLBACK =
        new DiffUtil.ItemCallback<CallLogItem>() {
            @Override
            public boolean areItemsTheSame(CallLogItem oldItem, CallLogItem newItem) {
                return oldItem.getHeaderDateVisibility() == newItem.getHeaderDateVisibility()
                        && oldItem.getCallId().equals(newItem.getCallId());
            }

            @Override
            public boolean areContentsTheSame(@NonNull CallLogItem oldItem, @NonNull CallLogItem newItem) {
                return areItemsTheSame(oldItem, newItem);
            }
        };
}

The Dao

@Dao
public interface CallLogDao extends BaseDao<CallLog>{
    @Query("SELECT * FROM log")
    List<CallLog> getAll();

    @Query("SELECT * FROM log WHERE number=:number")
    CallLog findByName(String number);

    @Query("SELECT * FROM log order by date desc")
    LiveData<List<CallLog>> getAllLive();

    @Query("SELECT * FROM log order by date desc")
    DataSource.Factory<Integer, CallLog> getAllLivePaged();
}

The ViewModel

public class CallLogListViewModel extends ViewModel {
    private LiveData<List<CallLogItem>> callLogList;

    public CallLogListViewModel() {
        callLogList = Transformations.map(new LivePagedListBuilder<>(AppDatabase.get().callLogDao().getAllLivePaged(), 3).build(), input -> {
            List<CallLogItem> list = new ArrayList<>();
            for (int i = 0; i < input.size(); i++) {
                boolean isHeader = true;
                CallLog callLog = input.get(i);
                if(callLog!=null) {
                    if (i > 0) {
                        CallLog previousCallLog = input.get(i - 1);
                        if(previousCallLog!=null) {
                            isHeader = TimeFormat.isDifferentDate(callLog.date, previousCallLog.date);
                        }
                    }
                    list.add(CallLogItem.Companion.from(callLog, isHeader));
                }
            }
            return list;
        });
    }

    LiveData<List<CallLogItem>> getCallLogList() {
        return callLogList;
    }
}

Later I tried to make

private LiveData<List<CallLogItem>> callLogList; 

to Paged list like

private LiveData<PagedList<CallLogItem>> callLogList; 

But I found no proper way to transform into that.

Ifta
  • 1,536
  • 18
  • 25

2 Answers2

3

In order to be able to return a mapped PagedList you should know that DataSource and DataSource.Factory has map() and mapByPage(). You can map the DataSource Factory items with mapByPage() instead using Transformation, like this:

DataSource.Factory<Integer, CallLog> dataSourceFactoryCallLog = AppDatabase.get().callLogDao().getAllLivePaged();
                
DataSource.Factory<Integer, CallLogItem> dataSourceFactoryCallLogItem = dataSourceFactoryCallLog.mapByPage(input -> {
   List<CallLogItem> list = new ArrayList<>();
   for (int i = 0; i < input.size(); i++) {
       boolean isHeader = true;
       CallLog callLog = input.get(i);
       if(callLog!=null) {
           if (i > 0) {
               CallLog previousCallLog = input.get(i - 1);
               if(previousCallLog!=null) {
                   isHeader = TimeFormat.isDifferentDate(callLog.date, previousCallLog.date);
               }
           }
           list.add(CallLogItem.Companion.from(callLog, isHeader));
       }
   }
   return list;
 });
        
LiveData<PagedList<CallLogItem>> callLogItems = new LivePagedListBuilder<>(dataSourceFactoryCallLogItem, 3).build()

EDIT

According PagedList documentation

With placeholders, the PagedList is always the full size of the data set. get(N) returns the Nth item in the data set, or null if its not yet loaded.

Without null placeholders, the PagedList is the sublist of data that has already been loaded. The size of the PagedList is the number of currently loaded items, and get(N) returns the Nth loaded item. This is not necessarily the Nth item in the data set.

Placeholders are enabled by default, but can be disabled in two ways. They are disabled if the DataSource does not count its data set in its initial load, or if false is passed to setEnablePlaceholders(boolean) when building a PagedList.Config.

You just need to create a PagedList.Config and add this to LivePagedListBuilder instantiation.

PagedList.Config pagedListConfig =
                (new PagedList.Config.Builder())
                        .setEnablePlaceholders(false)
                        .setPageSize(3).build();

LiveData<PagedList<CallLogItem>> callLogItems = new LivePagedListBuilder<>(dataSourceFactoryCallLogItem, pagedListConfig).build()
Community
  • 1
  • 1
haroldolivieri
  • 2,173
  • 18
  • 29
  • I have tried and i found that, in my adapter refreshData method is called only once with full list size. List size is 25 but contains 9 actual data, others are null. How can I configure PagedList to load the next page in the data list? – Ifta Nov 15 '18 at 03:55
  • try to create a [`PagedList.Config`](https://developer.android.com/reference/android/arch/paging/PagedList.Config) and add this to `LivePagedListBuilder ` instantiation. `new LivePagedListBuilder<>(dataSourceFactoryCallLogItem, pagedListConfig).build()` – haroldolivieri Nov 15 '18 at 08:55
  • change placeholders to false on `PagedList.Config` like this `.setEnablePlaceholders(false)` – haroldolivieri Nov 15 '18 at 09:02
  • thanks. that would help removing null elements, i know. but the problem was wrong implementation of my adapter class. I had edited your answer to make it correct answer and was waiting for the edit to approve by peer review. But now I see the edit is gone. – Ifta Nov 15 '18 at 10:56
  • Actually with correct implementation of Adapter class, the previous implementation of vew model class that i have posted would work fine. – Ifta Nov 15 '18 at 10:58
  • but with placeholders enabled you should keep receiving null objects, right? – haroldolivieri Nov 16 '18 at 12:07
  • yes. but actually my problem was not about receiving the null objects. it was that the null objects was not getting refreshed by next paged data load and kept null forever. – Ifta Nov 17 '18 at 02:55
  • 1
    I think you can edit your question to let the problem more clear and after this create the right answer for that. – haroldolivieri Nov 17 '18 at 21:01
2

For paged list adapter there is 2 thing to note. 1. Data will be handled internally and no need to declare any data structure to handle data manually. 2. There is a default method called submitList in PagedListAdapter. It is necessary to submit paged list over that method to the adapter.

Modified adapter

public class CallLogListAdapter extends PagedListAdapter<CallLogItem, CallLogListAdapter.ViewHolder> {
    private Context context;
    CallLogListAdapter(Context context) {
        super(DIFF_CALLBACK);
        this.context = context;
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return new ViewHolder(DataBindingUtil.inflate(
                LayoutInflater.from(parent.getContext()),
                R.layout.call_log_list_single_item,
                parent, false
        ));
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        CallLogItem item = getItem(position);
        if (item != null) {
            holder.binding.setCallLog(item);
            ImageUtil.setImage(holder.binding.ivProfileImage, item.getImageUrl(), item.getName());
        } else {
            holder.binding.invalidateAll();
        }
    }


    class ViewHolder extends RecyclerView.ViewHolder {
        public CallLogListSingleItemBinding binding;

        public ViewHolder(@NonNull CallLogListSingleItemBinding binding) {
            super(binding.getRoot());
            this.binding = binding;
        }
    }

    private static DiffUtil.ItemCallback<CallLogItem> DIFF_CALLBACK =
            new DiffUtil.ItemCallback<CallLogItem>() {
                @Override
                public boolean areItemsTheSame(CallLogItem oldItem, CallLogItem newItem) {
                    return oldItem.getHeaderDateVisibility() == newItem.getHeaderDateVisibility()
                            && oldItem.getCallId()!=null &&  oldItem.getCallId().equals(newItem.getCallId());
                }

                @Override
                public boolean areContentsTheSame(@NonNull CallLogItem oldItem, @NonNull CallLogItem newItem) {
                    return areItemsTheSame(oldItem, newItem);
                }
            };
}

Modified Data passing to adapter

CallLogListViewModel model = ViewModelProviders.of(this).get(CallLogListViewModel.class);
model.getCallLogList().observe(this, adapter::submitList);
Ifta
  • 1,536
  • 18
  • 25
  • It is only loading the first set of data – kgandroid May 02 '20 at 21:25
  • From where are you calling the refreshData() from this soln?? – kgandroid May 02 '20 at 21:37
  • `refreshData()` should not be called. We have to call `submitList()` for `PagedListAdapter` to work properly. If we don't use `submitList()`, it will load only first set of data. To ensure loading next sets, we must have to call `submitList()` method of the adapter. – Ifta May 03 '20 at 06:58
  • 1
    Yes, figured it out. The main idea is to avoid sending any list(data source) explicitly to the adapter.You can still call refreshData() and pass any other items i.e. the instance of viewmodel or clicklisteners to the adapter.BUT not the list.Thanks for your response – kgandroid May 03 '20 at 08:25
  • Hi there! I've been playing around with the new Paging 3 library. I've successfully implemented it in my app which simply displays a contacts list. Now I'm implementing a search contacts feature. I simply apply map and filter operations on the Flow and that gives me precise results. However, I've to scroll to the bottom to update all the list of contacts. Otherwise, search works only for the firstly displayed contacts (because bottom contacts aren't loaded into the flow yet). I want to force load the data (next pages) in the background. Any workaround for this? – Karthik Jul 30 '20 at 06:11