Requery as a Modern Approach for Local Storage in Android

Mobile applications that don’t use the network are a rare thing today. What shall we do when the Internet is not available? A well-designed application contains an offline mode using local storage. There are multiple libraries which support this solution. In the article, I would like to focus on one and - in my opinion - the best of them - Requery.

First, let’s see how the relations of entities look like in the below example:

A person has a unique phone number and can join many trips. A trip contains many participants and one location that is not unique because a lot of trips can have the same destination. This schema is really useful because it contains every type of relation: one-to-one, one-to-many and many-to-many.

Let’s jump to the code now. First, we have to import Requery to the project. The only thing that has to be done is adding these lines to your build.gradle file:

compile 'io.requery:requery:1.3.0'  
compile 'io.requery:requery-android:1.3.0'  
annotationProcessor 'io.requery:requery-processor:1.3.0'  

To get more information about Requery please visit the library’s homepage: https://github.com/requery/requery.

Now it’s time for the implementation of the entities. Every class that represents one of your models has to contain @Entity annotation - it generates classes able to work with your database.:

Phone class:

@Entity
public interface Phone {  
   @Key
   String getCountryCode();

   @Key
   String getNumber();
}

Location class:

@Entity
public interface Location {  
   @Key
   @Generated
   int getKey();

   double getLatitude();

   double getLongitude();
}

Person entity is in many-to-many relation with Trip entity. To implement it we need an extra table - Participants. It contains a pair of person and trip keys:

@Entity
public interface Participants {  
   @ForeignKey(references = Trip.class)
   @Key
   int getTripId();

   @ForeignKey(references = Person.class)
   @Key
   int getPersonId();
}

Now we have to inform classes to use this entity - if we don’t do it, the app will use an auto-generated class by Requery.

Trip class:

@Entity
public interface Trip {  
   @Key
   @Generated
   int getId();

   Date getStartDate();

   Date getEndDate();

   @ForeignKey
   @ManyToOne
   Location getLocation();

   @ManyToMany
   @JunctionTable(type = Participants.class)
   List<Person> getParticipants();
}

Person class:

@Entity
public interface Person {  
   @Key
   @Generated
   int getId();

   String getFirstName();

   String getLastName();

   @ForeignKey
   @OneToOne(cascade = CascadeAction.DELETE)
   Phone getPhone();

   @ManyToMany
   List<Trip> getTrips();
}

Then the project has to be rebuilt for generating model classes - each of them will get “Entity” to its class name, e.g. Trip -> TripEntity. It’s one of possible solutions - to check other ones please visit Requery home page.

public interface TripperDb {  
   void addTrip(@NonNull TripEntity entity);

   void deleteTrip(int id);

   @Nullable TripEntity getTrip(int id);

   @NonNull List<TripEntity> getAllTrips();

   @NonNull List<TripEntity> getPersonTrips(int userId);

   void deleteAllTrips();
}
public class TripperDbImpl implements TripperDb {  
   private static final int DATABASE_VERSION = 1;

   private static TripperDb sInstance;
   private ReactiveEntityStore<Persistable> mDataStore;

   private TripperDbImpl(@NonNull ReactiveEntityStore<Persistable> data) {
       mDataStore = data;
   }

   private static @NonNull ReactiveEntityStore<Persistable> getDataStore(@NonNull Context context) {
       DatabaseSource source = new DatabaseSource(context, Models.DEFAULT, DATABASE_VERSION);

       if (BuildConfig.DEBUG) {
           source.setTableCreationMode(TableCreationMode.DROP_CREATE);
       }

       Configuration configuration = source.getConfiguration();
       return ReactiveSupport.toReactiveStore(new EntityDataStore<Persistable>(configuration));
   }

   public static @NonNull TripperDb getInstance(@NonNull Context context) {
       if (sInstance == null) {
           sInstance = new TripperDbImpl(getDataStore(context));
       }

       return sInstance;
   }

   @Override
   public @NonNull List<TripEntity> getAllTrips() {
       return mDataStore.select(TripEntity.class)
               .get()
               .toList();
   }

   @Override
   public @NonNull List<TripEntity> getPersonTrips(int userId) {
       PersonEntity personEntity = mDataStore.select(PersonEntity.class)
               .where(PersonEntity.ID.eq(userId))
               .get()
               .firstOrNull();

       return personEntity != null ? personEntity.getTrips() : Collections.<TripEntity>emptyList();
   }

   @Override
   public void deleteAllTrips() {
       mDataStore.delete(TripEntity.class)
               .get()
               .single()
               .subscribe(/*no-op*/);
   }

   @Override
   public void addTrip(@NonNull TripEntity entity) {
       mDataStore.upsert(entity)
               .subscribe(/*no-op*/);
   }

   @Override
   public void deleteTrip(int id) {
       mDataStore.delete(TripEntity.class)
               .where(TripEntity.ID.eq(id))
               .get()
               .single()
               .subscribe(/*no-op*/);
   }

   @Override
   public @Nullable TripEntity getTrip(int id) {
       return mDataStore.select(TripEntity.class)
               .where(TripEntity.ID.eq(id))
               .get()
               .firstOrNull();
   }
}

Now we are able to fill the database. Here are standard operations on Trip entity used in Activity class:

public class MainActivity extends AppCompatActivity {  
   private TripperDb mDatabase;

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       mDatabase = TripperDbImpl.getInstance(this);
   }

   private void addNewTrip(double destinationLat, double destinationLon, @NonNull Date startDate, @NonNull Date endDate) {
       LocationEntity locationEntity = new LocationEntity();
       locationEntity.setLatitude(destinationLat);
       locationEntity.setLongitude(destinationLon);

       TripEntity tripEntity = new TripEntity();
       tripEntity.setStartDate(startDate);
       tripEntity.setEndDate(endDate);
       tripEntity.setLocation(locationEntity);

       mDatabase.addTrip(tripEntity);
   }

   private void deleteTrip(int id) {
       mDatabase.deleteTrip(id);
   }

   private void addPersonToTrip(@NonNull List<PersonEntity> personEntities, int tripId) {
       TripEntity tripEntity = mDatabase.getTrip(tripId);

       if (tripEntity != null) {
           tripEntity.getParticipants().addAll(personEntities);
           mDatabase.addTrip(tripEntity);
       }
   }

   private @NonNull List<TripEntity> getAllTrips() {
       return mDatabase.getAllTrips();
   }

   private void deleteAllTrips() {
       mDatabase.deleteAllTrips();
   }
}

There are absolutely no limits - we can insert, update or delete every element in the database.

Requery supports RxJava, what is definitely one of the biggest advantages of this library. Also, you don’t need to spend a lot of time for learning this solution - everything is intuitive and simple. remember to pay attention to your database version though - after every change in your schema you have to increase this number to update the whole database - migration is done automatically. Based on this example you are able to create a more complex databases - you know how to build every relation in database, so nothing will surprise you...