Storage



Storage



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.

Example 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.

Bundle

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!

Passing Data to Activities

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");

Shared Preferences

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.

Internal Storage

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.

Homework

  • Create a notes app that shows a text area where users can enter random notes. Save these notes when the app is closed, and load them when they app is opened.

More Info

Comments

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!