Question

I'm working on a quiz Game for Android. I'm trying to generate random quiz questions that haven't been picked yet.

My Main class (did not fit in here, sorry).

You can find the do-while in public void GenerateQuiz() which causes the crash. The app works fine without the do-while function, so something must be wrong with it. It just keeps crashing the app on the 6./7./8. random question but it works at least. What it does is just check if that random question has been asked already.

If yes -> Generates a new random question until it's a new one & hasn't been asked before.

If no -> That's gonna be the next question.

Snippet:

public void GenerateQuiz() {
    do{
        QuizID = ShuffleQuiz();
    }while(CheckIfQuestionIsNew(QuizID)==false);

There are 3 difficulties: Easy, Medium and Hard. Each of them has only 10 questions = TOTAL OF 30 QUESTIONS. Questions are being generated by random generated 1-10 INTs. Once the user completes 10 questions, the app will change the difficulty to the next highest. Example: If Question No 10 (Easy) has been answered correct, it will change the difficulty to MEDIUM. Once you finished the 10th question of MEDIUM it will change to HARD.

UPDATED LogCat Error Message once it crashes:

http://pastebin.com/fPiNrCEr

05-12 16:45:00.232 14067-14067/? E/ClockAlarmWidget﹕ [AlarmWidgetIdManager] getListItem() : itemIndex=0, widgetID:1 05-12 16:45:00.232 14067-14067/? E/ClockAlarmWidget﹕ [AlarmWidgetIdManager] getListItem() : ItemIndex exceed ListItemCount. itemIndex=1 05-12 16:45:00.232 14067-14067/? E/ClockAlarmWidget﹕ [AlarmWidgetIdManager] getListItem() : itemIndex=1, widgetID:1

Source:

 boolean CheckIfQuestionIsNew(int element) {
        List<Integer> ListDifficulty = new ArrayList<Integer>();

        //#########GET ARRAYLIST#########
        //Determine the Difficulty since each Difficulty got it's own arraylist.
        if (QuizDifficulty==1){//Example: If Difficulty==1, copy it's arrays to the new list of array.
            ListDifficulty.addAll(QuizIDsPassedD1);
        }else if (QuizDifficulty==2){
            ListDifficulty.addAll(QuizIDsPassedD2);
        }else if (QuizDifficulty==3){
            ListDifficulty.addAll(QuizIDsPassedD3);
        }

        if (ListDifficulty.contains(element))
            return false;

        //#########UPDATE ARRAYLIST#########
        // If Question was not asked before then --> Add the new question ID to the arraylist
        ListDifficulty.add(element);


        //#########SAVE NEW ARRAYLIST#########
        //Now it needs to determine the difficulty aggain to update its arraylist with the new items.
        if (QuizDifficulty==1){
            QuizIDsPassedD1.removeAll((QuizIDsPassedD1));//Remove All (Double Make Sure)
            QuizIDsPassedD1.addAll(ListDifficulty);//Transfer new Arraylist to the difficultyies array list
        }else if (QuizDifficulty==2){
            QuizIDsPassedD2.removeAll((QuizIDsPassedD2));
            QuizIDsPassedD2.addAll(ListDifficulty);
        }else if (QuizDifficulty==3){
            QuizIDsPassedD3.removeAll((QuizIDsPassedD3));
            QuizIDsPassedD3.addAll(ListDifficulty);
        }
        return true;
    }
Was it helpful?

Solution

The problem is an OutOfMemoryException:

java.lang.OutOfMemoryError at java.util.ArrayList.addAll(ArrayList.java:194) at 
de.hackert.wwequiz2014.QuizScreen.CheckIfQuestionIsNew(QuizScreen.java:49) at 

It is caused by adding items to an ArrayList inside your CheckIfQuestionIsNew() method. Most likely there is an infinite loop somewhere in there as you have to add A LOT of items - I'm talking thousands to millions - to a ListView to get an OutOfMemoryException. I gonna look through your code but I think you might identify the exact error faster as you are familiar with the code.

EDIT:

I think I found the error.

What exactly are you trying to do here:

ListDifficulty = QuizIDsPassedD1;
ListDifficulty.addAll(QuizIDsPassedD1);

Doesn't make much sense to me. This will duplicate all items in the list over and over again combined with the do/while loop this is most likely the culprit.

EDIT:

I commented your code from CheckIfQuestionIsNew():

boolean CheckIfQuestionIsNew(int element) {
    List<Integer> ListDifficulty = new ArrayList<Integer>();

    // What is this line suppsed to do? You are creating a new ArrayList, it is already empty why would you want to remove somthing here?
    ListDifficulty.removeAll(ListDifficulty);

    if (QuizDifficulty==1){
        // Now you are adding items to the new list for reasons I don't understand
        ListDifficulty.addAll(QuizIDsPassedD1);
    }else if (QuizDifficulty==2){
        // Same here
        ListDifficulty.addAll(QuizIDsPassedD2);
    }else if (QuizDifficulty==3){
        // Same here
        ListDifficulty.addAll(QuizIDsPassedD3);
    }

    if (ListDifficulty.contains(element))
        return false;

    // Where does this code belong? It is not part of the if statement above
    // I added empty lines around it to make it more clear that this is a statement which is not contained in any if statement
    ListDifficulty.add(element);


    if (QuizDifficulty==1){
        // What are you doing here? You remove all the items from the list and than add the List from above?
        // This code does absolutely nothing and makes the ifs at the top and the ifs right here completely useless
        QuizIDsPassedD1.removeAll((QuizIDsPassedD1));
        QuizIDsPassedD1.addAll(ListDifficulty);
    }else if (QuizDifficulty==2){
        // Same here
        QuizIDsPassedD2.removeAll((QuizIDsPassedD2));
        QuizIDsPassedD2.addAll(ListDifficulty);
    }else if (QuizDifficulty==3){
        // Same here
        QuizIDsPassedD3.removeAll((QuizIDsPassedD3));
        QuizIDsPassedD3.addAll(ListDifficulty);
    }
    return true;
}

I guess my main confusion comes from this:

You call ShuffleQuiz(); in a do/while loop, but for what purpose? What are trying to achieve here? If you just want a new question or questions which has/have not been answered before why don't you implement ShuffleQuiz() to return the new question directly instead of resorting to this - I guess - random picking and then check if it's ok in a loop?

EDIT:

Ok I have improved your code. Your old code was over 770 lines of code, my improved version is just below 210 lines of code. First I have created to two classes, Question and Answer. They respectively hold all the data associated with one question and with one answer. The Question class looks like this:

public class Question {

    private final int imageResId;
    private final int questionTextResId;
    private final List<Answer> answers = new ArrayList<Answer>();

    public Question(int questionTextResId, int imageResId) {
        this.imageResId = imageResId;
        this.questionTextResId = questionTextResId;
    }

    public Question addAnswer(int answerTextResId, boolean correct) {
        Answer answer = new Answer(answerTextResId, correct);
        this.answers.add(answer);
        return this;
    }

    public int getQuestionTextResId() {
        return questionTextResId;
    }

    public int getImageResId() {
        return imageResId;
    }

    public List<Answer> getAnswers() {
        return answers;
    }
}

As you can see nothing special. It has as member variables the resource id of the question text, the resource id of the image and a List of Answers. Additionally I have added an addAnswer() method to conveniently add answers to a question.

The Answer class looks like this:

public class Answer {

    private final int answerTextResId;
    private final boolean correct;

    public Answer(int answerTextResId, boolean correct) {
        this.answerTextResId = answerTextResId;
        this.correct = correct;
    }

    public int getAnswerTextResId() {
        return answerTextResId;
    }

    public boolean isCorrect() {
        return correct;
    }
}

As you can see nothing special again, but here are just two member variables, one is the resource id of the answer text and the other one is a boolean if this answer is correct or not.

Before showing you the complete code of my improved QuizScreen Activity I will explain to you all the changes I made and how it works. First I have created member variables for all the Views you use. You should not call findViewById() that often. When you save the reference in a member variable you never have to call findViewById() again:

private Button buttonAntwort1;
private Button buttonAntwort2;
private Button buttonAntwort3;
private Button buttonAntwort4;
private TextView textViewFrage;
private ImageView imageViewBild;

private Button[] answerButtons;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_quiz_screen);

    this.imageViewBild = (ImageView) findViewById(R.id.imageViewBild);
    this.textViewFrage = (TextView) findViewById(R.id.textViewFrage);
    this.buttonAntwort1 = (Button) findViewById(R.id.buttonAntwort1);
    this.buttonAntwort2 = (Button) findViewById(R.id.buttonAntwort2);
    this.buttonAntwort3 = (Button) findViewById(R.id.buttonAntwort3);
    this.buttonAntwort4 = (Button) findViewById(R.id.buttonAntwort4);

    this.answerButtons = new Button[] { this.buttonAntwort1, this.buttonAntwort2, this.buttonAntwort3, this.buttonAntwort4 };

    createQuestions();
    startGame();
}

As you can see I have also created a Button[] which contains all the Buttons which can be used to answer questions. My solution is completely flexible in that regards. You can have Questions with as many Answers as you want and every Question can have a different amount of Answers. You just have to make sure that there are enough Buttons in the Button[]. If you want to have Question with more than 4 answers simply add as many Buttons as you need to this Button[] and then just add the Questions as explained further down. The rest is completely automatic and you don't have to worry about having too many Buttons. All Buttons that are not needed/used by a Question are automatically hidden by making them invisible.

I have created 3 Lists, each of them contains all the questions of one difficulty level, and one additional List which will be a question queue. When the game is actually running we go through the question queue one after another. The question queue is generated once at the beginning of the game, so we don't have to pick questions at random all the time and check if they were answered.

private final List<Question> easyQuestions = new ArrayList<Question>();
private final List<Question> mediumQuesitons = new ArrayList<Question>();
private final List<Question> hardQuestions = new ArrayList<Question>();

private final List<Question> questionQueue = new ArrayList<Question>();

I also added 3 convenience methods to make it easy to fill those Lists with Questions:

private Question newEasy(int questionTextResId, int imageResId) {
    Question question = new Question(questionTextResId, imageResId);
    this.easyQuestions.add(question);
    return question;
}

private Question newMedium(int questionTextResId, int imageResId) {
    Question question = new Question(questionTextResId, imageResId);
    this.mediumQuesitons.add(question);
    return question;
}

private Question newHard(int questionTextResId, int imageResId) {
    Question question = new Question(questionTextResId, imageResId);
    this.hardQuestions.add(question);
    return question;
}

In the method createQuestions() all the Questions will be created using these 3 convenience methods, as I already said I didn't copy the Questions you had in your source code, you are going to have to add them again here:

private void createQuestions() {
    newEasy(R.string.question1_text, R.drawable.question1_picture1)
            .addAnswer(R.string.question1_answer1, false).addAnswer(R.string.question1_answer2, true)
            .addAnswer(R.string.question1_answer3, false).addAnswer(R.string.question1_answer4, false);


    newMedium(R.string.question2_text, R.drawable.question2_picture1)
            .addAnswer(R.string.question2_answer1, false).addAnswer(R.string.question2_answer2, true)
            .addAnswer(R.string.question2_answer3, false).addAnswer(R.string.question2_answer4, false);


    newHard(R.string.question3_text, R.drawable.question3_picture1)
            .addAnswer(R.string.question3_answer1, false).addAnswer(R.string.question3_answer2, true)
            .addAnswer(R.string.question3_answer3, false).addAnswer(R.string.question3_answer4, false);

}

As you can see you just call newEasy() if you want to add an easy Question, newMedium() if you want to add a medium Question or newHard() if you want to add a hard Question. You can simply daisy chain calls to addAnswer() to add as many Answers as you want to the Question. This should all be pretty self explanatory.

After all the Questions are created startGame() will be called. You can restart the game at any point just by calling startGame() so if you want to add the feature to restart the game you can do so quite simply. The startGame() method looks like this:

private void startGame() {
    Collections.shuffle(this.easyQuestions);
    Collections.shuffle(this.mediumQuesitons);
    Collections.shuffle(this.hardQuestions);

    this.questionQueue.clear();
    this.questionQueue.addAll(this.easyQuestions);
    this.questionQueue.addAll(this.mediumQuesitons);
    this.questionQueue.addAll(this.hardQuestions);
    this.questionIndex = 0;

    moveToQuestion(0);
}

The Collections.shuffle() at the top shuffles the Lists, it reorders the elements randomly. In the middle we create our questionQueue. First we clear() it to remove all questions that were in there from a previous game, and then we add first the easy questions, then the medium ones and finally the hard ones. And we reset our questionIndex to 0. The questionIndex keeps track at which position we are in the quesitonQueue. Finally at the bottom we call moveToQuestion(0); to go to the first question in the queue.

The moveToQuestion() method is again pretty straight forward but this time since this method is a little more complex I will add comments to it to explain it. the method looks like this:

private void moveToQuestion(int index) {
    // First we check if we have reached the end of the queue
    if(index < this.questionQueue.size()) {

        // If not we get the current question
        Question question = this.questionQueue.get(index);

        // Here we set the question text to the TextView
        int questionTextResId = question.getQuestionTextResId();
        this.textViewFrage.setText(questionTextResId);

        // And here the question image to the ImageView.
        int imageResId = question.getImageResId();
        this.imageViewBild.setImageResource(imageResId);

        // We get the answers from the question and create two count variables for convenience
        List<Answer> answers = question.getAnswers();
        int answerCount = answers.size();
        int buttonCount = this.answerButtons.length;

        // We start a loop through all the answer buttons
        for(int i = 0; i < buttonCount; i++) {
            // We get the current button from the Button[] which contains all the answer buttons
            Button button = this.answerButtons[i];

            // There might not be as many answers as there are buttons, that's what we check here
            if(i < answerCount) {
                // If there is an answer for this button make it visible
                button.setVisibility(View.VISIBLE);

                // We get the answer and bind to the button by calling bindAnswerToButton()
                Answer answer = answers.get(i);
                bindAnswerToButton(button, answer);
            } else {
               // If no answer exists for the Button we make it invisible.
               button.setVisibility(View.GONE);
            }
        }
    } else {
        // We have reached the end of the queue
        // You have to decide what happens when the game is won
        Toast toast = Toast.makeText(this, R.string.game_won, Toast.LENGTH_SHORT);
        toast.show();
    }
}

In bindAnswerToButton() we set the text and the OnClickListener of the Button:

private void bindAnswerToButton(Button button, Answer answer) {
    int answerTextResId = answer.getAnswerTextResId();
    button.setText(answerTextResId);

    button.setOnClickListener(new AnswerClickListener(answer));
}

As you can see the OnClickListener is custom and takes an answer as parameter in its constructor. This custom OnClickListener is what validates our answers and checks if we picked the right one. The custom OnClickListener looks like this:

private class AnswerClickListener implements View.OnClickListener {

    private final Answer answer;

    private AnswerClickListener(Answer answer) {
        this.answer = answer;
    }

    @Override
    public void onClick(View v) {
        if(this.answer.isCorrect()) {
            gotoNextQuestion();
        } else {
            // You have to decide what happens when someone picks the wrong answer
            Toast toast = Toast.makeText(QuizScreen.this, R.string.toast_wrong_answer, Toast.LENGTH_SHORT);
            toast.show();
        }
    }
}

The only thing it really does is check in onClick if the answer that has been passed to it is correct and if it is calls gotoNextQuestion() to move to the next question. If the answer is not correct currently only a Toast will be displayed. You have to decide what you want to happen in this case.

gotoNextQuestion() is again just a convenience method, all it does is increment our questionIndex and then calls moveToQuestion with the incremented questionIndex to move to the next Quesiton:

private void gotoNextQuestion() {
    this.questionIndex++;
    moveToQuestion(this.questionIndex);
}

And that is pretty much it. That was the whole code. Remember, the only thing you have to do is add all the questions in createQuestions() as I explained above. Here is the complete source code of the QuizScreen Activity:

public static class QuizScreen extends ActionBarActivity  {

    private final List<Question> easyQuestions = new ArrayList<Question>();
    private final List<Question> mediumQuesitons = new ArrayList<Question>();
    private final List<Question> hardQuestions = new ArrayList<Question>();

    private final List<Question> questionQueue = new ArrayList<Question>();
    private int questionIndex = 0;

    private Button buttonAntwort1;
    private Button buttonAntwort2;
    private Button buttonAntwort3;
    private Button buttonAntwort4;
    private TextView textViewFrage;
    private ImageView imageViewBild;

    private Button[] answerButtons;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_quiz_screen);

        this.imageViewBild = (ImageView) findViewById(R.id.imageViewBild);
        this.textViewFrage = (TextView) findViewById(R.id.textViewFrage);
        this.buttonAntwort1 = (Button) findViewById(R.id.buttonAntwort1);
        this.buttonAntwort2 = (Button) findViewById(R.id.buttonAntwort2);
        this.buttonAntwort3 = (Button) findViewById(R.id.buttonAntwort3);
        this.buttonAntwort4 = (Button) findViewById(R.id.buttonAntwort4);

        this.answerButtons = new Button[] { this.buttonAntwort1, this.buttonAntwort2, this.buttonAntwort3, this.buttonAntwort4 };

        createQuestions();
        startGame();
    }

    private void createQuestions() {
        newEasy(R.string.question1_text, R.drawable.question1_picture1)
                .addAnswer(R.string.question1_answer1, false).addAnswer(R.string.question1_answer2, true)
                .addAnswer(R.string.question1_answer3, false).addAnswer(R.string.question1_answer4, false);


        newMedium(R.string.question2_text, R.drawable.question2_picture1)
                .addAnswer(R.string.question2_answer1, false).addAnswer(R.string.question2_answer2, true)
                .addAnswer(R.string.question2_answer3, false).addAnswer(R.string.question2_answer4, false);


        newHard(R.string.question3_text, R.drawable.question3_picture1)
                .addAnswer(R.string.question3_answer1, false).addAnswer(R.string.question3_answer2, true)
                .addAnswer(R.string.question3_answer3, false).addAnswer(R.string.question3_answer4, false);

    }

    private Question newEasy(int questionTextResId, int imageResId) {
        Question question = new Question(questionTextResId, imageResId);
        this.easyQuestions.add(question);
        return question;
    }

    private Question newMedium(int questionTextResId, int imageResId) {
        Question question = new Question(questionTextResId, imageResId);
        this.mediumQuesitons.add(question);
        return question;
    }

    private Question newHard(int questionTextResId, int imageResId) {
        Question question = new Question(questionTextResId, imageResId);
        this.hardQuestions.add(question);
        return question;
    }

    private void startGame() {
        Collections.shuffle(this.easyQuestions);
        Collections.shuffle(this.mediumQuesitons);
        Collections.shuffle(this.hardQuestions);

        this.questionQueue.clear();
        this.questionQueue.addAll(this.easyQuestions);
        this.questionQueue.addAll(this.mediumQuesitons);
        this.questionQueue.addAll(this.hardQuestions);
        this.questionIndex = 0;

        moveToQuestion(0);
    }

    private void moveToQuestion(int index) {
        if(index < this.questionQueue.size()) {
            Question question = this.questionQueue.get(index);

            int questionTextResId = question.getQuestionTextResId();
            this.textViewFrage.setText(questionTextResId);

            int imageResId = question.getImageResId();
            this.imageViewBild.setImageResource(imageResId);

            List<Answer> answers = question.getAnswers();
            int answerCount = answers.size();
            int buttonCount = this.answerButtons.length;

            for(int i = 0; i < buttonCount; i++) {
                Button button = this.answerButtons[i];

                if(i < answerCount) {
                    button.setVisibility(View.VISIBLE);

                    Answer answer = answers.get(i);
                    bindAnswerToButton(button, answer);
                } else {
                   button.setVisibility(View.GONE);
                }
            }
        }
    }

    private void gotoNextQuestion() {
        this.questionIndex++;
        moveToQuestion(this.questionIndex);
    }

    private void bindAnswerToButton(Button button, Answer answer) {
        int answerTextResId = answer.getAnswerTextResId();
        button.setText(answerTextResId);
        button.setOnClickListener(new AnswerClickListener(answer));
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.quiz_screen, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();
        if (id == R.id.action_settings) {
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    private class AnswerClickListener implements View.OnClickListener {

        private final Answer answer;

        private AnswerClickListener(Answer answer) {
            this.answer = answer;
        }

        @Override
        public void onClick(View v) {
            if(this.answer.isCorrect()) {
                gotoNextQuestion();
            } else {
                Toast toast = Toast.makeText(QuizScreen.this, R.string.toast_wrong_answer, Toast.LENGTH_SHORT); 
                toast.show();
            }
        }
    }
}

If you have any further questions feel free to ask!

OTHER TIPS

Instead use below code and before that check whether CheckIfQuestionIsNew(QuizID) is returning boolean value or not

 do{
        QuizID = ShuffleQuiz();
    }while(CheckIfQuestionIsNew(QuizID));

In a "DO/WHILE" loop, the code between braces is executed at least one time. So if you get an exception in it, this should be because you're entering the loop as you shouldn't. Consider using a while clause instead?

If this is the line between braces that causes crash, please add the method's code.

Also add the logcat, please.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top