Search code examples
javaandroidandroid-fragmentsarraylistbundle

Why ArrayList in Bundle persists?


SetUp:

Activity with one fragment which is instantiated with button click. In fragment's constructor Bundle is used. In Bundle a String (surname) and an ArrayList<String> (fornames) are set. Fragment gets detached through callback.

Problem: When fragment is detached the String (surname) gets destroyed as expected, but the ArrayList persists. In consequence the former ArrayList entries appears when a new instance of the fragment is called. The callback isn't the problem. The behavior appears without the callback, too.

I have checked the variables (surname = Black and fornename = Joe) with Log.d at the points fragment constructor (FRAG_CONSTRUCTOR), fragment getArguments() in onCreate method (FRAG_ARGS_ONCREATE) and in fragments callback (FRAG_CALLBACK). The surname Black isn't as expected shown in LogCat, but the forname Joe persists.

Activity:

public class MainActivity extends AppCompatActivity implements FragRecycler.FragRecyclerCallBackListener {

    private Button button;
    private String surname;
    private ArrayList<String> fornames;

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

        fornames = new ArrayList<String>();
        button = (Button) findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                addFragmentWithTransition(R.id.container, FragRecycler.newInstance(surname, fornames), "FRAG_RECYCLER");
            }
        });

    }

    public void addFragmentWithTransition(int containerViewId, Fragment fragment, String fragmentTag) {
        getSupportFragmentManager()
                .beginTransaction()
                .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
                .add(containerViewId, fragment, fragmentTag)
                .addToBackStack(fragmentTag)
                .commit();
    }

    @Override
    public void onFragRecyclerCallback() {
        Log.d("FRAG_CALLBACK", "forname: " + fornames + " surname: " + surname);
        getSupportFragmentManager().popBackStack();
    }

    @Override
    public void onBackPressed() {
        if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
            getSupportFragmentManager().popBackStack();
        } else {
            super.onBackPressed();
        }
    }
}

Fragment:

public class FragRecycler extends Fragment {

    private View v;
    private Toolbar toolbar;
    private TextInputEditText vSurname;
    private RecyclerView rvForenames;
    private AdapterForName adapter;

    private FragRecyclerCallBackListener callback;

    public interface FragRecyclerCallBackListener {
        void onFragRecyclerCallback();
    }


    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        if (context instanceof AppCompatActivity){
            try {
                callback = (FragRecyclerCallBackListener) context;
            } catch (ClassCastException e) {
                throw new ClassCastException(context.toString() + " must implement FragRecyclerCallBackListener");
            }
        }
    }

    @Override
    public void onDetach() {
        super.onDetach();
        callback = null;
    }

    public static FragRecycler newInstance(String surname, ArrayList<String> fornames) {
        Log.d("FRAG_CONSTRUCTOR", "forname: " + fornames + " surname: " + surname);
        FragRecycler p = new FragRecycler();
        Bundle b = new Bundle();
        b.putString("SURNAME", surname);
        b.putStringArrayList("FORNAMES", fornames);
        p.setArguments(b);

        return p;
    }

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

        Log.d("FRAG_ARGS_ONCREATE", "forname: " + getArguments().getStringArrayList("FORNAMES") + " surname: " + getArguments().getString("SURNAME"));
        v = inflater.inflate(R.layout.frag_recycler, container, false);
        toolbar = (Toolbar) v.findViewById(R.id.toolbar);

        vSurname = (TextInputEditText) v.findViewById(R.id.surname);
        rvForenames = (RecyclerView) v.findViewById(R.id.rv_forenames);

        vSurname.setText(getArguments().getString("SURNAME"));
        adapter = new AdapterForName("Forename", getArguments().getStringArrayList("FORNAMES"));
        rvForenames.setAdapter(adapter);
        rvForenames.setLayoutManager(new LinearLayoutManager(getContext()));

        toolbar.setNavigationIcon(ContextCompat.getDrawable(getContext(), R.drawable.ic_clear));
        toolbar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
               callback.onFragRecyclerCallback();
            }
        });

        return v;
    }


    class AdapterForName extends RecyclerView.Adapter<AdapterForName.ViewHolder> {

        private ArrayList<String> names;
        private String callType;

        public AdapterForName(String callType, ArrayList<String> names) {
            this.callType = callType;
            this.names = names;
            if (names.size() == 0) {
                addEmptyEntryToList();
            } else {
                if (!(names.get(names.size() - 1).trim().length() < 1)) {
                    addEmptyEntryToList();
                }
            }
        }

        protected void addEmptyEntryToList() {
            names.add("");
            notifyDataSetChanged();
        }

        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

            View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.name, parent, false);
            return new ViewHolder(v);
        }

        @Override
        public void onBindViewHolder(final ViewHolder holder,  int position) {

            if (position == 0) {
                holder.inputLayout.setHint(callType);
            } else {
                holder.inputLayout.setHint(String.valueOf(position + 1) + " th." + " " + callType);
            }

            holder.input.setText(names.get(position));
            holder.input.setTag(position);
            holder.input.addTextChangedListener(new TextWatcher() {

                public void afterTextChanged(Editable s) {

                    final String inputText = s.toString().trim();

                    names.set(holder.getAdapterPosition(), inputText);

                }

                public void beforeTextChanged(CharSequence s, int start,
                                              int count, int after) {

                }

                public void onTextChanged(CharSequence s, int start,
                                          int before, int count) {

                }

            });

            holder.input.setOnFocusChangeListener(new View.OnFocusChangeListener() {
                @Override
                public void onFocusChange(View view, boolean b) {
                    if (!b) { // inputEditText hat keinen Focus mehr.
                        if (holder.input.getText().toString().trim().length() > 0){
                            int count = 0;
                            for (int i = 0; i < names.size(); i++) {
                                if (names.get(i).trim().length() == 0) {
                                    count = count + 1;
                                }
                            }

                            if (count == 0) {
                                names.add("");
                                notifyDataSetChanged();
                            }
                        }
                        if (holder.input.getText().toString().trim().length() == 0){
                            int count = 0;
                            for (int i = 0; i < names.size(); i++) {
                                if (names.get(i).trim().length() == 0) {
                                    count = count + 1;
                                }
                            }
                            if (count > 0) {
                                names.remove(holder.getAdapterPosition());
                                notifyDataSetChanged();
                            }
                        }
                    }
                }
            });
        }

        public ArrayList<String> getList() {
            ArrayList<String> trimmedList = new ArrayList<>();
            for (int i = 0; i < names.size(); i++) {
                if (names.get(i).trim().length() > 0) {
                    trimmedList.add(names.get(i));
                }
            }
            return trimmedList;
        }

        @Override
        public int getItemCount() {
            return names.size();
        }


        class ViewHolder extends RecyclerView.ViewHolder {

            public TextInputLayout inputLayout;
            public TextInputEditText input;

            public ViewHolder(View itemView) {
                super(itemView);
                inputLayout = (TextInputLayout) itemView.findViewById(R.id.input_layout);
                input = (TextInputEditText) itemView.findViewById(R.id.input);
            }
        }
    }

}

LogCat:

08-05 21:42:34.387 17055-17055/com.example.user.recyclertest W/art: Before Android 4.1, method int android.support.v7.widget.ListViewCompat.lookForSelectablePosition(int, boolean) would have incorrectly overridden the package-private method in android.widget.ListView

08-05 21:42:36.625 17055-17055/com.example.user.recyclertest D/FRAG_CONSTRUCTOR: forname: [] surname: null
08-05 21:42:36.643 17055-17055/com.example.user.recyclertest D/FRAG_ARGS_ONCREATE: forname: [] surname: null

08-05 21:42:41.852 17055-17055/com.example.user.recyclertest W/IInputConnectionWrapper: finishComposingText on inactive InputConnection

08-05 21:42:41.857 17055-17055/com.example.user.recyclertest W/IInputConnectionWrapper: finishComposingText on inactive InputConnection

08-05 21:42:45.200 17055-17055/com.example.user.recyclertest D/FRAG_CALLBACK: forname: [Joe] surname: null
08-05 21:42:45.486 17055-17055/com.example.user.recyclertest W/IInputConnectionWrapper: finishComposingText on inactive InputConnection

08-05 21:42:45.487 17055-17055/com.example.user.recyclertest W/IInputConnectionWrapper: finishComposingText on inactive InputConnection

08-05 21:43:03.441 17055-17055/com.example.user.recyclertest D/FRAG_CONSTRUCTOR: forname: [Joe, ] surname: null

Solution

  • In your Activity's onCreate() method, you write

    fornames = new ArrayList<String>();
    

    which assigns a new (empty) ArrayList instance to your Activity's forenames instance variable. You then write

    FragRecycler.newInstance(surname, fornames)
    

    which causes fornames to be added to a new Fragment's "arguments" Bundle

        b.putStringArrayList("FORNAMES", fornames);
    

    which eventually is passed to your Adapter's constructor

        adapter = new AdapterForName("Forename", getArguments().getStringArrayList("FORNAMES"));
    

    where your Adapter assigns it to its names instance variable:

            this.names = names;
    

    Your program then goes on to modify the Adapter's names list as the user uses your app.

    The key thing to realize here is that all of these bits of code are talking about the same ArrayList instance. So when your app adds names or removes names from your Adapter's names list, it is also adding and removing names from your Activity's fornames list. This is happening because these two variables are pointing to the same object.

    If you want to make sure that your Fragment can't modify your Activity's fornames list instance, you should change your Fragment's newInstance() method as follows. Replace this

    b.putStringArrayList("FORNAMES", fornames);
    

    with this:

    List<String> fornamesCopy = new ArrayList<>(fornames);
    b.putStringArrayList("FORNAMES", fornamesCopy);
    

    The new keyword means that now your Fragment has a different ArrayList instance than your Activity, so modifying this one won't affect the other. And the particular constructor used here will make sure that this new ArrayList instance still holds all the same values as the original.