简体   繁体   中英

android - Check if the room database is populated on startup without Livedata

I'm fairly new to Android and I want to have a database in my app. I'm introduced to Room the documents say it's the best way to implement databases in the android.

Now I have to pre-populate some data in the database, and make sure that it gets populated before the app startup.

I see that there are many things like LiveData , Repositories , ViewModels and MediatorLiveData .

But I just want to keep it plain and simple, without using the said things how can one find if the database has been populated before the application launch.

I'm getting loads of NullPointerExceptions .

I'm using onCreateCallback() to populate the database but when I try to get the item from database it produces NullPointerException and after some time it may or may not produce the same warning, and the question remains the same what is the best way to know when the database is populated completely.

Here is a Minimal Example

public class MainActivity extends AppCompatActivity {
private TextView nameView;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    nameView = findViewById(R.id.name);
    new NamesAsyncTask().execute();
}

private class NamesAsyncTask extends AsyncTask<Void,Void,String> {
    private NameDao mNameDao;
    @Override
    public String doInBackground(Void... params) {
        NameDatabase db = NameDatabase.getDatabase(MainActivity.this);
        mNameDao = db.nameDao();
        String name = mNameDao.getNameByName("Body").name;
        return name;
    }

    @Override
    public void onPostExecute(String name) {
        nameView.setText(name);
    }
}
}

Entity

@Entity(tableName = "name")
public class Name {
@NonNull
@PrimaryKey(autoGenerate = true)
public Integer id;

@NonNull
@ColumnInfo(name = "name")
public String name ;

public Name(Integer id, String name) {
    this.id = id;
    this.name = name;
}

public Integer getId() {
    return this.id;
}

public void setId(Integer id ) {
    this.id = id;
}

public String getName() {
    return this.name;
}

public void setName(String name) {
    this.name = name;
}
}

Dao

@Dao
public interface NameDao {
@Insert
void insertAll(List<Name> names);

@Query("SELECT * from name")
List<Name> getAllNames();

@Query("DELETE FROM name")
void deleteAll();

@Query("SELECT * FROM name WHERE name = :name LIMIT 1")
Name getNameByName(String name);

@Query("SELECT * FROM name WHERE id = :id LIMIT 1")
Name getNameById(int id);
}

Database

@Database(entities = {Name.class}, version = 1)
public abstract class NameDatabase extends RoomDatabase {
public abstract NameDao nameDao();
private static NameDatabase INSTANCE;
public boolean setDatabaseCreated = false;

public static NameDatabase getDatabase(final Context context) {
    if (INSTANCE == null) {
        synchronized (NameDatabase.class) {
            if (INSTANCE == null) {
                INSTANCE = buildDatabase(context);
                INSTANCE.updateDatabaseCreated(context);
            }
        }
    }
    return INSTANCE;
}

private static NameDatabase buildDatabase(final Context appContext) {
    return Room.databaseBuilder(appContext, NameDatabase.class,
            "name_database").addCallback(new Callback() {
                                                  @Override
                                                  public void onCreate(@NonNull SupportSQLiteDatabase db) {
                                                      super.onCreate(db);
                                                      Executors.newSingleThreadScheduledExecutor().execute(() -> {
                                                          // Add Delay to stimulate a long running opeartion
                                                          addDelay();
                                                          // Generate the data for pre-population
                                                          NameDatabase database = NameDatabase.getDatabase(appContext);
                                                          List<Name> names = createNames();

                                                          insertData(database, names);
                                                          // notify that the database was created and it's ready to be used
                                                          database.setDatabaseCreated();

                                                      });

                                                  }
                                              }
    ).build();
}

private void updateDatabaseCreated(final Context context) {
    if (context.getDatabasePath("name_database").exists()) {
        setDatabaseCreated();
    }
}

private boolean setDatabaseCreated() {
    return this.setDatabaseCreated = true;
}


protected static List<Name> createNames() {
    List<Name> cList = new ArrayList<>();

    cList.add(new Name(1, "Body"));
    cList.add(new Name(2, "Mind"));
    cList.add(new Name(3, "Love"));
    cList.add(new Name(4, "Community"));
    cList.add(new Name(5, "Career"));
    cList.add(new Name(6, "Money"));
    cList.add(new Name(7, "Fun"));
    cList.add(new Name(8, "Home"));
    return cList;
}


private static void insertData(final NameDatabase database, final List<Name> names) {
    database.runInTransaction(() -> {
        database.nameDao().insertAll(names);
    });
}

private static void addDelay() {
    try {
        Thread.sleep(4000);
    } catch (InterruptedException ignored) {
    }
}
}

Gives me the exception on String name = mNameDao.getNameByName("Body").name;this line, when I install the app for first time, however if I close the app and start again it does not give the exception anymore. I think because the database has not been populated yet.

I read a post Pre-Populate Database that says on the first call to db.getInstance(context); the database will be populated on in my case NameDatabase.getDatabase(MainActivity.this) .

So what shall I do to know if the database has finished populating after the call?

I think because the database has not been populated yet.

Correct. You have forked one background thread ( AsyncTask ). That thread is forking a second background thread, via your getDatabase() call, as your database callback is forking its own thread via Executors.newSingleThreadScheduledExecutor().execute() . Your AsyncTask is not going to wait for that second thread.

Remove Executors.newSingleThreadScheduledExecutor().execute() from your callback. Initialize your database on the current thread (which, in this case, will be the AsyncTask thread). Make sure that you only access the database from a background thread, such as by having your database access be managed by a repository.

I hope I'm not late! Just a bit of a background before I answer.

I was also searching for a solution regarding this problem. I wanted a loading screen at startup of my application then it will go away when the database has finished pre-populating.

And I have come up with this (brilliant) solution: Have a thread that checks the sizes of the tables to wait. And if all entities are not size 0 then notify the main UI thread. (The 0 could also be the size of your entities when they finished inserting. And it's also better that way.)

One thing I want to note is that you don't have to make the variables in your entity class public . You already have getters / setters for them. I also removed your setDatabaseCreated boolean variable. (Believe me, I also tried to have a volatile variable for checking but it didn't work.)

Here's the solution: Create a Notifier class that notifies the main UI thread when the database has finished pre-populating. One problem that arises from this is memory leaks. Your database might take a long time to pre-populate and the user might do some configuration (like rotating the device for example) that will create multiple instances of the same activity. However, we can solve it with WeakReference .

And here's the code...

Notifier class

public abstract class DBPrePopulateNotifier {
    private Activity activity;

    public DBPrePopulateNotifier(Activity activity) {
        this.activity = activity;
    }

    public void execute() {
        new WaitDBToPrePopulateAsyncTask(this, activity).execute();
    }

    // This method will be called to set your UI controllers
    // No memory leaks will be caused by this because we will use
    // a weak reference of the activity
    public abstract void onFinished(String name);

    private static class WaitDBToPrePopulateAsyncTask extends AsyncTask<Void, Void, String> {
        private static final int SLEEP_BY_MILLISECONDS = 500;
        private WeakReference<Activity> weakReference;
        private DBPrePopulateNotifier notifier;

        private WaitDBToPrePopulateAsyncTask(DBPrePopulateNotifier notifier, Activity activity) {
            // We use a weak reference of the activity to prevent memory leaks
            weakReference = new WeakReference<>(activity);
            this.notifier = notifier;
        }

        @Override
        protected String doInBackground(Void... voids) {
            int count;
            Activity activity;
            while (true) {
                try {
                    // This is to prevent giving the pc too much unnecessary load
                    Thread.sleep(SLEEP_BY_MILLISECONDS);
                }
                catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }

                // We check if the activity still exists, if not then stop looping
                activity = weakReference.get();
                if (activity == null || activity.isFinishing()) {
                    return null;
                }

                count = NameDatabase.getDatabase(activity).nameDao().getAllNames().size();
                if (count == 0) {
                    continue;
                }

                // Add more if statements here if you have more tables.
                // E.g.
                //    count = NameDatabase.getDatabase(activity).anotherDao().getAll().size();
                //    if (count == 0) continue;

                break;
            }

            activity = weakReference.get();
            // Just to make sure that the activity is still there
            if (activity == null || activity.isFinishing()) {
                return null;
            }

            // This is the piece of code you wanted to execute
            NameDatabase db = NameDatabase.getDatabase(activity);
            NameDao nameDao = db.nameDao();
            return nameDao.getNameByName("Body").getName();
        }

        @Override
        protected void onPostExecute(String name) {
            super.onPostExecute(name);
            // Check whether activity is still alive if not then return
            Activity activity = weakReference.get();
            if (activity == null|| activity.isFinishing()) {
                return;
            }
            // No need worry about memory leaks because
            // the code below won't be executed anyway
            // if a configuration has been made to the
            // activity because of the return statement
            // above
            notifier.onFinished(name);
        }
    }
}

MainActivity

public class MainActivity extends AppCompatActivity {
    private TextView nameView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        nameView = findViewById(R.id.name);
        new DBPrePopulateNotifier(this) {
            @Override
            public void onFinished(String name) {
                // You set your UI controllers here
                // Don't worry and this won't cause any memory leaks
                nameView.setText(name);
            }
        }.execute();
    }
}

As you can see, our Notifier class has a thread in it that checks if the entities are not empty.

I didn't change anything in your other classes: Name , NameDao and NameDatabase except that I removed the boolean variable in NameDatabase and made private the variables in Name .

I hope that this answers your question perfectly. As you said, no LiveData , Repository , etc.

And I really hope I ain't late to answer!


Now I want to write down what I tried before I came up to the final solution.

Keep in mind that what I am trying to do here is for my app to show a progress bar (that infinite spinning circle) and put it away after the database has finished pre-populating.

Tried: 1. Thread inside thread

Practically, there's a thread that checks if the size of an entity is still 0. The query is done by another thread .

Outcome: Failed . Due to my lack of knowledge, you cannot start a thread within another thread . Threads can only be started from the main thread .

  1. Tables' sizes loop checker

A thread that queries the tables to be checked if they have been initialized through an infinite loop. Only breaks if all sizes of the tables to be checked are greater than 0.

Outcome: Solution . This is by far the most elegant and working solution to this problem. It doesn't cause memory leaks because as soon as a configuration has been made, the thread that loops continually will break.

  1. Static variable

A static volatile variable in the database class in which will turn to true when the thread has finished inserting the values.

Outcome: Failed . For unknown reason that I still search for, it won't run the thread for initializing the database. I have tried 3 versions of the code implementation but to no avail. Hence, a failure.

  1. Initialize database then notify

A listener that is defined in the UI thread then passed by argument to the repository . All database initialization is done also in the repository . After populating the database, it will then notify/call the listener .

Outcome: Failed . Can cause memory leaks.


As always, happy coding!

登录 onCreateCallback ofc!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM