RecyclerView adds items multiple times
<p>I use a <code>RecyclerView</code> with a <code>CardLayout</code> and the <code>CardLayout</code> contains multiple components. For filling the <code>RecyclerView</code> I use an <code>Adapter</code> but my problem is, that sometimes when I add new items the item is added multiple times into the <code>RecyclerView</code>. But it is no behavior which is always reproducible (so maybe it has to do with threading?).</p>

<p>I don't know the reason :/ I would be very glad for each hint or advice.</p>

<p>The code below is a "nearly" working TodoManager, the only problem is the one explained above.</p>

<p>My <code>MainActivity</code>:</p>

<pre><code>public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    private static String fileName = "todos.ser";

    private ArrayList&lt;Todo&gt; todos;
    private RecyclerViewAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_layout);

        updateTodos();
        adapter = new RecyclerViewAdapter(todos, getWindow());

        final RecyclerView recyclerView = (RecyclerView) findViewById(R.id.todos);
        assert recyclerView != null;
        final LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
        recyclerView.setLayoutManager(linearLayoutManager);
        recyclerView.setHasFixedSize(true);
        recyclerView.setItemAnimator(new DefaultItemAnimator());
        recyclerView.setAdapter(adapter);

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        assert fab != null;
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                adapter.addTodo(new Todo(""), todos.size());
                int count = adapter.getItemCount();
                recyclerView.smoothScrollToPosition(count - 1);
            }
        });
    }

    private void updateTodos() {
        todos = new ArrayList&lt;&gt;();
        todos.add(new Todo(""));
    }

    @Override
    public void onStop() {
        super.onStop();

        Log.i(TAG, "save todos=" + todos.size());
        // Save logic
    }
}
</code></pre>

<p>The <code>Todo</code>:</p>

<pre><code>public class Todo implements Serializable {

    // The creationDate is not used at the moment
    private Date creationDate;
    private String description;
    private boolean isChecked;

    public Todo(String description) {
        this.description = description;
        this.creationDate = new Date();
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public boolean isChecked() {
        return isChecked;
    }

    public void setIsChecked(boolean isChecked) {
        this.isChecked = isChecked;
    }

    public Date getCreationDate() {
        return creationDate;
    }
}
</code></pre>

<p>The <code>RecyclerViewAdapter</code>:</p>

<pre><code>public class RecyclerViewAdapter extends RecyclerView.Adapter&lt;RecyclerViewAdapter.TodoViewHolder&gt; {

    private static final String TAG = "RecyclerViewAdapter";

    private final List&lt;Todo&gt; todos;
    private final Window window;

    public RecyclerViewAdapter(List&lt;Todo&gt; todos, Window window) {
        this.todos = todos;
        this.window = window;
    }

    public class TodoViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, CompoundButton.OnCheckedChangeListener, TextWatcher {
        CheckBox cbDone;
        EditText tvDescription;
        FloatingActionButton btnDelete;

        public TodoViewHolder(View itemView) {
            super(itemView);

            cbDone = (CheckBox)itemView.findViewById(R.id.cbDone);
            tvDescription = (EditText) itemView.findViewById(R.id.tvDescription);
            btnDelete = (FloatingActionButton) itemView.findViewById(R.id.btnDelete);

            tvDescription.addTextChangedListener(this);
            cbDone.setOnCheckedChangeListener(this);
            btnDelete.setOnClickListener(this);
        }

        @Override
        public void onClick(View v) {
            Log.i(TAG, "onClick called: remove todo.");
            removeTodo(getAdapterPosition());
        }

        @Override
        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
            Log.i(TAG, "onCheckedChanged called: isDone=" + isChecked);
            Todo todo = getTodo(getAdapterPosition());
            todo.setIsChecked(isChecked);
        }

        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            // Do nothing
        }

        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
            // Do nothing
        }

        @Override
        public void afterTextChanged(Editable s) {
            Log.i(TAG, "afterTextChanged called: text=" + s.toString());
            Todo todo = getTodo(getAdapterPosition());
            todo.setDescription(s.toString());
        }
    }

    public void addTodo(Todo todo, int position) {
        if (position &lt; 0 || position &gt; todos.size()) {
            Log.e(TAG, " add: index=" + position);
        } else {
            todos.add(position, todo);
            notifyItemInserted(position);
        }
    }

    public void removeTodo(int position) {
        if (position &lt; 0 || position &gt;= todos.size()) {
            Log.e(TAG, "remove: index=" + position);
        } else {
            todos.remove(position);
            notifyItemRemoved(position);
        }
    }

    public Todo getTodo(int position) {
        Todo todo = null;
        if (position &lt; 0 || position &gt;= todos.size()) {
            Log.e(TAG, "get: index=" + position);
        } else {
            todo = todos.get(position);
        }
        return todo;
    }

    @Override
    public TodoViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
        View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.todo_layout, viewGroup, false);
        return new TodoViewHolder(view);
    }

    @Override
    public void onBindViewHolder(TodoViewHolder holder, int position) {
        holder.cbDone.setChecked(todos.get(position).isChecked());
        holder.tvDescription.setText(todos.get(position).getDescription());

        if(holder.tvDescription.requestFocus()) {
            window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
        }
    }

    @Override
    public int getItemCount() {
        return todos.size();
    }
}
</code></pre>

<p>The <code>main_layout</code>:</p>

<pre><code>&lt;RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/transparent"&gt;

    &lt;RelativeLayout
        android:id="@+id/header_layout"
        android:layout_width="match_parent"
        android:layout_height="@dimen/app_bar_height"
        android:background="@color/colorPrimary"
        android:layout_alignParentTop="true"&gt;

        &lt;TextView
            android:id="@+id/header"
            android:layout_height="match_parent"
            android:text="@string/todomanager"
            android:textColor="@android:color/white"
            android:layout_width="wrap_content"
            android:layout_alignParentStart="true"
            android:layout_marginStart="@dimen/margin_header_text"
            android:textSize="@dimen/header_text_size"
            android:gravity="center_vertical" /&gt;

        &lt;android.support.design.widget.FloatingActionButton
            android:id="@+id/fab"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_margin="@dimen/margin_header_button"
            app:backgroundTint="@color/colorAccent"
            android:tint="@android:color/white"
            app:fabSize="mini"
            android:src="@android:drawable/ic_input_add"
            android:layout_alignParentEnd="true" /&gt;
    &lt;/RelativeLayout&gt;

    &lt;android.support.v7.widget.RecyclerView
        android:id="@+id/todos"
        android:layout_below="@id/header_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="vertical" /&gt;

&lt;/RelativeLayout&gt; 
</code></pre>

<p>The layout for the <code>CardView</code>:</p>

<pre><code>&lt;android.support.v7.widget.CardView
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/todo"
    android:layout_below="@id/header_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    card_view:cardElevation="5dp"
    android:layout_marginLeft="10dp"
    android:layout_marginRight="10dp"
    android:layout_marginBottom="5dp"
    android:layout_marginTop="5dp"&gt;

    &lt;RelativeLayout
        android:layout_width="fill_parent"
        android:layout_height="match_parent"&gt;

        &lt;CheckBox
            android:id="@+id/cbDone"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentStart="true"
            android:layout_centerVertical="true" /&gt;

        &lt;EditText
            android:id="@+id/tvDescription"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_toStartOf="@+id/btnDelete"
            android:layout_toEndOf="@+id/cbDone"
            android:hint="@string/insert_description"
            android:background="@android:color/transparent"
            android:padding="@dimen/padding_todo_text"
            android:layout_centerVertical="true"
            /&gt;

        &lt;android.support.design.widget.FloatingActionButton
            android:id="@+id/btnDelete"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="@dimen/margin_todo_button"
            app:fabSize="mini"
            app:backgroundTint="@color/colorAccent"
            android:tint="@android:color/white"
            android:src="@android:drawable/ic_menu_delete"
            android:layout_alignParentEnd="true"
            android:layout_centerVertical="true" /&gt;
    &lt;/RelativeLayout&gt;
&lt;/android.support.v7.widget.CardView&gt;
</code></pre>

<p>The following code is needed for the styling:
<code>dimens.xml</code>:</p>

<pre><code>&lt;resources&gt;
    &lt;dimen name="app_bar_height"&gt;60dp&lt;/dimen&gt;
    &lt;dimen name="fab_margin"&gt;16dp&lt;/dimen&gt;
    &lt;dimen name="text_margin"&gt;16dp&lt;/dimen&gt;
    &lt;dimen name="margin_header_text"&gt;10sp&lt;/dimen&gt;
    &lt;dimen name="header_text_size"&gt;24sp&lt;/dimen&gt;
    &lt;dimen name="margin_header_button"&gt;10sp&lt;/dimen&gt;
    &lt;dimen name="padding_todo_text"&gt;10dp&lt;/dimen&gt;
    &lt;dimen name="margin_todo_button"&gt;10sp&lt;/dimen&gt;
&lt;/resources&gt;
</code></pre>

<p><code>colors.xml</code>:</p>

<pre><code>&lt;resources&gt;
    &lt;color name="colorPrimary"&gt;#212121&lt;/color&gt;
    &lt;color name="colorPrimaryDark"&gt;#000000&lt;/color&gt;
    &lt;color name="colorAccent"&gt;#009688&lt;/color&gt;
&lt;/resources&gt;
</code></pre>

<p><code>strings.xml</code>:</p>

<pre><code>&lt;resources&gt;
    &lt;string name="todomanager"&gt;Todos&lt;/string&gt;
    &lt;string name="insert_description"&gt;insert description ...&lt;/string&gt;
&lt;/resources&gt;
</code></pre>

<p>The following screenshots displays the wrong behavior. The left one is correct, the middle one is still correct but when there starts scrolling some entries are duplicated in the view.</p>

<p><a href="https://i.stack.imgur.com/gQmRlm.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/gQmRlm.png" alt="Correct behavior at the beginning"></a>
<a href="https://i.stack.imgur.com/z8kg4m.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/z8kg4m.png" alt="Still correct behavior"></a>
<a href="https://i.stack.imgur.com/YEtI3m.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/YEtI3m.png" alt="Here you can see, that some entries are duplicated in the list"></a></p>

<p>After some tests I discovered that these entries are the same entries, so when I click once on the delete of an entry all duplicated entries also disappear. And the problem starts whit scrolling as far as I have some entries out of view and I scroll around the displayed entries are duplicated.</p>

<p>I also discovered that it starts when I add the 12th element:</p>

<p>The logcat output before the 12th element was added:</p>

<pre><code>I/MainActivity: fab.onClick count=[7]
I/RecyclerViewAdapter: afterTextChanged called: text= position=[6]
I/RecyclerViewAdapter: afterTextChanged called: text=t position=[6]
I/RecyclerViewAdapter: afterTextChanged called: text=te position=[6]
I/RecyclerViewAdapter: afterTextChanged called: text=tes position=[6]
I/RecyclerViewAdapter: afterTextChanged called: text=test position=[6]
I/RecyclerViewAdapter: afterTextChanged called: text=test5 position=[6]
I/RecyclerViewAdapter: addTodo position=[7] count=[7]
I/MainActivity: fab.onClick count=[8]
I/RecyclerViewAdapter: afterTextChanged called: text= position=[7]
I/RecyclerViewAdapter: afterTextChanged called: text=t position=[7]
I/RecyclerViewAdapter: afterTextChanged called: text=te position=[7]
I/RecyclerViewAdapter: afterTextChanged called: text=tes position=[7]
I/RecyclerViewAdapter: afterTextChanged called: text=test position=[7]
I/RecyclerViewAdapter: afterTextChanged called: text=test6 position=[7]
I/RecyclerViewAdapter: addTodo position=[8] count=[8]
I/MainActivity: fab.onClick count=[9]
I/RecyclerViewAdapter: afterTextChanged called: text= position=[8]
I/RecyclerViewAdapter: afterTextChanged called: text=t position=[8]
I/RecyclerViewAdapter: afterTextChanged called: text=te position=[8]
I/RecyclerViewAdapter: afterTextChanged called: text=tes position=[8]
I/RecyclerViewAdapter: afterTextChanged called: text=test position=[8]
I/RecyclerViewAdapter: afterTextChanged called: text=test7 position=[8]
I/RecyclerViewAdapter: addTodo position=[9] count=[9]
</code></pre>

<p>The first strange behavior starts when adding the 10th item, because there are <code>afterTextChanged</code> calls which should not be there and also the <code>GC</code> free:</p>

<pre><code>I/MainActivity: fab.onClick count=[10]
I/RecyclerViewAdapter: afterTextChanged called: text= position=[9]
I/RecyclerViewAdapter: afterTextChanged called: text=t position=[9]
I/RecyclerViewAdapter: afterTextChanged called: text=te position=[9]
I/RecyclerViewAdapter: afterTextChanged called: text=tes position=[9]
I/RecyclerViewAdapter: afterTextChanged called: text=test position=[9]
I/RecyclerViewAdapter: afterTextChanged called: text=test8 position=[9]
I/RecyclerViewAdapter: afterTextChanged called: text=test3 position=[4]
I/RecyclerViewAdapter: afterTextChanged called: text=test1 position=[2]
I/RecyclerViewAdapter: afterTextChanged called: text=training position=[1]
I/RecyclerViewAdapter: afterTextChanged called: text=work position=[0]
I/art: Background partial concurrent mark sweep GC freed 52541(3MB) AllocSpace objects, 1(52KB) LOS objects, 35% free, 28MB/44MB, paused 7.486ms total 62.227ms
I/RecyclerViewAdapter: addTodo position=[10] count=[10]
</code></pre>

<p>And when adding the 11th and 12th item the <code>afterTextChanged</code> calls explode and the strange behavior starts:</p>

<pre><code>I/MainActivity: fab.onClick count=[11]
I/RecyclerViewAdapter: afterTextChanged called: text= position=[10]
I/RecyclerViewAdapter: afterTextChanged called: text=t position=[10]
I/RecyclerViewAdapter: afterTextChanged called: text=te position=[10]
I/RecyclerViewAdapter: afterTextChanged called: text=tes position=[10]
I/RecyclerViewAdapter: afterTextChanged called: text=test position=[10]
I/RecyclerViewAdapter: afterTextChanged called: text=test9 position=[10]
I/RecyclerViewAdapter: afterTextChanged called: text=test3 position=[4]
I/RecyclerViewAdapter: afterTextChanged called: text=test1 position=[2]
I/RecyclerViewAdapter: afterTextChanged called: text=training position=[1]
I/RecyclerViewAdapter: afterTextChanged called: text=work position=[0]
I/RecyclerViewAdapter: afterTextChanged called: text=work position=[0]
I/RecyclerViewAdapter: afterTextChanged called: text=test7 position=[8]
I/RecyclerViewAdapter: afterTextChanged called: text=test9 position=[10]
I/RecyclerViewAdapter: afterTextChanged called: text=training position=[1]
I/RecyclerViewAdapter: addTodo position=[11] count=[11]
I/MainActivity: fab.onClick count=[12]
I/RecyclerViewAdapter: afterTextChanged called: text= position=[11]
I/RecyclerViewAdapter: afterTextChanged called: text=t position=[11]
I/RecyclerViewAdapter: afterTextChanged called: text=te position=[11]
I/RecyclerViewAdapter: afterTextChanged called: text=tes position=[11]
I/RecyclerViewAdapter: afterTextChanged called: text=test position=[11]
I/RecyclerViewAdapter: afterTextChanged called: text=test1 position=[11]
I/RecyclerViewAdapter: afterTextChanged called: text=test10 position=[11]
I/RecyclerViewAdapter: afterTextChanged called: text=test5 position=[6]
I/RecyclerViewAdapter: afterTextChanged called: text=test3 position=[4]
I/RecyclerViewAdapter: afterTextChanged called: text=test2 position=[3]
I/RecyclerViewAdapter: afterTextChanged called: text=test1 position=[2]
I/RecyclerViewAdapter: afterTextChanged called: text=training position=[1]
I/RecyclerViewAdapter: afterTextChanged called: text=work position=[0]
I/RecyclerViewAdapter: afterTextChanged called: text=work position=[0]
I/RecyclerViewAdapter: afterTextChanged called: text=training position=[1]
I/RecyclerViewAdapter: afterTextChanged called: text=work position=[0]
I/RecyclerViewAdapter: afterTextChanged called: text=test6 position=[7]
I/RecyclerViewAdapter: afterTextChanged called: text=test8 position=[9]
I/RecyclerViewAdapter: afterTextChanged called: text=test9 position=[10]
I/RecyclerViewAdapter: afterTextChanged called: text=test10 position=[11]
I/RecyclerViewAdapter: afterTextChanged called: text=test10 position=[11]
I/RecyclerViewAdapter: afterTextChanged called: text=test9 position=[10]
I/RecyclerViewAdapter: afterTextChanged called: text=test10 position=[11]
I/RecyclerViewAdapter: afterTextChanged called: text=test4 position=[5]
I/RecyclerViewAdapter: afterTextChanged called: text=test2 position=[3]
I/RecyclerViewAdapter: afterTextChanged called: text=test1 position=[2]
I/RecyclerViewAdapter: afterTextChanged called: text=training position=[1]
I/RecyclerViewAdapter: afterTextChanged called: text=work position=[0]
I/RecyclerViewAdapter: afterTextChanged called: text=work position=[0]
I/RecyclerViewAdapter: afterTextChanged called: text=training position=[1]
I/RecyclerViewAdapter: afterTextChanged called: text=work position=[0]
I/RecyclerViewAdapter: afterTextChanged called: text=test1 position=[2]
I/RecyclerViewAdapter: afterTextChanged called: text=training position=[1]
I/RecyclerViewAdapter: afterTextChanged called: text=work position=[0]
I/RecyclerViewAdapter: afterTextChanged called: text=work position=[0]
</code></pre>

<p>Another strange behavior which I found is that my data is correct and when I go into the edit mode the data looks wrong and when I leave edit mode it looks okay again.</p>

<p><a href="https://i.stack.imgur.com/do0I4m.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/do0I4m.png" alt="expected entries"></a>
<a href="https://i.stack.imgur.com/UEodWm.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/UEodWm.png" alt="duplicated entries in edit mode"></a>
<a href="https://i.stack.imgur.com/do0I4m.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/do0I4m.png" alt="expected entries again"></a></p>