Groovy on Android -- Groovy Grails eXchange 2014

Groovy on Android -- Groovy Grails eXchange 2014

For 10 years, Groovy has dramatically improved the productivity of Java developers on the desktop. With unique like closures, builders, AST transformations, traits, optional static compilation and many more, Groovy turned out to be a very competitive language on the JVM. Compared to other JVM languages, Groovy has the major advantage of being totally Java-friendly, both in terms of syntax and interpretability. But during those years, what happened on the mobile world? In particular, Android developers are used to develop applications in Java, so why Groovy, a JVM language, wouldn't be usable for Android development too? Can we ease the pain of Android developers too?

137d3908243acfc30e126615d59d4e6d?s=128

Guillaume Laforge

December 12, 2014
Tweet

Transcript

  1. 2.
  2. 4.
  3. 11.

    New York Times — Getting Groovy with Android 5 New

    York Times recruits a Groovy / Android expert http://bit.ly/nyt-job
  4. 12.

    What does NYT likes about Groovy on Android? • No

    Java 8, no lambda on Android… 6 Func0 func = new Func0<string>() { @Override public String call() { return "my content"; } }; Async.start(func);
  5. 13.

    What does NYT likes about Groovy on Android? • No

    Java 8, no lambda on Android… 7 Async.start { "my content" }
  6. 14.

    What does NYT likes about Groovy on Android? • No

    Java 8, no lambda on Android… 7 Async.start { "my content" } Good bye annonymous inner classes!
  7. 15.

    What does NYT likes about Groovy on Android? • Groovy

    code more concise and more readable • but just as type-safe as needed!
 (with @TypeChecked) • but just as fast as needed!
 (with @CompileStatic) 8
  8. 17.

    @glaforge — Groovy on Android Brief overview of Groovy You

    probably know it through Gradle already!
  9. 18.

    Groovy — a multi-faceted language Open-source project (Apache 2 licensed)

    • http://groovy.codehaus.org/ • http://beta.groovy-lang.org/ 10
  10. 19.

    Groovy — a multi-faceted language Open-source project (Apache 2 licensed)

    • http://groovy.codehaus.org/ • http://beta.groovy-lang.org/ 10 New « shiny » website coming soon!
  11. 21.
  12. 24.
  13. 30.
  14. 47.

    Modify your app’s Gradle build 23 apply plugin: 'com.android.application' android

    { compileSdkVersion 20 buildToolsVersion "20.0.0" defaultConfig { applicationId "com.appspot.glaforge.hellogroovyworld" minSdkVersion 15 targetSdkVersion 20 versionCode 1 versionName "1.0" } buildTypes { release { runProguard false proguardFiles getDefaultProguardFile('proguard-android.txt'), 
 'proguard-rules.pro' } } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) }
  15. 48.

    Modify your app’s Gradle build 23 apply plugin: 'com.android.application' android

    { compileSdkVersion 20 buildToolsVersion "20.0.0" defaultConfig { applicationId "com.appspot.glaforge.hellogroovyworld" minSdkVersion 15 targetSdkVersion 20 versionCode 1 versionName "1.0" } buildTypes { release { runProguard false proguardFiles getDefaultProguardFile('proguard-android.txt'), 
 'proguard-rules.pro' } } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) } buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:0.12.2' classpath 'me.champeau.gradle:gradle-groovy-android-plugin:0.3.0' } }
  16. 49.

    Modify your app’s Gradle build 23 apply plugin: 'com.android.application' android

    { compileSdkVersion 20 buildToolsVersion "20.0.0" defaultConfig { applicationId "com.appspot.glaforge.hellogroovyworld" minSdkVersion 15 targetSdkVersion 20 versionCode 1 versionName "1.0" } buildTypes { release { runProguard false proguardFiles getDefaultProguardFile('proguard-android.txt'), 
 'proguard-rules.pro' } } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) } apply plugin: 'me.champeau.gradle.groovy-android' buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:0.12.2' classpath 'me.champeau.gradle:gradle-groovy-android-plugin:0.3.0' } }
  17. 50.

    Modify your app’s Gradle build 23 apply plugin: 'com.android.application' android

    { compileSdkVersion 20 buildToolsVersion "20.0.0" defaultConfig { applicationId "com.appspot.glaforge.hellogroovyworld" minSdkVersion 15 targetSdkVersion 20 versionCode 1 versionName "1.0" } buildTypes { release { runProguard false proguardFiles getDefaultProguardFile('proguard-android.txt'), 
 'proguard-rules.pro' } } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) } compile 'org.codehaus.groovy:groovy:2.4.0-beta-3:grooid' apply plugin: 'me.champeau.gradle.groovy-android' buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:0.12.2' classpath 'me.champeau.gradle:gradle-groovy-android-plugin:0.3.0' } }
  18. 51.

    Java to Groovy… • Rename your activity from .java to

    .groovy • Remove… • public and return keywords • some parentheses • use the property notation • getMenuInflater() becomes menuInflater • use interpolated strings • and more. 24
  19. 52.

    Java to Groovy… 25 import android.app.Activity import android.os.Bundle import android.view.*

    class HelloGroovyWorld extends Activity { protected void onCreate(Bundle savedInstanceState) { super.onCreate savedInstanceState setContentView R.layout.activity_hello_groovy_world } boolean onCreateOptionsMenu(Menu menu) { menuInflater.inflate R.menu.hello_groovy_world, menu true } boolean onOptionsItemSelected(MenuItem item) { int id = item.itemId if (id == R.id.action_settings) { return true } super.onOptionsItemSelected item }
  20. 54.

    What about weight? On a demo application (conference agenda) 27

    Artifact Size Groovy JAR 4.5 MB Generated APK 2 MB After ProGuard 1 MB (8K methods)
  21. 55.

    ProGuard rules 28 -dontobfuscate -keep class org.codehaus.groovy.vmplugin.** -keep class org.codehaus.groovy.runtime.dgm*

    -keepclassmembers class org.codehaus.groovy.runtime.dgm* { *; } -keepclassmembers class ** implements org.codehaus.groovy.runtime.GeneratedClosure { *; } -dontwarn org.codehaus.groovy.** -dontwarn groovy**
  22. 68.

    First attempt — DiscoBot 31 Compile-time Runtime .class .jar .dex

    .apk « Ruboto » approach, slow & inneficient
  23. 86.

    From Java to Groovy… 36 public class Greeter { private

    String owner; public String getOwner() { return owner; } public void setOwner(String owner) { this.owner = owner; } public String greet(String name) { return "Hello " + name + ", I am " + owner; } } Greeter greeter = new Greeter(); greeter.setOwner("Guillaume"); System.out.println(greeter.greet("Marion"));
  24. 87.

    From Java to Groovy… 36 public class Greeter { private

    String owner; public String getOwner() { return owner; } public void setOwner(String owner) { this.owner = owner; } public String greet(String name) { return "Hello " + name + ", I am " + owner; } } Greeter greeter = new Greeter(); greeter.setOwner("Guillaume"); System.out.println(greeter.greet("Marion")); class Greeter { String owner String greet(String name) { "Hello ${name}, I am ${owner}" } } def greeter = new Greeter(owner: "Guillaume") println greeter.greet("Marion")
  25. 89.

    Special Groovy syntax sugar — special operators 37 // Groovy

    truth // if (s != null && s.length() > 0) {...} if (s) { ... } // Elvis def name = person.name ?: "unknown" // save navigation order?.lineItem?.item?.name
  26. 90.

    Special Groovy syntax sugar — special operators 37 // Groovy

    truth // if (s != null && s.length() > 0) {...} if (s) { ... } // Elvis def name = person.name ?: "unknown" // save navigation order?.lineItem?.item?.name if (person.name != null && person.name.length() > 0)
  27. 91.

    Special Groovy syntax sugar — special operators 37 // Groovy

    truth // if (s != null && s.length() > 0) {...} if (s) { ... } // Elvis def name = person.name ?: "unknown" // save navigation order?.lineItem?.item?.name Copied by Swift, C# 
 and CoffeeScript!
  28. 92.

    Special Groovy syntax sugar — special operators 37 // Groovy

    truth // if (s != null && s.length() > 0) {...} if (s) { ... } // Elvis def name = person.name ?: "unknown" // save navigation order?.lineItem?.item?.name Anything null in the chain? Null
  29. 93.

    Special Groovy syntax sugar — special operators 37 // Groovy

    truth // if (s != null && s.length() > 0) {...} if (s) { ... } // Elvis def name = person.name ?: "unknown" // save navigation order?.lineItem?.item?.name Better NPEs: Cannot get property name on null object
  30. 94.

    Special Groovy syntax sugar — collections, regex, closures 38 //

    lists def list = [1, 2, 3, 4, 5] // maps def map = [a: 1, b: 2, c: 3] // regular expressions def regex = ~/.*foo.*/ // ranges def range 128..255 // closures def adder = { a, b -> a + b }
  31. 95.
  32. 97.

    Groovy builders — XML, JSON… Android views & layouts! 40

    import groovy.json.* def json = new JsonBuilder() json.person { name 'Guillaume' age 37 daughters 'Marion', 'Erine' address { street '1 Main St' zip 75001 city 'Paris' } }
  33. 98.

    Groovy builders — XML, JSON… Android views & layouts! 40

    import groovy.json.* def json = new JsonBuilder() json.person { name 'Guillaume' age 37 daughters 'Marion', 'Erine' address { street '1 Main St' zip 75001 city 'Paris' } } { "person": { "name": "Guillaume", "age": 37, "daughters": [ "Marion", "Erine" ], "address": { "street": "1 Main St", "zip": 75001, "city": "Paris" } } }
  34. 99.

    HTTP GET and JSON parsing in 4 lines 41 import

    groovy.json.* def url = "https://api.github.com/repos" +
 "groovy/groovy-core/commits" def commits = new JsonSlurper().parseText(url.toURL().text) assert commits[0].commit.author.name == 'Cedric Champeau'
  35. 100.

    HTTP GET and JSON parsing in 4 lines 41 import

    groovy.json.* def url = "https://api.github.com/repos" +
 "groovy/groovy-core/commits" def commits = new JsonSlurper().parseText(url.toURL().text) assert commits[0].commit.author.name == 'Cedric Champeau' An HTTP GET in a one-liner!
  36. 101.

    HTTP GET and JSON parsing in 4 lines 41 import

    groovy.json.* def url = "https://api.github.com/repos" +
 "groovy/groovy-core/commits" def commits = new JsonSlurper().parseText(url.toURL().text) assert commits[0].commit.author.name == 'Cedric Champeau' No complex object graph marshalling!
  37. 105.

    AST transformations — only a short list! Code generation •

    @ToString • @EqualsAndHashCode • @Canonical • @TupleConstructor • @InheritConstructors • @Category • @Lazy • @Sortable • @Builder Class design • @Delegate • @Immutable • @Memoized • @Singleton Compiler directives • @AnnotationCollector • @DelegatesTo • @TypeChecked • @CompileStatic • @TailRecursive 43
  38. 106.

    AST transformations — @Immutable Implement immutability by the book •

    final class • tuple-style constructor • private final backing fields • defensive copying of collections • equals() and hashCode() methods • toString() method • ... 44
  39. 107.

    AST transformations — @Immutable • A person class • a

    String name • an int age 45 public final class Person { private final String name; private final int age; public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } public int hashCode() { return age + 31 * name.hashCode(); } public boolean equals(Object other) { if (other == null) { return false; } if (this == other) { return true; } if (Person.class != other.getClass()) { return false; } Person otherPerson = (Person)other; if (!name.equals(otherPerson.getName()) { return false; } if (age != otherPerson.getAge()) { return false; } return true; } public String toString() { return "Person(" + name + ", " + age + ")"; } }
  40. 108.

    AST transformations — @Immutable • A person class • a

    String name • an int age 46 import groovy.transform.* @Immutable class Person { String name int age }
  41. 114.

    Traits • Like interfaces, but with method bodies – similar

    to Java 8 interface default methods • Elegant way to compose behavior – multiple inheritance without the « diamond » problem • Traits can also be stateful – traits can have properties like normal classes • Compatible with static typing and static compilation – class methods from traits also visible from Java classes • Also possible to implement traits at runtime 48
  42. 115.

    Traits — example: melix / speakertime 49 @CompileStatic class PresentationActivity

    extends Activity implements GoogleApiProvider { // ... protected void onCreate(Bundle savedState) { super.onCreate(savedState) // ... createGoogleApi() } protected void onStart() { super.onStart() connectGoogleApi() } protected void onStop() { super.onStop() disconnectGoogleApi() } // ... @CompileStatic trait GoogleApiProvider { GoogleApiClient googleApiClient void createGoogleApi() { /* ... */ } void connectGoogleApi() { /* ... */ } void disconnectGoogleApi() { /* ... */ } }
  43. 119.

    Event listeners 51 button.onClickListener = { Log.i(tag, "clicked!") } button.setOnClickListener(new

    OnClickListener() { @Override public void onClick(View view) { Log.i(tag, "clicked!") } })
  44. 121.
  45. 123.

    Groovy Development Kit methods 53 new Thread() { public void

    run() { // … } }.start(); Thread.start { // … }
  46. 124.

    Groovy’s with {} method 54 view = new TextView(context); view.setText(name);

    view.setTextSize(16f); view.setTextColor(Color.WHITE);
  47. 125.

    Groovy’s with {} method 54 view = new TextView(context); view.setText(name);

    view.setTextSize(16f); view.setTextColor(Color.WHITE); view = new TextView(context) view.with { text = name textSize = 16f textColor = Color.WHITE }
  48. 126.

    Groovy Truth and GStrings 55 EditText phone = (EditText)findViewById(R.id.phone); if

    ((phone.getText() != null) && !phone.getText().equals("")) { String phoneString = parsePhone(phone.getText().toString()); Intent intent = new Intent(Intent.ACTION_CALL, Uri.parse("tel:" + phoneString)); startActivity(intent); }
  49. 127.

    Groovy Truth and GStrings 55 EditText phone = (EditText)findViewById(R.id.phone); if

    ((phone.getText() != null) && !phone.getText().equals("")) { String phoneString = parsePhone(phone.getText().toString()); Intent intent = new Intent(Intent.ACTION_CALL, Uri.parse("tel:" + phoneString)); startActivity(intent); } EditText phone = findViewById(R.id.phone) if (phone.text) { def phoneString = parsePhone(phone.text) def intent = new Intent(Intent.ACTION_CALL, Uri.parse("tel:${phoneString}")) startActivity intent }
  50. 128.

    Resource handling with closures and file methods 56 File f

    = new File("/sdcard/dir/f.txt"); if (f.exists() && f.canRead()) { FileInputStream fis = null; try { fis = new FileInputStream(rFile); byte[] bytes = new byte[fis.available()]; while (fis.read(bytes) != -1) {} textView.setText(new String(bytes)); } catch (IOException e) { // log and or handle } finally { if (fis != null) { try { fis.close(); } catch (IOException e) { // swallow } } } }
  51. 129.

    Resource handling with closures and file methods 56 File f

    = new File("/sdcard/dir/f.txt"); if (f.exists() && f.canRead()) { FileInputStream fis = null; try { fis = new FileInputStream(rFile); byte[] bytes = new byte[fis.available()]; while (fis.read(bytes) != -1) {} textView.setText(new String(bytes)); } catch (IOException e) { // log and or handle } finally { if (fis != null) { try { fis.close(); } catch (IOException e) { // swallow } } } } def f = new File("/sdcard/dir/f.txt") if (f.exists() && f.canRead()) { f.withInputStream { fis -> def bytes = new byte[fis.available()] while (fis.read(bytes) != -1) {} textView.text = new String(bytes) } }
  52. 130.

    Resource handling with closures and file methods 56 File f

    = new File("/sdcard/dir/f.txt"); if (f.exists() && f.canRead()) { FileInputStream fis = null; try { fis = new FileInputStream(rFile); byte[] bytes = new byte[fis.available()]; while (fis.read(bytes) != -1) {} textView.setText(new String(bytes)); } catch (IOException e) { // log and or handle } finally { if (fis != null) { try { fis.close(); } catch (IOException e) { // swallow } } } } def f = new File("/sdcard/dir/f.txt") if (f.exists() && f.canRead()) { f.withInputStream { fis -> def bytes = new byte[fis.available()] while (fis.read(bytes) != -1) {} textView.text = new String(bytes) } } def f = new File("/sdcard/dir/f.txt") if (f.exists() && f.canRead()) { textView.text = f.text }
  53. 132.

    What else? • Projects with dedicated Groovy support • Swiss

    Knife • similar to Butter Knife & 
 Android Annotations • Grooid Playground • a builder for creating views
 in a hierarchical & visual manner 58
  54. 134.

    Swiss Knife — Arasthel / SwissKnife 59 Like Butter Knife

    & Android Annotations, but for Groovy
  55. 135.

    Swiss Knife 60 class MyActivity extends Activity { @OnClick(R.id.button) void

    onButtonClicked(Button button) { Toast.makeText(this, "Clicked", Toast.LENGTH_SHOT).show() } @OnBackground void doSomeProcessing(URL url) { // Contents will be executed on background } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate savedInstanceState setContentView R.layout.activity_main SwissKnife.inject this } }
  56. 136.

    Swiss Knife 60 class MyActivity extends Activity { @OnClick(R.id.button) void

    onButtonClicked(Button button) { Toast.makeText(this, "Clicked", Toast.LENGTH_SHOT).show() } @OnBackground void doSomeProcessing(URL url) { // Contents will be executed on background } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate savedInstanceState setContentView R.layout.activity_main SwissKnife.inject this } } View injection
  57. 137.

    Swiss Knife 60 class MyActivity extends Activity { @OnClick(R.id.button) void

    onButtonClicked(Button button) { Toast.makeText(this, "Clicked", Toast.LENGTH_SHOT).show() } @OnBackground void doSomeProcessing(URL url) { // Contents will be executed on background } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate savedInstanceState setContentView R.layout.activity_main SwissKnife.inject this } } Execute code outside the UI thread
  58. 138.

    Swiss Knife 60 class MyActivity extends Activity { @OnClick(R.id.button) void

    onButtonClicked(Button button) { Toast.makeText(this, "Clicked", Toast.LENGTH_SHOT).show() } @OnBackground void doSomeProcessing(URL url) { // Contents will be executed on background } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate savedInstanceState setContentView R.layout.activity_main SwissKnife.inject this } } Injection point of views and callbacks
  59. 139.

    Swiss Knife — available annotations • @OnUIThread • @OnBackground •

    @OnClick • @OnLongClick • @OnItemClick • @OnItemLongClick • @OnItemSelected • @OnChecked • @OnFocusChanged • @OnTouch • @OnPageChanged • @OnTextChanged • @OnEditorAction 61
  60. 140.

    Swiss Knife — available annotations • @OnUIThread • @OnBackground •

    @OnClick • @OnLongClick • @OnItemClick • @OnItemLongClick • @OnItemSelected • @OnChecked • @OnFocusChanged • @OnTouch • @OnPageChanged • @OnTextChanged • @OnEditorAction 61 AST transformations
  61. 142.

    Grooid Playground — karfunkel / grooid-playground • When you need

    to build dynamic views (ie. not XML-driven) 63 def layout = new RelativeLayout(this) layout.width = MATCH_PARENT layout.height = MATCH_PARENT layout.setPaddingRelative(64, 16, 64, 16) def textView = new TextView(this) textView.width = MATCH_PARENT textView.height = 20 textView.setText R.string.hello_world layout.addView textView setContentView(layout)
  62. 143.

    Grooid Playground — karfunkel / grooid-playground • When you need

    to build dynamic views (ie. not XML-driven) 63 def layout = new RelativeLayout(this) layout.width = MATCH_PARENT layout.height = MATCH_PARENT layout.setPaddingRelative(64, 16, 64, 16) def textView = new TextView(this) textView.width = MATCH_PARENT textView.height = 20 textView.setText R.string.hello_world layout.addView textView setContentView(layout) Imperative
  63. 144.

    Grooid Playground — karfunkel / grooid-playground • When you need

    to build dynamic views (ie. not XML-driven) 63 setContentView new AndroidBuilder().build(this) { relativeLayout(width: MATCH_PARENT, height: MATCH_PARENT, padding: [64.dip, 16.dip]) { textView(width: MATCH_PARENT, height: 20.dip, text: R.string.hello_world) } } Declarative, hierarchical, visual
  64. 146.

    Programming your Android applications in Groovy • You can use

    Groovy to code Android apps! • use Groovy 2.4.0-beta-1+ (latest: beta-4) • prefer @CompileStatic • Two great posts to get started: • http://bit.ly/grooid-1 • http://bit.ly/grooid-2 • Gradle plugin support: • http://bit.ly/grooid-gradle 65
  65. 152.

    Summary • Groovy can make your Android code… • more

    concise & readable • more maintainable • Without compromising • type safety • speed 69
  66. 156.
  67. 157.

    Image credits • Stone age • http://3.bp.blogspot.com/-lMDyyBdibG4/UxDYLq5ItiI/AAAAAAAALf8/HPpKRixHOIE/s1600/Neanderthal+Man+1.jpg • Many thanks

    • http://www.trys.ie/wp-content/uploads/2013/06/many-thanks.jpg • Disclaimer • http://3.bp.blogspot.com/-RGnBpjXTCQA/Tj2h_JsLigI/AAAAAAAABbg/AB5ZZYzuE5w/s1600/disclaimer.jpg • Builders • http://detroittraining.com/wp-content/uploads/Construction-Women2.jpg • Newbie • http://contently.com/strategist/wp-content/uploads/2014/05/bigstock-Shocked-Computer-Nerd-1520709.jpg • Alternative rock band • http://upload.wikimedia.org/wikipedia/commons/b/b2/Arcade_Fire_live_20050315.ext.jpg • Twins • http://images.doctissimo.fr/1/jumeaux-jumelles/photo/hd/4424027442/137540656da/jumeaux-jumelles-jumeaux-jumelles-2-big.jpg • Annotations • http://3.bp.blogspot.com/-f94n9BHko_s/T35ELs7nYvI/AAAAAAAAAKg/qe06LRPH9U4/s1600/IMG_1721.JPG • Apple & Orange • http://cdni.wired.co.uk/1920x1280/a_c/comparison.jpg • We need you • http://media.moddb.com/images/mods/1/14/13849/WickedSunshine_UncleSam_Blank_800x1000.png 73
  68. 158.

    Image credits • Macarons • http://www.thatfoodcray.com/wp-content/uploads/2013/12/that-food-cray-pierre-herme-paris-hong-kong-christmas-flavors-foie-gras- truffle-9.jpg?4dd047 • Bridge •

    http://tenspeedhero.com/wp-content/uploads/Bridge_3.jpg • Swiss knife • http://www.formengifts.com/wp-content/uploads/2011/04/swiss-army-champ-multitool-knife2.jpg • Hello World • http://www.warrenpriestley.com/wp-content/uploads/2012/01/helloworld.gif • George Clooney • http://www.brewville.ca/wp-content/uploads/2014/05/nespresso-george-clooney.jpg • Weight scale • http://images.wisegeek.com/person-stands-on-scale.jpg • Nexus 5 • http://fs01.androidpit.info/userfiles/2692059/image/Blog/nexus-5-weiss_628.jpg • File icon • https://dl.vecnet.org/assets/default.png • Gear • http://www.pvzgears.com/wp-content/uploads/2013/03/gears2a.png • Sugar • http://images.wisegeek.com/cane-sugar.jpg • London • http://www.100percentoptical.com/images/2014/10/london.jpg 74