Android Kotlin Fundamentals: 03.3 Start an external Activity

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 the GameFragment 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.

90c4d2581da9a673.png

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

  1. 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:

  1. Open the app in Android Studio.
  2. 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.
  3. Explore the app and play the game. When you win the game by answering three questions correctly, you see the Congratulations screen.

c8ae94827d6241e.png

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

  1. In Android Studio, open the project-level build.gradle file.
  2. 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"

}
  1. Open the app-level build.gradle file.
  2. At the top of the file, after all the other plugins, add the apply plugin statement with the androidx.navigation.safeargs plugin:
// Adding the apply plugin statement for safeargs
apply plugin: 'androidx.navigation.safeargs'
  1. 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).

  1. Open the GameFragment.kt Kotlin file that's in the java folder.
  2. Inside the onCreateView() method, locate the game-won conditional statement ("We've won!"). Change the parameter that's passed into the NavController.navigate() method: Replace the action ID for the game-won state with an ID that uses the actionGameFragmentToGameWonFragment() method from the GameFragmentDirections 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())
  1. 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

  1. 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.
  2. In the preview, select the gameWonFragment.
  3. In the Attributes pane, expand the Arguments section.
  4. 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. 608122a9dd799e75.png
  5. 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. 4aaae742feb9850e.png

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.

  1. 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())
}
  1. Pass the numQuestions and questionIndex parameters to the actionGameFragmentToGameWonFragment() 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.)

  1. In GameWonFragment.kt, extract the arguments from the bundle, then use a Toast to display the arguments. Put the following code in the onCreateView() method, before the return statement:
val args = GameWonFragmentArgs.fromBundle(requireArguments())
Toast.makeText(context, "NumCorrect: ${args.numCorrect}, NumQuestions: ${args.numQuestions}", Toast.LENGTH_LONG).show()
  1. 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:

  1. Open the TitleFragment.kt Kotlin file. In onCreateView(), locate the navigate() method in the Play button's click handler. Pass TitleFragmentDirections.actionTitleFragmentToGameFragment() as the method's argument:
binding.playButton.setOnClickListener { view: View ->
    view.findNavController()
            .navigate(TitleFragmentDirections.actionTitleFragmentToGameFragment())
}
  1. In the GameOverFragment.kt file, in the Try Again button's click handler, pass GameOverFragmentDirections.actionGameOverFragmentToGameFragment() as the navigate() method's argument:
binding.tryAgainButton.setOnClickListener { view: View ->
    view.findNavController()
        .navigate(GameOverFragmentDirections.actionGameOverFragmentToGameFragment())
}
  1. In the GameWonFragment.kt file, in the Next Match button's click handler, pass GameWonFragmentDirections.actionGameWonFragmentToGameFragment() as the navigate() method's argument:
binding.nextMatchButton.setOnClickListener { view: View ->
    view.findNavController()
          .navigate(GameWonFragmentDirections.actionGameWonFragmentToGameFragment())
}
  1. 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

  1. Open the GameWonFragment.kt Kotlin file.
  2. Inside the onCreateView() method, before the return, call the setHasOptionsMenu() method and pass in true:
  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.

  1. Inside the GameWonFragment class, after the onCreateView() method, create a private method called getShareIntent(), as shown below. The line of code that sets a value for args is identical to the line of code used in the class's onCreateView().

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
}
  1. Below the getShareIntent() method, create a shareSuccess() method. This method gets the Intent from getShareIntent() and calls startActivity() to begin sharing.
// Starting an Activity with our new Intent
private fun shareSuccess() {
   startActivity(getShareIntent())
}
  1. The starter code already contains a winner_menu.xml menu file. Override onCreateOptionsMenu() in the GameWonFragment class to inflate winner_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
       }
}
  1. To handle the menu item, override onOptionsItemSelected() in the GameWonFragment class. Call the shareSuccess() 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)
}
  1. 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.

f599422a76c68eb5.png 90c4d2581da9a673.png

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 the NavDirection 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 be Intent.ACTION_SEND.
  • To add an options menu to a Fragment, set the setHasOptionsMenu() method to true in the Fragment code.
  • In the Fragment code, override the onCreateOptionsMenu() method to inflate the menu.
  • Override the onOptionsItemSelected() to use startActivity() to send the Intent 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

Start the next lesson:

For links to other codelabs in this course, see the Android Kotlin Fundamentals codelabs landing page.