1. Welcome
In the previous codelab, you modified the AndroidTrivia app to add navigation to the app. In this codelab, you modify the app so that the user can share their game-play results. The user can initiate an email or text, or they can copy their game-play results to the clipboard.
What you should already know
- The fundamentals of Kotlin
- How to create basic Android apps in Kotlin
What you'll learn
- How to use the
Bundle
class to pass arguments from one Fragment to another - How to use the Safe Args Gradle plugin for type safety
- How to add a "share" menu item to an app
- What an implicit intent is and how to create one
What you'll do
- Modify the AndroidTrivia code to use the Safe Args plugin, which generates
NavDirection
classes. - Use one of the generated
NavDirection
classes to pass type-safe arguments between theGameFragment
and the game-state fragments. - Add a "share" menu item to the app.
- Create an implicit intent that launches a chooser that the user can use to share a message about their game results.
2. App overview
The AndroidTrivia app, which you worked on in the previous two codelabs, is a game in which users answer questions about Android development. If the user answers three questions correctly, they win the game.
Download the AndroidTriviaNavigation starter code, or if you successfully completed the previous codelab, use that code as the starter code for this codelab.
In this codelab, you update the AndroidTrivia app so that users can send their game results to other apps and share their results with friends.
3. Task: Set up and use the Safe Args plugin
Before users can share their game results from within the AndroidTrivia app, your code needs to pass parameters from one Fragment to another. To prevent bugs in these transactions and make them type-safe, you use a Gradle plugin called Safe Args. The plugin generates NavDirection
classes, and you add these classes to your code.
In later tasks in this codelab, you use the generated NavDirection
classes to pass arguments between fragments.
Why you need the Safe Args plugin
Often your app will need to pass data between fragments. One way to pass data from one Fragment to another is to use an instance of the Bundle
class. An Android Bundle
is a key-value store.
A key-value store, also known as a dictionary or associative array, is a data structure where you use a unique key (a string) to fetch the value associated with that key. For example:
Key | Value |
"name" | "Anika" |
"favorite_weather" | "sunny" |
"favorite_color" | "blue" |
Your app could use a Bundle
to pass data from Fragment A to Fragment B. For example, Fragment A creates a bundle and saves the information as key-value pairs, then passes the Bundle
to Fragment B. Then Fragment B uses a key to fetch a key-value pair from the Bundle
. This technique works, but it can result in code that compiles, but then has the potential to cause errors when the app runs.
The kinds of errors that can occur are:
- Type mismatch errors. For example, if Fragment A sends a string but Fragment B requests an integer from the bundle, the request returns the default value of zero. Since zero is a valid value, this kind of type mismatch problem does not throw an error when the app is compiled. However, when the user runs the app, the error might make the app misbehave or crash.
- Missing key errors. If Fragment B requests an argument that isn't set in the bundle, the operation returns
null
. Again, this doesn't throw an error when the app is compiled but could cause severe problems when the user runs the app.
You want to catch these errors when you compile the app in Android Studio, so that you catch these errors before deploying the app into production. In other words, you want to catch the errors during app development so that your users don't encounter them.
To help with these problems, Android's Navigation Architecture Component includes a feature called Safe Args. Safe Args is a Gradle plugin that generates code and classes that help detect errors at compile-time that might not otherwise be surfaced until the app runs.
Step 1: Open the starter app and run it
- Download the AndroidTriviaNavigation starter app for this codelab:
If you completed the previous codelab on adding navigation to your app, use your solution code from that codelab.
Run the app from Android Studio:
- Open the app in Android Studio.
- Run the app on an Android-powered device or on an emulator. The app is a trivia game with a navigation drawer, an options menu on the title screen, and an Up button at the top of most of the screens.
- Explore the app and play the game. When you win the game by answering three questions correctly, you see the Congratulations screen.
In this codelab, you add a share icon to the top of the Congratulations screen. The share icon lets the user share their results in an email or text message.
Step 2: Add Safe Args to the project
- In Android Studio, open the project-level
build.gradle
file. - Add the
navigation-safe-args-gradle-plugin
dependency, as shown below:
// Adding the safe-args dependency to the project Gradle file
dependencies {
...
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"
}
- Open the app-level
build.gradle
file. - At the top of the file, after all the other plugins, add the
apply plugin
statement with theandroidx.navigation.safeargs
plugin:
// Adding the apply plugin statement for safeargs
apply plugin: 'androidx.navigation.safeargs'
- Re-build the project. If you are prompted to install additional build tools, install them.
The app project now includes generated NavDirection
classes.
The Safe Args plugin generates a NavDirection
class for each Fragment. These classes represent navigation from all the app's actions.
For example, GameFragment
now has a generated GameFragmentDirections
class. You use the GameFragmentDirections
class to pass type-safe arguments between the game Fragment and other fragments in the app.
To see the generated files, explore the generatedJava folder in the Project > Android pane.
Step 3: Add a NavDirection class to the game Fragment
In this step, you add the GameFragmentDirections
class to the game Fragment. You'll use this code later to pass arguments between the GameFragment
and the game-state fragments (GameWonFragment
and GameOverFragment
).
- Open the
GameFragment.kt
Kotlin file that's in the java folder. - Inside the
onCreateView()
method, locate the game-won conditional statement ("We've won!"). Change the parameter that's passed into theNavController.navigate()
method: Replace the action ID for the game-won state with an ID that uses theactionGameFragmentToGameWonFragment()
method from theGameFragmentDirections
class.
The conditional statement now looks like the following code. You'll add parameters to the actionGameFragmentToGameWonFragment()
method in the next task.
// Using directions to navigate to the GameWonFragment
view.findNavController()
.navigate(GameFragmentDirections.actionGameFragmentToGameWonFragment())
- Likewise, locate the game-over conditional statement ("Game over!"). Replace the action ID for the game-over state with an ID that uses the game-over method from the
GameFragmentDirections
class:
// Using directions to navigate to the GameOverFragment
view.findNavController()
.navigate(GameFragmentDirections.actionGameFragmentToGameOverFragment())
4. Task: Add and pass arguments
In this task, you add type-safe arguments to the gameWonFragment
and pass the arguments safely into a GameFragmentDirections
method. Similarly you then will replace the other Fragment classes with their equivalent NavDirection
classes.
Step 1: Add arguments to the game-won Fragment
- Open the
navigation.xml
file, which is in the res > navigation folder. Click the Design tab to open the navigation graph, which is where you'll set the arguments in the fragments. - In the preview, select the gameWonFragment.
- In the Attributes pane, expand the Arguments section.
- Click the + icon to add an argument. Name the argument
numQuestions
and set the type to Integer, then click Add. This argument represents the number of questions the user answered. - Still with the gameWonFragment selected, add a second argument. Name this argument numCorrect and set its type to Integer. This argument represents the number of questions the user answered correctly.
If you try to build the app now, you will likely get two compile errors.
No value passed for parameter 'numQuestions'
No value passed for parameter 'numCorrect'
You fix this error in the coming steps.
Step 2: Pass the arguments
In this step, you pass the numQuestions
and questionIndex
arguments into the actionGameFragmentToGameWonFragment()
method from the GameFragmentDirections
class.
- Open the
GameFragment.kt
Kotlin file and locate the game-won conditional statement:
else {
// We've won! Navigate to the gameWonFragment.
view.findNavController()
.navigate(GameFragmentDirections
.actionGameFragmentToGameWonFragment())
}
- Pass the
numQuestions
andquestionIndex
parameters to theactionGameFragmentToGameWonFragment()
method:
// Adding the parameters to the Action
view.findNavController()
.navigate(GameFragmentDirections
.actionGameFragmentToGameWonFragment(numQuestions, questionIndex))
You pass the total number of questions as numQuestions
and the current question being attempted as questionIndex
. The app is designed in such a way that the user can only share their data if they answer all the questions correctly—the number of correct questions always equals the number of questions answered. (You can change this game logic later, if you want.)
- In
GameWonFragment.kt
, extract the arguments from the bundle, then use aToast
to display the arguments. Put the following code in theonCreateView()
method, before thereturn
statement:
val args = GameWonFragmentArgs.fromBundle(requireArguments())
Toast.makeText(context, "NumCorrect: ${args.numCorrect}, NumQuestions: ${args.numQuestions}", Toast.LENGTH_LONG).show()
- Run the app and play the game to make sure that the arguments are passed successfully to the
GameWonFragment
. The toast message appears on the Congratulations screen, saying "NumCorrect: 3, NumQuestions: 3".
You do have to win the trivia game first, though. To make the game easier, you can change it to a single-question game by setting the value of numQuestions
to 1
in the GameFragment.kt
Kotlin file.
Step 3: Replace Fragment classes with NavDirection classes
When you use "safe arguments," you can replace Fragment classes that are used in navigation code with NavDirection
classes. You do this so that you can use type-safe arguments with other fragments in the app.
In TitleFragment
, GameOverFragment
, and GameWonFragment
, change the action ID that's passed into the navigate()
method. Replace the action ID with the equivalent method from the appropriate NavDirection
class:
- Open the
TitleFragment.kt
Kotlin file. InonCreateView()
, locate thenavigate()
method in the Play button's click handler. PassTitleFragmentDirections.actionTitleFragmentToGameFragment()
as the method's argument:
binding.playButton.setOnClickListener { view: View ->
view.findNavController()
.navigate(TitleFragmentDirections.actionTitleFragmentToGameFragment())
}
- In the
GameOverFragment.kt
file, in the Try Again button's click handler, passGameOverFragmentDirections.actionGameOverFragmentToGameFragment()
as thenavigate()
method's argument:
binding.tryAgainButton.setOnClickListener { view: View ->
view.findNavController()
.navigate(GameOverFragmentDirections.actionGameOverFragmentToGameFragment())
}
- In the
GameWonFragment.kt
file, in the Next Match button's click handler, passGameWonFragmentDirections.actionGameWonFragmentToGameFragment()
as thenavigate()
method's argument:
binding.nextMatchButton.setOnClickListener { view: View ->
view.findNavController()
.navigate(GameWonFragmentDirections.actionGameWonFragmentToGameFragment())
}
- Run the app.
You won't find any changes to the app's output, but now the app is set up so that you can easily pass arguments using NavDirection
classes whenever needed.
5. Task: Sharing game results
In this task, you add a sharing feature to the app so that the user can share their game results. This is implemented by using an Android Intent, specifically an implicit intent. The sharing feature will be accessible via an options menu inside the GameWonFragment
class. In the app's UI, the menu item will appear as a share icon at the top of the Congratulations screen.
Implicit intents
Up until now, you've used navigation components to navigate among fragments within your Activity. Android also allows you to use intents to navigate to activities that other apps provide. You use this functionality in the AndroidTrivia app to let the user share their game-play results with friends.
An Intent
is a simple message object that's used to communicate between Android components. There are two types of intents: explicit and implicit. You can send a message to a specific target using an explicit intent. With an implicit intent, you initiate an Activity without knowing which app or Activity will handle the task. For example, if you want your app to take a photo, you typically don't care which app or Activity performs the task. When multiple Android apps can handle the same implicit intent, Android shows the user a chooser, so that the user can select an app to handle the request.
Each implicit intent must have an ACTION
that describes the type of thing that is to be done. Common actions, such as ACTION_VIEW
, ACTION_EDIT
, and ACTION_DIAL
, are defined in the Intent
class.
For more about implicit intents, see Sending the User to Another App.
Step 1: Add an options menu to the Congratulations screen
- Open the
GameWonFragment.kt
Kotlin file. - Inside the
onCreateView()
method, before thereturn
, call thesetHasOptionsMenu()
method and pass intrue
:
setHasOptionsMenu(true)
Step 2: Build and call an implicit intent
Modify your code to build and call an Intent
that sends the message about the user's game data. Because several different apps can handle an ACTION_SEND
intent, the user will see a chooser that lets them select how they want to send their information.
- Inside the
GameWonFragment
class, after theonCreateView()
method, create a private method calledgetShareIntent()
, as shown below. The line of code that sets a value forargs
is identical to the line of code used in the class'sonCreateView()
.
In the rest of the method's code, you build an ACTION_SEND
intent to deliver the message that the user wants to share. The data's MIME type is specified by the setType()
method. The actual data to be delivered is specified in the EXTRA_TEXT
. (The share_success_text
string is defined in the strings.xml
resource file.)
// Creating our Share Intent
private fun getShareIntent() : Intent {
val args = GameWonFragmentArgs.fromBundle(requireArguments())
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.setType("text/plain")
.putExtra(Intent.EXTRA_TEXT, getString(R.string.share_success_text, args.numCorrect, args.numQuestions))
return shareIntent
}
- Below the
getShareIntent()
method, create ashareSuccess()
method. This method gets theIntent
fromgetShareIntent()
and callsstartActivity()
to begin sharing.
// Starting an Activity with our new Intent
private fun shareSuccess() {
startActivity(getShareIntent())
}
- The starter code already contains a
winner_menu.xml
menu file. OverrideonCreateOptionsMenu()
in theGameWonFragment
class to inflatewinner_menu
.
Use getShareIntent()
to get the shareIntent
. To make sure the shareIntent
resolves to an Activity
, check with the Android package manager ( PackageManager
), which keeps track of the apps and activities installed on the device. Use the Activity's packageManager
property to gain access to the package manager, and call resolveActivity()
. If the result equals null
, which means that the shareIntent
doesn't resolve, find the sharing menu item from the inflated menu and make the menu item invisible.
// Showing the Share Menu Item Dynamically
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.winner_menu, menu)
if(getShareIntent().resolveActivity(requireActivity().packageManager)==null){
menu.findItem(R.id.share).isVisible = false
}
}
- To handle the menu item, override
onOptionsItemSelected()
in theGameWonFragment
class. Call theshareSuccess()
method when the menu item is clicked:
// Sharing from the Menu
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when(item.itemId){
R.id.share -> shareSuccess()
}
return super.onOptionsItemSelected(item)
}
- Now run your app. (You might need to import some packages into
GameWonFragment.kt
before the code will run.) After you win the game, notice the share icon that appears at the top right of the app bar. Click the share icon to share a message about your victory.
6. Solution code
Android Studio project: AndroidTrivia-Solution
7. Summary
Safe Args:
- To help catch errors caused by missing keys or mismatched types when you pass data from one Fragment to another, use a Gradle plugin called Safe Args.
- For each Fragment in your app, the Safe Args plugin generates a corresponding
NavDirection
class. You add theNavDirection
class to the Fragment code, then use the class to pass arguments between the Fragment and other fragments. - The
NavDirection
classes represent navigation from all the app's actions.
Implicit intents:
- An implicit intent declares an action that your app wants some other app (such as a camera app or email app) to perform on its behalf.
- If several Android apps could handle an implicit intent, Android shows the user a chooser. For example, when the user taps the share icon in the AndroidTrivia app, the user can select which app they want to use to share their game results.
- To build an intent, you declare an action to perform, for example
ACTION_SEND
. - Several
Intent()
constructors are available to help you build intents.
Sharing functionality:
- In the case of sharing your success with your friends, the
Intent
action would beIntent.ACTION_SEND.
- To add an options menu to a Fragment, set the
setHasOptionsMenu()
method totrue
in the Fragment code. - In the Fragment code, override the
onCreateOptionsMenu()
method to inflate the menu. - Override the
onOptionsItemSelected()
to usestartActivity()
to send theIntent
to other apps that can handle it.
When the user taps the menu item, the intent is fired, and the user sees a chooser for the SEND
action.
8. Learn more
Udacity course:
Android developer documentation:
9. Homework
This section lists possible homework assignments for students who are working through this codelab as part of a course led by an instructor. It's up to the instructor to do the following:
- Assign homework if required.
- Communicate to students how to submit homework assignments.
- Grade the homework assignments.
Instructors can use these suggestions as little or as much as they want, and should feel free to assign any other homework they feel is appropriate.
If you're working through this codelab on your own, feel free to use these homework assignments to test your knowledge.
Answer these questions
Question 1
If you pass arguments from Fragment A to Fragment B without using Safe Args to make sure your arguments are type-safe, which of the following problems can occur that might cause the app to crash when the app runs? Select all that apply.
- Fragment B requests data that Fragment A doesn't send to it.
- Fragment A might send data that Fragment B hasn't requested.
- Fragment A might send data that's a different type than Fragment B needs. For example, Fragment A might send a string but Fragment B requests an integer, resulting in a return value of zero.
- Fragment A uses different parameter names than Fragment B requests.
Question 2
What does the Safe Args Gradle plugin do? Select all that apply:
- Generates simple object and builder classes for type-safe access to arguments specified for destinations and actions.
- Creates
Navigation
classes that you can edit to simplify the passing of parameters between fragments. - Makes it so you don't need to use Android bundles in your code.
- Generates a method for each action that you've defined in the navigation graph.
- Prevents your code from using the wrong key when extracting data from a bundle.
Question 3
What's an implicit intent?
- A task that your app initiates in one Fragment in your app, and that's completed in another Fragment.
- A task that your app always completes by showing the user a chooser.
- A task that your app initiates without knowing which app or Activity will handle the task.
- An implicit intent is the same thing as the action that you set between destinations in the navigation graph.
10. Next codelab
For links to other codelabs in this course, see the Android Kotlin Fundamentals codelabs landing page.