Android and Model-View-Controller

In this chapter, you are going to upgrade GeoQuiz to present more than one question

To make this happen, you are going to add a class named Question to the GeoQuiz project. An instance of this class will encapsulate a single true-false question.

Then, you will create an array of Question objects for QuizActivity to manage.

Creating a New Class

In the project tool window, right-click the com.bignerdranch.android.geoquiz package and select New → Java Class. Name the class Question and click OK

public class Question {
  private int mTextResId;
  private boolean mAnswerTrue;
  public Question(int textResId, boolean answerTrue) {
     mTextResId = textResId;
     mAnswerTrue = answerTrue;
  }
}

The Question class holds two pieces of data: the question text and the question answer (true or false).

These variables need getter and setter methods. Rather than typing them in yourself, you can have Android Studio generate the implementations for you.

Generating getters and setters

public class Question {
  private int mTextResId;
  private boolean mAnswerTrue;
  ...
  public int getTextResId() {
 return mTextResId;
  }
  public void setTextResId(int textResId) {
 mTextResId = textResId;
  }
  public boolean isAnswerTrue() {
 return mAnswerTrue;
  }
  public void setAnswerTrue(boolean answerTrue) {
 mAnswerTrue = answerTrue;
  }
}

Object diagram for GeoQuiz

Model-View-Controller and Android

  • A model object holds the application’s data and “business logic.”

  • View objects know how to draw themselves on the screen and how to respond to user input, like touches. A simple rule of thumb is that if you can see it on screen, then it is a view.

  • Controller objects tie the view and model objects together. They contain “application logic.” Controllers are designed to respond to various events triggered by view objects and to manage the flow of data to and from model objects and the view layer.

MVC flow with user input

Updating the View Layer

Now that you have been introduced to MVC, you are going to update GeoQuiz’s view layer to include a NEXT button.

So the changes you need to make to the view layer are:

  • Remove the android:text attribute from the TextView. You no longer want a hardcoded question to be part of its definition.

  • Give the TextView an android:id attribute. This widget will need a resource ID so that you can set its text in QuizActivity’s code.

  • Add the new Button widget as a child of the root LinearLayout.

New button... and changes to the text view

<LinearLayout ... >
  <TextView
  android:id="@+id/question_text_view"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:padding="24dp" />
 <LinearLayout ... >
 ...
 </LinearLayout>
  <Button
  android:id="@+id/next_button"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="@string/next_button" />
</LinearLayout>

Return to res/values/strings.xml. Rename question_text and add a string for the new button.

<string name="app_name">GeoQuiz</string>
<string name="question_australia">Canberra is the capital of Australia.</string>
<string name="true_button">True</string>
<string name="false_button">False</string>
<string name="next_button">Next</string>
<string name="correct_toast">Correct!</string>

While you have strings.xml open, go ahead and add the strings for the rest of the geography questions that will be shown to the user.

<string name="question_australia">Canberra is the capital of Australia.</string>
<string name="question_oceans">The Pacific Ocean is larger than
        the Atlantic Ocean.</string>
<string name="question_mideast">The Suez Canal connects the Red Sea
        and the Indian Ocean.</string>
<string name="question_africa">The source of the Nile River is in Egypt.</string>
<string name="question_americas">The Amazon River is the longest river
        in the Americas.</string>
<string name="question_asia">Lake Baikal is the world\'s oldest and deepest
        freshwater lake.</string>
        ...

Notice that you use the escape sequence \' in the last value to get an apostrophe in your string.

Updating the Controller Layer

Open QuizActivity.java. Add variables for the TextView and the new Button. Also, create an array of Question objects and an index for the array.

Adding variables and a Question array

    private Button mTrueButton;
    private Button mFalseButton;
    private Button mNextButton;
    private TextView mQuestionTextView;
    private Question[] mQuestionBank = new Question[] {
            new Question(R.string.question_australia, true),
            new Question(R.string.question_oceans, true),
            new Question(R.string.question_mideast, false),
            new Question(R.string.question_africa, false),
            new Question(R.string.question_americas, true),
            new Question(R.string.question_asia, true),
    };
    private int mCurrentIndex = 0;
 ...

Wiring up the TextView

public class QuizActivity extends AppCompatActivity {
 ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_quiz);
        mQuestionTextView = (TextView) findViewById(R.id.question_text_view);
        int question = mQuestionBank[mCurrentIndex].getTextResId();
        mQuestionTextView.setText(question);
        mTrueButton = (Button) findViewById(R.id.true_button);
 ...
    }
}

Wiring up the new button

public class QuizActivity extends AppCompatActivity {
 ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
 ...
        mFalseButton.setOnClickListener(new View.OnClickListener() {
 ...
        }
    });
    mNextButton = (Button) findViewById(R.id.next_button);
 mNextButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            mCurrentIndex = (mCurrentIndex + 1) % mQuestionBank.length;
            int question = mQuestionBank[mCurrentIndex].getTextResId();
            mQuestionTextView.setText(question);
        }
    });
}
}

Encapsulating with updateQuestion()

public class QuizActivity extends AppCompatActivity {
 ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
...
        mQuestionTextView = (TextView) findViewById(R.id.question_text_view);
        int question = mQuestionBank[mCurrentIndex].getTextResId();
        mQuestionTextView.setText(question);
...
        mNextButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mCurrentIndex = (mCurrentIndex + 1) % mQuestionBank.length;
                int question = mQuestionBank[mCurrentIndex].getTextResId();
                mQuestionTextView.setText(question);
                updateQuestion();
            }
        });
        updateQuestion();
    }
    private void updateQuestion() {
        int question = mQuestionBank[mCurrentIndex].getTextResId();
        mQuestionTextView.setText(question);
    }
}

Run GeoQuiz and test your new 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. Here again, you will implement a private method to encapsulate code rather than writing similar code in two places.

The method that you are going to add to QuizActivity is:

private void checkAnswer(boolean userPressedTrue)

Adding checkAnswer(boolean)

public class QuizActivity extends AppCompatActivity {
 ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
 ...
    }
    private void updateQuestion() {
        int question = mQuestionBank[mCurrentIndex].getTextResId();
        mQuestionTextView.setText(question);
    }
    private void checkAnswer(boolean userPressedTrue) {
        boolean answerIsTrue = mQuestionBank[mCurrentIndex].isAnswerTrue();
        int messageResId = 0;
        if (userPressedTrue == answerIsTrue) {
            messageResId = R.string.correct_toast;
        } else {
            messageResId = R.string.incorrect_toast;
        }
        Toast.makeText(this, messageResId, Toast.LENGTH_SHORT)
                .show();
    }
}

Calling checkAnswer(boolean)

public class QuizActivity extends AppCompatActivity {
 ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
...
        mTrueButton = (Button) findViewById(R.id.true_button);
        mTrueButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                checkAnswer(true);
            }
        });
        mFalseButton = (Button) findViewById(R.id.false_button);
        mFalseButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                checkAnswer(false);
            }
        });
...
    }
 ...
}

Challenge

Adding an Icon

Add Next and Previous Icons instead of text, some information you will need to know are:

The suffixes on these directory names refer to the screen pixel density of a device:

Suffixes

Description

mdpi

medium-density screens (~160dpi)

hdpi

high-density screens (~240dpi)

xhdpi

extra-high-density screens (~320dpi)

xxhdpi

extra-extra-high-density screens (~480dpi)

Adding an icon to the NEXT button

<LinearLayout ... >
 ...
 <LinearLayout ... >
...
 </LinearLayout>
 <Button
android:id="@+id/next_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/next_button"
android:drawableRight="@drawable/arrow_right"
android:drawablePadding="4dp" />
</LinearLayout>

Last updated

Was this helpful?