Search code examples
androidandroid-edittextandroid-recyclerviewandroid-textwatcher

notifyDataSetHasChanged() throws an error when used inside TextWatcher.onTextChanged() inside a RecyclerView


I'm building an app that allows user to have a real time base-conversion of a number. Users input their number in an edit text, and they choose the base using plus and minus button. The problem I encountered so far is providing the real time conversion. All the editText inside the recycler view set their text to a BigInteger that can be converted depending on their base.

My idea was to update the BigInteger as the user is inputting a new number. Therefore every time users input a digit I should be able to update the BigInteger, notify the recycler view that the data as changed and then the edit text views should update automatically. Here's my ConvertViewHolder

    public ConvertViewHolder(final View itemView) {
        super(itemView);
        mBaseTextView = (TextView) itemView.findViewById(R.id.baseLabel);
        mEditText = (EditText) itemView.findViewById(R.id.numberEditText);
        mMinusButton = (Button) itemView.findViewById(R.id.minusButton);
        mPlusButton = (Button) itemView.findViewById(R.id.plusButton);
        mRemoveButton = (ImageButton)itemView.findViewById(R.id.removeButton);

        mMinusButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (mRoot.get(getPosition()) > MIN_BASE) {
                    mRoot.set(getPosition(), (mRoot.get(getPosition()) - 1));
                    notifyDataSetChanged();
                }
            }
        });

        mPlusButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (mRoot.get(getPosition()) < MAX_BASE){
                    mRoot.set(getPosition(), (mRoot.get( getPosition() ) +1));
                    notifyDataSetChanged();
                }
            }
        });

        mRemoveButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                mRoot.remove(getPosition());
                notifyDataSetChanged();
            }
        });

        mEditText.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
            }

            @Override
            public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
            }

            @Override
            public void afterTextChanged(Editable editable) {
                if(editable.toString().length() > 0) {
                    // change Big Integer
                    mNumber.setDecimalNumber(editable.toString(), mRoot.get( getPosition() ) );
                    // notify change
                    notifyDataSetChanged();
                }
            }
        });


        // TODO: convert numbers at the same time

    }

    public void bindConverter(final int root){
        mBaseTextView.setText(String.format("%02d", root));

        // String containing all the allowed digits depending on base
        String digits = mNumber.getScaleFromBase(root);

        if (root < 11) {
            // filter input
            mEditText.setInputType(InputType.TYPE_CLASS_NUMBER |
                    InputType.TYPE_TEXT_FLAG_MULTI_LINE);
            mEditText.setKeyListener(DigitsKeyListener.getInstance(digits));
            mEditText.setSingleLine(false);
            mEditText.setMaxLines(2);

        } else {
            mEditText.setInputType(InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS |
                    InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
            mEditText.setSingleLine(false);
            mEditText.setMaxLines(2);
            mEditText.setImeOptions(EditorInfo.IME_FLAG_NO_ENTER_ACTION);
            // TODO: filter input for base higher than 10
        }

        // set editText to BigInteger displaying it in the correct base
        // i.e. if the BigInteger is "8" it will be displayed as 8 if the base is 10
        // and as 1000 if the base is 2

        mEditText.setText(mNumber.getDecimalNumber(mRoot.get( getPosition() )));
    }
}

But apparently I am not allowed to call notifySetDataHasChanged inside the TextWatcher.onTextChanged() as the compiler throws me this error:

 java.lang.IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling

which is pretty self-explanatory, but unfortunately I haven't figured out a possible workaround.


Solution

  • view.post(
    
       new Runnable() {
    
            public void run() { notifyDatasetChanged(); };
       }
    
    
    );
    

    self explanatory:

    call this method after RecyclerView compute a layout or scrolling (in next loop)

    more explanation:

    • one thread = code flow synchronous
    • main thread = looper
    • looper = message que = runnable = loop

    other possible solution ??

    call after u exit from recycler view method

    ps.

    one more hint if this "piece of code" will run more then once pull the runnable in higher stack existence place (in GC scope of RecyclerView) for less usage of resources and computing time :)

    private Runnable r = new Runnable {
         public void run() { notifyDatasetChanged(); }
    }  
     ...
    view.post(r);
     ...
    

    or for generic solution pull in method using interface

    public Runnable postNotify(ListAdapter la) {
         return = new Runnable {
             public void run() { la.notifyDatasetChanged(); }
         };
     }  
    
     ...
     private Runnable changed = postNotify(adapter); 
     ...
     view.pos(changed);