So far, we’ve talked about apps that run, respond to user input, and follow the activity lifecycle. All of the apps we’ve built so far have to start over whenever the user runs them. In othe words, they don’t store any data between runs. This tutorial talks about data storage, which lets us save and load data from our app.
Let’s start with an example app that shows a button and a label that displays how many times the button was pressed. Here’s the activity_main.xml
file:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<Button
android:id="@+id/button_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Click me."
android:textSize="24sp" />
<TextView
android:id="@+id/label_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="You clicked 0 times."
android:textSize="42sp" />
</LinearLayout>
This layout contains a button and a label. Here’s the ActivityMain
activity:
package io.happycoding.helloworldapp;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends Activity {
int clickCount = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final TextView label = findViewById(R.id.label_id);
final Button button = findViewById(R.id.button_id);
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
clickCount++;
label.setText("You clicked " + clickCount + " times.");
}
});
}
}
This activity adds a click listener to the button that increments a clickCount
variable and updates the label. In other words, the label displays how many times the button was pressed.
The above app has one major problem: when the device is rotated, the count restarts at zero. That’s because when the device is rotated, the activity is recreated from scratch. Variables are reinitialized, and the onCreate()
function is called. In our case, this causes the count to start back over at zero.
We can fix this problem by storing the count just before the activity is destroyed, and loading it when it’s recreated. We can store it using the onSaveInstanceState()
function, which takes a Bundle
argument. A Bundle
is a map of keys to values, which lets you store your app’s state in key-value pairs.
Here’s an example that stores the current clickCount
in the Bundle
argument:
@Override
public void onSaveInstanceState(Bundle map) {
map.putInt("clickCount", clickCount);
}
The data stored in the Bundle
argument is then passed as an argument to the onCreate()
function. So now that we stored the state in the onSaveInstanceState()
function, we can load that state in the onCreate()
function. Putting it all together, it looks like this:
package io.happycoding.helloworldapp;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends Activity {
int clickCount = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if(savedInstanceState != null) {
clickCount = savedInstanceState.getInt("clickCount");
}
final TextView label = findViewById(R.id.label_id);
label.setText("You clicked " + clickCount + " times.");
final Button button = findViewById(R.id.button_id);
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
clickCount++;
label.setText("You clicked " + clickCount + " times.");
}
});
}
@Override
public void onSaveInstanceState(Bundle map) {
map.putInt("clickCount", clickCount);
}
}
Now the onCreate()
function uses the savedInstanceState
to load the previous clickCount
value, which it uses to set the text of the label. Now when we rotate the device, the clickCount
is properly maintained!
We can also use Bundle
instances to pass data from one activity to another. Here’s an example:
Intent intent = new Intent(this, ActivityToLaunch.class);
Bundle dataMap = new Bundle();
dataMap.putInt("clickedCount", clickCount);
intent.putExtras(dataMap);
startActivity(intent);
Then in the launched activity, we can access this stored data using the getIntent()
function:
int clickedCount = getIntent().getExtras().getInt("clickedCount");
Now our app properly maintains the clickCount
variable when the device is rotated. But the count will still reset whenever the app is closed and reopened by the user. To fix this, we need to save the count to a file, and load that file when the app starts up.
If you’re only storing basic data and you don’t care about the file format, you can use the built-in SharedPreferences
class to store your data in key-value pairs. Call the getPreferences()
function to get a SharedPreferences
instance, and then store or load your data in one of the lifecycle functions.
For example, this code stores the clickCount
variable in the SharedPreferences
object:
@Override
public void onStop() {
super.onStop();
SharedPreferences preferences = getPreferences(Context.MODE_PRIVATE);
preferences.edit().putInt("clickCount", clickCount).apply();
}
Remember that the onStop()
function is called whenever the activity is no longer visible (when the user exits the app, launches a new Activity, or rotates the device). We can use this function to save our data before exiting our app. The SharedPreferences
class takes care of creating and storing a file for us. Then we can load the data in the onCreate()
function:
SharedPreferences preferences = getPreferences(Context.MODE_PRIVATE);
clickCount = preferences.getInt("clickCount", 0);
Putting it all together, it looks like this:
package io.happycoding.helloworldapp;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends Activity {
int clickCount = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
SharedPreferences preferences = getPreferences(Context.MODE_PRIVATE);
clickCount = preferences.getInt("clickCount", 0);
final TextView label = findViewById(R.id.label_id);
label.setText("You clicked " + clickCount + " times.");
final Button button = findViewById(R.id.button_id);
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
clickCount++;
label.setText("You clicked " + clickCount + " times.");
}
});
}
@Override
public void onStop() {
super.onStop();
SharedPreferences preferences = getPreferences(Context.MODE_PRIVATE);
preferences.edit().putInt("clickCount", clickCount).apply();
}
}
Now our data is saved, even when the user exits and reopens the app.
SharedPreferences
is a great option if you only need to store simple data and don’t care about the format of the file. But if you want more control over how your data is stored, then you can create a file inside your app’s internal storage, which is a directory that only your app can access.
To create and write to a file in your app’s internal storage directory, call the openFileOutput()
function. Here’s an example:
String fileContent = "clickCount:" + clickCount;
String filename = "data.txt";
try {
FileOutputStream outputStream = openFileOutput(filename, Context.MODE_PRIVATE);
outputStream.write(fileContent.getBytes());
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
This example stores the clickCount
variable in a simple key-value pair in a .txt
file, but it’s completey up to you what goes in your files. You could use JSON, or XML, or serialized data, or a custom format that you come up with.
Now that we have a file in our internal storage, we can load it and read its data. One approach is to use the openFileInput()
function to get a FileInputStream
to a file, or we could use the getFilesDir()
function to get the path to our internal storage. We can use this with the Scanner
class to read the file line-by-line.
File file = new File(getFilesDir(), "data.txt");
if(file.exists()) {
try {
Scanner scanner = new Scanner(file);
String line = scanner.nextLine();
String clickCountString = line.split(":")[1];
clickCount = Integer.parseInt(clickCountString);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
This code checks whether the file exists (it won’t exist the first time the program runs), and if so it uses a Scanner
to read the contents of the file. Our file only includes a single line, but more advanced code might read multiple lines.
Putting it all together, it looks like this:
package io.happycoding.helloworldapp;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Scanner;
public class MainActivity extends Activity {
int clickCount = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
File file = new File(getFilesDir(), "data.txt");
if(file.exists()) {
try {
Scanner scanner = new Scanner(file);
String line = scanner.nextLine();
String clickCountString = line.split(":")[1];
clickCount = Integer.parseInt(clickCountString);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
final TextView label = findViewById(R.id.label_id);
label.setText("You clicked " + clickCount + " times.");
final Button button = findViewById(R.id.button_id);
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
clickCount++;
label.setText("You clicked " + clickCount + " times.");
}
});
}
@Override
public void onStop() {
super.onStop();
String fileContent = "clickCount:" + clickCount;
String filename = "data.txt";
try {
FileOutputStream outputStream = openFileOutput(filename, Context.MODE_PRIVATE);
outputStream.write(fileContent.getBytes());
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
More advanced code might put the file reading and writing inside its own function or utility class.
Happy Coding is a community of folks just like you learning about coding.
Do you have a comment or question? Post it here!
Comments are powered by the Happy Coding forum. This page has a corresponding forum post, and replies to that post show up as comments here. Click the button above to go to the forum to post a comment!