9.
Communicating Between Activities
Written by Darryl Bayliss
So far in this book, you’ve made use of a single Activity for your apps. As your apps get more complicated, however, trying to cram more visual elements into a single Activity can make your app confusing for users. Keeping an Activity dedicated to a single task removes this problem.
At the moment, Listmaker has no way to add items to the lists you create. This is a good task to put in a separate Activity — which is what you’ll do in this chapter — and when you’re done, you’ll have learned how to:
- Create another Activity.
- Communicate between Activities using an Intent.
- Pass data between Activities.
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.
Note: If you added lists in the previous chapter, you’ll continue to see them inside your app. If you want to start fresh, delete the app from your device, then keep going with this chapter. All of the previous list data gets deleted when you delete the app.
With the Listmaker project open, run the app. When it appears, tap the Floating Action Button in the bottom-right and enter the list title as My List.
Tap Create, and the new list name shows up in the RecyclerView.
That works, but it isn’t too useful. If you tap the title of the list in the RecyclerView, nothing happens. Wouldn’t it be great if something were to happen? Absolutely!
You’ll fix this by creating another Activity. As a general rule, Activities should focus on a single task, so the logic within an Activity stays clean and simple as you build it. Single task Activities also benefit your users because navigation between screens becomes more intuitive.
Note: This rule of thumb also applies to Fragments. Every Fragment should be responsible for one job. You’ll learn more about Fragments in Chapter 11.
In the Project navigator, right-click com.raywenderlich.listmaker.ui.main.
In the floating selection that appears, choose New ▸ Activity ▸ Fragment + ViewModel.
Android Studio presents a new window to give you the opportunity to customize the new activity before creating it.
Creating a new Activity
The Configure Activity wizard provides some fields to customize the Activity:
-
Activity Name: Sets the name for the Activity. This is used to name the Kotlin class associated with the Activity.
-
Activity Layout Name: Sets the name of the XML file used to hold the Activity’s layout.
-
Fragment Name: Sets the name of the Fragment’s class and file name.
-
Fragment Layout Name: Sets the name of the XML file used to hold the Fragment layout.
-
ViewModel Name: Sets the name of the viewmodel generated alongside the Activity and Fragment.
-
Launcher Activity: This checkbox allows you to set the Activity as the Launcher Activity. Meaning it will be the first Activity shown when your app loads.
-
Package Name: Sets the package the Activity class will be created in.
-
Fragment package path: Sets the path of the package for the Fragment class.
-
Source Language: This drop-down lets you choose the programming language used to generate the classes. The choices are Java and Kotlin. In this project, the default is Kotlin.
Note: If you don’t see Source Language as an option, try scrolling inside the area where all of the options are located. Depending on your screen size, the value for language might not be visible unless you scroll.
For this screen, make sure the textfields are set to the following:
-
Activity Name: ListDetailActivity
-
Activity Layout Name: list_detail_activity
-
Fragment Name: ListDetailFragment
-
Fragment Layout Name: list_detail_fragment
-
ViewModel Name: ListDetailViewModel
-
Launcher Activity checkbox: Unchecked
-
Package Name: com.raywenderlich.listmaker.ui.detail
-
Fragment Package Path: ui.detail
-
Source Language: Kotlin
Click Finish in the bottom-right of the window and Android Studio creates the new Activity.
Android Studio even hooks up your Layout, so you don’t have to do this yourself. There’s also another place that’s been updated too, called the app manifest. You’ll learn more about it in the next section.
The app manifest
Every Android app has an app manifest. It’s important because it tells an Android device everything it needs to know about your app.
Android is strict about its requirements for a manifest. The file name must be AndroidManifest.xml and has to be located in the correct spot in the project file hierarchy. Without this file, Android refuses to run your app.
On the left side of Android Studio in the Project navigator, navigate to app ▸ manifests ▸ AndroidManifest.xml.
Note: The manifests folder in the sidebar is a virtual folder generated by Android Studio’s Android project view and is not directly related to anything in the file system. The actual file is kept at the root of your app’s main folder inside app/src.
Also, for now, don’t worry about any warnings that appear in the manifest.
This is an XML-based file containing various tags. The main tags in this file are manifest, application, and activity; there are plenty more you’ll use in chapters to come.
The manifest tag is the root element of the app manifest. You must declare all of the other tags within this tag. You also need to declare the package where your code sits within this tag as well. This is a security measure to ensure only your package is associated with this app.
The application tag contains app-specific information for the Android system, such as the icon to use for the app, the name of the app, and what theme style it uses. This information tells Android how to present the app on the home screen and how to represent it in other areas such as the Settings.
Perhaps the most interesting tags are the activity tags. Every Activity within an app should have a corresponding tag within the manifest. This is to ensure that your app only runs Activities from within your app, not any that may have come from elsewhere.
There’s a .MainActivity declared in there, with another tag, intent-filter, inside this declaration. This tells Android that MainActivity is the Activity to start when the app launches.
This happens because of the action and category tags inside intent-filter. You don’t need to be concerned about the details behind these tags at the moment — you’ll learn more about intents later in this chapter. What you need to know is the intent-filter is used to set your main Activity as the startup Activity.
You’ll also see .ListDetailActivity, which is the Activity you created in the first part of this chapter. When you create a new project or use the new Activity wizard, Android Studio does the difficult work of updating the manifest, so you don’t have to do this yourself.
If you prefer, you can edit the manifest manually, which you’ll do in future chapters. However, it’s best if you let Android Studio do the hard work to reduce the chance of human error.
Open up ListDetailFragment. This was created along with the activity. It comes with a newInstance method, a ViewModel and Layout. In onActivityCreated you will see the view model created with:
viewModel = ViewModelProvider(this).get(ListDetailViewModel::class.java)
Since we want the ViewModel to be shared between the activity and Fragment, change this to requireActivity so that it looks like:
viewModel = ViewModelProvider(requireActivity()).get(ListDetailViewModel::class.java)
Intents
Now that you have the two Activities, it’s time to give your app the ability to navigate between them. In the MainActivity, you have two main points of entry for the new Activity:
- When a user taps the name of the list in the RecyclerView.
- When a user enters the name of a new list and taps Create.
You’ll navigate between the two Activities using an Intent. An Intent is an object used to indicate work or an action your app will perform at some point in the future.
The Android OS relies heavily on Intents as its primary form of communication, so it’s best that you use them for your app communication as well. Intents are incredibly flexible and can perform a wide range of tasks such as communicating with other apps, providing data to processes, or starting up another screen.
In fact, your app is launched by the Android system via an Intent. Remember intent-filter in the app manifest? The filter allows an Activity to be picky about what Intents it handles. In the case of your MainActivity, it only wants to handle Intents that attempt to launch it.
With the theory done, you’re ready to begin creating your first Intent. In MainActivity.kt, add a method to create an Intent at the bottom of the file:
private fun showListDetail(list: TaskList) {
// 1
val listDetailIntent = Intent(this, ListDetailActivity::class.java)
// 2
listDetailIntent.putExtra(INTENT_LIST_KEY, list)
// 3
startActivity(listDetailIntent)
}
Here’s the breakdown of the method:
-
You create an Intent and pass in the current Activity and class of the Activity you want to show on the screen. Think of this as saying you’re currently on this screen, now you want to move to that screen.
-
Next, you add an Extra. Extras are keys with associated values you can provide to Intents, they give more information to the receiver about the action to be done. In this case, you want to display a list. This is why the method expects a
listvariable to be passed in, which you use as a parameter in theputExtra()call.You also pass in a constant named
INTENT_LIST_KEY. This is a string that the receiver of the Intent uses as a key to reference the list. You’ll add this constant later on, so it’s ok to ignore the Unresolved reference error for now. -
The final line is a method call to inform the current Activity to start another Activity, making use of the information provided within the Intent.
With the intent created, the next step is to make sure a list can be passed through to the ListDetailActivity through the Intent. At the moment this is a problem, since TaskLists can’t be passed through Intents. You’ll discover why and how to solve this in the next section.
Intents and Parcels
Open TaskList.kt and change the class declaration so it implements the Parcelable interface:
class TaskList(val name: String, val tasks: ArrayList<String> = ArrayList()) : Parcelable {
Parcelable lets you break down your object into types the Intent system is already familiar with: strings, ints, floats, Booleans, and other objects which conform to Parcelable. You can then put all of that information into a Parcel.
To help transfer data, Intents use a Bundle object which can contain Parcelable objects. This is exactly what you’re using to pass the list as an Extra in the Intent you set up earlier.
Next, you need to implement some required methods so your object can be parceled up. Add the following constructor and methods inside the braces of the TaskList class:
//1
constructor(source: Parcel) : this(
source.readString()!!,
source.createStringArrayList()!!
)
override fun describeContents() = 0
//2
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(name)
dest.writeStringList(tasks)
}
// 3
companion object CREATOR: Parcelable.Creator<TaskList> {
// 4
override fun createFromParcel(source: Parcel): TaskList = TaskList(source)
override fun newArray(size: Int): Array<TaskList?> = arrayOfNulls(size)
}
There’s a lot of boilerplate code here. For now, you only need to know about the four most important parts:
-
Reading from a Parcel: Here, you add a second constructor (as opposed to the primary constructor in the class declaration) so a
TaskListobject can be created from a passed-inParcel.The constructor grabs the values from the
Parcelfor the title (by callingreadStringon theParcel) and the list of tasks (by callingcreateStringArrayListon theParcel), then passes them into the primary constructor usingthis().Note that
readString()andcreateStringArrayList()return optionals. You know that the objects aTaskListexpect are a string and an ArrayList of strings, so you use the non-null assertion operator (!!) to get the non-optional values. -
Writing to a Parcel: This method is called when a
Parcelneeds to be created from theTaskListobject. The parcel being created is handed into this function, and you fill it in with the appropriate contents using the assortedwrite...functions. -
Fulfilling
staticinterface requirements: TheParcelableprotocol requires you to create apublic static Parcelable.Creator<T> CREATORfield and override some methods in it using Java. However,staticmethods don’t exist in Kotlin. Instead, you create acompanionobject meeting the same requirements and override the appropriate functions within that object. -
Calling your constructor: In the
CREATORcompanion object, you override the interface functioncreateFromParcel, and pass the parcel you get from this function along to the second constructor you just created, giving back a nice newTaskListwith all of the data from theParcel.
Note: For more information about the
Parcelableinterface, review the Android Documentation: https://developer.android.com/reference/android/os/Parcelable.html.
With the Parcelable interface implemented, any TaskList can be passed through an Intent.
Bringing everything together
Now that the TaskList can be passed around on Android, it’s time to tie everything together so you can pass TaskLists through to the next screen. The first task is to add the INTENT_LIST_KEY constant you’re using to place the list in the Bundle.
At the bottom MainActivity.kt, create a companion object and add the constant inside:
companion object {
const val INTENT_LIST_KEY = "list"
}
This constant is used by the Intent to refer to a list whenever it needs to pass one to the new Activity.
Next, you need to hook up showListDetail() to be called from a few different places. You’ll start with the list creation.
Inside showCreateListDialog(), go to the bottom of the setPositiveButton closure code. Add a call to showListDetail() after the dialog is dismissed, so it looks like this:
builder.setPositiveButton(positiveButtonTitle) { dialog, _ ->
dialog.dismiss()
val taskList = TaskList(listTitleEditText.text.toString())
viewModel.saveList(taskList)
showListDetail(taskList)
}
Now, when you create a new list, the app passes that list to the new Activity. Perfect!
You also want to show the details of the list if a user taps on an existing list in the RecyclerView. To do that, you need your RecyclerView to communicate with the Activity whenever a list item is tapped.
The easiest way to do that is to create an Interface on your RecyclerView, which your Activity can implement. Then, the ViewHolder used by the RecyclerView can inform the RecyclerView of any taps.
Let’s do that. Open ListSelectionRecyclerViewAdapter.kt and add the following new interface above onCreateViewHolder:
interface ListSelectionRecyclerViewClickListener {
fun listItemClicked(list: TaskList)
}
In the class declaration above that, update the constructor to allow passing in a ListSelectionRecyclerViewClickListener:
class ListSelectionRecyclerViewAdapter(val lists: MutableList<TaskList>, val clickListener: ListSelectionRecyclerViewClickListener) : RecyclerView.Adapter<ListSelectionViewHolder>() {
Finally, edit onBindViewHolder to add an onClickListener to the View of itemHolder:
override fun onBindViewHolder(holder: ListSelectionViewHolder, position: Int) {
holder.binding.itemNumber.text = (position + 1).toString()
holder.binding.itemString.text = lists[position].name
holder.itemView.setOnClickListener {
clickListener.listItemClicked(lists[position])
}
}
Open MainFragment.kt and update the class declaration to state that it conforms to the ListSelectionRecyclerViewClickListener interface you just created:
class MainFragment : Fragment(), ListSelectionRecyclerViewAdapter.ListSelectionRecyclerViewClickListener {
Then, at the bottom of the class, implement the method to conform to the interface:
override fun listItemClicked(list: TaskList) {
clickListener.listItemTapped(list)
}
Now, whenever a tap happens on a list item in the recyclerView, the Fragment is informed about it and calls clickListener.listItemTapped(), passing in the list the user taps on. Now pass in our new ListSelectionRecyclerViewClickListener() (MainFragment) to the adapter. Go to onActivityCreated() and update the adapter creation to:
val recyclerViewAdapter = ListSelectionRecyclerViewAdapter(viewModel.lists, this)
What is clickListener you might be wondering? This is another interface you will create shortly. Its job is to pass the list from the Fragment to the Activity. Where finally you will be able to pass the list to your second Activity.
At the top of MainFragment.kt, add the following interface:
interface MainFragmentInteractionListener {
fun listItemTapped(list: TaskList)
}
In the class declaration, update the constructor to allow passing in a MainFragmentInteractionListener.
class MainFragment(val clickListener: MainFragmentInteractionListener) : Fragment(), ListSelectionRecyclerViewAdapter.ListSelectionRecyclerViewClickListener {
In the companion object, update the newInstance() method to allow the Fragment to setup itself up with the listener passed in.
fun newInstance(clickListener: MainFragmentInteractionListener) = MainFragment(clickListener)
The Fragment is setup. Now it’s time to set the MainActivity up. Open MainActivity.kt, then update the class declaration to implement the MainFragmentInteractionListener interface.
class MainActivity : AppCompatActivity(), MainFragment.MainFragmentInteractionListener {
Next, update the creation of MainFragment in onCreate() so it passes in the Activity. This allows the Activity to receive the task list tapped by the user.
if (savedInstanceState == null) {
val mainFragment = MainFragment.newInstance(this)
supportFragmentManager.beginTransaction()
.replace(R.id.container, mainFragment)
.commitNow()
}
Finally, implement the interface method to pass the list to your method that creates the new Activity.
override fun listItemTapped(list: TaskList) {
showListDetail(list)
}
MainActivity is now ready to send your list. Before you can see it in action, you need to do one final thing: Handle the Intent on the other side in ListDetailActivity.
Open ListDetailActivity.kt, then add a variable to store the TaskList received from MainActivity , at the top of the class above onCreate():
lateinit var list: TaskList
Next, in onCreate(), you need to retrieve the list you passed in as an Extra. Change onCreate() so it matches this:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.list_detail_activity)
// 1
list = intent.getParcelableExtra(MainActivity.INTENT_LIST_KEY)!!
// 2
title = list.name
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.container, ListDetailFragment.newInstance())
.commitNow()
}
}
In this code, you:
- Use the key assigned to the list in MainActivity.kt to reference the list in the Intent and assign it to the
listvariable. - Assign the title of the Activity to the name of the list to let the user know what list they’re viewing.
Time to see your hard work in action!
Click the Run App button in the toolbar of Android Studio. Once the app is running, create a new list and name it Your New List. Tap Create and behold: The new Activity appears on screen with the new list.
Android took the intent you created in MainActivity.kt and passed it to ListDetailActivity.kt so that it can use the list in the new Activity.
Key Points
Listmaker is beginning to take shape. You now have two Activities, each dedicated to a particular task. You also know how to pass data between Activities. You’ve learned:
-
How to create a new Activity.
-
What an Intent is, and how to use it to pass data between Activities.
-
What the App Manifest is and why it’s important.
-
How to use interfaces to communicate between Fragments and Activities.
Where to go from here?
Intents are another common pattern you’ll see in all Android apps. They’re used for all kinds of purposes beyond starting Activities. Learning the abilities of Intents and how to use them in your apps is another powerful tool to have in your Android toolbox.