Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4384

Android Fragments Tutorial: An Introduction

$
0
0

Fragments-featureThis tutorial is an introduction to Android Fragments. You will learn the fundamental concepts of Android Fragments while creating an app that displays the rage comics.

Update note: This tutorial has been updated to API 25 and Android Studio 2.2.2 by Huyen Tue Dao. The original tutorial was written by Huyen Tue Dao.

fragment | noun | /’frag-mənt/
an isolated or incomplete part of something

A fragment is an Android component that holds part of the behavior and/or UI of an activity. As the name intimates, fragments are not independent entities, but are subservient to a single activity.

In many ways, they resemble and echo the functionality of activities.

Imagine for a moment that you’re an activity. You have a lot to do, so you’d employ a few mini-me’s to run around and do your laundry and taxes in exchange for lodging and food. That’s kind of like the relationship between activities and fragments.

Now just like you don’t actually need a few minions to do your bidding, you don’t have to use fragments. However, if you do use them and use them well, they can provide:

  • Modularity: dividing complex activity code across fragments for better organization and maintenance.
  • Reusability: placing behavior or UI parts into fragments that can be shared across multiple activities.
  • Adaptability: representing sections of a UI as different fragments and utilizing different layouts depending on screen orientation and size.

android_fragments_d001_why_fragments
In this introduction to Android fragments tutorial, you’ll build a mini encyclopedia of Rage Comics. The app will display a list of Rage Comics arranged in a grid. When a Rage Comic is selected, the app displays information about it. In this tutorial, you’ll learn:

  • How to create and add fragments to an activity.
  • How to let your fragments send information to an activity.
  • How to add and replace fragments by using transactions.

Note: This tutorial assumes you’re comfortable the basics of Android programming and understand what activity lifecycle means. If you’re brand new to Android, you should work through both the Android Tutorial for Beginners and the Introduction to Activities first. This tutorial also utilizes an Android RecyclerView. If you have never used RecyclerView or need a refresher, you should also look at the Android RecyclerView Tutorial.

The time has come to release the fragments!

Getting Started With Android Fragments

Download the starter project, unzip and start Android Studio.

In the Welcome to Android Studio dialog, select Import project (Eclipse ADT, Gradle, etc.).
Android Studio Welcome Screen

Choose the top-level directory of the starter project, and click OK.

Select project to import

Check out the project, and you’ll find some resource files; strings.xml, activity_main.xml, and drawable and layout files. There are also some boilerplate layouts for your fragments, non-fragment code that you’ll need and a fragment class that you’ll use later to write your own.

The MainActivity will host all your wee fragments, and RageComicListFragment contains code to display a list of the Rage Comic content so that you can focus on fragments.

Build and run the project. You’ll see that it’s pretty quiet in there.
Running the starter app

You’ll fix that…

android_fragments_005_app_soon

Android Fragment Lifecycle

Like an activity, a fragment has a lifecycle with events that occur when the fragment’s status changes. For instance, an event happens when the fragment becomes visible and active. Or when the fragment becomes unused and is removed. Also like an activity, you can add code and behaviors to callbacks for these events.

Here’s a fragment lifecycle diagram from the official Android Developer documentation.android_fragments_d002_fragment_lifecycle

android_fragments_006_fragment_lifecycle_hmm

The following lifecycle events come into play when you add a fragment:

  • onAttach: when the fragment attaches to its host activity
  • onCreate: when a new fragment instance initializes, which always happens after it attaches to the host — fragments are a bit like viruses
  • onCreateView: when a fragment creates its portion of the view hierarchy, which is added to its activity’s view hierarchy
  • onActivityCreated: when the fragment’s activity has finished its own onCreate event
  • onStart: when the fragment is visible; a fragment starts only after its activity starts and often starts immediately after its activity does
  • onResume: when the fragment is visible and interactable; a fragment resumes only after its activity resumes and often resumes immediately after the activity does

But wait, the fragment isn’t done. These lifecycle events happen when you remove a fragment:

  • onPause: when the fragment is no longer interactable; this occurs when either the fragment is about to be removed or replaced or when the fragment’s activity pauses
  • onStop: when the fragment is no longer visible; this occurs either after the fragment is about to be removed or replaced or when the fragment’s activity stops
  • onDestroyView: when the view and related resources created in onCreateView are removed from the activity’s view hierarchy and destroyed
  • onDestroy: when the fragment does its final clean up
  • onDetach: when the fragment is detached from its activity

As you can see, the fragment’s lifecycle is intertwined with the activity’s lifecycle. But it has extra events that are particular to the fragment’s view hierarchy, state and attachment to its activity.

The v4 Support Library

Fragments were introduced as part of the oft-forgotten, tablet-targeted Honeycomb release for creating device-specific layouts for a single app.

The v4 Support Library provides a fragment implementation for devices running less than Android 3.0, specifically under android.support.v4.app.Fragment package.

Even if your app is running 4.0+, you should probably still use support fragments.

android_fragments_007_support_fragment_why

It’s not just developers that depend on the Support Library. Other libraries also need it, like the v7 AppCompat Library, which holds the AppCompatActivity and other back-porting of API 21 functionality. In fact, AppCompatActivity is a subclass of the v4 FragmentActivity.

So, if you want to get that sweet functionality on Lollipop and above, you’ll need to take the same road as v4.

Creating a Fragment

Eventually, All the Rages will show a list of Rage Comics on launch, and tapping on any of the items will display details about that particular comic. To start, you’ll work backwards and first create the detail page.

Open the starter project in Android Studio and find fragment_rage_comic_details.xml under app -> res -> layout; this XML file lays out the comic detail display. It also displays one of the drawable resources and the associated string resources.

Fragment details preview

Select Android Studio’s Project tab and locate the RageComicDetailsFragment file. This class will be responsible for displaying details for a selected comic.

In RageComicDetailsFragment.java, the code looks like what is shown below:

import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
 
//1	
public class RageComicDetailsFragment extends Fragment {
  //2	  
  public static RageComicDetailsFragment newInstance() {
    return new RageComicDetailsFragment();
  }
 
  //3	
  @Nullable
  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    return inflater.inflate(R.layout.fragment_rage_comic_details, container, false);
  }
}

This is what this code does:

  1. Declares RageComicDetailsFragment as a subclass of Fragment.
  2. Provides a method for creating new instances of the fragment, a factory method.
  3. Creates the view hierarchy controlled by the fragment.

Activities use setContentView() to specify the XML file that defines their layouts, but fragments create their view hierarchy in onCreateView(). Here you called LayoutInflater.inflate to create the hierarchy of RageComicDetailsFragment.

The third parameter of inflate specifies whether the inflated fragment should be added to the container. The container is the parent view that will hold the fragment’s view hierarchy. You should always set this to false: the FragmentManager will take care of adding the fragment to the container.

There’s a new kid in town here: FragmentManager. Each activity has a FragmentManager that manages its fragments. It also provides an interface for you to access, add and remove those fragments.

You’ll notice that while RageComicDetailsFragment has a factory instance method, newInstance(), it does not have any constructors.

Wait, why do you need a factory method but not a constructor?

First, because you did not define any constructors, the compiler automatically generates an empty, default constructor that takes no arguments. This is all that you should have for a fragment: no other constructors.

If you specify a non-empty constructor but not explicitly write an empty constructor, Lint will give you an error:

Lint warning for no default constructor

Now it will compile still, but when you run your application, you’ll get an even nastier exception.

You probably know that Android may destroy and later re-create an activity and all its associated fragments when the app goes into the background. When the activity comes back, its FragmentManager starts re-creating fragments by using the empty default constructor. If it cannot find one, you get an exception.

Because of this, it is best practice to never specify any non-empty constructors, and in fact, the easiest thing to do is to specify none as you just did.

Wait, what if you need to pass information or data to a Fragment? Hold on tight: you’ll get the answer to that later.

Adding a Fragment

Here’s where you get to add your own shiny new fragment using the simplest approach: adding it to the activity’s XML layout.

To do this, open activity_main.xml, select the Text tab and add the following inside of the root FrameLayout:

<fragment
  android:id="@+id/details_fragment"
  class="com.raywenderlich.alltherages.RageComicDetailsFragment"
  android:layout_width="match_parent"
  android:layout_height="match_parent"/>

Here you’re placing a <fragment> tag inside of the activity layout and specifying the type of fragment the class attribute should inflate. The view ID of the <fragment> is required by the FragmentManager.

Build and run. You will see the fragment:

The details fragment in its full glory

Adding a Fragment Dynamically

First, open activity_main.xml again and remove the <fragment> you just placed. (Yes, I know, you just put it there — sorry.) You’ll replace it with the list of Rage Comics.

Open RageComicListFragment.java, which has all the lovely list code. You can see that the RageComicListFragment has no explicit constructors and a newInstance().

The list code in RageComicListFragment depends on some resources. You have to ensure that the fragment has a valid reference to a Context for accessing those resources. That’s where onAttach() comes into play.

Open RageComicListFragment.java, and add these imports directly below the existing imports:

import android.content.res.Resources;
import android.content.res.TypedArray;
import android.support.annotation.Nullable;
import android.os.Bundle;
import android.support.v7.widget.GridLayoutManager;
import android.app.Activity;

The first two imports allow you to access some string resources that you will use as data in the list. The fifth import, the GridLayoutManager, helps in positioning items in the Rage Comic list. The other imports are for standard fragment overrides.

Inside of RageComicListFragment.java, add the following two methods above the definition of the RageComicAdapter:

@Override
public void onAttach(Context context) {
  super.onAttach(context);
 
  // Get rage face names and descriptions.
  final Resources resources = context.getResources();
  mNames = resources.getStringArray(R.array.names);
  mDescriptions = resources.getStringArray(R.array.descriptions);
  mUrls = resources.getStringArray(R.array.urls);
 
  // Get rage face images.
  final TypedArray typedArray = resources.obtainTypedArray(R.array.images);
  final int imageCount = mNames.length;
  mImageResIds = new int[imageCount];
  for (int i = 0; i < imageCount; i++) {
    mImageResIds[i] = typedArray.getResourceId(i, 0);
  }
  typedArray.recycle();
}
 
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
  final View view = inflater.inflate(R.layout.fragment_rage_comic_list, container, false);
 
  final Activity activity = getActivity();
  final RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
  recyclerView.setLayoutManager(new GridLayoutManager(activity, 2));
  recyclerView.setAdapter(new RageComicAdapter(activity));
  return view;
}

onAttach() contains code that accesses the resources you need via the Context to which the fragment has attached. Because the code is in onAttach(), you can rest assured that the fragment has a valid Context.

In onCreateView(), you inflate the view hierarchy of RageComicListFragment, which contains a RecyclerView, and perform some setup.

Generally, if you have to poke and prod at a fragment’s view, onCreateView() is a good place to start because you have the view right there.

Next open MainActivity.java and replace onCreate() with the following:

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
 
  if (savedInstanceState == null) {
    getSupportFragmentManager()
      .beginTransaction()
      .add(R.id.root_layout, RageComicListFragment.newInstance(), "rageComicList")
      .commit();
  }
}

Here you get RageComicListFragment into MainActivity. You ask your new friend, FragmentManager, to add it.

Build, run and you’ll see a Rage-filled list once the app launches:

The list of Rage Comics. Woo!

FragmentManager helped achieve this awesomeness through FragmentTransactions, which are basically fragment operations such as, add, remove, etc.

First you grab the FragmentManager by calling getSupportFragmentManager(), as opposed to getFragmentManager since you are using support fragments.

Then you ask that FragmentManager to start a new transaction by calling beginTransaction() — you probably figured that out yourself. Next you specify the add operation that you want by calling add and passing in:

  • The view ID of a container for the fragment’s view hierarchy in the activity’s layout. If you sneak a quick peek at activity_main.xml, you’ll find @+id/root_layout.
  • The fragment instance to be added.
  • A string that acts as a tag/identifier for the fragment instance. This allows the FragmentManager to later retrieve the fragment for you.

Finally, you ask the FragmentManager to execute the transaction by calling commit().

And with that, the fragment is added!

An if block contains the code that displays the fragment and checks that the activity doesn’t have saved state. When an activity is saved, all of its active fragments are also saved. If you don’t perform this check, this could happen:

android_fragments_d003_fragments_too_many

And you would be like this:

android_fragments_014_y_u_no_have

The lesson: Always keep in mind how the saved state affects your fragments.

Communicating with the Activity

Even though fragments are attached to an activity, they don’t necessarily all talk to one another without some further “encouragement” from you.

For All the Rages, you’ll need RageComicListFragment to let MainActivity know when the user has made a selection so that RageComicDetailsFragment can display the selection.

To start, open RageComicListFragment.java and add the following Java interface at the bottom:

public interface OnRageComicSelected {
  void onRageComicSelected(int imageResId, String name,
    String description, String url);
}

This defines a listener interface for the activity to listen to the fragment. The activity will implement this interface, and the fragment will invoke the onRageComicSelected() when an item is selected, passing the selection to the activity.

Add this new field below the existing ones in RageComicListFragment:

private OnRageComicSelected mListener;

This field is a reference to the fragment’s listener, which will be the activity.

In onAttach(), add the following just below super.onAttach(context);:

if (context instanceof OnRageComicSelected) {
  mListener = (OnRageComicSelected) context;
} else {
  throw new ClassCastException(context.toString() + " must implement OnRageComicSelected.");
}

This initializes the listener reference. You wait until onAttach() to ensure that the fragment actually attached itself. Then you verify that the activity implements the OnRageComicSelected interface via instanceof.

If it doesn’t, it throws an exception since you can’t proceed. If it does, then you set the activity as the listener for RageComicListFragment.

In the onBindViewHolder() method, add this code to the bottom — okay, I fibbed a little; the RageComicAdapter doesn’t have everything you need):

viewHolder.itemView.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    mListener.onRageComicSelected(imageResId, name, description, url);
  }
});

This adds a View.OnClickListener to each Rage Comic so that it invokes the callback on the listener (the activity) to pass along the selection.

Open MainActivity.java and update the class definition to following:

public class MainActivity extends AppCompatActivity
  implements RageComicListFragment.OnRageComicSelected {

You will get an error asking you to make MainActivity abstract or implement abstract method OnRageComicSelected(int, String, String, String). Don’t fret just yet, you’ll resolve it soon.

This code specifies that MainActivity is an implementation of the OnRageComicSelected interface.

For now, you’ll just show a toast to verify that the code works. Add the following import below the existing imports so that you can use toasts:

import android.widget.Toast;

And then add the following method below onCreate():

@Override
public void onRageComicSelected(int imageResId, String name, String description, String url) {
  Toast.makeText(this, "Hey, you selected " + name + "!", Toast.LENGTH_SHORT).show();
}

There you go, the error is gone! Build and run. Once the app launches, click one of the Rage Comics. You should see a toast message naming the clicked item:

You selected Neil deGrasse Tyson!

You got the activity and its fragments talking. You’re like a master digital diplomat.

Fragment Arguments and Transactions

Currently, RageComicDetailsFragment displays a static Drawable and set of Strings, but say you want it to display the user’s selection.

Open RageComicDetailsFragment.java and add the following constants at the top of the class definition:

private static final String ARGUMENT_IMAGE_RES_ID = "imageResId";
private static final String ARGUMENT_NAME = "name";
private static final String ARGUMENT_DESCRIPTION = "description";
private static final String ARGUMENT_URL = "url";

These constants are keys you will use to save and restore the fragment’s state.

Replace newInstance() with the code shown below:

public static RageComicDetailsFragment newInstance(int imageResId, String name,
  String description, String url) {
 
  final Bundle args = new Bundle();
  args.putInt(ARGUMENT_IMAGE_RES_ID, imageResId);
  args.putString(ARGUMENT_NAME, name);
  args.putString(ARGUMENT_DESCRIPTION, description);
  args.putString(ARGUMENT_URL, url);
  final RageComicDetailsFragment fragment = new RageComicDetailsFragment();
  fragment.setArguments(args);
  return fragment;
}

A fragment can take initialization parameters through its arguments, which you access via getArguments() and setArguments(). The arguments are actually a Bundle that stores them as key-value pairs, just like the Bundle in Activity.onSaveInstanceState.

You create and populate the arguments’ Bundle, call setArguments, and when you need the values later, you call getArguments to retrieve them.

As you learned earlier, when a fragment is re-created, the default empty constructor is used — no parameters for you.

Because the fragment can recall initial parameters from its persisted arguments, you can utilize them in the re-creation. The above code also stores information about the selected Rage Comic in the RageComicDetailsFragment arguments.

Add the following imports to the top of RageComicDetailsFragment.java:

import android.widget.ImageView;
import android.widget.TextView;

Now, replace onCreateView() with the following:

@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
  final View view = inflater.inflate(R.layout.fragment_rage_comic_details, container, false);
  final ImageView imageView = (ImageView) view.findViewById(R.id.comic_image);
  final TextView nameTextView = (TextView) view.findViewById(R.id.name);
  final TextView descriptionTextView = (TextView) view.findViewById(R.id.description);
 
  final Bundle args = getArguments();
  imageView.setImageResource(args.getInt(ARGUMENT_IMAGE_RES_ID));
  nameTextView.setText(args.getString(ARGUMENT_NAME));
  final String text = String.format(getString(R.string.description_format), args.getString
    (ARGUMENT_DESCRIPTION), args.getString(ARGUMENT_URL));
  descriptionTextView.setText(text);
  return view;
}

Since you want to dynamically populate the UI of the RageComicDetailsFragment with the selection, you grab references to the ImageView and TextViews in the fragment view in onCreateView. Then you populate them with the image and text you passed to RageComicDetailsFragment, using them as arguments.

Finally, you need to create and display a RageComicDetailsFragment when a user clicks an item, instead of just showing a dinky little toast. Open MainActivity and replace the logic inside onRageComicSelected with:

@Override
public void onRageComicSelected(int imageResId, String name, String description, String url) {
  final RageComicDetailsFragment detailsFragment =
    RageComicDetailsFragment.newInstance(imageResId, name, description, url);
  getSupportFragmentManager()
    .beginTransaction()
    .replace(R.id.root_layout, detailsFragment, "rageComicDetails")
    .addToBackStack(null)
    .commit();
}

The code includes some classes you haven’t used previously. So you need to fire off a couple of option + enter sequences to import the missing classes.

You’ll find that this code is similar to your first transaction that added the list to MainActivity, but there are some notable differences.

  • You create a fragment instance that included some nifty parameters.
  • You call replace(), instead of add, which removes the fragment currently in the container and then adds the new Fragment.
  • You call another new friend: the addToBackStack() of FragmentTransaction. Fragments have a back stack, or history, just like activities.

The fragment back stack is not independent of the activity back stack. Think of it as an extra stack of history on top of that of the host activity.

Fragments and back stack

When you navigate between activities, each one gets placed on the activity back stack. Whenever you commit a FragmentTransaction, you have the option to add that transaction to the back stack.

So what does addToBackStack() do? It adds the replace() to the back stack so that when the user hits the device’s back button it undoes the transaction. In this case, hitting the back button sends the user back to the full list.

The add() transaction for the list omits calling addToBackStack(). This means that the transaction is part of the same history entry as the entire activity. If the user hits the back button from the list, it backs the user out of the app.

Guess what? That’s all the code, so build and run.

There won’t be too much difference at first; it’s still the same ol’ list. This time, however, if you click on a Rage Comic, you should see the details for that comic instead of a dinky little toast:

Yay! Actual details on Neil deGrasse Tyson

Woo hoo! Your app is now officially All the Rages, and you have a nice understanding of fragments.

Where To Go From Here?

You can download the final project here.

There is a lot more to learn and do with fragments. Like any kind of tool or feature, consider whether fragments fit your app’s needs and if they do, try to follow best practices and conventions.

To take your skills to the next level, here are some things to explore:

  • Using fragments within a ViewPager: many apps, including the Play Store, utilize a swipeable, tabbed content structure via ViewPagers.
  • Using a more powerful, advantageous DialogFragment instead of a plain old dialog or AlertDialog.
  • Playing with how fragments interact with other parts of the activity, like the app bar.
  • Creating adaptive UIs with fragments. In fact, you should run through Adaptive UI in Android Tutorial.
  • Using fragments as part of the implementation of a high-level behavioral architecture. You can take a look at Common Design Patterns for Android and finding a good starting off point to get the architecture ball rolling.

We hope you enjoyed this introduction to Android fragments tutorial, and if you have any questions or comments, please join the forum discussion below!

The post Android Fragments Tutorial: An Introduction appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4384

Trending Articles