Yesterday I asked this question on the network about how to resume a simple Tic-tac-toe game after closing and opening the fragment. As I said in my question:
I'm learning Android development by creating a test project: Tic-tac-toe. My Tic-tac-toe app starts with a Main Menu activity with a Button that says New Game. After clicking New Game, an activity with a fragment containing a Tic-Tac-Toe game (in a GridLayout) launches. The user can play the game, but when they go back to the Main Menu, the game state is not saved.
I want to change this so that when the user goes back to the Main Menu, they will see a new Button called "Continue" (in addition to the "New Game" Button). Once the user clicks "Continue", the game they were playing before continues. If the click the "New Game" button, a new game will be launched like before.
Three answers were proposed with two different methods: Utilizing SharedPreferences
or using startActivityForResult()
. Both of those methods seem to work fine, but I stumbled upon a cleaner solution, by just declaring the Tic-Tac-Toe object as static:
public static TicTacToe t = new TicTacToe();
This appears to do everything I need since now I will only have one instance of t
no matter how often I create or destroy the fragment. So on orientation changes, going back to Main Menu and opening the game again, or even on closing and restarting the app, the game state stays the same just like I wanted. However, since nobody proposed this solution and because I am a novice in Android development, I must ask: is there something wrong with using this method?
Here is my full code, with only that single change made:
My MainMenu
class:
public class MainMenu extends ActionBarActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_menu);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main_menu, 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);
}
public void startGame(View view) {
Intent intent = new Intent(this,MainGame.class);
startActivity(intent);
}
}
with a corresponding xml file activity_main_game
:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/new_game"
android:onClick="startGame"
/>
</RelativeLayout>
Now my MainGame.class
looks like this:
public class MainGame extends ActionBarActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_game);
if (savedInstanceState == null) {
getSupportFragmentManager().beginTransaction()
.add(R.id.container, new BoardFragment()).commit();
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main_game, 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);
}
public static class BoardFragment extends Fragment {
public static TicTacToe t = new TicTacToe(); //A class I wrote that launches a simple TicTacToe game
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if (savedInstanceState != null) {
t = new TicTacToe(savedInstanceState.getInt("TicTacToeData"),
);
}
//Graphics stuff here: variable rootView which contains the TicTacToe grid is defined
//and onClickListeners are added to the ImageViews in the GridLayout which makes corresponding
//changes to t.
return rootView;
}
@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState.putInt("TicTacToeData", t.getGameData());
}
}
}
Static data is not persistent, and will be cleared as soon as the class is unloaded (at some point after your app is in the background and Android kills the process to reclaim memory).
Static data is not a place to store persistent data -- you should use either SharedPreferences (easiest route) or a SQLite database (more difficult but probably best route, long-term), or even just serializing your state to a file somewhere.
First, generally speaking,
static
data members should be used carefully, as they represent intentional memory leaks. Whatever is in thisTicTacToe
object cannot be garbage collected, and anything that can be indirectly reached by thisTicTacToe
object cannot be garbage collected. In classic Java development,static
data members are considered to be poor form as a result. In Android, they tend to be used somewhat more, but ideally only by experienced developers.Related to this is choosing the right tool for the job. We could cure cancer by beheading the patient, as the tumor will stop growing. However, this epitomizes "the cure is worse than the disease". In this case, there are better solutions for specific problems, such as handling configuration changes (e.g., screen rotations) that should be considered in lieu of
static
data members.Second,
static
data members do not live forever. They go away when your process does. Your process can go away at any point once you are no longer in the foreground, and when your process is terminated, yourTicTacToe
object goes poof. And, since your process can be terminated within seconds of moving into the background (depending on what else is going on with the device), your data model is at risk.This gets back to user expectations.
If the user would expect that the "Continue" option should be available at any point, then while you could use
TicTacToe
as some sort of in-process cache, the real data model needs to be a persistent data store:SharedPreferences
, database, or an arbitrary file.On the far other end of the spectrum, if you don't care that the user's game may be lost when, say, they take a phone call and you move into the background, then saying that your data model is purely in memory is certainly an option.
And there are "middle ground" approaches (e.g., saved instance state) as well.
The solutions from your other question that focus on
startActivityForResult()
are inappropriate IMHO.