Week 4 - Fragments
Corresponding Text
Android Programming, pp. 121-147
UI Flexibility
We're going to start working on a new application, one that will allow us to store contact information. To begin, our app will consist of two parts: a list of all contacts and details of individual contacts. We could create two activities, one for each of the parts. When that app starts, we'd see a list of contacts and tapping one of the contacts would start the details activity. We can switch back to the list using the back button.
While this seems reasonable for a phone, it doesn't seem like the best way of presenting contact information on a tablet. Given the larger screen of a tablet compared to a phone, displaying the list and details simultaneously would provide the user with a better experience. When viewed on a phone, we might want the user to be able to swipe left and right when viewing details to move from one contact to another.
Unfortunately, this sort of interface flexibility is not possible with activities.
Fragments
We can create the type of interface described above using fragments. A fragment represents a behavior or part of an activity. A fragment can be be combined with other fragments and added to an activity and can be used by different activities. While fragments are useful for creating flexible interfaces, their views cannot be displayed on screen without an activity to host the fragments. Like activities, fragments are part of the controller layer in an application.
Before we create fragments, let's create a new application. In Android Studio, select File -> New... -> New Project. Enter a name and domain and choose a project location. On the next screen, choose "Phone and Tablet" and select an API level, in class we'll use API 19. Next, choose "Empty Activity". Finally, name the activity something like "ContactActivity".
Fragments were introduced as part of API level 11 when Android tablets were first introduced. At the time many developers were supporting API level 8 and newer. Rather than configure their projects to support API level 11 at a minimum, developers were able to take advantage of fragments using Android's support library. The support library offers backward-compatible versions of new features - allowing developers to cater to a majority of devices while being able to utilize new features.
We'll make use of two classes from the support library: Fragment and
FragmentActivity; in addition to using fragments, we'll need to make use
of activities that are designed to interact with fragments. In order to use
the support library and these classes, we'll have to add the support library
to the list of libraries our project depends upon. While we could modify
the build.gradle
file to add the necessary information, Android Studio
provides an easier way of adding dependencies. Choose File -> Project
Structure from the menu bar, select app, and click the Dependencies
tab. Here's we can see all the libraries our application depends on. We can
add a dependency by clicking the + button and selecting Library
Dependency. Begin typing support
and choose the support-v4
library.
Click OK to close the open dialog boxes. Gradle should automatically sync
to include our change.
The next step to using fragments is to modify our activity so that it extends the FragmentActivity class rather than AppCompatActivity. The code in the activity Java file should look similar to the following:
package com.arthurneuman.mycontacts;
import android.support.v4.app.FragmentActivity;
import android.os.Bundle;
public class ContactActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_contact);
}
}
Next, we'll create the model layer for our application consisting of one class
representing contact information. This class will have three fields: one for
the contact's name, one for an email address, and one that represents a
unique ID for the contact. We'll also create the appropriate getters and
setters. We won't have a setter for the ID but will assign a value using the
class's constructor instead. To create the file, right-click on your package
in the app/java directory in the project view and select
New->Java class.... Choose a name like Contact
.
package com.arthurneuman.mycontacts;
import java.util.UUID;
public class Contact {
private UUID mID;
private String mName;
private String mEmail;
public Contact() {
mID = UUID.randomUUID();
}
public UUID getID() {
return mID;
}
public String getName() {
return mName;
}
public void setName(String name) {
mName = name;
}
public String getEmail() {
return mEmail;
}
public void setEmail(String email) {
mEmail = email;
}
}
Our next step will be to begin working with fragments. Recall, that our application has to have an activity and, in order to make use of fragments, the activity has to host the fragment. In addition to hosting fragments, an activity must also manage the lifecycle of fragment instances.
Hosting a Fragment
There are two approaches to hosting a fragment in an activity: by adding the fragment to the activity's layout or by adding the fragment in the activity's code. A fragment added to an activity's layout is a layout fragment and while simple to add, it does not aid in flexibility. Once added to an activity's view, a layout fragment cannot be swapped with another fragment during the activity's lifetime.
Adding the fragment in the activity's code provides us with more flexibility, allow us to replace it during runtime. This is the approach we'll use. Eventually, we'll create a fragment named ContactFragment but first we'll need to create a spot for its view in ContactActivity's view hierarchy. Creating a place for the fragment's view is not the same as adding the fragment to the activity's layout.
To add a place in the activity's view, we'll replace the default
RelativeLayout with a FrameLayout. Because this is the top-level
layout, we can't use the design view to make the change; we'll have to use the
text view. Replace the content of activity_contact.xml
with the following:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
</FrameLayout>
Creating a Fragment
Before we begin working with a new fragment, let's add some strings to our
string resource file, app/res/values/strings.xml
for text hints:
<string name="name_hint">Name</string>
<string name="email_hint">Email</string>
Creating a new UI fragment is similar to creating a new activity. To begin,
we'll work on it's view but adding a new layout file for it. To create the new
layout file, right-click on the res/layout
folder (the same folder that
contains activity_contact.xml
) and select New -> Layout resource file.
Use the following to create the new fragment.
Field | Value |
---|---|
File name | fragment_contact.xml |
Root element | LinearLayout |
To the LinearLayout, we'll add two EditText widgets by dragging and dropping "Plain Text" objects onto the layout. Set the following properties.
View Object | Property | Value |
---|---|---|
First EditText | layout_width | match_parent |
First EditText | id | contact_name |
First EditText | hint | @string/name_hint |
First EditText | padding, all | 20 dp |
Second EditText | layout_width | match_parent |
Second EditText | id | contact_email |
Second EditText | hint | @string/email_hint |
Second EditText | padding, all | 20 dp |
The fragment's layout XML should look similar to the following:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/contact_name"
android:hint="@string/name_hint"
android:padding="20dp"/>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/contact_email"
android:hint="@string/email_hint"
android:padding="20dp"/>
</LinearLayout>
Now that we have a view element for our new fragment, we can begin working
on a the controller. We can create a new Java class file,
ContactFragment.java
in the same way we created Contact.java
.
The first thing to do is to extend the Fragment class from the android.support.v4.app package. Just like activities, fragments have an OnCreate() method that is called when the fragment is created. When our ContactFragment is created, we'll call the parent's onCreate() method and assign a new instance of the Contact class to a private mCrime field. Just like with an activity, we can pass saved instance data, in the form of a Bundle to the fragment when it's created.
package com.arthurneuman.mycontacts;
import android.os.Bundle;
import android.support.v4.app.Fragment;
public class ContactFragment extends Fragment {
private Contact mContact;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mContact = new Contact();
}
}
If we were to compare the code in ContactFragment.onCreate() to code in previous activities, we might notice that we haven't done anything with the view yet. For fragments, we rely on the onCreateView() method to inflate the view. Specifically, ContactFragment.onCreateView() will be defined as follows:
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_contact, container, false);
return v;
}
Similar to the parameters in the onCreate() method, the LayoutInflater is
responsible for creating the view from a specified XML file,
the ViewGroup specifies where the inflated view will be contained,
and the Bundle contains any state information that can be used to recreate
the view. The arguments passed to the LayoutInflater.inflate() method
specify the layout resource id, the ViewGroup, and a boolean that
determines whether or not to add the view to the parent; we'll specify false
because we'll add the view in the activity's code.
In onCreateView(), we can also add listeners to our EditText widgets.
We'll update the Contact instance, mCrime
, when the text in the the
widgets change. To do this, we'll add a listener that implements the
TextWatcher interface. The interface specifies three methods, but we're only
interested in onTextChanged(), we won't define any behavior for the other
methods.
When we were working with activities, we could find view elements using the
Activity.findViewById() method; this method was simply a convenience method
for the activity's View.findViewById() method; fragments do not have this
convenience method, so we have to have an instance of View. Notice
we do have an instance in the onCreateView() method. The following code
will update the name associated with mCrime
by first finding the name
EditText and assigning it to a field on on ContactFragment then getting
the text as it's changed and calling mCrime.setName().
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_contact, container, false);
mNameField = (EditText)v.findViewById(R.id.contact_name);
mNameField.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
// No new behavior
}
@Override
public void onTextChanged(CharSequence s, int start, int before,
int count) {
mContact.setName(s.toString());
}
@Override
public void afterTextChanged(Editable s) {
// No new behavior
}
});
return v;
}
We can do something similar with the email EditText and mCrime.setEmail() method.
After we've added all the code to create the fragment, inflate its view, and
add logic to the various widgets, ContactFragment.java
should contain code
similar to the following:
package com.arthurneuman.mycontacts;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
public class ContactFragment extends Fragment {
private Contact mContact;
private EditText mNameField;
private EditText mEmailField;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mContact = new Contact();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_contact, container, false);
mNameField = (EditText)v.findViewById(R.id.contact_name);
mNameField.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
// No new behavior
}
@Override
public void onTextChanged(CharSequence s, int start, int before,
int count) {
mContact.setName(s.toString());
}
@Override
public void afterTextChanged(Editable s) {
// No new behavior
}
});
mEmailField = (EditText)v.findViewById(R.id.contact_email);
mEmailField.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
// No new behavior
}
@Override
public void onTextChanged(CharSequence s, int start, int before,
int count) {
mContact.setEmail(s.toString());
}
@Override
public void afterTextChanged(Editable s) {
// No new behavior
}
});
return v;
}
}
Though ContactFragment is not complete, if we start our app, we won't see its view; a fragment can't put its view on the screen on its own. In order to see the fragment, we'll have to rely on an activity.
The FragmentManager and the Fragment Lifecycle
Much like an activity, a fragment transitions between states such as running, paused, and stopped and has corresponding methods for each state transition.
For a given activity, the FragmentManager is responsible for managing fragments and adding their views to the view hierarchy. The FragmentManager handles a list of fragments and and a back stack of fragment transactions, providing functionality when a user presses the back button. The FragmentManager is also responsible for calling the lifecycle methods of a fragment.
To use the Fragment manger, we can add the following to ContactActivity's onCreate() method:
FragmentManager fm = getSupportFragmentManager();
With the FragmentManager, we can now create a fragment transaction. Fragment transactions are used to add, remove, or replace fragments in the fragment list; we have to use a fragment transaction to add our fragment. To create a new transaction we'll use FragmentManager's beginTransaction(), add(), and commit() methods.
Fragment fragment = fm.findFragmentById(R.id.fragment_container);
if (fragment==null) {
fragment = new ContactFragment();
fm.beginTransaction()
.add(R.id.fragment_container, fragment)
.commit();
}
Here we first check to see if the fragment was previously added by asking the FragmentManager for the fragment identified by the fragment_container. If there is no existing fragment, we create a new fragment transaction and add a new ContactFragment with fragment_container. It's important to check if the fragment is already part of the FragmentManager's fragment list. If ContactActivity is destroyed and reloaded due to rotation or Android's attempt to conserve memory, the Fragmentmanager will save it's list of fragments and this can be reloaded.
ContactActivity.java
should now look similar to this:
package com.arthurneuman.mycontacts;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.os.Bundle;
import android.support.v4.app.FragmentManager;
public class ContactActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_contact);
FragmentManager fm = getSupportFragmentManager();
Fragment fragment = fm.findFragmentById(R.id.fragment_container);
if (fragment==null) {
fragment = new ContactFragment();
fm.beginTransaction()
.add(R.id.fragment_container, fragment)
.commit();
}
}
}
If we run the app, we should now see ContactFragment.