2

For the question I have prepared a simple and working example at Github:

app screenshot

My example app downloads a JSON array containing top 30 players in a game using okhttp and stores them into SQLite Room. In the fragment I observe the corresponding LiveData<List<TopEntity>> object and update the FastAdapter instance:

public class TopFragment extends Fragment {
    private final ItemAdapter<TopItem> mItemAdapter = new ItemAdapter<>();
    private final FastAdapter<TopItem> mFastAdapter = FastAdapter.with(mItemAdapter);

    private TopViewModel mViewModel;
    private ProgressBar mProgressBar;
    private RecyclerView mRecyclerView;

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater,
                             ViewGroup container,
                             Bundle savedInstanceState) {

        mViewModel = ViewModelProviders.of(this).get(TopViewModel.class);
        mViewModel.getTops().observe(this, tops -> {
            mItemAdapter.clear();
            for (TopEntity top: tops) {
                TopItem item = new TopItem(top);
                mItemAdapter.add(item);
            }
        });

        View v = inflater.inflate(R.layout.top_fragment, container, false);
        mProgressBar = v.findViewById(R.id.progressBar);
        mRecyclerView = v.findViewById(R.id.recyclerView);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
        mRecyclerView.setAdapter(mFastAdapter);

        fetchJsonData();

        return v;
    }

My problem is: the recycler view flickers once - every time the data is downloaded.

Even though the top 30 does not change often, i.e. the data stays same.

When I look at the example app in the excellent FastAdapter library by Mr. Penz, I do not see any flickering there.

UPDATE:

I've got a hint that the flickering is obviously caused by calling mItemAdapter.clear(); in the onChanged method and I should use DiffUtil class instead.

So I have added a new class:

public class DiffCallback extends DiffUtil.Callback {
    private final List<TopItem> mOldList;
    private final List<TopItem> mNewList;

    public DiffCallback(List<TopItem> oldStudentList, List<TopItem> newStudentList) {
        this.mOldList = oldStudentList;
        this.mNewList = newStudentList;
    }

    @Override
    public int getOldListSize() {
        return mOldList.size();
    }

    @Override
    public int getNewListSize() {
        return mNewList.size();
    }

    @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        TopItem oldItem = mOldList.get(oldItemPosition);
        TopItem newItem = mNewList.get(newItemPosition);

        return oldItem.uid == newItem.uid;
    }

    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        TopItem oldItem = mOldList.get(oldItemPosition);
        TopItem newItem = mNewList.get(newItemPosition);

        return oldItem.elo == newItem.elo &&
                oldItem.given.equals(newItem.given) &&
                //oldItem.photo != null && oldItem.photo.equals(newItem.photo) &&
                oldItem.avg_time != null && oldItem.avg_time.equals(newItem.avg_time) &&
                oldItem.avg_score == newItem.avg_score;
    }

    @Nullable
    @Override
    public Object getChangePayload(int oldItemPosition, int newItemPosition) {
        return super.getChangePayload(oldItemPosition, newItemPosition);
    }
}

And then I am trying to use it in the onChanged method instead of just calling mItemAdapter.clear() but the RecyclerView stays empty even though the tops has elements:

mViewModel = ViewModelProviders.of(this).get(TopViewModel.class);
mViewModel.getTops().observe(this, tops -> {
    List<TopItem> oldList = mItemAdapter.getAdapterItems();
    List<TopItem> newList = new ArrayList<>();
    for (TopEntity top: tops) {
        TopItem item = new TopItem(top);
        newList.add(item);
    }

    DiffCallback diffCallback = new DiffCallback(oldList, newList);
    DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback);

    mItemAdapter.getAdapterItems().clear();
    mItemAdapter.getAdapterItems().addAll(newList);
    diffResult.dispatchUpdatesTo(mFastAdapter);
});

SHORT SUMMARY OF MY PROBLEM:

My real app uses FastAdapter for several reasons and I am looking for a way to "marry" it with DiffUtil in order to eliminate the one-time flickering when the data is downloaded over HTTPS and updated in Room.

Alexander Farber
  • 21,519
  • 75
  • 241
  • 416
  • 1
    In unit or instrumented tests, does `diffResult` seem to get calculated correctly? If so, then perhaps there is some issue with `DiffResult` and `FastAdapter` (I have not used `FastAdapter`, so I have no experience with it). – CommonsWare Nov 01 '18 at 13:20
  • 1
    Does your code highly depend on `FastAdapter`? I mean is it possible to you to change it? As I read your code and related parts of `FastAdapter`, I think the source of this issue comes from thinking behind `FastAdapter` architecture. – aminography Nov 02 '18 at 08:12
  • Thanks for asking, but I would like to use FastAdapter, because [my app](https://play.google.com/store/apps/details?id=de.slova) uses its search and filtering capabilities in several fragments and also it uses MaterialDrawer - which is also based on FastAdapter. My target is to get FastAdapter to work either with DiffUtil or with its own [FastAdapterDiffUtil](https://github.com/mikepenz/FastAdapter/issues/737) - but I haven't succeeded with either path yet. – Alexander Farber Nov 02 '18 at 09:24

3 Answers3

5

A simple way to avoid blinking is to disable the RecyclerView's ItemAnimator.

mRecyclerView.setItemAnimator(null);
aminography
  • 21,986
  • 13
  • 70
  • 74
  • 1
    I'm not sure this is the right solution. If some items move up/down I would still want the re-arrange animation. It just should not blink if it's the same item. – mliu Nov 15 '19 at 06:49
  • @mliu: You are right. I know. This issue comes from `mItemAdapter.clear();` in the library that I think it is not right. However, this solution does not cover the library problems. – aminography Nov 15 '19 at 06:56
5

With the help of FastAdapter author I have been able to eliminate the one-time flickering by adding an identifier to my TopItem being displayed in the RecyclerView:

@Override
public long getIdentifier() {
    return uid;
}

And by using the FastAdapterDiffUtil in my TopFragment:

mViewModel = ViewModelProviders.of(this).get(TopViewModel.class);
mViewModel.getTops().observe(this, tops -> {
    List<TopItem> newList = new ArrayList<>();
    for (TopEntity top: tops) {
        TopItem item = new TopItem(top);
        newList.add(item);
    }

    DiffUtil.DiffResult diffResult = FastAdapterDiffUtil.calculateDiff(mItemAdapter, newList);
    FastAdapterDiffUtil.set(mItemAdapter, diffResult);
});
Alexander Farber
  • 21,519
  • 75
  • 241
  • 416
  • 1
    What if identifier is a string? I.e. a guid. There's no good way to hash it into a long without collision. – mliu Nov 15 '19 at 06:36
  • I just use `public long getIdentifier() { return word.hashCode(); }` it seems to work for me in an [app](https://play.google.com/store/apps/details?id=de.slova) with 111867 words – Alexander Farber Nov 15 '19 at 08:37
1

Try returning some marker object from getChangePayload() method.

@Nullable
@Override
public Object getChangePayload(final int oldItemPosition, final int newItemPosition) {
    return mNewList.get(newItemPosition);
}
UserX
  • 176
  • 4