Vinyl
Vinyl is an annotation processor that makes it simple to work with Cursors and ContentValues using your application's domain language. Vinyl is an amazing sounding record.
Motivation
Assume there is this cursor describing employees:
name | age | manager |
---|---|---|
John | 30 | 0 |
Jane | 30 | 1 |
Accessing the employee fields for the cursor would normally look similar to this (undesirable) code:
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
String name = cursor.getString(cursor.getColumnIndex("name"));
int age = cursor.getInt(cursor.getColumnIndex("age"));
boolean manager = cursor.getInt(cursor.getColumnIndex("manager")) != 0;
cursor.moveToNext();
}
Usage
Start by creating an interface, annotated with @Record
, with no argument methods named the same as fields in the cursor. @Record
triggers a code generator that will cause a new class named EmployeeRecord
to be generated.
@Record public interface Employee {
String name();
int age();
boolean manager();
}
Accessing Fields
Accessing the fields for the cursor can now be written as:
Employee employee = EmployeeRecord.wrapCursor(cursor);
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
String name = employee.name();
int age = employee.age();
boolean manager = employee.manager();
cursor.moveToNext();
}
Take note that the Employee
returned from wrapCursor
reflects the current state of the cursor. If you need an instance of Employee
that will not change with the cursor you can call buildFromCursor
instead.
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
Employee employee = EmployeeRecord.buildFromCursor(cursor);
cursor.moveToNext();
}
Custom Projections
The fields of a cursor will not always match up to how we want to access them in code. Assume the cursor above contains a field named hire_dt
represented by the INTEGER
type that described the hire date for an employee.
We can specify that a Java method should map to a differently named underlying cursor field with the @Projection
annotation.
@Record public interface Employee {
String name();
int age();
boolean manager();
@Projection("hire_dt")
long hireDate();
}
If the projection you need to you will vary at runtime (ex: getting the name of an Android contact), you can specify a class that will perform the dynamic projection evaluation.
@Record public interface ContactProjection {
@Projection(conditionalProjection = DisplayNameProjection.class)
String displayName();
class DisplayNameProjection implements ConditionalProjection {
@Override
public String projection() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB ?
Contacts.DISPLAY_NAME_PRIMARY :
Contacts.DISPLAY_NAME;
}
}
}
Converters
Going one step further, we really don't want a long
for the hire date but instead would rather have a java.util.Date
.
@Record public interface Employee {
String name();
int age();
boolean manager();
@Converter(fieldClass = Long.class, converter = LongToDateConverter.class)
@Projection("hire_dt")
Date hireDate();
}
Where LongToDateConverter
has this implementation:
public class LongToDateConverter {
public Date convertFrom(Long value) {
return (value == null) ? null : new Date(value);
}
public Long convertTo(Date value) {
return (value == null) ? null : value.getTime();
}
}
Now we can access hire dates like this:
Employee employee = EmployeeRecord.wrapCursor(cursor);
Date hireDate = employee.hireDate();
Supported Types
Without specifying a converter, the following types are supported:
boolean
short
int
long
float
double
byte[]
String
Boolean
Short
Integer
Long
Float
Double
A few things to note about these types:
- Cursors do not expose boolean types. Under the covers the generated code will evaluate to boolean with a test like this:
cursor.getInt(cursor.getColumnIndex("...")) != 0
. If this default behavior does not match how your cursor exposes booleans (ex: Stringtrue
andfalse
) you will need to use a converter. - Non-primitive types will perform
cursor.isNull(...)
on the field and will either return null or the value of the field.
@NotNull
If you know that a field will never be null, you can annotate the field with the @NotNull
annotation from the Android Support Annotations library.
@Record public interface Employee {
@NotNull
String name();
int age();
boolean manager();
@Converter(cursorClass = Long.class, converter = LongToDateConverter.class)
@Projection("hire_dt")
Date hireDate();
}
ContentValues
Accessing values via cursors only represent one half of the solution. ContentValues
are the typical compliment to Cursors
. Creating content values that map back to the same fields looks like this:
ContentValues cv = EmployeeRecord.contentValuesBuilder()
.name("Coder McCoder")
.age(42)
.manager(false)
.hireDate(today)
.build();
Philosophy
Vinyl's purpose is to make it easier to operate on Cursors and ContentValues. While these types are frequently associated with databases, SQL, or ContentProviders, they are typically used at a higher layer of the application stack. As such, no direct attempt will be made at this time to simplify those other components.
Including in your project
buildscript {
repositories {
mavenCentral()
}
dependencies {
// Or latest versions
classpath 'com.android.tools.build:gradle:1.1.2'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4'
}
}
apply plugin: 'com.android.application' apply plugin: 'android-apt' dependencies {
apt 'com.madebyatomicrobot:vinyl-compiler:{
latest-version
}
'
compile 'com.madebyatomicrobot:vinyl-annotations:{
latest-version
}
'
}
Artifact | Latest Version |
---|---|
vinyl-compiler | |
vinyl-annotations |
Snapshots of the development version are available in Sonatype’s snapshots
repository.
Alternatives
Related Projects
- Schematic is a library to generate ContentProviders.
License
Copyright 2015 Atomic Robot LLC 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.
About Atomic Robot
We are iOS & Android experts that develop native mobile applications for startups and enterprise clients. From wireframes to final design, we understand the unique challenges and opportunities in designing for mobile. From concept to launch, we can guide you every step of the way.
Our clients partner with us to help them deliver exceptional mobile solutions to grow their business. Hire us to work on your next project.