wake-0
wake-0

Reputation: 3966

RecyclerView adds items multiple times

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

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

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

My MainActivity:

public class MainActivity extends AppCompatActivity {

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

    private ArrayList<Todo> 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<>();
        todos.add(new Todo(""));
    }

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

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

The Todo:

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;
    }
}

The RecyclerViewAdapter:

public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.TodoViewHolder> {

    private static final String TAG = "RecyclerViewAdapter";

    private final List<Todo> todos;
    private final Window window;

    public RecyclerViewAdapter(List<Todo> 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 < 0 || position > todos.size()) {
            Log.e(TAG, " add: index=" + position);
        } else {
            todos.add(position, todo);
            notifyItemInserted(position);
        }
    }

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

    public Todo getTodo(int position) {
        Todo todo = null;
        if (position < 0 || position >= 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();
    }
}

The main_layout:

<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">

    <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">

        <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" />

        <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" />
    </RelativeLayout>

    <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" />

</RelativeLayout> 

The layout for the CardView:

<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">

    <RelativeLayout
        android:layout_width="fill_parent"
        android:layout_height="match_parent">

        <CheckBox
            android:id="@+id/cbDone"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentStart="true"
            android:layout_centerVertical="true" />

        <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"
            />

        <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" />
    </RelativeLayout>
</android.support.v7.widget.CardView>

The following code is needed for the styling: dimens.xml:

<resources>
    <dimen name="app_bar_height">60dp</dimen>
    <dimen name="fab_margin">16dp</dimen>
    <dimen name="text_margin">16dp</dimen>
    <dimen name="margin_header_text">10sp</dimen>
    <dimen name="header_text_size">24sp</dimen>
    <dimen name="margin_header_button">10sp</dimen>
    <dimen name="padding_todo_text">10dp</dimen>
    <dimen name="margin_todo_button">10sp</dimen>
</resources>

colors.xml:

<resources>
    <color name="colorPrimary">#212121</color>
    <color name="colorPrimaryDark">#000000</color>
    <color name="colorAccent">#009688</color>
</resources>

strings.xml:

<resources>
    <string name="todomanager">Todos</string>
    <string name="insert_description">insert description ...</string>
</resources>

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.

Correct behavior at the beginning Still correct behavior Here you can see, that some entries are duplicated in the list

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.

I also discovered that it starts when I add the 12th element:

The logcat output before the 12th element was added:

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]

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

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]

And when adding the 11th and 12th item the afterTextChanged calls explode and the strange behavior starts:

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]

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.

expected entries duplicated entries in edit mode expected entries again

Upvotes: 0

Views: 2886

Answers (1)

wake-0
wake-0

Reputation: 3966

The problem was in the onBindViewHolder method the focus was set, so when I remove the focus code it works.

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

     // Remove the following code
     // if(holder.tvDescription.requestFocus()) {
     //    window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
     // }
}

Upvotes: 1

Related Questions