EDIT: THE MAIN ISSUE, DEALING WITH SAVING STATE ON BACK BUTTON PRESSED. And then re-load the bundle onCreate. Not sure, how this can be done correctly. Nothing, I tried so far has worked correctly.
I am trying to save and recover the state of a simple game I made in Android. I am utilizing, the following events: onSaveInstanceState(), onBackPressed(), onCreate() and onPostCreate(). I am making my objects Serializable and Parcelable.
However, it does not successfully remember or recover in the state, before, pushing the back button or when just switching apps. Not, sure, of what I am doing wrong here. It's a behavioral error, i.e., no real error occurs.
Dice.java
public class Dice implements Parcelable {
private int value;
private int currentImage;
private boolean marked = false;
private boolean enabled = true;
private final Random random = new Random();
// Mapping of drawable resources:
private final int[] defaultDiceImages = {0,
R.drawable.white1, R.drawable.white2,
R.drawable.white3, R.drawable.white4,
R.drawable.white5, R.drawable.white6
};
private final int[] selectedDiceImages = { 0,
R.drawable.grey1, R.drawable.grey2, R.drawable.grey3,
R.drawable.grey4, R.drawable.grey5, R.drawable.grey6
};
private final int[] redDiceImages = { 0,
R.drawable.red1, R.drawable.red2, R.drawable.red3,
R.drawable.red4, R.drawable.red5, R.drawable.red6
};
// Constructor
Dice() { }
Dice(int value){
this.value = value;
this.currentImage = defaultDiceImages[this.value];
}
protected Dice(Parcel in) {
value = in.readInt();
/* currentImage = in.readInt();
marked = in.readByte() != 0;
enabled = in.readByte() != 0;
defaultDiceImages = in.createIntArray();
selectedDiceImages = in.createIntArray();
redDiceImages = in.createIntArray(); */
}
public static final Creator<Dice> CREATOR = new Creator<Dice>() {
@Override
public Dice createFromParcel(Parcel in) { return new Dice(in); }
@Override
public Dice[] newArray(int size) { return new Dice[size]; }
};
public boolean IsMarked() { return marked; }
public int GetValue() { return value; }
public void Toss() {
if(enabled) {
this.value = random.nextInt(6) + 1;
this.currentImage = defaultDiceImages[this.value];
}
}
public int GetCurrentImage(){ return currentImage; }
public void ToggleMarked() {
marked = !marked;
currentImage = (this.marked) ? selectedDiceImages[this.value] : defaultDiceImages[this.value];
}
public void ToggleEnabled() { enabled = !enabled; }
@NonNull
@Override
public String toString() {
return "Dice{" +
" value=" + this.value +
", currentImage=" + this.currentImage +
", marked=" + this.marked +
", enabled=" + this.enabled +
", random=" + this.random +
'}';
}
@Override
public int describeContents() { return 0; }
@Override
public void writeToParcel(Parcel parcel, int i)
{
parcel.writeInt(this.value);
}
}
Score.java
public class Score implements Serializable {
private int score = 0;
private String choice;
private static final int SCORE_LOW = 3;
public Score() {}
public Score(int score, String choice) {
this.score = score;
this.choice = choice;
}
public int getScore(){
return this.score;
}
public void setScore(int score){
this.score = score;
}
public String getChoice() {
return choice;
}
public void setChoice(String choice) {
choice = choice;
}
}
GameRound.java
import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import android.view.Gravity;
import android.widget.Toast;
import java.io.Serializable;
import java.util.ArrayList;
public class GameRound implements Parcelable, Serializable {
// Constants
private final int NUMBER_OF_DICES = 6;
private final int MAX_ALLOWED_THROWS = 3;
private ArrayList<Dice> dices;
public int totalDices;
private int throwsLeft;
private Score roundScore;
public GameRound() {
this.dices = GenerateNewDices();
this.roundScore = new Score();
this.throwsLeft = MAX_ALLOWED_THROWS;
}
protected GameRound(Parcel in) {
this.totalDices = in.readInt();
this.throwsLeft = in.readInt();
this.dices = (ArrayList<Dice>)in.readSerializable();
this.roundScore = (Score)in.readSerializable();
}
public ArrayList<Dice> GetDices() { return dices; }
public ArrayList<Dice> GenerateNewDices() {
ArrayList<Dice> tmp = new ArrayList<>(NUMBER_OF_DICES);
if(totalDices == 0)
totalDices = NUMBER_OF_DICES;
for(int i = 0; i<totalDices; i++){
tmp.add(new Dice(i));
}
return tmp;
}
public Score GetScore() { return roundScore; }
public int GetThrowsLeftCount() { return throwsLeft; }
public void SetRoundScore(Score score) {
this.roundScore = score;
}
public void TossDices(Context context){
if(ValidateAttempt(context)){
int selectedCount = (int)this.dices.stream().filter(Dice::IsMarked).count();
if(selectedCount > 0) {
for (Dice d : this.dices) {
if(d.IsMarked()) {
d.Toss();
d.ToggleMarked(); // Reset
}
}
} else {
for (Dice d : this.dices) {
d.Toss();
}
}
}
}
private boolean ValidateAttempt(Context context) {
if(throwsLeft > 0){
--throwsLeft;
return true;
} else {
Toast toast = Toast.makeText(context, "", Toast.LENGTH_SHORT);
toast.setGravity(Gravity.TOP, 0, 200);
toast.show();
return false;
}
}
public boolean CanPlay(){
return throwsLeft != 0;
}
public void Reset() {
for(Dice d: this.dices){
d.Toss();
}
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(totalDices);
dest.writeInt(throwsLeft);
dest.writeSerializable(dices);
dest.writeSerializable(roundScore);
}
@Override
public int describeContents() { return 0; }
public static final Creator<GameRound> CREATOR = new Creator<GameRound>() {
@Override
public GameRound createFromParcel(Parcel in) { return new GameRound(in); }
@Override
public GameRound[] newArray(int size) { return new GameRound[size]; }
};
}
ThirtyThrowsGame.java:
import android.os.Parcel;
import android.os.Parcelable;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import androidx.core.util.Pair;
public class ThirtyThrowsGame implements Parcelable, Serializable {
public enum ScoreChoice {
LOW(3),
FOUR(4),
FIVE(5),
SIX(6),
SEVEN(7),
EIGHT(8),
NINE(9),
TEN(10),
ELEVEN(11),
TWELVE(12),
;
private final int value;
ScoreChoice(int n) {
value = n;
}
public int getValue() {
return value;
}
}
public final int MAX_ROUNDS;
private int currentRound;
private GameRound round;
private ArrayList<Score> scores = new ArrayList<>();
private ArrayList<ScoreChoice> availableScoreChoices = new ArrayList<>();
public ThirtyThrowsGame(){
this.currentRound = 1;
MAX_ROUNDS = 10;
this.round = new GameRound();
this.availableScoreChoices.addAll(Arrays.asList(ScoreChoice.values()));
Collections.reverse(availableScoreChoices);
}
protected ThirtyThrowsGame(Parcel in) {
this.scores = (ArrayList<Score>)in.readSerializable();
MAX_ROUNDS = in.readInt();
this.currentRound = in.readInt();
this.round = in.readParcelable(GameRound.class.getClassLoader());
this.availableScoreChoices.addAll(Arrays.asList(ScoreChoice.values()));
Collections.reverse(availableScoreChoices);
}
private static int[] DiceValuesToArray(ArrayList<Dice> dices) {
int[] a = new int[dices.size()];
for (int i = 0; i < dices.size(); i++) {
a[i] = dices.get(i).GetValue();
}
return a;
}
public int calculateScoreLow(ArrayList<Dice> dices, int value) {
int sum = 0;
int[] a = DiceValuesToArray(dices);
for (int j : a) {
if (j <= value) {
sum += j;
}
}
return sum;
}
public int calculateScore(ArrayList<Dice> dices, int value) {
ArrayList<Integer> matches = new ArrayList<>();
int[] a = DiceValuesToArray(dices);
for(int i = 0; i<a.length; ++i)
for(int j = i + 1; j<a.length; ++j)
if(a[i] + a[j] == value){ // <- Pairs that match sum
matches.add(a[i]);
matches.add(a[j]);
break;
} else if(a[i] == value){ // <- Single candidates that match sum
matches.add(a[i]); break;
}
return matches.stream().mapToInt(Integer::intValue).sum(); // <- Return the sum of ints
}
public void Restart(){
Clear();
this.round = new GameRound();
}
public ArrayList<Score> GetRegistredScores() { return scores; }
public void SaveScore() { this.scores.add(round.GetScore()); }
public int TotalScore() {
return this.scores.stream().mapToInt(Score::getScore).sum();
}
public boolean NextRound() {
if(currentRound < MAX_ROUNDS) {
++currentRound;
SaveScore();
this.round = null;
this.round = new GameRound();
return false;
}
SaveScore();
return true;
}
public int GetCurrentRound() {
return currentRound;
}
public GameRound GetCurrentGameRound() { return round; }
public ArrayList<ScoreChoice> GetAvailableScoreChoices(){
return new ArrayList<>(availableScoreChoices);
}
public void Clear(){
this.currentRound = 1;
//rounds.clear();
this.scores.clear();
this.round = null;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeSerializable((scores));
dest.writeInt(MAX_ROUNDS);
dest.writeInt(currentRound);
dest.writeSerializable(round);
}
@Override
public int describeContents() { return 0; }
public static final Creator<ThirtyThrowsGame> CREATOR = new Creator<ThirtyThrowsGame>() {
@Override
public ThirtyThrowsGame createFromParcel(Parcel in) { return new ThirtyThrowsGame(in); }
@Override
public ThirtyThrowsGame[] newArray(int size) { return new ThirtyThrowsGame[size]; }
}; }
MainActivity.java:
// Parcelable key
private final String STATE_GAME = "STATE_GAME";
// Classes
private ThirtyThrowsGame game;
// View components
private Button rollBtn;
private Button collectScoreBtn;
private TextView roundText;
private TextView currentScoreText;
private Spinner scoreSelectionSpinner;
private ArrayAdapter<ThirtyThrowsGame.ScoreChoice> adapter;
private ArrayList<ImageView> diceViews;
// Data
int score = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Find elements on the UI by id.
rollBtn = findViewById(R.id.btnRoll);
collectScoreBtn = findViewById(R.id.btnCollectScore);
scoreSelectionSpinner = findViewById(R.id.spinner);
roundText = findViewById(R.id.RoundText);
currentScoreText = findViewById(R.id.CurrentScoreText);
if (savedInstanceState != null) {
this.game = (ThirtyThrowsGame) savedInstanceState.getSerializable(STATE_GAME);
} else {
this.game = new ThirtyThrowsGame();
}
rollBtn.setOnClickListener(e -> {
if (game.GetCurrentGameRound().CanPlay()) {
RefreshScene(this);
SetScore();
} else {
Toast toast = Toast.makeText(
this,
"Please collect score to run next round.",
Toast.LENGTH_SHORT
);
toast.show();
}
});
collectScoreBtn.setOnClickListener(e -> {
scoreSelectionSpinner.setSelection(0);
boolean _continue = game.NextRound();
score = 0;
SetDefaultDiceView();
RefreshScene();
if (_continue) {
NextActivity();
}
});
SetupDropDown();
GetDiceViews();
SetupDiceClickEventListeners();
currentScoreText.setText(getString(R.string.score, 0));
roundText.setText(getString(R.string.round, game.GetCurrentRound()));
RefreshRollButtonText();
}
@Override
protected void onPostCreate(@Nullable Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
if (savedInstanceState != null) {
if (game != null) {
score = game.GetCurrentGameRound().GetScore().getScore();
if (score == 0) {
SetDefaultDiceView();
} else {
UpdateImageViews();
}
RefreshScene();
}
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
outState.putSerializable(STATE_GAME, game);
super.onSaveInstanceState(outState);
}
@Override
public void onBackPressed() {
moveTaskToBack(true);
}
private void SetDefaultDiceView() {
diceViews.get(0).setImageResource(R.drawable.white1);
diceViews.get(1).setImageResource(R.drawable.white2);
diceViews.get(2).setImageResource(R.drawable.white3);
diceViews.get(3).setImageResource(R.drawable.white4);
diceViews.get(4).setImageResource(R.drawable.white5);
diceViews.get(5).setImageResource(R.drawable.white6);
}
private void SetupDiceClickEventListeners() {
int index = 0;
for (ImageView v : diceViews) {
int finalIndex = index;
v.setOnClickListener(e -> {
this.game.GetCurrentGameRound().GetDices().get(finalIndex).ToggleMarked();
diceViews.get(finalIndex).setImageResource(game.GetCurrentGameRound()
.GetDices().get(finalIndex).GetCurrentImage());
});
++index;
}
}
public void RefreshScene() {
roundText.setText(getString(R.string.round, game.GetCurrentRound()));
currentScoreText.setText(getString(R.string.score, game.GetCurrentGameRound().GetScore().getScore()));
RefreshRollButtonText();
}
public void RefreshScene(Context context) {
game.GetCurrentGameRound().TossDices(context);
roundText.setText(getString(R.string.round, game.GetCurrentRound()));
UpdateImageViews();
RefreshRollButtonText();
}
This is pretty much almost all of the code here. If someone, can spot the issues, please let me know. I been struggling with this for weeks now, and it is getting deeply annoying. I need to ensure, it works when you leave the back in the background, so that it recovers the game state and continues to reflect this on the UI.
EDIT: Yes, I ran this in debug mode stepping throw the code.
EDIT 6/27/2022: I have developer mode enabled with "Don't keep activities" enabled.