Fragment And Activity Working Together
Written by Mike James   
Thursday, 27 March 2014
Article Index
Fragment And Activity Working Together
Using Bundles
Persisting State
An Interface Callback
Persisting Fragments

 

If you get an error message about not being able to use the add method then make sure you are importing the same Fragment library in both the Fragment and the Activity. For example android.app.Fragment can't be mixed with the support library. Mixing support library imports with standard OS imports can also cause the app to just hang.

There is one more thing you have to do before the program will run correctly. For reasons to be explained before the Activity can use the Fragment it has to implement the OnFragmentInteractionListener. All you have to do is change the class definition to:

public class MainActivity extends ActionBarActivity
implements BlankFragment.
              OnFragmentInteractionListener{

If you right click on OnFragmentInterationListener and select  generate method then the Android Studio will automatically add:

@Override
public void onFragmentInteraction(Uri uri) {
}

We will return very soon to what this is all about. If you don't add the interface and the method then you will discover that you don't get any error messages but the app hangs when you run it. 

If you have made all of these changes then when you run the app you will see String1 and String2 appear in the Button and TextView.

If you rotate the emulator, Ctrl-F11, then you will see that the Strings are preserved even though the Fragment has been destroyed and recreated - that's the bundle doing its job!

If the emulator you are using doesn't seem to rotate the view you are probably using SDK 4.4. At the time of writing this has a bug in the emulator which means that the rotation doesn't work. Down grade by installing the system images for SDK 4.3 and you will discover that it does work as advertised. 

If you don't want to use parameters then uncheck the Include Factory Method in the template or remove the Factory method, onCreate and the associated variables.

Persisting State

if you look more carefully at the onCreate and onCreateView methods you will see that they are both passed a savedInstanceState object .

So what is the savedInstanceState bundle for? 

The answer is that it plays exactly the same role as in the case of the Activity. It is a Bundle that can be used to save and restore the state of the Fragment. 

First try the previous example out again and this time alter what it stored in the EditText field. If you now rotate the screen what do you expect to see - the initializing string or the text you entered? 

If you are surprised to see the text you entered restored rather than the initializing string you have forgotten that the system automatically saves and restores the state of any UI component that has a label and can be modified by the user. 

So for many aspects of the the UI you don't have to explicitly write code that maintains state. 

To see how to explicitly maintain state let's add an event handler to the Button that changes its caption. This isn't very deep but it is enough to demonstrate that in this case the state isn't automatically saved and restored. 

To add the Button's click event handler we can use the idiom:

bt.setOnClickListener(new View.OnClickListener() {
 @Override
 public void onClick(View view) {
            ((Button)view).setText("Clicked");
 }
});

which uses an anonymous class as before but now assigns it directly to the OnClickListener in one statement. 

Now if you run the app you will first see String1 as the Button's caption. Then if you click the Button it changes to Clicked, but now if you rotate the display you will discover that it goes back to String1. In this case the system doesn't automatically save the state because Button captions aren't normally supposed to change - and in this case it only does to provide an example.  

You can add items to the savedInstanceState bundle, again as in the case of the Activity.

To save the state the simplest thing to do is override the onSaveInstanceState method which the system calls when it is about to dispose of the Fragment.  For example to save the Button's caption: 

@Override
public void onSaveInstanceState(Bundle outState) {
 super.onSaveInstanceState(outState);
 View v=getView();
 Button bt=(Button) v.findViewById(R.id.button);
 outState.putCharSequence("ButtonText",  
                               bt.getText());
}

Now the Button's caption is saved in the Bundle and all we have to do is restore it - but where? 

If the state information relates the the UI then you need to restore it in the onCreateView otherwise use onCreate. You also have to remember to restore the state after the initialization provided by the arguments has been applied.

That is:

Button bt=(Button) v.findViewById(R.id.button);
bt.setText(mParam1);
if(savedInstanceState!=null){
 bt.setText(savedInstanceState.getCharSequence(
                                   "ButtonText"));
}

Notice that we first set the argument and then restore any saved state.

If you now run the app and rotate the screen you will discover that the Button's caption is preserved no matter what it is. 

At this point you might be thinking why bother with the saved state why not just change the argument so that the new value will be restored to mParam1? 

There is no hard and fast reason for doing things either way but logically the separation into initialization and state preservation is useful. 

So - use SetArguments and its Bundle to save initialization and use the savedinstanceState Bundle to save changes to the state. First restore the arguments and then restore the state changes if there are any.

Fragment Events/Callbacks

Our next problem leads us into some sophisticated Java and some round about thinking. 

It is obvious that if a Fragment is to serve the role of a reusable chunk of UI complete with some behaviour it is going to have to have the facility to "tell" the Activity when things happen. In simple terms it has to have a way to signal to the Activity that something has happened and provide it with some data. For example there might be a Button on the Fragment which indicates that the user has finished entering data and this needs to trigger and even handler in the Activity to give it a chance to process the data supplied by the Fragment. 

If you think about it for a moment the fact that the Fragment generates part of the View hierarchy for the Activity might make you think that the obvious way to do this is to access the View object that the Fragment creates. That is the Activity can simply hook up and event handler to a Button that that the Fragment creates. After all there is no difference between a Button that the Activity creates and a Button the Fragment creates. 

Unfortunately there is. 

The Fragment is added to the Activity using the FragmentManager in the Activity's OnCreate handler. You might think that you can simply hook up an event handler straight after this. Something like:

 if (savedInstanceState == null) {
 BlankFragment  bf=BlankFragment.
              newInstance("String1","String2");
 getFragmentManager()
    .beginTransaction()
    .add(R.id.container,bf)
    .commit();
 Button bt=(Button) findViewById(R.id.button);
 bt.setOnClickListener(onclick);
}

This doesn't work. 

The reason is, as already mentioned, that the Fragment doesn't contribute its part of the View hierarchy until its OnCreateView event handler gets to run and this happens after the Activity's OnCreate ends. The Java UI is single threaded. 

So your next thought might be to add the event handler to the arguments so that the event handler can be added as part of the OnCreateView method. This doesn't work because you would have to find a way to persist the reference to the event or callback object in the Activity and you can't save a reference to an object in a Bundle. Even if you could it wouldn't be a good idea because if the Activity is destroyed and then recreated the event/callback object would be different and there persisted reference would be invalid. 

You can't pass the event/callback object to the Fragment but the Fragment but if the object is a public field then the Fragment can just access it.

There is an onAttach event which is fired when ever the Fragment is attached to an Activity - no matter if it is as a result of you or the system creating the Fragment. When the OnAttach event handler is called the Fragment  can be 100% certain that its Activity exists and so it can access any properties, fields or methods the Activity has. 

So our plan is  to 

  1. Create an event or callback object in the Activity as a public field
  2. Allow the Fragment to store a reference to the object during the onAttach method
  3. Then allow the Fragment to use the event or callback later

For example if we simply set the OnClickListener to be public: 

public View.OnClickListener onclick =
               new View.OnClickListener() {
 @Override
 public void onClick(View view) {
  TextView tv =
        (TextView) findViewById(R.id.textView);
  Button bt = (Button) view;
  bt.setText(tv.getText());
 }
};

Now the Fragment can gain access to the event handler in the onAttach handler:

@Override
public void onAttach(Activity activity) {
 super.onAttach(activity);
 MainActivity activity= (MainActivity)activity;
 mClickListener=activity.onclick;

This works, and it is the standard Java way of implementing a callback, but in this case it has a huge disadvantage.

The problem is you have to cast the general Activity to the more specific MainActivity in which the onClickListener object exists. If you want to use the Fragment with another Activity - HelperActivity or whatever. If you want a Fragment to be a general purpose UI component we have to find another way of passing the event handler/callback.

In fact the modification needed to make it work is quite small. 



Last Updated ( Saturday, 25 July 2015 )