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

10. Completing the Detail View
Written by Darryl Bayliss

In the last chapter, you set up a new Activity to display the contents of a list. At the moment, that Activity is empty.

In this chapter, you’ll add to that Activity using familiar components such as a RecyclerView to display the list, and a FloatingActionButton to add tasks to the list. You’ll also learn how to communicate back to the previous Activity using an Intent.

Getting started

If you’re following along with your own project, 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.

Open ListDetailActivity.kt and review its contents.

Currently, you pass in a list from MainActivity.kt via an Intent and set the title of the Activity to the name of the list. That’s good, but this Activity needs to do more. For starters, it needs to let a user view all of the items in the list, as well as add new items.

You can accomplish the first task — viewing all of the items — using a RecyclerView.

Open activity_list_detail.xml from the res/layout folder, and show the Design view in the Layout window if it’s not already selected.

In the Palette window, select the Common option from the left-hand list. You’ll see the RecyclerView available for selection in the right-hand list.

Click and drag the RecyclerView to the whitespace in the Layout shown on the right of the Layout Window.

With the RecyclerView added, you need to give it an ID and some dimensions. In the Attributes window, change the ID of the RecyclerView to list_items_recyclerview.

Next, update the layout_width and layout_height to 0dp match_constraint. This ensures the RecyclerView adheres to the constraints you’re about to set, and that it takes up the entire screen.

In the Constraint Widget, click the four + buttons around the square to add constraints to the RecyclerView. Change the margins for each constraint to 0.

With the RecyclerView set up in the layout, it’s time to use it in your code.

Coding the RecyclerView

Open ListDetailActivity.kt. At the top of the class, add a property to hold a reference to the RecyclerView:

lateinit var listItemsRecyclerView : RecyclerView

If offered a choice when importing the RecyclerView, select the androidx.recyclerview.widget.RecyclerView version of the RecyclerView component.

In the bottom of onCreate(savedInstanceState: Bundle?), connect the RecyclerView from the Layout to the Activity. Then create an Adapter and Layout Manager for the RecyclerView:

// 1
listItemsRecyclerView =
    findViewById(R.id.list_items_recyclerview)
// 2
listItemsRecyclerView.adapter = ListItemsRecyclerViewAdapter(list)
// 3
listItemsRecyclerView.layoutManager = LinearLayoutManager(this)

As a recap, here’s how it works:

  1. Find the RecyclerView in the Activity Layout and assign it to the local variable.
  2. Assign the RecyclerView an Adapter, and pass in the list. It needs to know about the list so it can tell the RecyclerView what tasks to show. You’ll create the Adapter shortly, so ignore the Unresolved reference for now.
  3. Assign the RecyclerView a Layout Manager that uses a LinearLayoutManager to handle the presentation.

You’re ready to create a new Adapter for the RecyclerView. In the Project navigator, right-click com.raywenderlich.listmaker.

In the popup that appears, navigate to New ▸ Kotlin File ▸ Class.

Name the class ListItemsRecyclerViewAdapter, and ensure Kind is set to Class. When you’re ready, press the return key.

Android Studio creates the new Kotlin class and opens it.

Before using the class, you need to make a few adjustments.

In ListDetailActivity.kt, you need to pass a list to the RecyclerView Adapter. Then, to use this list, the Adapter needs a constructor that accepts a TaskList.

You also need to make the class implement the RecyclerView.Adapter<ViewHolder> Interface so the Adapter can create ViewHolders for the RecyclerView and reuse them as necessary.

Finally, you need to create a custom ViewHolder that you can use to show the tasks in the list.

First, update the class definition so it has a primary constructor that accepts a TaskList and have it conform to RecyclerView.Adapter<ListItemViewHolder>:

class ListItemsRecyclerViewAdapter(var list: TaskList) : RecyclerView.Adapter<ListItemViewHolder>()

You’ll create the ListItemViewHolder shortly, so ignore the Unresolved reference here too.

Create another Kotlin class. Set the name of the file to ListItemViewHolder, and set Kind to Class.

After Android Studio creates the class, update its definition so it has a primary constructor to pass in a View. Also make it implement the RecyclerView.ViewHolder(itemView) interface:

class ListItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)

With the bare bones of the Adapter and ViewHolder set up, your next task is to instruct the Adapter how to work with the list of tasks.

Adapting the Adapter

Open ListItemsRecyclerViewAdapter.kt.

This Adapter has to implement the methods required by RecyclerView.Adapter so the RecyclerView knows how to present each task in the list.

To get started quickly, there’s a way to let Android Studio do most of the work for you. Click the class name (the part where the red squiggly line is) and press Option-Return (or Alt-Return if you’re not on a Mac).

In the popup that appears, you’ll see the first option highlighted is Implement Members. Press Return again, and Android Studio presents another window.

This window shows the methods you need to implement to conform to RecyclerView.Adapter, the Interface your class has implemented. You need to implement all of these methods, so hold down Shift and click the bottom-most method.

This highlights all of the methods in blue, which means you’ve selected all of the ones you want Android Studio to implement. To finish this set up, click OK.

With that, Android Studio automatically generates the chosen methods for you:

Next, you need to do is write the logic behind each method.

Begin with getItemCount(). This method tells the RecyclerView how many items to display. You want it to show all of the tasks in your list, so update the method so it returns the number of tasks it contains:

override fun getItemCount(): Int {
  return list.tasks.size
}

Next, move onto creating the ViewHolder in onCreateViewHolder().

Because you haven’t created the Layout yet, you’ll add the code and then create the Layout. Update onCreateViewHolder() so it creates a View from the Layout using a LayoutInflater, which it passes into your ViewHolder, ready for use:

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListItemViewHolder {

  val view = LayoutInflater.from(parent.context)
             .inflate(R.layout.task_view_holder, parent, false)
  return ListItemViewHolder(view)
}

Don’t worry about the Unresolved reference for task_view_holder; you’ll create that next.

With the ViewHolder ready for use, you now need a Layout for the LayoutInflater to inflate. In the Project navigator, to the left of Android Studio, right-click layout inside res. Move the cursor to new, and click Layout Resource File.

The New Resource File window appears. Enter the file name as task_view_holder. Change the Root Element below the name from androidx.constraintlayout.widget.ConstraintLayout to LinearLayout.

This sets the Layout to use a LinearLayout. A LinearLayout allows you to stack Views in a vertical or horizontal direction. For simple Views like task_view_holder, a LinearLayout is easier to use than a ConstraintLayout.

Click OK, and let Android Studio create the new Layout.

Before you continue, you need to set the LinearLayout to be only as tall as the content within it; Otherwise, every row will be set to the size of its entire parent - in this case, the RecyclerView, which takes up the whole screen!

Select LinearLayout in the Component Tree window. In the Attributes window, set the layout_height to wrap_content:

The layout is now only as tall as whatever is inside of it. At this point, you might see its height shrink down to nothing in the Design tab as there’s nothing in it yet.

You can change this by adding the Widgets you want to use with the ViewHolder. In this case, you only need a TextView to hold a task in the list.

In the Palette window, click Common, and then drag a TextView into the LinearLayout via the Component Tree.

In the Attributes window, with the TextView selected, change the ID to textview_task, and set the layout_width and layout_height to wrap_content.

There’s one final tweak needed here, adding margin spacings so the TextView isn’t pushed to the edge of the ViewHolder.

To add margin spacings, you need to get to the larger list of attributes for the TextView. Scroll through the list of attributes until you find the layout_margin attribute.

Click the arrow next to layout_margin to expand the list. Then, in the layout_marginLeft and layout_marginTop text fields, enter 16dp.

Note: You’ll notice that in addition to the left and right margins, there are also layout margins for “start” and “end”. These are used to ensure that your layout can handle both languages that read left to right, such as English, and those that read right to left, such as Arabic and Hebrew.

For now, to keep things simpler, we’ll use “right” and “left” instead of “start” and “end”, but this is a good thing to keep in mind if you’re potentially supporting any right-to-left languages.

With the Layout ready, it’s time to use it in your code.

Visualizing the ViewHolder

You now have a Layout for the ViewHolder, next, you need to reference the TextView in the Layout in your code.

Open ListItemViewHolder.kt and create a property to hold the reference to the TextView, between the class brackets:

val taskTextView = itemView.findViewById(R.id.textview_task)
    as TextView

Now, when the ViewHolder is instantiated, it knows how to reference the TextView. Next, you need to hook up the data to the ViewHolder.

Open ListItemsRecyclerViewAdapter.kt, and in onBindViewHolder(), update it to bind to a specific task from the list depending on the position of the ViewHolder:

override fun onBindViewHolder(holder: ListItemViewHolder, position: Int) {
  holder.taskTextView.text = list.tasks[position]
}

Run the app on the emulator or a device and select one of the lists in the main Activity. It will runs, but you won’t see much.

Currently, there’s no way to add tasks to the lists. That’s ok though, your next task is to add a button to add tasks.

Open activity_list_detail.xml, and ensure the Design window is open. In the palette, select Buttons and grab a FloatingActionButton.

Drag the FAB into the Layout using the Component Tree Window. Be careful to drag it into the ConstraintLayout and not the RecyclerView. Otherwise, it won’t show up.

Once the button is dropped into the layout, a window appears asking you to select a resource for the action button.

In the Search bar, type add to filter the list of resources available. Click the ic_menu_add resource and click OK.

The button appears in the Layout, ready for you to position.

Select the button. In the Blueprint view (the blue screen next to the design preview), drag the right button constraint to the right-edge of the Layout. Then, drag the bottom button constraint to the bottom-edge of the Layout so it’s positioned at the bottom-right.

In the Attributes window, change the ID of the FAB to add_task_button. In the Constraints view, make sure the bottom and right constraints are added. If they’re not, add them by using +. Set the margin of the bottom and right constraints to 16 so the FAB has space from the screen.

The button is positioned correctly, your next task is to use the button to add tasks to your list. Open ListDetailActivity.kt , then add a new property to the top of the class to hold the reference for the new button:

lateinit var addTaskButton: FloatingActionButton

At the bottom of onCreate(), reference the button, then add a click listener to the button:

addTaskButton = findViewById(R.id.add_task_button)
addTaskButton.setOnClickListener {
  showCreateTaskDialog()
}

In the click listener, you call a method to prompt the user for the task to add to the list. You’ll create that method next.

Below onCreate(), add the method, showing a dialog to the user to ask for the task to add to the list:

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

  //2
  AlertDialog.Builder(this)
          .setTitle(R.string.task_to_add)
          .setView(taskEditText)
          .setPositiveButton(R.string.add_task) { dialog, _ ->
            // 3
            val task = taskEditText.text.toString()
            list.tasks.add(task)
            // 4
            val recyclerAdapter = listItemsRecyclerView.adapter as ListItemsRecyclerViewAdapter
            recyclerAdapter.notifyItemInserted(list.tasks.size-1)
            //5
            dialog.dismiss()
          }
          //6
          .create()
          .show()
}

The code will look familiar to you. It’s similar to showCreateListDialog(), which you created in MainActivity.kt. Here’s what’s happening:

  1. Create an EditText so you can receive text input from the user.
  2. Create an AlertDialogBuilder and use method chaining to set up various aspects of the AlertDialog. Method chaining can happen when each method returns a value, which can then be used for the next method. Here, when any method is called on the Builder, it returns the builder instance, modified with whatever you just added.
  3. In the Positive Button’s click listener, you access the EditText to grab the text input and create a task from the input.
  4. Still in the click listener, you notify the ListItemsRecyclerViewAdapter that a new item was added. This gives the Adapter a chance to check its datasource (the list) so it can inform the RecyclerView to create any new rows with the new information.
  5. Once the RecyclerAdapter updates, you close the dialog by dismissing it.
  6. Back outside the click listener, you continue to use method chaining to create and then show the AlertDialog without ever needing to have the AlertDialogBuilder as a separate variable.

Android Studio will let you know you’re missing some strings used for the title and positive button of the dialog.

Open strings.xml and add the following new string elements between the resources tags:

<string name="task_to_add">What is the task you want to add?</string>
<string name="add_task">Add</string>

These are shown in the app when the user adds a new task.

Finally, you need to save any new tasks added to the list.

Remember that ListDataManager you created when you first began to save lists? You’ll use it again to update the saved list with any new tasks it might have.

Returning Results from Activities

ListDataManager is declared and used in MainActivity.kt. To save the list with the newly added task, you could also declare it in ListDetailActivity.kt and save the list anytime the user adds a new task.

That would work, but it means you have two separate places in your app where data is saved, which gives you double the places where bugs could occur. To avoid that, you’ll pass the list back to the MainActivity and use the original list data manager.

You can ask Activities to return values to other Activities, you’ll learn how to do that now.

Open MainActivity.kt and edit showListDetail() so it looks like this:

private fun showListDetail(list: TaskList) {
  val listDetailIntent = Intent(this, ListDetailActivity::class.java)
  listDetailIntent.putExtra(INTENT_LIST_KEY, list)

  startActivityForResult(listDetailIntent, LIST_DETAIL_REQUEST_CODE)
}

The only change here is the final line. startActivity() has changed to startActivityForResult().

While this change may seem small, the difference is very important. This line starts the detail Activity, then MainActivity.kt will wait hear back from ListDetailActivity.kt once it finishes and removes itself from the screen.

Think of it as asking someone to do something for you, and then reporting back with the results when they’re finished. That’s what’s going on here: You want to hear back about that list you’re passing to ListDetailActivity.kt.

There’s also an additional parameter passed into startActivityForResult(). The second parameter is a request code that lets you know which result you’re dealing with.

Because you can be dealing with multiple Activities that pass back multiple results, having a unique way of identifying results is handy.

Add the request code in the companion object at the bottom of MainActivity.kt:

companion object {
  const val INTENT_LIST_KEY = "list"
  const val LIST_DETAIL_REQUEST_CODE = 123
}

Next, you need to handle the returned result. To do that, you need to override a new method in MainActivity named onActivityResult.

This method allows the Activity to receive the result of an Activities it starts. In this case, it looks for the result that ListDetailActivity.kt provides once it finishes adding tasks to a list:

override fun onActivityResult(requestCode: Int, resultCode: Int, data:
    Intent?) {
  super.onActivityResult(requestCode, resultCode, data)
  // 1
  if (requestCode == LIST_DETAIL_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
    // 2
    data?.let {
      // 3
      listDataManager.saveList(data.getParcelableExtra(INTENT_LIST_KEY) as TaskList)
      updateLists()
    }
  }
}

Going through this method step-by-step:

  1. You first check the request code is the same code you’re expecting to get back. Making sure you’re not dealing with any other requests here. You also check that the resultCode is RESULT_OK because there are times where a user might cancel an action.

  2. Once you know you’re dealing with the right request, you unwrap the data Intent passed in. It’s possible there isn’t any data at all here and it contains null, so it’s good to first make sure you have something to deal with.

  3. Once you confirm there’s data here, you save the list to the list data manager and then call updateLists(), which you’ll create shortly.

Note: You may have noticed the data?.let block in the code snippet. The .let function is a shorthand method available in Kotlin. It allows you to only execute a block of code if the variable .let is used on is not null.

This is what is meant by unwrapping the data Intent in the code snippet: You’re trying to unwrap the optional value to get at the actual value.

You can still use a null check like in Java, it’s all down to personal preference. All of this falls under the Null Safety paradigm of Kotlin, you can read more about it here: https://kotlinlang.org/docs/reference/null-safety.html.

Below onActivityResult(), add the updateLists() method:

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

This method reads the saved lists again so the RecyclerView is aware of any new tasks added to a List.

Finally, open ListDetailActivity.kt, and at the bottom of the class, add a new override method named onBackPressed():

override fun onBackPressed() {
  val bundle = Bundle()
  bundle.putParcelable(MainActivity.INTENT_LIST_KEY, list)

  val intent = Intent()
  intent.putExtras(bundle)
  setResult(Activity.RESULT_OK, intent)
  super.onBackPressed()
}

onBackPressed() gives you a chance to run code whenever the back button is tapped to get back to the List Activity. In this case, you literally bundle up the list in its current state, then put it into an Intent.

Finally, you set the result to RESULT_OK and pass in the Intent, informing the Activity that everything happened according to plan.

Time to test the app again. Click Run App at the top of Android Studio and select your device. Create a list if necessary, or select an existing list. Once inside the list, add a new task.

Tap the back button, then click into the list where you added a task, and you’ll see the newly added task. Well done!

Where to go from here?

You reused a lot of your knowledge in this chapter and picked up new tricks to reuse code in your apps in a clean way.

You also learned how to pass data between Activities and have a fully functioning list app.

In the next chapter, you’ll learn how to take your app and make it work on Android tablets, as well as on Android phones!

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.