Challenge: Preventing Repeat Answers: How to best save which question has been answered


#1

Hi everybody,

I’d like to see alternative solutions to save the questions’ “answered state” to find out what’s the best solution.

This is what I did:

I added another field with a getter and setter to Question.java:

private boolean mAlreadyAnswered = false;
public boolean isAlreadyAnswered() { return mAlreadyAnswered; }
public void setAlreadyAnswered(boolean alreadyAnswered) { mAlreadyAnswered = alreadyAnswered; }

Then I thought about how to preserve that alreadyAnswered value over the activities destruction during a device rotation. I thought about making Question serializable and put them into my bundel but instead I decided to save all the questions’ booleans in an extra array and store that one in the bundle.

@Override
protected void onSaveInstanceState(Bundle outState) {
    ...

    boolean[] questionsAnswered = new boolean[mQuestionBank.length];
    for (int i = 0; i < mQuestionBank.length; i++)
        questionsAnswered[i] = mQuestionBank[i].isAlreadyAnswered();
    outState.putBooleanArray(KEY_INDEX_QUESTIONS_ANSWERED, questionsAnswered);
}

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...

    if (savedInstanceState != null) {
        mCurrentIndex = savedInstanceState.getInt(KEY_INDEX_CURRENT_INDEX, 0);

        boolean[] questionsAnswered =
                savedInstanceState.getBooleanArray(KEY_INDEX_QUESTIONS_ANSWERED);
        assert questionsAnswered != null;
        for (int i = 0; i < questionsAnswered.length; i++)
            mQuestionBank[i].setAlreadyAnswered(questionsAnswered[i]);
    }

    ...
}

Does someone have concerns and suggestions for improvement for this solution or an alternative one?

Also, how did you handle the warning about getBooleanArray possibly yielding null (the line after which I wrote the assert statement)?


#2

I believe this was a very smart way to do things, the Getter and Setter within the Question class is brilliant.

I put them in a HashMap and when clicked, I added the true value to the map, then when the user answers the question the button are disabled/enabled accordingly.

private HashMap<Question, Boolean> mAnsweredQuestions = new HashMap<>();

mAnsweredQuestions.put(mQuestionBank[mCurrentIndex], true);

if (mAnsweredQuestions.containsKey(mQuestionBank[mCurrentIndex])) {
                mTrueButton.setEnabled(false);
                mFalseButton.setEnabled(false);
            } else {
                mTrueButton.setEnabled(true);
                mFalseButton.setEnabled(true);
            }

I am having issues with the screen rotating though. It seems the addresses that it points to in a memory are different so having the serializable isn’t the way I should do it.

mAnsweredQuestions = (HashMap<Question, Boolean>) savedInstanceState.getSerializable("mAnsweredQuestionsHashMap");

outState.putSerializable("mAnsweredQuestionsHashMap", mAnsweredQuestions);

I’ve found a solution using pieces from your solution.


#3

My solution is similar to the first one, but I stored the answers in an int[]. I also created a method to set the buttons enabled or disabled.

Added code to QuizActivity.java
private static final String QUESTION_LIST = “question_list”;

    if (savedInstanceState != null) {
        // Save Current Index of question
        mCurrentIndex = savedInstanceState.getInt(KEY_INDEX, 0);
        // Save whether question has been answered.  Do not let user answer again.
        int[] mQuestionAnswerArray = savedInstanceState.getIntArray(QUESTION_LIST);
        for (int i=0; i<mQuestionBank.length; i++)         {
            mQuestionBank[i].setAnswered(mQuestionAnswerArray[i]);
        }
    }



@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
    super.onSaveInstanceState(savedInstanceState);
    Log.i(TAG, "onSaveInstnaceState");
    savedInstanceState.putInt(KEY_INDEX, mCurrentIndex);
    int[] mQuestionAnswerArray = new int[mQuestionBank.length];
    for (int i=0; i<mQuestionBank.length; i++)         {
        mQuestionAnswerArray[i] = mQuestionBank[i].isAnswered();
    }
    savedInstanceState.putIntArray(QUESTION_LIST, mQuestionAnswerArray);
}


private void updateQuestion() {
    int question = mQuestionBank[mCurrentIndex].getTextResId();
    mQuestionTextView.setText(question);
    setButtons();
}

private void setButtons() {
    if (mQuestionBank[mCurrentIndex].isAnswered() > 0) {
        // make buttons disabled
        mTrueButton.setEnabled(false);
        mFalseButton.setEnabled(false);
    } else {
        mTrueButton.setEnabled(true);
        mFalseButton.setEnabled(true);
    }
}

private void checkAnswer(boolean userPressedTrue) {
    boolean answerIsTrue = mQuestionBank[mCurrentIndex].isAnswerTrue();

    int messageResId = 0;

    if (userPressedTrue == answerIsTrue) {
        mQuestionBank[mCurrentIndex].setAnswered(2);
        messageResId = R.string.correct_toast;
    } else {
        mQuestionBank[mCurrentIndex].setAnswered(1);
        messageResId = R.string.incorrect_toast;
    }
    setButtons();
    Toast.makeText(this, messageResId, Toast.LENGTH_SHORT).show();
}

Question.java
private int mAnswered;

public Question(int textResId, boolean answerTrue) {
    mTextResId = textResId;
    mAnswerTrue = answerTrue;
    mAnswered = 0;
}

public int isAnswered() { return mAnswered; }

public void setAnswered(int answered) { mAnswered = answered; }

*** I would probably change isAnswered to getAnswered. I try to only use is when returning boolean values.

To prevent the warning on getBooleanArray, wouldn’t you need to initialize the values to false first? I did that in my example by initializing the mAnswered value in the constructor as it wasn’t used by the end user, just a tracking answer mechanism.


#4

did anybody try with parcelable class?


#7

Hey guys,

This is an interesting problem to solve. There are many ways to do this. This is how I have done it, hopefully it is easy to follow:

First step - create a boolean array to keep track of which questions have been answered:
private boolean[] mQuestionsAnswered = new boolean[mQuestionBank.length];

Second step - in checkAnswer() method, set mQuestionsAnswered[mCurrentIndex] = true to specify that the question in that position has been answered. Also, at the same time, we have to immediately set the buttons’ enabled state to false:

private void checkAnswer(boolean userPressedTrue) {
    ...
    mQuestionsAnswered[mCurrentIndex] = true;
    mTrueButton.setEnabled(false);
    mFalseButton.setEnabled(false);
    ...
}

Third step - in updateQuestion() method, we toggle the buttons’ enabled state based on the question state:

private void updateQuestion() { 
    mTrueButton.setEnabled(!mQuestionsAnswered[mCurrentIndex]); 
    mFalseButton.setEnabled(!mQuestionsAnswered[mCurrentIndex]); 
    ... 
}

Fourth step - in onSaveInstanceState(Bundle outState) method, make sure you save the boolean array:

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    Log.d(TAG, "onSaveInstanceState() called");
    outState.putInt(QUESTION_INDEX_KEY, mCurrentIndex);
    outState.putBooleanArray(QUESTIONS_ANSWERED_KEY, mQuestionsAnswered);
}

Fifth step - restore the data inside the onCreate(Bundle savedInstanceState) method:

if (savedInstanceState != null) {
    mCurrentIndex = savedInstanceState.getInt(QUESTION_INDEX_KEY);
    mQuestionsAnswered = savedInstanceState.getBooleanArray(QUESTIONS_ANSWERED_KEY);
}

That should solve it :slight_smile:

If you have any questions or suggestion, please feel free to add to this post.

Thanks!


#8

Hi, guys!
I made it like this:

I added new method;

private void setButtonEnabled(boolean enabled) {
trueButton = (Button) findViewById(R.id.true_button);
falseButton = (Button) findViewById(R.id.false_button);
trueButton.setEnabled(enabled);
falseButton.setEnabled(enabled);
}

and called it in :
private void checkAnswer(boolean userPressedTrue) {

if (userPressedTrue == answerIsTrue) {

setButtonEnabled(false);
} else {

setButtonEnabled(false);
}

and:
private void updateQuestion() {

setButtonEnabled(true);
}

But I think here is something not perfect.


#9

Does not help. After turning the screen, the buttons are again active.


#10

It works for me. Could you please paste your code?

You need to make sure to preserve the mQuestionsAnswered array in onSaveInstanceState(Bundle outState) {...} and get it immediately at the beginning of onCreate(Bundle savedInstanceState)

Also, I call updateQuestion() in onCreate(Bundle savedInstanceState) so make sure you call this piece of code before it:
if (savedInstanceState != null) { mCurrentIndex = savedInstanceState.getInt(QUESTION_INDEX_KEY); mQuestionsAnswered = savedInstanceState.getBooleanArray(QUESTIONS_ANSWERED_KEY); }


#11

Great, simple solution :+1:
But, when rotate it is available again :slight_smile:


#12

Yes, I left it for other inquiring minds :slight_smile:


#13

ljubinkovicd’s answer is best.(I don’t know why someone thinks it doesn’t work). The OP’s solution can work too if other parts are correct, but it is confusing, and not very well.


#14

Work fine and at the same time solved my confusion about protected and outState.

Thanks ♫


#15

Your answer is very organized.
perfect!
Thank you for your answer.


#16

but you cannot have two onSaveInstanceState() metod.


#17

Your answer is very helpful to me.
Thanks!


#18

I declared a boolean variable questionHasBeenAnswered to False at the beginning of quizActivity.

checkAnswer() is modified to check questionHasBeenAnswered for false before doing anything and sets it to true when it is done.

upDateQuestion() sets questionHasBeenAnswered to true.

This works because checkAnswer() is called anytime that True or False is pressed and upDateQuestion() is called whenever Next is pressed.

Something gives me a feeling that this is not the OOP way to do it.


#19

This is the best solution, but one catch: boolean[] getBooleanArray (String key) may return null
and unexpectedly redefine your mQuestionsAnswered
So, I do check:

private static final String KEY_ANSWERED = "answered";
...
private boolean[] questionsAnswered = new boolean[questionBank.length];
...
@Override protected void onCreate(Bundle savedInstanceState) {
    ...
    if (savedInstanceState != null) {
        currentIndex = savedInstanceState.getInt(KEY_INDEX, 0);
        if (savedInstanceState.containsKey(KEY_ANSWERED)) {
            questionsAnswered = savedInstanceState.getBooleanArray(KEY_ANSWERED);
        }
    }
    ...
}

#20

Works great, thanks ljubinkovicd. Both mCurrentIndex and mQuestionAnswered[] data were saved correctly in bundle. But the true and false buttons defaulted back to enabled when screen rotated. @ ntristo the fix is to call the checkAnswer method after both true and false buttons are inflated. This will check mQuestionAnswered when screen is rotated and disable clickable button if the current question was answered.


#22

Thanks, nice algorithm, solve the issue…


#23

Hi Everyone, Thank you all for submitting your questions ans answers it really helped me out to come to a solution.

However i still encounter a problem after i complete the quiz and rotate my device. The app just crashes. I don’t know it has to do with the onSavedInstance Bundle but it’s the last method to be called before it dies.

private static final String KEY_SCORE = "score";
// Boolean array of answered indexes
private boolean[] mQuestionsAnswered = new boolean[mQuestionBank.length];
// score
private int mScore = 0;

@Override
protected void onCreate(Bundle savedInstanceState) {
    if (savedInstanceState != null) {
        mCurrentIndex = savedInstanceState.getInt(KEY_INDEX, 0);
        mQuestionsAnswered = savedInstanceState.getBooleanArray(KEY_QUESTIONS_ANSWERED);
        mScore = savedInstanceState.getInt(KEY_SCORE);
    }

@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
    super.onSaveInstanceState(savedInstanceState);
    Log.i(TAG, "onSavedInstanceState");
    //writes the value of mCurrentIndex to the bundle with KEY_INDEX = index
    savedInstanceState.putInt(KEY_INDEX, mCurrentIndex);
    savedInstanceState.putBooleanArray(KEY_QUESTIONS_ANSWERED, mQuestionsAnswered);
    savedInstanceState.putInt(KEY_SCORE, mScore);
}

 private void updateQuestion() {
    int question = mQuestionBank[mCurrentIndex].getTextResId();
    mQuestionTextView.setText(question);
    if(mQuestionsAnswered[mCurrentIndex] == true) {
        mTrueButton.setEnabled(false);
        mFalseButton.setEnabled(false);
    }
}
private void checkAnswer(boolean userPressedTrue) {
    mQuestionBank[mCurrentIndex].setAnswered(true);
    mQuestionsAnswered[mCurrentIndex] = mQuestionBank[mCurrentIndex].isAnswered();
    boolean answerIsTrue = mQuestionBank[mCurrentIndex].isAnswerTrue();
    int messageResId = 0;
    if (userPressedTrue == answerIsTrue) {
        messageResId = R.string.correct_toast;
        mScore++;
    }
    else {
        messageResId = R.string.incorrect_toast;
    }
    Toast answerToast = Toast.makeText(this, messageResId, Toast.LENGTH_SHORT);
    answerToast.show();

    /**
     * Final Score
     */
    for (boolean answered : mQuestionsAnswered)
    {
        if(!answered) return;
    }
    int scorePercent = Math.round(((float) mScore / (float) mQuestionBank.length) * 100);
    String finalScore = "You Scored " + scorePercent + "%";
    Toast scoreToast = Toast.makeText(this, finalScore, Toast.LENGTH_LONG);
    scoreToast.setGravity(Gravity.TOP, 0, 400);
    scoreToast.show();
}