This should be a quick post. Last time I tested restarting my app from the question display - I got the "Sorry - the application has stopped unexpectedly, please try again.
Well, this proves to be repeatable. So, let's take a look at logcat. Ah-ha. The trace shows that it went south in our old friend getWordGrouping. This is an in-memory hash table of similarly grouped words which are used to provide candidate answers to quizzes. Clearly, the problem has to do with the fact that it's not getting re-initialized when the activity resumes.
This gives me a chance to straighten out an annoying lack of symmetry in the code. Now that I've restructured, I no longer have to worry about resetting the quiz sequence; it limited to the gap between start and end quiz numbers, and that shouldn't be too high. Hmmm...questions answered could go a bit off; I'll have to check that.
Hmmm - I don't get it. At first I put in the init words, but I had some inconsistent results, so I took it back out - and now I can't repeat the error.
Well, the problem seems to be, if I kill it in the middle of a sequence, the same words are done over again. That's ok if the sequence is small, but what about a large sequence?
That I don't care about. But, the real problem is, once they finish that sequence, it goes back to that sequence one more time before it moves to the next one. Why would that be? Where does the update happen, and why is it reverting to the old numbers? Am I not saving them on the update? Ah, yes that was it. Let's put the save in and try again.
Ah, ok, now it's crashing again - as it was supposed to, because I didn't fix that part. If I restart again, it works, because it goes through the start display, and the numbers were properly incremented.
Now Let's just add the code that will fix the word hash setup, and try again.
Perfect. On the third answer and kill, it came back to the same question, then wrapped and went to the next question.
Now try on the third answer, before answering the question...
Good - it wrapped again.
Now, we'll try it on the answer side of the second question...
Whoah. Crash! Somehow it got into the onclick, although that had already happened. I was on the answer side.
So, num questions asked was less than num questions in seq, and it went to increment the question - except - there was an index out of bounds exception, because nothing is in quiz state. Clearly the problem here is that it didn't enter through the onResume method - but rather through the onClick, which somehow got re-queued.
Sooo...the obvious answer is to re-initialize the state, just as in on resume. We'll extract the relevant calls to a method, and retest.
Crash again! Ah, I misspoke when I said the click got queued - it came back to the question, not the answer. I pushed the click. So, remove the extracted method from the onclick, and find out why the method is wrong, the one time it is called.
I'm starting to thing I should just call initialize, as I had originally planned.
Right now, it's doing something like this:
InitUtils.setAppState(this);
mAppState = (AppState) this.getApplication();
if (mAppState.currentQuestion == 0) {
incrementQuestion();
}
InitUtils.initializeWordGroupings(this, mAppState);
Initialize does this:
public static void initialize(Activity a) {
AppState appState = (AppState) a.getApplication();
initializeWordGroupings(a, appState);
appState.initQuizSequence(a, appState.quizStartNum, appState.quizEndNum);
Log.d(TAG, "exit initialize");
}
The other place where initialize could be called from, StartActivity, is this:
InitUtils.setAppState(this);
InitUtils.initializeWordGroupings(this, appState);
It's not initializing sequence either; so, that's just plain wrong. Well, leave it for now - change as few variables as possible. Come back to it after QA code works.
This will be initialize:
public static void initialize(Activity a) {
AppState appState = (AppState) a.getApplication();
InitUtils.setAppState(this);
initializeWordGroupings(a, appState);
appState.initQuizSequence(a, appState.quizStartNum, appState.quizEndNum);
Log.d(TAG, "exit initialize");
}
And after it's called, we'll check the counter for zero in question activity.
Whoah - it crashed again! This time,
E/AndroidRuntime( 4890): Caused by: java.lang.NullPointerException
E/AndroidRuntime( 4890): at com.jlptquiz.app.QuestionActivity.onResume(QuestionActivity.java:64)
E/AndroidRuntime( 4890): at android.app.Instrumentation.callActivityOnResume(Instrumentation.java:1150)
E/AndroidRuntime( 4890): at android.app.Activity.performResume(Activity.java:3832)
E/AndroidRuntime( 4890): at android.app.ActivityThread.performResumeActivity(ActivityThread.java:2110)
E/AndroidRuntime( 4890): ... 12 more
Naturally, on the onResume, line 64. I lost the initialization of appState when shuffling the code in and out of that method.
Ok, I went through one level, and now have stopped at the second one. Let's kill and restart.
Well, it restarted on same question ok - but then went on to ask exactly the same question. This is a bug I'm going to have to live with - because i don't want to get into saving and restoring the current sequence. It's on a restart condition, it's a mobile app, and it's not worth the complexity it would introduce.
Ok, now I'm going to kill it on the question part of the 2nd of 3 questions.
Actually, it came back to the same question. I'm wondering which is better - to skip a word by potentially selecting the same question; or come back into the middle of a sequence, and then say you've advanced, and then go back to the original sequence you were on? The second option looks like a mistake - but so doesn't going to the next question and having it be the same question. What I would prefer as a user is to err on the side of caution repeat the questions. Definitely.
Actually, instead if going to second question, where I killed it, it went back to the first, ran through the whole level again, then went to the next level; which is actually the best solution now that I think about it. But, why? Let's confirm that behavior. Kill it on the second question, no second answer.
Goes back to the second question...than asks the same question again - and increments to the next set of questions. Did I forget to relaunch it?
Let's do it again. Relaunch.
Restart on the second *answer*. Goes back to the same question. Asks the first question (only 1 in 3 chance of getting the one that wasn't asked) - says congrats - and it advances! Did I "undo" the save on the wrong part? Why can't I find it? If the num questions asked == the last quest number - the first quiz number, go to the next set. It would be on the button push. Where's the button push? Find the click.
There is is - in incrementQuestion, which comes from checkIncrementQuestion, which comes from onClick, which comes from onSetupButton.
Same result, save there, or not there. It doesn't seem to make a difference, either way. It might have been that way before, I was testing it on the question, not the answer? I don't think that would make a difference. Well, I'm a bit confused, but the more I think about it, whether I save it or not, it's going to get to the end of the current sequence of questions and jump to the next. Sooo, I might as well save it, just to be consistent.
Ok, now I'm in the second answer again. Kill and restart. And of course it advances to the same question, then goes to the next level, missing whatever the third one was. Actually - if I don't save it - you'll get two shots at the getting the question instead of one. I'm going to flop again, and comment out the save.
Kill on the second question...and it comes back to the same question, and answer it. Then it asks for what was the first queston again - now, if i didn't save it, it should ask one more after this? No, because it had asked 1 already when I killed it. So, this *is* that "second" shot, i.e. the third question. Well, it's better to have at least one shot at getting that unshown question.
Ok, I finally have the behavior defined, even if it's not perfect. Ideally I'd like to reset it to the beginning on a destroy. But what if there's a loong sequence. I just had a thought - when I kill and restart, it's coming in through start activity anyway. So, why not just reinitialize the sequence there? Actually, that's happening when it goes into question. Wait - wouldn't that mean that questions could be duplicated? No, I was wrong, it isn't coming through start every time. But how is it possible than questions. aren't getting duplicated if it's re-initiated the sequence every time? Oh, right, because it's not part of the question loop. Right.
Ok, now, the question is, why am I initializing *anything* in start activity? It's just handing off to question activity, which does the same thing. Well, not necessarily. It also hands off to settings. And what does settings do? Let's take a peek. Right, it depends on something being in settings. Hmm. I could just do a setAppstate, which sets the defaults.
I'll leave it for now. I'll put a comment for a posible refactor later.
There. That turned out to be a bit tricker than I anticipated. It's not perfect, but it's satisfactory.