Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Groovy on Android -- DroidCon Paris 2014

Groovy on Android -- DroidCon Paris 2014

Groovy 2.4+ is providing support for developing Android applications in Groovy. In this presentation, we see how Groovy can help streamline the development of Android apps with the Groovy language.

Guillaume Laforge

September 22, 2014
Tweet

More Decks by Guillaume Laforge

Other Decks in Programming

Transcript

  1. Groovy on Android Guillaume Laforge Groovy project lead (at Pivotal)

    @glaforge I’ll put the slides online on speakerdeck.com/glaforge
  2. New York Times — Getting Groovy with Android 5 New

    York Times recruits a Groovy / Android expert http://bit.ly/nyt-job
  3. 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);
  4. What does NYT likes about Groovy on Android? • No

    Java 8, no lambda on Android… 7 ! ! ! ! ! ! Async.start { "my content" }
  5. 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!
  6. 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
  7. @glaforge — Groovy on Android Brief overview of Groovy You

    probably know it through Gradle already!
  8. Groovy — a multi-faceted language Open-source project (Apache 2 licensed)

    ! • http://groovy.codehaus.org/ • http://beta.groovy-lang.org/ 10
  9. 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!
  10. 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']) }
  11. 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' } }
  12. 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' } }
  13. 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' } }
  14. 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
  15. 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 }
  16. 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)
  17. 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**
  18. First attempt — DiscoBot 31 Compile-time Runtime .class .jar .dex

    .apk « Ruboto » approach, slow & inneficient
  19. 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"));
  20. 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")
  21. 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
  22. 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)
  23. 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!
  24. 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
  25. 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
  26. 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 }
  27. 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' } }
  28. 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" } } }
  29. 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'
  30. 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!
  31. 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!
  32. 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
  33. 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
  34. 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 + ")"; } }
  35. AST transformations — @Immutable • A person class • a

    String name • an int age 46 import groovy.transform.* ! @Immutable class Person { String name int age }
  36. 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
  37. 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() { /* ... */ } }
  38. Event listeners 51 button.onClickListener = { Log.i(tag, "clicked!") } button.setOnClickListener(new

    OnClickListener() { @Override public void onClick(View view) { Log.i(tag, "clicked!") } })
  39. Groovy Development Kit methods 53 new Thread() { public void

    run() { // … } }.start(); Thread.start { // … }
  40. Groovy’s with {} method 54 view = new TextView(context); view.setText(name);

    view.setTextSize(16f); view.setTextColor(Color.WHITE);
  41. 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 }
  42. 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); }
  43. 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 }
  44. 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 } } } }
  45. 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) } }
  46. 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 }
  47. 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
  48. Swiss Knife — Arasthel / SwissKnife 59 Like Butter Knife

    & Android Annotations, but for Groovy
  49. 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 } }
  50. 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
  51. 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
  52. 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
  53. Swiss Knife — available annotations ! • @OnUIThread • @OnBackground

    ! • @OnClick • @OnLongClick • @OnItemClick • @OnItemLongClick ! ! ! ! • @OnItemSelected • @OnChecked • @OnFocusChanged • @OnTouch • @OnPageChanged • @OnTextChanged • @OnEditorAction 61
  54. Swiss Knife — available annotations ! • @OnUIThread • @OnBackground

    ! • @OnClick • @OnLongClick • @OnItemClick • @OnItemLongClick ! ! ! ! • @OnItemSelected • @OnChecked • @OnFocusChanged • @OnTouch • @OnPageChanged • @OnTextChanged • @OnEditorAction 61 AST transformations
  55. 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)
  56. 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
  57. 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
  58. Programming your Android applications in Groovy • You can use

    Groovy to code Android apps! • use Groovy 2.4.0-beta-1+ (latest: beta-3) • 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
  59. Summary ! • Groovy can make your Android code… •

    more concise & readable • more maintainable ! • Without compromising • type safety • speed 69
  60. 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
  61. 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 74