Wiring Up the User Interface

First, create a list of Question objects in MainActivity, along with an index for the list.

Listing 2.5  Adding a Question list (MainActivity.kt)

class MainActivity : AppCompatActivity() {

    private lateinit var trueButton: Button
    private lateinit var falseButton: Button

    private val questionBank = listOf(
            Question(R.string.question_australia, true),
            Question(R.string.question_oceans, true),
            Question(R.string.question_mideast, false),
            Question(R.string.question_africa, false),
            Question(R.string.question_americas, true),
            Question(R.string.question_asia, true))

    private var currentIndex = 0
    ...
}

Here you call the Question constructor several times and create a list of Question objects.

(In a more complex project, this list would be created and stored elsewhere. In later apps, you will see better options for storing model data. For now, you are keeping it simple and just creating the list within MainActivity.)

You are going to use questionBank, currentIndex, and the properties in Question to get a parade of questions onscreen.

In the previous chapter, there was not much happening in GeoQuiz’s MainActivity. It displayed the layout defined in activity_main.xml. Using Activity.findViewById(id: Int), it got references to two buttons. Then, it set listeners on the buttons and wired them to make toasts.

Now that you have multiple questions to retrieve and display, MainActivity will have to work harder to respond to user input and update the UI. You could continue to use Activity.findViewById(…) and obtain references to your new views, but that is boring code that you would probably prefer not to write yourself.

Thankfully, there is View Binding, a feature of the build process that generates that boilerplate code for you and allows you to safely and easily interact with your UI elements. You will use View Binding to write less code and manage the complexity of even this relatively simple app.

Much like the R class (which you read about in Chapter 1), View Binding works by generating code during the build process for your app. However, View Binding is not enabled by default, so you must enable it yourself.

In the project tool window, under Gradle Scripts, locate and open the build.gradle file labeled (Module: GeoQuiz.app). (This file is actually located within the app module, but the Android view collects your project’s Gradle files to make them easier to find.)

Listing 2.6  Enabling View Binding (app/build.gradle)

plugins {
  id 'com.android.application'
  id 'kotlin-android'
}

android {
  ...
  kotlinOptions {
    jvmTarget = '1.8'
  }
  buildFeatures {
    viewBinding true
  }
}
...

After making this change, a banner will appear at the top of the file prompting you to sync the file (Figure 2.5).

Figure 2.5  Gradle sync prompt

Gradle sync prompt

Whenever you make changes in a .gradle file, you must sync the changes so the build process for your app is up to date. Click Sync Now in the banner or select FileSync Project with Gradle Files.

Now that View Binding is enabled, open MainActivity.kt and start using this build feature. Make the changes in Listing 2.7, and we will explain them afterward.

Listing 2.7  Initializing ActivityMainBinding (MainActivity.kt)

  package com.bignerdranch.android.geoquiz

  import android.os.Bundle
  import android.view.View
  ...
  import com.bignerdranch.android.geoquiz.databinding.ActivityMainBinding

  class MainActivity : AppCompatActivity() {

      private lateinit var binding: ActivityMainBinding

      private lateinit var trueButton: Button
      private lateinit var falseButton: Button
      ...
      override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        ...
      }
      ...
  }

(Like the R class, View Binding generates code within your package structure, which is why the import statement includes your package name. If you gave your package a name other than com.bignerdranch.android.geoquiz, your import statement will look different.)

View Binding does require a little bit of setup within your MainActivity, so let’s break down what is happening here.

Much like Activity.findViewById(…) allows you to get references to your individual UI elements, ActivityMainBinding allows you to get references to each UI element in your activity_main.xml layout. (View Binding generates classes based on the layout file’s name; so, for example, it would generate an ActivityCheatBinding for a layout named activity_cheat.xml.)

In Chapter 1, you passed R.layout.activity_main into Activity.setContentView(layoutResID: Int) to display your UI. That function performed two actions: First, it inflated your activity_main.xml layout; then, it put the UI onscreen. Here, when you initialize binding you are obtaining a reference to the layout and inflating it in the same line.

You pass layoutInflater, a property inherited from the Activity class, into the ActivityMainBinding.inflate() call. As its name implies, layoutInflater is responsible for inflating your XML layouts into UI elements.

When you previously called Activity.setContentView(layoutResID: Int), your MainActivity internally used its layoutInflater to display your UI. Now, using a different implementation of setContentView(), you pass a reference to the root UI element in your layout to display your UI.

Now that View Binding is set up within MainActivity, use it to wire up your UI. Start with the TRUE and FALSE buttons:

Listing 2.8  Using ActivityMainBinding (MainActivity.kt)

  class MainActivity : AppCompatActivity() {

      private lateinit var binding: ActivityMainBinding

      private lateinit var trueButton: Button
      private lateinit var falseButton: Button
      ...
      override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        trueButton = findViewById(R.id.true_button)
        falseButton = findViewById(R.id.false_button)

        trueButton.setOnClickListener { view: View ->
        binding.trueButton.setOnClickListener { view: View ->
            ...
        }
        falseButton.setOnClickListener { view: View ->
        binding.falseButton.setOnClickListener { view: View ->
            ...
        }
      }
      ...
  }

For each view with the android:id attribute defined in its layout XML, View Binding will generate a property on a corresponding ViewBinding class. Even better, View Binding automatically declares the type of the property to match the type of the view in your XML. So, for example, binding.trueButton is of type Button, because the view with the ID true_button is a <Button>.

Unlike a findViewById(…) call, this has the benefit of keeping your XML layouts and activities in sync if you change the kinds of views in your UI.

Next, using questionBank and currentIndex, retrieve the resource ID for the question text of the current question. Use the binding property to set the text for the question’s TextView.

Listing 2.9  Wiring up the TextView (MainActivity.kt)

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    binding.falseButton.setOnClickListener { view: View ->
        ...
    }

    val questionTextResId = questionBank[currentIndex].textResId
    binding.questionTextView.setText(questionTextResId)
}

Save your files and check for any errors. Then run GeoQuiz. You should see the first question in the array appear in the TextView, as before.

Now, make the NEXT button functional by setting a View.OnClickListener on it. This listener will increment the index and update the TextView’s text.

Listing 2.10  Wiring up the new button (MainActivity.kt)

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    binding.falseButton.setOnClickListener { view: View ->
        ...
    }

    binding.nextButton.setOnClickListener {
        currentIndex = (currentIndex + 1) % questionBank.size
        val questionTextResId = questionBank[currentIndex].textResId
        binding.questionTextView.setText(questionTextResId)

    }

    val questionTextResId = questionBank[currentIndex].textResId
    binding.questionTextView.setText(questionTextResId)
}

You now have the same code in two places that updates the text displayed in binding.questionTextView. Take a moment to put this code into a function instead, as shown in Listing 2.11. Then invoke that function in the nextButton’s listener and at the end of onCreate(Bundle?) to initially set the text in the activity’s view.

Listing 2.11  Encapsulating with a function (MainActivity.kt)

class MainActivity : AppCompatActivity() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        binding.nextButton.setOnClickListener {
            currentIndex = (currentIndex + 1) % questionBank.size
            val questionTextResId = questionBank[currentIndex].textResId
            binding.questionTextView.setText(questionTextResId)
            updateQuestion()
        }

        val questionTextResId = questionBank[currentIndex].textResId
        binding.questionTextView.setText(questionTextResId)
        updateQuestion()
    }

    private fun updateQuestion() {
        val questionTextResId = questionBank[currentIndex].textResId
        binding.questionTextView.setText(questionTextResId)
    }
}

Run GeoQuiz and test your NEXT button.

Now that you have the questions behaving appropriately, it is time to turn to the answers. At the moment, GeoQuiz thinks that the answer to every question is true. Let’s rectify that. You will add a private named function to MainActivity to encapsulate code rather than writing similar code in two places:

    private fun checkAnswer(userAnswer: Boolean)

This function will accept a Boolean variable that identifies whether the user pressed TRUE or FALSE. Then, it will check the user’s answer against the answer in the current Question object. Finally, after determining whether the user answered correctly, it will make a Toast that displays the appropriate message to the user.

In MainActivity.kt, add the implementation of checkAnswer(Boolean) shown in Listing 2.12.

Listing 2.12  Adding checkAnswer(Boolean) (MainActivity.kt)

class MainActivity : AppCompatActivity() {
    ...
    private fun updateQuestion() {
        ...
    }

    private fun checkAnswer(userAnswer: Boolean) {
        val correctAnswer = questionBank[currentIndex].answer

        val messageResId = if (userAnswer == correctAnswer) {
            R.string.correct_toast
        } else {
            R.string.incorrect_toast
        }

        Toast.makeText(this, messageResId, Toast.LENGTH_SHORT)
                .show()
    }
}

Within the buttons’ listeners, call checkAnswer(Boolean), as shown in Listing 2.13.

Listing 2.13  Calling checkAnswer(Boolean) (MainActivity.kt)

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    binding.trueButton.setOnClickListener { view: View ->
        Toast.makeText(
            this,
            R.string.correct_toast,
            Toast.LENGTH_SHORT
        )
            .show()
        checkAnswer(true)
    }

    binding.falseButton.setOnClickListener { view: View ->
        Toast.makeText(
            this,
            R.string.correct_toast,
            Toast.LENGTH_SHORT
        )
            .show()
        checkAnswer(false)
    }
    ...
}

Run GeoQuiz. Verify that the toasts display the right message based on the answer to the current question and the button you press.

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

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