As you progress through the chapters of this book, it will become increasingly evident that many of the design concepts behind the Android system were conceived to promote the reuse of and interaction between the different elements that make up an application. One such area that will be explored in this chapter involves using Fragments.
This chapter will provide an overview of the basics of fragments in terms of what they are and how they can be created and used within applications. The next chapter will work through a tutorial designed to show fragments in action when developing applications in Android Studio, including the implementation of communication between fragments.
What is a Fragment?
A fragment is a self-contained, modular section of an application’s user interface and corresponding behavior that can be embedded within an activity. Fragments can be assembled to create an activity during the application design phase and added to or removed from an activity during application runtime to create a dynamically changing user interface.
Fragments may only be used as part of an activity and cannot be instantiated as standalone application elements. However, a fragment can be considered a functional “sub-activity” with its own lifecycle similar to that of a full activity.
Fragments are stored in the form of XML layout files. They may be added to an activity by placing appropriate <fragment> elements in the activity’s layout file or through code within the activity’s class implementation.
Creating a Fragment
The two components that make up a fragment are an XML layout file and a corresponding Kotlin class. The XML layout file for a fragment takes the same format as a layout for any other activity layout and can contain any combination and complexity of layout managers and views. The following XML layout, for example, is for a fragment consisting of a ConstraintLayout with a red background containing a single TextView with a white foreground:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/constraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/holo_red_dark"
tools:context=".FragmentOne">
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="My First Fragment"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
android:textColor="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Code language: HTML, XML (xml)
The corresponding class to go with the layout must be a subclass of the Android Fragment class. This class should, at a minimum, override the onCreateView() method, which is responsible for loading the fragment layout. For example:
package com.example.myfragmentdemo
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
class FragmentOne : Fragment() {
private var _binding: FragmentTextBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentTextBinding.inflate(inflater, container, false)
return binding.root
}
}
Code language: Kotlin (kotlin)
In addition to the onCreateView() method, the class may also override the standard lifecycle methods.
Once the fragment layout and class have been created, the fragment is ready to be used within application activities.
Adding a Fragment to an Activity using the Layout XML File
Fragments may be incorporated into an activity by writing Kotlin code or embedding the fragment into the activity’s XML layout file. Regardless of the approach used, a key point to be aware of is that when the support library is being used for compatibility with older Android releases, any activities using fragments must be implemented as a subclass of FragmentActivity instead of the AppCompatActivity class:
package com.example.myFragmentDemo
import androidx.fragment.app.FragmentActivity
import android.os.Bundle
class MainActivity : FragmentActivity() {
.
.
Code language: Kotlin (kotlin)
Fragments are embedded into activity layout files using the FragmentContainerView class. The following example layout embeds the fragment created in the previous section of this chapter into an activity layout:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment2"
android:name="com.ebookfrenzy.myfragmentdemo.FragmentOne"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:layout="@layout/fragment_one" />
</androidx.constraintlayout.widget.ConstraintLayout>
Code language: HTML, XML (xml)
The key properties within the <fragment> element are android:name, which must reference the class associated with the fragment, and tools:layout, which must reference the XML resource file containing the fragment’s layout.
Once added to the layout of an activity, fragments may be viewed and manipulated within the Android Studio Layout Editor tool. Figure 37-1, for example, shows the above layout with the embedded fragment within the Android Studio Layout Editor:
Adding and Managing Fragments in Code
The ease of adding a fragment to an activity via the activity’s XML layout file comes at the cost of the activity not being able to remove the fragment at runtime. To achieve full dynamic control of fragments during runtime, those activities must be added via code. This has the advantage that the fragments can be added, removed, and even made to replace one another dynamically while the application is running.
When using code to manage fragments, the fragment will still consist of an XML layout file and a corresponding class. The difference comes when working with the fragment within the hosting activity. There is a standard sequence of steps when adding a fragment to an activity using code:
- Create an instance of the fragment’s class.
- Pass any additional intent arguments through to the class instance.
- Obtain a reference to the fragment manager instance.
- Call the beginTransaction() method on the fragment manager instance. This returns a fragment transaction instance.
- Call the add() method of the fragment transaction instance, passing through as arguments the resource ID of the view that is to contain the fragment and the fragment class instance.
- Call the commit() method of the fragment transaction.
The following code, for example, adds a fragment defined by the FragmentOne class so that it appears in the container view with an ID of LinearLayout1:
val firstFragment = FragmentOne()
firstFragment.arguments = intent.extras
val transaction = fragmentManager.beginTransaction()
transaction.add(R.id.LinearLayout1, firstFragment)
transaction.commit()
Code language: Kotlin (kotlin)
The above code breaks down each step into a separate statement for clarity. The last four lines can, however, be abbreviated into a single line of code as follows:
supportFragmentManager.beginTransaction().add(
R.id.LinearLayout1, firstFragment).commit()
Code language: Kotlin (kotlin)
Once added to a container, a fragment may subsequently be removed via a call to the remove() method of the fragment transaction instance, passing through a reference to the fragment instance that is to be removed:
transaction.remove(firstFragment)
Code language: Kotlin (kotlin)
Similarly, one fragment may be replaced with another by a call to the replace() method of the fragment transaction instance. This takes as arguments the ID of the view containing the fragment and an instance of the new fragment. The replaced fragment may also be placed on what is referred to as the back stack so that it can be quickly restored if the user navigates back to it. This is achieved by making a call to the addToBackStack() method of the fragment transaction object before making the commit() method call:
val secondFragment = FragmentTwo()
transaction.replace(R.id.LinearLayout1, secondFragment)
transaction.addToBackStack(null)
transaction.commit()
Code language: HTML, XML (xml)
Handling Fragment Events
As previously discussed, a fragment is like a sub-activity with its layout, class, and lifecycle. The view components (such as buttons and text views) within a fragment can generate events like regular activity. This raises the question of which class receives an event from a view in a fragment, the fragment itself, or the activity in which the fragment is embedded. The answer to this question depends on how the event handler is declared.
In the chapter entitled An Android Event Handling Tutorial, two approaches to event handling were discussed. The first method involved configuring an event listener and callback method within the activity’s code. For example:
binding.button.setOnClickListener { // Code to be performed on button click }
Code language: Kotlin (kotlin)
In the case of intercepting click events, the second approach involved setting the android:onClick property within the XML layout file:
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="onClick"
android:text="Click me" />
Code language: HTML, XML (xml)
The general rule for events generated by a view in a fragment is that if the event listener were declared in the fragment class using the event listener and callback method approach, the event would be handled first by the fragment. However, if the android:onClick resource is used, the event will be passed directly to the activity containing the fragment.
Implementing Fragment Communication
Once one or more fragments are embedded within an activity, the chances are good that some form of communication will need to take place between the fragments and the activity and between one fragment and another. Good practice dictates that fragments do not communicate directly with one another. All communication should take place via the encapsulating activity.
To communicate with a fragment, the activity must identify the fragment object via the ID assigned to it. Once this reference has been obtained, the activity can call the public methods of the fragment object.
Communicating in the other direction (from fragment to activity) is a little more complicated. In the first instance, the fragment must define a listener interface, which is then implemented within the activity class. For example, the following code declares a ToolbarListener interface on a fragment named ToolbarFragment. The code also declares a variable in which a reference to the activity will later be stored:
class ToolbarFragment : Fragment() {
var activityCallback: ToolbarFragment.ToolbarListener? = null
interface ToolbarListener {
fun onButtonClick(fontsize: Int, text: String)
}
.
.
}
Code language: Kotlin (kotlin)
The above code dictates that any class that implements the ToolbarListener interface must also implement a callback method named onButtonClick which, in turn, accepts an integer and a String as arguments.
Next, the onAttach() method of the fragment class needs to be overridden and implemented. This method is called automatically by the Android system when the fragment has been initialized and associated with an activity. The method is passed a reference to the activity in which the fragment is contained. The method must store a local reference to this activity and verify that it implements the ToolbarListener interface:
override fun onAttach(context: Context?) {
super.onAttach(context)
try {
activityCallback = context as ToolbarListener
} catch (e: ClassCastException) {
throw ClassCastException(context?.toString()
+ " must implement ToolbarListener")
}
}
Code language: Kotlin (kotlin)
Upon execution of this example, a reference to the activity will be stored in the local activityCallback variable, and an exception will be thrown if that activity does not implement the ToolbarListener interface.
The next step is to call the callback method of the activity from within the fragment. When and how this happens depends entirely on the circumstances under which the activity needs to be contacted by the fragment. The following code, for example, calls the callback method on the activity when a button is clicked:
override fun onButtonClick(arg1: Int, arg2: String) {
activityCallback.onButtonClick(arg1, arg2)
}
Code language: Kotlin (kotlin)
All that remains is to modify the activity class to implement the ToolbarListener interface. For example:
class MainActivity : FragmentActivity(),
ToolbarFragment.ToolbarListener {
override fun onButtonClick(arg1: Int, arg2: String) {
// Implement code for callback method
}
.
.
}
Code language: Kotlin (kotlin)
As we can see from the above code, the activity declares that it implements the ToolbarListener interface of the ToolbarFragment class and then proceeds to implement the onButtonClick() method as required by the interface.
Summary
Fragments provide a powerful mechanism for creating reusable modules of user interface layout and application behavior, which, once created, can be embedded in activities. A fragment consists of a user interface layout file and a class. Fragments may be utilized in an activity by adding the fragment to the activity’s layout file or writing code to manage the fragments at runtime. Fragments added to an activity in code can be removed and replaced dynamically at runtime. All communication between fragments should be performed via the activity within which the fragments are embedded.
Having covered the basics of fragments in this chapter, the next chapter will work through a tutorial designed to reinforce the techniques outlined in this chapter.