1

I am having an issue with an app I am creating.
I created a series of Fragments within a single main Activity.
In each Fragment is either a form for data collection, or a ListView for displaying the colelcted data.

The data manipulation is controlled by the listener methods of several widgets and the data model behind the scenes.

Basically I am using the data collection Fragment to store data in an SQLite database, which is then queried upon travel to a display fragment. The display fragments have buttons for returning to the collection fragments to add, edit, or delete a selected item. In the case of an add/edit button press I would in certain states send a bundle which could be filled differently based on the needs of the user. Typically a bundle contains information sent from a parent context to a child context upon which the child unpacks the bundle and places data in text fields and/or sets spinners containing static data.

I tried using an over complicated approach where I created a new Bundle directly inside whatever widget listener was being used.
But I found that it was creating unnecessary complexity in the following Fragment where I would need to test for keys contained within the Bundles from several different possible origins.

The thought was that inside the beginning of any Fragment I would call

Bundle bundle = this.getArguments()

Which would set bundle to whichever Bundle was passed in:

fragment.setArguments(*bundle based on conditions*)

so from this flow I had to create an incredibly long and hard to read succession of if-conditionals looking for specific keys based on bundle.isEmpty() which of course throws a NullPointerException if you're arriving at that Fragment for the first time and no bundle has been sent yet.
Then to compound the nightmare I threw all of this in a try-catch to handle the bundle.isEmpty() null pointer, which, I tried to use as an indicator of current state (/sigh... I know).

What I would like to do is use Bundle savedInstanceState for any traffic of info/state to a new Fragment.
So ideally I would only ever have to access one Bundle and test for containing keys.
Then it seems that I would be able to test for whether or not savedInstanceState is null and set it somehow.

The problem is that I have these widgets in my Fragments which I am using to interact with my model and possibly navigate to the next necessary Fragment.
If I use savedInstanceState within an onClick listener method of one of the widgets, I am forced to make it final which promptly removes my ability to use savedInstanceStatein any other scope but the initial usage of it or at least that is my understanding.
This causes NullPointerException errors when I try to access savedInstanceStateoutside of the first scope.

If I remove final the IDE tells me (correctly so) that I cannot access savedInstanceState from within an inner class, which in my case is several widgets to which I have attached listener methods.

I have read:

Are the bundles in android permanent in saving activity states ?

How to use the anonymous inner class?

How to use Outer Method's input in Anonymous Inner Class?

Here is a code example of two Fragments reliant upon sending Bundles to each other;

The display and select class:

public class Row_main extends Fragment {

Spinner location_spinner;
Button add_row_btn,edit_row_button,delete_row_btn;

ListView row_list_holder;

StorageHandler sh;
List<Row> row_list;

ArrayAdapter<CharSequence> ls_adapter;
String [] location_test_values = {"test_location"};


boolean has_items;

public Row_main()
{
    //default constructor
}

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


    if(savedInstanceState == null)
    {
        //this will satisfy if arriving here from navigation menu.
        //I would like to set savedInstanceState here if null.
        savedInstanceState = new Bundle();
        //but I get error: 'cannot assign a value to final variable'
    }

    getActionBar().setTitle("Row");

    //Create view
    final View v = inflater.inflate(R.layout.row_main_layout, container, false);

    add_row_btn = (Button) v.findViewById(R.id.add_row_btn);
    edit_row_button = (Button) v.findViewById(R.id.edit_row_btn);
    delete_row_btn = (Button) v.findViewById(R.id.delete_row_btn);

    add_row_btn.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {

            addRow fragment = new addRow();

            FragmentManager fragmentManager = getActivity().getSupportFragmentManager();
            fragment.setArguments(savedInstanceState);
            FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
            fragmentTransaction.replace(R.id.fragment_container, fragment);
            fragmentTransaction.addToBackStack(null);
            fragmentTransaction.commit();

            getActionBar().setTitle("Add Row");
        }
    });

    edit_row_button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            for(int i=0; i<row_list.size(); i++)
            {
                if(row_list.get(i).isSelected())
                {
                    row_list.get(i).setEdited(true);

                    addRow fragment = new addRow();

                    savedInstanceState.putStringArrayList("current_row",row_list.get(i).getList());

                    fragment.setArguments(savedInstanceState);

                    FragmentManager fragmentManager = getActivity().getSupportFragmentManager();
                    FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
                    fragmentTransaction.replace(R.id.fragment_container, fragment);
                    fragmentTransaction.addToBackStack(null);
                    fragmentTransaction.commit();
                    getActionBar().setTitle("Add Row");
                }
            }
        }
    });

    return v;
}

private ActionBar getActionBar() {
    return ((AppCompatActivity) getActivity()).getSupportActionBar();
}

and here is the data collect form class:

public class addRow extends Fragment {

IdGenerator generator;

String new_id;

AlertDialog.Builder alertDialogBuilder, fmdDialogBuilder;

EditText row_name,
         rack_name,
         tile_location,
         tile_name,
         separation,
         bf_offset;

Spinner object_type,
        direction,
        rotation;

ArrayAdapter<CharSequence> objType_adapter,
                     direction_adapter,
                     rotation_adapter;

String[] objtype_values = {"Rack","FMD"};
String[] direction_values = {"Right","Left"};
String[] rotation_values = {"0","90","180","270"};

int RACK = 0,FMD = 1,RIGHT = 0,LEFT = 1;

Row current_row;

ArrayList<String> list_to_unpack;

Button  save_row_btn,
        add_btn,
        cancel_btn;

public addRow() {
    // Required empty public constructor
}


@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                         final Bundle savedInstanceState) {
    // Inflate the layout for this fragment
    View v = inflater.inflate(R.layout.add_row_layout, container, false);

    generator = new IdGenerator();
    new_id = generator.randomString(5);

    if(savedInstanceState != null)
    {
        //Bundle passed, edit button was used, unpack
        if (savedInstanceState.containsKey("current_row")) {
            Log.d("BUNDLE-CONTENTS", "" + savedInstanceState.size());

            list_to_unpack = savedInstanceState.getStringArrayList("current_row");

            current_row.setId(list_to_unpack.get(0));
            current_row.setRow_name(list_to_unpack.get(1));
            current_row.setObject_type(list_to_unpack.get(2));
            current_row.setRack_name(list_to_unpack.get(3));
            current_row.setTile_location(list_to_unpack.get(4));
            current_row.setTile_name(list_to_unpack.get(5));
            current_row.setDirection(list_to_unpack.get(6));
            current_row.setRotation(list_to_unpack.get(7));
            current_row.setSeparation(list_to_unpack.get(8));
            current_row.setBf_offset(list_to_unpack.get(9));
            current_row.setEdited(true);

            row_name.setText(current_row.getRow_name());

            if (current_row.getObject_type().equalsIgnoreCase("Rack")) {
                object_type.setSelection(RACK);
            } else {
                object_type.setSelection(FMD);
            }

            rack_name.setText(current_row.getRack_name());

            tile_location.setText(current_row.getTile_location());

            tile_name.setText(current_row.getTile_name());

            if (current_row.getDirection().equalsIgnoreCase("Right")) {
                direction.setSelection(RIGHT);
            } else {
                direction.setSelection(LEFT);
            }

            switch(current_row.getRotation())
            {
                case "0": rotation.setSelection(0); break;
                case "90": rotation.setSelection(1); break;
                case "180": rotation.setSelection(2); break;
                case "270": rotation.setSelection(3); break;
            }

            separation.setText(current_row.getSeparation());

            bf_offset.setText(current_row.getBf_offset());
        }
    }

    row_name = (EditText) v.findViewById(R.id.rowName_value);
    rack_name = (EditText) v.findViewById(R.id.rackName_value);
    tile_location = (EditText) v.findViewById(R.id.tileLoc_value);
    tile_name = (EditText) v.findViewById(R.id.tileName_value);
    separation = (EditText) v.findViewById(R.id.separation_value);
    bf_offset = (EditText) v.findViewById(R.id.bfOffset_value);

    object_type = (Spinner) v.findViewById(R.id.obj_type);
    direction = (Spinner) v.findViewById(R.id.direction);
    rotation = (Spinner) v.findViewById(R.id.rotation_spinner);

    objType_adapter = new ArrayAdapter<CharSequence>(this.getActivity(),
            android.R.layout.simple_spinner_dropdown_item, objtype_values);
    direction_adapter = new ArrayAdapter<CharSequence>(this.getActivity(),
            android.R.layout.simple_spinner_dropdown_item, direction_values);
    rotation_adapter = new ArrayAdapter<CharSequence>(this.getActivity(),
            android.R.layout.simple_spinner_dropdown_item, rotation_values);

    object_type.setAdapter(objType_adapter);
    direction.setAdapter(direction_adapter);
    rotation.setAdapter(rotation_adapter);

    save_row_btn = (Button) v.findViewById(R.id.save_row_btn);
    add_btn = (Button) v.findViewById(R.id.add_btn);
    cancel_btn = (Button) v.findViewById(R.id.cancel_btn);

    alertDialogBuilder = new AlertDialog.Builder(getActivity());
    fmdDialogBuilder = new AlertDialog.Builder(getActivity());

    alertDialogBuilder.setMessage("Add Rack or FMD:");
    fmdDialogBuilder.setMessage("Add Power Providing or Power Consuming FMD:");

    alertDialogBuilder.setNegativeButton("Rack", new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            //if user selects power providing fmd
            //send id with it
            //StorageHandler sh = new StorageHandler(getActivity());

            Row row = new Row(
                    new_id,
                    row_name.getText().toString(),
                    object_type.getSelectedItem().toString(),
                    rack_name.getText().toString(),
                    tile_location.getText().toString(),
                    tile_name.getText().toString(),
                    direction.getSelectedItem().toString(),
                    rotation.getSelectedItem().toString(),
                    separation.getText().toString(),
                    bf_offset.getText().toString());



            ArrayList<String> rowList = row.getList();

            for(String i : rowList)
            {
                Log.d("ADD ROW BTN","rowList[i]=" + i);
            }

            savedInstanceState.putStringArrayList("current_row", rowList);

            addRack fragment = new addRack();
            fragment.setArguments(savedInstanceState);
            FragmentManager fragmentManager = getActivity().getSupportFragmentManager();
            FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
            fragmentTransaction.replace(R.id.fragment_container, fragment);
            fragmentTransaction.addToBackStack(null);
            fragmentTransaction.commit();
            getActionBar().setTitle("Add Rack to: " + row.getRow_name());
        }
    });

    alertDialogBuilder.setPositiveButton("FMD", new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            //todo choice between consuming and providing
            fmdDialogBuilder.show();
        }
    });

    fmdDialogBuilder.setPositiveButton("Providing", new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {

            Row row = new Row(
                    new_id,
                    row_name.getText().toString(),
                    object_type.getSelectedItem().toString(),
                    rack_name.getText().toString(),
                    tile_location.getText().toString(),
                    tile_name.getText().toString(),
                    direction.getSelectedItem().toString(),
                    rotation.getSelectedItem().toString(),
                    separation.getText().toString(),
                    bf_offset.getText().toString());

            ArrayList<String> obj_as_list = row.getList();

            Log.d("newId", obj_as_list.get(0));
            Log.d("row_name", obj_as_list.get(1));
            Log.d("object_type", obj_as_list.get(2));
            Log.d("rack_name", obj_as_list.get(3));
            Log.d("objaslistsize", ""+obj_as_list.size());

            savedInstanceState.putStringArrayList("current_row", obj_as_list);

            addPowerProvidingFMD fragment = new addPowerProvidingFMD();

            fragment.setArguments(savedInstanceState);
            FragmentManager fragmentManager = getActivity().getSupportFragmentManager();
            FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
            fragmentTransaction.replace(R.id.fragment_container, fragment);
            fragmentTransaction.addToBackStack(null);
            fragmentTransaction.commit();
            getActionBar().setTitle("add FMD to: " + row_name.getText().toString());
        }
    });

    fmdDialogBuilder.setNegativeButton("Consuming", new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {

            Row row = new Row(
                    new_id,
                    row_name.getText().toString(),
                    object_type.getSelectedItem().toString(),
                    rack_name.getText().toString(),
                    tile_location.getText().toString(),
                    tile_name.getText().toString(),
                    direction.getSelectedItem().toString(),
                    rotation.getSelectedItem().toString(),
                    separation.getText().toString(),
                    bf_offset.getText().toString());

            ArrayList<String> obj_as_list = row.getList();

            savedInstanceState.putStringArrayList("current_row", obj_as_list);

            addPowerConsumingFMD fragment = new addPowerConsumingFMD();
            fragment.setArguments(savedInstanceState);
            FragmentManager fragmentManager = getActivity().getSupportFragmentManager();
            FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
            fragmentTransaction.replace(R.id.fragment_container, fragment);
            fragmentTransaction.addToBackStack(null);
            fragmentTransaction.commit();
            getActionBar().setTitle("add FMD to: " + row_name.getText().toString());
        }
    });

    save_row_btn.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            //save row info to database
            StorageHandler sh = new StorageHandler(getActivity());

            if(current_row.isEdited())
            {
                //update row
                String current_id = current_row.getId();
                Log.d("*update row*", "id=" + current_id);

                //get updated values from fields, send to database
                Row row = new Row(current_id, row_name.getText().toString(), object_type.getSelectedItem().toString(),
                        rack_name.getText().toString(), tile_location.getText().toString(),
                        tile_name.getText().toString(), direction.getSelectedItem().toString(),
                        rotation.getSelectedItem().toString(), separation.getText().toString(),
                        bf_offset.getText().toString());

                sh.updateRow(row);

                sh.close();
            }
            else
            {
                //save new row
                Log.d("*save row*", "id=" + new_id);

                //save row to database
                sh.addRow(new Row(new_id, row_name.getText().toString(), object_type.getSelectedItem().toString(),
                        rack_name.getText().toString(), tile_location.getText().toString(),
                        tile_name.getText().toString(), direction.getSelectedItem().toString(),
                        rotation.getSelectedItem().toString(), separation.getText().toString(),
                        bf_offset.getText().toString()));

                sh.close();
            }

            Row_main fragment = new Row_main();
            FragmentManager fragmentManager = getActivity().getSupportFragmentManager();
            FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
            fragmentTransaction.replace(R.id.fragment_container, fragment);
            fragmentTransaction.addToBackStack(null);
            fragmentTransaction.commit();
            getActionBar().setTitle("Row");
        }
    });

    add_btn.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
             alertDialogBuilder.show();
        }
    });

    cancel_btn.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Row_main fragment = new Row_main();
            FragmentManager fragmentManager = getActivity().getSupportFragmentManager();
            FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
            fragmentTransaction.replace(R.id.fragment_container, fragment);
            fragmentTransaction.addToBackStack(null);
            fragmentTransaction.commit();
            getActionBar().setTitle("Row");
        }
    });

    return v;
}

private ActionBar getActionBar() {
    return ((AppCompatActivity) getActivity()).getSupportActionBar();
}

I would like to use Bundles here since it is not important to restore anything from a Bundle if the user leaves the app or presses the back Button.

Am I going about this right? what should I do about the final Bundle situation in onClickListener methods? I absolutely need to pass Bundles from them the way I have things setup now and I would like to use the same Bundle if I can.

I have a deadline to consider so a complete overhaul might be out of the question.

Any input is greatly appreciated.

Community
  • 1
  • 1
Corey
  • 405
  • 2
  • 9
  • 18
  • 1
    See [best practice for instantiating Fragment](http://stackoverflow.com/questions/9245408/best-practice-for-instantiating-a-new-android-fragment) and [how to make Objects Parcelable](http://stackoverflow.com/questions/7181526/how-can-i-make-my-custom-objects-parcelable) – OneCricketeer Nov 03 '16 at 21:20
  • 1
    You may also find libraries like EventBus to be easier to work with than Bundles and Intents, and there are libraries to aid with SQLite as well. – OneCricketeer Nov 03 '16 at 21:22
  • thank you for the quick response, reading now. – Corey Nov 03 '16 at 21:22
  • So after some serious experimenting with Eventbus from greenrobot, it doesn't seem to be what I need. The eventbus can't post to a new fragment because the fragment doesn't subscribe to it until it is loaded. I am using FragmentManager as shown in the code above... I'm not sure of the lifecycle behind the scenes but searching for answers or comprehensive docs has proved a giant waste of a day so far. Thanks but I'll stick with bundles for now... perhaps I can make use of parcelable objects... I'll report back. – Corey Nov 04 '16 at 21:15
  • So parcelable objects make my code more concise but don't address the complexity issue. I am still forced to created different bundles. My question was and still is; How can I use the same bundle throughout a fragment? widgets with listeners are entirely necessary and it just makes sense that I would pack a bundle inside an onlick listener since that is the point at which I am loading another fragment. Whether I use a fragment manager or create some fancy constructor for new fragments, which, I'm literally told not to do in android; the problem still exists. – Corey Nov 04 '16 at 22:25
  • You use the `newInstance` static method in place of a constuctor. That passes a Bundle of Arguments. You can use `getArguments()` anywhere within the Fragment, so I'm not sure I understand the problem. – OneCricketeer Nov 04 '16 at 22:29
  • 1
    Some things I see that could be overall refactored and better about your code is that you "call up" to the activity to replace the Fragment, [here's how not to do that](http://stackoverflow.com/questions/13700798/basic-communication-between-two-fragments). Another thing is the repeated calls to `new Row` with the exact same data, but a different ID. Make a method `getRow(int id)`, and just return that constructed row. And instead of passing in a String arraylist that you have to pull apart/put together across the Bundle, you should pass in a list of Parcelable objects which make up a Row – OneCricketeer Nov 04 '16 at 22:35
  • Now I think I am understanding a little better... when I create my fragment I should tell newInstance() what I'm going to pass. The constructor can be left out altogether. I'm going to look at your latest link for avoiding the manager now. Parcelable will be very helpful. – Corey Nov 04 '16 at 22:49

1 Answers1

1

I think it'll be easier to isolate your data-passing problems once you refactor some code.


1. Replace these similar blocks with a method. You seem to have copied this same chuck of code about five times.

Make it a method! Call it getRow(), give it a parameter, if you have to, like for the int id.

// int id = <some_value>;

return new Row(id, row_name.getText().toString(), object_type.getSelectedItem().toString(),
        rack_name.getText().toString(), tile_location.getText().toString(),
        tile_name.getText().toString(), direction.getSelectedItem().toString(),
        rotation.getSelectedItem().toString(), separation.getText().toString(),
        bf_offset.getText().toString());

2. Now, it seems you are passing around this Row class, so suggested to make that implement Parcelable so you can more easily pass that between Bundles when the need arises.


3. Next, you should let the Fragment interactions happen across the Activity boundary, not from within each Fragment. So, you add interfaces, for example. You can split this into many interfaces, if you do not need all this functionality.

public interface OnRowInteractionListener {
    public void onRowAdded(Row row);
    public void onRowEdited(Row row);
    public void onRowRemoved(Row row);
    public void onRowActionCancel();
}

3a. Implement onAttach of the Fragment class(es) to set these interfaces.

private OnRowInteractionListener mListener;

@Override
public void onAttach(Context context) {
    super.onAttach(context);
    final String contextName = context.class.getSimpleName();
    try {
        mListener = (OnRowInteractionListener) context;
    } catch (ClassCastException e) {
        throw new ClassCastException(contextName + " must implement OnRowInteractionListener");
    }
}

3b. Now, clean up the Fragment buttons

add_row_btn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Row added = getRow(); // remember about that parameter value
        if (mListener != null) mListener.onRowAdded(added);
    }
});

3c. In the Activity, you need to implement those interfaces after you make the class implements OnRowInteractionListener.

@Override
public void onRowAdded(Row row) { 

    getActionBar().setTitle("Add Row");

    // This acts like your 'constructor'
    AddRowFragment fragment = AddRowFragment.newInstance(row);
    // fragment.setArguments(savedInstanceState);

    getSupportFragmentManager()
            .beginTransaction()
            .replace(R.id.fragment_container, fragment)
            .addToBackStack(null)
            .commit();
}

(similar for onRowEdited)


4. I also see this Fragment replacement block of code repeated for cancel and save buttons, so make that a method as well.

Row_main fragment = new Row_main();
FragmentManager fragmentManager = getActivity().getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.replace(R.id.fragment_container, fragment);
fragmentTransaction.addToBackStack(null);
fragmentTransaction.commit();
getActionBar().setTitle("Row");

I would really suggest you make one really generic method in the Activity to handle all Fragment transitions.

public void transitionFragment(Fragment fragment, Bundle arguments, String title) { 

    getSupportActionBar().setTitle(title);

    if (arguments != null) fragment.setArguments(arguments);

    getSupportFragmentManager()
            .beginTransaction()
            .replace(R.id.fragment_container, fragment)
            .addToBackStack(null)
            .commit();
}

Then, just pass in the respective newInstance()'d Fragment to that method whenever you need it. Optionally pass extra arguments, else, just null.

So, then, you now can use that method for the previous block of code, for example, in the cancel action of the interface implementation.

@Override
public void onRowActionCancel() {
    Fragment fragment = RowMainFragment.newInstance();
    transitionFragment(fragment, null, "Add Row");
}

As far as the SQLite processing goes, maybe look into RealmDB, SugarORM, or really any ORM library.

The findViewById code could be simplified with Butterknife. Also, the Android Data Binding library in case you are frequently mapping data to the View objects from a Java object.

Community
  • 1
  • 1
OneCricketeer
  • 179,855
  • 19
  • 132
  • 245
  • I am in the process of understanding/implementing all of this. If (more likely when) I figure out that I can use it; I will give you the check mark cricket. Admittedly some of this goes a little over my head but not too far as to keep me from trying it. Thank you very much for your help. – Corey Nov 05 '16 at 14:39
  • No worries, feel free to break it down and create new posts, if you have to – OneCricketeer Nov 05 '16 at 14:59