Creating a UI Fragment

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

  • compose a UI by defining widgets in a layout file

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

  • wire up the widgets inflated from the layout in code

Defining CrimeFragment’s layout

CrimeFragment’s view will display the information contained within an instance of Crime.

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

Listing 8.2  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 CrimeFragment 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.xml and enter LinearLayout as the root element.

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

Listing 8.3  Layout file for fragment’s view (res/layout/fragment_crime.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
            style="?android:listSeparatorTextViewStyle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            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"/>

    <TextView
            style="?android:listSeparatorTextViewStyle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            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 Nov 14 11:56 EST 2018"/>

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

</LinearLayout>

(The first TextView’s definition includes some new syntax related to view style: style="?android:listSeparatorTextViewStyle". Fear not. You will learn the meaning behind this syntax in the section called Styles, themes, and theme attributes in Chapter 10.)

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 8.9).

Figure 8.9  Previewing updated crime fragment layout

Previewing updated crime fragment layout

Creating the CrimeFragment class

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

Listing 8.4  Subclassing the Fragment class (CrimeFragment.kt)

class CrimeFragment : 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 8.10. (Recall that the Jetpack libraries are in packages that begin with androidx.)

Figure 8.10  Choosing the Jetpack Fragment class

Choosing the Jetpack Fragment class

If you do not see this dialog, try clicking into 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 consider migrating apps that use them to the current Jetpack version.

Fragments were introduced in API level 11, along with the first Android tablets and the sudden 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. No further updates will be made to this version, so you should not use it for new projects. Also, the earlier support library fragments have been moved to the Jetpack libraries. No further updates will be made to the support libraries after version 28. All future updates will apply to the Jetpack version, instead of the framework or v4 support fragments.

So: 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

CrimeFragment is a controller 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 their controller 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 FragmentManager and the fragment lifecycle later in this chapter.)

In CrimeFragment.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 the name of the onCreate(Bundle?) function. Android Studio will provide a list of suggestions, as shown in Figure 8.11.

Figure 8.11  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 8.5.

Listing 8.5  Overriding Fragment.onCreate(Bundle?) (CrimeFragment.kt)

class CrimeFragment : Fragment() {

    private lateinit var crime: Crime

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        crime = Crime()
    }
}

There are a couple of things to notice in this implementation. First, Fragment.onCreate(Bundle?) is public; Kotlin functions default to public when no visibility modifier is included in the definition. 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 whatever activity is hosting the fragment.

Second, similar to an activity, a fragment has a bundle to which it saves and retrieves its state. You can override Fragment.onSaveInstanceState(Bundle) for your own purposes, just as you can override Activity.onSaveInstanceState(Bundle).

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 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 the layout. The Bundle will contain data that this function can use to re-create the view from a saved state.

In CrimeFragment.kt, add an implementation of onCreateView(…) that inflates fragment_crime.xml. You can use the same trick from Figure 8.11 to fill out the function declaration.

Listing 8.6  Overriding onCreateView(…) (CrimeFragment.kt)

class CrimeFragment : Fragment() {

    private lateinit var crime: Crime

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        crime = Crime()
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_crime, container, false)
        return view
    }
}

Within onCreateView(…), you explicitly inflate the fragment’s view by calling LayoutInflater.inflate(…) and passing in the layout resource ID. The second parameter is your view’s parent, which is usually needed to configure the widgets 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.

Wiring up widgets in a fragment

You are now going to hook up the EditText, CheckBox, and Button in your fragment. The onCreateView(…) function is the place to wire up these widgets.

Start with the EditText. After the view is inflated, get a reference to the EditText using findViewById.

Listing 8.7  Wiring up the EditText widget (CrimeFragment.kt)

class CrimeFragment : Fragment() {

    private lateinit var crime: Crime
    private lateinit var titleField: EditText
    ...
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_crime, container, false)

        titleField = view.findViewById(R.id.crime_title) as EditText

        return view
    }
}

Getting references in Fragment.onCreateView(…) works nearly the same as in Activity.onCreate(Bundle?). The only difference is that you call View.findViewById(Int) on the fragment’s view. The Activity.findViewById(Int) function that you used before is a convenience function that calls View.findViewById(Int) behind the scenes. The Fragment class does not have a corresponding convenience function, so you have to call the real thing.

Once the reference is set, add a listener in the onStart() lifecycle callback.

Listing 8.8  Adding a listener to the EditText widget (CrimeFragment.kt)

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

    override fun onStart() {
        super.onStart()

        val titleWatcher = object : TextWatcher {

            override fun beforeTextChanged(
                sequence: CharSequence?,
                start: Int,
                count: Int,
                after: Int
            ) {
                // This space intentionally left blank
            }

            override fun onTextChanged(
                sequence: CharSequence?,
                start: Int,
                before: Int,
                count: Int
            ) {
                crime.title = sequence.toString()
            }

            override fun afterTextChanged(sequence: Editable?) {
                // This one too
            }
        }

        titleField.addTextChangedListener(titleWatcher)
    }
}

Setting listeners in a fragment works exactly the same as in an activity. Here, you create an anonymous class that implements the verbose TextWatcher interface. TextWatcher has three functions, but you only care about one: onTextChanged(…).

In onTextChanged(…), you call toString() on the CharSequence that is the user’s input. This function returns a string, which you then use to set the Crime’s title.

Notice that the TextWatcher listener is set up in onStart(). Some listeners are triggered not only when the user interacts with them but also when data is set on them when the view state is restored, such as on rotation. Listeners that react to data input, such as the TextWatcher for an EditText or an OnCheckChangedListener for a CheckBox, are susceptible to this behavior.

Listeners that only react to user interaction, such as an OnClickListener, are not susceptible to this, because they are not impacted by setting data on a view. This is the reason you did not encounter this in GeoQuiz. That app only used click listeners, which are not triggered on rotation, so you could set everything up in onCreate(…) – before any state restoration.

View state is restored after onCreateView(…) and before onStart(). When the state is restored, the contents of the EditText will get set to whatever value is currently in crime.title. At this point, if you have already set a listener on the EditText (such as in onCreate(…) or onCreateView(…)), TextWatcher’s beforeTextChanged(…), onTextChanged(…), and afterTextChanged(…) functions will execute. Setting the listener in onStart() avoids this behavior since the listener is hooked up after the view state is restored.

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

Listing 8.9  Setting Button text (CrimeFragment.kt)

class CrimeFragment : Fragment() {

    private lateinit var crime: Crime
    private lateinit var titleField: EditText
    private lateinit var dateButton: Button
    ...
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_crime, container, false)

        titleField = view.findViewById(R.id.crime_title) as EditText
        dateButton = view.findViewById(R.id.crime_date) as Button

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

        return view
    }
}

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

Moving on to the CheckBox, get a reference to it in onCreateView(…). Then, set a listener in onStart() that will update the solvedCheckBox field of the Crime, as shown in Listing 8.10. Even though the OnClickListener is not triggered by the state restoration of the fragment, putting it in onStart helps keep all of your listeners in one place and easy to find.

Listing 8.10  Listening for CheckBox changes (CrimeFragment.kt)

class CrimeFragment : Fragment() {

    private lateinit var crime: Crime
    private lateinit var titleField: EditText
    private lateinit var dateButton: Button
    private lateinit var solvedCheckBox: CheckBox
    ...
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_crime, container, false)

        titleField = view.findViewById(R.id.crime_title) as EditText
        dateButton = view.findViewById(R.id.crime_date) as Button
        solvedCheckBox = view.findViewById(R.id.crime_solved) as CheckBox
        ...
    }

    override fun onStart() {
        ...
        titleField.addTextChangedListener(titleWatcher)

        solvedCheckBox.apply {
            setOnCheckedChangeListener { _, isChecked ->
                crime.isSolved = isChecked
            }
        }
    }
}

Your code for CrimeFragment is now complete. It would be great if you could run CriminalIntent and play with the code you have written. But you cannot. Fragments cannot put their views onscreen on their own. To realize your efforts, you first have to add a CrimeFragment to MainActivity.

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

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