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 II: Building a List App

Section 2: 7 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

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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.

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()
    }
  }
}

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.

val listDataManager: ListDataManager = ListDataManager(this)
lateinit var listsRecyclerView: RecyclerView
lateinit var listDataManager: ListDataManager
lateinit var listsRecyclerView: RecyclerView
override fun onAttach(context: Context) {
  super.onAttach(context)
  if (context is OnListItemFragmentInteractionListener) {
      listener = context
      listDataManager = ListDataManager(context)
  } else {
      throw RuntimeException("$context must implement OnListItemFragmentInteractionListener")
  }
}
val lists = listDataManager.readLists()

listsRecyclerView = findViewById(R.id.lists_recyclerview)
listsRecyclerView.layoutManager = LinearLayoutManager(this)
listsRecyclerView.adapter = ListSelectionRecyclerViewAdapter(lists, this)
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)
  }
}
class MainActivity : AppCompatActivity(), ListSelectionFragment.OnListItemFragmentInteractionListener {
override fun onListItemClicked(list: TaskList) {
  showListDetail(list)
}
class ListSelectionFragment : Fragment(), ListSelectionRecyclerViewAdapter.ListSelectionRecyclerViewClickListener {
override fun listItemClicked(list: TaskList) {
  listener?.onListItemClicked(list)
}

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.

private var listSelectionFragment: ListSelectionFragment = ListSelectionFragment.newInstance()
builder.setPositiveButton(positiveButtonTitle) { dialog, _ ->

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

  dialog.dismiss()
  showListDetail(list)
}
fun addList(list : TaskList) {

  listDataManager.saveList(list)

  val recyclerAdapter = listsRecyclerView.adapter as ListSelectionRecyclerViewAdapter
  recyclerAdapter.addList(list)
}
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)
    }
  }
}
fun saveList(list: TaskList) {
  listDataManager.saveList(list)
  updateLists()
}
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.

<androidx.recyclerview.widget.RecyclerView
  android:id="@+id/lists_recyclerview"
  android:layout_width="match_parent"
  android:layout_height="match_parent" />
<FrameLayout 
    android:id="@+id/fragment_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
private var fragmentContainer: FrameLayout? = null
fragmentContainer = findViewById(R.id.fragment_container)

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

Creating your next Fragment

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

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
    }
  }
}
  lateinit var listItemsRecyclerView: RecyclerView

  lateinit var list: TaskList
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    arguments?.let {
      list = it.getParcelable(MainActivity.INTENT_LIST_KEY)!!
    }
}
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
}
fun addTask(item: String) {

    list.tasks.add(item)

    val listRecyclerAdapter =  listItemsRecyclerView.adapter as ListItemsRecyclerViewAdapter
    listRecyclerAdapter.list = list
    listRecyclerAdapter.notifyDataSetChanged()
}
<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" />

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.

<?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>
<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" />
private var largeScreen = false
private var listFragment : ListDetailFragment? = null
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()
  }
}
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()
      }
  }
}
  <string name="list_fragment_tag">List Fragment</string>
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()
}
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()
  }
}

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.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now