Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android lifecycle and null pointer problem

I have an application which uses a SQL database. This is encapsulated by a SQLiteOpenHelper class. When the splash screen launches, it calls init on a DataProvider class which stores a protected static instance of the SQLiteOpenHelper. init simply calls the constructor of the SQLiteOpenHelper:

public class UKMPGData extends SQLiteOpenHelper
{
public UKMPGData(Context context, String databaseName)
{
    super(context, databaseName, null, DATABASE_VERSION);
}

@Override
public void onCreate(SQLiteDatabase db)
{
    //create table and set up triggers etc
}
    @Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
{
    onCreate(db);
}
}

public class UKMPGDataProvider
{
protected static UKMPGData uKMpgData;

public static void init(Context aApplicationContext, String aDatabaseName)
{       
    uKMpgData = new UKMPGData(applicationContext, databaseName);
}

public static void close()
{
    uKMpgData.close();
}
}

I then had two further classes which extended UKMPGDataProvider and therefore had access to uKMpgData. These classes retrieve and store particular types of data from the database. Eg.

public class VehicleDataProvider extends UKMPGDataProvider
{

    public static Cursor getVehicles()
{
    Cursor cursor = null;

    SQLiteDatabase db = uKMpgData.getReadableDatabase();
    cursor = db.query(VEHICLE_TABLE_NAME, GET_VEHICLES_FROM_CLAUSE, null, null, null, null, ORDER_BY);

    return cursor;
}
//...
}

This all seemed to work fine until I noticed that if the application was running and then forced to the background, if it was left for a number of hours, when the app was brought back to the foreground I would get a null pointer in an Activity class which called getVehicles() (see above). It turns out, uKMpgData was no longer referencing an object.

I understand that Android can kill processes when it is deemed necessary, but don't understand what happened to my app to get the null pointer - if my app's process was killed, then wouldn't a new instance of the app be launched? In other words, a new SplashScreen would initialise the database object and therefore no null pointer exception.

I must be missing something - what state was my app in to have memory reclaimed (the reference to the database object) but to display the last visible activity upon relaunch.

Incidentally, the bug is now fixed. VehicleDataProvider and the other similar class no longer extend the superclass data provider (UKMPGDataProvider), which now holds a private reference to ukMpgData. All methods which touch the database now go through UKMPGDataProvider which will check for null and reinitialise if necessary.

Thanks in advance, Barry

like image 376
barry Avatar asked Jan 20 '23 14:01

barry


1 Answers

When Android kills your application to reclaim memory, your Application/Activity objects are all unloaded and will no longer exist. However, Android saves certain information so that your application can be restored to that approximate state when you try to run it again.

Part of this includes information about the state of the Activity stack (i.e. which Activities were running) before it was destroyed. As you noticed, Android restores your application to the Activity that was last running, and not the beginning of your app (the splash screen). This is a 'feature', and I can only assume it is an attempt to provide a seamless experience for the user who should not even notice that the app was unloaded.

There is no reason why Android should recreate your splash screen (even if it still existed in the Activity stack) before recreating the current Activity. That kind of dependency is not encouraged, and is not something you can rely on as Android loads/unloads Activities as it sees fit. In general, having static state that is initialised in previous activities is a sure way to run into problems in this situation. Since the splash screen was not recreated, it won't have initialiased the UKMPGDataProvider before you are using it in another Activity, hence the NullPointerException.

You can solve this in two ways.

  1. Initialise the UKMPGDataProvider inside Activity.onCreate(Bundle) of each Activity that uses it. If one of these Activities are being restored, it is guaranteed to get a callback to onCreate and hence the data provider will always be initialised.

  2. Initialise the UKMPGDataProvider inside Application.onCreate(). This is guaranteed to be called before any Activity is started, even in the even of memory reclaimation/restoration. If you do not yet have a custom Application class, you should create a subclass and then point to it in your AndroidManifest.xml for it to be used.

As an aside, implementing Activity.onSaveInstanceState(Bundle) is one way to save additional state relevant to your application, which will be given back to you in Activity.onCreate(Bundle) (it is the Bundle argument) upon restoration. It is meant for transient state only, such as some view state that arose after some user actions (in particular the state of custom views). It is not suitable for this case, since you can generate a new UKMPGDataProvider easily and it does not have any state linked to the previous session.

like image 80
antonyt Avatar answered Jan 28 '23 09:01

antonyt