Passing Data Between Activities

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

Figure 6.8  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:

    questionBank[currentIndex].answer

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 6.9.

Figure 6.9  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 MainActivity.onSaveInstanceState(Bundle).

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.

Listing 6.8  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 6.8, 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 6.9  A newIntent(…) function for CheatActivity (CheatActivity.kt)

class CheatActivity : AppCompatActivity() {

    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 their launching intents.

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

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

cheatButton.setOnClickListener {
    // Start CheatActivity
    val intent = Intent(this, CheatActivity::class.java)
    val answerIsTrue = quizViewModel.currentQuestionAnswer
    val intent = CheatActivity.newIntent([email protected], answerIsTrue)
    startActivity(intent)
}

You only need one extra, 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 is the name of the extra. The second argument of getBooleanExtra(…) is a default answer 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 6.11  Using an extra (CheatActivity.kt)

class CheatActivity : AppCompatActivity() {

    private var answerIsTrue = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_cheat)

        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 6.12  Enabling cheating (CheatActivity.kt)

class CheatActivity : AppCompatActivity() {

    private lateinit var answerTextView: TextView
    private lateinit var showAnswerButton: Button

    private var answerIsTrue = false

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

        answerIsTrue = intent.getBooleanExtra(EXTRA_ANSWER_IS_TRUE, false)
        answerTextView = findViewById(R.id.answer_text_view)
        showAnswerButton = findViewById(R.id.show_answer_button)
        showAnswerButton.setOnClickListener {
            val answerText = when {
                answerIsTrue -> R.string.true_button
                else -> R.string.false_button
            }
            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 call the Activity.startActivityForResult(Intent, Int) function.

The first parameter is the same intent as before. The second parameter is the request code. The request code is a user-defined integer that is sent to the child activity and then received back by the parent. It is used when an activity starts more than one type of child activity and needs to know who is reporting back. MainActivity will only ever start one type of child activity, but using a constant for the request code is a best practice that will set you up well for future changes.

In MainActivity, modify cheatButton’s listener to call startActivityForResult(Intent, int).

Listing 6.13  Calling startActivityForResult(…) (MainActivity.kt)

private const val TAG = "MainActivity"
private const val KEY_INDEX = "index"
private const val REQUEST_CODE_CHEAT = 0

class MainActivity : AppCompatActivity() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        cheatButton.setOnClickListener {
            ...
            startActivity(intent)
            startActivityForResult(intent, REQUEST_CODE_CHEAT)
        }

        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 6.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?) {
        ...
        showAnswerButton.setOnClickListener {
            ...
            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 calls the following function on the parent activity:

    onActivityResult(requestCode: Int, resultCode: Int, data: Intent)

The parameters are the original request code from MainActivity and the result code and intent passed into setResult(Int, Intent).

Figure 6.10 shows this sequence of interactions.

Figure 6.10  Sequence diagram for GeoQuiz

Sequence diagram for GeoQuiz

The final step is to override onActivityResult(Int, Int, Intent) in MainActivity to handle the result.

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 instead of MainActivity means the value will persist across a configuration change rather than being destroyed with the activity, as discussed in Chapter 4.

Listing 6.15  Tracking cheating in QuizViewModel (QuizViewModel.kt)

class QuizViewModel : ViewModel() {

    var currentIndex = 0
    var isCheater = false
    ...
}

Next, in MainActivity.kt, override onActivityResult(…) to pull the value out of the result sent back from CheatActivity. Check the request code and result code to be sure they are what you expect. This, again, is a best practice to make future maintenance easier.

Listing 6.16  Implementing onActivityResult(…) (MainActivity.kt)

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

    override fun onActivityResult(requestCode: Int,
                                  resultCode: Int,
                                  data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        if (resultCode != Activity.RESULT_OK) {
            return
        }

        if (requestCode == REQUEST_CODE_CHEAT) {
            quizViewModel.isCheater =
                data?.getBooleanExtra(EXTRA_ANSWER_SHOWN, false) ?: false
        }
    }
    ...
}

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

Listing 6.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, Chapter 7, you will add some flair to GeoQuiz by including an activity transition animation when displaying CheatActivity. In doing so 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