Slide 1

Slide 1 text

Speeding up development with AutoValue and SQLDelight Andrii Rakhimov Mkhytar Mkhoian Lalafo

Slide 2

Slide 2 text

"The best code is code not written"

Slide 3

Slide 3 text

Problems public class User implements Parcelable { public static final Creator CREATOR = new Creator() { @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; }

Slide 4

Slide 4 text

Problems ● DRY

Slide 5

Slide 5 text

Problems ● DRY ● Mutable

Slide 6

Slide 6 text

Problems ● DRY ● Mutable ● Maintainability

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

AutoValue to the rescue

Slide 9

Slide 9 text

Value classes public class Money { public Currency currency; public long amount; }

Slide 10

Slide 10 text

How does it works? @AutoValue public abstract class User { @Nullable public abstract String username(); public abstract String email(); }

Slide 11

Slide 11 text

How does it works? @Retention(RetentionPolicy.SOURCE) @Target({ ElementType.TYPE}) public @interface AutoValue { @Retention(RetentionPolicy.SOURCE) @Target({ElementType.TYPE}) public @interface Builder { } }

Slide 12

Slide 12 text

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.

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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; do not mutate the * returned object. */ public abstract MutablePropertyType ouch(); }

Slide 15

Slide 15 text

AutoValue Extensions, API-invisible

Slide 16

Slide 16 text

Parcelable final class AutoValue_User extends $AutoValue_User { public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @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]; } ...

Slide 17

Slide 17 text

Gson @AutoValue public abstract class User implements Parcelable { @Nullable public abstract String username(); public abstract String email(); public static TypeAdapter typeAdapter(Gson gson) { return new AutoValue_User.GsonTypeAdapter(gson); } }

Slide 18

Slide 18 text

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(); }

Slide 19

Slide 19 text

Gson abstract class $AutoValue_User extends $$AutoValue_User { ... public static final class GsonTypeAdapter extends TypeAdapter { ... @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;

Slide 20

Slide 20 text

TypeAdapters for free

Slide 21

Slide 21 text

No content

Slide 22

Slide 22 text

All Extensions ● JSON(Gson/Jackson) ● Parcelable ● Cursor ● Firebase ● Redact(Omitting selected fields from toString())

Slide 23

Slide 23 text

Pros/Cons ● DRY ● Immutable(Type-safe, null-safe and thread-safe) ● Maintainable(180->20, no need for tests(builder), faster PRs)

Slide 24

Slide 24 text

Pros/Cons ● DRY ● Immutable(Type-safe, null-safe and thread-safe) ● Maintainable(180->20, no need for tests(builder), faster PRs) ● API-invisible

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

Cons ● Require to compile

Slide 28

Slide 28 text

Cons ● Require to compile ● Fabrics for class instantiation may break

Slide 29

Slide 29 text

Cons ● Require to compile ● Fabrics for class instantiation may break ● Mutable fields, arrays, lists

Slide 30

Slide 30 text

SQLDelight

Slide 31

Slide 31 text

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);

Slide 32

Slide 32 text

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; }

Slide 33

Slide 33 text

Features that we want: ● RxJava ● Immutability ● Type Safety ● Compiler Errors ● SQLite features

Slide 34

Slide 34 text

Features that we want: ● RxJava - SQLBrite ● Immutability ● Type Safety ● Compiler Errors ● SQLite features

Slide 35

Slide 35 text

Features that we want: ● RxJava - SQLBrite ● Immutability - AutoValue ● Type Safety ● Compiler Errors ● SQLite features

Slide 36

Slide 36 text

Features that we want: ● RxJava - SQLBrite ● Immutability - AutoValue ● Type Safety - SQLDelight ● Compiler Errors - SQLDelight ● SQLite features - SQLDelight

Slide 37

Slide 37 text

Package structure

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

Java model @AutoValue public abstract class Image implements ImagesModel { public static final Factory FACTORY = new Factory<>(new Creator() { @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 GET_ALL_IMAGES_MAPPER = FACTORY.getAllImagesMapper(); public static final RowMapper GET_IMAGES_BY_ID_MAPPER = FACTORY.getImagesByIdMapper(); }

Slide 40

Slide 40 text

Cursor mapper final class Mapper implements RowMapper { private final Factory imagesModelFactory; public Mapper(Factory 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));

Slide 41

Slide 41 text

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 (?, ?);

Slide 42

Slide 42 text

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();

Slide 43

Slide 43 text

Example of query getImagesById: SELECT * FROM images WHERE ad_id = ? ORDER BY _id ASC; public SqlDelightStatement getAdsByStatus(@NonNull Ad.Status status) { List args = new ArrayList(); 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.singleton("ads")); }

Slide 44

Slide 44 text

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);

Slide 45

Slide 45 text

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);

Slide 46

Slide 46 text

Query with join selectAll: SELECT * FROM ads JOIN images ON ads.ad_id = images.ad_id; interface SelectAllModel { @NonNull T1 ads(); @NonNull T2 images(); }

Slide 47

Slide 47 text

Query with join public static final RowMapper SELECT_ALL_MAPPER = FACTORY.selectAllMapper( new SelectAllCreator() { @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 { }

Slide 48

Slide 48 text

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 );

Slide 49

Slide 49 text

Support imports like Java in .sq files public final class DateAdapter implements ColumnAdapter { @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; } }

Slide 50

Slide 50 text

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_ADAPTER = EnumColumnAdapter.create(Status.class); public static final Factory FACTORY = new Factory<>(new Creator() { @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); }

Slide 51

Slide 51 text

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);

Slide 52

Slide 52 text

SQLDelight with SQLBrite private BriteDatabase db; private Observable 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); }

Slide 53

Slide 53 text

● 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

Slide 54

Slide 54 text

Conclusion ● RxJava, Immutability, Type Safety, Compiler Errors, SQLite features ● Shared language ● Tight coupling with views (MVVM) ● Easier code review ● Confidence writing SQL

Slide 55

Slide 55 text

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]