Chapters

Hide chapters

Android Apprentice

Third Edition · Android 10 · Kotlin 1.3 · Android Studio 3.6

Before You Begin

Section 0: 4 chapters
Show chapters Hide chapters

Section III: Creating Map-Based Apps

Section 3: 7 chapters
Show chapters Hide chapters

11. Using Fragments
Written by Darryl Bayliss

Thanks to the standard set of hardware and software features Android includes across devices, adding new features to your app is easy. When it comes to designing an appealing user interface that adapts across all of these devices with varying screen sizes, things can get tricky!

In this chapter, you’ll adapt Listmaker to make full use of the additional screen space a tablet provides. Along the way, you’ll also learn:

  • What Fragments are and how they work with Activities.
  • How to split Activities into Fragments.
  • How to provide different Layout files for your app depending on the device’s screen size.

Getting started

If you’re following along with your own app, open it and keep using it with this chapter. If not, don’t worry. Locate the projects folder for this chapter and open the Listmaker app inside the starter folder.

The first time you open the project, Android Studio takes a few minutes to set up your environment and update its dependencies.

You’ll start off by creating a virtual device to emulates a tablet. If you have a physical tablet available, you can use that if you prefer.

With the Listmaker project open, click Android Virtual Device Manager along the top of Android Studio.

The AVD window pops up, showing you the emulators already available on your machine.

Click Create Virtual Device at the bottom left of the window.

A new window pops up asking what hardware you want the virtual device to emulate.

Select the Tablet category on the left. Notice the table in the middle of the window changes to offer a selection of tablets.

Select Pixel C. Then, in the bottom-right, click Next to show the next screen.

This screen asks what version of Android you want the device to run. Select the release name titled Q and click Next:

Note: You may need to download the Android image for Tablets before selecting the Android version.

If so, don’t worry. Just click the download button next to the release name, Android Studio will display a new window to show the download progress.

Once the download is complete. Press the Finish button to return back to the previous screen. Then, select Android Q and click Next.

The final screen displays the configuration for the device, allowing you to tweak properties like the device name, the orientation of the device on startup, and a range of advanced settings. Don’t worry about changing anything here. Click Finish to complete setting up the emulator.

Run your app using the new emulator. Close the AVD window, then next to the run app button at the top of Android Studio, select the new device using the dropdown.

Next, click the run button. The tablet emulator will begin to load. Once the tablet has started and the app loads, you’ll see that it looks exactly as it did on the phone.

Note: For Android Pie devices and above, you may need to enable auto-rotate on your device or emulator if the screen doesn’t rotate automatically.

To do this, swipe the notification drawer down to reveal the quick settings and ensure the auto-rotate button is not grayed out. Tap on it to enable auto-rotation if it is.

Try out using Listmaker on a bigger device.Create some lists and add tasks to each one, taking note of the extra real estate in the app.

Although the app works on a tablet, its design isn’t optimized for the extra space available on the screen. That’s your main task for this chapter, you need to consider how to make your app adapt to the size of a device’s screen.

You can do this restructuring Listmaker to support the best layout for both phones and tablets. This is where the concept of Fragments comes in.

What is a Fragment?

A Fragment is part of an Activity’s user interface and contributes its own Layout to the Activity. This lets you dynamically add and remove pieces of the user interface from the app while it’s running.

For instance, you can use this to your advantage to decide how many Fragments an Activity should show at runtime depending on the size of a screen.

If Listmaker is running on a tablet, you can have an Activity display two Fragments: one dedicated to selecting a list, and another to display the selected list. If Listmaker is running on a phone, you can show one of the Fragments in an Activity and show the next Activity when a list selection is made.

Fragments give you a lot of power to help you use as much of the available screen space on a device as possible.

Fragments have their own Lifecycles that work alongside the Activity’s lifecycle in which they are embedded. Since it’s unknown whether a Fragment will be displayed at runtime, it’s important they are self-contained as much as possible.

Note: If you want to read more about Fragments, read the official documentation available at https://developer.android.com/guide/components/fragments.html.

With the theory out of the way, you’re ready to create a Fragment.

In the Project navigator, right-click com.raywenderlich.listmaker. In the selection dialog that appears, select New ▸ Fragment ▸ Fragment (Blank).

Click Fragment (Blank), Android Studio displays a new window to customise the Fragment.

Let’s go through the options available:

The Fragment Name allows you to name the Fragment, similar to the way you name an Activity.

Change the Fragment name to ListSelectionFragment.

The next two options are Create Layout XML and Fragment Layout Name:

Similar to creating an Activity, Android Studio can create a Layout file for your Fragment. The Create Layout XML checkbox is checked by default, meaning Android Studio will create the Layout for the Fragment.

The Fragment Layout Name is used to name the Layout file for the Fragment. Android Studio has pre-populated this field with fragment_list_selection, based on the Fragment Name you entered. Leave this as it is.

The final option is Source Language:

The drop-down tells Android Studio what language to use to generate the code for your Fragment. Make sure Kotlin is selected and click the Finish button in the bottom-right of the window.

Android Studio uses the information from the window to create the Fragment, and opens up ListSelectionFragment.kt.

The generated Fragment has alot of generate coded you can remove to make it easier to understand. Change the file so it matches this:

class ListSelectionFragment : Fragment() {

  // 1
  private var listener: OnListItemFragmentInteractionListener? = null

  // 2
  override fun onAttach(context: Context) {
    super.onAttach(context)
    if (context is OnListItemFragmentInteractionListener) {
      listener = context
    } else {
      throw RuntimeException("$context must implement OnListItemFragmentInteractionListener")
    }
  }

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

  // 4
  override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
            savedInstanceState: Bundle?): View? {
    return inflater.inflate(R.layout.fragment_list_selection, container, false)
  }

  // 5
  override fun onDetach() {
    super.onDetach()
    listener = null
  }

  interface OnListItemFragmentInteractionListener {
    fun onListItemClicked(list: TaskList)
  }

  // 6
  companion object {

    fun newInstance(): ListSelectionFragment {
      return ListSelectionFragment()
    }
  }
}

There’s a lot of code here, all responsible for different things. Let’s go through the file:

  1. You define a private OnListItemFragmentInteractionListener variable to hold a reference to an object implementing the Fragment interface. The interface is defined at the bottom of the class, requiring a single method to be implemented to inform objects that a list has been tapped. MainActivity will implement this interface.

  2. onAttach is a lifecycle method run by a Fragment. Fragments have lifecycle methods available to override, similar to Activities. onAttach is run when the Fragment is first associated with an Activity, giving you a chance to set up anything required before the Fragment is created. In this method, you assign the context of the Fragment to listener if it implements the interface. This context is the MainActivity because it implements that interface.

  3. The next overridden lifecycle method is onCreate(savedInstanceState: Bundle?). This functions similarly to the method of the same name in an Activity, except it’s used when a Fragment is in the process of being created.

  4. Another lifecycle method, this one named onCreateView(). This is where the Fragment acquires the layout it wants to present within the Activity. Here, a Layout inflater is used to inflate the Layout and pass it back to the Fragment.

  5. This is the final lifecycle method in the class that’s called by a Fragment. onDetach() is called when a Fragment is no longer attached to an Activity, which happens when the Activity containing the Fragment is destroyed or the Fragment is removed. At this point within the method, listener is set to null as the Activity is no longer available.

  6. You define a companion object here with a newInstance() method inside. This is used by any object wanting to create a new instance of the Fragment.

From Activity to Fragments

With the code cleaned up, the next task is to move parts of MainActivity.kt and its Layout to the new Fragment.

Remember that splitting your code into individual, isolated Fragments makes them reusable. It’s essential that the Fragment needs nothing inside the Activity.

Open MainActivity.kt and remove the following properties:

val listDataManager: ListDataManager = ListDataManager(this)
lateinit var listsRecyclerView: RecyclerView

Move the properties to the top of ListSelectionFragment.kt with a slight modification:

lateinit var listDataManager: ListDataManager
lateinit var listsRecyclerView: RecyclerView

Notice that you’re no longer initializing listDataManager inline since Fragment does not extend from Context. This means you’ll have to initialize listDataManager at the earliest moment you get a Context, which is in onAttach(). You’ll do that next.

Update onAttach() in the Fragment to instantiate the ListDataManager when the Activity is attached:

override fun onAttach(context: Context) {
  super.onAttach(context)
  if (context is OnListItemFragmentInteractionListener) {
      listener = context
      listDataManager = ListDataManager(context)
  } else {
      throw RuntimeException("$context must implement OnListItemFragmentInteractionListener")
  }
}

Your ListDataManager works exactly the same, except it now gets the Context via the Fragment. You’ll notice errors in MainActivity.kt after you remove the last two variables. You’ll fix that now.

In onCreate() from MainActivity.kt, cut the following lines (you’ll paste them shortly inside the Fragment):

val lists = listDataManager.readLists()

listsRecyclerView = findViewById(R.id.lists_recyclerview)
listsRecyclerView.layoutManager = LinearLayoutManager(this)
listsRecyclerView.adapter = ListSelectionRecyclerViewAdapter(lists, this)

You need to move these lines into a new lifecycle method in the Fragment named onActivityCreated(). This method runs when the Activity to which the Fragment is attached has finished running its lifecycle method onCreate().

This ensures you have an Activity to work with and something to show your widgets.

Add the complete onActivityCreated() to ListSelectionFragment.kt:

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

  val lists = listDataManager.readLists()
  view?.let {
    listsRecyclerView = it.findViewById(R.id.lists_recyclerview)
    listsRecyclerView.layoutManager = LinearLayoutManager(activity)
    listsRecyclerView.adapter = ListSelectionRecyclerViewAdapter(lists, this)
  }
}

The next item to move from your Activity is the ListSelectionRecyclerViewClickListener interface implementation. This is the interface ListSelectionRecyclerViewAdapter provides to inform interested objects a list was selected.

Since ListSelectionRecyclerViewAdapter no longer exists in MainActivity.kt, you can move the adapter to the new Fragment. Your Activity, however, still needs to be aware of the list click event. This is because only Activities should start other Activities. Fragments, being isolated views, should inform Activities of any events to handle.

You may recall that ListSelectionFragment provides an interface you can use to talk back to its Activity. You can use that. At the top of MainActivity.kt, change the class declaration to implement the interface:

class MainActivity : AppCompatActivity(), ListSelectionFragment.OnListItemFragmentInteractionListener {

In MainActivity.kt, replace listItemClicked() from ListSelectionRecyclerViewClickListener with onListItemClicked() from OnListItemFragmentInteractionListener:

override fun onListItemClicked(list: TaskList) {
  showListDetail(list)
}

Similar to the ListSelectionRecyclerViewClickListener interface, when this method runs, it shows the detail of the TaskList in another Activity. The ListSelectionRecyclerViewClickListener interface method now has to be moved into the Fragment. The Fragment also needs to implement the interface to receive the list item click and pass up to the Activity.

In ListSelectionFragment.kt, update the class declaration to implement the ListSelectionRecyclerViewClickListener interface:

class ListSelectionFragment : Fragment(), ListSelectionRecyclerViewAdapter.ListSelectionRecyclerViewClickListener {

Next, implement the interface method by adding listItemClick() to the ListSelectionFragment class:

override fun listItemClicked(list: TaskList) {
  listener?.onListItemClicked(list)
}

When the method receives an item click from the RecyclerView Adapter, it uses listener to inform the Activity that it’s received an item click. This, in turn, allows the Activity to receive the list and to start a new Activity to show the list while keeping the app logic intact.

Adding Lists to the Data Manager

So far so good! There are still a few things to move over to your Fragment, so keep at it. The next piece of logic to move over to the Fragment is adding a list to the ListDataManager.

The data manager is now handled by your Fragment, but you still need to be able to use the data manager. Since it now resides in ListSelectionFragment, you need a reference to the Fragment in your Activity.

At the top of MainActivity.kt, create an instance of the ListSelectionFragment:

private var listSelectionFragment: ListSelectionFragment = ListSelectionFragment.newInstance()

This Fragment instance is created when the Activity is created.

Next, In showCreateListDialog() of the Activity, update the positive button click listener to pass the list through to your fragment:

builder.setPositiveButton(positiveButtonTitle) { dialog, _ ->

  val list = TaskList(listTitleEditText.text.toString())
  listSelectionFragment.addList(list)

  dialog.dismiss()
  showListDetail(list)
}

The change is subtle but important. You’ve removed the lines that added the list to the Data Manager residing in the Activity, and replaced them with a method call to the Fragment. By doing so, the Fragment now adds the list to the Data Manager.

You’ll see listSelectionFragment.addList(list) highlight in red telling you about an unresolved reference error; that’s because the Fragment doesn’t yet know how to add a list. You’ll fix that now.

In ListSelectionFragment.kt, add the missing method so it saves the list to the data manager and updates its RecyclerView:

fun addList(list : TaskList) {

  listDataManager.saveList(list)

  val recyclerAdapter = listsRecyclerView.adapter as ListSelectionRecyclerViewAdapter
  recyclerAdapter.addList(list)
}

Next, you have to save a list returned from the List Detail Activity. Again, the Fragment needs to handle this.

In MainActivity.kt, change onActivityResult() so the parcelable extra is passed into a method provided by the Fragment:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
  super.onActivityResult(requestCode, resultCode, data)

  if (requestCode == LIST_DETAIL_REQUEST_CODE) {
    data?.let {
      listSelectionFragment.saveList(data.getParcelableExtra(INTENT_LIST_KEY) as TaskList)
    }
  }
}

The saveList method will let you know about another unresolved reference error. That’s ok, you’ll fix that by adding the method to your Fragment.

In ListSelectionFragment.kt, add a new method so that the Fragment saves the updated state of the list received from MainActivity and update the RecyclerView:

fun saveList(list: TaskList) {
  listDataManager.saveList(list)
  updateLists()
}

Finally, move updateLists() from MainActivity into the Fragment.

private fun updateLists() {
  val lists = listDataManager.readLists()
  listsRecyclerView.adapter = ListSelectionRecyclerViewAdapter(lists, this)
}

Showing the Fragment

You’ve spent most of your time moving logic from the Activity to the Fragment. If you recall, the RecyclerView also resides in the Layout of this Activity, so you need to move the RecyclerView from the Activity into the Fragment.

You also need to ensure the Activity Layout knows to show the Fragment. This means you’re going to have to dive into the not-quite-so pretty side of Layouts, and use XML.

Open content_main.xml. If not already selected, select the Code button in the top right -corner of the Layout editor.

The editor updates to show the XML for the layout, rather than the user interface:

Until now, you’ve used the Design tab to create your Layouts. For this part, it’s easier to work with the XML representation of the layout because you need to copy the Views across different files.

In content_main.xml, cut the entirety of the androidx.recyclerview.widget.RecyclerView tag:

Open the fragment_list_selection.xml Layout. Select the Code button to show the XML if needed, then paste the RecyclerView over the generated textview:

The RecyclerView properties prefixed with app will show warnings, because the layout doesn’t understand what app is. app is what provides the constraint attributes for a View to lay it’s position out on the screen.

That’s ok, because in the new layout. The layout uses a FrameLayout, rather than a ConstraintLayout.

Remove the Constraint attributes in the RecyclerView, so it looks like the following:

<androidx.recyclerview.widget.RecyclerView
  android:id="@+id/lists_recyclerview"
  android:layout_width="match_parent"
  android:layout_height="match_parent" />

With the RecyclerView in its new layout, it’s time to update the Activity Layout to show the Fragment. Open up content_main.xml, then add a FrameLayout in between the ConstraintLayout tags:

<FrameLayout 
    android:id="@+id/fragment_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

A FrameLayout lets you allocate space for a single item. This is perfect for something like a Fragment that could take up an entire screen. You also give the FrameLayout an ID, to reference it in the Activity and set the layout_width and layout_height to match the size of your Activity.

Open MainActivity.kt and add a variable to hold a reference to the FrameLayout at the top of the file:

private var fragmentContainer: FrameLayout? = null

Update onCreate() in the Activity to grab the reference to the FrameLayout via the ID you assigned in the Layout. Add this code just before fab.setOnClickListener:

fragmentContainer = findViewById(R.id.fragment_container)

supportFragmentManager
  .beginTransaction()
  .add(R.id.fragment_container, listSelectionFragment)
  .commit()

Notice the use of supportFragmentManager? A FragmentManager is an object that lets you dynamically add and remove Fragments at runtime. This gives you a powerful tool to make the UI as flexible as possible across various screen sizes.

It’s called a support Fragment manager rather than just a Fragment manager because some older versions of Android didn’t include fragments. By calling supportFragmentManager, you’re able to take advantage of work the Android team has done to seamlessly support Fragments even on older operating systems where they weren’t originally included, without you having to do any extra work.

The SupportFragmentManager makes use of a FragmentTransaction. Transactions are how you describe to the SupportFragmentManager how to present the Fragments.

To begin presenting Fragments using SupportFragmentManager, you first call beginTransaction() to begin a transaction.

Once the transaction starts, you then call add(), telling SupportFragmentManager to add a Fragment into a container view that will hold the Fragment. To do this, add() takes two parameters: the ID of the container view, and an instance of the Fragment to show.

You pass in the ID of the FrameLayout and the instance of the ListSelectionFragment your Activity creates. Once the transaction is defined, commit() informs the SupportFragmentManager to add the Fragment so it’s visible in the Activity.

Finally, after all that moving around of code, build and run your app.

Click the Run App button at the top of Android Studio, making sure you run the app on the Tablet Emulator created earlier.

The app doesn’t look any different at this point, but under the hood, you‘re now using an Activity containing a Fragment. This is a good foundation to start making use of all that space on the tablet screen.

The next step is to replicate the ListDetailActivity screen into its own Fragment. You’ll do this in the next part.

Creating your next Fragment

Right-click com.raywenderlich.listmaker in the Project navigator and create a new blank Fragment.

The Create Fragment window you used earlier pops up. Change the Fragment name to ListDetailFragment and click Finish in the bottom-right.

Android Studio creates a ListDetailFragment.kt and a fragment_list_detail.xml Layout file for the Fragment. Open ListDetailFragment.kt, then update the entire class to remove unneeded template code:

class ListDetailFragment : Fragment() {

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

  override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                            savedInstanceState: Bundle?): View? {
    // Inflate the layout for this fragment
    return inflater.inflate(R.layout.fragment_list_detail, container, false)
  }

  companion object {

    private const val ARG_LIST = "list"

    fun newInstance(list: TaskList): ListDetailFragment {
      val fragment = ListDetailFragment()
      val args = Bundle()
      args.putParcelable(ARG_LIST, list)
      fragment.arguments = args
      return fragment
    }
  }
}

The main change here to the original code is the bundle argument passed in via newInstance(). It now expects a TaskList to be passed in since this Fragment is responsible for showing your list.

You also define an ARG_LIST constant as the key to put into the Bundle object for the Fragment, and also retrieve the TaskList from the Bundle object when the Fragment is created.

Next, you need to transfer some of the properties in ListDetailActivity.kt to the new Fragment. From the top of the Activity, copy the following lines to the top of ListDetailFragment.kt — be careful not to delete these from the Activity, you need them later:

  lateinit var listItemsRecyclerView: RecyclerView

  lateinit var list: TaskList

Next, update onCreate() in ListDetailFragment.kt so it grabs the list from the bundle passed in, if it exists:

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

    arguments?.let {
      list = it.getParcelable(MainActivity.INTENT_LIST_KEY)!!
    }
}

Change onCreateView() to set up the RecyclerView via the ID in the Layout and initialize the RecyclerView Adapter and LayoutManager:

override fun onCreateView(
  inflater: LayoutInflater,
  container: ViewGroup?,
  savedInstanceState: Bundle?): View? {

  // Inflate the layout for this fragment
  val view = inflater.inflate(R.layout.fragment_list_detail, container, false)

  view?.let {
    listItemsRecyclerView = it.findViewById(R.id.list_items_recyclerview)
    listItemsRecyclerView.adapter = ListItemsRecyclerViewAdapter(list)
    listItemsRecyclerView.layoutManager = LinearLayoutManager(context)
  }

  return view
}

Don’t worry about the unresolved reference when assigning listItemsRecyclerView it’s view, you’ll fix that soon.

Finally, add a method named addTask to the Fragment. You’ll use this method later to instruct the Fragment to add tasks to the list:

fun addTask(item: String) {

    list.tasks.add(item)

    val listRecyclerAdapter =  listItemsRecyclerView.adapter as ListItemsRecyclerViewAdapter
    listRecyclerAdapter.list = list
    listRecyclerAdapter.notifyDataSetChanged()
}

Now the Fragment is using the RecyclerView, you also need to make sure the RecyclerView exists in the Fragment Layout. To do that, you need to copy the RecyclerView from the ListDetailActivity layout to the ListDetailFragment layout.

In activity_list_detail.xml, with the Code editor open, copy (don’t cut!) the following lines from the Layout:

<androidx.recyclerview.widget.RecyclerView
        android:id="@+id/list_items_recyclerview"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

Open fragment_list_detail.xml, still with the Code editor open, and paste the RecyclerView in between the FrameLayout tags, replacing the TextView that was auto-generated when you created the Fragment.

Remove the lines that begin with app:layout_constraint and update the RecyclerViews layout_width and layout_height from 0dp to match_parent. You no longer need the constraint attributes now the RecyclerView is sitting within a FrameLayout.

Bringing the Activity into action

So far, you’ve focused on transferring code over from Activities to Fragments. Remember though, that Fragments need to exist within an Activity to be of use. The Activity also needs to be able to coordinate how it communicates with the Fragment and when it appears on the screen.

Your final job for this chapter is to make sure MainActivity.kt is able to:

  1. Show the new Fragments at the right time
  2. Provide information to each Fragment
  3. Let your app shift its appearance depending on the device.

First, you need to create a Layout that works for a large screen. In the Project navigator, right-click layout, then select New ▸ Layout resource file.

You’re going to create a new Layout file, with a tiny difference. You’re creating a version of the content_main.xml Layout that only displays on large screens.

This gives you the option to customize the UI for various sizes of screens. Android is even intelligent enough to automatically choose which Layout it should use as well. Very helpful!

For the File name, name your Layout content_main. Then, in the Available qualifiers list in the bottom-left of the window, select the size option and click >>.

From here, you can select various screen sizes to determine which sizes your Layout will use. You want the layout to be used by big screens (for example, tablets), so in the screen size dropdown, choose X-Large.

Click OK in the bottom-right and Android Studio creates the new Layout. Take a moment to look at the Project navigator to the left:

Android Studio now shows both the Layout files together in a drop-down, and even shows the qualifier you set to distinguish between the two. Now, you just have to populate it with the Layout you’d like.

You’re going to use the Text editor again for this, as it’s faster for this task. In content_main.xml (xlarge), ensure the Code editor is shown at the top right of the Layout editor window.

Replace the existing XML with the following code for the entire layout, so it contains a ConstraintLayout, a Fragment and a FrameLayout:

<?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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:tools="http://schemas.android.com/tools"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context="com.raywenderlich.listmaker.MainActivity"
    tools:showIn="@layout/activity_main">

    <!-- 1 -->
    <fragment
        android:id="@+id/list_selection_fragment"
        android:name="com.raywenderlich.listmaker.ListSelectionFragment"
        android:layout_width="300dp"
        android:layout_height="match_parent"
        android:layout_marginStart="0dp"
        android:layout_marginTop="8dp"
        android:layout_marginBottom="0dp"
        android:layout_weight="1"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <!-- 2 -->
    <FrameLayout
        android:id="@+id/fragment_container"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_weight="2"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="1.0"
        app:layout_constraintStart_toEndOf="@+id/list_selection_fragment"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

ConstraintLayout should be familiar to you now. But what’s going on with the Fragment and FrameLayout? When the larger layout is shown on a large screen, you need both the ListSelectionFragment and ListDetailFragment to appear to make use of the extra space.

The list selection Fragment is static and never hidden, so you dedicate an entire fragment tag to it. You also tell it which Fragment to use via the android:name attribute.

The FrameLayout is where the list detail fragment will sit. This is changeable because you want to show different lists depending on which list is selected in the selection Fragment.

Rather than update the entire Fragment, it’s easier to load up a new one that contains the newly selected list.

You’ll also need to show the Fragment in the original content_main.xml. Open the original content_main.xml file and replace the FrameLayout in between the ConstraintLayout tags so it shows a single Fragment:

<fragment
    android:id="@+id/list_selection_fragment"
    android:name="com.raywenderlich.listmaker.ListSelectionFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginStart="0dp"
    android:layout_marginTop="8dp"
    android:layout_marginBottom="0dp"
    android:layout_weight="1"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

This change makes it easier for you to work out whether or not your app is running on a device with a large screen. You’ll investigate this in closer detail later.

Next, you need to change MainActivity.kt to handle both Layouts, depending on the size of the device screen Listmaker is running on. The first thing you need is a way to know if you’re using the larger layout.

At the top of MainActivity.kt, add a Boolean to track whether the larger Layout is in use. You also need a ListDetailFragment instance for use later, so create a property for it while you’re here:

private var largeScreen = false
private var listFragment : ListDetailFragment? = null

In onCreate(), update the method to use the supportFragmentManager to find your ListSelectionFragment by its identifier, as well as the FrameLayout. Because your FrameLayout only exists in the larger layout, you use a null check here to find out whether the larger Layout is in use.

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  setContentView(R.layout.activity_main)
  setSupportActionBar(toolbar)

  listSelectionFragment = supportFragmentManager.findFragmentById(R.id.list_selection_fragment) as ListSelectionFragment

  fragmentContainer = findViewById(R.id.fragment_container)

  largeScreen = (fragmentContainer != null)

  fab.setOnClickListener {
    showCreateListDialog()
  }
}

Next, update showListDetail() to use the largeScreen Boolean to work out whether to show the Activity or replace the ListDetailFragment shown by using the supportFragmentManager. If a ListDetailFragment is already showing, then it will automatically show the new Fragment instead:

private fun showListDetail(list: TaskList) {

  if (!largeScreen) {

    val listDetailIntent = Intent(this, ListDetailActivity::class.java)
    listDetailIntent.putExtra(INTENT_LIST_KEY, list)

    startActivityForResult(listDetailIntent, LIST_DETAIL_REQUEST_CODE)
  } else {
    title = list.name

    listFragment = ListDetailFragment.newInstance(list)
    listFragment?.let {
        supportFragmentManager.beginTransaction()
                .replace(R.id.fragment_container, it, getString(R.string.list_fragment_tag))
                .addToBackStack(null)
                .commit()
      }

      fab.setOnClickListener {
        showCreateTaskDialog()
      }
  }
}

Note that you unwrap listFragment using a ?.let because the compiler has no way of knowing if it was reset between the assignment and trying to pass it into the fragment manager.

You’re also using the list_fragment_tag string above to use with the replace transaction. This string is called a tag and is used by the supportFragmentManager in case you want to reference it in the future.

Android Studio is throwing a Unresolved Reference error for the string, as well as the showCreateTaskDialog method, because they don’t exist yet in Listmaker. You’ll add these now.

Open strings.xml and add the following string:

  <string name="list_fragment_tag">List Fragment</string>

Note: If you get an error stating that list_fragment_tag is unresolved after adding the string, this usually means that Android Studio hasn’t recompiled the project’s R file. Click the build button in the top tool bar, or from the menu Build, select Make Project.

In MainActivity.kt. You must also change the behavior of the FloatingActionButton when adding tasks to a list. Since the RecyclerView was moved into the Fragment, you’ll see a compilation error at this point.

Add the missing showCreateTaskDialog method to MainActivity.kt:

private fun showCreateTaskDialog() {
  val taskEditText = EditText(this)
  taskEditText.inputType = InputType.TYPE_CLASS_TEXT

  AlertDialog.Builder(this)
          .setTitle(R.string.task_to_add)
          .setView(taskEditText)
          .setPositiveButton(R.string.add_task) { dialog, _ ->
            val task = taskEditText.text.toString()
            listFragment?.addTask(task)
            dialog.dismiss()
          }
          .create()
          .show()
}

Finally, override onBackPressed() so the Activity knows how to deal with the back button being pressed when using Fragments.

override fun onBackPressed() {
  super.onBackPressed()

  // 1
  title = resources.getString(R.string.app_name)

  // 2
  listFragment?.list?.let {
    listSelectionFragment.listDataManager.saveList(it)
  }

  // 3
  listFragment?.let {
    supportFragmentManager
            .beginTransaction()
            .remove(it)
            .commit()
    listFragment = null
  }

  // 4
  fab.setOnClickListener {
    showCreateListDialog()
  }
}

Going through the method:

  1. When the back button is pressed and isn’t focused on a List, you want the Activity to show the name of the app. You retrieve the name of the app from Strings.xml by using the resources property available through MainActivity.

  2. Since you aren’t using two Activities, you cannot rely on onActivityResult to update your ListDataManager with any updates made to your list. Therefore, you need to tell the ListDataManager to save the list.

  3. Remove the detail Fragment from the Layouts. Since a user can tap the back button as much as they wish, you’ll have to make sure that the detail Fragment is only removed once. You use a ?.let since if you directly check the listFragment variable, it can get reset between the check and the Fragment manager transaction.

  4. Update the FAB to create lists again.

It’s worth mentioning that because super.onBackPressed() is being called, you’re also deferring to any behavior from MainActivity. In this case, if a detail Fragment isn’t visible to the user, the app will close and bring you to the Android home screen.

With that done, you’re ready to see all your hard work in action! Run your app on a phone-sized device and start creating lists.

Run your app on a tablet-sized device, tap the FAB to create a list and then when the title changes to the name of the list. Tap the FAB again to add a task. You’ll immediately see the difference.

Your app now displays two different screens, at the same time, making better use of the available space.

Where to go from here?

Fragments are a difficult concept to grasp in Android. What you’ve encountered here is a brief dip into the benefits they can provide. Any app that wants to succeed across multiple devices and multiple size classes need to use Fragments to ensure it provides the best experience for its users.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.