Slide 1

Slide 1 text

Android Libraries I wish I knew when I started github.com/ChrisGuzman/taasty

Slide 2

Slide 2 text

Chris Guzman Developer Advocate Nexmo @speaktochris | chris-guzman.com

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

Starting a new app? Not sure what libraries to use?

Slide 5

Slide 5 text

A lot of apps do similar things → Manage multiple views → Load and display images → Fetch data from an API → Parse JSON → Start new activities with extras → Persist data to storage

Slide 6

Slide 6 text

These libraries prevent you reinventing the wheel with every project. → Butter Knife v8.5.1 → Picasso v2.5.2 → Gson v2.8.0 → Retrofit v2.3.0 → Realm v3.3.1 → Dart & Henson v2.0.2

Slide 7

Slide 7 text

The 45 Hour Minute Hackathon

Slide 8

Slide 8 text

What should I make?

Slide 9

Slide 9 text

Introducing TaaSTY

Slide 10

Slide 10 text

Tacos as a Service To You

Slide 11

Slide 11 text

Tinder for Tacos

Slide 12

Slide 12 text

No content

Slide 13

Slide 13 text

Step 1 Set up our views

Slide 14

Slide 14 text

Slide 15

Slide 15 text

Step 2 Use views

Slide 16

Slide 16 text

Butter Knife Use annotations to write less boilerplate code

Slide 17

Slide 17 text

→ How many times have you wrote findViewById? → No additional cost at run-time - does not slow down your app at all! → Improved View lookups → Improved Listener attachments → Improved Resource lookups

Slide 18

Slide 18 text

Bind views in an activity public class MainActivity extends Activity { @BindView(R.id.name) TextView name; @Override protected void onCreate(Bundle bundle) { ... ButterKnife.bind(this); name.setText("Tofu with Cheese on a tortilla"); } }

Slide 19

Slide 19 text

ButterKnife.bind(this) Generates code that looks up views/resources and saves them as a property on the Activity. public void bind(MainActivity activity) { activity.description = (android.widget.TextView) activity.findViewById(2130968577); }

Slide 20

Slide 20 text

Bind and unbind views in a fragment public class TacoFragment extends Fragment { @BindView(R.id.tags) EditText tags; @Override public View onCreateView(args...) { ... ButterKnife.bind(this, parentView); //Important! unbinder = ButterKnife.bind(this, parentView); tags.setHint("Add tags. Eg: Tasty!, Want to try") return view; } }

Slide 21

Slide 21 text

@Override public void onDestroyView() { super.onDestroyView(); //sets the views to null unbinder.unbind(); }

Slide 22

Slide 22 text

Event listeners @OnClick(R.id.save) public void saveTaco(Button button) { button.setText("Saved!"); } Arguments to the listener method are optional @OnClick(R.id.reject) public void reject() { Log.d("RejectBtn", "onClick") }

Slide 23

Slide 23 text

Butter Knife also supports: - OnLongClick - OnEditorAction - OnFocusChange - OnItemClick - OnItemLongClick - OnItemSelected - OnPageChange - OnTextChanged - OnTouch - OnCheckedChanged

Slide 24

Slide 24 text

Inject resources once, no need to save them as member variables! class MainActivity extends Activity { @BindString(R.string.title) String title; @BindDrawable(R.drawable.star) Drawable star; // int or ColorStateList @BindColor(R.color.guac_green) int guacGreen; // int (in pixels) or float (for exact value) @BindDimen(R.dimen.spacer) Float spacer; }

Slide 25

Slide 25 text

Group views together: @OnClick({ R.id.save, R.id.reject}) public void saveOrReject(View view) { if (view.getId() == R.reject) { Toast.makeText(this, "Ew Gross!", LENGTH_SHORT).show(); } else { Toast.makeText(this, "Yummy :)", LENGTH_SHORT).show(); } //TODO: implement ButterKnife.apply(actionButtons, DISABLE); getNewTaco(); }

Slide 26

Slide 26 text

Act on list of views at once with ButterKnife.apply. @BindViews({R.id.save, R.id.reject}) List actionButtons; ButterKnife.apply(actionButtons, View.ALPHA, 0.0f);

Slide 27

Slide 27 text

Action and Setter interfaces allow specifying simple behavior. ButterKnife.apply(actionButtons, DISABLE); ButterKnife.apply(actionButtons, ENABLED, false); static final ButterKnife.Action DISABLE = new ButterKnife.Action() { @Override public void apply(View view, int index) { view.setEnabled(false); } }; static final ButterKnife.Setter ENABLED = new ButterKnife.Setter() { @Override public void set(View view, Boolean value, int index) { view.setEnabled(value); } };

Slide 28

Slide 28 text

private void getNewTaco() { //TODO: implement setTacoImage(); }

Slide 29

Slide 29 text

Step 3 Add pictures of tasty tacos

Slide 30

Slide 30 text

Picasso Download and display images with ease!

Slide 31

Slide 31 text

→ makes HTTP Requests → caches the images → easy resizing/cropping/centering/scaling → takes care of downloading off the main thread → properly recycles views in RecyclerView

Slide 32

Slide 32 text

Pop quiz Which do you prefer?

Slide 33

Slide 33 text

private Bitmap DownloadImage(String url) { Bitmap bitmap = null; InputStream in = null; try { in = OpenHttpGETConnection(url); bitmap = BitmapFactory.decodeStream(in); in.close(); } catch (Exception e) { Log.d("DownloadImage", e.getLocalizedMessage()); } return bitmap; }

Slide 34

Slide 34 text

No content

Slide 35

Slide 35 text

Or... Picasso.with(context) .load("http://placekitten.com/200/300") .into(imageView);

Slide 36

Slide 36 text

No content

Slide 37

Slide 37 text

But wait there's more! .placeholder(R.mipmap.loading) //can be a resource or a drawable .error(R.drawable.sad_taco) //fallback image if error .fit() //reduce the image size to the dimensions of imageView .resize(imgWidth, imgHeight) //resizes the image in pixels .centerCrop() //or .centerInside() .rotate(90f) //or rotate(degrees, pivotX, pivotY) .noFade() //don't fade all fancy-like

Slide 38

Slide 38 text

Not just for loading images from the web Picasso.with(context).load(R.drawable.salsa).into(imageView1); Picasso.with(context).load("file:///asset/salsa.png").into(imageView2); Picasso.with(context).load(new File(...)).into(imageView3);

Slide 39

Slide 39 text

//Butter Knife! @BindView(R.id.taco_img) ImageView tacoImg; private void setTacoImage() { Picasso.with(context) .load("http://tacoimages.com/random.jpg") .into(tacoImg); } private void getNewTaco() { setTacoImage(); //TODO: implement loadTacoDescription(); }

Slide 40

Slide 40 text

No content

Slide 41

Slide 41 text

Step 4 Set up models Get ready for models by setting up what JSON will look like

Slide 42

Slide 42 text

Gson Convert JSON to a java object and vice versa!

Slide 43

Slide 43 text

→ Without requiring you place Java annotations in your classes → Super performant → Commonly used

Slide 44

Slide 44 text

Example time! class Taco { private String name; private String url; //not included in JSON serialization or deserialization private transient String imageUrl; Taco(args...) { ... } }

Slide 45

Slide 45 text

// Serialize to JSON Taco breakfastTaco = new Taco( "Eggs with syrup on pancake", "tacofancy.com/breakfast", "imgur.com/123"); Gson gson = new Gson(); String json = gson.toJson(breakfastTaco); json = { "name":"Eggs with syrup on pancake", "url": "tacofancy.com/breakfast" } // Deserialize to POJO Taco yummyTaco = gson.fromJson(json, Taco.class); // ==> yummyTaco is just like breakfastTaco without imagUrl

Slide 46

Slide 46 text

Benefits → All fields in the current class (and from all super classes) are included by default → Supports multi-dimensional arrays

Slide 47

Slide 47 text

Gotchas → While serializing, a null field is skipped from the output → While deserializing, a missing entry in JSON results in setting the corresponding field in the object to null

Slide 48

Slide 48 text

Cool customization //Set properties to null instead of ignoring them Gson gson = new GsonBuilder().serializeNulls().create(); //Keep whitespace Gson gson = new GsonBuilder().setPrettyPrinting().create();

Slide 49

Slide 49 text

Need to rename a variable from an API? public class Taco { @SerializedName("serialized_labels") private String tags; }

Slide 50

Slide 50 text

Or use a custom date format? public String DATE_FORMAT = "yyyy-MM-dd"; GsonBuilder gsonBuilder = new GsonBuilder(); gsonBuilder.setDateFormat(DATE_FORMAT); Gson gson = gsonBuilder.create();

Slide 51

Slide 51 text

No content

Slide 52

Slide 52 text

private void getNextTaco() { setTacoImage(); //TODO: implement loadTacoDescription(); }

Slide 53

Slide 53 text

Step 5 Get taco recipe from web API

Slide 54

Slide 54 text

Retrofit Stop using AsyncTask Please, just stop.

Slide 55

Slide 55 text

The better way → Typesafe → Built in support for: → Authentication → parse JSON to POJOs → Supports RxJava → Can be executed synchronously or asynchronously

Slide 56

Slide 56 text

API Endpoints public interface TacoApi { // Request method and URL specified in the annotation // Callback for the parsed response is the last parameter @GET("random") Call randomTaco(@Query("full-taco") boolean full); @GET("contributions/{name}") Call getContributors(@Path("name") String username); @POST("recipe/new") Call createRecipe(@Body Recipe recipe); @GET("contributions") Call> getContributors(); }

Slide 57

Slide 57 text

Getting JSON synchronously Retrofit retrofit = new Retrofit.Builder() .baseUrl("http://taco-randomizer.herokuapp.com/") .addConverterFactory(GsonConverterFactory.create()) .build(); // Create an instance of our TacoApi interface. TacoApi tacoApi = retrofit.create(TacoApi.class); // Create a call instance for a random taco Call call = tacoApi.randomTaco(false); // Fetch a random taco // Do this off the main thread Taco taco = call.execute().body();

Slide 58

Slide 58 text

POSTing JSON Async Recipe recipe = new Recipe(); Call call = tacoApi.createRecipe(recipe); call.enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { } @Override public void onFailure(Call call, Throwable t) { }

Slide 59

Slide 59 text

Annotations → @Path: variable substitution for the API endpoint. → @Query: Add a query parameter. → @Body: Payload for the POST call (serialized from a Java object to a JSON string) → @Headers: Add a header to the HTTP call → @Url: Pass in a dynamic url

Slide 60

Slide 60 text

Cool tricks //Change the base url @POST("http://taco-randomizer.herokuapp.com/v2/taco") private Call getFromNewAPI(); //Add headers @Headers({"User-Agent: tacobot"}) @GET("contributions") private Call> getContributors();

Slide 61

Slide 61 text

private void getNextTaco() { ... loadTacoDescription(); } private void loadTacoDescription() { Call call = tacoApi.randomTaco(true); call.enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { //Set description from response Taco taco = response.body; //TODO: implement saveTaco(taco); } @Override public void onFailure(Call call, Throwable t) { //Show error } }

Slide 62

Slide 62 text

Save taco recipe for later!

Slide 63

Slide 63 text

Realm Replacement for sqlite

Slide 64

Slide 64 text

→ Works by extending your models → Made for mobile → Most queries in Realm are fast enough to be run synchronously → Can have multiple realm database in an app

Slide 65

Slide 65 text

public class Taco extends RealmObject { private String name; private String imageUrl; private String url; //getters and setters }

Slide 66

Slide 66 text

Set-up Realm Realm.init(this); // Get a Realm instance for this thread Realm realm = Realm.getDefaultInstance();

Slide 67

Slide 67 text

Persist to db // Persist your data in a transaction realm.beginTransaction(); // Persist unmanaged objects final Taco managedTaco = realm.copyToRealm(unmanagedTaco); // Create managed objects directly Taco taco = realm.createObject(Taco.class); realm.commitTransaction();

Slide 68

Slide 68 text

Accessing Data //find all favorite tacos final RealmResults likedTacos = realm.where(Taco.class).equalTo("favorite", true).findAll();

Slide 69

Slide 69 text

Typical SQL relationships → One to One → One to Many → Many to One → Many to Many

Slide 70

Slide 70 text

Conditions → between() → greaterThan(), lessThan() → greaterThanOrEqualTo() & lessThanOrEqualTo() → equalTo() & notEqualTo() → contains() → beginsWith() & endsWith()

Slide 71

Slide 71 text

Writing data

Slide 72

Slide 72 text

//Transaction block realm.executeTransaction(new Realm.Transaction() { @Override public void execute(Realm realm) { Taco taco = realm.createObject(Taco.class); taco.setName("Spaghetti Squash on Fresh Corn Tortillas"); taco.setUrl("http://tacofancy.com/squash"); } });

Slide 73

Slide 73 text

//Async realm.executeTransactionAsync(new Realm.Transaction() { public void execute(Realm bgRealm) { Taco taco = bgRealm.createObject(Taco.class); taco.name("Spaghetti Squash on Fresh Corn Tortillas"); taco.setUrl("http://tacofancy.com/squash"); } }, new Realm.Transaction.OnSuccess() { public void onSuccess() { // Transaction was a success. } }, new Realm.Transaction.OnError() { public void onError(Throwable error) { // Transaction failed and was automatically canceled. } });

Slide 74

Slide 74 text

Fact: Tacos have many ingredients

Slide 75

Slide 75 text

public class Taco extends RealmObject { ... private List ... } public class Ingredient extends RealmObject { private String name; private URL url; }

Slide 76

Slide 76 text

RealmResults limeTacos = realm.where(Taco.class) .equalTo("ingredients.name", "Lime") .findAll();

Slide 77

Slide 77 text

Delete results // All changes to data must happen in a transaction realm.executeTransaction(new Realm.Transaction() { @Override public void execute(Realm realm) { // remove single match limeTacos.deleteFirstFromRealm(); //or limeTacos.deleteLastFromRealm(); // remove a single object Taco fishTaco = limeTacos.get(1); fishTaco.deleteFromRealm(); // Delete all matches limeTacos.deleteAllFromRealm(); } });

Slide 78

Slide 78 text

Data change listeners Can be attached to RealmObject or RealmResults limeTacos.addChangeListener( new RealmChangeListener>() { @Override public void onChange(RealmResults tacosConLimon) { //tacosConLimon.size() == limeTacos.size() // Query results are updated in real time Log.d("LimeTacos", "Now we have" + limeTacos.size() + " tacos"); } } );

Slide 79

Slide 79 text

Tips → @PrimaryKey allows the use of copyToRealmOrUpdate() → Works with Gson and Retrofit easily

Slide 80

Slide 80 text

Prevent memory leaks @Override protected void onDestroy() { // Remove the listener. realm.removeChangeListener(realmListener); //or realm.removeAllChangeListeners(); // Close the Realm instance. realm.close(); ... }

Slide 81

Slide 81 text

Gotchas → No support for list of String or primitives → In the meantime add this Gson adapter → Need to save data to realm after getting response from Retrofit → Large datasets or complex queries should be run on background thread

Slide 82

Slide 82 text

//TODO: implement goToTacoDetailActivity();

Slide 83

Slide 83 text

Dart + Henson Inspired by Butter Knife Inject intent extras as a property on an object

Slide 84

Slide 84 text

Benefits → readable DSL for passing extras to intent → stop wasting time and write less of this: intent.putExtra("TACO_NAME", "Salsa Verde"); tacoName = getIntent().getExtras().getString("TACO_NAME");

Slide 85

Slide 85 text

public class TacoDetailActivity extends Activity { @InjectExtra String name; @Nullable @InjectExtra String imageUrl; //default value if left null @Nullable @InjectExtra String tag = "taco"; //Ingredient implements Parcelable @Nullable @InjectExtra Ingredient ingredient; @Override public void onCreate(Bundle bundle) { super.onCreate(bundle); Dart.inject(this); //TODO use member variables ... } }

Slide 86

Slide 86 text

Generate intent builders //Start intent for TacoDetailActivity Intent intent = Henson.with(this) .gotoTacoDetailActivity() .name(taco.getName()) .url(taco.getUrl()) .imageUrl(taco.getImageUrl()) .build(); // tag is null startActivity(intent);

Slide 87

Slide 87 text

Want to use Henson for an activity without injectable extras? Annotate the activity with @HensonNavigable

Slide 88

Slide 88 text

Is that it?

Slide 89

Slide 89 text

grep 'TODO: implement' => 0 results

Slide 90

Slide 90 text

→ Butter Knife - Manage multiple views → Picasso - Load and display images → Gson - Parse JSON → Retrofit - Fetch data from an API → Realm - Persist data to storage → Dart & Henson - Start new activities with extras

Slide 91

Slide 91 text

Questions? @speaktochris chris-guzman.com