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.