March 3, 2012

Fragment Tutorial part 2


Janus App

Janus is a simple app designed to show how to use Fragments. In order to demonstrate how Fragments can be reused in multiple activities without changing code, this app shall have a (quite) different layout in portrait with respect to landscape.

Landscape orientation : we have a single activity containing two fragments, clicking on the button in MenuFragment, text in BodyFragment will change.


Portrait orientation : we have a main activity containing MenuFragment, after clicking on the button a secondary activity containing the BodyFragment and text to be changed will appear.


With some trivial changes, this approach is valid also for having different layout for smartphone and tablet.


How to place Fragments in layout

Using fragments the activity layout becomes much simpler, in fact in our example it contains only Fragments. Let's see our layouts for both orientations.

Landscape JanusActivity layout : ac_main.xml


Portrait JanusActivity layout : ac_main.xml

Portrait BodyActivity layout : ac_body.xml

And these are fragments layouts, quite conventional.

MenuFragment layout : fr_menu.xml

BodyFragment layout : fr_body.xml

You can see that, placing fragments layout in place of fragment object, you return to the old standard activity layout.



Java code for fragments and activities

We have four Java files, one for each class:

  • JanusActivity.java : java class for main activity (for both orientations)
  • BodyActivity.java : java class for secondary activity (only in portrait)
  • MenuFragment.java : java class for menu fragment
  • BodyFragment.java : java class for body fragment


When drawing layout, a fragment is very similar to views, while its java class has a lot in common with an activity. A fragment is tightly linked to the parent activity lifecycle, in fact there are a lot of callback functions (onAttach, onCreate, onActivityCreated, onCreateView) that are linked to the similar callbacks at activity level.


MenuFragment.java : contains code for button management and a listener interface, used to route button events to the parent activity (explained later)

package it.anddev.bradipao.janus;

import android.app.Activity;
import android.support.v4.app.Fragment;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;

// extended from compatibility Fragment for pre-HC fragment support
public class MenuFragment extends Fragment {
   
   // views
   Button btn1,btn2;
   
   // activity listener
   private OnMenufragListener menufragListener;

   // interface for communication with activity
   public interface OnMenufragListener {
      public void onMenufrag(String s);
   }
   
   // onAttach
   @Override
   public void onAttach(Activity activity) {
      super.onAttach(activity);
      try {
         menufragListener = (OnMenufragListener) activity;
      } catch (ClassCastException e) {
         throw new ClassCastException(activity.toString()+" must implement OnMenufragListener");
      }
   }
   
   // onCreate
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
   }

   // onActivityCreated
   @Override
   public void onActivityCreated(Bundle savedInstanceState) {
      super.onActivityCreated(savedInstanceState);
   }
   
   // onCreateView
   @Override
   public View onCreateView(LayoutInflater inflater,ViewGroup container,Bundle savedInstanceState) {
      View view = inflater.inflate(R.layout.fr_menu,container,false);
      
      // get button BTN1
      btn1 = (Button)view.findViewById(R.id.btn1);
      // button listener
      btn1.setOnClickListener(new View.OnClickListener() {
         @Override
         public void onClick(View v) {
            sendBodyTextToFragment("Text From Fragment");
         }
      });
      
      // get button BTN2
      btn2 = (Button)view.findViewById(R.id.btn2);
      // button listener
      btn2.setOnClickListener(new View.OnClickListener() {
         @Override
         public void onClick(View v) {
            sendBodyTextToActivity("Text From Activity");
         }
      });
      
      return view;
   }
   
   // (recommended) method to send command to activity
   private void sendBodyTextToActivity(String s) {
      menufragListener.onMenufrag(s);
   }
   
   // alternate (not recommended) method with direct access to fragment
   private void sendBodyTextToFragment(String s) {
      
      // get body fragment (native method is getFragmentManager)
      BodyFragment fragment = (BodyFragment) getActivity().getSupportFragmentManager().findFragmentById(R.id.bodyFragment);
      
      // if fragment is not null and in layout, set text, else launch BodyActivity
      if ((fragment!=null)&&fragment.isInLayout()) {
         fragment.setText(s);
      } else {
         Intent intent = new Intent(getActivity().getApplicationContext(),BodyActivity.class);
         intent.putExtra("value",s);
         startActivity(intent);
      }
      
   }
   
}

BodyFragment.java : contains a public function to update text widget

package it.anddev.bradipao.janus;

import android.support.v4.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

//extended from compatibility Fragment for pre-HC fragment support
public class BodyFragment extends Fragment {

   // onCreate
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
   }

   // onActivityCreated
   @Override
   public void onActivityCreated(Bundle savedInstanceState) {
      super.onActivityCreated(savedInstanceState);
   }
   
   // onCreateView
   @Override
   public View onCreateView(LayoutInflater inflater,ViewGroup container,Bundle savedInstanceState) {
      View view = inflater.inflate(R.layout.fr_body,container,false);
      return view;
   }
   
   // set text
   public void setText(String item) {
      TextView view = (TextView) getView().findViewById(R.id.detailsText);
      view.setText(item);
   } 
   
}

JanusActivity.java : contains the listener implementation, used to call BodyFragment public function to update text widget; depending on orientation existing fragment is updated or secondary activity is invoked

package it.anddev.bradipao.janus;

import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;

// main activity (FragmentActivity provides fragment compatibility pre-HC)
public class JanusActivity extends FragmentActivity implements MenuFragment.OnMenufragListener {
   
   // called when the activity is first created
   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.ac_main);
   }
   
   // MenuFragment listener
   @Override
   public void onMenufrag(String s) {
      
      // get body fragment (native method is getFragmentManager)
      BodyFragment fragment = (BodyFragment) getSupportFragmentManager().findFragmentById(R.id.bodyFragment);
      
      // if fragment is not null and in layout, set text, else launch BodyActivity
      if ((fragment!=null)&&fragment.isInLayout()) {
         fragment.setText(s);
      } else {
         Intent intent = new Intent(this,BodyActivity.class);
         intent.putExtra("value",s);
         startActivity(intent);
      }
      
   }
   
}

BodyActivity.java : contains orientation detection logic, in order to go back to main activity in case of screen orientation change from portrait to landscape

package it.anddev.bradipao.janus;

import android.content.res.Configuration;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;

//created only when in portrait mode (FragmentActivity provides fragment compatibility pre-HC)
public class BodyActivity extends FragmentActivity {

   @Override
   protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      
      // check orientation to avoid crash (this activity is not necessary in landscape)
      if (getResources().getConfiguration().orientation==Configuration.ORIENTATION_LANDSCAPE) {
         finish();
         return;
      } else setContentView(R.layout.ac_body);
      
      // show body content as requested in Intent extra
      Bundle extras = getIntent().getExtras();
      if (extras != null) {
         // get data from Intent extra
         String s = extras.getString("value");
         // get body fragment
         BodyFragment fragment = (BodyFragment) getSupportFragmentManager().findFragmentById(R.id.bodyFragment);
         // if fragment is not null and in layout set text
         if ((fragment!=null)&&fragment.isInLayout()) {
            fragment.setText(s);
         }
      }

   }
   
}

Communicating between fragments

We said that when button in MenuFragment is clicked, text in BodyFragment shall be updated. In BodyFragment we have prepared the public function setText() in order to easily update the TextView from outside.

The easier (but not recommended) way to handle the Button click event and update TextView is to directly call the BodyFragment.setText() function from the MenuFragment button listener. From any fragment you can retrieve reference to any other fragment thanks to the getSupportFragmentManager() function.

A better (and recommended) way to do the same, is to generate an event for the activity listener and let the activity decide what to do. The finale code is quite similar to the previous one, the difference is that each fragment has not to be aware of the other one (no cross-fragment dependency), so in principle you could rewrite or even replace a fragment without changing code of the other fragments, only parent activity has to be adjusted.



5 comments:

  1. One of the best simple tutorial available on the net. Nice Job. continue posting. :)

    ReplyDelete
  2. @Movies4free, I agree. This tutorial is great, super simple and easy to understand. Thanks!

    ReplyDelete
  3. This comment has been removed by the author.

    ReplyDelete