Skip to content

Contentful Vault Library

Join Contentful Community Slack   Join Contentful Community Forum

vault - Contentful Offline Persistence for Android

Vault is an Android library that simplifies persisting data from Contentful to SQLite. It defines a Java representation of your Contentful models, and at compile-time generates the corresponding database schema plus all the required boilerplate. It ships with a complementary lightweight runtime that exposes a simple ORM-like API for pulling resources back out of the generated database.

This repository is actively maintained   Apache 2.0 License   Build Status

What is Contentful?

Contentful provides content infrastructure for digital teams to power websites, apps, and devices. Unlike a CMS, Contentful was built to integrate with the modern software stack. It offers a central hub for structured content, powerful management and delivery APIs, and a customizable web app that enable developers and content creators to ship their products faster.

Table of contents

Core Features

  • Defines a Java representation of your Contentful content model as plain classes annotated with @ContentType and @Field.
  • Generates the corresponding SQLite schema and all persistence boilerplate at compile time via an annotation processor — no manual SQLiteOpenHelper code required.
  • Keeps your local database in sync with Contentful using the Sync API, fetching only what changed since the last call.
  • A simple, ORM-like query API (.fetch(Type.class).where(…).first() / .all()) for reading persisted resources back out.
  • RxJava support: .observe(Type.class) returns an Observable for reactive queries, and Vault.observeSyncResults() reports sync completion reactively.
  • Multi-locale support: declare which locales to persist per @Space, and query resources by locale.
  • Schema migrations via a simple dbVersion bump on your @Space annotation.
  • Preseeding support: ship a pre-built SQLite database in your APK to avoid the cost of an initial sync on first launch.

Getting started

Requirements

Requirement Version
Java 8 or higher
Android Support depends on the contentful.java SDK version in use

Vault depends on the contentful.java SDK for all network access to the Content Delivery API.

Installation

  • Maven
<dependency>
  <groupId>com.contentful.vault</groupId>
  <artifactId>compiler</artifactId>
  <version>3.2.12</version>
</dependency>
<dependency>
  <groupId>com.contentful.vault</groupId>
  <artifactId>core</artifactId>
  <version>3.2.12</version>
</dependency>
  • Gradle
annotationProcessor 'com.contentful.vault:compiler:3.2.12'
implementation 'com.contentful.vault:core:3.2.12'

Note: With 3.2.12 and older, also add annotationProcessor 'com.contentful.vault:core:<version>' if the build fails with NoClassDefFoundError: com/contentful/vault/ContentType.

Note: Avoid 3.2.11. It was published from an older branch and is missing SyncConfig.Builder#setLimit and #setSingleLocale (added in 3.2.9). Use 3.2.12 or newer.

Your first sync

Define a model, group it into a @Space, then request a sync:

@ContentType("cat")
public class Cat extends Resource {
  @Field public String name;
}

@Space(
    value = "cfexampleapi",
    models = { Cat.class },
    locales = { "en-US" }
)
public class DemoSpace { }
CDAClient client = CDAClient.builder()
    .setSpace("cfexampleapi")
    .setToken("b4c0n73n7fu1")
    .build();

Vault.with(context, DemoSpace.class).requestSync(SyncConfig.builder().setClient(client).build());

Vault runs the sync on a worker thread and reflects the changes in its local database. Once complete, it broadcasts Vault.ACTION_SYNC_COMPLETE.

Using the SDK

Models and fields

Models are defined by declaring a subclass of Resource. Annotate the class with @ContentType, passing the Content Type's ID as its value.

Fields are defined by annotating class attributes with @Field:

@ContentType("cat")
public class Cat extends Resource {
  @Field public String name;
  @Field public Cat bestFriend;
  @Field public Asset image;
}

By default, the attribute name is used as the field's id, but it can also be specified explicitly:

@Field("field-id-goes-here")
public String someField;

Field ids are escaped automatically, but when writing a WHERE condition it's up to the caller to escape field names that collide with reserved SQL keywords:

@ContentType("...")
public class Foo extends Resource {
  @Field public String order;
}

Since order is a reserved SQLite keyword, a query referencing that field looks like this:

vault.fetch(Foo.class)
    .where("`" + Foo$Fields.ORDER + "` = ?", "bar")
    .first();

Spaces

Spaces are classes annotated with @Space. Specify the Space ID, an array of model classes, and an array of locale codes to persist:

@Space(
    value = "cfexampleapi",
    models = { Cat.class },
    locales = { "en-US", "tlh" }
)
public class DemoSpace { }

Synchronization

Once a Space is defined, invoke Vault to synchronize the local database with Contentful:

// Client.
CDAClient client = CDAClient.builder()
    .setSpace("cfexampleapi")
    .setToken("b4c0n73n7fu1")
    .build();

// Sync.
Vault.with(context, DemoSpace.class).requestSync(SyncConfig.builder().setClient(client).build());

Vault uses a worker thread to request updates from the Sync API and reflect the changes in its database. Once sync completes, Vault broadcasts Vault.ACTION_SYNC_COMPLETE.

Providing a SyncCallback invokes it once sync completes:

class SomeActivity extends Activity {
  SyncCallback callback;

  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    Vault.with(this, DemoSpace.class).requestSync(SyncConfig.builder().setClient(client).build(), callback = new SyncCallback() {
      @Override public void onResult(SyncResult result) {
        if (result.isSuccessful()) {
          // Success \o/
        } else {
          // Failure
        }
      }
    });
  }

  @Override protected void onDestroy() {
    Vault.cancel(callback);
    super.onDestroy();
  }
}

Note: Extra care needs to be taken for the lifecycle — cancel the callback on lifecycle events.

Sync options

SyncConfig takes either a CDAClient, or an access token and a space ID:

SyncConfig config = SyncConfig.builder()
    .setAccessToken("b4c0n73n7fu1")
    .setSpaceId("cfexampleapi")
    .setEnvironment("master")   // optional, defaults to "master"
    .setLimit(500)              // optional: page size of the initial sync, 1-1000
    .setSingleLocale(true)      // optional: store only the space's default locale
    .setInvalidate(false)       // optional: true wipes local data and syncs from scratch
    .build();

Vault.with(context, DemoSpace.class).requestSync(config);
  • setLimit only applies to the first page of an initial sync. Later pages and delta syncs use the API default. Values outside 1-1000 throw IllegalArgumentException.
  • setSingleLocale(true) stores only the default locale, and queries for any other locale return no results. Changing this setting, or opening an older database whose locale mode is unknown, makes the next sync replace all data with a fresh initial sync. Existing offline data remains available if fetching or saving the replacement fails. The locale mode and sync token are stored in the same database transaction.
  • setInvalidate(true) replaces local data with a fresh initial sync. The old data is only removed once the new data has been downloaded, so a failed sync keeps it.

To close a space's database connection, call vault.release(). It only affects that space; Vault.releaseAll() is deprecated because it closes every space. Afterwards, get a new instance with Vault.with(...).

RxJava users can subscribe to sync results via an Observable:

Vault.observeSyncResults() // Returns Observable<SyncResult>.

Queries

Vault wraps its generated database with a query API for fetching persisted objects:

Vault vault = Vault.with(context, DemoSpace.class);

// Fetch the first Cat.
vault.fetch(Cat.class).first();

// Fetch the most recently updated Cat.
vault.fetch(Cat.class)
    .order(Cat$Fields.UPDATED_AT + " DESC")
    .first();

// Fetch a Cat with a specific name.
vault.fetch(Cat.class)
    .where(Cat$Fields.NAME + " = ?", "Nyan Cat")
    .first();

// Fetch a Cat with a specific name pattern.
vault.fetch(Cat.class)
    .where(Cat$Fields.NAME + " LIKE ?", "%Nyan%")
    .first();

// Fetch a Cat with a specific boolean field.
// SQLite stores booleans as 0/1.
vault.fetch(Cat.class)
    .where(Cat$Fields.IS_GRUMPY + " = ?", "1")
    .first();

// Fetch all Cats, ordered by creation date.
vault.fetch(Cat.class)
    .order(Cat$Fields.CREATED_AT)
    .all();

// Fetch all Cats, using the Klingon locale.
vault.fetch(Cat.class)
    .all("tlh");

RxJava queries are created via .observe():

vault.observe(Cat.class)
    .where(Cat$Fields.NAME + " = ?", "Happy Cat")
    .all() // Returns Observable<Cat>.

The example above creates an Observable that subscribes and observes on the thread that initiated the query. Chain .subscribeOn(…)/.observeOn(…) if you need a different threading model:

vault.observe(Cat.class)
    .all()
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())

Migrations

Whenever a previously used model changes, apply a migration by incrementing the version number on your @Space:

@Space(value = "cfexampleapi", models = { Cat.class }, dbVersion = 2)
public class DemoSpace { }

Note: this deletes any previously persisted data and re-acquires it on the next sync.

Preseeding

Depending on the amount of content in a given space, initial synchronization can take some time. To avoid that cost, pre-seed the database with static content instead.

Use VaultDatabaseExporter to create the initial database file. It takes an Android Context and a Vault Space; calling .export(…) creates a SQLite database at src/main/assets/initial_seed.db. Point Vault at it as follows:

@Space(
    value = "{spaceid}",        // Space id of the space to use.
    models = { Cat.class },     // Model classes to be used.
    copyPath = "initial_seed.db" // Name of the just-created database file.
)
public class VaultSpace { }

Keep the bundled database up to date by running a Robolectric test before each release, which syncs live Contentful data into the existing database file:

@RunWith(RobolectricTestRunner.class)
public class TestSeedDB {
  @Test
  public void testSyncDBtoSqlite() throws Exception {
    final Activity activity = Robolectric.setupActivity(Activity.class);

    assertTrue(new VaultDatabaseExporter().export(activity, VaultSpace.class, VaultSpace.TOKEN));
  }
}

If this test fails, that indicates the bundled database content is out of date; follow the guidance in the failure message.

Note: To add preseeding to an already-shipped app, increment dbVersion — this invalidates any pre-existing content on device so the new bundled database takes effect.

Advanced configuration

Proguard

Grab the ProGuard configuration file and apply it to your project.

Documentation & References

Browse the Javadoc for the full API reference of this library. Every released change is recorded in the CHANGELOG.md.

Vault is built on top of the contentful.java SDK; consult its README for details on CDAClient and the underlying Content Delivery API.

Reach out to us

Have questions about how to use this library?

  • Reach out to our community forum: Contentful Community Forum
  • Jump into our community slack channel: Contentful Community Slack

You found a bug or want to propose a feature?

  • File an issue here on GitHub: File an issue. Make sure to remove any credential from your code before sharing it.

You need to share confidential information or have other questions?

  • File a support ticket at our Contentful Customer Support: File support ticket

Get involved

PRs Welcome

We appreciate any help on our repositories. Feel free to open a pull request or an issue.

License

Copyright 2017 Contentful, GmbH.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Code of Conduct

We want to provide a safe, inclusive, welcoming, and harassment-free space and experience for all participants, regardless of gender identity and expression, sexual orientation, disability, physical appearance, socioeconomic status, body size, ethnicity, nationality, level of experience, age, religion (or lack thereof), or other identity markers.

Read our full Code of Conduct.

About

Easy persistence of Contentful data for Android over SQLite.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

88 stars

Watchers

12 watching

Forks

Releases

Packages

Contributors

Languages