Share
Embed
Speeding up development with AutoValue and SQLDelight
Andrii Rakhimov
March 27, 2017
Transcript
55 slides
-
Problems public class User implements Parcelable { public static final
Creator<User> CREATOR = new Creator<User>() { @Override public User createFromParcel(Parcel in) { return new User(in); } @Override public User[] newArray(int size) { return new User[size]; } }; @SerializedName("username") private String userName; @SerializedName("email") private String email; public User(String userName) { this.userName = userName; } protected User(Parcel in) { userName = in.readString(); email = in.readString(); } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(userName); dest.writeString(email); } @Override public int describeContents() { return 0; } public String getUserName() { return userName; } public String setUserName(String userName) { this.userName = userName; } public String getEmail() { return email; } public String setEmail(String email) { this.email = email; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; if (userName != null ? !userName.equals(user.userName) : user.userName != null) return false; return email != null ? !email.equals(user.email) : user.email != null; } @Override public int hashCode() { int result = 31; result = 31 * result + (userName != null ? userName.hashCode() : 0); result = 31 * result + (email != null ? email.hashCode() : 0); return result; } @Override public String toString() { return "User{userName='" + userName + ", email='" + email; } -
How does it works? @AutoValue public abstract class User {
@Nullable public abstract String username(); public abstract String email(); } -
How does it works? @Retention(RetentionPolicy.SOURCE) @Target({ ElementType.TYPE}) public @interface AutoValue
{ @Retention(RetentionPolicy.SOURCE) @Target({ElementType.TYPE}) public @interface Builder { } } -
Generated implementation abstract class AutoValue_User extends User { private final
String email; private final String username; AutoValue_User(String email, @Nullable String username) { if (email == null) { throw new NullPointerException("Null email"); } this.username = username; } ...getters, equals, hashcode, toString, etc. -
Instantiation @AutoValue public abstract class User { public static Builder
builder() { return new AutoValue_User.Builder(); } @AutoValue.Builder public abstract static class Builder { public abstract Builder email(String email); public abstract Builder username(String username); public abstract User build(); } } AS plugin to generate builders https://github.com/afcastano/AutoValuePlugin -
Mutable types @AutoValue public abstract class MutableExample { public static
MutableExample create(MutablePropertyType ouch) { // Replace `.clone` below with the right copying code for this type return new AutoValue_MutableExample(ouch.clone()); } /** * Returns the ouch associated with this object; <b>do not mutate</b> the * returned object. */ public abstract MutablePropertyType ouch(); } -
Parcelable final class AutoValue_User extends $AutoValue_User { public static final
Parcelable.Creator<AutoValue_User> CREATOR = new Parcelable.Creator<AutoValue_User>() { @Override public AutoValue_User createFromParcel(Parcel in) { return new AutoValue_User( in.readString(), in.readInt() == 0 ? in.readString() : null ); } @Override public AutoValue_User[] newArray(int size) { return new AutoValue_User[size]; } ... -
Gson @AutoValue public abstract class User implements Parcelable { @Nullable
public abstract String username(); public abstract String email(); public static TypeAdapter<User> typeAdapter(Gson gson) { return new AutoValue_User.GsonTypeAdapter(gson); } } -
Gson @GsonTypeAdapterFactory public abstract class AdapterFactory implements TypeAdapterFactory { public
static AdapterFactory create() { return new AutoValueGson_AdapterFactory(); } } @Provides @Singleton Gson provideGson() { return new GsonBuilder() .registerTypeAdapterFactory(AdapterFactory.create()) .create(); } -
Gson abstract class $AutoValue_User extends $$AutoValue_User { ... public static
final class GsonTypeAdapter extends TypeAdapter<User> { ... @Override public void write(JsonWriter jsonWriter, User object) throws IOException { if (object == null) { jsonWriter.nullValue(); return; } jsonWriter.beginObject(); jsonWriter.name("username"); usernameAdapter.write(jsonWriter, object.username()); jsonWriter.name("email"); emailAdapter.write(jsonWriter, object.email()); jsonWriter.endObject(); } } @Override public User read(JsonReader jsonReader) throws IOException { if (jsonReader.peek() == JsonToken.NULL) { jsonReader.nextNull(); return null; } jsonReader.beginObject(); String email = this.defaultEmail; String username = this.defaultUsername; while (jsonReader.hasNext()) { String _name = jsonReader.nextName(); if (jsonReader.peek() == JsonToken.NULL) { jsonReader.nextNull(); continue; } switch (_name) { case "username": { username = usernameAdapter.read(jsonReader); break; } case "email": { email = emailAdapter.read(jsonReader); break; -
All Extensions • JSON(Gson/Jackson) • Parcelable • Cursor • Firebase
• Redact(Omitting selected fields from toString()) -
Pros/Cons • DRY • Immutable(Type-safe, null-safe and thread-safe) • Maintainable(180->20,
no need for tests(builder), faster PRs) -
Pros/Cons • DRY • Immutable(Type-safe, null-safe and thread-safe) • Maintainable(180->20,
no need for tests(builder), faster PRs) • API-invisible -
Pros/Cons • DRY • Immutable(Type-safe, null-safe and thread-safe) • Maintainable(180->20,
no need for tests(builder), faster PRs) • API-invisible • No runtime dependencies -
Pros/Cons • DRY • Immutable(Type-safe, null-safe and thread-safe) • Maintainable(No
need for tests(builder), faster PRs) • API-invisible • No runtime dependencies • Docs -
Cons • Require to compile • Fabrics for class instantiation
may break • Mutable fields, arrays, lists -
The problem static final String CREATE_CATEGORIES_TABLE = "create table "
+ Category.TABLE + "(" + COLUMN_ID + " integer primary key autoincrement, " + Category.COL_NAME + " text, " + Category.COL_COLOR + " integer, " + Category.COL_CREATE_BY_USER + " integer DEFAULT 0 " + ");"; cursor.getString(cursor.getColumnIndexOrThrow(columnName)); ContentValues values = new ContentValues(); values.put(COL_NAME, name); -
The problem private static String LIST_QUERY(String selection) { String query
= "SELECT *, COUNT(i." + ListItem.COL_STATUS + ") " + List.COL_SIZE + ", SUM(i." + ListItem.COL_STATUS + ") " + List.COL_BOUGHT_COUNT + " FROM " + List.TABLE + " s" + " LEFT OUTER JOIN " + ListItem.TABLE + " i" + " ON s." + List.COL_ID + " = i." + ListItem.COL_PARENT_LIST_ID; if (selection == null) { return query + " GROUP BY s." + List.COL_ID; } return query + " WHERE " + selection + " GROUP BY s." + List.COL_ID; } -
Features that we want: • RxJava • Immutability • Type
Safety • Compiler Errors • SQLite features -
Features that we want: • RxJava - SQLBrite • Immutability
• Type Safety • Compiler Errors • SQLite features -
Features that we want: • RxJava - SQLBrite • Immutability
- AutoValue • Type Safety • Compiler Errors • SQLite features -
Features that we want: • RxJava - SQLBrite • Immutability
- AutoValue • Type Safety - SQLDelight • Compiler Errors - SQLDelight • SQLite features - SQLDelight -
CREATE TABLE images ( _id INTEGER NOT NULL PRIMARY KEY
AUTOINCREMENT, ad_id INTEGER NOT NULL, url TEXT, size TEXT, image_id TEXT ); saveImage: INSERT INTO images(ad_id, url, size, image_id) VALUES (?, ?, ?, ?); getAllImages: SELECT * FROM images; SQL statements -
Java model @AutoValue public abstract class Image implements ImagesModel {
public static final Factory<Image> FACTORY = new Factory<>(new Creator<Image>() { @Override public Image create(long _id, long ad_id, @Nullable String url, @Nullable String size, @Nullable String image_id) { return new AutoValue_Image(_id, ad_id, url, size, image_id); } }); public static final RowMapper<Image> GET_ALL_IMAGES_MAPPER = FACTORY.getAllImagesMapper(); public static final RowMapper<Image> GET_IMAGES_BY_ID_MAPPER = FACTORY.getImagesByIdMapper(); } -
Cursor mapper final class Mapper<T extends ImagesModel> implements RowMapper<T> {
private final Factory<T> imagesModelFactory; public Mapper(Factory<T> imagesModelFactory) { this.imagesModelFactory = imagesModelFactory; } @Override public T map(@NonNull Cursor cursor) { return imagesModelFactory.creator.create( cursor.getLong(0), cursor.getLong(1), cursor.isNull(2) ? null : cursor.getString(2), cursor.isNull(3) ? null : cursor.getString(3), cursor.isNull(4) ? null : cursor.getString(4) ); } } cursor.getString(cursor.getColumnIndexOrThrow(columnName)); -
Example of insertion final class SaveImage extends SqlDelightCompiledStatement.Insert { public
SaveImage(SQLiteDatabase database) { super("images", database.compileStatement("" + "INSERT INTO images(ad_id, url)\n" + "VALUES (?, ?)")); } public void bind(long ad_id, @Nullable String url) { program.bindLong(1, ad_id); if (url == null) { program.bindNull(2); } else { program.bindString(2, url); } } } saveImage: INSERT INTO images(ad_id, url) VALUES (?, ?); -
Example of insertion saveImage: INSERT INTO images(ad_id, url) VALUES (?,
?); Image.SaveImage saveImage = new ImagesModel.SaveImage(db); saveImage.bind(10, "url"); saveImage.program.executeInsert(); -
Example of query getImagesById: SELECT * FROM images WHERE ad_id
= ? ORDER BY _id ASC; public SqlDelightStatement getAdsByStatus(@NonNull Ad.Status status) { List<String> args = new ArrayList<String>(); int currentIndex = 1; StringBuilder query = new StringBuilder(); query.append("SELECT *\n" + "FROM ads\n" + "WHERE status = "); query.append('?').append(currentIndex++); args.add((String) statusAdapter.encode(status)); return new SqlDelightStatement(query.toString(), args.toArray(new String[args.size()]), Collections.<String>singleton("ads")); } -
Example of query getImagesById: SELECT * FROM images WHERE ad_id
= ? ORDER BY _id ASC; SqlDelightStatement query = Image.FACTORY.getImagesById(10); Cursor cursor = db.rawQuery(query.statement, query.args); Ad.FACTORY.getAdsByStatusMapper().map(cursor); -
Sets of values also can be passed as an argument
getImagesById: SELECT * FROM images WHERE ad_id IN ? or WHERE ad_id IN :param_name ORDER BY _id ASC; SqlDelightStatement query = Image.FACTORY.getImagesById(new long[]{10, 20}); Cursor cursor = db.rawQuery(query.statement, query.args); -
Query with join selectAll: SELECT * FROM ads JOIN images
ON ads.ad_id = images.ad_id; interface SelectAllModel<T1 extends AdModel, T2 extends ImagesModel> { @NonNull T1 ads(); @NonNull T2 images(); } -
Query with join public static final RowMapper<AdWithImages> SELECT_ALL_MAPPER = FACTORY.selectAllMapper(
new SelectAllCreator<Ad, Image, AdWithImages>() { @Override public AdWithImages create(@NonNull Ad ads, @NonNull Image images) { return new AutoValue_Ad_AdWithImages(ads, images); } }, Image.FACTORY); @AutoValue public static abstract class AdWithImages implements SelectAllModel<Ad, Image> { } -
Support imports like Java in .sq files import com.justplay1.sqldelight.models.Ad.Status; import
java.util.Calendar; CREATE TABLE ads ( _id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, ad_id INTEGER NOT NULL UNIQUE ON CONFLICT IGNORE, title TEXT, description TEXT, price TEXT, status TEXT AS Status NOT NULL, create_time INTEGER AS Calendar NOT NULL, is_favorite INTEGER AS Boolean NOT NULL DEFAULT 0 ); -
Support imports like Java in .sq files public final class
DateAdapter implements ColumnAdapter<Calendar, Long> { @Override public Long encode(@NonNull Calendar date) { return date.getTimeInMillis(); } @Override @NonNull public Calendar decode(Long data) { Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(data); return calendar; } } -
Support imports like Java in .sq files @AutoValue public abstract
class Ad implements AdModel { public enum Status { ACTIVE, DEACTIVATED } private static final DateAdapter DATE_ADAPTER = new DateAdapter(); private static final EnumColumnAdapter<Status> STATUS_ADAPTER = EnumColumnAdapter.create(Status.class); public static final Factory<Ad> FACTORY = new Factory<>(new Creator<Ad>() { @Override public Ad create(long _id, long ad_id, @Nullable String title, @NonNull Status status, @NonNull Calendar create_time, boolean is_favorite) { return new AutoValue_Ad(_id, ad_id, title, status, create_time, is_favorite); } }, STATUS_ADAPTER, DATE_ADAPTER); } -
Support to pass any type into query from java CREATE
TABLE ads ( status TEXT AS Status NOT NULL ); getAdsByStatus: SELECT * FROM ads WHERE status = ?; SqlDelightStatement query = Ad.FACTORY.getAdsByStatus(Ad.Status.ACTIVE); Cursor cursor = db.rawQuery(query.statement, query.args); -
SQLDelight with SQLBrite private BriteDatabase db; private Observable<Ad> getAd() {
SqlDelightStatement statement = Ad.FACTORY.getAdsByStatus(Ad.Status.ACTIVE); return db.createQuery(statement.tables, statement.statement, statement.args) .mapToOne(Ad.ADS_BY_STATUS_MAPPER::map); } -
• Syntax highlighting • Refactoring/Find usages • Code autocompletion •
Generate Model files after edits • Right click to copy as valid SQLite • Compiler errors in IDE click through to file Intellij IDEA plugin -
Conclusion • RxJava, Immutability, Type Safety, Compiler Errors, SQLite features
• Shared language • Tight coupling with views (MVVM) • Easier code review • Confidence writing SQL -
Links and contacts AutoValue - https://github.com/google/auto/blob/master/value/userguide/index.md AutoValue plugins for AS
- https://github.com/afcastano/AutoValuePlugin https://github.com/robohorse/RoboPOJOGenerator AutoValue Extensions - https://github.com/blipinsk/awesome-auto-value-extensions SQLDelight - https://github.com/square/sqldelight SQLBrite - https://github.com/square/sqlbrite Rakhimov Andrii - https://www.facebook.com/profile.php?id=100000447429343 | [email protected] Mkhytar Mkhoian - https://www.facebook.com/mkhytar.mkhoian | [email protected]