Creating a Fragment

The steps to create a fragment are the same as those you followed to create an activity:

  • compose a UI by defining views in a layout file

  • create the class and set its view to be the layout that you defined

  • wire up the views inflated from the layout in code

Defining CrimeDetailFragment’s layout

CrimeDetailFragment’s view will display the information contained in an instance of Crime.

First, define the strings that the user will see in res/values/strings.xml.

Listing 9.3  Adding strings (res/values/strings.xml)

<resources>
    <string name="app_name">CriminalIntent</string>
    <string name="crime_title_hint">Enter a title for the crime.</string>
    <string name="crime_title_label">Title</string>
    <string name="crime_details_label">Details</string>
    <string name="crime_solved_label">Solved</string>
</resources>

Next, you will define the UI. The layout for CrimeDetailFragment will consist of a vertical LinearLayout that contains two TextViews, an EditText, a Button, and a CheckBox.

To create a layout file, right-click the res/layout folder in the project tool window and select NewLayout resource file. Name this file fragment_crime_detail.xml and enter LinearLayout as the root element.

Android Studio creates the file and adds the LinearLayout for you. Add the views that make up the fragment’s layout to res/layout/fragment_crime_detail.xml.

Listing 9.4  Layout file for fragment’s view (res/layout/fragment_crime_detail.xml)

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="16dp">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textAppearance="?attr/textAppearanceHeadline5"
        android:text="@string/crime_title_label" />

    <EditText
        android:id="@+id/crime_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="@string/crime_title_hint"
        android:importantForAutofill="no"
        android:inputType="text" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textAppearance="?attr/textAppearanceHeadline5"
        android:text="@string/crime_details_label" />

    <Button
        android:id="@+id/crime_date"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        tools:text="Wed May 11 11:56 EST 2022" />

    <CheckBox
        android:id="@+id/crime_solved"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/crime_solved_label" />
</LinearLayout>

(The TextViews’ definitions include some new syntax related to view style: textAppearance="?attr/textAppearanceHeadline5". This theme attribute applies the Headline 5 typography settings to the text as specified by Google’s Material Design library. [It can also be customized in your application theme, if you want.] You will learn more about this syntax in the section called Styles, Themes, and Theme Attributes in Chapter 11.)

Recall that the tools namespace allows you to provide information that the preview is able to display. In this case, you are adding text to the date button so that it will not be empty in the preview. Check the Design tab to see a preview of your fragment’s view (Figure 9.8).

Figure 9.8  Previewing updated crime fragment layout

Previewing updated crime fragment layout

Creating the CrimeDetailFragment class

Create a Kotlin file for the CrimeDetailFragment class. This time, select Class for the file type, and Android Studio will stub out the class definition for you. Turn the class into a fragment by subclassing the Fragment class.

Listing 9.5  Subclassing Fragment (CrimeDetailFragment.kt)

class CrimeDetailFragment : Fragment() {
}

As you subclass the Fragment class, you will notice that Android Studio finds two classes with the Fragment name. You will see android.app.Fragment and androidx.fragment.app.Fragment. The android.app.Fragment is the version of fragments built into the Android OS. You will use the Jetpack version, so be sure to select androidx.fragment.app.Fragment, as shown in Figure 9.9. (Recall that the Jetpack libraries are in packages that begin with androidx.)

Figure 9.9  Choosing the Jetpack Fragment class

Choosing the Jetpack Fragment class

If you do not see this dialog, try clicking the Fragment class name. If the dialog still does not appear, you can manually import the correct class: Add the line import androidx.fragment.app.Fragment at the top of the file.

If, on the other hand, you have an import for android.app.Fragment, remove that line of code. Then import the correct Fragment class with Option-Return (Alt-Enter).

Different types of fragments

New Android apps should always be built using the Jetpack (androidx) version of fragments. If you maintain older apps, you may see two other versions of fragments being used: the framework version and the v4 support library version. These are legacy versions of the Fragment class, and you should migrate apps that use them to the current Jetpack version.

Fragments were introduced in API level 11, when the first Android tablets created the need for UI flexibility. The framework implementation of fragments was built into devices running API level 11 or higher. Shortly afterward, a Fragment implementation was added to the v4 support library to enable fragment support on older devices. With each new version of Android, both of these fragment versions were updated with new features and security patches.

But as of Android 9.0 (API 28), the framework version of fragments is deprecated and the earlier support library fragments have been moved to the Jetpack libraries. No further updates will be made to either of those versions, so you should not use them for new projects. All future updates will apply only to the Jetpack version.

Bottom line: Always use the Jetpack fragments in your new projects, and migrate existing projects to ensure they stay current with new features and bug fixes.

Implementing fragment lifecycle functions

CrimeDetailFragment is the class that interacts with model and view objects. Its job is to present the details of a specific crime and update those details as the user changes them.

In GeoQuiz, your activities did most of that work in activity lifecycle functions. In CriminalIntent, this work will be done by fragments in fragment lifecycle functions. Many of these functions correspond to the Activity functions you already know, such as onCreate(Bundle?). (You will learn more about the fragment lifecycle in the section called The fragment lifecycle later in this chapter.)

In CrimeDetailFragment.kt, add a property for the Crime instance and an implementation of Fragment.onCreate(Bundle?).

Android Studio can provide some assistance when overriding functions. Begin typing onCreate(Bundle?); Android Studio will provide a list of suggestions, as shown in Figure 9.10.

Figure 9.10  Overriding the onCreate(Bundle?) function

Overriding the onCreate(Bundle?) function

Press Return to select the option to override the onCreate(Bundle?) function, and Android Studio will create the declaration for you, including the call to the superclass implementation. Update your code to create a new Crime, matching Listing 9.6.

Listing 9.6  Overriding Fragment.onCreate(Bundle?) (CrimeDetailFragment.kt)

class CrimeDetailFragment : Fragment() {

    private lateinit var crime: Crime

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        crime = Crime(
            id = UUID.randomUUID(),
            title = "",
            date = Date(),
            isSolved = false
        )
    }
}

Much like activities, fragments are re-created on configuration changes by default, so they are not good places to hold state. In Chapter 13, you will use a ViewModel to hold this state, but this will work for now.

Kotlin functions default to public when no visibility modifier is included in the definition. So Fragment.onCreate(Bundle?), which has no visibility modifier, is public. This differs from the Activity.onCreate(Bundle?) function, which is protected. Fragment.onCreate(Bundle?) and other Fragment lifecycle functions must be public, because they will be called by whichever activity is hosting the fragment.

Also, note what does not happen in Fragment.onCreate(Bundle?): You do not inflate the fragment’s view. You configure the fragment instance in Fragment.onCreate(Bundle?), but you create and configure the fragment’s view in another fragment lifecycle function: onCreateView(LayoutInflater, ViewGroup?, Bundle?).

This function is where you inflate and bind the layout for the fragment’s view and return the inflated View to the hosting activity. The LayoutInflater and ViewGroup parameters are necessary to inflate and bind the layout. The Bundle will contain data that this function can use to re-create the view from a saved state.

In CrimeDetailFragment.kt, add an implementation of onCreateView(…) that inflates and binds fragment_crime_detail.xml. You can use the same trick from Figure 9.10 to fill out the function declaration.

Listing 9.7  Overriding onCreateView(…) (CrimeDetailFragment.kt)

class CrimeDetailFragment : Fragment() {

    private lateinit var binding: FragmentCrimeDetailBinding

    private lateinit var crime: Crime

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding =
            FragmentCrimeDetailBinding.inflate(inflater, container, false)
        return binding.root
    }
}

Much like in GeoQuiz, View Binding will generate a binding class that you can use to inflate and bind your layout. This time it is called FragmentCrimeDetailBinding.

As before, you call the inflate(…) function to accomplish the task. However, this time you call a slightly different version of the function – one that takes in three parameters instead of one. The first parameter is the same LayoutInflater you used before. The second parameter is your view’s parent, which is usually needed to configure the views properly.

The third parameter tells the layout inflater whether to immediately add the inflated view to the view’s parent. You pass in false because the fragment’s view will be hosted in the activity’s container view. The fragment’s view does not need to be added to the parent view immediately – the activity will handle adding the view later.

Once you return the root view within the binding, you are ready to start wiring up the views.

Wiring up views in a fragment

You are now going to hook up the EditText, CheckBox, and Button in your fragment. Your first instinct might be to add some code to onCreateView(…), but it is best if you keep onCreateView(…) simple and do not do much more there than bind and inflate your view. The onViewCreated(…) lifecycle callback is invoked immediately after onCreateView(…), and it is the perfect spot to wire up your views.

Start by adding a listener to the EditText in the onViewCreated(…) lifecycle callback.

Listing 9.8  Adding a listener to the EditText view (CrimeDetailFragment.kt)

class CrimeDetailFragment : Fragment() {
    ...
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        ...
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding.apply {
            crimeTitle.doOnTextChanged { text, _, _, _ ->
                crime = crime.copy(title = text.toString())
            }
        }
    }
}

Setting listeners in a fragment works exactly the same as in an activity. Here, you add a listener that will be invoked whenever the text in the EditText is changed. The lambda is invoked with four parameters, but you only care about the first one, text. The text is provided as a CharSequence, so to set the Crime’s title, you call toString() on it.

(The doOnTextChanged() function is actually a Kotlin extension function on the EditText class. Do not forget to import it from the androidx.core.widget package.)

When you are not using a parameter, like the remaining lambda parameters here, you name it _. Lambda arguments named _ are ignored, which removes unnecessary variables and can help keep your code tidy.

Next, connect the Button to display the date of the crime.

Listing 9.9  Setting Button text (CrimeDetailFragment.kt)

class CrimeDetailFragment : Fragment() {
    ...
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding.apply {
            crimeTitle.doOnTextChanged { text, _, _, _ ->
                crime = crime.copy(title = text.toString())
            }

            crimeDate.apply {
                text = crime.date.toString()
                isEnabled = false
            }
        }
    }
}

Disabling the button ensures that it will not respond to the user pressing it. It also changes its appearance to advertise its disabled state. In Chapter 14, you will enable the button and allow the user to choose the date of the crime.

The last change you need to make within this class is to set a listener on the CheckBox that will update the isSolved property of the Crime, as shown in Listing 9.10.

Listing 9.10  Listening for CheckBox changes (CrimeDetailFragment.kt)

class CrimeDetailFragment : Fragment() {
    ...
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding.apply {
            crimeTitle.doOnTextChanged { text, _, _, _ ->
                crime = crime.copy(title = text.toString())
            }

            crimeDate.apply {
                text = crime.date.toString()
                isEnabled = false
            }

            crimeSolved.setOnCheckedChangeListener { _, isChecked ->
                crime = crime.copy(isSolved = isChecked)
            }
        }
    }
}

It would be great if you could run CriminalIntent and play with the code you have written. But you cannot – yet. Remember, fragments cannot put their views onscreen on their own. To realize your efforts, you first have to add a CrimeDetailFragment to MainActivity.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset