79
Scala on Android Tom Adams @tomjadams

Scala on Android - YOW! Conferences & Workshopsyowconference.com.au/.../Adams-ScalaonAndroid.pdf · Scala on Android Tom Adams @tomjadams. Me • SVP of Polyglot Engineering @ Cogent

  • Upload
    letram

  • View
    216

  • Download
    0

Embed Size (px)

Citation preview

Scala on AndroidTom Adams@tomjadams

Me• SVP of Polyglot Engineering @ Cogent

• Rails, iOS, etc.

• Co-Founder & CTO Oomph

• 4 shipping + 1 prototype Android app

• > 100 bespoke iOS apps

• ~250 iOS apps on Oomph platform

• Enterprise Java, HPC, “big data” background

• Founder of BFPG

What?

• Experiences building a native Android app

• Oomph Viewer, Subaru Symmetry, etc.

• Not looking at Phonegap, Ximian, Titanium, etc.

• Not looking at “non-Scala” problems

• Not teaching you Scala

• Lots of code, the deck will be available

App Goals

• Oomph - digital publishing, think mags on iPads

• A technical proof of concept client for Android

• Build as a platform not a single app

• Develop in parallel with server

• No Java

• Take advantage of Scala language & library support

The Good

Why Scala?• Had done Java before, don’t want to use again

• “Better Java”

• Succinct/less boilerplate - closures, type classes, type aliases, type

inference, no semi-colons, no ‘.’

• Features - immutability, equational reasoning, functions, case

classes, implicits, packages, mixins, currying/partial application, etc.

• Standard & other library support - option, either, future, etc.

• Cool stuff! scalaz, actors, higher-kinds, etc.

• Share concepts/code/DB schema with server

• Monadic code FTW!

Example 1 - Java

context.runOnUiThread(new  Runnable()  {      @Override      public  void  run()  {          dialog.dismiss();          String  standaloneStartUrl  =  "file://"  +                    context.getFilesDir()  +  "/content/index.html";          Intent  next  =  new  Intent(context,  IssueViewerActivity.class);          next.putExtra("start_url",  standaloneStartUrl);          context.startActivity(next);          context.finish();      }  });

Example 1 - Scala

context.runOnUiThread  {      dialog.dismiss()      val  standaloneStartUrl  =  "file://"  +                context.getFilesDir  +  "/content/index.html"      val  next  =  new  Intent(context,  classOf[IssueViewerActivity])      next.putExtra("start_url",  standaloneStartUrl)      context.startActivity(next)      context.finish()  }

Example 2 - Java

Button  button  =  new  Button(context);  button.setText("Greet");  button.setOnClickListener(new  OnClickListener()  {      @Override      public  void  onClick(View  v)  {          Toast.makeText(                  context,  "Hello!",  Toast.LENGTH_SHORT).show();      }  });  layout.addView(button);

Example 2 - Scala

val  button  =  new  Button(context)  button.setText("Greet")  button.setOnClickListener(new  OnClickListener()  {      def  onClick(v:  View)  {          Toast.makeText(                  context,  "Hello!",  Toast.LENGTH_SHORT).show()      }  })  layout.addView(button)

Example 2 - Scala

val  button  =  new  Button(context)  button.setText("Greet")  button.setOnClickListener(Toast.makeText(          context,  "Hello!",  Toast.LENGTH_SHORT).show())  layout.addView(button)

Example 2 - Scala(OID)

SButton("Greet",  toast("Hello!"))

Example 2 - Scala(OID)

SButton("Greet",  toast("Hello!"))

Objections

• “Functions slow down VM”

• “Too many small classes”

• Not a problem in practice (YMMV)

• Can proguard away

• Language issues…

Scala ≈ swift

Toolchain

• sbt

• Android Plugin [1]

• Scalastyle - stylechecker

• IntelliJ w/ Scala plugin

• TeamCity

[1] https://github.com/pfn/android-sdk-plugin

sbT

• “Simple” build tool

• Native Scala build tool

• Not great, but better than alternatives

• Supported by TypeSafe

sbt Plugin

• Most mature at the time

• Requires giter8

• All the things you’d need: emulator & device

support, signing, tests, etc.

IntelliJ

IntelliJ

• Awesome IDE

• Multiple platforms - RubyMine, AppCode, PHP Storm,

etc.

• Familiar

• Decent support for Scala & sbt

Other choices

• Eclipse (?)

• Android Studio (IntelliJ)

• Android SDK Plugin [1]

• Scaloid template [2] (uses [1])

[1] https://github.com/pfn/android-sdk-plugin [2] https://github.com/pocorall/hello-scaloid-sbt

Scaloid

• Take advantage of language features

• Simplifies common patterns

• Alerts, inter-activity comms

• DSL for UI building

Scaloidvar  connectivityListener:  BroadcastReceiver  =  null  !def  onResume()  {      super.onResume()      connectivityListener  =  new  BroadcastReceiver  {          def  onReceive(context:  Context,  intent:  Intent)  {            doSomething()          }      }        registerReceiver(connectivityListener,              new  IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION))  }  !def  onPause()  {      unregisterReceiver(connectivityListener)      super.onPause()  }

Scaloid

broadcastReceiver(ConnectivityManager.CONNECTIVITY_ACTION)  {      (context,  intent)  =>  doSomething()  }

Scaloid

new  AsyncTask[String,  Void,  String]  {      def  doInBackground(params:  Array[String])  =  {          doAJobTakeSomeTime(params)      }  !    override  def  onPostExecute(result:  String)  {          alert("Done!",  result)      }  }.execute("param")

Scaloid

Future  {      val  result  =  doAJobTakeSomeTime(params)      runOnUiThread(alert("Done!",  result))  }

scalaz

• An extension to the core Scala library for

functional programming

• New datatypes (Validation, NonEmptyList, etc.)

• Extensions to standard classes (OptionOps,

ListOps, etc.)

• Implementations of general functions you need

(ad-hoc polymorphism, traits + implicits)

Argonautval  input  =  """      [          {  "name":  "Mark",  "age":  191  },          {  "name":  "Fred",  "age":  33,  "greeting":  "hey  ho,  lets  go!"  },          {  "name":  "Barney",  "age":  35,  "address":  {              "street":  "rock  street",  "number":  10,  "post_code":  2039          }}      ]  """      val  people  =  input.decodeOption[List[Person]].getOrElse(Nil)      val  nice  =  people.map(person  =>          person.copy(greeting  =  person.greeting.orElse(Some("Hello  good  sir!"))))      val  result  =  nice.asJson  println(result.spaces4)      assert(result.array.exists(_.length  ==  3))

Specs2

final  class  HelloWorldSpec  extends  Specification  {      "The  'Hello  world'  string"  should  {          "contain  11  characters"  in  {              "Hello  world"  must  have  size(11)          }          "start  with  'Hello'"  in  {              "Hello  world"  must  startWith("Hello")          }          "end  with  'world'"  in  {              "Hello  world"  must  endWith("world")          }      }  }

Slick

• Type safe DB access library for Scala

• Supported by TypeSafe

• Works well on device or server

• Had used in several projects before (ScalaQuery)

• Added in integration with Android DB lifecycle

• Added migration support

Slick

class  Coffees(tag:  Tag)            extends  Table[(String,  Double)](tag,  "COFFEES")  {      def  name  =  column[String]("COF_NAME",  O.PrimaryKey)      def  price  =  column[Double]("PRICE")      def  *  =  (name,  price)  }      val  coffees  =  TableQuery[Coffees]  coffees.map(_.name)  coffees.filter(_.price  <  10.0)  val  coffeeNames:  Seq[Double]  =  coffees.map(_.price).list  coffees.filter(_.price  <  10.0).sortBy(_.name).map(_.name)

Google

• Access to the infrastructure Google provide

• https://www.buzzingandroid.com/2013/01/

push-messages-on-android-and-iphone/

The App

The App• 3 activities

• Main

• Library Screen

• Content Viewer

Main Activity

final  class  MainActivity  extends  OomphActivity  with  Messaging  {      lazy  val  onInstall  =  new  OnInstall(this)          val  onError:  HttpFailure  =>  Unit  =  {  v  =>  Log.i("Oomph",  v.toString)}          val  initializer  =  config.isStandalone  ?  standalone  |  library          onCreate(OomphDatabase.migrate(this))      onCreate(setContentView(R.layout.main))      onCreate(initializer.configure(this,  config))      onStart(register())          def  register()  {          val  installDetails  =  onInstall.installationDetails()          registerForGCM  {              //  register  -­‐  see  later          }      }  }

List Activity

final  class  LibraryListActivity            extends  OomphActivity  with  OomphDatabase.ActivityDB  {      onCreate({          val  fragmentManager  =  getFragmentManager          val  fragmentTransaction  =  fragmentManager.beginTransaction          fragmentTransaction.add(android.R.id.content,  new  AllIssues,  "")          fragmentTransaction.commit      })          onStart({startService(          new  Intent(this,  classOf[GoAndGetEmTigerService]))})  }

Issue Viewer Activityfinal  class  IssueViewerActivity  extends  OomphActivity  {      lazy  val  oomphUi:  WebView  =  findView(TR.oomphUi)      var  startUrl:  String  =  _      onCreate  {          startUrl  =  getIntent.getExtras.getString("start_url")          setContentView(R.layout.main)          if  (!config.isRelease)  {              List(TR.refresh_button,  TR.select_oomph_issues_list)  foreach  (b  =>                    Option(findView(b)).map(_.setVisibility(View.VISIBLE)))          }          OomphUi.setup(this,  oomphUi,  getWindowManager)          oomphUi.loadUrl(startUrl)      }          def  webViewReload(view:  View)  {          oomphUi.loadUrl(startUrl)      }          def  selectLibraryList(view:  View)  {          startActivity(new  Intent(this,  classOf[LibraryListActivity]))      }  }

JSON Parsing

case  class  Installation(uuid:  String)      object  Installation  {      implicit  val  codec  =  casecodec1(Installation.apply,  Installation.unapply)("uuid")  }      case  class  ApprovedDownload(productId:  String,  url:  String,  md5:  String)      object  ApprovedDownload  {      implicit  val  codec  =  {          val  c  =  casecodec3(ApprovedDownload.apply,  ApprovedDownload.unapply)          c("productId",  "url",  “md5")      }  }

API CALLINGcase  class  OomphApi(client:  OomphClient)  {      def  register(registrationId:  String,  uuid:  String)(f:  ApiResponder[Unit])  {          client.post("register",                    Map("registration_id"  -­‐>  registrationId,  "device_uuid"  -­‐>  uuid),  f)      }          def  issues(f:  ApiResponder[List[RemoteIssue]])  {          implicit  val  decodeI  =  casecodec4(RemoteIssue,  RemoteIssue.unapply)(              "product_id",              "name",              "cover_url",              "content_md5")          client.get("issues",  Map.empty,  f)      }          def  download(productId:  String)(f:  ApiResponder[ApprovedDownload])  {          val  decoder  =  jdecode2L((url:  String,  md5:  String)  =>                  ApprovedDownload(productId,  url,md5))("url",  "md5")          client.post("download",  Map("product_id"  -­‐>  productId),  f)(decoder)      }  }

Fetch & Store Dataobject  GoAndGetEmTiger  {      def  go[A](o:  OomphApi,  database:  ODB,  f:  ()  =>  Unit)  {          o.issues(ApiResponder(_  =>  (),  {  issues  =>              database.runSession(install(issues))              f()          }))      }          def  install(issues:  List[RemoteIssue]):  DB[Unit]  =  for  {          _  <-­‐  Issues.markAllInactive          _  <-­‐  issues.traverse_(recordOrUpdate)      }  yield  ()          def  recordOrUpdate(newIssue:  RemoteIssue):  DB[Unit]  =  DB  {  implicit  s  =>          val  byProductQuery  =  for  {              i  <-­‐  Issues  if  i.productId  ===  newIssue.productId          }  yield  i.nextMd5  ~  i.title  ~  i.imageUrl  ~  i.active              if  (Query(byProductQuery.length).first  >  0)  {              byProductQuery.update((newIssue.md5,  newIssue.title,  newIssue.coverUrl,  true))          }  else  {              Issues.insert(newIssue.asIssue)          }      }  }

Futuresdef  copyContent(context:  Activity):  Unit  =  {      val  dialog  =  ProgressDialog.show(context,  "",  "Installing...",  true)      val  r  =  future  {ContentHelper.assetsToInternalStore(context)}      r  onComplete  {  v  =>          v  match  {              case  TryFailure(e)  =>  e.printStackTrace()              case  _  =>  ()          }          context.runOnUiThread  {              dialog.dismiss()              val  next  =  new  Intent(context,  classOf[IssueViewerActivity])              next.putExtra(“start_url",                      "file://"  +  context.getFilesDir  +  "/content/index.html")              context.startActivity(next)              context.finish()          }      }  }

GCMtrait  Messaging  {  self:  Context  with  Configured  =>      def  registerForGCM(f:  Throwable  \/  String  =>  Unit)  {          val  gcm  =  GoogleCloudMessaging.getInstance(this)          val  r  =  future  {  gcm.register(config.gcmSenderId)  }          r  onSuccess  {  case  v  =>  f(\/-­‐(v))  }          r  onFailure  {  case  e  =>  f(-­‐\/(e))  }      }  }      val  onError:  HttpFailure  =>  Unit  =  {  v  =>  Log.e("Oomph",  v.toString)}      registerForGCM  {      case  -­‐\/(e)  =>  Log.e("Oomph",  "Registration  failed:  %s"  format  e)      case  \/-­‐(v)  =>  {          val  r  =  config.oomphApi.register(v,  installDetails.uuid)          r(ApiResponder(onError))  {  _  =>                    Log.i("Oomph",  "Registration  happened”)          })      }  }

DB Mechanics - Migrations

object  OomphDatabase  extends  AndroidDatabaseAccess  {      val  databaseName  =  "oomph"          val  migrations:  Migrations  =  Migrations(NonEmptyList(          Migration(1,              """CREATE  TABLE  issues  (                        _id  INTEGER  PRIMARY  KEY  AUTOINCREMENT,  product_id  TEXT  NOT  NULL,                          title  TEXT  NOT  NULL,  image_url  TEXT  NOT  NULL,  installed_md5  TEXT,                          next_md5  TEXT,  active  BIT  DEFAULT  0                    );              """          )      ))  }

Monadic DB

case  class  DB[A](f:  Session  =>  A)  {      def  map[B](ff:  A  =>  B):  DB[B]  =  DB(s  =>  ff(f(s)))          def  flatMap[B](ff:  A  =>  DB[B]):  DB[B]  =  DB(s  =>  ff(f(s)).f(s))  }      object  DB  {      implicit  def  instances:  Applicative[DB]  =  new  Applicative[DB]  {          def  point[A](a:  =>  A):  DB[A]  =  DB(_  =>  a)              def  ap[A,  B](fa:  =>  DB[A])(f:  =>  DB[A  =>  B]):  DB[B]  =                  DB(function1Covariant.ap(fa.f)(f.f))      }  }

Android Database Integration

object  Database  {      def  open(databaseName:  String,  context:  Context):  Database  =  {          val  path  =  context.getDatabasePath(databaseName).getCanonicalPath          val  url  =  "jdbc:sqldroid:%s".format(path)          val  database  =  SlickDatabase.forDriver(new  SQLDroidDriver(),  url)              new  Database  {              def  runSession[T](f:  DB[T]):  T  =  database.withSession(f.f)                  def  runTransaction[T](f:  DB[T]):  T  =  database.withTransaction(f.f)          }      }  }

DB Mechanicstrait  AndroidDatabaseAccess  {      val  databaseName:  String      val  migrations:  Migrations          def  migrate(c:  Context)  {  Migrator.migrate(databaseName,  migrations,  c)  }          trait  ActivityDB  extends  Activity  with  DBState  {          abstract  override  def  onCreate(b:  Bundle)  {              super.onCreate(b)              db  =  Database.open(databaseName,  this)          }      }          trait  IntentDB  extends  IntentService  with  DBState  {          //  ...      }          trait  FragmentDB  extends  Fragment  with  DBState  {          //  ...      }  }

Entity DefInition

case  class  RemoteIssue(productId:  String,  title:  String,  coverUrl:  String,  md5:  String)  {      def  asIssue:  Issue  =  Issue(productId,  title,  coverUrl,  None,  md5,  true,  None)  }      case  class  Issue(productId:  String,  title:  String,  imageUrl:  String,          installedMd5:  Option[String],  nextMd5:  String,  active:  Boolean,  id:  Option[Int])      object  Issues  extends  Table[Issue]("ISSUES")  {      def  id  =  column[Int]("_ID",  O.PrimaryKey,  O.AutoInc,  O.NotNull)      def  productId  =  column[String]("PRODUCT_ID",  O.NotNull)      def  title  =  column[String]("TITLE",  O.NotNull)      def  imageUrl  =  column[String]("IMAGE_URL",  O.NotNull)      def  installedMd5  =  column[Option[String]]("INSTALLED_MD5")      def  nextMd5  =  column[String]("NEXT_MD5")      def  active  =  column[Boolean]("ACTIVE",  O.Default(false))      def  baseProjection  =  productId  ~  title  ~  imageUrl  ~  installedMd5  ~  nextMd5  ~  active      def  *  =  baseProjection  ~  id.?  <>(Issue,  Issue.unapply  _)          def  markAllInactive:  DB[Unit]  =  DB  {  implicit  s  =>  this.map(_.active).update(false)  }  }

DB Usage

class  AllIssues  extends  LibraryListFragment  {      def  query:  DB[List[Issue]]  =  Library.allIssues  }      class  InstalledIssues  extends  LibraryListFragment  {      def  query:  DB[List[Issue]]  =  Library.installed  }

DB Usage - Readobject  Library  {      def  allIssues:  DB[List[Issue]]  =  DB  {  implicit  s  =>              val  q2  =  for  {                  i  <-­‐  Issues              }  yield  i              q2.list          }          def  issueForProductId(pId:  String):  DB[Issue]  =  DB  {  implicit  s  =>              val  q2  =  for  {                  i  <-­‐  Issues  if  i.productId  ===  pId              }  yield  i              q2.first()          }          def  installed:  DB[List[Issue]]  =  DB  {  implicit  s  =>              val  q2  =  for  {                  i  <-­‐  Issues  if  i.installedMd5  ===  i.nextMd5              }  yield  i              q2.list          }  }

DB Access - Write

object  Library  {      def  insertIssues(issues:  List[Issue]):  DB[Unit]  =  DB  {  implicit  s  =>          issues.map(i  =>  Issues.insert(i))      }          def  updateIssueNextMd5(issuePid:  String,  nextMd5:  String):  DB[Unit]  =  {          DB  {  implicit  s  =>              val  q  =  for  {i  <-­‐  Issues  if  i.productId  ===  issuePid}  yield  i.nextMd5              q.update(nextMd5)          }      }          def  updateIssueInstalledMd5(issuePid:  String,  installedMd5:  String):  DB[Unit]  =  {          DB  {  implicit  s  =>              val  q  =  for  {i  <-­‐  Issues  if  i.productId  ===  issuePid}  yield  i.installedMd5              q.update(Some(installedMd5))          }      }  }

The Bad

SBT - The Promise

//  build.sbt  name  :=  "hello"  version  :=  "1.0"  scalaVersion  :=  “2.10.3"  !!//  Hi.scala  object  Hi  {      def  main(args:  Array[String])  =  println("Hi!")  }

SBT - The Realityobject  OomphAndroidBuild  extends  Build  {      lazy  val  baseSettings  =  Defaults.defaultSettings  ++  org.scalastyle.sbt.ScalastylePlugin.Settings  ++  Seq(          scalaVersion  :=  "2.10.1",          version  :=  "1.0.4",          versionCode  :=  5,          platformName  in  Android  :=  "android-­‐17",          buildToolsVersion  in  Android  :=  "18.1.1",          scalacOptions  ++=  Seq(...),          commands  ++=  Seq(Command.command("ci",  Help.empty)(s  =>  Seq("...")  :::  s))      )  ++  addCommandAlias("compile-­‐and-­‐check",  ";app/compile;app/scalastyle")          val  signingSettings  =  Seq(          keyalias  in  Android  :=  "oomph",          cachePasswords  in  Android  :=  true,          keystorePath  in  Android  <<=  baseDirectory(_  /  "etc"  /  "keystore")      )          val  proguardSettings  =  ...          lazy  val  appSettings  =  baseSettings  ++  AndroidProject.androidSettings  ++  proguardSettings  ++              TypedResources.settings  ++  AndroidManifestGenerator.settings  ++  AndroidMarketPublish.settings  ++              AndroidManifestGenerator.settings  ++  signingSettings  ++              Seq(                  name  :=  "Oomph  Android",                  installEmulator  in  Android  ~=  {  _  =>                      Seq("adb",  "-­‐e",  "shell",  "touch  /data/data/com.oomphhq.android/i_am_an_emulator")  !                  }              )          lazy  val  testsSettings  =  baseSettings  ++  AndroidTest.androidSettings  ++  proguardSettings  ++  AndroidManifestGenerator.settings  ++              Seq(                  name  :=  "Tests",                  proguardInJars  in  Android  :=  Seq(),  //  wot!                  libraryDependencies  ++=  Seq(...)              )          lazy  val  app  =  Project("app",  file("."),  settings  =  appSettings)          lazy  val  tests  =  Project("tests",  file("tests"),  settings  =  testsSettings)  dependsOn  (app  %  "provided")  }

Little BUILD CHAIN Hacks

• sbt plugin to fix proguard

• Library jars not being included in binary!

• Custom JRE (script) in IntelliJ to fix memory issues (fixed

now)

• Double up of libraries “provided” scope [1] & [2]

• Build tools in v19 has dex bug

[1] https://github.com/jberkel/android-plugin/pull/177

[2] https://groups.google.com/forum/?fromgroups=#!topic/scala-on-android/OuGYJtvQdZo

No Migration Support

• Migration support not built in

• Some Scala support (Java?)

• You will probably need to roll your own

• Moderately difficult to integrate Slick with

Android DB with migrations

ToolChain

• The toolchain described above isn’t (wasn’t?)

well supported

• Many moving parts; IDE, Android, compiler,

plugin

• Not as simple as iOS, normal for JVM people

• However, plenty of support for Java…

Learning

• Scala has a moderately high learning curve

• Quite a complicated language, syntactically &

conceptually

• Lots of ways to do the same thing, e.g. _

• 3 kinds of Scala; Java, “idiomatic”, FP

FP

• Functional patterns can make learning Scala

harder

• Can be hard to fit FP concepts Android;

immutability, (lack of) subclass inheritance

Compiler

• Compiler bugs, much better now

• Compiler speed, though many work arounds &

sbt’s incremental compiler quite decent

The Ugly

Still in Kansas

• You may have a nicer language, but it’s still

Android

• Stub classes when testing

• Same device compatibility issues

• Lack of standardisation, storage paths

• Binary size restrictions

Implicits

• Deceptively simple

• People seem to suffer horrendously with them

• Incredibly hard to debug

• Tip: Use them sparingly, apply some rules

• Bijection

• Import them close to the call site, keep scope small

JDK8

[info]  Compiling  1  Scala  source  to  /Users/tom/Projects/Oomph/oomph-­‐android/project/project/target/scala-­‐2.9.2/sbt-­‐0.12/classes...  [error]  error  while  loading  CharSequence,  class  file  '/Library/Java/JavaVirtualMachines/jdk1.8.0_05.jdk/Contents/Home/jre/lib/rt.jar(java/lang/CharSequence.class)'  is  broken  [error]  (bad  constant  pool  tag  18  at  byte  10)  [error]  error  while  loading  Comparator,  class  file  '/Library/Java/JavaVirtualMachines/jdk1.8.0_05.jdk/Contents/Home/jre/lib/rt.jar(java/util/Comparator.class)'  is  broken  [error]  (bad  constant  pool  tag  18  at  byte  20)  [error]  two  errors  found  [error]  (compile:compile)  Compilation  failed

JDK8

$  export  JAVA_HOME=/Library/Java/Home  $  sbt

Proguard is you’re friend (?)

• Heavily optimise proguard to not hit (class) limits

• Hard to know if you’ve removed too much

• Don’t know if binary runs, classes missing!

• Hard to exercise all code paths

• Automated tests, QA team

• Static analysis?

Proguardval  proguardSettings  =  Seq(      useProguard  in  Android  :=  true,      proguardOption  in  Android  :=              """                  |-­‐keep  public  class  scala.Function1                  |-­‐keep  public  class  scala.reflect.ScalaSignature                  |-­‐keep  class  *  extends  java.util.ListResourceBundle  {                  |      protected  Object[][]  getContents();                  |}                  |-­‐keepclassmembers  enum  *  {                  |      public  static  **[]  values();                  |      public  static  **  valueOf(java.lang.String);                  |}                  |-­‐keepclassmembers  class  scala.collection.**  {                  |      *;                  |}                  |-­‐keep  public  class  scala.math.Ordering                  |-­‐keepclassmembers  class  com.oomphhq.whiplash.**  {                  |      *;                  |}                  |-­‐keep  public  class  scala.slick.lifted.DDL  {  *;  }              """.stripMargin  )

Webviews

• Well…

• Webviews are webviews

• Scala can’t help us here

• Still bad support across devices

Tips

Learn FP

• Use Scala as a vehicle to learn FP

• Can share concepts; iOS (Swift), Android (Scala), Web (Elm,

TypeScript, PureScript, Roy)

• Gradual learning curve

• Start with Scala syntax

• Use current programming style

• Gradually introduce FP concepts

• Read lots; FP in Scala, LYAH, “reactive”, etc.

Invest in toolchain

• The toolchain is ordinary, but

• The toolchain is critical

• Invest in it

• Become build experts

(F)RP• (Functional) reactive programming is all the rage

• Promises easy composability

• https://github.com/ReactiveX/RxJava, & on Android [1, 3]

• Good introduction to FRP [2]

[1] http://mttkay.github.io/blog/2013/08/25/functional-reactive-programming-on-android-with-rxjava/

[2] https://gist.github.com/staltz/868e7e9bc2a7b8c1f754

[3] https://github.com/andrewhr/rxjava-android-example

//  send  button  enabled  when  we  have  a  message  messageBodyText.map  {!_.trim().equals("")  }.          subscribe(Properties.enabledFrom(message));

Roboelectric

• Run tests locally rather than on device/emulator

• Implementations of Android SDK

• No mocking

• Java-based API

• JUnit runner

https://github.com/robolectric/robolectric

Robospecs

• Roboelectric for Specs2

https://github.com/jbrechtel/robospecs

class  MainActivitySpecs  extends  RoboSpecs  with  Mockito  {      "clicking  the  showMessageButton"  should  {          "show  a  toast  popup  with  text  from  the  message  input  field"  in  {              val  activity  =  new  MainActivity()              activity.onCreate(null)              activity.messageEditText.setText("expected  message")              activity.showMessageButton.performClick()              ShadowToast.getTextOfLatestToast  must  beEqualTo(“...”)          }      }  }

Misc…

• Can pre-load Scala onto rooted devices [1]

• Android SBT plugin is built into IntelliJ 14 [2, 3]

[1] https://github.com/jbrechtel/Android-Scala-Installer [2] https://github.com/JetBrains/sbt-structure/pull/5

[3] http://youtrack.jetbrains.com/issue/SCL-6273

Fork

• Paul Phillips https://github.com/paulp/policy/blob/

master/README.md

• Comments https://news.ycombinator.com/item?

id=8276565

• TypeLevel http://typelevel.org/blog/2014/09/02/

typelevel-scala.html

• Watch Paul’s talk: https://www.youtube.com/watch?

v=TS1lpKBMkgg

References

• Scala my Android: http://ktoso.github.io/scala-

android-presentation/

Tom Adams@tomjadams