pushtorefresh / storio

Reactive API for SQLiteDatabase and ContentResolver.
Apache License 2.0
2.55k stars 182 forks source link
android rxjava sqlite storio

StorIO — modern API for SQLiteDatabase and ContentResolver

Overview:

Why StorIO?

Documentation:

Easy ways to learn how to use StorIO -> check out Documentation, Design Tests and Sample App:

Download:

// If you need StorIO for SQLite
implementation 'com.pushtorefresh.storio3:sqlite:3.0.1'

// If you need StorIO for ContentResolver
implementation 'com.pushtorefresh.storio3:content-resolver:3.0.1'

// Notice that RxJava is optional dependency for StorIO,
// So if you need it -> please add it manually.

You can find all releases on Maven Central.

Some examples

Get list of objects from SQLiteDatabase
List<Tweet> tweets = storIOSQLite
  .get()
  .listOfObjects(Tweet.class) // Type safety
  .withQuery(Query.builder() // Query builder
    .table("tweets")
    .where("author = ?")
    .whereArgs("artem_zin") // Varargs Object..., no more new String[] {"I", "am", "tired", "of", "this", "shit"}
    .build()) // Query is immutable — you can save it and share without worries
  .prepare() // Operation builder
  .executeAsBlocking(); // Control flow is readable from top to bottom, just like with RxJava
Put something to SQLiteDatabase
storIOSQLite
  .put() // Insert or Update
  .objects(someTweets) // Type mapping!
  .prepare()
  .executeAsBlocking();
Delete something from SQLiteDatabase
storIOSQLite
  .delete()
  .byQuery(DeleteQuery.builder()
    .table("tweets")
    .where("timestamp <= ?")
    .whereArgs(System.currentTimeMillis() - 86400) // No need to write String.valueOf()
    .build())
  .prepare()
  .executeAsBlocking();

Reactive? Single.just(true)!

Get something as io.reactivex.Flowable and receive updates!
storIOSQLite
  .get()
  .listOfObjects(Tweet.class)
  .withQuery(Query.builder()
    .table("tweets")
    .build())
  .prepare()
  .asRxFlowable(BackpressureStrategy.LATEST) // Get Result as io.reactivex.Flowable and subscribe to further updates of tables from Query!
  .observeOn(mainThread()) // All Rx operations work on Schedulers.io()
  .subscribe(tweets -> { // Please don't forget to dispose
      // Will be called with first result and then after each change of tables from Query
      // Several changes in transaction -> one notification
      adapter.setData(tweets);
    }
  );
Want to work with plain Cursor, no problems
Cursor cursor = storIOSQLite
  .get()
  .cursor()
  .withQuery(Query.builder() // Or RawQuery
    .table("tweets")
    .where("who_cares = ?")
    .whereArgs("nobody")
    .build())
  .prepare()
  .executeAsBlocking();

How object mapping works?

You can set default type mappings when you build instance of StorIOSQLite or StorIOContentResolver
StorIOSQLite storIOSQLite = DefaultStorIOSQLite.builder()
  .sqliteOpenHelper(someSQLiteOpenHelper)
  .addTypeMapping(Tweet.class, SQLiteTypeMapping.<Tweet>builder()
    .putResolver(new TweetPutResolver()) // object that knows how to perform Put Operation (insert or update)
    .getResolver(new TweetGetResolver()) // object that knows how to perform Get Operation
    .deleteResolver(new TweetDeleteResolver())  // object that knows how to perform Delete Operation
    .build())
  .addTypeMapping(...)
  // other options
  .build(); // This instance of StorIOSQLite will know how to work with Tweet objects

You can override Operation Resolver per each individual Operation, it can be useful for working with SQL JOIN.


To save you from coding boilerplate classes we created Annotation Processor which will generate PutResolver, GetResolver and DeleteResolver at compile time, you just need to use generated classes

Notice that annotation processors are not part of the library core, you can work with StorIO without them, we just made them to save you from boilerplate.

StorIOSQLite:

dependencies {
  implementation 'com.pushtorefresh.storio3:sqlite-annotations:insert-latest-version-here'

  annotationProcessor 'com.pushtorefresh.storio3:sqlite-annotations-processor:insert-latest-version-here'
}

StorIOContentResolver:

dependencies {
  implementation 'com.pushtorefresh.storio3:content-resolver-annotations:insert-latest-version-here'

  annotationProcessor 'com.pushtorefresh.storio3:content-resolver-annotations-processor:insert-latest-version-here'
}
@StorIOSQLiteType(table = "tweets")
public class Tweet {

  // Annotated fields should have package-level visibility.
  @StorIOSQLiteColumn(name = "author")
  String author;

  @StorIOSQLiteColumn(name = "content")
  String content;

  // Please leave default constructor with package-level visibility.
  Tweet() {}
}

Kotlin:

In order to make annotation processors work with Kotlin you need to add the following to your build.gradle:

apply plugin: 'kotlin-kapt'

Then use kapt configuration instead of annotationProcessor.

 @StorIOSQLiteType(table = "tweets")
 data class Tweet @StorIOSQLiteCreator constructor(
        StorIOSQLiteColumn(name = "author") val author: String,
        StorIOSQLiteColumn(name = "content") val content: String)

AutoValue:

@AutoValue
@StorIOSQLiteType(table = "tweets")
public abstract class Tweet {

  // Annotated methods should have package-level or public visibility.
  @StorIOSQLiteColumn(name = "author")
  abstract String author();

  @StorIOSQLiteColumn(name = "content")
  abstract String content();

  // Parameters order depends on declaration order.
  @StorIOSQLiteCreator
  static Tweet create(String author, String content) {
    return new AutoValue_Tweet(author, content);
  }
}

Annotation Processor will generate three classes in same package as annotated class during compilation:

You just need to apply them:

StorIOSQLite storIOSQLite = DefaultStorIOSQLite.builder()
  .sqliteOpenHelper(someSQLiteOpenHelper)
  .addTypeMapping(Tweet.class, SQLiteTypeMapping.<Tweet>builder()
    .putResolver(new TweetStorIOSQLitePutResolver()) // object that knows how to perform Put Operation (insert or update)
    .getResolver(new TweetStorIOSQLiteGetResolver()) // object that knows how to perform Get Operation
    .deleteResolver(new TweetStorIOSQLiteDeleteResolver())  // object that knows how to perform Delete Operation
    .build())
  .addTypeMapping(...)
  // other options
  .build(); // This instance of StorIOSQLite will know how to work with Tweet objects

BTW: Here is a class with all types of fields, supported by StorIO SQLite Annotation Processor.

Few tips about Operation Resolvers:

API of StorIOContentResolver is same.


Versioning:

Because StorIO works with important things like User data and so on, we use Semantic Versioning 2.0.0 scheme for releases (http://semver.org).

Short example: 1.2.3 -> MAJOR.MINOR.PATCH

Please read CHANGELOG and check what part of the version has changed, before switching to new version.

Architecture:

StorIOSQLite and StorIOContentResolver — are abstractions with default implementations: DefaultStorIOSQLite and DefaultStorIOContentResolver.

It means, that you can have your own implementation of StorIOSQLite and StorIOContentResolver with custom behavior, such as memory caching, verbose logging and so on or mock implementation for unit testing (we are working on MockStorIO).

One of the main goals of StorIO — clean API for Humans which will be easy to use and understand, that's why StorIOSQLite and StorIOContentResolver have just several methods, but we understand that sometimes you need to go under the hood and StorIO allows you to do it: StorIOSQLite.LowLevel and StorIOContentResolver.LowLevel encapsulates low-level methods, you can use them if you need, but please try to avoid it.

Queries

All Query objects are immutable, you can share them safely.

Concept of Prepared Operations

You may notice that each Operation (Get, Put, Delete) should be prepared with prepare(). StorIO has an entity called PreparedOperation, and you can use them to perform group execution of several Prepared Operations or provide PreparedOperation as a return type of your API (for example in Model layer) and client will decide how to execute it: executeAsBlocking() or asRxFlowable(). Also, Prepared Operations might be useful for ORMs based on StorIO.

You can customize behavior of every Operation via Resolvers: GetResolver, PutResolver, DeleteResolver.

Rx Support Design

Every Operation can be executed as io.reactivex.Flowable, io.reactivex.Single, io.reactivex.Completable or io.reactivex.Maybe. Get Operations will be automatically subscribed to the updates of the data. Every rx operation runs on Schedulers.io(). You can change it by defaultRxScheduler() or set it to null to execute on current thread.

3rd party additions/integrations for StorIO


Master branch build status: Master branch build status

Made with love in Pushtorefresh.com by @artem_zin, @nikitin-da and @geralt-encore