Saving Objects Transparently in Android

Automagically save your objects

Hakan Eryargi
The Startup
6 min readMay 19, 2020

--

Introduction

I will show you a new and innovative way of saving your objects in Android.

We will use neither SQLite, nor Room, nor any DAO or SharedPreferences. Our data objects will be automagically and transparently persisted!

We will use Chainvayler for that. Chainvayler is a new and innovative way of persisting and replicating POJO (Plain Old Java Object) graphs transparently. We will use Chainvayler’s persistence capabilities.

See this blog post for details about Chainvayler.

Lets do it!

This demo application is published at Google Play. The complete source code can be found here.

Chainvayler is not in central Maven repository. So first clone the Chainvayler repo from this link.

And run this command in the Chainvayler/chainvayler folder:

./gradlew clean build publishToMavenLocal

Now create a new project in Android Studio and add Chainvayler dependencies to build.gradle

implementation 'raft.chainvayler:chainvayler:0.1'
implementation 'org.prevayler:prevayler-core:2.6'
implementation 'org.javassist:javassist:3.26.0-GA'

Now we can start writing our data classes. First the Library class:

@Chained
public class Library implements Serializable {

private static final long serialVersionUID = 1L;

private final Map<Integer, Book> books = new HashMap<>();
private final Map<Integer, Author> authors = new HashMap<>();

private int lastId = 1;

private boolean populated = false;

public Book getBook(int id) {
return books.get(id);
}

@Modification
public void addBook(Book book) {
book.setId(lastId++);
books.put(book.getId(), book);
}

@Modification
public void removeBook(Book book) {
books.remove(book.getId());
}

public Collection<Book> getBooks() {
return Collections.unmodifiableCollection(books.values());
}

@Modification
public void removeAllBooks() {
books.clear();
authors.values().forEach(author -> author.removeAllBooks());
}

public Author getAuthor(int id) {
return authors.get(id);
}

@Modification
public void addAuthor(Author author) {
author.setId(lastId++);
authors.put(author.getId(), author);
}

@Modification
public void removeAuthor(Author author) {
authors.remove(author.getId());
}

public Collection<Author> getAuthors() {
return Collections.unmodifiableCollection(authors.values());
}

public Author findAuthorByName(String name) {
return authors.values().stream()
.filter(author -> name.equalsIgnoreCase(author.getName()))
.findAny()
.orElse(null);
}

@Modification
public void removeAllAuthors() {
authors.clear();
books.values().forEach(book -> book.setAuthor(null));
}

public Set<String> getGenres() {
Set<String> genres = new TreeSet<>();
books.values().forEach(book -> genres.addAll(book.getGenres()));
return genres;
}

public Set<String> getCountries() {
Set<String> genres = new TreeSet<>();
authors.values().forEach(author -> {
String country = author.getCountry();
if ((country != null) && !country.trim().isEmpty())
genres.add(author.getCountry());
});
return genres;
}

public boolean isPopulated() {
return populated;
}

@Modification
public void setPopulated() {
populated = true;
}
}

As can be seen this is quite a POJO class, nothing special.

@Chained annotation marks the classes which will be managed by Chainvayler.

@Modification annotation marks the methods which modify the data in @Chained classes. All methods which modify the data should be marked with @Modification annotation and they should be deterministic.

Next lets write the Book class:

@Chained
public class Book implements Serializable {

private static final long serialVersionUID = 1L;

private int id;
private String name;
private Author author;
private boolean read = false;
private boolean favorite = false;
private String notes = "";
private Set<String> genres = new HashSet<>();

public Book(String name) {
this.name = name;
}

public Book(String name, Author author) {
this(name);
setAuthor(author);
}

public int getId() {
return id;
}

@Modification
void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

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

public Set<String> getGenres() {
return Collections.unmodifiableSet(genres);
}

@Modification
public void addGenre(String genre) {
genres.add(genre);
}

@Modification
public void setGenres(Collection<String> genres) {
this.genres.clear();
this.genres.addAll(genres);
}

@Modification
public void removeGenre(String genre) {
genres.remove(genre);
}


public Author getAuthor() {
return author;
}

@Modification
public void setAuthor(Author author) {
if (this.author != null) {
this.author.removeBook(this);
}
this.author = author;
if (author != null) {
author.addBook(this);
}
}

public boolean isRead() {
return read;
}

@Modification
public void setRead(boolean read) {
this.read = read;
}

public boolean isFavorite() {
return favorite;
}

@Modification
public void setFavorite(boolean favorite) {
this.favorite = favorite;
}

public String getNotes() {
return notes;
}

@Modification
public void setNotes(String notes) {
this.notes = notes;
}

@Override
public String toString() {
return "Book:" + name;
}
}

And finally the Author class:

@Chained
public class Author implements Serializable {

private static final long serialVersionUID = 1L;

private int id;
private String name;
private String country;
private Date birth;
private Date death;
private Set<Book> books = new HashSet<>();

public Author(String name) {
this.name = name;
}

public int getId() {
return id;
}

@Modification
void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

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

public Set<Book> getBooks() {
return Collections.unmodifiableSet(books);
}

@Modification
void addBook(Book book) {
books.add(book);
}

@Modification
void removeBook(Book book) {
books.remove(book);
}

@Modification
void removeAllBooks() {
books.clear();
}

public String getCountry() {
return country;
}

@Modification
public void setCountry(String country) {
this.country = country;
}

public Date getBirth() {
return birth;
}

@Modification
public void setBirth(Date birth) {
this.birth = birth;
}

public Date getDeath() {
return death;
}

@Modification
public void setDeath(Date death) {
this.death = death;
}

@Override
public String toString() {
return "Author:" + name;
}
}

How shall we access our Library?

Lets create the LibraryApplication class:

public class LibraryApplication extends Application {

private Library library;
@Override
public void onCreate() {
super.onCreate();

String persistDir = getApplicationContext().getFilesDir().getAbsolutePath() + "/persist";
try {
this.library = Chainvayler.create(Library.class, persistDir);
if (!library.isPopulated()) {
populate();
library.setPopulated();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public Library getLibrary() {
return library;
}

private void populate() throws Exception {
// create some initial books and authors
library.addAuthor(new Author("Jorge Luis Borges"));
library.addAuthor(new Author("Jack London"));

library.addBook(new Book("Call of the Wild", library.findAuthorByName("Jack London")));
library.addBook(new Book("The Book of Sand", library.findAuthorByName("Jorge Luis Borges")));
}
}

And modify AdnroidManifest.xml to use our LibraryApplication class:

<application
android:name=".LibraryApplication"

In your Activity classes, just cast the application to LibraryApplication and get your Library instance.

public class BookListActivity extends AppCompatActivity {

private Library library;
@Override
protected void onCreate(Bundle savedInstanceState) {
// ..
LibraryApplication application = (LibraryApplication) getApplication();
this.library = application.getLibrary();
}
}

Almost there. Chainvayler does its magic by instrumentation (injecting bytecode) of Java classes. So we need to invoke Chainvayler compiler during our build process.

Add these lines to build.gradle:

afterEvaluate {
android.applicationVariants.each { variant ->
variant.javaCompileProvider.configure {
def
javaPath = it.classpath + files(it.destinationDir)

// below line is required only if Android specific classes (Log etc.) are used inside @Chained classes
// javaPath += files(android.sdkDirectory.absolutePath + '/platforms/' + android.compileSdkVersion + '/android.jar')

it.doLast {
javaexec {
classpath += javaPath
main = 'raft.chainvayler.compiler.Compiler' // Chainvayler compiler
args 'raft.chainvayler.samples.android.data.Library' // Root class of @Chained graph
jvmArgs = ['-ea']
// enable assertions, generally a good idea especially at build/debug time
}
}
}
}
}

Next step is optional but a good idea. Put these lines to your LibraryApplication class:

/** Take a snapshot of @Chained graph after at least this much transactions.
* This is to accelerate application startup next time. */
private static final int SNAPSHOT_TRANSACTIONS = 100;
/** Maybe take a snapshot of @Chained graph, to accelerate app launch time next time */
public void maybeTakeSnapshot() {
try {
if (Chainvayler.getTransactionCount() > Chainvayler.getLastSnapshotVersion() + SNAPSHOT_TRANSACTIONS) {
Chainvayler.takeSnapshot();
Chainvayler.deleteRedundantFiles();
}
} catch (Exception e) {
Log.e(LOG_TAG, "Failed to take snapshot", e);
}
}

And invoke inonStop method of your main activity:

@Override
protected void onStop() {
super.onStop();
((LibraryApplication) getApplication()).maybeTakeSnapshot();
}

Behind the scenes, Chainvayler writes transaction classes to disk for each call to @Modification methods, and invokes the methods with the exact same order and arguments at startup time. Taking a snapshot just accelerates this process.

Done! We are good to go! The rest is basically UI programming.

Limitations

All of your @Chained objects are always in memory. This provides you lightning fast read performance but at the same time implies your objects will fit into memory.

For most of the applications this will not be an issue, possibly tens of thousands of objects will be just fine.

But of course, if your application just keeps creating @Chained objects like there is no tomorrow, then Chainvayler is not suitable for you.

BTW, Garbage Collection works as expected with @Chained classes.

Conclusion

As promised, we used neither SQLite, nor Room, nor any DAO or SharedPreferences. Our data objects are just automagically and transparently persisted!

So, cheers and happy persistence in Android :)

--

--

Hakan Eryargi
The Startup

Hakan is an inventor, engineer and technology architect with a wide range of experience even including 3D games. He is also a fan of KISS principle.