Passing Data Between Activities

Now that you have a MainActivity and a CheatActivity, you can think about passing data between them. Figure 7.7 shows what data you will pass between the two activities.

Figure 7.7  The conversation between MainActivity and CheatActivity

The conversation between MainActivity and CheatActivity

The MainActivity will inform the CheatActivity of the answer to the current question when the CheatActivity is started.

When the user presses the Back button to return to the MainActivity, the CheatActivity will be destroyed. In its last gasp, it will send data to the MainActivity about whether the user cheated.

You will start with passing data from MainActivity to CheatActivity.

Using intent extras

To inform the CheatActivity of the answer to the current question, you will pass it the value of:

    quizViewModel.currentQuestionAnswer

You will send this value as an extra on the Intent that is passed into startActivity(Intent).

Extras are arbitrary data that the calling activity can include with an intent. You can think of them like constructor arguments, even though you cannot use a custom constructor with an activity subclass. (Android creates activity instances and is responsible for their lifecycle.) The OS forwards the intent to the recipient activity, which can then access the extras and retrieve the data, as shown in Figure 7.8.

Figure 7.8  Intent extras: communicating with other activities

Intent extras: communicating with other activities

An extra is structured as a key-value pair, like the one you used to save out the value of currentIndex in QuizViewModel.

To add an extra to an intent, you use Intent.putExtra(…). In particular, you will be calling putExtra(name: String, value: Boolean).

Intent.putExtra(…) comes in many flavors, but it always has two arguments. The first argument is always a String key, and the second argument is the value, whose type will vary. It returns the Intent itself, so you can chain multiple calls if you need to.

In CheatActivity.kt, add a key for the extra. (We have broken the new line of code to fit on the printed page. You can enter it on one line.)

Listing 7.7  Adding an extra constant (CheatActivity.kt)

private const val EXTRA_ANSWER_IS_TRUE =
        "com.bignerdranch.android.geoquiz.answer_is_true"

class CheatActivity : AppCompatActivity() {
    ...
}

An activity may be started from several different places, so you should define keys for extras on the activities that retrieve and use them. Using your package name as a qualifier for your extra, as shown in Listing 7.7, prevents name collisions with extras from other apps.

Now you could return to MainActivity and put the extra on the intent, but there is a better approach. There is no reason for MainActivity, or any other code in your app, to know the implementation details of what CheatActivity expects as extras on its Intent. Instead, you can encapsulate that work into a newIntent(…) function.

Create this function in CheatActivity now. Place the function inside a companion object.

Listing 7.8  A newIntent(…) function for CheatActivity (CheatActivity.kt)

class CheatActivity : AppCompatActivity() {

    private lateinit var binding: ActivityCheatBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
    }

    companion object {
        fun newIntent(packageContext: Context, answerIsTrue: Boolean): Intent {
            return Intent(packageContext, CheatActivity::class.java).apply {
                putExtra(EXTRA_ANSWER_IS_TRUE, answerIsTrue)
            }
        }
    }
}

This function allows you to create an Intent properly configured with the extras CheatActivity will need. The answerIsTrue argument, a Boolean, is put into the intent with a private name using the EXTRA_ANSWER_IS_TRUE constant. You will extract this value momentarily.

A companion object allows you to access functions without having an instance of a class, similar to static functions in Java. Using a newIntent(…) function inside a companion object like this for your activity subclasses will make it easy for other code to properly configure its launching intents.

Speaking of other code, use this new function in MainActivity’s cheat button listener now.

Listing 7.9  Launching CheatActivity with an extra (MainActivity.kt)

binding.cheatButton.setOnClickListener {
    // Start CheatActivity
    val intent = Intent(this, CheatActivity::class.java)
    val answerIsTrue = quizViewModel.currentQuestionAnswer
    val intent = CheatActivity.newIntent(this@MainActivity, answerIsTrue)
    startActivity(intent)
}

You only need one extra in this case, but you can put multiple extras on an Intent if you need to. If you do, add more arguments to your newIntent(…) function to stay consistent with the pattern.

To retrieve the value from the extra, you will use Intent.getBooleanExtra(String, Boolean).

The first argument of getBooleanExtra(…) is the name of the extra. The second argument is a default answer to use if the key is not found.

In CheatActivity, retrieve the value from the extra in onCreate(Bundle?) and store it in a member variable.

Listing 7.10  Using an extra (CheatActivity.kt)

class CheatActivity : AppCompatActivity() {

    private lateinit var binding: ActivityCheatBinding

    private var answerIsTrue = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityCheatBinding.inflate(layoutInflater)
        setContentView(binding.root)

        answerIsTrue = intent.getBooleanExtra(EXTRA_ANSWER_IS_TRUE, false)
    }
    ...
}

Note that Activity.getIntent() always returns the Intent that started the activity. This is what you sent when calling startActivity(Intent).

Finally, wire up the answer TextView and the SHOW ANSWER button to use the retrieved value.

Listing 7.11  Enabling cheating (CheatActivity.kt)

class CheatActivity : AppCompatActivity() {

    private lateinit var binding: ActivityCheatBinding

    private var answerIsTrue = false

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        answerIsTrue = intent.getBooleanExtra(EXTRA_ANSWER_IS_TRUE, false)

        binding.showAnswerButton.setOnClickListener {
            val answerText = when {
                answerIsTrue -> R.string.true_button
                else -> R.string.false_button
            }
            binding.answerTextView.setText(answerText)
        }
    }
    ...
}

This code is pretty straightforward. You set the TextView’s text using TextView.setText(Int). TextView.setText(…) has many variations, and here you use the one that accepts the resource ID of a string resource.

Run GeoQuiz. Press CHEAT! to get to CheatActivity. Then press SHOW ANSWER to reveal the answer to the current question.

Getting a result back from a child activity

At this point, the user can cheat with impunity. Let’s fix that by having the CheatActivity tell the MainActivity whether the user chose to view the answer.

When you want to hear back from the child activity, you register your MainActivity for an ActivityResult using the Activity Results API.

The Activity Results API is different from other APIs you have interacted with so far within the Activity class. Instead of overriding a lifecycle method, you will initialize a class property within your MainActivity using the registerForActivityResult() function. That function takes in two parameters: The first is a contract that defines the input and output of the Activity you are trying to start. And the second is a lambda in which you parse the output that is returned.

In MainActivity, initialize a property named cheatLauncher using registerForActivityResult().

Listing 7.12  Creating cheatLauncher (MainActivity.kt)

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    private val quizViewModel: QuizViewModel by viewModels()

    private val cheatLauncher = registerForActivityResult(
        ActivityResultContracts.StartActivityForResult()
    ) { result ->
        // Handle the result
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
    }
    ...
}

The contract you are using is ActivityResultContracts.StartActivityForResult. It is a basic contract that takes in an Intent as input and provides an ActivityResult as output. There are many other contracts you can use to accomplish other tasks (such as capturing video or requesting permissions). You can even define your own custom contract. In Chapter 16, you will use a different contract to allow the user to select a contact from their contacts list.

For now, you will do nothing with the result, but you will get back to this in a bit.

To use your cheatLauncher, call the launch(Intent) function, which takes in the Intent you already created.

Listing 7.13  Launching cheatLauncher (MainActivity.kt)

class MainActivity : AppCompatActivity() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        binding.cheatButton.setOnClickListener {
            // Start CheatActivity
            val answerIsTrue = quizViewModel.currentQuestionAnswer
            val intent = CheatActivity.newIntent(this@MainActivity, answerIsTrue)
            startActivity(intent)
            cheatLauncher.launch(intent)
        }

        updateQuestion()
    }
    ...
}

Setting a result

There are two functions you can call in the child activity to send data back to the parent:

    setResult(resultCode: Int)
    setResult(resultCode: Int, data: Intent)

Typically, the result code is one of two predefined constants: Activity.RESULT_OK or Activity.RESULT_CANCELED. (You can use another constant, RESULT_FIRST_USER, as an offset when defining your own result codes.)

Setting result codes is useful when the parent needs to take different action depending on how the child activity finished.

For example, if a child activity had an OK button and a Cancel button, the child activity would set a different result code depending on which button was pressed. Then the parent activity would take a different action depending on the result code.

Calling setResult(…) is not required of the child activity. If you do not need to distinguish between results or receive arbitrary data on an intent, then you can let the OS send a default result code. A result code is always returned to the parent if the child activity was started with startActivityForResult(…). If setResult(…) is not called, then when the user presses the Back button, the parent will receive Activity.RESULT_CANCELED.

Sending back an intent

In this implementation, you are interested in passing some specific data back to MainActivity. So you are going to create an Intent, put an extra on it, and then call Activity.setResult(Int, Intent) to get that data into MainActivity’s hands.

In CheatActivity, add a constant for the extra’s key and a private function that does this work. Then call this function in the SHOW ANSWER button’s listener.

Listing 7.14  Setting a result (CheatActivity.kt)

const val EXTRA_ANSWER_SHOWN = "com.bignerdranch.android.geoquiz.answer_shown"
private const val EXTRA_ANSWER_IS_TRUE =
        "com.bignerdranch.android.geoquiz.answer_is_true"

class CheatActivity : AppCompatActivity() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        binding.showAnswerButton.setOnClickListener {
            ...
            binding.answerTextView.setText(answerText)
            setAnswerShownResult(true)
        }
    }

    private fun setAnswerShownResult(isAnswerShown: Boolean) {
        val data = Intent().apply {
            putExtra(EXTRA_ANSWER_SHOWN, isAnswerShown)
        }
        setResult(Activity.RESULT_OK, data)
    }
    ...
}

When the user presses the SHOW ANSWER button, the CheatActivity packages up the result code and the intent in the call to setResult(Int, Intent).

Then, when the user presses the Back button to return to the MainActivity, the ActivityManager invokes the lambda defined within cheatLauncher on the parent activity. The parameters are the original request code from MainActivity and the result code and intent passed into setResult(Int, Intent).

Figure 7.9 shows this sequence of interactions.

Figure 7.9  Sequence diagram for GeoQuiz

Sequence diagram for GeoQuiz

The final step is to extract the data returned in the lambda of cheatLauncher in MainActivity.

Handling a result

In QuizViewModel.kt, add a new property to hold the value that CheatActivity is passing back. The user’s cheat status is part of the UI state. Stashing the value in QuizViewModel and using SavedStateHandle means the value will persist across configuration changes and process death rather than being destroyed with the activity, as discussed in Chapter 4.

Listing 7.15  Tracking cheating in QuizViewModel (QuizViewModel.kt)

...
const val IS_CHEATER_KEY = "IS_CHEATER_KEY"

class QuizViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
    ...
    private val questionBank = listOf(
        ...
    )

    var isCheater: Boolean
        get() = savedStateHandle.get(IS_CHEATER_KEY) ?: false
        set(value) = savedStateHandle.set(IS_CHEATER_KEY, value)
    ...
}

Next, in MainActivity.kt, add the following lines in the lambda of cheatLauncher to pull the value out of the result sent back from CheatActivity. You do not want to accidentally mark the user as a cheater, so check whether the result code is Activity.RESULT_OK first.

Listing 7.16  Pulling out the data in cheatLauncher (MainActivity.kt)

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    private val quizViewModel: QuizViewModel by viewModels()

    private val cheatLauncher = registerForActivityResult(
        ActivityResultContracts.StartActivityForResult()
    ) { result ->
        // Handle the result
        if (result.resultCode == Activity.RESULT_OK) {
            quizViewModel.isCheater =
                result.data?.getBooleanExtra(EXTRA_ANSWER_SHOWN, false) ?: false
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
    }
    ...
}

Finally, modify the checkAnswer(Boolean) function in MainActivity to check whether the user cheated and respond appropriately.

Listing 7.17  Changing toast message based on value of isCheater (MainActivity.kt)

class MainActivity : AppCompatActivity() {
    ...
    private fun checkAnswer(userAnswer: Boolean) {
        val correctAnswer: Boolean = quizViewModel.currentQuestionAnswer

        val messageResId = if (userAnswer == correctAnswer) {
            R.string.correct_toast
        } else {
            R.string.incorrect_toast
        }
        val messageResId = when {
            quizViewModel.isCheater -> R.string.judgment_toast
            userAnswer == correctAnswer -> R.string.correct_toast
            else -> R.string.incorrect_toast
        }
        Toast.makeText(this, messageResId, Toast.LENGTH_SHORT)
                .show()
    }
}

Run GeoQuiz. Press CHEAT!, then press SHOW ANSWER on the cheat screen. Once you cheat, press the Back button. Try answering the current question. You should see the judgment toast appear.

What happens if you go to the next question? Still a cheater. If you wish to relax your rules around cheating, try your hand at the challenge outlined in the section called Challenge: Tracking Cheat Status by Question.

At this point, GeoQuiz is feature complete. In the next chapter, you will learn how to include the newest Android features available while still supporting older versions of Android in the same application.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset