Orma


Source link: https://github.com/gfx/Android-Orma

Android Orma

Orma is a ORM (Object-Relation Mapper) for Android SQLiteDatabase. Because it generates helper classes at compile time with annotation processing, its query builders are type-safe.

The interface of Orma is simple and easy to use, as the author respects the Larry Wall's wisdom:

Easy things should be easy, and hard things should be possible -- Larry Wall

Table of Contents

Motivation

There are already a lot of ORMs for Android. Why I have to add another wheel?

The answer is that I need an ORM that has all the following features:

  • Fast as hand-written code
  • POJO models
    • Model classes should have no restriction
    • Might implement Parcelable and/or extend any classes
    • They should be passed to another thread
  • A database handle must be an object instance
    • Not a static-method based class
    • Even though it is designed to be used as a singleton scope
  • Easy migration
    • Some ALTER TABLE, e.g. add column and drop column, should be detected and processed
    • There is a wheel in Perl: SQL::Translator::Diff
  • Type safe and code completion friendly
    • db.selectFromModel() is better than new Select(Model.class)
    • todos.idEq(id).toList() is better than todos.equalTo("id", id)
  • Custom raw queries are sometimes inevitable
    • GROUP BY ... HAVING ...
    • SELECT max(value), min(value), avg(value), count(value) FROM ...

And now they are exactly what Orma has.

Requirements

  • JDK 8 (1.8.0_66 or later) to build
  • Android API level 15 to use

Getting Started

Declare dependencies to use Orma and its annotation processor.

dependencies {

  annotationProcessor 'com.github.gfx.android.orma:orma-processor:4.2.5'
  compile 'com.github.gfx.android.orma:orma:4.2.5' 
}

NOTE: if you use Android Gradle Plugin before 2.2.0, you must use android-apt plugin instead of annotationProcessor configuration.

Synopsis

First, define model classes annotated with @Table, @Column, and @PrimaryKey and run the Build APK command to generate helper classes.

package com.github.gfx.android.orma.example;  import com.github.gfx.android.orma.annotation.Column; import com.github.gfx.android.orma.annotation.PrimaryKey; import com.github.gfx.android.orma.annotation.Table;  import android.support.annotation.Nullable;  @Table public class Todo {

@PrimaryKey(autoincrement = true)
  public long id;

@Column(indexed = true)
  public String title;

@Column
  @Nullable // allows NULL (default: NOT NULL)
  public String content;

@Column
  public long createdTimeMillis; 
}

Second, instantiate a database handle OrmaDatabase, which is generated by orma-processor.

Here is an example to configure OrmaDatabase:

// See OrmaDatabaseBuilderBase for other options. OrmaDatabase orma = OrmaDatabase.builder(context)
  .name("main.db") // default: "${
applicationId
}
.orma.db"
  .build();

Then, you can create, read, update and delete models via OrmaDatabase:

Todo todo = ...;  // create orma.insertIntoTodo(todo);
  // prepared statements with transaction orma.transactionSync( -> {
 // or transactionAsync() to execute tasks in background
  Inserter<Todo> inserter = orma.prepareInsertIntoTodo();

  inserter.execute(todo);
 
}
);
  // read orma.selectFromTodo()
.titleEq("foo") // equivalent to `where("title = ?", "foo")`
.executeAsObservable() // first-class RxJava interface
.subscribe(...);
  // update orma.updateTodo()
.titleEq("foo")
.content("a new content") // to setup what are updated
.execute();
  // delete orma.deleteFromTodo()
.titleEq("foo")
.execute();

The Components

Database Handles

A database handle, named OrmaDatabase by default, is generated by orma-processor, which is an entry point of all the high-level database operations.

This is typically used as a singleton instance and you don't need to manage its lifecycle. That is, you don't need to explicitly close it.

Models

A model in Orma is a Java class that is annotated with @Table, which has at least one column, a field annotated with @Column or @PrimaryKey.

orma-processor generates helper classes for each model: Schema, Relation, Selector, Updater, and Deleter.

Because these helper classes are generated at the compile time, you can use Orma as a type-safe ORM.

Schema Helpers

A Schema helper, e.g. Todo_Schema, has metadata for the corresponding model.

This is an internal helper class and not intended to be employed by users.

Relation Helpers

A Relation helper, e.g. Todo_Relation, is an entry point of table operations.

This is created by a database handle:

public static Todo_Relation relation() {

return orma.relationOfTodo();
 
}

And is able to create Selector, Updater, Deleter, and Inserter for the target model.

Todo_Relation todos = orma.relationOfTodo();
  todos.selector().toList();
 // Todo_Selector todos.updater().content("foo").execute();
 // Todo_Updater todos.inserter().execute(todo);
 // Inserter<Todo> todos.deleter().execute();
 // Todo_Deleter

This can be a subset of a table which has ORDER BY clauses and WHERE clauses with some List-like methods:

Todo_Relation todos = orma.relationOfTodo()
.doneEq(false) // can have conditions
.orderByCreatedTimeMillis();
 // can have orders  // List-like features: int count = todos.count();
 Todo todo = todos.get(0);
  // Convenience utilities int position = todos.indexOf(todo);
 todos.deleteWithTransactionAsObservable()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(position -> {

  notifyItemRemoved(position);
 // assumes Adapter#notifyItemRemoved()

}
) todos.truncateWithTransactionAsObservable()
.subscribeOn(Schedulers.io())
.subscribe();
  // Todo_Relation implements Iterable<Todo> for (Todo todo : todos) {

// ... 
}

And has convenience #upsert() to "save it anyway", returning a new model:

Todo_Relation todos = orma.relationOfTodo()  Todo newTodo = todos.upsert(todo);
 // INSERT if it's not persistent; UPDATE Otherwise

Unlike INSERT with OnConflict.REPLACE, #upsert() doesn't break associations.

NOTE: if you use a model after #upsert(), you must use the returned newModel. This is because Orma does not change the model's primary key on INSERT.

Selector Helpers

A Selector helper, e.g. Todo_Selector, is created by a Relation:

Todo_Selector selector = relation().selector();
 // or orma.selectFromTodo();

This is a query builder for SELECT ... FROM * statements.

Updater Helpers

An Updater helper, e.g. Todo_Updater, is created by a Relation:

Todo_Updater updater = relation().updater();
 // or orma.updateTodo();

This is a query builder for UPDATE * statements.

Deleter Helpers

A Deleter helper, e.g. Todo_Deleter, is created by a Relation:

Todo_Deleter deleter = relation().deleter();
 // or orma.deleteFromTodo();

This is a query builder for DELETE FROM * statements.

Query Helper Methods

There are Query Helpers which are generated to query conditions and orders in a type-safe way.

For example, titleEq() shown in the synopsis section, are generated to help make WHERE and ORDER BY clauses, for Relation, Selector, Deleter, and Updater.

Most of them are generated for columns with indexed = true, and some are for @PrimaryKey columns.

List of Query Helper Methods

Here is a list of Query Helpers that are generated for all the indexed columns, where * is a column name pladeholder:

Method SQL
*Eq(value) * = value
*NotEq(value) * <> value
*In(values) * IN (values)
*NotIn(values) * NOT IN (values)

The following are generated for @Nullable columns.

Method SQL
*IsNull() * IS NULL
*IsNotNull() * IS NOT NULL

The following are generated for numeric columns (i.e. byte, short, int, long, float, double, and their corresponding box types)

Method SQL
*Lt(value) * < value
*Le(values) * <= value
*Gt(value) * > value
*Ge(value) * >= value
*Between(a, b) * BETWEEN a AND b

The following are generated for TEXT and not PRIMARY KEY columns.

Method SQL
*Glob(pattern) * GLOB pattern
*NotGlob(pattern) * NOT GLOB pattern
*Like(pattern) * LIKE pattern
*NotLike(pattern) * NOT LIKE pattern

And ORDER BY helpers:

Method SQL
orderBy*Asc() ORDER BY * ASC
orderBy*Desc() ORDER BY * DESC

How to Control Generation of Query Helpers

This is an advanced setting for those who know what they do.

You can control which Query Helpers are generated for a column by @Column(helpers = ...) attribute:

@Column(
  helpers = Column.Helpers.AUTO // default to AUTO )

Here are the definition of options defined in Column.java:

long AUTO = -1; // the default, a smart way long NONE = 0;  long CONDITION_EQ = 0b01; long CONDITION_NOT_EQ = CONDITION_EQ << 1; long CONDITION_IS_NULL = CONDITION_NOT_EQ << 1; long CONDITION_IS_NOT_NULL = CONDITION_IS_NULL << 1; long CONDITION_IN = CONDITION_IS_NOT_NULL << 1; long CONDITION_NOT_IN = CONDITION_IN << 1;  long CONDITION_LT = CONDITION_NOT_IN << 1; long CONDITION_LE = CONDITION_LT << 1; long CONDITION_GT = CONDITION_LE << 1; long CONDITION_GE = CONDITION_GT << 1; long CONDITION_BETWEEN = CONDITION_GE << 1;  long CONDITIONS = CONDITION_EQ | CONDITION_NOT_EQ | CONDITION_IS_NULL | CONDITION_IS_NOT_NULL

| CONDITION_IN | CONDITION_NOT_IN

| CONDITION_LT | CONDITION_LE | CONDITION_GT | CONDITION_GE | CONDITION_BETWEEN;  long ORDER_IN_ASC = CONDITION_BETWEEN << 1; long ORDER_IN_DESC = ORDER_IN_ASC << 1;  long ORDERS = ORDER_IN_ASC | ORDER_IN_DESC;  long ALL = CONDITIONS | ORDERS;

The Inserter Helpers

This is a prepared statement for INSERT INTO ... for bulk insertions.

Inserter<Todo> inserter = relation().inserter();
 // or orma.insertIntoTodo()  inserter.execute(todo);
 inserter.executeAll(todos);

Details of Database Handles

The section describes the details of database handles.

Configuration of Database Handles

The database class is configured by the @Database annotation:

@Database(
  databaseClassName = "OrmaDatabase", // default to "OrmaDatabase"
  includes = {
 /* ... */ 
}
 // Give model classes to handle
  excludes = {
 /* ... */ 
}
 // Give model classes not to handle ) public class DatabaseConfiguration {
 
}

The annotated class is not used for now, but the package is used to place the OrmaDatabase class.

Database Handle Builders

OrmaDatabase.builder(Context) returns a builder isntance, which has configure the database handle instance:

Method Description Default
name(String) The filename of SQLite DB "${ package } .orma.db"
migrationEngine(MigrationEngine) Custom migration engine OrmaMigration
writeAheadLogging(boolean) SQLite WAL flag true
foreignKeys(boolean) SQLite FOREIGN_KEYS flag true
migrationStep(int, ManualStepMigration.Step) A migration step none
trace(boolean) Output executed queries to logcat if true dynamic (*1)
readOnMainThread(AccessThreadConstraint) Check read operation on main thread dynamic (*2)
writeOnMainThread(AccessThreadConstraint) Check write operation on main thread dynaimc (*3)
  • *1 BuildConfig.DEBUG ? true : false
  • *2 BuildConfig.DEBUG ? WARN : NONE
  • *3 BuildConfig.DEBUG ? FATAL : NONE

Note that Orma aborts if writing occurs on main thread in debug build.

Use background threads, e.g. via AsyncTask for writing, or RxJava interfaces with Schedulers.io().

Otherwise you can disable this behavior:

OrmaDatabase orma = OrmaDatabase.builder(context)
  .writeOnMainThread(AccessThreadConstraint.NONE)
  .build();

In-Memory Database

You can create in-memory databases by passing null to OrmaDatabase.Builder#name().

This is useful for testing.

Details of Models

The section describes the details of model definition.

Setters and Getters

Orma can use getters and setters if columns have corresponding methods.

You can also connect getters and setters with @Getter and @Setter respectively, which tells orma-processor to use accessors.

Each accessor name can have a column name in SQLite databases, which is inferred from its method name if omitted.

@Table public class KeyValuePair {

static final String kKey = "Key";

@Column(kKey) // specifies the name
  private String key;

@Column // omits the name
  private String value;

@Getter(kKey)
  public String getKey() {

return key;
  
}

@Setter(kKey)
  public void setKey(String key) {

this.key = key;
  
}

// used as a getter for the "value" column
  // @Getter is optional in this case
  public String getValue() {

return value;
  
}

// used as a setter for the "value" column
  // @Setter is optional in this case
  public void setValue(String value) {

this.value = value;
  
}
 
}

Immutable Models

Immutable models, where all the fields are declared with final, are supported by annotating a constructor with @Setter.

@Table public class KeyValuePair {

@Column
  public final String key;

@Column
  public final String value;

@Setter
  KeyValuePair(String key, String value) {

this.key = key;

this.value = value;
  
}
 
}

It can be declared with custom names:

@Table public class KeyValuePair {

  static final String kKey = "Key";
  static final String kValue = "Value";

@Column(kKey)
  public final String key;

@Column(kValue)
  public final String value;

KeyValuePair(@Setter(kKey) String key, @Setter(kValue) String value) {

this.key = key;

this.value = value;
  
}
 
}

Composite Indexes

There is the indexes parameter that @Table takes in order to create composite indexes (a.k.a. multi-column indexes).

// for CREATE INDEX: @Table(indexes = @Index(value = {
"resourceType", "resourceId"
}
)) public class Entry {

@PrimaryKey
  public long id;

@Column
  public String resourceType;

@Column
  public long resourceId; 
}
// for CREATE UNIQUE INDEX: @Table(
  indexes = @Index(

  value = {
"resourceType", "resourceId"
}
,

  unique = true

) ) public class Entry {

@PrimaryKey
  public long id;

@Column
  public String resourceType;

@Column
  public long resourceId; 
}

Composite indexes generate query helper methods only for == and ORDER BY for helper classes like the following:

  • Selector#resourceTypeAndResourceIdEq(String, long)
  • Selector#orderByResourceTypeAndResourceIdAsc()
  • Selector#orderByResourceTypeAndResourceIdDesc()

You can control generated helpers with the helpers parameter.

See also Query Helper Methods.

Reserved Names

Column names starting $ are reserved for metadata.

Other names, including SQLite keywords, are allowed.

RxJava Integration

RxJava integration provides a set of powerful API to transform, filter, and combine DB rows.

For example, there is a model named Book with @Column(unique = true) String title and you'd like to get a Map<String, Book> where the key is Book#title:

Map<String, Book> map = db.selectFromBook()
  .executeAsObservable()

.toMap(new Function<Book, String>() {

 @Override

 public String apply(Book book) throws Exception {

  return book.title;

 
}

}
).blockingGet();

Associations

Two or more Orma models can be associated with association mechanism.

There are two type of associations: has-one and has-many.

In addition, there are another two kind of association supports: indirect associations with SingleAssociation<T> and direct associations.

Has-One Associations with SingleAssociation<T>

There is SingleAssociation<T> to support has-one associations, which is retrieved on demand; this is useful if the associations are not always used.

For example, a book has a publisher:

@Table class Publisher {

@PrimaryKey
public long id; 
}
  @Table class Book {

@Column
  public SingleAssociation<Publisher> publisher; 
}

To save this a book:

Book book = new Book();
 Publisher publisher = new Publisher();
  long publisherId = db.insertIntoPublisher()
  .execute(publisher);
  // if publisher has a valid primary key, `just(publisher)` is also okay. book.publisher = SingleAssociation.just(publisherId);
  db.insertIntoBook()
  .execute(book)

To get a publisher from books:

db.selectFromBook()
  .forEach((book) -> {

// blocking:

Publisher publisher = book.publisher.get();

 // with RxJava Single<Publisher>:

book.publisher.single()

 .subscribe((publisher) -> {

  // use publisher

 
}
)
  
}
);

Direct Associations

There are direct associations, where an Orma model has another Orma model directly.

Given a has-one association, Book has-one Publisher:

@Table class Publisher {

@PrimaryKey
public long id;
 @Column
public String name; 
}
  @Table class Book {

@PrimaryKey
  public long id;

@column
  public String title;

@Column
  public Publisher publisher; 
}

The corresponding table definition is something like this:

CREATE TABLE `Publisher` (
`id` INTEGER PRIMARY KEY,
`name` TEXT NOT NULL ) CREATE TABLE `Book` (
`id` INTEGER PRIMARY KEY,
`title` TEXT NOT NULL,
`publisher` INTEGER NOT NULL
  REFERENCES `Publisher`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )

In SQL, Book#publisher refers Publisher#id, indicating the two tables should be joined in SELECT statements.

In Java, Book#publisher is a Publisher instance, which is retrieved in each SELECT operations. There is no lazy loading in direct associations.

Has-Many Associations with SingleAssociation<T>

Has-many associations are not directly supported but you can define a method to get associated objects:

@Table class Publisher {

  @PrimaryKey
  public long id;

// NOTE: If OrmaDatabase is a singleton, no parameter is required!
  public Book_Relation getBooks(OrmaDatabase db) {

return db.relationOfBook().publisherEq(this);

  
}
 
}
  @Table class Book {

@Column(indexed = true)
  public SingleAssociation<Publisher> publisher;  
}

Has-Many Associations with Direct Associations

As SingleAssociation is, you can define a helper method to get has-many associations:

@Table class Publisher {

  @PrimaryKey
  public long id;

// NOTE: If OrmaDatabase is a singleton, no parameter is required!
  public Book_Relation getBooks(OrmaDatabase db) {

return db.relationOfBook().publisherEq(this);

  
}
 
}
  @Table class Book {

@Column(indexed = true)
  public Publisher publisher;  
}

Limitations in Associations

  • There are no methods to query associated models

These issues will be fixed in a future.

Type Adapters

Orma models are able to have embedded objects with type adapters, a.k.a. static type adapters, by defining classes with @StaticTypeAdapter annotation.

For example, if you want to embed LatLng in your Orma model, you can define a type adapter like this:

@StaticTypeAdapter(
  targetType = LatLng.class, // required
  serializedType = String.class // required ) public class LatLngAdapter {

// SerializedType serialize(TargetType source)
  @NonNull
  public static String serialize(@NonNull LatLng source) {

return source.latitude + "," + source.longitude
  
}

// TargetType deserialize(SerializedType serialized)
  @NonNull
  public static LatLng deserialize(@NonNull String serialized) {

String[] values = serialized.split(",");

return new LatLng(

 Double.parseDouble(values[0]),

 Double.parseDouble(values[1]));

  
}
 
}

@StaticTypeAdapter requires targetType and serializedType options and two static methods SerializedType serialize(TargetType) and TargetType deserialize(SerializedType).

How Serialized Types Used

A @StaticTypeAdapter#serializedType is bound to an SQLite storage type. Thus it must be one of the "Java Type" listed the table below, where each "Java Type" has a corresponding "SQLite Type":

Java Type SQLite Type
int INTEGER
short INTEGER
long INTEGER
boolean INTEGER
float REAL
double REAL
String TEXT
byte[] BLOB

@StaticTypeAdapters for Multiple Serializers at Once

You can also define multiple type serializers to single class with @StaticTypeAdapters annotation containers:

@StaticTypeAdapters({

  @StaticTypeAdapter(

targetType = MutableInt.class,

serializedType = int.class,

serializer = "serializeMutableInt",

deserializer = "deserializeMutableInt"
  ),
  @StaticTypeAdapter(

targetType = MutableLong.class,

serializedType = long.class,

serializer = "serializeMutableLong",

deserializer = "deserializeMutableLong"
  ) 
}
) public class TypeAdapters {

public static int serializeMutableInt(@NonNull MutableInt target) {

return target.value;
  
}

@NonNull
  public static MutableInt deserializeMutableInt(int deserialized) {

return new MutableInt(deserialized);

  
}

public static long serializeMutableLong(@NonNull MutableLong target) {

return target.value;
  
}

@NonNull
  public static MutableLong deserializeMutableLong(long deserialized) {

return new MutableLong(deserialized);

  
}
 
}

Built-In Type Adapters

There are built-in type adapters for typically used value objects and collections:

  • java.math.BigDecimal
  • java.math.BigInteger
  • java.nio.ByteBuffer
  • java.util.Currency
  • java.util.Date
  • java.sql.Date
  • java.sql.Time
  • java.sql.Timestamp
  • java.util.UUID
  • java.util.List<String>
  • java.util.ArrayList<String>
  • java.util.Set<String>
  • java.util.HashSet<String>
  • android.net.Uri

Generic Type Adapters

If your deserialize() takes a Class<T> parameter, the type serializer is generic, handling classes with the common base classe.

For example, if you have some enums that implement EnumDescription, e.g. T extends Enum<T> & EnumDescription, you can handle it with a generic type adapter.

Given an interface EnumDescription:

public interface EnumDescription {

long getValue();
 
}

And here is its type adapter:

@StaticTypeAdapter(

targetType = EnumDescription.class,

serializedType = long.class ) public class EnumTypeAdapter {

public static <T extends Enum<T> & EnumDescription> long serialize(@NonNull T value) {

return value.getValue();

  
}

@NonNull
  public static <T extends Enum<T> & EnumDescription> T deserialize(long serialized, @NonNull Class<T> type) {

 for (T enumValue : type.getEnumConstants()) {

 if (enumValue.getValue() == serialized) {

  return enumValue;

 
}

}

 throw new RuntimeException("Unknown id: " + serialized + " for " + type);

  
}
 
}

Now deserialize() uses the type information for the conclete target class.

Pagination

There are two style pagination. You can use either, but not mixed.

limit and offset

SQL style pagination:

for (Todo todo : orma.selectFromTodo().titleEq("buy").offset(0).limit(10)) {

  // ... 
}

page and per

"paging" style pagination inspired from Ruby's kaminari.

for (Todo todo : orma.selectFromTodo().titleEq("buy").page(1).per(10)) {

  // ... 
}

Note that page starts from 1.

Raw Queries

For low-level operations, e.g. executing a raw query, you can use OrmaDatabase#getConnection(), which returns OrmaConnection.

For example:

Cursor cursor = db.getConnection().rawQuery(

Resources

A custom animated confirmation dialog for Android.

jadx - Dex to Java decompiler.

Command line and GUI tools for produce Java source code from Android Dex and Apk files.

LinearLayout with "pull-to-refresh" feature.

FragmentSwapper is an Open Source Android library that allows easy fragment's management. It is somewhat similar to Activities management model. For instance, new fragment can be launched from another one with action's request (using request code) and then recieve the result.

A couple of sticky header decorations for android's RecyclerView.

A 'Google Fit' like activity indicator for Android.

Topics


2D Engines   3D Engines   9-Patch   Action Bars   Activities   ADB   Advertisements   Analytics   Animations   ANR   AOP   API   APK   APT   Architecture   Audio   Autocomplete   Background Processing   Backward Compatibility   Badges   Bar Codes   Benchmarking   Bitmaps   Bluetooth   Blur Effects   Bread Crumbs   BRMS   Browser Extensions   Build Systems   Bundles   Buttons   Caching   Camera   Canvas   Cards   Carousels   Changelog   Checkboxes   Cloud Storages   Color Analysis   Color Pickers   Colors   Comet/Push   Compass Sensors   Conferences   Content Providers   Continuous Integration   Crash Reports   Credit Cards   Credits   CSV   Curl/Flip   Data Binding   Data Generators   Data Structures   Database   Database Browsers   Date &   Debugging   Decompilers   Deep Links   Dependency Injections   Design   Design Patterns   Dex   Dialogs   Distributed Computing   Distribution Platforms   Download Managers   Drawables   Emoji   Emulators   EPUB   Equalizers &   Event Buses   Exception Handling   Face Recognition   Feedback &   File System   File/Directory   Fingerprint   Floating Action   Fonts   Forms   Fragments   FRP   FSM   Functional Programming   Gamepads   Games   Geocaching   Gestures   GIF   Glow Pad   Gradle Plugins   Graphics   Grid Views   Highlighting   HTML   HTTP Mocking   Icons   IDE   IDE Plugins   Image Croppers   Image Loaders   Image Pickers   Image Processing   Image Views   Instrumentation   Intents   Job Schedulers   JSON   Keyboard   Kotlin   Layouts   Library Demos   List View   List Views   Localization   Location   Lock Patterns   Logcat   Logging   Mails   Maps   Markdown   Mathematics   Maven Plugins   MBaaS   Media   Menus   Messaging   MIME   Mobile Web   Native Image   Navigation   NDK   Networking   NFC   NoSQL   Number Pickers   OAuth   Object Mocking   OCR Engines   OpenGL   ORM   Other Pickers   Parallax List   Parcelables   Particle Systems   Password Inputs   PDF   Permissions   Physics Engines   Platforms   Plugin Frameworks   Preferences   Progress Indicators   ProGuard   Properties   Protocol Buffer   Pull To   Purchases   Push/Pull   QR Codes   Quick Return   Radio Buttons   Range Bars   Ratings   Recycler Views   Resources   REST   Ripple Effects   RSS   Screenshots   Scripting   Scroll Views   SDK   Search Inputs   Security   Sensors   Services   Showcase Views   Signatures   Sliding Panels   Snackbars   SOAP   Social Networks   Spannable   Spinners   Splash Screens   SSH   Static Analysis   Status Bars   Styling   SVG   System   Tags   Task Managers   TDD &   Template Engines   Testing   Testing Tools   Text Formatting   Text Views   Text Watchers   Text-to   Toasts   Toolkits For   Tools   Tooltips   Trainings   TV   Twitter   Updaters   USB   User Stories   Utils   Validation   Video   View Adapters   View Pagers   Views   Watch Face   Wearable Data   Wearables   Weather   Web Tools   Web Views   WebRTC   WebSockets   Wheel Widgets   Wi-Fi   Widgets   Windows   Wizards   XML   XMPP   YAML   ZIP Codes