5

I'm new to Android and I'm trying to implement a Barcode reader scenario with the new architecture components.

Every time a barcode is read, I want to update a list in the ViewModel adding a new element if the barcode is not present on the list or increasing the quantity otherwise.

The following solution is working, but it's not satisfying me about calling "notifyDataSetChanged" on the adapter in order to update UI. That's because ViewModel list and adapter internal list contain references to same objects so DiffUtil won't catch any changes.

Is there a better way to update UI? Beside adapter, are there any improvement I should consider to handle architecture components?

ViewModel

public class ScannerViewModel extends ViewModel {

    private MutableLiveData<List<ProductScan>> scanListLD;

    public ScannerViewModel() {   
        scanListLD = new MutableLiveData<>();
        scanListLD.setValue(new ArrayList<ProductScan>());
    }

    public LiveData<List<ProductScan>> getScanList() {
        return scanListLD;
    }

    public void addBarcode(String barcode) {
        List<ProductScan> list = scanListLD.getValue();
        ProductScan scan = null;
        for (ProductScan item : list) {
            if (item.barcode.equals(barcode)) {
                scan = item;
                break;
            }
        }
        if (scan == null) {
            scan = new ProductScan();
            scan.barcode = barcode;
            scan.qt = 0;
            list.add(scan);
        }
        scan.qt += 1;

        scanListLD.postValue(list);
    }
}

Adapter

public class ScannerAdapter extends RecyclerView.Adapter<ScannerAdapter.ViewHolder> {

    private List<ProductScan> scans;

    public interface OnItemClickListener {
        void onItemClick();
    }

    public ScannerAdapter(List<ProductScan> scans) {
        this.scans = scans;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.template_scan, parent, false);
        ScannerAdapter.ViewHolder viewHolder = new ScannerAdapter.ViewHolder(view);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        holder.bindData(scans.get(position), position);
    }

    @Override
    public int getItemCount() {
        return scans != null ? scans.size() : 0;
    }

    //Not used since scans and newScans contain same objects
    public void setScans(final List<ProductScan> newScans) {
        if (scans == null) {
            scans = new ArrayList<ProductScan>();
            scans.addAll(newScans);
            notifyItemRangeInserted(0, newScans.size());
        } else {
            DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() {
                @Override
                public int getOldListSize() {
                    return scans != null ? scans.size() : 0;
                }

                @Override
                public int getNewListSize() {
                    return newScans != null ? newScans.size() : 0;
                }

                @Override
                public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
                    ProductScan oldScan = scans.get(oldItemPosition);
                    ProductScan newScan = newScans.get(newItemPosition);
                    return Utils.equals(oldScan.barcode,newScan.barcode);
                }

                @Override
                public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
                    ProductScan oldScan = scans.get(oldItemPosition);
                    ProductScan newScan = newScans.get(newItemPosition);
                    return Utils.equals(newScan.barcode, oldScan.barcode)
                            && Utils.equals(newScan.productId, oldScan.productId)
                            && Utils.equals(newScan.productDescription, oldScan.productDescription)
                            && Utils.equals(newScan.qt, oldScan.qt);
                }
            });
            scans.clear();
            scans.addAll(newScans);
            result.dispatchUpdatesTo(this);
        }
    }

    public class ViewHolder extends RecyclerView.ViewHolder {

        private TemplateScanBinding binding;

        public ViewHolder(View itemView) {
            super(itemView);
            binding = DataBindingUtil.bind(itemView);
        }

        public void bindData(ProductScan scan, int position) {
            binding.setScan(scan);
            binding.setBtnIncreaseQtListener(() -> {
                scan.qt += 1;
                notifyItemChanged(position);
            });
            binding.setBtnDecreaseQtListener(() -> {
                scan.qt = Math.max(1, scan.qt - 1);
                notifyItemChanged(position);
            });
            binding.edtQt.setOnEditorActionListener(new TextView.OnEditorActionListener() {
                @Override
                public boolean onEditorAction(TextView textView, int i, KeyEvent keyEvent) {
                    if (i == EditorInfo.IME_ACTION_DONE) {
                        binding.edtQt.clearFocus();
                    }
                    return false;
                }
            });
            binding.edtQt.setOnFocusChangeListener(new View.OnFocusChangeListener() {
                @Override
                public void onFocusChange(View view, boolean b) {
                    if (scan.qt == null || scan.qt < 1) {
                        scan.qt = 1;
                        notifyItemChanged(position);
                    }
                    InputMethodManager imm = (InputMethodManager) binding.getRoot().getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
                    imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
                }
            });
        }
    }
}

Activity

public class ScannerActivity extends ScannerBaseActivity {

    @Inject
    ViewModelFactory viewModelFactory;
    private ScannerViewModel viewModel;

    private ActivityScannerBinding binding;
    private ScannerAdapter adapter;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ((MyApplication) getApplication()).getComponent().inject(this);
        viewModel = ViewModelProviders.of(this, viewModelFactory).get(ScannerViewModel.class);
        binding = DataBindingUtil.setContentView(this, R.layout.activity_scanner);

        adapter = new ScannerAdapter(viewModel.getScanList().getValue());
        binding.recvScans.setLayoutManager(new LinearLayoutManager(this));
        binding.recvScans.setAdapter(adapter);
        viewModel.getScanList().observe(this, (list) -> {
            adapter.notifyDataSetChanged();
        });

        binding.btnScan.setOnClickListener((v) -> {
             //calls viewModel.addBarcode(String barcode)
              readBarcode(readBarcodeCallback);
        });

    }
}
Phantômaxx
  • 37,901
  • 21
  • 84
  • 115
user2692281
  • 51
  • 1
  • 3
  • `addBarCode` is not even called and only `ScannerViewModel` constructor is the only one setting the value to your `MutableLiveData`. – Enzokie Oct 16 '17 at 13:53
  • 1
    For the sake of readability I omitted some code, `viewModel.addBarcode(barcode)` is called by the listener set to button `btnScan` (see Activity). Furthermore `ScannerViewModel.addBarcode(String barcode)` calls `scanListLD.postValue(list)` which notify observers. – user2692281 Oct 16 '17 at 14:06
  • Does this answer your question? [PagedListAdapter Not Using DiffUtil On invalidate Data](https://stackoverflow.com/questions/58016613/pagedlistadapter-not-using-diffutil-on-invalidate-data) – tir38 Jan 09 '20 at 22:28

2 Answers2

1

You can use PagedListAdapterHelper in your existing adapter as shown below

class UserAdapter extends RecyclerView.Adapter<UserViewHolder> {
     private final PagedListAdapterHelper<User> mHelper;
     public UserAdapter(PagedListAdapterHelper.Builder<User> builder) {
         mHelper = new PagedListAdapterHelper(this, DIFF_CALLBACK);
     }
     @Override
     public int getItemCount() {
         return mHelper.getItemCount();
     }
     public void setList(PagedList<User> pagedList) {
         mHelper.setList(pagedList);
     }
     @Override
     public void onBindViewHolder(UserViewHolder holder, int position) {
         User user = mHelper.getItem(position);
         if (user != null) {
             holder.bindTo(user);
         } else {
             // Null defines a placeholder item - PagedListAdapterHelper will automatically
             // invalidate this row when the actual object is loaded from the database
             holder.clear();
         }
     }
     public static final DiffCallback<User> DIFF_CALLBACK = new DiffCallback<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

User PageListAdapter

class UserAdapter extends PagedListAdapter<User, UserViewHolder> {
     public UserAdapter() {
         super(DIFF_CALLBACK);
     }
     @Override
     public void onBindViewHolder(UserViewHolder holder, int position) {
         User user = getItem(position);
         if (user != null) {
             holder.bindTo(user);
         } else {
             // Null defines a placeholder item - PagedListAdapter will automatically invalidate
             // this row when the actual object is loaded from the database
             holder.clear();
         }
     }
     public static final DiffCallback<User> DIFF_CALLBACK = new DiffCallback<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);
         }
     }
  • 1
    A link to a solution is welcome, but please ensure your answer is useful without it: [add context around the link](//meta.stackexchange.com/a/8259) so your fellow users will have some idea what it is and why it’s there, then quote the most relevant part of the page you're linking to in case the target page is unavailable. [Answers that are little more than a link may be deleted.](//stackoverflow.com/help/deleted-answers) – Filnor Dec 04 '17 at 11:09
-1

try to make a deep copy of your list before calling the Adapter. then you can use difutil to update your recycler. for more information about deep copy refer to this link.

Amir jodat
  • 569
  • 6
  • 13